summaryrefslogtreecommitdiffstats
path: root/js/gdm/util.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--js/gdm/util.js785
1 files changed, 785 insertions, 0 deletions
diff --git a/js/gdm/util.js b/js/gdm/util.js
new file mode 100644
index 0000000..8d09356
--- /dev/null
+++ b/js/gdm/util.js
@@ -0,0 +1,785 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+/* exported BANNER_MESSAGE_KEY, BANNER_MESSAGE_TEXT_KEY, LOGO_KEY,
+ DISABLE_USER_LIST_KEY, fadeInActor, fadeOutActor, cloneAndFadeOutActor,
+ ShellUserVerifier */
+
+const { Clutter, Gdm, Gio, GLib } = imports.gi;
+const Signals = imports.misc.signals;
+
+const Batch = imports.gdm.batch;
+const OVirt = imports.gdm.oVirt;
+const Vmware = imports.gdm.vmware;
+const Main = imports.ui.main;
+const { loadInterfaceXML } = imports.misc.fileUtils;
+const Params = imports.misc.params;
+const SmartcardManager = imports.misc.smartcardManager;
+
+const FprintManagerIface = loadInterfaceXML('net.reactivated.Fprint.Manager');
+const FprintManagerProxy = Gio.DBusProxy.makeProxyWrapper(FprintManagerIface);
+const FprintDeviceIface = loadInterfaceXML('net.reactivated.Fprint.Device');
+const FprintDeviceProxy = Gio.DBusProxy.makeProxyWrapper(FprintDeviceIface);
+
+Gio._promisify(Gdm.Client.prototype, 'open_reauthentication_channel');
+Gio._promisify(Gdm.Client.prototype, 'get_user_verifier');
+Gio._promisify(Gdm.UserVerifierProxy.prototype,
+ 'call_begin_verification_for_user');
+Gio._promisify(Gdm.UserVerifierProxy.prototype, 'call_begin_verification');
+
+var PASSWORD_SERVICE_NAME = 'gdm-password';
+var FINGERPRINT_SERVICE_NAME = 'gdm-fingerprint';
+var SMARTCARD_SERVICE_NAME = 'gdm-smartcard';
+var FADE_ANIMATION_TIME = 160;
+var CLONE_FADE_ANIMATION_TIME = 250;
+
+var LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen';
+var PASSWORD_AUTHENTICATION_KEY = 'enable-password-authentication';
+var FINGERPRINT_AUTHENTICATION_KEY = 'enable-fingerprint-authentication';
+var SMARTCARD_AUTHENTICATION_KEY = 'enable-smartcard-authentication';
+var BANNER_MESSAGE_KEY = 'banner-message-enable';
+var BANNER_MESSAGE_TEXT_KEY = 'banner-message-text';
+var ALLOWED_FAILURES_KEY = 'allowed-failures';
+
+var LOGO_KEY = 'logo';
+var DISABLE_USER_LIST_KEY = 'disable-user-list';
+
+// Give user 48ms to read each character of a PAM message
+var USER_READ_TIME = 48;
+const FINGERPRINT_ERROR_TIMEOUT_WAIT = 15;
+
+var MessageType = {
+ // Keep messages in order by priority
+ NONE: 0,
+ HINT: 1,
+ INFO: 2,
+ ERROR: 3,
+};
+
+const FingerprintReaderType = {
+ NONE: 0,
+ PRESS: 1,
+ SWIPE: 2,
+};
+
+function fadeInActor(actor) {
+ if (actor.opacity == 255 && actor.visible)
+ return null;
+
+ let hold = new Batch.Hold();
+ actor.show();
+ let [, naturalHeight] = actor.get_preferred_height(-1);
+
+ actor.opacity = 0;
+ actor.set_height(0);
+ actor.ease({
+ opacity: 255,
+ height: naturalHeight,
+ duration: FADE_ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => {
+ this.set_height(-1);
+ hold.release();
+ },
+ });
+
+ return hold;
+}
+
+function fadeOutActor(actor) {
+ if (!actor.visible || actor.opacity == 0) {
+ actor.opacity = 0;
+ actor.hide();
+ return null;
+ }
+
+ let hold = new Batch.Hold();
+ actor.ease({
+ opacity: 0,
+ height: 0,
+ duration: FADE_ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => {
+ this.hide();
+ this.set_height(-1);
+ hold.release();
+ },
+ });
+ return hold;
+}
+
+function cloneAndFadeOutActor(actor) {
+ // Immediately hide actor so its sibling can have its space
+ // and position, but leave a non-reactive clone on-screen,
+ // so from the user's point of view it smoothly fades away
+ // and reveals its sibling.
+ actor.hide();
+
+ const clone = new Clutter.Clone({
+ source: actor,
+ reactive: false,
+ });
+
+ Main.uiGroup.add_child(clone);
+
+ let [x, y] = actor.get_transformed_position();
+ clone.set_position(x, y);
+
+ let hold = new Batch.Hold();
+ clone.ease({
+ opacity: 0,
+ duration: CLONE_FADE_ANIMATION_TIME,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => {
+ clone.destroy();
+ hold.release();
+ },
+ });
+ return hold;
+}
+
+var ShellUserVerifier = class extends Signals.EventEmitter {
+ constructor(client, params) {
+ super();
+ params = Params.parse(params, { reauthenticationOnly: false });
+ this._reauthOnly = params.reauthenticationOnly;
+
+ this._client = client;
+
+ this._defaultService = null;
+ this._preemptingService = null;
+
+ this._settings = new Gio.Settings({ schema_id: LOGIN_SCREEN_SCHEMA });
+ this._settings.connect('changed',
+ this._updateDefaultService.bind(this));
+ this._updateDefaultService();
+
+ this._fprintManager = new FprintManagerProxy(Gio.DBus.system,
+ 'net.reactivated.Fprint',
+ '/net/reactivated/Fprint/Manager',
+ null,
+ null,
+ Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES);
+ this._smartcardManager = SmartcardManager.getSmartcardManager();
+
+ // We check for smartcards right away, since an inserted smartcard
+ // at startup should result in immediately initiating authentication.
+ // This is different than fingerprint readers, where we only check them
+ // after a user has been picked.
+ this.smartcardDetected = false;
+ this._checkForSmartcard();
+
+ this._smartcardManager.connectObject(
+ 'smartcard-inserted', this._checkForSmartcard.bind(this),
+ 'smartcard-removed', this._checkForSmartcard.bind(this), this);
+
+ this._messageQueue = [];
+ this._messageQueueTimeoutId = 0;
+ this.reauthenticating = false;
+
+ this._failCounter = 0;
+ this._unavailableServices = new Set();
+
+ this._credentialManagers = {};
+ this._credentialManagers[OVirt.SERVICE_NAME] = OVirt.getOVirtCredentialsManager();
+ this._credentialManagers[Vmware.SERVICE_NAME] = Vmware.getVmwareCredentialsManager();
+
+ for (let service in this._credentialManagers) {
+ if (this._credentialManagers[service].token) {
+ this._onCredentialManagerAuthenticated(this._credentialManagers[service],
+ this._credentialManagers[service].token);
+ }
+
+ this._credentialManagers[service].connectObject('user-authenticated',
+ this._onCredentialManagerAuthenticated.bind(this), this);
+ }
+ }
+
+ get hasPendingMessages() {
+ return !!this._messageQueue.length;
+ }
+
+ get allowedFailures() {
+ return this._settings.get_int(ALLOWED_FAILURES_KEY);
+ }
+
+ get currentMessage() {
+ return this._messageQueue ? this._messageQueue[0] : null;
+ }
+
+ begin(userName, hold) {
+ this._cancellable = new Gio.Cancellable();
+ this._hold = hold;
+ this._userName = userName;
+ this.reauthenticating = false;
+
+ this._checkForFingerprintReader();
+
+ // If possible, reauthenticate an already running session,
+ // so any session specific credentials get updated appropriately
+ if (userName)
+ this._openReauthenticationChannel(userName);
+ else
+ this._getUserVerifier();
+ }
+
+ cancel() {
+ if (this._cancellable)
+ this._cancellable.cancel();
+
+ if (this._userVerifier) {
+ this._userVerifier.call_cancel_sync(null);
+ this.clear();
+ }
+ }
+
+ _clearUserVerifier() {
+ if (this._userVerifier) {
+ this._disconnectSignals();
+ this._userVerifier.run_dispose();
+ this._userVerifier = null;
+ if (this._userVerifierChoiceList) {
+ this._userVerifierChoiceList.run_dispose();
+ this._userVerifierChoiceList = null;
+ }
+ }
+ }
+
+ clear() {
+ if (this._cancellable) {
+ this._cancellable.cancel();
+ this._cancellable = null;
+ }
+
+ this._clearUserVerifier();
+ this._clearMessageQueue();
+ }
+
+ destroy() {
+ this.cancel();
+
+ this._settings.run_dispose();
+ this._settings = null;
+
+ this._smartcardManager.disconnectObject(this);
+ this._smartcardManager = null;
+
+ for (let service in this._credentialManagers) {
+ let credentialManager = this._credentialManagers[service];
+ credentialManager.disconnectObject(this);
+ credentialManager = null;
+ }
+ }
+
+ selectChoice(serviceName, key) {
+ this._userVerifierChoiceList.call_select_choice(serviceName, key, this._cancellable, null);
+ }
+
+ async answerQuery(serviceName, answer) {
+ try {
+ await this._handlePendingMessages();
+ this._userVerifier.call_answer_query(serviceName, answer, this._cancellable, null);
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ logError(e);
+ }
+ }
+
+ _getIntervalForMessage(message) {
+ if (!message)
+ return 0;
+
+ // We probably could be smarter here
+ return message.length * USER_READ_TIME;
+ }
+
+ finishMessageQueue() {
+ if (!this.hasPendingMessages)
+ return;
+
+ this._messageQueue = [];
+
+ this.emit('no-more-messages');
+ }
+
+ increaseCurrentMessageTimeout(interval) {
+ if (!this._messageQueueTimeoutId && interval > 0)
+ this._currentMessageExtraInterval = interval;
+ }
+
+ _serviceHasPendingMessages(serviceName) {
+ return this._messageQueue.some(m => m.serviceName === serviceName);
+ }
+
+ _filterServiceMessages(serviceName, messageType) {
+ // This function allows to remove queued messages for the @serviceName
+ // whose type has lower priority than @messageType, replacing them
+ // with a null message that will lead to clearing the prompt once done.
+ if (this._serviceHasPendingMessages(serviceName))
+ this._queuePriorityMessage(serviceName, null, messageType);
+ }
+
+ _queueMessageTimeout() {
+ if (this._messageQueueTimeoutId != 0)
+ return;
+
+ const message = this.currentMessage;
+
+ delete this._currentMessageExtraInterval;
+ this.emit('show-message', message.serviceName, message.text, message.type);
+
+ this._messageQueueTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT,
+ message.interval + (this._currentMessageExtraInterval | 0), () => {
+ this._messageQueueTimeoutId = 0;
+
+ if (this._messageQueue.length > 1) {
+ this._messageQueue.shift();
+ this._queueMessageTimeout();
+ } else {
+ this.finishMessageQueue();
+ }
+
+ return GLib.SOURCE_REMOVE;
+ });
+ GLib.Source.set_name_by_id(this._messageQueueTimeoutId, '[gnome-shell] this._queueMessageTimeout');
+ }
+
+ _queueMessage(serviceName, message, messageType) {
+ let interval = this._getIntervalForMessage(message);
+
+ this._messageQueue.push({ serviceName, text: message, type: messageType, interval });
+ this._queueMessageTimeout();
+ }
+
+ _queuePriorityMessage(serviceName, message, messageType) {
+ const newQueue = this._messageQueue.filter(m => {
+ if (m.serviceName !== serviceName || m.type >= messageType)
+ return m.text !== message;
+ return false;
+ });
+
+ if (!newQueue.includes(this.currentMessage))
+ this._clearMessageQueue();
+
+ this._messageQueue = newQueue;
+ this._queueMessage(serviceName, message, messageType);
+ }
+
+ _clearMessageQueue() {
+ this.finishMessageQueue();
+
+ if (this._messageQueueTimeoutId != 0) {
+ GLib.source_remove(this._messageQueueTimeoutId);
+ this._messageQueueTimeoutId = 0;
+ }
+ this.emit('show-message', null, null, MessageType.NONE);
+ }
+
+ async _checkForFingerprintReader() {
+ this._fingerprintReaderType = FingerprintReaderType.NONE;
+
+ if (!this._settings.get_boolean(FINGERPRINT_AUTHENTICATION_KEY) ||
+ this._fprintManager == null) {
+ this._updateDefaultService();
+ return;
+ }
+
+ try {
+ const [device] = await this._fprintManager.GetDefaultDeviceAsync(
+ Gio.DBusCallFlags.NONE, this._cancellable);
+ const fprintDeviceProxy = new FprintDeviceProxy(Gio.DBus.system,
+ 'net.reactivated.Fprint',
+ device);
+ const fprintDeviceType = fprintDeviceProxy['scan-type'];
+
+ this._fingerprintReaderType = fprintDeviceType === 'swipe'
+ ? FingerprintReaderType.SWIPE
+ : FingerprintReaderType.PRESS;
+ this._updateDefaultService();
+ } catch (e) {}
+ }
+
+ _onCredentialManagerAuthenticated(credentialManager, _token) {
+ this._preemptingService = credentialManager.service;
+ this.emit('credential-manager-authenticated');
+ }
+
+ _checkForSmartcard() {
+ let smartcardDetected;
+
+ if (!this._settings.get_boolean(SMARTCARD_AUTHENTICATION_KEY))
+ smartcardDetected = false;
+ else if (this._reauthOnly)
+ smartcardDetected = this._smartcardManager.hasInsertedLoginToken();
+ else
+ smartcardDetected = this._smartcardManager.hasInsertedTokens();
+
+ if (smartcardDetected != this.smartcardDetected) {
+ this.smartcardDetected = smartcardDetected;
+
+ if (this.smartcardDetected)
+ this._preemptingService = SMARTCARD_SERVICE_NAME;
+ else if (this._preemptingService == SMARTCARD_SERVICE_NAME)
+ this._preemptingService = null;
+
+ this.emit('smartcard-status-changed');
+ }
+ }
+
+ _reportInitError(where, error, serviceName) {
+ logError(error, where);
+ this._hold.release();
+
+ this._queueMessage(serviceName, _('Authentication error'), MessageType.ERROR);
+ this._failCounter++;
+ this._verificationFailed(serviceName, false);
+ }
+
+ async _openReauthenticationChannel(userName) {
+ try {
+ this._clearUserVerifier();
+ this._userVerifier = await this._client.open_reauthentication_channel(
+ userName, this._cancellable);
+ } catch (e) {
+ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ return;
+ if (e.matches(Gio.DBusError, Gio.DBusError.ACCESS_DENIED) &&
+ !this._reauthOnly) {
+ // Gdm emits org.freedesktop.DBus.Error.AccessDenied when there
+ // is no session to reauthenticate. Fall back to performing
+ // verification from this login session
+ this._getUserVerifier();
+ return;
+ }
+
+ this._reportInitError('Failed to open reauthentication channel', e);
+ return;
+ }
+
+ if (this._client.get_user_verifier_choice_list)
+ this._userVerifierChoiceList = this._client.get_user_verifier_choice_list();
+ else
+ this._userVerifierChoiceList = null;
+
+ this.reauthenticating = true;
+ this._connectSignals();
+ this._beginVerification();
+ this._hold.release();
+ }
+
+ async _getUserVerifier() {
+ try {
+ this._clearUserVerifier();
+ this._userVerifier =
+ await this._client.get_user_verifier(this._cancellable);
+ } catch (e) {
+ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ return;
+ this._reportInitError('Failed to obtain user verifier', e);
+ return;
+ }
+
+ if (this._client.get_user_verifier_choice_list)
+ this._userVerifierChoiceList = this._client.get_user_verifier_choice_list();
+ else
+ this._userVerifierChoiceList = null;
+
+ this._connectSignals();
+ this._beginVerification();
+ this._hold.release();
+ }
+
+ _connectSignals() {
+ this._disconnectSignals();
+
+ this._userVerifier.connectObject(
+ 'info', this._onInfo.bind(this),
+ 'problem', this._onProblem.bind(this),
+ 'info-query', this._onInfoQuery.bind(this),
+ 'secret-info-query', this._onSecretInfoQuery.bind(this),
+ 'conversation-stopped', this._onConversationStopped.bind(this),
+ 'service-unavailable', this._onServiceUnavailable.bind(this),
+ 'reset', this._onReset.bind(this),
+ 'verification-complete', this._onVerificationComplete.bind(this),
+ this);
+
+ if (this._userVerifierChoiceList) {
+ this._userVerifierChoiceList.connectObject('choice-query',
+ this._onChoiceListQuery.bind(this), this);
+ }
+ }
+
+ _disconnectSignals() {
+ this._userVerifier?.disconnectObject(this);
+ this._userVerifierChoiceList?.disconnectObject(this);
+ }
+
+ _getForegroundService() {
+ if (this._preemptingService)
+ return this._preemptingService;
+
+ return this._defaultService;
+ }
+
+ serviceIsForeground(serviceName) {
+ return serviceName == this._getForegroundService();
+ }
+
+ serviceIsDefault(serviceName) {
+ return serviceName == this._defaultService;
+ }
+
+ serviceIsFingerprint(serviceName) {
+ return this._fingerprintReaderType !== FingerprintReaderType.NONE &&
+ serviceName === FINGERPRINT_SERVICE_NAME;
+ }
+
+ _updateDefaultService() {
+ if (this._settings.get_boolean(PASSWORD_AUTHENTICATION_KEY))
+ this._defaultService = PASSWORD_SERVICE_NAME;
+ else if (this._settings.get_boolean(SMARTCARD_AUTHENTICATION_KEY))
+ this._defaultService = SMARTCARD_SERVICE_NAME;
+ else if (this._fingerprintReaderType !== FingerprintReaderType.NONE)
+ this._defaultService = FINGERPRINT_SERVICE_NAME;
+
+ if (!this._defaultService) {
+ log("no authentication service is enabled, using password authentication");
+ this._defaultService = PASSWORD_SERVICE_NAME;
+ }
+ }
+
+ async _startService(serviceName) {
+ this._hold.acquire();
+ try {
+ if (this._userName) {
+ await this._userVerifier.call_begin_verification_for_user(
+ serviceName, this._userName, this._cancellable);
+ } else {
+ await this._userVerifier.call_begin_verification(
+ serviceName, this._cancellable);
+ }
+ } catch (e) {
+ if (e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ return;
+ if (!this.serviceIsForeground(serviceName)) {
+ logError(e,
+ `Failed to start ${serviceName} for ${this._userName}`);
+ this._hold.release();
+ return;
+ }
+ this._reportInitError(
+ this._userName
+ ? `Failed to start ${serviceName} verification for user`
+ : `Failed to start ${serviceName} verification`,
+ e, serviceName);
+ return;
+ }
+ this._hold.release();
+ }
+
+ _beginVerification() {
+ this._startService(this._getForegroundService());
+
+ if (this._userName &&
+ this._fingerprintReaderType !== FingerprintReaderType.NONE &&
+ !this.serviceIsForeground(FINGERPRINT_SERVICE_NAME))
+ this._startService(FINGERPRINT_SERVICE_NAME);
+ }
+
+ _onChoiceListQuery(client, serviceName, promptMessage, list) {
+ if (!this.serviceIsForeground(serviceName))
+ return;
+
+ this.emit('show-choice-list', serviceName, promptMessage, list.deepUnpack());
+ }
+
+ _onInfo(client, serviceName, info) {
+ if (this.serviceIsForeground(serviceName)) {
+ this._queueMessage(serviceName, info, MessageType.INFO);
+ } else if (this.serviceIsFingerprint(serviceName)) {
+ // We don't show fingerprint messages directly since it's
+ // not the main auth service. Instead we use the messages
+ // as a cue to display our own message.
+ if (this._fingerprintReaderType === FingerprintReaderType.SWIPE) {
+ // Translators: this message is shown below the password entry field
+ // to indicate the user can swipe their finger on the fingerprint reader
+ this._queueMessage(serviceName, _('(or swipe finger across reader)'),
+ MessageType.HINT);
+ } else {
+ // Translators: this message is shown below the password entry field
+ // to indicate the user can place their finger on the fingerprint reader instead
+ this._queueMessage(serviceName, _('(or place finger on reader)'),
+ MessageType.HINT);
+ }
+ }
+ }
+
+ _onProblem(client, serviceName, problem) {
+ const isFingerprint = this.serviceIsFingerprint(serviceName);
+
+ if (!this.serviceIsForeground(serviceName) && !isFingerprint)
+ return;
+
+ this._queuePriorityMessage(serviceName, problem, MessageType.ERROR);
+
+ if (isFingerprint) {
+ // pam_fprintd allows the user to retry multiple (maybe even infinite!
+ // times before failing the authentication conversation.
+ // We don't want this behavior to bypass the max-tries setting the user has set,
+ // so we count the problem messages to know how many times the user has failed.
+ // Once we hit the max number of failures we allow, it's time to failure the
+ // conversation from our side. We can't do that right away, however, because
+ // we may drop pending messages coming from pam_fprintd. In order to make sure
+ // the user sees everything, we queue the failure up to get handled in the
+ // near future, after we've finished up the current round of messages.
+ this._failCounter++;
+
+ if (!this._canRetry()) {
+ if (this._fingerprintFailedId)
+ GLib.source_remove(this._fingerprintFailedId);
+
+ const cancellable = this._cancellable;
+ this._fingerprintFailedId = GLib.timeout_add(GLib.PRIORITY_DEFAULT,
+ FINGERPRINT_ERROR_TIMEOUT_WAIT, () => {
+ this._fingerprintFailedId = 0;
+ if (!cancellable.is_cancelled())
+ this._verificationFailed(serviceName, false);
+ return GLib.SOURCE_REMOVE;
+ });
+ }
+ }
+ }
+
+ _onInfoQuery(client, serviceName, question) {
+ if (!this.serviceIsForeground(serviceName))
+ return;
+
+ this.emit('ask-question', serviceName, question, false);
+ }
+
+ _onSecretInfoQuery(client, serviceName, secretQuestion) {
+ if (!this.serviceIsForeground(serviceName))
+ return;
+
+ let token = null;
+ if (this._credentialManagers[serviceName])
+ token = this._credentialManagers[serviceName].token;
+
+ if (token) {
+ this.answerQuery(serviceName, token);
+ return;
+ }
+
+ this.emit('ask-question', serviceName, secretQuestion, true);
+ }
+
+ _onReset() {
+ // Clear previous attempts to authenticate
+ this._failCounter = 0;
+ this._unavailableServices.clear();
+ this._updateDefaultService();
+
+ this.emit('reset');
+ }
+
+ _onVerificationComplete() {
+ this.emit('verification-complete');
+ }
+
+ _cancelAndReset() {
+ this.cancel();
+ this._onReset();
+ }
+
+ _retry(serviceName) {
+ this._hold = new Batch.Hold();
+ this._connectSignals();
+ this._startService(serviceName);
+ }
+
+ _canRetry() {
+ return this._userName &&
+ (this._reauthOnly || this._failCounter < this.allowedFailures);
+ }
+
+ async _verificationFailed(serviceName, shouldRetry) {
+ if (serviceName === FINGERPRINT_SERVICE_NAME) {
+ if (this._fingerprintFailedId)
+ GLib.source_remove(this._fingerprintFailedId);
+ }
+
+ // For Not Listed / enterprise logins, immediately reset
+ // the dialog
+ // Otherwise, when in login mode we allow ALLOWED_FAILURES attempts.
+ // After that, we go back to the welcome screen.
+ this._filterServiceMessages(serviceName, MessageType.ERROR);
+
+ const doneTrying = !shouldRetry || !this._canRetry();
+
+ this.emit('verification-failed', serviceName, !doneTrying);
+ try {
+ if (doneTrying) {
+ this._disconnectSignals();
+ await this._handlePendingMessages();
+ this._cancelAndReset();
+ } else {
+ await this._handlePendingMessages();
+ this._retry(serviceName);
+ }
+ } catch (e) {
+ if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
+ logError(e);
+ }
+ }
+
+ _handlePendingMessages() {
+ if (!this.hasPendingMessage)
+ return Promise.resolve();
+
+ const cancellable = this._cancellable;
+ return new Promise((resolve, reject) => {
+ let signalId = this.connect('no-more-messages', () => {
+ this.disconnect(signalId);
+ if (cancellable.is_cancelled())
+ reject(new GLib.Error(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED, 'Operation was cancelled'));
+ else
+ resolve();
+ });
+ });
+ }
+
+ _onServiceUnavailable(_client, serviceName, errorMessage) {
+ this._unavailableServices.add(serviceName);
+
+ if (!errorMessage)
+ return;
+
+ if (this.serviceIsForeground(serviceName) || this.serviceIsFingerprint(serviceName))
+ this._queueMessage(serviceName, errorMessage, MessageType.ERROR);
+ }
+
+ _onConversationStopped(client, serviceName) {
+ // If the login failed with the preauthenticated oVirt credentials
+ // then discard the credentials and revert to default authentication
+ // mechanism.
+ let foregroundService = Object.keys(this._credentialManagers).find(service =>
+ this.serviceIsForeground(service));
+ if (foregroundService) {
+ this._credentialManagers[foregroundService].token = null;
+ this._preemptingService = null;
+ this._verificationFailed(serviceName, false);
+ return;
+ }
+
+ this._filterServiceMessages(serviceName, MessageType.ERROR);
+
+ if (this._unavailableServices.has(serviceName))
+ return;
+
+ // if the password service fails, then cancel everything.
+ // But if, e.g., fingerprint fails, still give
+ // password authentication a chance to succeed
+ if (this.serviceIsForeground(serviceName))
+ this._failCounter++;
+
+ this._verificationFailed(serviceName, true);
+ }
+};