From d83985af197ec3ee78cdd354838e00fc4c6892f8 Mon Sep 17 00:00:00 2001 From: Ryan Lortie Date: Wed, 17 Apr 2013 12:33:14 -0400 Subject: [PATCH 2/3] 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/Makefile.am | 1 + src/daemon.c | 12 +++ src/daemon.h | 3 + src/extensions.c | 167 +++++++++++++++++++++++++++++++++ src/user.c | 274 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 457 insertions(+) create mode 100644 src/extensions.c diff --git a/src/Makefile.am b/src/Makefile.am index a053976..13c57ca 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -34,6 +34,7 @@ accounts_daemon_SOURCES = \ types.h \ daemon.h \ daemon.c \ + extensions.c \ user.h \ user.c \ util.h \ diff --git a/src/daemon.c b/src/daemon.c index 81dcbde..0ee8b79 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 (c) 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 @@ -110,6 +111,7 @@ struct DaemonPrivate { guint autologin_id; PolkitAuthority *authority; + GHashTable *extension_ifaces; }; typedef struct passwd * (* EntryGeneratorFunc) (GHashTable *, gpointer *); @@ -757,6 +759,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]), @@ -852,6 +856,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); } @@ -1675,6 +1681,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 7d89ef5..6792f50 100644 --- a/src/daemon.h +++ b/src/daemon.h @@ -100,6 +100,9 @@ gboolean daemon_local_set_automatic_login (Daemon *daemon, gboolean enabled, GError **error); +GHashTable * daemon_read_extension_ifaces (void); +GHashTable * daemon_get_extension_ifaces (Daemon *daemon); + G_END_DECLS #endif /* __DAEMON_H__ */ diff --git a/src/extensions.c b/src/extensions.c new file mode 100644 index 0000000..9e9a7ea --- /dev/null +++ b/src/extensions.c @@ -0,0 +1,167 @@ +/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*- + * + * Copyright (c) 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 + * the Free Software Foundation; either version 3 of the License, 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, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + * Written by: Ryan Lortie + */ + +#include "config.h" + +#include "daemon.h" + +#include +#include + +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[0]) || (iface->signals && iface->signals[0])) + 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) +{ + GError *error = NULL; + GDBusNodeInfo *node; + gchar *contents; + gint i; + + if (!g_file_get_contents (filename, &contents, NULL, &error)) { + g_warning ("Unable to read extension file %s: %s. Ignoring.", filename, error->message); + g_error_free (error); + return; + } + + node = g_dbus_node_info_new_for_xml (contents, &error); + 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); + } else { + g_warning ("Failed to parse file %s: %s", filename, error->message); + g_error_free (error); + } + + 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; + + /* Extensions are installed as normal D-Bus interface + * files with an annotation. + * + * D-Bus interface files are supposed to be installed in + * $(datadir)/dbus-1/interfaces/ but we don't want to + * scan all of the files there looking for interfaces + * that may contain our annotation. + * + * The solution is to install a symlink into a directory + * private to accountsservice and point it at the file, + * as installed in the usual D-Bus directory. + * + * This ensures compatibility with the future if we ever + * decide to check the interfaces directory directly + * (which might be a reasonable thing to do if we ever + * get an efficient cache of the contents of this + * directory). + * + * By introducing such a restrictive way of doing this + * now we ensure that everyone will do it in this + * forwards-compatible way. + */ + filename = g_build_filename (path, name, NULL); + symlink = g_file_read_link (filename, NULL); + + if (!symlink) { + g_warning ("Found accounts service vendor extension file %s, but file must be a symlink to " + "'../../dbus-1/interfaces/%s' for forwards-compatibility reasons.", filename, name); + 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); + } + else { + g_warning ("Found accounts service vendor extension symlink %s, but it must be exactly " + "equal to '../../dbus-1/interfaces/%s' for forwards-compatibility reasons.", + filename, name); + } + + g_free (filename); + g_free (symlink); + } + + g_dir_close (dir); +} + +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; +} diff --git a/src/user.c b/src/user.c index ec11876..85203f0 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 @@ -458,6 +462,259 @@ 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_get_property (User *user, + Daemon *daemon, + GDBusInterfaceInfo *interface, + GDBusMethodInvocation *invocation) +{ + 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_INVALID_ARGS, + "Key '%s' is not set and has no default value", + property->name); + } +} + +static void +user_extension_get_all_properties (User *user, + Daemon *daemon, + GDBusInterfaceInfo *interface, + GDBusMethodInvocation *invocation) +{ + 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)); +} + +static void +user_extension_set_property (User *user, + Daemon *daemon, + GDBusInterfaceInfo *interface, + GDBusMethodInvocation *invocation) +{ + 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 ("()")); +} + +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")) + user_extension_get_property (user, daemon, interface, invocation); + else if (g_str_equal (method_name, "GetAll")) + user_extension_get_all_properties (user, daemon, interface, invocation); + else if (g_str_equal (method_name, "Set")) + user_extension_set_property (user, daemon, interface, invocation); + 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) { @@ -495,6 +752,8 @@ user_register (User *user) } return; } + + user_register_extensions (user); } void @@ -507,6 +766,21 @@ 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; + } } void -- 1.8.2.1