/* -*- Mode: C; indent-tabs-mode: nil; c-basic-offset: 2; tab-width: 2 -*- */ /* * Copyright 2009 Canonical Ltd. * * Authors: * Cody Russell * Charles Kerr * * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License version 3, as published * by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranties of * MERCHANTABILITY, SATISFACTORY QUALITY, 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 . */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include /* getpwuid() */ #include "dbus-accounts.h" #include "dbus-login1-manager.h" #include "dbus-login1-session.h" #include "dbus-login1-user.h" #include "dbus-display-manager.h" #include "dbus-user.h" #include "shared-names.h" #include "users-service-dbus.h" #define LOGIND_ADDR "org.freedesktop.login1" /** *** **/ static void update_user_list (UsersServiceDbus * self); static gchar* get_seat (UsersServiceDbus * service); static void on_user_added (Accounts * o, const gchar * user_object_path, UsersServiceDbus * service); static void on_user_deleted (Accounts * o, const gchar * user_object_path, UsersServiceDbus * service); static void on_session_added (Login1Manager * proxy, const gchar * ssid, const gchar * path, UsersServiceDbus * service); static void on_session_removed (Login1Manager * proxy, const gchar * ssid, const gchar * path, UsersServiceDbus * service); static void on_session_list (Login1Manager * proxy, GAsyncResult * result, UsersServiceDbus * service); /*** **** Priv Struct ***/ struct _UsersServiceDbusPrivate { gchar * seat; gchar * guest_ssid; /* ssid -> AccountsUser lookup */ GHashTable * sessions; /* user object path -> AccountsUser lookup */ GHashTable * users; GCancellable * cancellable; Login1Manager * manager_proxy; Accounts * accounts_proxy; }; /*** **** GObject ***/ enum { USER_LIST_CHANGED, USER_LOGGED_IN_CHANGED, GUEST_LOGGED_IN_CHANGED, N_SIGNALS }; static guint signals[N_SIGNALS] = { 0 }; G_DEFINE_TYPE (UsersServiceDbus, users_service_dbus, G_TYPE_OBJECT); static void users_service_dbus_dispose (GObject *object) { UsersServiceDbusPrivate * priv = USERS_SERVICE_DBUS(object)->priv; g_clear_object (&priv->accounts_proxy); g_clear_object (&priv->manager_proxy); if (priv->cancellable != NULL) { g_cancellable_cancel (priv->cancellable); g_clear_object (&priv->cancellable); } if (priv->users != NULL) { g_hash_table_destroy (priv->users); priv->users = NULL; } if (priv->sessions != NULL) { g_hash_table_destroy (priv->sessions); priv->sessions = NULL; } G_OBJECT_CLASS (users_service_dbus_parent_class)->dispose (object); } static void users_service_dbus_finalize (GObject *object) { UsersServiceDbusPrivate * priv = USERS_SERVICE_DBUS(object)->priv; g_free (priv->guest_ssid); g_free (priv->seat); G_OBJECT_CLASS (users_service_dbus_parent_class)->finalize (object); } static void users_service_dbus_class_init (UsersServiceDbusClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); g_type_class_add_private (object_class, sizeof (UsersServiceDbusPrivate)); object_class->dispose = users_service_dbus_dispose; object_class->finalize = users_service_dbus_finalize; signals[USER_LIST_CHANGED] = g_signal_new ( "user-list-changed", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (UsersServiceDbusClass, user_list_changed), NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); signals[USER_LOGGED_IN_CHANGED] = g_signal_new ( "user-logged-in-changed", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (UsersServiceDbusClass, user_logged_in_changed), NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, G_TYPE_OBJECT); signals[GUEST_LOGGED_IN_CHANGED] = g_signal_new ( "guest-logged-in-changed", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, G_STRUCT_OFFSET (UsersServiceDbusClass, guest_logged_in_changed), NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); } static void users_service_dbus_init (UsersServiceDbus *self) { GError * error = NULL; self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, USERS_SERVICE_DBUS_TYPE, UsersServiceDbusPrivate); UsersServiceDbusPrivate * p = self->priv; p->cancellable = g_cancellable_new (); /* ssid -> AccountsUser */ p->sessions = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref); /* user object path -> AccountsUser */ p->users = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref); /** *** create the logind manager proxy... **/ p->manager_proxy = login1_manager_proxy_new_for_bus_sync ( G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, "org.freedesktop.login1", "/org/freedesktop/login1", NULL, &error); if (error != NULL) { g_warning ("%s: %s", G_STRLOC, error->message); g_clear_error (&error); } p->seat = get_seat (self); g_signal_connect (p->manager_proxy, "session-new", G_CALLBACK (on_session_added), self); g_signal_connect (p->manager_proxy, "session-removed", G_CALLBACK (on_session_removed), self); login1_manager_call_list_sessions (p->manager_proxy, p->cancellable, (GAsyncReadyCallback) on_session_list, self); /** *** create the accounts manager proxy... **/ Accounts * proxy = accounts_proxy_new_for_bus_sync ( G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, "org.freedesktop.Accounts", "/org/freedesktop/Accounts", NULL, &error); if (error != NULL) { g_warning ("%s: %s", G_STRFUNC, error->message); g_clear_error (&error); } else { g_signal_connect (proxy, "user-added", G_CALLBACK(on_user_added), self); g_signal_connect (proxy, "user-deleted", G_CALLBACK(on_user_deleted), self); p->accounts_proxy = proxy; update_user_list (self); } } /*** **** ***/ static void emit_user_list_changed (UsersServiceDbus * self) { g_signal_emit (self, signals[USER_LIST_CHANGED], 0); } static void emit_user_login_changed (UsersServiceDbus * self, AccountsUser * user) { g_signal_emit (self, signals[USER_LOGGED_IN_CHANGED], 0, user); } static void emit_guest_login_changed (UsersServiceDbus * self) { g_signal_emit (self, signals[GUEST_LOGGED_IN_CHANGED], 0); } /*** **** ***/ static Login1User* create_login1_user_proxy (const char * path) { GError * error = NULL; Login1User * p = login1_user_proxy_new_for_bus_sync ( G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, LOGIND_ADDR, path, NULL, &error); if (error != NULL) { g_warning ("%s: %s", G_STRLOC, error->message); g_error_free (error); } return p; } static Login1User * create_login1_user_proxy_from_uid (UsersServiceDbus * self, guint64 uid) { Login1Manager * manager = self->priv->manager_proxy; Login1User * user_proxy = NULL; gchar * user_object_path = NULL; GError * error = NULL; login1_manager_call_get_user_sync (manager, uid, &user_object_path, NULL, &error); if (error != NULL) { g_warning ("%s: %s", G_STRLOC, error->message); g_error_free (error); } if (user_object_path != NULL) user_proxy = create_login1_user_proxy (user_object_path); return user_proxy; } static Login1Session * create_login1_session_proxy (const char * path) { GError * error = NULL; Login1Session * p = login1_session_proxy_new_for_bus_sync ( G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, LOGIND_ADDR, path, NULL, &error); if (error != NULL) { g_warning ("%s: %s", G_STRLOC, error->message); g_error_free (error); } return p; } static gchar * get_seat_from_session_proxy (Login1Session * session_proxy) { gchar * seat; GVariant * seatobj = login1_session_get_seat (session_proxy); g_variant_get (seatobj, "(so)", &seat, NULL); return seat; } static const gchar * get_display_from_session_proxy (Login1Session * session_proxy) { const gchar * display; display = login1_session_get_display (session_proxy); return display; } static gchar * get_seat (UsersServiceDbus *service) { gchar * seat = NULL; gchar * path = NULL; GError * error = NULL; UsersServiceDbusPrivate * priv = service->priv; Login1Session * session_proxy = NULL; pid_t pid = getpid(); login1_manager_call_get_session_by_pid_sync (priv->manager_proxy, pid, &path, NULL, &error); if (error != NULL) { g_debug ("%s: %s", G_STRLOC, error->message); g_error_free (error); goto out; } session_proxy = create_login1_session_proxy (path); if (!session_proxy) { g_debug ("%s: Could't get session proxy object", G_STRLOC); } seat = get_seat_from_session_proxy (session_proxy); out: g_object_unref (session_proxy); return seat; } /*** **** AccountsUser add-ons for tracking sessions ***/ static GHashTable* user_get_sessions_hashset (AccountsUser * user) { static GQuark q = 0; if (G_UNLIKELY(!q)) { q = g_quark_from_static_string ("sessions"); } GObject * o = G_OBJECT (user); GHashTable * h = g_object_get_qdata (o, q); if (h == NULL) { h = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); g_object_set_qdata_full (o, q, h, (GDestroyNotify)g_hash_table_destroy); } return h; } static void user_add_session (AccountsUser * user, const char * ssid) { g_hash_table_add (user_get_sessions_hashset(user), g_strdup(ssid)); } static void user_remove_session (AccountsUser * user, const char * ssid) { g_hash_table_remove (user_get_sessions_hashset(user), ssid); } static guint user_count_sessions (AccountsUser * user) { return g_hash_table_size (user_get_sessions_hashset(user)); } /*** **** Users ***/ /* adds this user session to the user's and service's session tables */ static void add_user_session (UsersServiceDbus * service, AccountsUser * user, const gchar * ssid, const gchar * path) { Login1Session * session_proxy = create_login1_session_proxy (path); if (session_proxy != NULL) { UsersServiceDbusPrivate * priv = service->priv; gchar * seat = get_seat_from_session_proxy (session_proxy); /* is this session in our seat? */ if (seat && priv->seat && !g_strcmp0 (seat, priv->seat)) { /* does this session have a display? */ const gchar * display = get_display_from_session_proxy (session_proxy); const gboolean has_display = g_strcmp0 ("", display) != 0; if (has_display) { const gchar * username = accounts_user_get_user_name (user); g_debug ("%s adding %s's session '%s' to our tables", G_STRLOC, username, ssid); g_hash_table_insert (priv->sessions, g_strdup (ssid), g_object_ref (user)); user_add_session (user, ssid); } } g_free (seat); g_object_unref (session_proxy); } } /* calls add_user_session() for each of this user's sessions */ static void add_user_sessions (UsersServiceDbus *self, AccountsUser * user) { const guint64 uid = accounts_user_get_uid (user); const char * username = accounts_user_get_user_name (user); g_debug ("%s adding %s (%i)", G_STRLOC, username, (int)uid); GVariant * sessions = NULL; Login1User * user_proxy = create_login1_user_proxy_from_uid (self, uid); if (user_proxy != NULL) { sessions = login1_user_get_sessions (user_proxy); } if (sessions != NULL) { GVariantIter iter; g_variant_iter_init (&iter, sessions); gchar * id; gchar * object_path; while (g_variant_iter_loop (&iter, "(so)", &id, &object_path)) { g_debug ("%s adding %s's session %s", G_STRLOC, username, id); add_user_session (self, user, id, object_path); } } g_object_unref (user_proxy); } /* returns true if this property is one we use */ static gboolean is_interesting_user_property (const char * key) { return !g_strcmp0 (key, "IconFile") || !g_strcmp0 (key, "LoginFrequency") || !g_strcmp0 (key, "RealName") || !g_strcmp0 (key, "Uid") || !g_strcmp0 (key, "UserName"); } static void sync_user_properties (GDBusProxy * source, GDBusProxy * target) { gchar ** keys = g_dbus_proxy_get_cached_property_names (source); if (keys != NULL) { int i; GVariantBuilder builder; gboolean changed = FALSE; g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{sv}")); for (i=0; keys[i]; i++) { const gchar * const key = keys[i]; if (is_interesting_user_property (key)) { GVariant * oldval = g_dbus_proxy_get_cached_property (target, key); GVariant * newval = g_dbus_proxy_get_cached_property (source, key); /* all the properties we're interested in are basic types safe for g_variant_compare()... */ g_assert (g_variant_type_is_basic(g_variant_get_type(newval))); if (g_variant_compare (oldval, newval)) { changed = TRUE; g_dbus_proxy_set_cached_property (target, key, newval); g_variant_builder_add (&builder, "{sv}", key, newval); } g_variant_unref (newval); g_variant_unref (oldval); } } if (changed) { g_signal_emit_by_name (target, "g-properties-changed", g_variant_builder_end(&builder), keys); } g_variant_builder_clear (&builder); g_strfreev (keys); } } /** * The AccountsUserProxy's properties aren't being updated automatically * for some reason... the only update we get is the 'changed' signal. * This function is a workaround to update our User object's properties. */ static void on_user_changed (AccountsUser * user, UsersServiceDbus * service) { AccountsUser * tmp = accounts_user_proxy_new_for_bus_sync ( G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, "org.freedesktop.Accounts", g_dbus_proxy_get_object_path (G_DBUS_PROXY(user)), NULL, NULL); if (tmp != NULL) { sync_user_properties (G_DBUS_PROXY(tmp), G_DBUS_PROXY(user)); g_object_unref (tmp); } } static void add_user_from_object_path (UsersServiceDbus * self, const char * user_object_path) { GError * error = NULL; AccountsUser * user = accounts_user_proxy_new_for_bus_sync ( G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, "org.freedesktop.Accounts", user_object_path, NULL, &error); if (error != NULL) { g_warning ("%s: %s", G_STRLOC, error->message); g_clear_error (&error); } else { AccountsUser * prev = g_hash_table_lookup (self->priv->users, user_object_path); if (prev != NULL) /* we've already got this user... sync its properties */ { sync_user_properties (G_DBUS_PROXY(user), G_DBUS_PROXY(prev)); g_object_unref (user); user = prev; } else /* ooo, we got a new user */ { g_signal_connect (user, "changed", G_CALLBACK(on_user_changed), self); g_hash_table_insert (self->priv->users, g_strdup(user_object_path), user); } add_user_sessions (self, user); } } /* asks org.freedesktop.Accounts for a list of users and * calls add_user_from_object_path() on each of those users */ static void update_user_list (UsersServiceDbus *self) { g_return_if_fail(IS_USERS_SERVICE_DBUS(self)); GError * error = NULL; char ** object_paths = NULL; UsersServiceDbusPrivate * priv = self->priv; accounts_call_list_cached_users_sync (priv->accounts_proxy, &object_paths, NULL, &error); if (error != NULL) { g_warning ("%s: %s", G_STRFUNC, error->message); g_clear_error (&error); } else if (object_paths != NULL) { gint i; for (i=0; object_paths[i] != NULL; ++i) { add_user_from_object_path (self, object_paths[i]); } emit_user_list_changed (self); g_strfreev (object_paths); } g_debug ("%s finished updating the user list", G_STRLOC); } static void on_user_added (Accounts * o G_GNUC_UNUSED, const gchar * user_path G_GNUC_UNUSED, UsersServiceDbus * service) { /* We see a new user but we might not want to list it -- for example, lightdm shows up when we switch to the greeter. So instead of adding the user directly here, let's ask org.freedesktop.Accounts for a fresh list of users because it filters out special cases. */ update_user_list (service); } static void on_user_deleted (Accounts * o G_GNUC_UNUSED, const gchar * user_path, UsersServiceDbus * service) { AccountsUser * user = g_hash_table_lookup (service->priv->users, user_path); if (user != NULL) { GObject * o = g_object_ref (G_OBJECT(user)); g_hash_table_remove (service->priv->users, user_path); emit_user_list_changed (service); g_object_unref (o); } } static AccountsUser * find_user_from_username (UsersServiceDbus * self, const gchar * username) { AccountsUser * match = NULL; g_return_val_if_fail (IS_USERS_SERVICE_DBUS(self), match); gpointer user; GHashTableIter iter; g_hash_table_iter_init (&iter, self->priv->users); while (!match && g_hash_table_iter_next (&iter, NULL, &user)) { if (!g_strcmp0 (username, accounts_user_get_user_name (user))) { match = user; } } return match; } /*** **** Sessions ***/ static void on_session_removed (Login1Manager * proxy, const gchar * ssid, const gchar * path, UsersServiceDbus * service) { g_return_if_fail (IS_USERS_SERVICE_DBUS (service)); UsersServiceDbusPrivate * priv = service->priv; g_debug ("%s %s() session removed %s", G_STRLOC, G_STRFUNC, ssid); if (!g_strcmp0 (ssid, priv->guest_ssid)) { g_debug ("%s removing guest session %s", G_STRLOC, ssid); g_clear_pointer (&priv->guest_ssid, g_free); emit_guest_login_changed (service); } else { AccountsUser * user = g_hash_table_lookup (priv->sessions, ssid); if (user == NULL) { g_debug ("%s we're not tracking ssid %s", G_STRLOC, ssid); } else { GObject * o = g_object_ref (G_OBJECT(user)); g_hash_table_remove (service->priv->users, ssid); user_remove_session (user, ssid); emit_user_login_changed (service, user); g_object_unref (o); } } } static gchar* get_unix_username_from_path (UsersServiceDbus * self, const gchar * path) { Login1Session * session_proxy = create_login1_session_proxy (path); if (session_proxy != NULL) { gchar * username = g_strdup (login1_session_get_name (session_proxy)); g_debug ("%s Getting username for %s: %s", G_STRLOC, path, username); g_object_unref (session_proxy); return username; } else { return NULL; } } static gboolean is_guest_username (const char * username) { if (!g_strcmp0 (username, "guest")) return TRUE; if (username && g_str_has_prefix (username, "guest-")) return TRUE; return FALSE; } /* If the new session belongs to 'guest', update our guest_ssid. Otherwise, call add_user_session() to update our session tables */ static void on_session_added (Login1Manager * proxy G_GNUC_UNUSED, const gchar * ssid, const gchar * path, UsersServiceDbus * service) { g_return_if_fail (IS_USERS_SERVICE_DBUS(service)); gchar * username = get_unix_username_from_path (service, path); g_debug ("%s %s() username %s has new session %s", G_STRLOC, G_STRFUNC, username, ssid); if (is_guest_username (username)) { /* handle guest as a special case -- it's not in the GDM user tables and there isn't be an AccountsUser for it */ g_debug("Found guest session: %s", ssid); g_free (service->priv->guest_ssid); service->priv->guest_ssid = g_strdup (ssid); emit_guest_login_changed (service); } else { AccountsUser * user = find_user_from_username (service, username); if (user != NULL) { add_user_session (service, user, ssid, path); emit_user_login_changed (service, user); } } } /* Receives a list of sessions and calls on_session_added() for each of them */ static void on_session_list (Login1Manager * proxy, GAsyncResult * result, UsersServiceDbus * self) { GError * error = NULL; GVariant * sessions; g_debug ("%s bootstrapping the session list", G_STRLOC); login1_manager_call_list_sessions_finish (proxy, &sessions, result, &error); if (error != NULL) { g_debug ("%s: %s", G_STRLOC, error->message); g_error_free (error); } else { GVariantIter * iter; gchar * seat; gchar * path; g_variant_get (sessions, "a(susso)", &iter); while (g_variant_iter_loop (iter, "(susso)", NULL, NULL, NULL, &seat, &path)) { if (g_strcmp0 (seat, self->priv->seat) == 0) { g_debug ("%s adding initial session '%s'", G_STRLOC, path); on_session_added (proxy, seat, path, self); } } g_variant_iter_free (iter); g_variant_unref (sessions); } g_debug ("%s done bootstrapping the session list", G_STRLOC); } static DisplayManagerSeat * create_display_proxy (UsersServiceDbus * self) { const gchar * const seat = g_getenv ("XDG_SEAT_PATH"); g_debug ("%s creating a DisplayManager proxy for seat %s", G_STRLOC, seat); GError * error = NULL; DisplayManagerSeat * p = display_manager_seat_proxy_new_for_bus_sync ( G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, "org.freedesktop.DisplayManager", seat, NULL, &error); if (error != NULL) { g_warning ("%s: %s", G_STRLOC, error->message); g_error_free (error); } return p; } /*** **** Public API ***/ /** * users_service_dbus_get_user_list: * * Returns: (transfer container): a list of AccountsUser objects */ GList * users_service_dbus_get_user_list (UsersServiceDbus * self) { g_return_val_if_fail(IS_USERS_SERVICE_DBUS(self), NULL); return g_hash_table_get_values (self->priv->users); } /** * users_service_dbus_show_greeter: * * Ask the Display Mnaager to switch to the greeter screen. */ void users_service_dbus_show_greeter (UsersServiceDbus * self) { DisplayManagerSeat * dp; g_return_if_fail (IS_USERS_SERVICE_DBUS(self)); dp = create_display_proxy (self); if (dp != NULL) { display_manager_seat_call_switch_to_greeter_sync (dp, NULL, NULL); g_clear_object (&dp); } } /** * users_service_dbus_activate_guest_session: * * Activates the guest account. */ void users_service_dbus_activate_guest_session (UsersServiceDbus * self) { DisplayManagerSeat * dp; g_return_if_fail (IS_USERS_SERVICE_DBUS(self)); dp = create_display_proxy (self); if (dp != NULL) { display_manager_seat_call_switch_to_guest_sync (dp, "", NULL, NULL); g_clear_object (&dp); } } /** * users_service_dbus_activate_user_session: * * Activates a specific user. */ void users_service_dbus_activate_user_session (UsersServiceDbus * self, AccountsUser * user) { DisplayManagerSeat * dp; g_return_if_fail (IS_USERS_SERVICE_DBUS(self)); dp = create_display_proxy (self); if (dp != NULL) { const char * const username = accounts_user_get_user_name (user); display_manager_seat_call_switch_to_user_sync (dp, username, "", NULL, NULL); g_clear_object (&dp); } } /** * users_service_dbus_guest_session_enabled: * * Tells whether or not guest sessions are allowed. */ gboolean users_service_dbus_guest_session_enabled (UsersServiceDbus * self) { DisplayManagerSeat * dp; gboolean enabled = FALSE; g_return_val_if_fail (IS_USERS_SERVICE_DBUS(self), enabled); dp = create_display_proxy (self); if (dp != NULL) { enabled = display_manager_seat_get_has_guest_account (dp); g_clear_object (&dp); } return enabled; } gboolean users_service_dbus_is_guest_logged_in (UsersServiceDbus * self) { g_return_val_if_fail (IS_USERS_SERVICE_DBUS(self), FALSE); return self->priv->guest_ssid != NULL; } gboolean users_service_dbus_is_user_logged_in (UsersServiceDbus * self, AccountsUser * user) { g_return_val_if_fail (IS_USERS_SERVICE_DBUS(self), FALSE); g_return_val_if_fail (IS_ACCOUNTS_USER(user), FALSE); return user_count_sessions (user) > 0; }