From 95c9f630f8ab52a344ca3fbfa690307a5cda1daf Mon Sep 17 00:00:00 2001 From: Ryan Lortie Date: Wed, 17 Apr 2013 12:33:14 -0400 Subject: [PATCH 2/2] service: add support for extension interfaces First pass at what a patch might look like. Requires the new GDBus async property handling changes. https://bugs.freedesktop.org/show_bug.cgi?id=63733 --- src/daemon.c | 173 +++++++++++++++++++++++++++++++++++++++++ src/daemon.h | 2 + src/user.c | 250 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 425 insertions(+) diff --git a/src/daemon.c b/src/daemon.c index 89ce39d..fa1a8a2 100644 --- a/src/daemon.c +++ b/src/daemon.c @@ -1,6 +1,7 @@ /* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- * * Copyright (C) 2009-2010 Red Hat, Inc. + * Copyright © 2013 Canonical Limited * * 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 @@ -102,6 +103,7 @@ struct DaemonPrivate { guint autologin_id; PolkitAuthority *authority; + GHashTable *extension_ifaces; }; typedef struct passwd * (* EntryGeneratorFunc) (GHashTable *, gpointer *); @@ -733,6 +735,167 @@ on_gdm_monitor_changed (GFileMonitor *monitor, queue_reload_autologin (daemon); } +static gchar * +read_symlink (const gchar *filename) +{ + gchar buffer[1024]; + gssize s; + + /* Try with a fixed-sized buffer. This will almost always be + * sufficient. If the string is too big, we do more complicated + * handling below. + */ + s = readlink (filename, buffer, sizeof buffer); + + if (s < 0) + return NULL; + + /* readlink() is a bad interface. Instead of telling you how + * many bytes would have been required in case the filename gets + * truncated, it simply returns the truncated size. It is not + * possible to tell the difference between a filename that + * filled the buffer exactly and one that got truncated. + * + * For this reason, you must always 'waste' one buffer byte. + * + * Check that we're strictly less than the buffer size to ensure + * we're not in the truncated case. + */ + if G_LIKELY (s < (int) sizeof buffer) { + /* Since we waste the byte anyway, we may as well put + * the nul there so we can use memdup instead of + * strndup. + */ + buffer[s] = '\0'; + return g_memdup (buffer, s + 1); + } + else { + /* The only way we can find out the true size is with + * ever-increasing buffers... + */ + gssize bufsize = sizeof buffer * 2; + gchar *buf = NULL; + + while (TRUE) { + buf = g_malloc (bufsize); + s = readlink (filename, buf, bufsize); + + if (s < 0) + return NULL; + + if (s < bufsize) { + buf[s] = '\0'; + return buf; + } + + g_free (buf); + } + } +} + +static void +daemon_maybe_add_extension_interface (GHashTable *ifaces, + GDBusInterfaceInfo *iface) +{ + gint i; + + /* We visit the XDG data dirs in precedence order, so if we + * already have this one, we should not add it again. + */ + if (g_hash_table_contains (ifaces, iface->name)) + return; + + /* Only accept interfaces with only properties */ + if ((iface->methods && *iface->methods)|| (iface->signals && *iface->signals)) + return; + + /* Add it only if we can find the annotation */ + for (i = 0; iface->annotations && iface->annotations[i]; i++) { + if (g_str_equal (iface->annotations[i]->key, "org.freedesktop.Accounts.VendorExtension")) { + g_hash_table_insert (ifaces, g_strdup (iface->name), g_dbus_interface_info_ref (iface)); + return; + } + } +} + +static void +daemon_read_extension_file (GHashTable *ifaces, + const gchar *filename) +{ + GDBusNodeInfo *node; + gchar *contents; + gint i; + + if (!g_file_get_contents (filename, &contents, NULL, NULL)) + return; + + node = g_dbus_node_info_new_for_xml (contents, NULL); + if (node) { + for (i = 0; node->interfaces && node->interfaces[i]; i++) + daemon_maybe_add_extension_interface (ifaces, node->interfaces[i]); + + g_dbus_node_info_unref (node); + } + + g_free (contents); +} + +static void +daemon_read_extension_directory (GHashTable *ifaces, + const gchar *path) +{ + const gchar *name; + GDir *dir; + + dir = g_dir_open (path, 0, NULL); + if (!dir) + return; + + while ((name = g_dir_read_name (dir))) { + gchar *filename; + gchar *symlink; + + filename = g_build_filename (path, name, NULL); + symlink = read_symlink (filename); + + if (!symlink) { + g_free (filename); + continue; + } + + /* Ensure it looks like "../../dbus-1/interfaces/${name}" */ + const gchar * const prefix = "../../dbus-1/interfaces/"; + if (g_str_has_prefix (symlink, prefix) && g_str_equal (symlink + strlen (prefix), name)) + daemon_read_extension_file (ifaces, filename); + + g_free (filename); + g_free (symlink); + } + + g_dir_close (dir); +} + +static GHashTable * +daemon_read_extension_ifaces (void) +{ + const gchar * const *data_dirs; + GHashTable *ifaces; + gint i; + + ifaces = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_dbus_interface_info_unref); + + data_dirs = g_get_system_data_dirs (); + for (i = 0; data_dirs[i]; i++) { + gchar *path = g_build_filename (data_dirs[i], "accountsservice/interfaces", NULL); + + daemon_read_extension_directory (ifaces, path); + + g_free (path); + } + + return ifaces; +} + static void daemon_init (Daemon *daemon) { @@ -747,6 +910,8 @@ daemon_init (Daemon *daemon) g_free, NULL); + daemon->priv->extension_ifaces = daemon_read_extension_ifaces (); + for (i = 0; default_excludes[i] != NULL; i++) { g_hash_table_insert (daemon->priv->exclusions, g_strdup (default_excludes[i]), @@ -823,6 +988,8 @@ daemon_finalize (GObject *object) g_hash_table_destroy (daemon->priv->users); + g_hash_table_unref (daemon->priv->extension_ifaces); + G_OBJECT_CLASS (daemon_parent_class)->finalize (object); } @@ -1628,6 +1795,12 @@ daemon_local_set_automatic_login (Daemon *daemon, return TRUE; } +GHashTable * +daemon_get_extension_ifaces (Daemon *daemon) +{ + return daemon->priv->extension_ifaces; +} + static void get_property (GObject *object, guint prop_id, diff --git a/src/daemon.h b/src/daemon.h index 2ef4d0b..dd0c022 100644 --- a/src/daemon.h +++ b/src/daemon.h @@ -99,6 +99,8 @@ gboolean daemon_local_set_automatic_login (Daemon *daemon, gboolean enabled, GError **error); +GHashTable * daemon_get_extension_ifaces (Daemon *daemon); + G_END_DECLS #endif /* __DAEMON_H__ */ diff --git a/src/user.c b/src/user.c index 9e18b0d..8648f6a 100644 --- a/src/user.c +++ b/src/user.c @@ -3,6 +3,7 @@ * Copyright (C) 2004-2005 James M. Cape . * Copyright (C) 2007-2008 William Jon McCann * Copyright (C) 2009-2010 Red Hat, Inc. + * Copyright © 2013 Canonical Limited * * 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 @@ -102,6 +103,9 @@ struct User { gboolean automatic_login; gboolean system_account; gboolean local_account; + + guint *extension_ids; + guint n_extension_ids; }; typedef struct UserClass @@ -433,6 +437,235 @@ move_extra_data (const gchar *old_name, g_free (new_filename); } +static GVariant * +user_extension_get_value (User *user, + GDBusInterfaceInfo *interface, + const GDBusPropertyInfo *property) +{ + const GVariantType *type = G_VARIANT_TYPE (property->signature); + GVariant *value; + gchar *printed; + gint i; + + /* First, try to get the value from the keyfile */ + printed = g_key_file_get_value (user->keyfile, interface->name, property->name, NULL); + if (printed) { + value = g_variant_parse (type, printed, NULL, NULL, NULL); + g_free (printed); + + if (value != NULL) + return value; + } + + /* If that didn't work, try for a default value annotation */ + for (i = 0; property->annotations && property->annotations[i]; i++) { + GDBusAnnotationInfo *annotation = property->annotations[i]; + + if (g_str_equal (annotation->key, "org.freedesktop.Accounts.DefaultValue.String")) { + if (g_str_equal (property->signature, "s")) + return g_variant_ref_sink (g_variant_new_string (annotation->value)); + } + else if (g_str_equal (annotation->key, "org.freedesktop.Accounts.DefaultValue")) { + value = g_variant_parse (type, annotation->value, NULL, NULL, NULL); + if (value != NULL) + return value; + } + } + + /* Nothing found... */ + return NULL; +} + +static void +user_extension_authentication_done (Daemon *daemon, + User *user, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + GDBusInterfaceInfo *interface = user_data; + const gchar *method_name; + + method_name = g_dbus_method_invocation_get_method_name (invocation); + + if (g_str_equal (method_name, "Get")) { + const GDBusPropertyInfo *property = g_dbus_method_invocation_get_property_info (invocation); + GVariant *value; + + value = user_extension_get_value (user, interface, property); + + if (value) { + g_dbus_method_invocation_return_value (invocation, g_variant_new ("(v)", value)); + g_variant_unref (value); + } + else { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, G_DBUS_ERROR_FILE_NOT_FOUND, + "Key '%s' is not set and has no default value", + property->name); + } + } + else if (g_str_equal (method_name, "GetAll")) { + GVariantBuilder builder; + gint i; + + g_variant_builder_init (&builder, G_VARIANT_TYPE_VARDICT); + for (i = 0; interface->properties && interface->properties[i]; i++) { + GDBusPropertyInfo *property = interface->properties[i]; + GVariant *value; + + value = user_extension_get_value (user, interface, property); + + if (value) { + g_variant_builder_add (&builder, "{sv}", property->name, value); + g_variant_unref (value); + } + } + + g_dbus_method_invocation_return_value (invocation, g_variant_new ("(a{sv})", &builder)); + } + else if (g_str_equal (method_name, "Set")) { + const GDBusPropertyInfo *property = g_dbus_method_invocation_get_property_info (invocation); + GVariant *value; + gchar *printed; + gchar *prev; + + g_variant_get_child (g_dbus_method_invocation_get_parameters (invocation), 2, "v", &value); + + /* We'll always have the type when we parse it back so + * we don't need it to be printed with annotations. + */ + printed = g_variant_print (value, FALSE); + + /* May as well try to avoid the thrashing... */ + prev = g_key_file_get_value (user->keyfile, interface->name, property->name, NULL); + + if (!prev || !g_str_equal (printed, prev)) { + g_key_file_set_value (user->keyfile, interface->name, property->name, printed); + + /* Emit a change signal. Use invalidation + * because the data may not be world-readable. + */ + g_dbus_connection_emit_signal (g_dbus_method_invocation_get_connection (invocation), + NULL, /* destination_bus_name */ + g_dbus_method_invocation_get_object_path (invocation), + "org.freedesktop.DBus.Properties", "PropertiesChanged", + g_variant_new_parsed ("( %s, %a{sv}, [ %s ] )", + interface->name, NULL, property->name), + NULL); + + accounts_user_emit_changed (ACCOUNTS_USER (user)); + save_extra_data (user); + } + + g_variant_unref (value); + g_free (printed); + g_free (prev); + + g_dbus_method_invocation_return_value (invocation, g_variant_new ("()")); + } + else + g_assert_not_reached (); +} + +static void +user_extension_method_call (GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *method_name, + GVariant *parameters, + GDBusMethodInvocation *invocation, + gpointer user_data) +{ + User *user = user_data; + GDBusInterfaceInfo *iface_info; + const gchar *annotation_name; + const gchar *action_id; + gint uid; + gint i; + + /* We don't allow method calls on extension interfaces, so we + * should only ever see property calls here. + */ + g_assert_cmpstr (interface_name, ==, "org.freedesktop.DBus.Properties"); + + /* Now get the real interface name */ + g_variant_get_child (parameters, 0, "&s", &interface_name); + + if (get_caller_uid (invocation, &uid) && (uid_t) uid == user->uid) { + /* Operation on sender's own User object */ + if (g_str_equal (method_name, "Set")) { + annotation_name = "org.freedesktop.Accounts.Authentication.ChangeOwn"; + action_id = "org.freedesktop.accounts.change-own-user-data"; + } + else { + annotation_name = "org.freedesktop.Accounts.Authentication.ReadOwn"; + action_id = ""; /* reading allowed by default */ + } + } + else { + /* Operation on someone else's User object */ + if (g_str_equal (method_name, "Set")) { + annotation_name = "org.freedesktop.Accounts.Authentication.ChangeAny"; + action_id = "org.freedesktop.accounts.user-administration"; + } + else { + annotation_name = "org.freedesktop.Accounts.Authentication.ReadAny"; + action_id = ""; /* reading allowed by default */ + } + } + + iface_info = g_hash_table_lookup (daemon_get_extension_ifaces (user->daemon), interface_name); + g_assert (iface_info != NULL); + + for (i = 0; iface_info->annotations && iface_info->annotations[i]; i++) { + if (g_str_equal (iface_info->annotations[i]->key, annotation_name)) { + action_id = iface_info->annotations[i]->value; + break; + } + } + + if (action_id[0] == '\0') { + /* Should always allow this call, so just do it now */ + user_extension_authentication_done (user->daemon, user, invocation, iface_info); + } + else { + daemon_local_check_auth (user->daemon, user, action_id, TRUE, + user_extension_authentication_done, + invocation, iface_info, NULL); + } +} + +static void +user_register_extensions (User *user) +{ + static const GDBusInterfaceVTable vtable = { + user_extension_method_call, + NULL /* get_property */, + NULL /* set_property */ + }; + GHashTable *extensions; + GHashTableIter iter; + gpointer iface; + gint i = 0; + + g_assert (user->extension_ids == NULL); + g_assert (user->n_extension_ids == 0); + + extensions = daemon_get_extension_ifaces (user->daemon); + user->n_extension_ids = g_hash_table_size (extensions); + user->extension_ids = g_new (guint, user->n_extension_ids); + g_hash_table_iter_init (&iter, extensions); + + /* Ignore errors when registering more interfaces because (a) + * they won't happen and (b) even if they do, we still want to + * publish the main user interface. + */ + while (g_hash_table_iter_next (&iter, NULL, &iface)) + user->extension_ids[i++] = g_dbus_connection_register_object (user->system_bus_connection, + user->object_path, iface, + &vtable, user, NULL, NULL); +} + static gchar * compute_object_path (User *user) { @@ -470,12 +703,29 @@ user_register (User *user) } return; } + + user_register_extensions (user); } void user_unregister (User *user) { g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (user)); + + if (user->extension_ids) { + guint i; + + for (i = 0; i < user->n_extension_ids; i++) { + /* In theory, if an error happened during registration, we could have 0 here. */ + if (user->extension_ids[i] == 0) + continue; + + g_dbus_connection_unregister_object (user->system_bus_connection, user->extension_ids[i]); + } + + g_clear_pointer (&user->extension_ids, g_free); + user->n_extension_ids = 0; + } } User * -- 1.8.1.4