Skip to content

Commit

Permalink
fix authentication and better api's reply error handling
Browse files Browse the repository at this point in the history
close #257 (including bonus)
close #261
close #259
close #224
  • Loading branch information
SimonSAMPERE committed Oct 3, 2019
1 parent 37dcecb commit dd8302d
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 114 deletions.
1 change: 0 additions & 1 deletion isogeo.py
Expand Up @@ -391,7 +391,6 @@ def token_slot(self, token_signal: str):
form to provide good ones.
- "NoInternet" : Asks to user to check his Internet connection.
"""
logger.debug("*=====* token_signal : {}".format(token_signal))
if token_signal == "ok":
if self.savedSearch == "first":
self.authenticator.ui_auth_form.btn_ok_cancel.buttons()[0].setEnabled(True)
Expand Down
52 changes: 24 additions & 28 deletions modules/api/auth.py
Expand Up @@ -8,10 +8,14 @@

# PyQT
from qgis.PyQt.QtCore import QSettings, QCoreApplication, QTranslator, qVersion, QObject, pyqtSignal
from qgis.PyQt.QtWidgets import QMessageBox
from qgis.PyQt.QtWidgets import QMessageBox, QSizePolicy

# PyQGIS
from qgis.gui import QgsMessageBar

# Plugin modules
from ..tools import IsogeoPlgTools
from ..user_inform import UserInformer

# UI class
from ...ui.auth.dlg_authentication import IsogeoAuthentication
Expand Down Expand Up @@ -55,10 +59,13 @@ class Authenticator(QObject):
where oAuth2 file is stored.
"""

auth_sig = pyqtSignal()
auth_sig = pyqtSignal(str)

# ui reference - authentication form
ui_auth_form = IsogeoAuthentication()
# display messages to the user
msgbar = QgsMessageBar(ui_auth_form)
ui_auth_form.msgbar_vlayout.addWidget(msgbar)

# api parameters
api_params = {
Expand Down Expand Up @@ -96,10 +103,10 @@ def __init__(self):
self.tr = object
self.lang = str

#inform user
self.informer = object
self.first_auth = bool

def emit_auth_sig(self):
self.auth_sig.emit()
# MANAGER -----------------------------------------------------------------
def manage_api_initialization(self):
"""Perform several operations to use Isogeo API:
Expand Down Expand Up @@ -263,8 +270,9 @@ def credentials_update(self, credentials_source: str = "QSettings"):
def display_auth_form(self):
"""Show authentication form with prefilled fields and connected widgets.
"""

# connecting widgets
self.informer = UserInformer(message_bar = self.msgbar, trad = self.tr)
self.auth_sig.connect(self.informer.authentication_slot)
# self.ui_auth_form.finished.connect(partial(self.disconnect_msgbar, informer.authentication_slot))
self.ui_auth_form.chb_isogeo_editor.stateChanged.connect(
lambda: qsettings.setValue(
"isogeo/user/editor",
Expand Down Expand Up @@ -294,7 +302,7 @@ def display_auth_form(self):
if self.first_auth:
pass
else :
self.auth_sig.emit()
self.auth_sig.emit("ok")
pass

def credentials_uploader(self):
Expand All @@ -304,24 +312,26 @@ def credentials_uploader(self):
"""
self.ui_auth_form.btn_browse_credentials.fileChanged.disconnect()

selected_file = Path(self.ui_auth_form.btn_browse_credentials.filePath())
# test file structure
selected_file = Path(self.ui_auth_form.btn_browse_credentials.filePath())
logger.debug("Loading credentials from file indicated by the user : {}".format(selected_file))
try:
api_credentials = plg_tools.credentials_loader(self.ui_auth_form.btn_browse_credentials.filePath())
except IOError as e:
self.auth_sig.emit("path")
logger.error("Fail to load credentials from authentication file. IOError : {}".format(e))
self.show_error("path")
self.ui_auth_form.btn_browse_credentials.fileChanged.connect(
self.credentials_uploader
)
self.ui_auth_form.btn_ok_cancel.buttons()[0].setEnabled(False)
return False
except ValueError as e:
logger.error("Fail to load credentials from authentication file. Error : {}".format(e))
self.show_error("file")
self.auth_sig.emit("file")
logger.error("Fail to load credentials from authentication file. ValueError : {}".format(e))
self.ui_auth_form.btn_browse_credentials.fileChanged.connect(
self.credentials_uploader
)
self.ui_auth_form.btn_ok_cancel.buttons()[0].setEnabled(False)
return False
# move credentials file into the plugin file structure
dest_path = self.cred_filepath
Expand All @@ -345,10 +355,11 @@ def credentials_uploader(self):
)
except Exception as e:
logger.debug("Fail to rename authentication file : {}".format(e))
self.show_error("path")
self.auth_sig.emit("path")
self.ui_auth_form.btn_browse_credentials.fileChanged.connect(
self.credentials_uploader
)
self.ui_auth_form.btn_ok_cancel.buttons()[0].setEnabled(False)
return False
# set form
self.ui_auth_form.ent_app_id.setText(api_credentials.get("client_id"))
Expand All @@ -361,23 +372,8 @@ def credentials_uploader(self):
self.ui_auth_form.btn_browse_credentials.fileChanged.connect(
self.credentials_uploader
)
self.emit_auth_sig()
self.auth_sig.emit("ok")
return True

