From d83985af197ec3ee78cdd354838e00fc4c6892f8 Mon Sep 17 00:00:00 2001
From: Ryan Lortie <desrt@desrt.ca>
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

Index: accountsservice-0.6.34/src/Makefile.am
===================================================================
--- accountsservice-0.6.34.orig/src/Makefile.am	2013-06-26 20:02:05.695804882 +0200
+++ accountsservice-0.6.34/src/Makefile.am	2013-06-26 20:02:13.000000000 +0200
@@ -41,6 +41,7 @@
 	types.h			\
 	daemon.h		\
 	daemon.c		\
+	extensions.c		\
 	user.h			\
 	user.c			\
 	util.h			\
Index: accountsservice-0.6.34/src/daemon.c
===================================================================
--- accountsservice-0.6.34.orig/src/daemon.c	2013-06-26 20:02:05.695804882 +0200
+++ accountsservice-0.6.34/src/daemon.c	2013-06-26 20:02:13.000000000 +0200
@@ -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 @@
         guint autologin_id;
 
         PolkitAuthority *authority;
+        GHashTable *extension_ifaces;
 };
 
 typedef struct passwd * (* EntryGeneratorFunc) (GHashTable *, gpointer *);
@@ -759,6 +761,8 @@
                                                           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]),
@@ -854,6 +858,8 @@
 
         g_hash_table_destroy (daemon->priv->users);
 
+        g_hash_table_unref (daemon->priv->extension_ifaces);
+
         G_OBJECT_CLASS (daemon_parent_class)->finalize (object);
 }
 
@@ -1792,6 +1798,12 @@
         return TRUE;
 }
 
+GHashTable *
+daemon_get_extension_ifaces (Daemon *daemon)
+{
+  return daemon->priv->extension_ifaces;
+}
+
 static void
 get_property (GObject    *object,
               guint       prop_id,
Index: accountsservice-0.6.34/src/daemon.h
===================================================================
--- accountsservice-0.6.34.orig/src/daemon.h	2013-06-26 20:02:05.695804882 +0200
+++ accountsservice-0.6.34/src/daemon.h	2013-06-26 20:02:13.000000000 +0200
@@ -101,6 +101,9 @@
                                              gboolean        enabled,
                                              GError        **error);
 
+GHashTable * daemon_read_extension_ifaces (void);
+GHashTable * daemon_get_extension_ifaces (Daemon *daemon);
+
 G_END_DECLS
 
 #endif /* __DAEMON_H__ */
Index: accountsservice-0.6.34/src/extensions.c
===================================================================
--- /dev/null	1970-01-01 00:00:00.000000000 +0000
+++ accountsservice-0.6.34/src/extensions.c	2013-06-26 20:02:05.683804881 +0200
@@ -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 <desrt@desrt.ca>
+ */
+
+#include "config.h"
+
+#include "daemon.h"
+
+#include <gio/gio.h>
+#include <string.h>
+
+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;
+}
Index: accountsservice-0.6.34/src/user.c
===================================================================
--- accountsservice-0.6.34.orig/src/user.c	2013-06-26 20:02:05.695804882 +0200
+++ accountsservice-0.6.34/src/user.c	2013-06-26 20:02:13.000000000 +0200
@@ -3,6 +3,7 @@
   * Copyright (C) 2004-2005 James M. Cape <jcape@ignore-your.tv>.
   * Copyright (C) 2007-2008 William Jon McCann <mccann@jhu.edu>
   * 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
@@ -115,6 +116,9 @@
         gboolean      automatic_login;
         gboolean      system_account;
         gboolean      local_account;
+
+        guint        *extension_ids;
+        guint         n_extension_ids;
 };
 
 typedef struct UserClass
@@ -511,6 +515,259 @@
         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)
 {
@@ -548,6 +805,8 @@
                 }
                 return;
         }
+
+        user_register_extensions (user);
 }
 
 void
@@ -560,6 +819,21 @@
 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
