From f821b65401284cc31f68f0eb1b2e71ae3a90a122 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 18 Jul 2017 12:58:14 -0400 Subject: [PATCH 1/2] gdm: Add AuthList control Ultimately, we want to add support for GDM's new ChoiceList PAM extension. That extension allows PAM modules to present a list of choices to the user. Before we can support that extension, however, we need to have a list control in the login-screen/unlock screen. This commit adds that control. For the most part, it's a copy-and-paste of the gdm userlist, but with less features. It lacks API specific to the users, lacks the built in timed login indicator, etc. It does feature a label heading. --- .../widgets/_login-dialog.scss | 26 +++ js/gdm/authList.js | 176 ++++++++++++++++++ js/js-resources.gresource.xml | 1 + 3 files changed, 203 insertions(+) create mode 100644 js/gdm/authList.js diff --git a/data/theme/gnome-shell-sass/widgets/_login-dialog.scss b/data/theme/gnome-shell-sass/widgets/_login-dialog.scss index 84539342d..f68d5de99 100644 --- a/data/theme/gnome-shell-sass/widgets/_login-dialog.scss +++ b/data/theme/gnome-shell-sass/widgets/_login-dialog.scss @@ -86,60 +86,86 @@ .caps-lock-warning-label, .login-dialog-message-warning { color: $osd_fg_color; } } .login-dialog-logo-bin { padding: 24px 0px; } .login-dialog-banner { color: darken($osd_fg_color,10%); } .login-dialog-button-box { width: 23em; spacing: 5px; } .login-dialog-message { text-align: center; } .login-dialog-message-hint, .login-dialog-message { color: darken($osd_fg_color, 20%); min-height: 2.75em; } .login-dialog-user-selection-box { padding: 100px 0px; } .login-dialog-not-listed-label { padding-left: 2px; .login-dialog-not-listed-button:focus &, .login-dialog-not-listed-button:hover & { color: $osd_fg_color; } } .login-dialog-not-listed-label { @include fontsize($base_font_size - 1); font-weight: bold; color: darken($osd_fg_color,30%); padding-top: 1em; } +.login-dialog-auth-list-view { -st-vfade-offset: 1em; } +.login-dialog-auth-list { + spacing: 6px; + margin-left: 2em; +} + +.login-dialog-auth-list-title { + margin-left: 2em; +} + +.login-dialog-auth-list-item { + border-radius: $base_border_radius + 4px; + padding: 6px; + color: darken($osd_fg_color,30%); + &:focus, &:selected { background-color: $selected_bg_color; color: $selected_fg_color; } +} + +.login-dialog-auth-list-label { + @include fontsize($base_font_size + 2); + font-weight: bold; + padding-left: 15px; + + &:ltr { padding-left: 14px; text-align: left; } + &:rtl { padding-right: 14px; text-align: right; } +} + .login-dialog-user-list-view { -st-vfade-offset: 1em; } .login-dialog-user-list { spacing: 12px; width: 23em; &:expanded .login-dialog-user-list-item:selected { background-color: $selected_bg_color; color: $selected_fg_color; } &:expanded .login-dialog-user-list-item:logged-in { border-right: 2px solid $selected_bg_color; } } .login-dialog-user-list-item { border-radius: $base_border_radius + 4px; padding: 6px; color: darken($osd_fg_color,30%); &:ltr .user-widget { padding-right: 1em; } &:rtl .user-widget { padding-left: 1em; } .login-dialog-timed-login-indicator { height: 2px; margin-top: 6px; background-color: $osd_fg_color; } &:focus .login-dialog-timed-login-indicator { background-color: $selected_fg_color; } } .user-widget-label { color: $osd_fg_color; } .user-widget.horizontal .user-widget-label { @include fontsize($base_font_size + 2); font-weight: bold; padding-left: 15px; diff --git a/js/gdm/authList.js b/js/gdm/authList.js new file mode 100644 index 000000000..fb223a972 --- /dev/null +++ b/js/gdm/authList.js @@ -0,0 +1,176 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- +/* + * Copyright 2017 Red Hat, Inc + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ +/* exported AuthList */ + +const { Clutter, GObject, Meta, St } = imports.gi; + +const SCROLL_ANIMATION_TIME = 500; + +const AuthListItem = GObject.registerClass({ + Signals: { 'activate': {} }, +}, class AuthListItem extends St.Button { + _init(key, text) { + this.key = key; + const label = new St.Label({ + text, + style_class: 'login-dialog-auth-list-label', + y_align: Clutter.ActorAlign.CENTER, + x_expand: false, + }); + + super._init({ + style_class: 'login-dialog-auth-list-item', + button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, + can_focus: true, + child: label, + reactive: true, + }); + + this.connect('key-focus-in', + () => this._setSelected(true)); + this.connect('key-focus-out', + () => this._setSelected(false)); + this.connect('notify::hover', + () => this._setSelected(this.hover)); + + this.connect('clicked', this._onClicked.bind(this)); + } + + _onClicked() { + this.emit('activate'); + } + + _setSelected(selected) { + if (selected) { + this.add_style_pseudo_class('selected'); + this.grab_key_focus(); + } else { + this.remove_style_pseudo_class('selected'); + } + } +}); + +var AuthList = GObject.registerClass({ + Signals: { + 'activate': { param_types: [GObject.TYPE_STRING] }, + 'item-added': { param_types: [AuthListItem.$gtype] }, + }, +}, class AuthList extends St.BoxLayout { + _init() { + super._init({ + vertical: true, + style_class: 'login-dialog-auth-list-layout', + x_align: Clutter.ActorAlign.START, + y_align: Clutter.ActorAlign.CENTER, + }); + + this.label = new St.Label({ style_class: 'login-dialog-auth-list-title' }); + this.add_child(this.label); + + this._scrollView = new St.ScrollView({ + style_class: 'login-dialog-auth-list-view', + }); + this._scrollView.set_policy( + St.PolicyType.NEVER, St.PolicyType.AUTOMATIC); + this.add_child(this._scrollView); + + this._box = new St.BoxLayout({ + vertical: true, + style_class: 'login-dialog-auth-list', + pseudo_class: 'expanded', + }); + + this._scrollView.add_actor(this._box); + this._items = new Map(); + + this.connect('key-focus-in', this._moveFocusToItems.bind(this)); + } + + _moveFocusToItems() { + let hasItems = this.numItems > 0; + + if (!hasItems) + return; + + if (global.stage.get_key_focus() !== this) + return; + + let focusSet = this.navigate_focus(null, St.DirectionType.TAB_FORWARD, false); + if (!focusSet) { + Meta.later_add(Meta.LaterType.BEFORE_REDRAW, () => { + this._moveFocusToItems(); + return false; + }); + } + } + + _onItemActivated(activatedItem) { + this.emit('activate', activatedItem.key); + } + + scrollToItem(item) { + let box = item.get_allocation_box(); + + let adjustment = this._scrollView.get_vscroll_bar().get_adjustment(); + + let value = (box.y1 + adjustment.step_increment / 2.0) - (adjustment.page_size / 2.0); + adjustment.ease(value, { + duration: SCROLL_ANIMATION_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + } + + addItem(key, text) { + this.removeItem(key); + + let item = new AuthListItem(key, text); + this._box.add(item); + + this._items.set(key, item); + + item.connect('activate', this._onItemActivated.bind(this)); + + // Try to keep the focused item front-and-center + item.connect('key-focus-in', () => this.scrollToItem(item)); + + this._moveFocusToItems(); + + this.emit('item-added', item); + } + + removeItem(key) { + if (!this._items.has(key)) + return; + + let item = this._items.get(key); + + item.destroy(); + + this._items.delete(key); + } + + get numItems() { + return this._items.size; + } + + clear() { + this.label.text = ''; + this._box.destroy_all_children(); + this._items.clear(); + } +}); diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml index e65e0e9cf..b2c603a55 100644 --- a/js/js-resources.gresource.xml +++ b/js/js-resources.gresource.xml @@ -1,33 +1,34 @@ + gdm/authList.js gdm/authPrompt.js gdm/batch.js gdm/loginDialog.js gdm/oVirt.js gdm/credentialManager.js gdm/vmware.js gdm/realmd.js gdm/util.js misc/config.js misc/extensionUtils.js misc/fileUtils.js misc/gnomeSession.js misc/history.js misc/ibusManager.js misc/inputMethod.js misc/introspect.js misc/jsParse.js misc/keyboardManager.js misc/loginManager.js misc/modemManager.js misc/objectManager.js misc/params.js misc/parentalControlsManager.js misc/permissionStore.js misc/smartcardManager.js misc/systemActions.js misc/util.js misc/weather.js -- 2.34.1 From 5a2fda2fe2526f81c4dbbee6512182f19fc76a74 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Mon, 17 Jul 2017 16:48:03 -0400 Subject: [PATCH 2/2] gdmUtil: Enable support for GDM's ChoiceList PAM extension This commit hooks up support for GDM's ChoiceList PAM extension. --- js/gdm/authPrompt.js | 71 +++++++++++++++++++++++++++++++++++++++++-- js/gdm/loginDialog.js | 5 +++ js/gdm/util.js | 28 +++++++++++++++++ js/ui/unlockDialog.js | 7 +++++ 4 files changed, 109 insertions(+), 2 deletions(-) diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js index 84c608b2f..4da91e096 100644 --- a/js/gdm/authPrompt.js +++ b/js/gdm/authPrompt.js @@ -1,36 +1,37 @@ // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- /* exported AuthPrompt */ const { Clutter, GLib, GObject, Meta, Pango, Shell, St } = imports.gi; const Animation = imports.ui.animation; +const AuthList = imports.gdm.authList; const Batch = imports.gdm.batch; const GdmUtil = imports.gdm.util; const OVirt = imports.gdm.oVirt; const Vmware = imports.gdm.vmware; const Params = imports.misc.params; const ShellEntry = imports.ui.shellEntry; const UserWidget = imports.ui.userWidget; const Util = imports.misc.util; var DEFAULT_BUTTON_WELL_ICON_SIZE = 16; var DEFAULT_BUTTON_WELL_ANIMATION_DELAY = 1000; var DEFAULT_BUTTON_WELL_ANIMATION_TIME = 300; var MESSAGE_FADE_OUT_ANIMATION_TIME = 500; var AuthPromptMode = { UNLOCK_ONLY: 0, UNLOCK_OR_LOG_IN: 1, }; var AuthPromptStatus = { NOT_VERIFYING: 0, VERIFYING: 1, VERIFICATION_FAILED: 2, VERIFICATION_SUCCEEDED: 3, VERIFICATION_CANCELLED: 4, VERIFICATION_IN_PROGRESS: 5, }; var BeginRequestType = { @@ -48,144 +49,164 @@ var AuthPrompt = GObject.registerClass({ 'reset': { param_types: [GObject.TYPE_UINT] }, }, }, class AuthPrompt extends St.BoxLayout { _init(gdmClient, mode) { super._init({ style_class: 'login-dialog-prompt-layout', vertical: true, x_expand: true, x_align: Clutter.ActorAlign.CENTER, }); this.verificationStatus = AuthPromptStatus.NOT_VERIFYING; this._gdmClient = gdmClient; this._mode = mode; this._defaultButtonWellActor = null; this._cancelledRetries = 0; this._idleMonitor = Meta.IdleMonitor.get_core(); let reauthenticationOnly; if (this._mode == AuthPromptMode.UNLOCK_ONLY) reauthenticationOnly = true; else if (this._mode == AuthPromptMode.UNLOCK_OR_LOG_IN) reauthenticationOnly = false; this._userVerifier = new GdmUtil.ShellUserVerifier(this._gdmClient, { reauthenticationOnly }); this._userVerifier.connect('ask-question', this._onAskQuestion.bind(this)); this._userVerifier.connect('show-message', this._onShowMessage.bind(this)); + this._userVerifier.connect('show-choice-list', this._onShowChoiceList.bind(this)); this._userVerifier.connect('verification-failed', this._onVerificationFailed.bind(this)); this._userVerifier.connect('verification-complete', this._onVerificationComplete.bind(this)); this._userVerifier.connect('reset', this._onReset.bind(this)); this._userVerifier.connect('smartcard-status-changed', this._onSmartcardStatusChanged.bind(this)); this._userVerifier.connect('credential-manager-authenticated', this._onCredentialManagerAuthenticated.bind(this)); this.smartcardDetected = this._userVerifier.smartcardDetected; this.connect('destroy', this._onDestroy.bind(this)); this._userWell = new St.Bin({ x_expand: true, y_expand: true, }); this.add_child(this._userWell); this._hasCancelButton = this._mode === AuthPromptMode.UNLOCK_OR_LOG_IN; - this._initEntryRow(); + this._initInputRow(); let capsLockPlaceholder = new St.Label(); this.add_child(capsLockPlaceholder); this._capsLockWarningLabel = new ShellEntry.CapsLockWarning({ x_expand: true, x_align: Clutter.ActorAlign.CENTER, }); this.add_child(this._capsLockWarningLabel); this._capsLockWarningLabel.bind_property('visible', capsLockPlaceholder, 'visible', GObject.BindingFlags.SYNC_CREATE | GObject.BindingFlags.INVERT_BOOLEAN); this._message = new St.Label({ opacity: 0, styleClass: 'login-dialog-message', y_expand: true, x_expand: true, y_align: Clutter.ActorAlign.START, x_align: Clutter.ActorAlign.CENTER, }); this._message.clutter_text.line_wrap = true; this._message.clutter_text.ellipsize = Pango.EllipsizeMode.NONE; this.add_child(this._message); } _onDestroy() { if (this._preemptiveAnswerWatchId) { this._idleMonitor.remove_watch(this._preemptiveAnswerWatchId); this._preemptiveAnswerWatchId = 0; } this._userVerifier.destroy(); this._userVerifier = null; } vfunc_key_press_event(keyPressEvent) { if (keyPressEvent.keyval == Clutter.KEY_Escape) this.cancel(); return super.vfunc_key_press_event(keyPressEvent); } - _initEntryRow() { + _initInputRow() { this._mainBox = new St.BoxLayout({ style_class: 'login-dialog-button-box', vertical: false, }); this.add_child(this._mainBox); this.cancelButton = new St.Button({ style_class: 'modal-dialog-button button cancel-button', accessible_name: _('Cancel'), button_mask: St.ButtonMask.ONE | St.ButtonMask.THREE, reactive: this._hasCancelButton, can_focus: this._hasCancelButton, x_align: Clutter.ActorAlign.START, y_align: Clutter.ActorAlign.CENTER, child: new St.Icon({ icon_name: 'go-previous-symbolic' }), }); if (this._hasCancelButton) this.cancelButton.connect('clicked', () => this.cancel()); else this.cancelButton.opacity = 0; this._mainBox.add_child(this.cancelButton); + this._authList = new AuthList.AuthList(); + this._authList.set({ + visible: false, + }); + this._authList.connect('activate', (list, key) => { + this._authList.reactive = false; + this._authList.ease({ + opacity: 0, + duration: MESSAGE_FADE_OUT_ANIMATION_TIME, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => { + this._authList.clear(); + this._authList.hide(); + this._userVerifier.selectChoice(this._queryingService, key); + }, + }); + }); + this._mainBox.add_child(this._authList); + let entryParams = { style_class: 'login-dialog-prompt-entry', can_focus: true, x_expand: true, }; this._entry = null; this._textEntry = new St.Entry(entryParams); ShellEntry.addContextMenu(this._textEntry, { actionMode: Shell.ActionMode.NONE }); this._passwordEntry = new St.PasswordEntry(entryParams); ShellEntry.addContextMenu(this._passwordEntry, { actionMode: Shell.ActionMode.NONE }); this._entry = this._passwordEntry; this._mainBox.add_child(this._entry); this._entry.grab_key_focus(); this._timedLoginIndicator = new St.Bin({ style_class: 'login-dialog-timed-login-indicator', scale_x: 0, }); this.add_child(this._timedLoginIndicator); [this._textEntry, this._passwordEntry].forEach(entry => { entry.clutter_text.connect('text-changed', () => { if (!this._userVerifier.hasPendingMessages && this._queryingService && !this._preemptiveAnswer) this._fadeOutMessage(); }); @@ -276,60 +297,74 @@ var AuthPrompt = GObject.registerClass({ this._entry = this._textEntry; } this._capsLockWarningLabel.visible = secret; } _onAskQuestion(verifier, serviceName, question, secret) { if (this._queryingService) this.clear(); this._queryingService = serviceName; if (this._preemptiveAnswer) { this._userVerifier.answerQuery(this._queryingService, this._preemptiveAnswer); this._preemptiveAnswer = null; return; } this._updateEntry(secret); // Hack: The question string comes directly from PAM, if it's "Password:" // we replace it with our own to allow localization, if it's something // else we remove the last colon and any trailing or leading spaces. if (question === 'Password:' || question === 'Password: ') this.setQuestion(_('Password')); else this.setQuestion(question.replace(/: *$/, '').trim()); this.updateSensitivity(true); this.emit('prompted'); } + _onShowChoiceList(userVerifier, serviceName, promptMessage, choiceList) { + if (this._queryingService) + this.clear(); + + this._queryingService = serviceName; + + if (this._preemptiveAnswer) + this._preemptiveAnswer = null; + + this.setChoiceList(promptMessage, choiceList); + this.updateSensitivity(true); + this.emit('prompted'); + } + _onCredentialManagerAuthenticated() { if (this.verificationStatus != AuthPromptStatus.VERIFICATION_SUCCEEDED) this.reset(); } _onSmartcardStatusChanged() { this.smartcardDetected = this._userVerifier.smartcardDetected; // Most of the time we want to reset if the user inserts or removes // a smartcard. Smartcard insertion "preempts" what the user was // doing, and smartcard removal aborts the preemption. // The exceptions are: 1) Don't reset on smartcard insertion if we're already verifying // with a smartcard // 2) Don't reset if we've already succeeded at verification and // the user is getting logged in. if (this._userVerifier.serviceIsDefault(GdmUtil.SMARTCARD_SERVICE_NAME) && this.verificationStatus == AuthPromptStatus.VERIFYING && this.smartcardDetected) return; if (this.verificationStatus != AuthPromptStatus.VERIFICATION_SUCCEEDED) this.reset(); } _onShowMessage(_userVerifier, serviceName, message, type) { this.setMessage(serviceName, message, type); this.emit('prompted'); } _onVerificationFailed(userVerifier, serviceName, canRetry) { @@ -411,109 +446,141 @@ var AuthPrompt = GObject.registerClass({ if (actor) { if (isSpinner) this._spinner.play(); if (!animate) { actor.opacity = 255; } else { actor.ease({ opacity: 255, duration: DEFAULT_BUTTON_WELL_ANIMATION_TIME, delay: DEFAULT_BUTTON_WELL_ANIMATION_DELAY, mode: Clutter.AnimationMode.LINEAR, }); } } this._defaultButtonWellActor = actor; } startSpinning() { this.setActorInDefaultButtonWell(this._spinner, true); } stopSpinning() { this.setActorInDefaultButtonWell(null, false); } clear() { this._entry.text = ''; this.stopSpinning(); + this._authList.clear(); + this._authList.hide(); } setQuestion(question) { if (this._preemptiveAnswerWatchId) { this._idleMonitor.remove_watch(this._preemptiveAnswerWatchId); this._preemptiveAnswerWatchId = 0; } this._entry.hint_text = question; + this._authList.hide(); this._entry.show(); this._entry.grab_key_focus(); } + _fadeInChoiceList() { + this._authList.set({ + opacity: 0, + visible: true, + reactive: false, + }); + this._authList.ease({ + opacity: 255, + duration: MESSAGE_FADE_OUT_ANIMATION_TIME, + transition: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => (this._authList.reactive = true), + }); + } + + setChoiceList(promptMessage, choiceList) { + this._authList.clear(); + this._authList.label.text = promptMessage; + for (let key in choiceList) { + let text = choiceList[key]; + this._authList.addItem(key, text); + } + + this._entry.hide(); + if (this._message.text === '') + this._message.hide(); + this._fadeInChoiceList(); + } + getAnswer() { let text; if (this._preemptiveAnswer) { text = this._preemptiveAnswer; this._preemptiveAnswer = null; } else { text = this._entry.get_text(); } return text; } _fadeOutMessage() { if (this._message.opacity == 0) return; this._message.remove_all_transitions(); this._message.ease({ opacity: 0, duration: MESSAGE_FADE_OUT_ANIMATION_TIME, mode: Clutter.AnimationMode.EASE_OUT_QUAD, }); } setMessage(serviceName, message, type) { if (type == GdmUtil.MessageType.ERROR) this._message.add_style_class_name('login-dialog-message-warning'); else this._message.remove_style_class_name('login-dialog-message-warning'); if (type == GdmUtil.MessageType.HINT) this._message.add_style_class_name('login-dialog-message-hint'); else this._message.remove_style_class_name('login-dialog-message-hint'); + this._message.show(); if (message) { this._message.remove_all_transitions(); this._message.text = message; this._message.opacity = 255; } else { this._message.opacity = 0; } if (type === GdmUtil.MessageType.ERROR && this._userVerifier.serviceIsFingerprint(serviceName)) { // TODO: Use Await for wiggle to be over before unfreezing the user verifier queue const wiggleParameters = { duration: 65, wiggleCount: 3, }; this._userVerifier.increaseCurrentMessageTimeout( wiggleParameters.duration * (wiggleParameters.wiggleCount + 2)); Util.wiggle(this._message, wiggleParameters); } } updateSensitivity(sensitive) { if (this._entry.reactive === sensitive) return; this._entry.reactive = sensitive; if (sensitive) { this._entry.grab_key_focus(); } else { diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js index d2a82b43d..41dd99646 100644 --- a/js/gdm/loginDialog.js +++ b/js/gdm/loginDialog.js @@ -391,60 +391,65 @@ var SessionMenuButton = GObject.registerClass({ let item = new PopupMenu.PopupMenuItem(sessionName); this._menu.addMenuItem(item); this._items[id] = item; item.connect('activate', () => { this.setActiveSession(id); this.emit('session-activated', this._activeSessionId); }); } } }); var LoginDialog = GObject.registerClass({ Signals: { 'failed': {}, 'wake-up-screen': {}, }, }, class LoginDialog extends St.Widget { _init(parentActor) { super._init({ style_class: 'login-dialog', visible: false }); this.get_accessible().set_role(Atk.Role.WINDOW); this.add_constraint(new Layout.MonitorConstraint({ primary: true })); this.connect('destroy', this._onDestroy.bind(this)); parentActor.add_child(this); this._userManager = AccountsService.UserManager.get_default(); this._gdmClient = new Gdm.Client(); + try { + this._gdmClient.set_enabled_extensions([Gdm.UserVerifierChoiceList.interface_info().name]); + } catch (e) { + } + this._settings = new Gio.Settings({ schema_id: GdmUtil.LOGIN_SCREEN_SCHEMA }); this._settings.connect('changed::%s'.format(GdmUtil.BANNER_MESSAGE_KEY), this._updateBanner.bind(this)); this._settings.connect('changed::%s'.format(GdmUtil.BANNER_MESSAGE_TEXT_KEY), this._updateBanner.bind(this)); this._settings.connect('changed::%s'.format(GdmUtil.DISABLE_USER_LIST_KEY), this._updateDisableUserList.bind(this)); this._settings.connect('changed::%s'.format(GdmUtil.LOGO_KEY), this._updateLogo.bind(this)); this._textureCache = St.TextureCache.get_default(); this._updateLogoTextureId = this._textureCache.connect('texture-file-changed', this._updateLogoTexture.bind(this)); this._userSelectionBox = new St.BoxLayout({ style_class: 'login-dialog-user-selection-box', x_align: Clutter.ActorAlign.CENTER, y_align: Clutter.ActorAlign.CENTER, vertical: true, visible: false }); this.add_child(this._userSelectionBox); this._userList = new UserList(); this._userSelectionBox.add_child(this._userList); this._authPrompt = new AuthPrompt.AuthPrompt(this._gdmClient, AuthPrompt.AuthPromptMode.UNLOCK_OR_LOG_IN); this._authPrompt.connect('prompted', this._onPrompted.bind(this)); this._authPrompt.connect('reset', this._onReset.bind(this)); this._authPrompt.hide(); this.add_child(this._authPrompt); diff --git a/js/gdm/util.js b/js/gdm/util.js index e62114cb1..3f327400f 100644 --- a/js/gdm/util.js +++ b/js/gdm/util.js @@ -211,90 +211,98 @@ var ShellUserVerifier = class { 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.disconnect(this._smartcardInsertedId); this._smartcardManager.disconnect(this._smartcardRemovedId); this._smartcardManager = null; for (let service in this._credentialManagers) { let credentialManager = this._credentialManagers[service]; credentialManager.disconnect(credentialManager._authenticatedSignalId); credentialManager = null; } } + selectChoice(serviceName, key) { + this._userVerifierChoiceList.call_select_choice(serviceName, key, this._cancellable, null); + } + answerQuery(serviceName, answer) { if (!this.hasPendingMessages) { this._userVerifier.call_answer_query(serviceName, answer, this._cancellable, null); } else { const cancellable = this._cancellable; let signalId = this.connect('no-more-messages', () => { this.disconnect(signalId); if (!cancellable.is_cancelled()) this._userVerifier.call_answer_query(serviceName, answer, cancellable, null); }); } } _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'); } @@ -429,103 +437,116 @@ var ShellUserVerifier = class { _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._signalIds = []; let id = this._userVerifier.connect('info', this._onInfo.bind(this)); this._signalIds.push(id); id = this._userVerifier.connect('problem', this._onProblem.bind(this)); this._signalIds.push(id); id = this._userVerifier.connect('info-query', this._onInfoQuery.bind(this)); this._signalIds.push(id); id = this._userVerifier.connect('secret-info-query', this._onSecretInfoQuery.bind(this)); this._signalIds.push(id); id = this._userVerifier.connect('conversation-stopped', this._onConversationStopped.bind(this)); this._signalIds.push(id); id = this._userVerifier.connect('service-unavailable', this._onServiceUnavailable.bind(this)); this._signalIds.push(id); id = this._userVerifier.connect('reset', this._onReset.bind(this)); this._signalIds.push(id); id = this._userVerifier.connect('verification-complete', this._onVerificationComplete.bind(this)); this._signalIds.push(id); + + if (this._userVerifierChoiceList) + this._userVerifierChoiceList.connect('choice-query', this._onChoiceListQuery.bind(this)); } _disconnectSignals() { if (!this._signalIds || !this._userVerifier) return; this._signalIds.forEach(s => this._userVerifier.disconnect(s)); this._signalIds = []; } _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; } @@ -554,60 +575,67 @@ var ShellUserVerifier = class { } 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 %s for %s'.format(serviceName, this._userName)); this._hold.release(); return; } this._reportInitError(this._userName ? 'Failed to start %s verification for user'.format(serviceName) : 'Failed to start %s verification'.format(serviceName), 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.deep_unpack()); + } + _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) { diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js index 5b55cb08a..f4655b25b 100644 --- a/js/ui/unlockDialog.js +++ b/js/ui/unlockDialog.js @@ -466,60 +466,67 @@ class UnlockDialogLayout extends Clutter.LayoutManager { else actorBox.x1 = box.x2 - (natWidth * 2); actorBox.y1 = box.y2 - (natHeight * 2); actorBox.x2 = actorBox.x1 + natWidth; actorBox.y2 = actorBox.y1 + natHeight; this._switchUserButton.allocate(actorBox); } } }); var UnlockDialog = GObject.registerClass({ Signals: { 'failed': {}, 'wake-up-screen': {}, }, }, class UnlockDialog extends St.Widget { _init(parentActor) { super._init({ accessible_role: Atk.Role.WINDOW, style_class: 'unlock-dialog', visible: false, reactive: true, }); parentActor.add_child(this); this._gdmClient = new Gdm.Client(); + try { + this._gdmClient.set_enabled_extensions([ + Gdm.UserVerifierChoiceList.interface_info().name, + ]); + } catch (e) { + } + this._adjustment = new St.Adjustment({ actor: this, lower: 0, upper: 2, page_size: 1, page_increment: 1, }); this._adjustment.connect('notify::value', () => { this._setTransitionProgress(this._adjustment.value); }); this._swipeTracker = new SwipeTracker.SwipeTracker(this, Clutter.Orientation.VERTICAL, Shell.ActionMode.UNLOCK_SCREEN); this._swipeTracker.connect('begin', this._swipeBegin.bind(this)); this._swipeTracker.connect('update', this._swipeUpdate.bind(this)); this._swipeTracker.connect('end', this._swipeEnd.bind(this)); this.connect('scroll-event', (o, event) => { if (this._swipeTracker.canHandleScrollEvent(event)) return Clutter.EVENT_PROPAGATE; let direction = event.get_scroll_direction(); if (direction === Clutter.ScrollDirection.UP) this._showClock(); else if (direction === Clutter.ScrollDirection.DOWN) this._showPrompt(); return Clutter.EVENT_STOP; }); -- 2.34.1