From 98c874503e14b40896a63ea36b86d39edccb8b30 Mon Sep 17 00:00:00 2001 From: Ivan Alejandro Date: Thu, 4 Sep 2014 13:06:56 -0300 Subject: Refactor login usage. - factor out the signal tracking code as a helper class, - move login logic from MainWindow to LoginWidget, - add new signals to the LoginWidget to interact with MainWindow, - add login sequence docs to LoginWidget class, - improve docs for login methods, - add LoginState class to handle login states, - disable login button until data is entered, - move some properties and actions to .ui file. --- src/leap/bitmask/gui/login.py | 537 +++++++++++++++++++++++++++++++++++------- 1 file changed, 450 insertions(+), 87 deletions(-) (limited to 'src/leap/bitmask/gui/login.py') diff --git a/src/leap/bitmask/gui/login.py b/src/leap/bitmask/gui/login.py index 2a79fafd..7487e888 100644 --- a/src/leap/bitmask/gui/login.py +++ b/src/leap/bitmask/gui/login.py @@ -16,13 +16,31 @@ # along with this program. If not, see . """ Login widget implementation + +The login sequence is the following: + - _do_login + - backend.provider_setup (check_name_resolution, check_https, download_provider_info) + - on error: _provider_setup_intermediate + - on success: _load_provider_config + - backend.provider_bootstrap (download_ca_cert, check_ca_fingerprint, check_api_certificate) + - on error: _provider_setup_intermediate + - on success: _provider_config_loaded + - backend.user_login + - on error: _authentication_error + - on success: _authentication_finished + """ import logging from PySide import QtCore, QtGui from ui_login import Ui_LoginWidget +# TODO: we should use a more granular signaling instead of passing error/ok as +# a result. +from leap.bitmask.backend.leapbackend import ERROR_KEY, PASSED_KEY from leap.bitmask.config import flags +from leap.bitmask.config.leapsettings import LeapSettings +from leap.bitmask.gui.signaltracker import SignalTracker from leap.bitmask.util import make_address from leap.bitmask.util.credentials import USERNAME_REGEX from leap.bitmask.util.keyring_helpers import has_keyring @@ -32,59 +50,71 @@ from leap.common.check import leap_assert_type logger = logging.getLogger(__name__) -class LoginWidget(QtGui.QWidget): +class LoginState(object): + """ + This class holds the states related to the login sequence. + """ + + def __init__(self): + # `wait_to_login` defines whether we should wait to start the login + # sequence or we should start right away. + # This is used, for instance, to hold on until EIP is started since the + # firewall could block the login attempt. + self.wait_to_login = False + + # This state indicates that the login sequence was required to start + # but it was set on hold since we `wait_to_login` was True + self.login_waiting = False + + # Full username of the logged user, with the format: 'user@provider' + self.full_logged_username = None + + +class LoginWidget(QtGui.QWidget, SignalTracker): """ Login widget that emits signals to display the wizard or to perform login. """ - # Emitted when the login button is clicked - login = QtCore.Signal() - logged_in_signal = QtCore.Signal() - cancel_login = QtCore.Signal() - logout = QtCore.Signal() + login_start = QtCore.Signal() + login_finished = QtCore.Signal() + login_offline_finished = QtCore.Signal() + login_failed = QtCore.Signal() + logged_out = QtCore.Signal() MAX_STATUS_WIDTH = 40 # Keyring KEYRING_KEY = "bitmask" - def __init__(self, settings, parent=None): + def __init__(self, backend, signaler, parent=None): """ Constructs the LoginWidget. - :param settings: client wide settings - :type settings: LeapSettings + :param backend: Backend being used + :type backend: Backend + :param signaler: Object in charge of handling communication + back to the frontend + :type signaler: Signaler :param parent: The parent widget for this widget :type parent: QWidget or None """ QtGui.QWidget.__init__(self, parent) - - self._settings = settings + SignalTracker.__init__(self) self.ui = Ui_LoginWidget() self.ui.setupUi(self) - self.ui.chkRemember.stateChanged.connect( - self._remember_state_changed) + self.ui.chkRemember.stateChanged.connect(self._remember_state_changed) self.ui.chkRemember.setEnabled(has_keyring()) - self.ui.lnPassword.setEchoMode(QtGui.QLineEdit.Password) - - self.ui.btnLogin.clicked.connect(self.login) - self.ui.lnPassword.returnPressed.connect(self.login) + self.ui.lnUser.textChanged.connect(self._credentials_changed) + self.ui.lnPassword.textChanged.connect(self._credentials_changed) - self.ui.lnUser.returnPressed.connect(self._focus_password) + self.ui.btnLogin.clicked.connect(self._do_login) + self.ui.btnLogout.clicked.connect(self.do_logout) - self.ui.btnLogout.clicked.connect( - self.logout) - - username_re = QtCore.QRegExp(USERNAME_REGEX) self.ui.lnUser.setValidator( - QtGui.QRegExpValidator(username_re, self)) - - self.logged_out() - - self.ui.btnLogout.clicked.connect(self.start_logout) + QtGui.QRegExpValidator(QtCore.QRegExp(USERNAME_REGEX), self)) self.ui.clblErrorMsg.hide() self.ui.clblErrorMsg.clicked.connect(self.ui.clblErrorMsg.hide) @@ -92,20 +122,73 @@ class LoginWidget(QtGui.QWidget): self.ui.lnUser.textEdited.connect(self.ui.clblErrorMsg.hide) self.ui.lnPassword.textEdited.connect(self.ui.clblErrorMsg.hide) + self._settings = LeapSettings() + self._backend = backend + self._leap_signaler = signaler + + # the selected provider that we'll use to login + self._provider = None + + self._state = LoginState() + + self._set_logged_out() + + @QtCore.Slot(int) def _remember_state_changed(self, state): """ - Saves the remember state in the LeapSettings + Save the remember state in the LeapSettings. + + :param state: the current state of the check box. + :type state: int + """ + # The possible state values of the checkbox (from QtCore.Qt.CheckState) + # are: Checked, Unchecked and PartiallyChecked + self._settings.set_remember(state == QtCore.Qt.Checked) + + @QtCore.Slot(unicode) + def _credentials_changed(self, text): + """ + TRIGGER: + self.ui.lnUser.textChanged + self.ui.lnPassword.textChanged - :param state: possible stats can be Checked, Unchecked and - PartiallyChecked - :type state: QtCore.Qt.CheckState + Update the 'enabled' status of the login button depending if we have + all the fields needed set. """ - enable = True if state == QtCore.Qt.Checked else False - self._settings.set_remember(enable) + enabled = self._provider and self.get_user() and self.get_password() + enabled = bool(enabled) # provider can be None + + self.ui.btnLogin.setEnabled(enabled) + + def wait_for_login(self, wait): + """ + Set the wait flag to True/False so the next time that a login action is + requested it will wait or not. + + If we set the wait to True and we have paused a login request before, + this will trigger a login action. + + :param wait: whether we should wait or not on the next login request. + :type wait: bool + """ + self._state.wait_to_login = wait + + if not wait and self._state.login_waiting: + logger.debug("No more waiting, triggering login sequence.") + self._do_login() + + def set_provider(self, provider): + """ + Set the provider to use in the login sequence. + + :param provider: the provider to use. + :type provider: unicode + """ + self._provider = provider def set_remember(self, value): """ - Checks the remember user and password checkbox + Check the remember user and password checkbox :param value: True to mark it checked, False otherwise :type value: bool @@ -132,12 +215,21 @@ class LoginWidget(QtGui.QWidget): def get_user(self): """ - Returns the user that appears in the widget. + Return the user that appears in the widget. :rtype: unicode """ return self.ui.lnUser.text() + def get_logged_user(self): + """ + Return the current logged user or None if no user is logged in. + The return value has the format: 'user@provider' + + :rtype: unicode or None + """ + return self._state.full_logged_username + def set_password(self, password): """ Sets the password for the widget @@ -200,12 +292,12 @@ class LoginWidget(QtGui.QWidget): :type enabled: bool """ text = self.tr("Cancel") - login_or_cancel = self.cancel_login + login_or_cancel = self._do_cancel hide_remember = enabled if not enabled: text = self.tr("Log In") - login_or_cancel = self.login + login_or_cancel = self._do_login self.ui.btnLogin.setText(text) @@ -220,40 +312,49 @@ class LoginWidget(QtGui.QWidget): """ self.ui.lnPassword.setFocus() - def start_login(self, provider): + def _check_login(self): """ - Setups the login widgets for actually performing the login and - performs some basic checks. + Check that we have the needed fields to do the actual login: provider, + username and password. - :param provider: the domain of the current provider - :type provider: unicode str - :returns: True if everything's good to go, False otherwise + :return: True if everything's good to go, False otherwise. :rtype: bool """ + provider = self._provider username = self.get_user() password = self.get_password() - self._enabled_services = self._settings.get_enabled_services(provider) - - if len(provider) == 0: - self.set_status( - self.tr("Please select a valid provider")) + if not provider: + self.set_status(self.tr("Please select a valid provider")) return False - if len(username) == 0: - self.set_status( - self.tr("Please provide a valid username")) + if not username: + self.set_status(self.tr("Please provide a valid username")) return False - if len(password) == 0: - self.set_status( - self.tr("Please provide a valid password")) + if not password: + self.set_status(self.tr("Please provide a valid password")) return False + return True + + def _set_logging_in(self): + """ + Set the status of the widget to "Logging in". + """ self.set_status(self.tr("Logging in..."), error=False) self.set_enabled(False) self.ui.clblErrorMsg.hide() + def _save_credentials(self): + """ + If the user asked to remember the credentials, we save them into the + keyring. + """ + provider = self._provider + username = self.get_user() + password = self.get_password() + self._settings.set_provider(provider) if self.get_remember() and has_keyring(): # in the keyring and in the settings @@ -262,36 +363,209 @@ class LoginWidget(QtGui.QWidget): try: keyring = get_keyring() keyring.set_password(self.KEYRING_KEY, - full_user_id, - password.encode("utf8")) + full_user_id, password.encode("utf8")) # Only save the username if it was saved correctly in # the keyring self._settings.set_user(full_user_id) except Exception as e: - logger.exception("Problem saving data to keyring. %r" - % (e,)) - return True + logger.exception("Problem saving data to keyring. %r" % (e,)) + + def do_login(self): + """ + Start the login sequence. + We check that we have the needed fields to do the actual login: + provider, username and password. + If everything is ok we perform the login. + + Note that the actual login won't be started if you set the + `wait_to_login` flag, it will be scheduled to get started when you set + that flag to False. + + :return: True if the login sequence started, False otherwise. + :rtype: bool + """ + ok = self._provider and self.get_user() and self.get_password() + + if ok: + self._do_login() + + return bool(ok) + + def _do_login(self): + """ + Start the login sequence. + """ + if self._state.wait_to_login: + logger.debug("Login delayed, waiting...") + + self._state.login_waiting = True + self.ui.btnLogin.setEnabled(False) + self.ui.btnLogin.setText(self.tr("Waiting...")) + # explicitly process events to display the button's text change. + QtCore.QCoreApplication.processEvents(0, 10) + + return + else: + self._state.login_waiting = False + self.ui.btnLogin.setEnabled(True) + + self.login_start.emit() + + provider = self._provider + if flags.OFFLINE: + self._do_offline_login() + return + + # connect to the backend signals, remember to disconnect after login. + self._backend_connect() + + if self._check_login(): + self._set_logging_in() + self._save_credentials() + self._backend.provider_setup(provider=provider) + + def _do_cancel(self): + logger.debug("Cancelling log in.") - def logged_in(self, provider): + self._backend.provider_cancel_setup() + self._backend.user_cancel_login() + self._set_logged_out() + + @QtCore.Slot() + def _set_login_cancelled(self): + """ + TRIGGERS: + Signaler.prov_cancelled_setup + + Re-enable the login widget and display a message for the cancelled + operation. + """ + self.set_status(self.tr("Log in cancelled by the user.")) + self.set_enabled(True) + + @QtCore.Slot(dict) + def _provider_setup_intermediate(self, data): + """ + TRIGGERS: + self._backend.signaler.prov_name_resolution + self._backend.signaler.prov_https_connection + + Handle a possible problem during the provider setup process. + If there was a problem, display it, otherwise it does nothing. + """ + if not data[PASSED_KEY]: + logger.error(data[ERROR_KEY]) + self._login_problem_provider() + + @QtCore.Slot() + def _login_problem_provider(self): + """ + Warn the user about a problem with the provider during login. + """ + self.set_status(self.tr("Unable to login: Problem with provider")) + self.set_enabled(True) + + @QtCore.Slot(dict) + def _load_provider_config(self, data): + """ + TRIGGERS: + self._backend.signaler.prov_download_provider_info + + Once the provider config has been downloaded, start the second + part of the bootstrapping sequence. + + :param data: result from the last stage of the + backend.provider_setup() + :type data: dict """ - Sets the widgets to the logged in state + if not data[PASSED_KEY]: + logger.error(data[ERROR_KEY]) + self._login_problem_provider() + return - :param provider: the domain of the current provider - :type provider: unicode str + self._backend.provider_bootstrap(provider=self._provider) + + @QtCore.Slot(dict) + def _provider_config_loaded(self, data): + """ + TRIGGERS: + self._backend.signaler.prov_check_api_certificate + + Once the provider configuration is loaded, this starts the SRP + authentication """ + if not data[PASSED_KEY]: + logger.error(data[ERROR_KEY]) + self._login_problem_provider() + return + + self._backend.user_login(provider=self._provider, + username=self.get_user(), + password=self.get_password()) + + # TODO check this! + def _do_offline_login(self): + logger.debug("OFFLINE mode! bypassing remote login") + # TODO reminder, we're not handling logout for offline mode. + self._set_logged_in() + self._logged_in_offline = True + self._set_label_offline() + self.login_offline_finished.emit() + + def _set_label_offline(self): + """ + Set the login label to reflect offline status. + """ + # TODO: figure out what widget to use for this. Maybe the window title? + + def _set_logged_in(self): + """ + Set the widgets to the logged in state. + """ + fullname = make_address(self.get_user(), self._provider) + self._state.full_logged_username = fullname self.ui.login_widget.hide() self.ui.logged_widget.show() - self.ui.lblUser.setText(make_address(self.get_user(), provider)) + self.ui.lblUser.setText(fullname) - if flags.OFFLINE is False: - self.logged_in_signal.emit() + @QtCore.Slot() + def _authentication_finished(self): + """ + TRIGGERS: + self._srp_auth.authentication_finished - def logged_out(self): + The SRP auth was successful, set the login status. """ - Sets the widgets to the logged out state + self.set_status(self.tr("Succeeded"), error=False) + self._set_logged_in() + + if not flags.OFFLINE: + self.login_finished.emit() + + @QtCore.Slot(unicode) + def _authentication_error(self, msg): """ - # TODO consider "logging out offline" too... - # how that would be ??? + TRIGGERS: + Signaler.srp_auth_error + Signaler.srp_auth_server_error + Signaler.srp_auth_connection_error + Signaler.srp_auth_bad_user_or_password + + Handle the authentication errors. + + :param msg: the message to show to the user. + :type msg: unicode + """ + self.set_status(msg) + self.set_enabled(True) + self.login_failed.emit() + + def _set_logged_out(self): + """ + Set the widgets to the logged out state. + """ + # TODO consider "logging out offline" too... how that would be ??? + self._state.full_logged_username = None self.ui.login_widget.show() self.ui.logged_widget.hide() @@ -300,30 +574,75 @@ class LoginWidget(QtGui.QWidget): self.set_enabled(True) self.set_status("", error=False) - def start_logout(self): + @QtCore.Slot() + def do_logout(self): """ - Sets the widgets to the logging out state + TRIGGER: + self.ui.btnLogout.clicked + + Start the logout sequence and set the widgets to the "logging out" + state. """ - self.ui.btnLogout.setText(self.tr("Logging out...")) - self.ui.btnLogout.setEnabled(False) + if self._state.full_logged_username is not None: + self._set_logging_out() + self._backend.user_logout() + else: + logger.debug("Not logged in.") - def done_logout(self): + def _set_logging_out(self, logging_out=True): """ - Sets the widgets to the logged out state + Set the status of the logout button. + + logging_out == True: + button text -> "Logging out..." + button enabled -> False + + logging_out == False: + button text -> "Logout + button enabled -> True + + :param logging_out: wether we are logging out or not. + :type logging_out: bool """ - self.ui.btnLogout.setText(self.tr("Logout")) - self.ui.btnLogout.setEnabled(True) - self.ui.clblErrorMsg.hide() + if logging_out: + self.ui.btnLogout.setText(self.tr("Logging out...")) + self.ui.btnLogout.setEnabled(False) + else: + self.ui.btnLogout.setText(self.tr("Logout")) + self.ui.btnLogout.setEnabled(True) + self.ui.clblErrorMsg.hide() + + @QtCore.Slot() + def _logout_error(self): + """ + TRIGGER: + self._srp_auth.logout_error + + Inform the user about a logout error. + """ + self._set_logging_out(False) + self.set_status(self.tr("Something went wrong with the logout.")) + + @QtCore.Slot() + def _logout_ok(self): + """ + TRIGGER: + self._srp_auth.logout_ok + + Switch the stackedWidget back to the login stage after logging out. + """ + self._set_logging_out(False) + self._set_logged_out() + self.logged_out.emit() def load_user_from_keyring(self, saved_user): """ - Tries to load a user from the keyring, returns True if it was - loaded successfully, False otherwise. + Try to load a user from the keyring. - :param saved_user: String containing the saved username as - user@domain + :param saved_user: the saved username as user@domain :type saved_user: unicode + :return: True if the user was loaded successfully, False otherwise. :rtype: bool """ leap_assert_type(saved_user, unicode) @@ -336,15 +655,13 @@ class LoginWidget(QtGui.QWidget): return False self.set_user(username) - self.set_remember(True) saved_password = None try: keyring = get_keyring() - saved_password = keyring.get_password(self.KEYRING_KEY, - saved_user - .encode("utf8")) + u_user = saved_user.encode("utf8") + saved_password = keyring.get_password(self.KEYRING_KEY, u_user) except ValueError as e: logger.debug("Incorrect Password. %r." % (e,)) @@ -353,3 +670,49 @@ class LoginWidget(QtGui.QWidget): return True return False + + def _backend_connect(self): + """ + Connect to backend signals. + + We track the signals in order to disconnect them on demand. + """ + sig = self._leap_signaler + conntrack = self.connect_and_track + auth_err = self._authentication_error + + # provider_setup signals + conntrack(sig.prov_name_resolution, self._provider_setup_intermediate) + conntrack(sig.prov_https_connection, self._provider_setup_intermediate) + conntrack(sig.prov_download_provider_info, self._load_provider_config) + + # provider_bootstrap signals + conntrack(sig.prov_download_ca_cert, self._provider_setup_intermediate) + # XXX missing check_ca_fingerprint connection + conntrack(sig.prov_check_api_certificate, self._provider_config_loaded) + + conntrack(sig.prov_problem_with_provider, self._login_problem_provider) + conntrack(sig.prov_cancelled_setup, self._set_login_cancelled) + + # Login signals + conntrack(sig.srp_auth_ok, self._authentication_finished) + + auth_error = lambda: auth_err(self.tr("Unknown error.")) + conntrack(sig.srp_auth_error, auth_error) + + auth_server_error = lambda: auth_err(self.tr( + "There was a server problem with authentication.")) + conntrack(sig.srp_auth_server_error, auth_server_error) + + auth_connection_error = lambda: auth_err(self.tr( + "Could not establish a connection.")) + conntrack(sig.srp_auth_connection_error, auth_connection_error) + + auth_bad_user_or_password = lambda: auth_err(self.tr( + "Invalid username or password.")) + conntrack(sig.srp_auth_bad_user_or_password, auth_bad_user_or_password) + + # Logout signals + sig.srp_logout_ok.connect(self._logout_ok) + sig.srp_logout_error.connect(self._logout_error) + # sig.srp_not_logged_in_error.connect(self._not_logged_in_error) -- cgit v1.2.3