b1ac1b47cd
Extensions don't need to poke at the internal this._credentialManagers variable in order to add their own credential manager. Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2399>
805 lines
27 KiB
JavaScript
805 lines
27 KiB
JavaScript
// -*- 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.addCredentialManager(OVirt.SERVICE_NAME, OVirt.getOVirtCredentialsManager());
|
|
this.addCredentialManager(Vmware.SERVICE_NAME, Vmware.getVmwareCredentialsManager());
|
|
}
|
|
|
|
addCredentialManager(serviceName, credentialManager) {
|
|
if (this._credentialManagers[serviceName])
|
|
return;
|
|
|
|
this._credentialManagers[serviceName] = credentialManager;
|
|
if (credentialManager.token) {
|
|
this._onCredentialManagerAuthenticated(credentialManager,
|
|
credentialManager.token);
|
|
}
|
|
|
|
credentialManager.connectObject('user-authenticated',
|
|
this._onCredentialManagerAuthenticated.bind(this), this);
|
|
}
|
|
|
|
removeCredentialManager(serviceName) {
|
|
let credentialManager = this._credentialManagers[serviceName];
|
|
if (!credentialManager)
|
|
return;
|
|
|
|
credentialManager.disconnectObject(this);
|
|
delete this._credentialManagers[serviceName];
|
|
}
|
|
|
|
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)
|
|
this.removeCredentialManager(service);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
_getForegroundService() {
|
|
if (this._preemptingService)
|
|
return this._preemptingService;
|
|
|
|
return this._defaultService;
|
|
}
|
|
|
|
serviceIsForeground(serviceName) {
|
|
return serviceName == this._getForegroundService();
|
|
}
|
|
|
|
foregroundServiceDeterminesUsername() {
|
|
for (let serviceName in this._credentialManagers) {
|
|
if (this.serviceIsForeground(serviceName))
|
|
return true;
|
|
}
|
|
|
|
return this.serviceIsForeground(SMARTCARD_SERVICE_NAME);
|
|
}
|
|
|
|
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);
|
|
}
|
|
};
|