gnome-shell/js/ui/screenShield.js

704 lines
23 KiB
JavaScript
Raw Normal View History

// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
/* exported ScreenShield */
const {
AccountsService, Clutter, Gio,
GLib, Graphene, Meta, Shell, St,
} = imports.gi;
const Signals = imports.misc.signals;
const GnomeSession = imports.misc.gnomeSession;
gdm: support pre-authenticated logins from oVirt oVirt is software for managing medium-to-large scale deployments of virtual machine guests across multiple hosts. It supports a feature where users can authenticate with a central server and get transparently connected to a guest system and then automatically get logged into that guest to an associated user session. Guests using old versions of GDM support this single-sign-on capability by means of a greeter plugin, using the old greeter's extension API. This commit adds similar support to the gnome-shell based login screen. How it works: * The OVirtCredentialsManager singleton listens for 'org.ovirt.vdsm.Credentials.UserAuthenticated' D-Bus signal on the system bus from the 'org.ovirt.vdsm.Credentials' bus name. The service that provides that bus name is called the oVirt guest agent. It is also responsible for interacting with the the central server to get user credentials. * This UserAuthenticated signal passes, as a parameter, the a token which needs to be passed through to the PAM service that is specifically set up to integrate with the oVirt authentication architecture. The singleton object keeps the token internally so it can be queried later on. * The OVirtCredentialsManager emits a signal 'user-authenticated' on it's object once the dbus signal is triggered * When the 'user-authenticated' signal is emitted, the login screen tells GDM to start user verification using the PAM service. The authentication stack of the service includes a PAM module provided by oVirt that securely retrieves user credentials from the oVirt guest agent. The PAM module then forwards those credentials on to other modules in the stack so, e.g., the user's gnome keyring can be automatically unlocked. * In case of the screen shield being visible, it also will react on that 'user-authenticated' signal and lift the shield. In that case the login screen will check on construction time if the signal has already been triggered, and a token is available. If a token is available it will immediately trigger the functionality as described above. Signed-off-by: Vinzenz Feenstra <evilissimo@redhat.com> https://bugzilla.gnome.org/show_bug.cgi?id=702162
2013-10-10 04:21:47 -04:00
const OVirt = imports.gdm.oVirt;
const LoginManager = imports.misc.loginManager;
const Lightbox = imports.ui.lightbox;
const Main = imports.ui.main;
const Overview = imports.ui.overview;
const MessageTray = imports.ui.messageTray;
const ShellDBus = imports.ui.shellDBus;
const SmartcardManager = imports.misc.smartcardManager;
const { adjustAnimationTime } = imports.ui.environment;
const SCREENSAVER_SCHEMA = 'org.gnome.desktop.screensaver';
const LOCK_ENABLED_KEY = 'lock-enabled';
const LOCK_DELAY_KEY = 'lock-delay';
const LOCKDOWN_SCHEMA = 'org.gnome.desktop.lockdown';
const DISABLE_LOCK_KEY = 'disable-lock-screen';
const LOCKED_STATE_STR = 'screenShield.locked';
// ScreenShield animation time
// - STANDARD_FADE_TIME is used when the session goes idle
// - MANUAL_FADE_TIME is used for lowering the shield when asked by the user,
// or when cancelling the dialog
// - CURTAIN_SLIDE_TIME is used when raising the shield before unlocking
var STANDARD_FADE_TIME = 10000;
var MANUAL_FADE_TIME = 300;
var CURTAIN_SLIDE_TIME = 300;
/**
* If you are setting org.gnome.desktop.session.idle-delay directly in dconf,
* rather than through System Settings, you also need to set
* org.gnome.settings-daemon.plugins.power.sleep-display-ac and
* org.gnome.settings-daemon.plugins.power.sleep-display-battery to the same value.
* This will ensure that the screen blanks at the right time when it fades out.
* https://bugzilla.gnome.org/show_bug.cgi?id=668703 explains the dependency.
*/
var ScreenShield = class extends Signals.EventEmitter {
constructor() {
super();
this.actor = Main.layoutManager.screenShieldGroup;
this._lockScreenState = MessageTray.State.HIDDEN;
this._lockScreenGroup = new St.Widget({
x_expand: true,
y_expand: true,
reactive: true,
can_focus: true,
name: 'lockScreenGroup',
visible: false,
});
this._lockDialogGroup = new St.Widget({
x_expand: true,
y_expand: true,
reactive: true,
can_focus: true,
pivot_point: new Graphene.Point({ x: 0.5, y: 0.5 }),
name: 'lockDialogGroup',
});
this.actor.add_actor(this._lockScreenGroup);
this.actor.add_actor(this._lockDialogGroup);
this._presence = new GnomeSession.Presence((proxy, error) => {
if (error) {
logError(error, 'Error while reading gnome-session presence');
return;
}
this._onStatusChanged(proxy.status);
});
this._presence.connectSignal('StatusChanged', (proxy, senderName, [status]) => {
this._onStatusChanged(status);
});
this._screenSaverDBus = new ShellDBus.ScreenSaverDBus(this);
this._smartcardManager = SmartcardManager.getSmartcardManager();
this._smartcardManager.connect('smartcard-inserted',
(manager, token) => {
if (this._isLocked && token.UsedToLogin)
this._activateDialog();
});
this._credentialManagers = {};
this.addCredentialManager(OVirt.SERVICE_NAME, OVirt.getOVirtCredentialsManager());
gdm: support pre-authenticated logins from oVirt oVirt is software for managing medium-to-large scale deployments of virtual machine guests across multiple hosts. It supports a feature where users can authenticate with a central server and get transparently connected to a guest system and then automatically get logged into that guest to an associated user session. Guests using old versions of GDM support this single-sign-on capability by means of a greeter plugin, using the old greeter's extension API. This commit adds similar support to the gnome-shell based login screen. How it works: * The OVirtCredentialsManager singleton listens for 'org.ovirt.vdsm.Credentials.UserAuthenticated' D-Bus signal on the system bus from the 'org.ovirt.vdsm.Credentials' bus name. The service that provides that bus name is called the oVirt guest agent. It is also responsible for interacting with the the central server to get user credentials. * This UserAuthenticated signal passes, as a parameter, the a token which needs to be passed through to the PAM service that is specifically set up to integrate with the oVirt authentication architecture. The singleton object keeps the token internally so it can be queried later on. * The OVirtCredentialsManager emits a signal 'user-authenticated' on it's object once the dbus signal is triggered * When the 'user-authenticated' signal is emitted, the login screen tells GDM to start user verification using the PAM service. The authentication stack of the service includes a PAM module provided by oVirt that securely retrieves user credentials from the oVirt guest agent. The PAM module then forwards those credentials on to other modules in the stack so, e.g., the user's gnome keyring can be automatically unlocked. * In case of the screen shield being visible, it also will react on that 'user-authenticated' signal and lift the shield. In that case the login screen will check on construction time if the signal has already been triggered, and a token is available. If a token is available it will immediately trigger the functionality as described above. Signed-off-by: Vinzenz Feenstra <evilissimo@redhat.com> https://bugzilla.gnome.org/show_bug.cgi?id=702162
2013-10-10 04:21:47 -04:00
this._loginManager = LoginManager.getLoginManager();
this._loginManager.connect('prepare-for-sleep',
this._prepareForSleep.bind(this));
this._loginSession = null;
this._getLoginSession();
this._settings = new Gio.Settings({ schema_id: SCREENSAVER_SCHEMA });
this._settings.connect(`changed::${LOCK_ENABLED_KEY}`, this._syncInhibitor.bind(this));
this._lockSettings = new Gio.Settings({ schema_id: LOCKDOWN_SCHEMA });
this._lockSettings.connect(`changed::${DISABLE_LOCK_KEY}`, this._syncInhibitor.bind(this));
this._isModal = false;
this._isGreeter = false;
this._isActive = false;
this._isLocked = false;
this._inUnlockAnimation = false;
this._inhibited = false;
this._activationTime = 0;
this._becameActiveId = 0;
this._lockTimeoutId = 0;
// The "long" lightbox is used for the longer (20 seconds) fade from session
// to idle status, the "short" is used for quickly fading to black when locking
// manually
this._longLightbox = new Lightbox.Lightbox(Main.uiGroup, {
inhibitEvents: true,
fadeFactor: 1,
});
this._longLightbox.connect('notify::active', this._onLongLightbox.bind(this));
this._shortLightbox = new Lightbox.Lightbox(Main.uiGroup, {
inhibitEvents: true,
fadeFactor: 1,
});
this._shortLightbox.connect('notify::active', this._onShortLightbox.bind(this));
this.idleMonitor = global.backend.get_core_idle_monitor();
this._cursorTracker = Meta.CursorTracker.get_for_display(global.display);
this._syncInhibitor();
}
async _getLoginSession() {
this._loginSession = await this._loginManager.getCurrentSessionProxy();
this._loginSession.connectSignal('Lock',
() => this.lock(false));
this._loginSession.connectSignal('Unlock',
() => this.deactivate(false));
this._loginSession.connect('g-properties-changed',
() => this._syncInhibitor());
this._syncInhibitor();
}
_setActive(active) {
let prevIsActive = this._isActive;
this._isActive = active;
if (prevIsActive != this._isActive)
this.emit('active-changed');
this._syncInhibitor();
}
_setLocked(locked) {
let prevIsLocked = this._isLocked;
this._isLocked = locked;
if (prevIsLocked !== this._isLocked)
this.emit('locked-changed');
if (this._loginSession)
this._loginSession.SetLockedHintAsync(locked).catch(logError);
}
_activateDialog() {
if (this._isLocked) {
this._ensureUnlockDialog(true /* allowCancel */);
this._dialog.activate();
} else {
this.deactivate(true /* animate */);
}
}
_maybeCancelDialog() {
if (!this._dialog)
return;
this._dialog.cancel();
if (this._isGreeter) {
// LoginDialog.cancel() will grab the key focus
// on its own, so ensure it stays on lock screen
// instead
this._dialog.grab_key_focus();
}
}
_becomeModal() {
if (this._isModal)
return true;
let grab = Main.pushModal(Main.uiGroup, { actionMode: Shell.ActionMode.LOCK_SCREEN });
// We expect at least a keyboard grab here
this._isModal = (grab.get_seat_state() & Clutter.GrabState.KEYBOARD) !== 0;
if (this._isModal)
this._grab = grab;
else
Main.popModal(grab);
return this._isModal;
}
async _syncInhibitor() {
const lockEnabled = this._settings.get_boolean(LOCK_ENABLED_KEY);
const lockLocked = this._lockSettings.get_boolean(DISABLE_LOCK_KEY);
const inhibit = !!this._loginSession && this._loginSession.Active &&
!this._isActive && lockEnabled && !lockLocked &&
!!Main.sessionMode.unlockDialog;
if (inhibit === this._inhibited)
return;
this._inhibited = inhibit;
this._inhibitCancellable?.cancel();
this._inhibitCancellable = new Gio.Cancellable();
if (inhibit) {
try {
this._inhibitor = await this._loginManager.inhibit(
_('GNOME needs to lock the screen'),
this._inhibitCancellable);
} catch (e) {
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED))
log('Failed to inhibit suspend: %s'.format(e.message));
}
} else {
this._inhibitor?.close(null);
this._inhibitor = null;
}
}
_prepareForSleep(loginManager, aboutToSuspend) {
if (aboutToSuspend) {
if (this._settings.get_boolean(LOCK_ENABLED_KEY))
this.lock(true);
} else {
this._wakeUpScreen();
}
}
_onStatusChanged(status) {
if (status != GnomeSession.PresenceStatus.IDLE)
return;
this._maybeCancelDialog();
if (this._longLightbox.visible) {
// We're in the process of showing.
return;
}
if (!this._becomeModal()) {
// We could not become modal, so we can't activate the
// screenshield. The user is probably very upset at this
// point, but any application using global grabs is broken
// Just tell them to stop using this app
//
// XXX: another option is to kick the user into the gdm login
// screen, where we're not affected by grabs
Main.notifyError(_("Unable to lock"),
_("Lock was blocked by an application"));
return;
}
if (this._activationTime == 0)
this._activationTime = GLib.get_monotonic_time();
let shouldLock = this._settings.get_boolean(LOCK_ENABLED_KEY) && !this._isLocked;
if (shouldLock) {
let lockTimeout = Math.max(
adjustAnimationTime(STANDARD_FADE_TIME),
this._settings.get_uint(LOCK_DELAY_KEY) * 1000);
this._lockTimeoutId = GLib.timeout_add(
GLib.PRIORITY_DEFAULT,
lockTimeout,
() => {
this._lockTimeoutId = 0;
this.lock(false);
return GLib.SOURCE_REMOVE;
});
GLib.Source.set_name_by_id(this._lockTimeoutId, '[gnome-shell] this.lock');
}
this._activateFade(this._longLightbox, STANDARD_FADE_TIME);
}
_activateFade(lightbox, time) {
Main.uiGroup.set_child_above_sibling(lightbox, null);
lightbox.lightOn(time);
if (this._becameActiveId == 0)
this._becameActiveId = this.idleMonitor.add_user_active_watch(this._onUserBecameActive.bind(this));
}
_onUserBecameActive() {
// This function gets called here when the user becomes active
// after we activated a lightbox
// There are two possibilities here:
// - we're called when already locked; we just go back to the lock screen curtain
// - we're called because the session is IDLE but before the lightbox
// is fully shown; at this point isActive is false, so we just hide
// the lightbox, reset the activationTime and go back to the unlocked
// desktop
// using deactivate() is a little of overkill, but it ensures we
// don't forget of some bit like modal, DBus properties or idle watches
//
// Note: if the (long) lightbox is shown then we're necessarily
// active, because we call activate() without animation.
this.idleMonitor.remove_watch(this._becameActiveId);
this._becameActiveId = 0;
if (this._isLocked) {
this._longLightbox.lightOff();
this._shortLightbox.lightOff();
} else {
this.deactivate(false);
}
}
_onLongLightbox(lightBox) {
if (lightBox.active)
this.activate(false);
}
_onShortLightbox(lightBox) {
if (lightBox.active)
this._completeLockScreenShown();
}
showDialog() {
if (!this._becomeModal()) {
// In the login screen, this is a hard error. Fail-whale
const error = new GLib.Error(
Gio.IOErrorEnum, Gio.IOErrorEnum.FAILED,
'Could not acquire modal grab for the login screen. Aborting login process.');
global.context.terminate_with_error(error);
}
this.actor.show();
this._isGreeter = Main.sessionMode.isGreeter;
this._isLocked = true;
this._ensureUnlockDialog(true);
}
_hideLockScreenComplete() {
this._lockScreenState = MessageTray.State.HIDDEN;
this._lockScreenGroup.hide();
if (this._dialog) {
this._dialog.grab_key_focus();
this._dialog.navigate_focus(null, St.DirectionType.TAB_FORWARD, false);
}
}
_showPointer() {
this._cursorTracker.set_pointer_visible(true);
if (this._motionId) {
global.stage.disconnect(this._motionId);
this._motionId = 0;
}
}
_hidePointerUntilMotion() {
this._motionId = global.stage.connect('captured-event', (stage, event) => {
if (event.type() === Clutter.EventType.MOTION)
this._showPointer();
return Clutter.EVENT_PROPAGATE;
});
this._cursorTracker.set_pointer_visible(false);
}
_hideLockScreen(animate) {
if (this._lockScreenState == MessageTray.State.HIDDEN)
return;
this._lockScreenState = MessageTray.State.HIDING;
this._lockDialogGroup.remove_all_transitions();
if (animate) {
// Animate the lock screen out of screen
// if velocity is not specified (i.e. we come here from pressing ESC),
// use the same speed regardless of original position
// if velocity is specified, it's in pixels per milliseconds
let h = global.stage.height;
let delta = h + this._lockDialogGroup.translation_y;
let velocity = global.stage.height / CURTAIN_SLIDE_TIME;
let duration = delta / velocity;
this._lockDialogGroup.ease({
translation_y: -h,
duration,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onComplete: () => this._hideLockScreenComplete(),
});
} else {
this._hideLockScreenComplete();
}
this._showPointer();
}
_ensureUnlockDialog(allowCancel) {
if (!this._dialog) {
let constructor = Main.sessionMode.unlockDialog;
if (!constructor) {
// This session mode has no locking capabilities
this.deactivate(true);
return false;
}
this._dialog = new constructor(this._lockDialogGroup);
let time = global.get_current_time();
if (!this._dialog.open(time)) {
// This is kind of an impossible error: we're already modal
// by the time we reach this...
log('Could not open login dialog: failed to acquire grab');
this.deactivate(true);
return false;
}
this._dialog.connect('failed', this._onUnlockFailed.bind(this));
this._wakeUpScreenId = this._dialog.connect(
'wake-up-screen', this._wakeUpScreen.bind(this));
}
this._dialog.allowCancel = allowCancel;
this._dialog.grab_key_focus();
return true;
}
_onUnlockFailed() {
this._resetLockScreen({
animateLockScreen: true,
fadeToBlack: false,
});
}
_resetLockScreen(params) {
// Don't reset the lock screen unless it is completely hidden
// This prevents the shield going down if the lock-delay timeout
// fires while the user is dragging (which has the potential
// to confuse our state)
if (this._lockScreenState != MessageTray.State.HIDDEN)
return;
this._lockScreenGroup.show();
this._lockScreenState = MessageTray.State.SHOWING;
let fadeToBlack = params.fadeToBlack;
if (params.animateLockScreen) {
this._lockDialogGroup.translation_y = -global.screen_height;
this._lockDialogGroup.remove_all_transitions();
this._lockDialogGroup.ease({
translation_y: 0,
duration: Overview.ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onComplete: () => {
this._lockScreenShown({ fadeToBlack, animateFade: true });
},
});
} else {
this._lockDialogGroup.translation_y = 0;
this._lockScreenShown({ fadeToBlack, animateFade: false });
}
this._dialog.grab_key_focus();
}
_lockScreenShown(params) {
this._hidePointerUntilMotion();
this._lockScreenState = MessageTray.State.SHOWN;
if (params.fadeToBlack && params.animateFade) {
// Take a beat
let id = GLib.timeout_add(GLib.PRIORITY_DEFAULT, MANUAL_FADE_TIME, () => {
this._activateFade(this._shortLightbox, MANUAL_FADE_TIME);
return GLib.SOURCE_REMOVE;
});
GLib.Source.set_name_by_id(id, '[gnome-shell] this._activateFade');
} else {
if (params.fadeToBlack)
this._activateFade(this._shortLightbox, 0);
this._completeLockScreenShown();
}
}
_completeLockScreenShown() {
this._setActive(true);
this.emit('lock-screen-shown');
}
_wakeUpScreen() {
if (!this.active)
return; // already woken up, or not yet asleep
this._onUserBecameActive();
this.emit('wake-up-screen');
}
get locked() {
return this._isLocked;
}
get active() {
return this._isActive;
}
get activationTime() {
return this._activationTime;
}
deactivate(animate) {
if (this._dialog)
this._dialog.finish(() => this._continueDeactivate(animate));
else
this._continueDeactivate(animate);
}
_continueDeactivate(animate) {
this._hideLockScreen(animate);
if (Main.sessionMode.currentMode == 'unlock-dialog')
Main.sessionMode.popMode('unlock-dialog');
this.emit('wake-up-screen');
if (this._isGreeter) {
// We don't want to "deactivate" any more than
// this. In particular, we don't want to drop
// the modal, hide ourselves or destroy the dialog
// But we do want to set isActive to false, so that
// gnome-session will reset the idle counter, and
// gnome-settings-daemon will stop blanking the screen
this._activationTime = 0;
this._setActive(false);
return;
}
if (this._dialog && !this._isGreeter)
this._dialog.popModal();
if (this._isModal) {
Main.popModal(this._grab);
this._grab = null;
this._isModal = false;
}
this._longLightbox.lightOff();
this._shortLightbox.lightOff();
this._lockDialogGroup.ease({
translation_y: -global.screen_height,
duration: Overview.ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onComplete: () => this._completeDeactivate(),
});
}
_completeDeactivate() {
if (this._dialog) {
this._dialog.destroy();
this._dialog = null;
}
this.actor.hide();
if (this._becameActiveId != 0) {
this.idleMonitor.remove_watch(this._becameActiveId);
this._becameActiveId = 0;
}
if (this._lockTimeoutId != 0) {
GLib.source_remove(this._lockTimeoutId);
this._lockTimeoutId = 0;
}
this._activationTime = 0;
this._setActive(false);
this._setLocked(false);
global.set_runtime_state(LOCKED_STATE_STR, null);
}
activate(animate) {
if (this._activationTime == 0)
this._activationTime = GLib.get_monotonic_time();
if (!this._ensureUnlockDialog(true))
return;
this.actor.show();
if (Main.sessionMode.currentMode !== 'unlock-dialog') {
this._isGreeter = Main.sessionMode.isGreeter;
if (!this._isGreeter)
Main.sessionMode.pushMode('unlock-dialog');
}
this._resetLockScreen({
animateLockScreen: animate,
fadeToBlack: true,
});
// On wayland, a crash brings down the entire session, so we don't
// need to defend against being restarted unlocked
if (!Meta.is_wayland_compositor())
global.set_runtime_state(LOCKED_STATE_STR, GLib.Variant.new('b', true));
// We used to set isActive and emit active-changed here,
// but now we do that from lockScreenShown, which means
// there is a 0.3 seconds window during which the lock
// screen is effectively visible and the screen is locked, but
// the DBus interface reports the screensaver is off.
// This is because when we emit ActiveChanged(true),
// gnome-settings-daemon blanks the screen, and we don't want
// blank during the animation.
// This is not a problem for the idle fade case, because we
// activate without animation in that case.
}
addCredentialManager(serviceName, credentialManager) {
if (this._credentialManagers[serviceName])
return;
this._credentialManagers[serviceName] = credentialManager;
credentialManager.connectObject('user-authenticated', () => {
if (this._isLocked)
this._activateDialog();
}, this);
}
removeCredentialManager(serviceName) {
let credentialManager = this._credentialManagers[serviceName];
if (!credentialManager)
return;
credentialManager.disconnectObject(this);
delete this._credentialManagers[serviceName];
}
lock(animate) {
if (this._lockSettings.get_boolean(DISABLE_LOCK_KEY)) {
log('Screen lock is locked down, not locking'); // lock, lock - who's there?
return;
}
// Warn the user if we can't become modal
if (!this._becomeModal()) {
Main.notifyError(_("Unable to lock"),
_("Lock was blocked by an application"));
return;
}
// Clear the clipboard - otherwise, its contents may be leaked
// to unauthorized parties by pasting into the unlock dialog's
// password entry and unmasking the entry
St.Clipboard.get_default().set_text(St.ClipboardType.CLIPBOARD, '');
St.Clipboard.get_default().set_text(St.ClipboardType.PRIMARY, '');
let userManager = AccountsService.UserManager.get_default();
let user = userManager.get_user(GLib.get_user_name());
this.activate(animate);
const lock = this._isGreeter
? true
: user.password_mode !== AccountsService.UserPasswordMode.NONE;
this._setLocked(lock);
}
// If the previous shell crashed, and gnome-session restarted us, then re-lock
lockIfWasLocked() {
if (!this._settings.get_boolean(LOCK_ENABLED_KEY))
return;
let wasLocked = global.get_runtime_state('b', LOCKED_STATE_STR);
if (wasLocked === null)
return;
const laters = global.compositor.get_laters();
laters.add(Meta.LaterType.BEFORE_REDRAW, () => {
this.lock(false);
return GLib.SOURCE_REMOVE;
});
}
};