def show_error(self, error_type:str):
message_type = {
"path" : "The specified file does not exist.",
"file" : "The selected credentials file's format is not valid.",
"creds" : "Authentication failed."
}
QMessageBox.warning(
self.ui_auth_form,
self.tr("Alert", "Authenticator"),
self.tr(
message_type.get(error_type), "Authenticator"
),
)


# REQUEST and RESULTS ----------------------------------------------------
def get_tags(self, tags: dict):
Expand Down
138 changes: 53 additions & 85 deletions modules/api/request.py
Expand Up @@ -29,16 +29,17 @@

class ApiRequester(QgsNetworkAccessManager):
"""Basic class to manage direct interactions with Isogeo's API :
- Authentication request for token
- Authentication request for tokenl
- Request about application's shares
- Request about ressources
- Building request URLs
"""

token_sig = pyqtSignal(str)
api_sig = pyqtSignal(str)
search_sig = pyqtSignal(dict, dict)
details_sig = pyqtSignal(dict, dict)
shares_sig = pyqtSignal(list)
error_sig = pyqtSignal(str)

def __init__(self):
# inheritance
Expand Down Expand Up @@ -81,7 +82,6 @@ def setup_api_params(self, dict_params: dict):
self.api_url_redirect = dict_params.get("url_redirect", "")
# sending an authentication request once API parameters are storer
self.send_request("token")
# self.auth_post_get_token()

def create_request(self, request_type: str):
"""Creates a QNetworkRequest() with appropriate headers and URL
Expand Down Expand Up @@ -165,7 +165,7 @@ def handle_reply(self, reply: QNetworkReply):
Depending on the reply's content validity and the request's type, an appropriated signal
is emitted with different data's value.
- For token requests : the token_sig signal is emitted wathever the replys's content but
- For token requests : the api_sig signal is emitted wathever the replys's content but
the mitted str's value depend on this content. A single slot is connected to this signal
and acts according to value of the string recieved (see isogeo.py : Isogeo.token_slot).
- For other requests : for each type of request there is a corresponding signal but the
Expand All @@ -179,72 +179,63 @@ def handle_reply(self, reply: QNetworkReply):
bytarray = reply.readAll()
content = bytarray.data().decode("utf8")
# if reply's content is valid
logger.debug("*=====* code : {}".format(reply.error()))
if reply.error() == 0 and content != "":
try:
parsed_content = json.loads(content)
except ValueError as e:
if "No JSON object could be decoded" in str(e):
logger.error("Internet connection failed")
self.token_sig.emit("NoInternet")
logger.error(
"'No JSON object could be decoded' --> Internet connection failed"
)
self.api_sig.emit("internet_issue")
else:
pass
return
logger.debug("*=====* content : {}".format(parsed_content))

url = reply.url().toString()
# for token request, one signal is emitted passing a string whose
# value depend on the reply content
if "token" in url:
logger.debug("Handling reply to a 'token' request")
logger.debug("(from : {}).".format(url))

if "access_token" in parsed_content:
logger.debug("Authentication succeeded, access token retrieved.")

QgsMessageLog.logMessage(
message="Authentication succeeded", tag="Isogeo", level=0
)
logger.debug("Access token retrieved.")

# storing token
self.token = "Bearer " + parsed_content.get("access_token")
self.token_sig.emit("tokenOK")
self.api_sig.emit("ok")

elif "error" in parsed_content:
logger.error(
"The API reply is an error: {}. ID and SECRET must be "
"invalid. Asking for them again.".format(
parsed_content.get("error")
"Authentication request failed. 'error' in parsed_content, may be "
"because of invalid credentials \n API's reply content : {}".format(
parsed_content
)
)
msgBar.pushMessage(
"Isogeo",
self.tr(
"API authentication failed.Isogeo API answered: {}"
).format(parsed_content.get("error")),
duration=10,
level=1,
)
self.token_sig.emit("credIssue")
self.api_sig.emit("creds_issue")

else:
msgBar.pushMessage(
"Isogeo",
self.tr(
"API authentication failed. Isogeo API answered: {}"
).format(parsed_content.get("error")),
duration=10,
level=1,
)
logger.debug(
"The API reply has an unexpected form: {}.".format(
parsed_content
)
logger.warning(
"Authentication request failed. API's reply's has an unexpected form."
"\n API's reply content : {}".format(parsed_content)
)
self.api_sig.emit("unkown_reply")

# for other types of request, a different signal is emitted depending
# on the type of request but it always pass the reply's content
# on the type of request but always passing the reply's content
else:
self.loopCount = 0
if "shares" in url:
logger.debug("Handling reply to a 'shares' request")
logger.debug("(from : {}).".format(url))
self.shares_sig.emit(parsed_content)
if len(parsed_content) > 0:
self.shares_sig.emit(parsed_content)
else :
self.api_sig.emit("shares_issue")
elif "resources/search?" in url:
logger.debug("Handling reply to a 'search' request")
logger.debug("(from : {}).".format(url))
Expand All @@ -258,7 +249,7 @@ def handle_reply(self, reply: QNetworkReply):
parsed_content, self.get_tags(parsed_content.get("tags"))
)
else:
logger.debug("Unkown reply type")
logger.debug("Unkown reply type : {}".format(parsed_content))
del parsed_content

# if replys's content is invalid
Expand All @@ -268,33 +259,20 @@ def handle_reply(self, reply: QNetworkReply):
self.send_request("token")

elif reply.error() >= 101 and reply.error() <= 105:
logger.error("Proxy issue code received : {}".format(reply.error()))
msgBar.pushMessage(
self.tr(
"Proxy issue code received : {}. Check your"
"OS and QGIS proxy configuration. If this error"
"keeps happening, please report it in the "
"bug tracker.".format(
reply.error()
)
),
duration=10,
level=1,
logger.error(
"Request to the API failed. Proxy issue code received : {}"
"\nsee https://doc.qt.io/qt-5/qnetworkreply.html#NetworkError-enum".format(
str(reply.error())
)
)
self.api_sig.emit("proxy_issue")

elif reply.error() == 302:
logger.error("Redirecting code received : 302")
msgBar.pushMessage(
self.tr(
"Redirecting code received. ID and SECRET "
"could be invalid. Asking for them again. "
"If this error keeps happening, please "
"report it in the bug tracker."
),
duration=10,
level=1,
logger.error(
"Request to the API failed. Redirecting code received : 302."
"Creds may be invalid or a proxy error wasn't catched."
)
self.token_sig.emit("credIssue")
self.api_sig.emit("creds_issue")

elif content == "":
if self.loopCount < 3:
Expand All @@ -303,31 +281,21 @@ def handle_reply(self, reply: QNetworkReply):
self.send_request("token")
else:
logger.error(
"Empty reply. Weither no catalog is shared with the "
"plugin, or there is a problem (2 requests sent "
"together)"
"Request to the API failed. Empty reply for the third time. "
"Weither no catalog is shared with the plugin, or there is a "
"problem (2 requests sent together)"
)
msgBar.pushMessage(
self.tr(
"The script is looping. Make sure you shared a "
"catalog with the plugin. If so, please report "
"this on the bug tracker."
),
duration=10,
level=1,
)
self.token_sig.emit("NoInternet")
return

self.api_sig.emit("shares_issue")

else:
logger.warning("Unknown error : {}".format(str(reply.error())))
QMessageBox.information(
iface.mainWindow(),
self.tr("Error"),
self.tr("You are facing an unknown error. " "Code: ")
+ str(reply.error())
+ "\nPlease report it on the bug tracker.",
logger.warning(
"Request to the API failed. Unkown error : {}"
"\n(see https://doc.qt.io/qt-5/qnetworkreply.html#NetworkError-enum)".format(
str(reply.error())
)
)

self.api_sig.emit("unkown_error")
return

def build_request_url(self, params: dict):
Expand Down Expand Up @@ -477,7 +445,7 @@ def get_tags(self, tags: dict):

# override API tags to allow all datasets filter - see #
if type_dataset == 2:
md_types[self.tr("Dataset", "Authenticator")] = "type:dataset"
md_types[self.tr("Dataset", "ApiRequester")] = "type:dataset"
else:
pass

Expand Down

0 comments on commit dd8302d

Please sign in to comment.