[gnome-settings-daemon/wip/hughsie/subman] subman: Add a new plugin to provide system subscription information



commit babc09a52282bdaae364db6f3b9d84b29c39270d
Author: Richard Hughes <richard hughsie com>
Date:   Thu Jun 6 16:14:03 2019 +0100

    subman: Add a new plugin to provide system subscription information

 meson.build                                        |   1 +
 plugins/meson.build                                |   1 +
 plugins/subman/README.md                           |  56 ++
 plugins/subman/gsd-subman-common.c                 |  36 +
 plugins/subman/gsd-subman-common.h                 |  40 +
 plugins/subman/gsd-subman-helper.c                 | 367 ++++++++
 plugins/subman/gsd-subscription-manager.c          | 998 +++++++++++++++++++++
 plugins/subman/gsd-subscription-manager.h          |  63 ++
 plugins/subman/main.c                              |   8 +
 plugins/subman/meson.build                         |  56 ++
 ...rg.gnome.SettingsDaemon.Subscription.desktop.in |   9 +
 ...ome.settings-daemon.plugins.subman.policy.in.in |  28 +
 .../org.gnome.settings-daemon.plugins.subman.rules |   7 +
 13 files changed, 1670 insertions(+)
---
diff --git a/meson.build b/meson.build
index a352d04f..65138ea4 100644
--- a/meson.build
+++ b/meson.build
@@ -97,6 +97,7 @@ libcanberra_gtk_dep = dependency('libcanberra-gtk3')
 libgeoclue_dep = dependency('libgeoclue-2.0', version: '>= 2.3.1')
 libnotify_dep = dependency('libnotify', version: '>= 0.7.3')
 libpulse_mainloop_glib_dep = dependency('libpulse-mainloop-glib', version: '>= 2.0')
+jsonglib_dep = dependency('json-glib-1.0', version: '>= 1.1.1')
 pango_dep = dependency('pango', version: '>= 1.20.0')
 polkit_gobject_dep = dependency('polkit-gobject-1', version: '>= 0.103')
 upower_glib_dep = dependency('upower-glib', version: '>= 0.99.0')
diff --git a/plugins/meson.build b/plugins/meson.build
index 3db69da7..af2b24e4 100644
--- a/plugins/meson.build
+++ b/plugins/meson.build
@@ -2,6 +2,7 @@ enabled_plugins = [
   ['a11y-settings', 'A11ySettings'],
   ['clipboard', 'Clipboard'],
   ['color', 'Color'],
+  ['subman', 'Subscription'],
   ['datetime', 'Datetime'],
   ['dummy', ''],
   ['power', 'Power'],
diff --git a/plugins/subman/README.md b/plugins/subman/README.md
new file mode 100644
index 00000000..3e1cc3cd
--- /dev/null
+++ b/plugins/subman/README.md
@@ -0,0 +1,56 @@
+GNOME Settings Daemon: Subscription Manager Plugin
+==================================================
+
+Testing:
+
+To add a test acccount on subscription.rhsm.stage.redhat.com, use Ethel:
+http://account-manager-stage.app.eng.rdu2.redhat.com/#view
+
+Register with a username and password
+-------------------------------------
+
+    gdbus call \
+     --session \
+     --dest org.gnome.SettingsDaemon.Subscription \
+     --object-path /org/gnome/SettingsDaemon/Subscription \
+     --method org.gnome.SettingsDaemon.Subscription.Register 
"{'kind':<'username'>,'hostname':<'subscription.rhsm.stage.redhat.com'>,'username':<'rhughes_test'>,'password':<'barbaz'>}"
+
+To register with a certificate
+------------------------------
+
+    gdbus call \
+     --session \
+     --dest org.gnome.SettingsDaemon.Subscription \
+     --object-path /org/gnome/SettingsDaemon/Subscription \
+     --method org.gnome.SettingsDaemon.Subscription.Register 
"{'kind':<'key'>,'hostname':<'subscription.rhsm.stage.redhat.com'>,'organisation':<'foo'>,'activation-key':<'barbaz'>}"
+
+To unregister
+-------------
+
+    gdbus call \
+     --session \
+     --dest org.gnome.SettingsDaemon.Subscription \
+     --object-path /org/gnome/SettingsDaemon/Subscription \
+     --method org.gnome.SettingsDaemon.Subscription.Unregister
+
+Debugging
+---------
+
+Get the UNIX socket using `Subscription.Register` then call something like:
+
+    sudo G_MESSAGES_DEBUG=all ./plugins/subman/gsd-subman-helper \
+     --address="unix:abstract=/var/run/dbus-ulGB1wfnbn,guid=71e6bf329d861ce366df7a1d5d036a5b" \
+     --kind="register-with-username" \
+     --username="rhughes_test" \
+     --password="barbaz" \
+     --hostname="subscription.rhsm.stage.redhat.com" \
+     --organisation=""
+
+You can all see some basic debugging running `rhsmd` in the foreground:
+
+    sudo /usr/libexec/rhsmd -d -k
+
+Known Limitations
+=================
+
+Proxy servers are not supported, nor are custom host ports or prefixes.
diff --git a/plugins/subman/gsd-subman-common.c b/plugins/subman/gsd-subman-common.c
new file mode 100644
index 00000000..e515131e
--- /dev/null
+++ b/plugins/subman/gsd-subman-common.c
@@ -0,0 +1,36 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2019 Richard Hughes <rhughes redhat com>
+ *
+ * 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 2 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, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "config.h"
+
+#include "gsd-subman-common.h"
+
+const gchar *
+gsd_subman_subscription_status_to_string (GsdSubmanSubscriptionStatus status)
+{
+       if (status == GSD_SUBMAN_SUBSCRIPTION_STATUS_VALID)
+               return "valid";
+       if (status == GSD_SUBMAN_SUBSCRIPTION_STATUS_INVALID)
+               return "invalid";
+       if (status == GSD_SUBMAN_SUBSCRIPTION_STATUS_DISABLED)
+               return "disabled";
+       if (status == GSD_SUBMAN_SUBSCRIPTION_STATUS_PARTIALLY_VALID)
+               return "partially-valid";
+       return "unknown";
+}
diff --git a/plugins/subman/gsd-subman-common.h b/plugins/subman/gsd-subman-common.h
new file mode 100644
index 00000000..fccf9f6a
--- /dev/null
+++ b/plugins/subman/gsd-subman-common.h
@@ -0,0 +1,40 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2019 Richard Hughes <rhughes redhat com>
+ *
+ * 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 2 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, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef __GSD_SUBMAN_COMMON_H
+#define __GSD_SUBMAN_COMMON_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+typedef enum {
+       GSD_SUBMAN_SUBSCRIPTION_STATUS_UNKNOWN,
+       GSD_SUBMAN_SUBSCRIPTION_STATUS_VALID,
+       GSD_SUBMAN_SUBSCRIPTION_STATUS_INVALID,
+       GSD_SUBMAN_SUBSCRIPTION_STATUS_DISABLED,
+       GSD_SUBMAN_SUBSCRIPTION_STATUS_PARTIALLY_VALID,
+       GSD_SUBMAN_SUBSCRIPTION_STATUS_LAST
+} GsdSubmanSubscriptionStatus;
+
+const gchar    *gsd_subman_subscription_status_to_string       (GsdSubmanSubscriptionStatus     status);
+
+G_END_DECLS
+
+#endif /* __GSD_SUBMAN_COMMON_H */
diff --git a/plugins/subman/gsd-subman-helper.c b/plugins/subman/gsd-subman-helper.c
new file mode 100644
index 00000000..2d6bb1d2
--- /dev/null
+++ b/plugins/subman/gsd-subman-helper.c
@@ -0,0 +1,367 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2019 Richard Hughes <rhughes redhat com>
+ *
+ * Licensed under the GNU General Public License Version 2
+ *
+ * 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 2 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 Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#include "config.h"
+
+#include <sys/types.h>
+#include <unistd.h>
+#include <stdlib.h>
+
+#include <gio/gio.h>
+#include <json-glib/json-glib.h>
+
+static void
+_helper_convert_error (const gchar *json_txt, GError **error)
+{
+       JsonNode *json_root;
+       JsonObject *json_obj;
+       const gchar *message;
+       g_autoptr(JsonParser) json_parser = json_parser_new ();
+
+       /* this may be plain text or JSON :| */
+       if (!json_parser_load_from_data (json_parser, json_txt, -1, NULL)) {
+               g_set_error_literal (error,
+                                    G_IO_ERROR,
+                                    G_IO_ERROR_NOT_SUPPORTED,
+                                    json_txt);
+               return;
+       }
+       json_root = json_parser_get_root (json_parser);
+       json_obj = json_node_get_object (json_root);
+       if (!json_object_has_member (json_obj, "message")) {
+               g_set_error (error,
+                            G_IO_ERROR,
+                            G_IO_ERROR_INVALID_DATA,
+                            "no message' in %s", json_txt);
+               return;
+       }
+       message = json_object_get_string_member (json_obj, "message");
+       if (g_strstr_len (message, -1, "Invalid user credentials") != NULL) {
+               g_set_error_literal (error,
+                                    G_IO_ERROR,
+                                    G_IO_ERROR_PERMISSION_DENIED,
+                                    message);
+               return;
+       }
+       g_set_error_literal (error,
+                            G_IO_ERROR,
+                            G_IO_ERROR_NOT_SUPPORTED,
+                            message);
+}
+
+static gboolean
+_helper_unregister (GError **error)
+{
+       g_autoptr(GDBusProxy) proxy = NULL;
+       g_autoptr(GVariantBuilder) proxy_options = NULL;
+       g_autoptr(GVariant) res = NULL;
+
+       g_debug ("unregistering");
+       proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM,
+                                              G_DBUS_PROXY_FLAGS_NONE,
+                                              NULL,
+                                              "com.redhat.RHSM1",
+                                              "/com/redhat/RHSM1/Unregister",
+                                              "com.redhat.RHSM1.Unregister",
+                                              NULL, error);
+       if (proxy == NULL) {
+               g_prefix_error (error, "Failed to get proxy: ");
+               return FALSE;
+       }
+       proxy_options = g_variant_builder_new (G_VARIANT_TYPE_VARDICT);
+       res = g_dbus_proxy_call_sync (proxy,
+                                     "Unregister",
+                                     g_variant_new ("(a{sv}s)",
+                                                    proxy_options,
+                                                    ""), /* lang */
+                                     G_DBUS_CALL_FLAGS_NONE,
+                                     -1, NULL, error);
+       return res != NULL;
+}
+
+static gboolean
+_helper_auto_attach (GError **error)
+{
+       const gchar *str = NULL;
+       g_autoptr(GDBusProxy) proxy = NULL;
+       g_autoptr(GVariantBuilder) proxy_options = NULL;
+       g_autoptr(GVariant) res = NULL;
+
+       g_debug ("auto-attaching subscriptions");
+       proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM,
+                                              G_DBUS_PROXY_FLAGS_NONE,
+                                              NULL,
+                                              "com.redhat.RHSM1",
+                                              "/com/redhat/RHSM1/Attach",
+                                              "com.redhat.RHSM1.Attach",
+                                              NULL, error);
+       if (proxy == NULL) {
+               g_prefix_error (error, "Failed to get proxy: ");
+               return FALSE;
+       }
+       proxy_options = g_variant_builder_new (G_VARIANT_TYPE_VARDICT);
+       res = g_dbus_proxy_call_sync (proxy,
+                                     "AutoAttach",
+                                     g_variant_new ("(sa{sv}s)",
+                                                    "", /* now? */
+                                                    proxy_options,
+                                                    ""), /* lang */
+                                     G_DBUS_CALL_FLAGS_NONE,
+                                     -1, NULL, error);
+       if (res == NULL)
+               return FALSE;
+       g_variant_get (res, "(&s)", &str);
+       g_debug ("Attach.AutoAttach: %s", str);
+       return TRUE;
+}
+
+static gboolean
+_helper_save_hostname (const gchar *hostname, GError **error)
+{
+       g_autoptr(GDBusProxy) proxy = NULL;
+       g_autoptr(GVariant) res = NULL;
+       proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM,
+                                              G_DBUS_PROXY_FLAGS_NONE,
+                                              NULL,
+                                              "com.redhat.RHSM1",
+                                              "/com/redhat/RHSM1/Config",
+                                              "com.redhat.RHSM1.Config",
+                                              NULL, error);
+       if (proxy == NULL) {
+               g_prefix_error (error, "Failed to get proxy: ");
+               return FALSE;
+       }
+       res = g_dbus_proxy_call_sync (proxy, "Set",
+                                     g_variant_new ("svs",
+                                                    "server.hostname", /* key */
+                                                    g_variant_new_string (hostname), /* value */
+                                                    ""), /* lang */
+                                     G_DBUS_CALL_FLAGS_NONE,
+                                     -1, NULL, error);
+       return res != NULL;
+}
+
+int
+main (int argc, char *argv[])
+{
+       const gchar *userlang = ""; /* as root, so no translations */
+       g_autofree gchar *activation_key = NULL;
+       g_autofree gchar *address = NULL;
+       g_autofree gchar *hostname = NULL;
+       g_autofree gchar *kind = NULL;
+       g_autofree gchar *organisation = NULL;
+       g_autofree gchar *password = NULL;
+       g_autofree gchar *port = NULL;
+       g_autofree gchar *prefix = NULL;
+       g_autofree gchar *proxy_server = NULL;
+       g_autofree gchar *username = NULL;
+       g_autoptr(GDBusConnection) conn_private = NULL;
+       g_autoptr(GDBusProxy) proxy = NULL;
+       g_autoptr(GError) error = NULL;
+       g_autoptr(GOptionContext) context = g_option_context_new (NULL);
+       g_autoptr(GVariantBuilder) proxy_options = NULL;
+       g_autoptr(GVariantBuilder) subman_conopts = NULL;
+       g_autoptr(GVariantBuilder) subman_options = NULL;
+
+       const GOptionEntry options[] = {
+               { "kind", '\0', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING,
+                       &kind, "Kind, e.g. 'username' or 'key'", NULL },
+               { "address", '\0', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING,
+                       &address, "UNIX address", NULL },
+               { "username", '\0', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING,
+                       &username, "Username", NULL },
+               { "password", '\0', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING,
+                       &password, "Password", NULL },
+               { "organisation", '\0', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING,
+                       &organisation, "Organisation", NULL },
+               { "activation-key", '\0', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING,
+                       &activation_key, "Activation keys", NULL },
+               { "hostname", '\0', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING,
+                       &hostname, "Registration server hostname", NULL },
+               { "prefix", '\0', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING,
+                       &prefix, "Registration server prefix", NULL },
+               { "port", '\0', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING,
+                       &port, "Registration server port", NULL },
+               { "proxy", '\0', G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_STRING,
+                       &proxy_server, "Proxy settings", NULL },
+               { NULL}
+       };
+
+       /* check calling UID */
+       if (getuid () != 0 || geteuid () != 0) {
+               g_printerr ("This program can only be used by the root user\n");
+               return G_IO_ERROR_NOT_SUPPORTED;
+       }
+       g_option_context_add_main_entries (context, options, NULL);
+       if (!g_option_context_parse (context, &argc, &argv, &error)) {
+               g_printerr ("Failed to parse arguments: %s\n", error->message);
+               return G_IO_ERROR_NOT_SUPPORTED;
+       }
+
+       /* uncommon actions */
+       if (kind == NULL) {
+               g_printerr ("No --kind specified\n");
+               return G_IO_ERROR_INVALID_DATA;
+       }
+       if (g_strcmp0 (kind, "unregister") == 0) {
+               if (!_helper_unregister (&error)) {
+                       g_printerr ("Failed to Unregister: %s\n", error->message);
+                       return G_IO_ERROR_NOT_INITIALIZED;
+               }
+               return EXIT_SUCCESS;
+       }
+       if (g_strcmp0 (kind, "auto-attach") == 0) {
+               if (!_helper_auto_attach (&error)) {
+                       g_printerr ("Failed to AutoAttach: %s\n", error->message);
+                       return G_IO_ERROR_NOT_INITIALIZED;
+               }
+               return EXIT_SUCCESS;
+       }
+
+       /* connect to abstract socket for reasons */
+       if (address == NULL) {
+               g_printerr ("No --address specified\n");
+               return G_IO_ERROR_INVALID_DATA;
+       }
+       conn_private = g_dbus_connection_new_for_address_sync (address,
+                                                              G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT,
+                                                              NULL, NULL,
+                                                              &error);
+       if (conn_private == NULL) {
+               g_printerr ("Invalid --address specified: %s\n", error->message);
+               return G_IO_ERROR_INVALID_DATA;
+       }
+       proxy = g_dbus_proxy_new_sync (conn_private,
+                                      G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES,
+                                      NULL, /* GDBusInterfaceInfo */
+                                      NULL, /* name */
+                                      "/com/redhat/RHSM1/Register",
+                                      "com.redhat.RHSM1.Register",
+                                      NULL, &error);
+       if (proxy == NULL) {
+               g_printerr ("Count not contact RHSM: %s\n", error->message);
+               return G_IO_ERROR_NOT_FOUND;
+       }
+
+       /* no options */
+       subman_options = g_variant_builder_new (G_VARIANT_TYPE("a{ss}"));
+
+       /* set registration server */
+       if (hostname == NULL || hostname[0] == '\0')
+               hostname = g_strdup ("subscription.rhsm.redhat.com");
+       if (prefix == NULL || prefix[0] == '\0')
+               prefix = g_strdup ("/subscription");
+       if (port == NULL || port[0] == '\0')
+               port = g_strdup ("443");
+       subman_conopts = g_variant_builder_new (G_VARIANT_TYPE("a{ss}"));
+       g_variant_builder_add (subman_conopts, "{ss}", "host", hostname);
+       g_variant_builder_add (subman_conopts, "{ss}", "handler", prefix);
+       g_variant_builder_add (subman_conopts, "{ss}", "port", port);
+
+       /* call into RHSM */
+       if (g_strcmp0 (kind, "register-with-key") == 0) {
+               g_auto(GStrv) activation_keys = NULL;
+               g_autoptr(GError) error_local = NULL;
+               g_autoptr(GVariant) res = NULL;
+
+               if (activation_key == NULL) {
+                       g_printerr ("Required --activation-key\n");
+                       return G_IO_ERROR_INVALID_DATA;
+               }
+               if (organisation == NULL) {
+                       g_printerr ("Required --organisation\n");
+                       return G_IO_ERROR_INVALID_DATA;
+               }
+
+               g_debug ("registering using activation key");
+               activation_keys = g_strsplit (activation_key, ",", -1);
+               res = g_dbus_proxy_call_sync (proxy,
+                                             "RegisterWithActivationKeys",
+                                             g_variant_new ("(s^asa{ss}a{ss}s)",
+                                                            organisation,
+                                                            activation_keys,
+                                                            subman_options,
+                                                            subman_conopts,
+                                                            userlang),
+                                             G_DBUS_CALL_FLAGS_NO_AUTO_START,
+                                             -1, NULL, &error_local);
+               if (res == NULL) {
+                       g_dbus_error_strip_remote_error (error_local);
+                       _helper_convert_error (error_local->message, &error);
+                       g_printerr ("Failed to RegisterWithActivationKeys: %s\n", error->message);
+                       return error->code;
+               }
+       } else if (g_strcmp0 (kind, "register-with-username") == 0) {
+               g_autoptr(GError) error_local = NULL;
+               g_autoptr(GVariant) res = NULL;
+
+               g_debug ("registering using username and password");
+               if (username == NULL) {
+                       g_printerr ("Required --username\n");
+                       return G_IO_ERROR_INVALID_DATA;
+               }
+               if (password == NULL) {
+                       g_printerr ("Required --password\n");
+                       return G_IO_ERROR_INVALID_DATA;
+               }
+               if (organisation == NULL) {
+                       g_printerr ("Required --organisation\n");
+                       return G_IO_ERROR_INVALID_DATA;
+               }
+               res = g_dbus_proxy_call_sync (proxy,
+                                             "Register",
+                                             g_variant_new ("(sssa{ss}a{ss}s)",
+                                                            organisation,
+                                                            username,
+                                                            password,
+                                                            subman_options,
+                                                            subman_conopts,
+                                                            userlang),
+                                             G_DBUS_CALL_FLAGS_NO_AUTO_START,
+                                             -1, NULL, &error_local);
+               if (res == NULL) {
+                       g_dbus_error_strip_remote_error (error_local);
+                       _helper_convert_error (error_local->message, &error);
+                       g_printerr ("Failed to Register: %s\n", error->message);
+                       return error->code;
+               }
+       } else {
+               g_printerr ("Invalid --kind specified: %s\n", kind);
+               return G_IO_ERROR_INVALID_DATA;
+       }
+
+       /* set the new hostname */
+       if (!_helper_save_hostname (hostname, &error)) {
+               g_printerr ("Failed to save hostname: %s\n", error->message);
+               return G_IO_ERROR_NOT_INITIALIZED;
+       }
+
+       /* wait for rhsmd to notice the new config */
+       g_usleep (G_USEC_PER_SEC * 5);
+
+       /* auto-attach */
+       if (!_helper_auto_attach (&error)) {
+               g_printerr ("Failed to AutoAttach: %s\n", error->message);
+               return G_IO_ERROR_NOT_INITIALIZED;
+       }
+
+       return EXIT_SUCCESS;
+}
diff --git a/plugins/subman/gsd-subscription-manager.c b/plugins/subman/gsd-subscription-manager.c
new file mode 100644
index 00000000..0f6466f8
--- /dev/null
+++ b/plugins/subman/gsd-subscription-manager.c
@@ -0,0 +1,998 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2019 Richard Hughes <richard hughsie com>
+ *
+ * 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 2 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, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <gdk/gdk.h>
+#include <gtk/gtk.h>
+#include <json-glib/json-glib.h>
+#include <libnotify/notify.h>
+
+#include "gnome-settings-profile.h"
+#include "gsd-subman-common.h"
+#include "gsd-subscription-manager.h"
+
+#define GSD_DBUS_NAME "org.gnome.SettingsDaemon"
+#define GSD_DBUS_PATH "/org/gnome/SettingsDaemon"
+#define GSD_DBUS_BASE_INTERFACE "org.gnome.SettingsDaemon"
+
+#define GSD_SUBSCRIPTION_DBUS_NAME             GSD_DBUS_NAME ".Subscription"
+#define GSD_SUBSCRIPTION_DBUS_PATH             GSD_DBUS_PATH "/Subscription"
+#define GSD_SUBSCRIPTION_DBUS_INTERFACE                GSD_DBUS_BASE_INTERFACE ".Subscription"
+
+static const gchar introspection_xml[] =
+"<node>"
+"  <interface name='org.gnome.SettingsDaemon.Subscription'>"
+"    <method name='Register'>"
+"      <arg type='a{sv}' name='options' direction='in'/>"
+"    </method>"
+"    <method name='Unregister'/>"
+"    <property name='SubscriptionStatus' type='u' access='read'/>"
+"  </interface>"
+"</node>";
+
+#define GSD_SUBSCRIPTION_MANAGER_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), 
GSD_TYPE_SUBSCRIPTION_MANAGER, GsdSubscriptionManagerPrivate))
+
+typedef enum {
+       _RHSM_INTERFACE_CONFIG,
+       _RHSM_INTERFACE_REGISTER_SERVER,
+       _RHSM_INTERFACE_ATTACH,
+       _RHSM_INTERFACE_ENTITLEMENT,
+       _RHSM_INTERFACE_PRODUCTS,
+       _RHSM_INTERFACE_CONSUMER,
+       _RHSM_INTERFACE_SYSPURPOSE,
+       _RHSM_INTERFACE_LAST
+} _RhsmInterface;
+
+struct GsdSubscriptionManagerPrivate
+{
+       /* D-Bus */
+       guint            name_id;
+       GDBusNodeInfo   *introspection_data;
+       GDBusConnection *connection;
+       GCancellable    *bus_cancellable;
+
+       GDBusProxy      *proxies[_RHSM_INTERFACE_LAST];
+       const gchar     *userlang;      /* owned by GLib internally */
+       GHashTable      *config;        /* str:str */
+       gchar           *address;
+
+       guint            check_registration_timeout_id;
+       GTimer          *timer_last_notified;
+       NotifyNotification      *notification_expired;
+       NotifyNotification      *notification_registered;
+       NotifyNotification      *notification_registration_required;
+       GsdSubmanSubscriptionStatus      subscription_status;
+       GsdSubmanSubscriptionStatus      subscription_status_last;
+};
+
+enum {
+       PROP_0,
+};
+
+static void     gsd_subscription_manager_class_init  (GsdSubscriptionManagerClass *klass);
+static void     gsd_subscription_manager_init        (GsdSubscriptionManager      *subscription_manager);
+static void     gsd_subscription_manager_finalize    (GObject             *object);
+
+G_DEFINE_TYPE (GsdSubscriptionManager, gsd_subscription_manager, G_TYPE_OBJECT)
+
+static gpointer manager_object = NULL;
+
+GQuark
+gsd_subscription_manager_error_quark (void)
+{
+       static GQuark quark = 0;
+       if (!quark)
+               quark = g_quark_from_static_string ("gsd_subscription_manager_error");
+       return quark;
+}
+
+static GsdSubmanSubscriptionStatus
+_client_subscription_status_from_text (const gchar *status_txt)
+{
+       if (g_strcmp0 (status_txt, "Unknown") == 0)
+               return GSD_SUBMAN_SUBSCRIPTION_STATUS_UNKNOWN;
+       if (g_strcmp0 (status_txt, "Current") == 0)
+               return GSD_SUBMAN_SUBSCRIPTION_STATUS_VALID;
+       if (g_strcmp0 (status_txt, "Invalid") == 0)
+               return GSD_SUBMAN_SUBSCRIPTION_STATUS_INVALID;
+       if (g_strcmp0 (status_txt, "Disabled") == 0)
+               return GSD_SUBMAN_SUBSCRIPTION_STATUS_DISABLED;
+       if (g_strcmp0 (status_txt, "Insufficient") == 0)
+               return GSD_SUBMAN_SUBSCRIPTION_STATUS_PARTIALLY_VALID;
+       g_warning ("Unknown subscription status: %s", status_txt); // 'Current'?
+       return GSD_SUBMAN_SUBSCRIPTION_STATUS_UNKNOWN;
+}
+
+static gboolean
+_client_subscription_status_update (GsdSubscriptionManager *manager, GError **error)
+{
+       GsdSubscriptionManagerPrivate *priv = manager->priv;
+       JsonNode *json_root;
+       JsonObject *json_obj;
+       const gchar *json_txt = NULL;
+       const gchar *status_txt = NULL;
+       g_autoptr(GVariant) val = NULL;
+       g_autoptr(JsonParser) json_parser = json_parser_new ();
+
+       /* save old value */
+       priv->subscription_status_last = priv->subscription_status;
+
+       val = g_dbus_proxy_call_sync (priv->proxies[_RHSM_INTERFACE_ENTITLEMENT],
+                                     "GetStatus",
+                                     g_variant_new ("(ss)",
+                                                    "", /* assumed as 'now' */
+                                                    priv->userlang),
+                                     G_DBUS_CALL_FLAGS_NONE,
+                                     -1, NULL, error);
+       if (val == NULL)
+               return FALSE;
+       g_variant_get (val, "(&s)", &json_txt);
+       g_debug ("Entitlement.GetStatus JSON: %s", json_txt);
+       if (!json_parser_load_from_data (json_parser, json_txt, -1, error))
+               return FALSE;
+       json_root = json_parser_get_root (json_parser);
+       json_obj = json_node_get_object (json_root);
+       if (!json_object_has_member (json_obj, "status")) {
+               g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
+                            "no Entitlement.GetStatus status in %s", json_txt);
+               return FALSE;
+       }
+
+       status_txt = json_object_get_string_member (json_obj, "status");
+       g_debug ("Entitlement.GetStatus: %s", status_txt);
+       priv->subscription_status = _client_subscription_status_from_text (status_txt);
+       return TRUE;
+}
+
+static gboolean
+_client_syspurpose_update (GsdSubscriptionManager *manager, GError **error)
+{
+       GsdSubscriptionManagerPrivate *priv = manager->priv;
+       JsonNode *json_root;
+       JsonObject *json_obj;
+       const gchar *json_txt = NULL;
+       g_autoptr(GVariant) val = NULL;
+       g_autoptr(JsonParser) json_parser = json_parser_new ();
+
+       val = g_dbus_proxy_call_sync (priv->proxies[_RHSM_INTERFACE_SYSPURPOSE],
+                                     "GetSyspurpose",
+                                     g_variant_new ("(s)", priv->userlang),
+                                     G_DBUS_CALL_FLAGS_NONE,
+                                     -1, NULL, error);
+       if (val == NULL)
+               return FALSE;
+       g_variant_get (val, "(&s)", &json_txt);
+       g_debug ("Syspurpose.GetSyspurpose JSON: %s", json_txt);
+       if (!json_parser_load_from_data (json_parser, json_txt, -1, error))
+               return FALSE;
+       json_root = json_parser_get_root (json_parser);
+       json_obj = json_node_get_object (json_root);
+       if (!json_object_has_member (json_obj, "status")) {
+               g_debug ("Syspurpose.GetSyspurpose: Unknown");
+               return TRUE;
+       }
+       g_debug ("Syspurpose.GetSyspurpose: '%s", json_object_get_string_member (json_obj, "status"));
+       return TRUE;
+}
+
+static gboolean
+_client_register_start (GsdSubscriptionManager *manager, GError **error)
+{
+       GsdSubscriptionManagerPrivate *priv = manager->priv;
+       const gchar *address = NULL;
+       g_autoptr(GDBusProxy) proxy = NULL;
+       g_autoptr(GVariant) val = NULL;
+
+       /* already started */
+       if (priv->address != NULL)
+               return TRUE;
+
+       /* apparently: "we can't send registration credentials over the regular
+        * system or session bus since those aren't really locked down..." */
+       proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM,
+                                              G_DBUS_PROXY_FLAGS_NONE,
+                                              NULL,
+                                              "com.redhat.RHSM1",
+                                              "/com/redhat/RHSM1/RegisterServer",
+                                              "com.redhat.RHSM1.RegisterServer",
+                                              NULL, error);
+       if (proxy == NULL)
+               return FALSE;
+       val = g_dbus_proxy_call_sync (proxy, "Start",
+                                     g_variant_new ("(s)", priv->userlang),
+                                     G_DBUS_CALL_FLAGS_NONE,
+                                     -1, NULL, error);
+       if (val == NULL)
+               return FALSE;
+       g_variant_get (val, "(&s)", &address);
+       g_debug ("RegisterServer.Start: %s", address);
+       priv->address = g_strdup (address);
+       return TRUE;
+}
+
+static gboolean
+_client_register_stop (GsdSubscriptionManager *manager, GError **error)
+{
+       GsdSubscriptionManagerPrivate *priv = manager->priv;
+       g_autoptr(GDBusProxy) proxy = NULL;
+       g_autoptr(GVariant) val = NULL;
+
+       /* already started */
+       if (priv->address == NULL)
+               return TRUE;
+
+       /* stop registration server */
+       proxy = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM,
+                                              G_DBUS_PROXY_FLAGS_NONE,
+                                              NULL,
+                                              "com.redhat.RHSM1",
+                                              "/com/redhat/RHSM1/RegisterServer",
+                                              "com.redhat.RHSM1.RegisterServer",
+                                              NULL, error);
+       if (proxy == NULL)
+               return FALSE;
+       val = g_dbus_proxy_call_sync (proxy, "Stop",
+                                     g_variant_new ("(s)", priv->userlang),
+                                     G_DBUS_CALL_FLAGS_NONE,
+                                     -1, NULL, error);
+       if (val == NULL)
+               return FALSE;
+       g_clear_pointer (&priv->address, g_free);
+       return TRUE;
+}
+
+static gboolean
+_client_subprocess_wait_check (GSubprocess *subprocess, GError **error)
+{
+       gint rc;
+       if (!g_subprocess_wait (subprocess, NULL, error)) {
+               g_prefix_error (error, "failed to run pkexec: ");
+               return FALSE;
+       }
+       rc = g_subprocess_get_exit_status (subprocess);
+       if (rc != 0) {
+               GInputStream *istream = g_subprocess_get_stderr_pipe (subprocess);
+               gchar buf[1024] = { 0x0 };
+               gsize sz = 0;
+               g_input_stream_read_all (istream, buf, sizeof(buf) - 1, &sz, NULL, NULL);
+               if (sz == 0) {
+                       g_set_error_literal (error, G_IO_ERROR, rc,
+                                            "Failed to run helper without stderr");
+                       return FALSE;
+               }
+               g_set_error_literal (error, G_IO_ERROR, rc, buf);
+               return FALSE;
+       }
+       return TRUE;
+}
+
+typedef enum {
+       _NOTIFY_EXPIRED,
+       _NOTIFY_REGISTRATION_REQUIRED,
+       _NOTIFY_REGISTERED
+} _NotifyKind;
+
+static void
+_show_notification (GsdSubscriptionManager *manager, _NotifyKind notify_kind)
+{
+       GsdSubscriptionManagerPrivate *priv = manager->priv;
+       switch (notify_kind) {
+       case _NOTIFY_EXPIRED:
+               notify_notification_close (priv->notification_registered, NULL);
+               notify_notification_close (priv->notification_registration_required, NULL);
+               notify_notification_show (priv->notification_expired, NULL);
+               break;
+       case _NOTIFY_REGISTRATION_REQUIRED:
+               notify_notification_close (priv->notification_registered, NULL);
+               notify_notification_close (priv->notification_expired, NULL);
+               notify_notification_show (priv->notification_registration_required, NULL);
+               break;
+       case _NOTIFY_REGISTERED:
+               notify_notification_close (priv->notification_expired, NULL);
+               notify_notification_close (priv->notification_registration_required, NULL);
+               notify_notification_show (priv->notification_registered, NULL);
+               break;
+       default:
+               break;
+       }
+       g_timer_reset (priv->timer_last_notified);
+}
+
+static void
+_client_maybe__show_notification (GsdSubscriptionManager *manager)
+{
+       GsdSubscriptionManagerPrivate *priv = manager->priv;
+
+       /* startup */
+       if (priv->subscription_status_last == GSD_SUBMAN_SUBSCRIPTION_STATUS_UNKNOWN &&
+           priv->subscription_status == GSD_SUBMAN_SUBSCRIPTION_STATUS_UNKNOWN) {
+               _show_notification (manager, _NOTIFY_REGISTRATION_REQUIRED);
+               return;
+       }
+
+       /* something changed */
+       if (priv->subscription_status_last != priv->subscription_status) {
+               g_debug ("transisition from subscription status '%s' to '%s'",
+                        gsd_subman_subscription_status_to_string (priv->subscription_status_last),
+                        gsd_subman_subscription_status_to_string (priv->subscription_status));
+
+               /* needs registration */
+               if (priv->subscription_status_last == GSD_SUBMAN_SUBSCRIPTION_STATUS_VALID &&
+                   priv->subscription_status == GSD_SUBMAN_SUBSCRIPTION_STATUS_INVALID) {
+                       _show_notification (manager, _NOTIFY_REGISTRATION_REQUIRED);
+                       return;
+               }
+
+               /* was unregistered */
+               if (priv->subscription_status_last == GSD_SUBMAN_SUBSCRIPTION_STATUS_VALID &&
+                   priv->subscription_status == GSD_SUBMAN_SUBSCRIPTION_STATUS_UNKNOWN) {
+                       _show_notification (manager, _NOTIFY_REGISTRATION_REQUIRED);
+                       return;
+               }
+
+               /* registered */
+               if (priv->subscription_status_last == GSD_SUBMAN_SUBSCRIPTION_STATUS_UNKNOWN &&
+                   priv->subscription_status == GSD_SUBMAN_SUBSCRIPTION_STATUS_VALID &&
+                   g_timer_elapsed (priv->timer_last_notified, NULL) > 60) {
+                       _show_notification (manager, _NOTIFY_REGISTERED);
+                       return;
+               }
+       }
+
+       /* nag again */
+       if (priv->subscription_status == GSD_SUBMAN_SUBSCRIPTION_STATUS_UNKNOWN &&
+           g_timer_elapsed (priv->timer_last_notified, NULL) > 60 * 60 * 24) {
+               _show_notification (manager, _NOTIFY_REGISTRATION_REQUIRED);
+               return;
+       }
+       if (priv->subscription_status == GSD_SUBMAN_SUBSCRIPTION_STATUS_INVALID &&
+           g_timer_elapsed (priv->timer_last_notified, NULL) > 60 * 60 * 24) {
+               _show_notification (manager, _NOTIFY_EXPIRED);
+               return;
+       }
+       if (priv->subscription_status == GSD_SUBMAN_SUBSCRIPTION_STATUS_PARTIALLY_VALID &&
+           g_timer_elapsed (priv->timer_last_notified, NULL) > 60 * 60 * 24) {
+               _show_notification (manager, _NOTIFY_EXPIRED);
+               return;
+       }
+}
+
+static gboolean
+_client_register_with_keys (GsdSubscriptionManager *manager,
+                                 const gchar *hostname,
+                                 const gchar *organisation,
+                                 const gchar *activation_key,
+                                 GError **error)
+{
+       GsdSubscriptionManagerPrivate *priv = manager->priv;
+       g_autoptr(GSubprocess) subprocess = NULL;
+
+       /* apparently: "we can't send registration credentials over the regular
+        * system or session bus since those aren't really locked down..." */
+       if (!_client_register_start (manager, error))
+               return FALSE;
+       g_debug ("spawning %s", LIBEXECDIR "/gsd-subman-helper");
+       subprocess = g_subprocess_new (G_SUBPROCESS_FLAGS_STDERR_PIPE, error,
+                                      "pkexec", LIBEXECDIR "/gsd-subman-helper",
+                                      "--kind", "register-with-key",
+                                      "--address", priv->address,
+                                      "--hostname", hostname,
+                                      "--organisation", organisation,
+                                      "--activation-key", activation_key,
+                                      NULL);
+       if (subprocess == NULL) {
+               g_prefix_error (error, "failed to find pkexec: ");
+               return FALSE;
+       }
+       if (!_client_subprocess_wait_check (subprocess, error))
+               return FALSE;
+
+       /* FIXME: also do on error? */
+       if (!_client_register_stop (manager, error))
+               return FALSE;
+       if (!_client_subscription_status_update (manager, error))
+               return FALSE;
+       _client_maybe__show_notification (manager);
+
+       /* success */
+       return TRUE;
+}
+
+static gboolean
+_client_register (GsdSubscriptionManager *manager,
+                        const gchar *hostname,
+                        const gchar *organisation,
+                        const gchar *username,
+                        const gchar *password,
+                        GError **error)
+{
+       GsdSubscriptionManagerPrivate *priv = manager->priv;
+       g_autoptr(GSubprocess) subprocess = NULL;
+
+       /* fallback */
+       if (organisation == NULL)
+               organisation = "";
+
+       /* apparently: "we can't send registration credentials over the regular
+        * system or session bus since those aren't really locked down..." */
+       if (!_client_register_start (manager, error))
+               return FALSE;
+       g_debug ("spawning %s", LIBEXECDIR "/gsd-subman-helper");
+       subprocess = g_subprocess_new (G_SUBPROCESS_FLAGS_STDERR_PIPE, error,
+                                      "pkexec", LIBEXECDIR "/gsd-subman-helper",
+                                      "--kind", "register-with-username",
+                                      "--address", priv->address,
+                                      "--hostname", hostname,
+                                      "--organisation", organisation,
+                                      "--username", username,
+                                      "--password", password,
+                                      NULL);
+       if (subprocess == NULL) {
+               g_prefix_error (error, "failed to find pkexec: ");
+               return FALSE;
+       }
+       if (!_client_subprocess_wait_check (subprocess, error))
+               return FALSE;
+
+       /* FIXME: also do on error? */
+       if (!_client_register_stop (manager, error))
+               return FALSE;
+       if (!_client_subscription_status_update (manager, error))
+               return FALSE;
+       _client_maybe__show_notification (manager);
+       return TRUE;
+}
+
+static gboolean
+_client_unregister (GsdSubscriptionManager *manager, GError **error)
+{
+       g_autoptr(GSubprocess) subprocess = NULL;
+
+       /* apparently: "we can't send registration credentials over the regular
+        * system or session bus since those aren't really locked down..." */
+       if (!_client_register_start (manager, error))
+               return FALSE;
+       g_debug ("spawning %s", LIBEXECDIR "/gsd-subman-helper");
+       subprocess = g_subprocess_new (G_SUBPROCESS_FLAGS_STDERR_PIPE, error,
+                                      "pkexec", LIBEXECDIR "/gsd-subman-helper",
+                                      "--kind", "unregister",
+                                      NULL);
+       if (subprocess == NULL) {
+               g_prefix_error (error, "failed to find pkexec: ");
+               return FALSE;
+       }
+       if (!_client_subprocess_wait_check (subprocess, error))
+               return FALSE;
+       if (!_client_subscription_status_update (manager, error))
+               return FALSE;
+       _client_maybe__show_notification (manager);
+       return TRUE;
+}
+
+static gboolean
+_client_update_config (GsdSubscriptionManager *manager, GError **error)
+{
+       GsdSubscriptionManagerPrivate *priv = manager->priv;
+       g_autoptr(GVariant) val = NULL;
+       g_autoptr(GVariant) val_server = NULL;
+       g_autoptr(GVariantDict) dict = NULL;
+       GVariantIter iter;
+       gchar *key;
+       gchar *value;
+
+       val = g_dbus_proxy_call_sync (priv->proxies[_RHSM_INTERFACE_CONFIG],
+                                     "GetAll",
+                                     g_variant_new ("(s)", priv->userlang),
+                                     G_DBUS_CALL_FLAGS_NONE,
+                                     -1, NULL, error);
+       if (val == NULL)
+               return FALSE;
+       dict = g_variant_dict_new (g_variant_get_child_value (val, 0));
+       val_server = g_variant_dict_lookup_value (dict, "server", G_VARIANT_TYPE("a{ss}"));
+       if (val_server != NULL) {
+               g_variant_iter_init (&iter, val_server);
+               while (g_variant_iter_next (&iter, "{ss}", &key, &value)) {
+                       g_debug ("%s=%s", key, value);
+                       g_hash_table_insert (priv->config,
+                                            g_steal_pointer (&key),
+                                            g_steal_pointer (&value));
+               }
+       }
+       return TRUE;
+}
+
+static void
+_subman_proxy_signal_cb (GDBusProxy *proxy,
+                        const gchar *sender_name,
+                        const gchar *signal_name,
+                        GVariant *parameters,
+                        GsdSubscriptionManager *manager)
+{
+       g_autoptr(GError) error = NULL;
+       if (!_client_syspurpose_update (manager, &error)) {
+               g_warning ("failed to update syspurpose: %s", error->message);
+               g_clear_error (&error);
+       }
+       if (!_client_subscription_status_update (manager, &error)) {
+               g_warning ("failed to update subscription status: %s", error->message);
+               g_clear_error (&error);
+       }
+       _client_maybe__show_notification (manager);
+}
+
+static void
+_client_unload (GsdSubscriptionManager *manager)
+{
+       GsdSubscriptionManagerPrivate *priv = manager->priv;
+       for (guint i = 0; i < _RHSM_INTERFACE_LAST; i++)
+               g_clear_object (&priv->proxies[i]);
+       g_hash_table_unref (priv->config);
+}
+
+static const gchar *
+_rhsm_interface_to_string (_RhsmInterface kind)
+{
+       if (kind == _RHSM_INTERFACE_CONFIG)
+               return "Config";
+       if (kind == _RHSM_INTERFACE_REGISTER_SERVER)
+               return "RegisterServer";
+       if (kind == _RHSM_INTERFACE_ATTACH)
+               return "Attach";
+       if (kind == _RHSM_INTERFACE_ENTITLEMENT)
+               return "Entitlement";
+       if (kind == _RHSM_INTERFACE_PRODUCTS)
+               return "Products";
+       if (kind == _RHSM_INTERFACE_CONSUMER)
+               return "Consumer";
+       if (kind == _RHSM_INTERFACE_SYSPURPOSE)
+               return "Syspurpose";
+       return NULL;
+}
+
+static gboolean
+_client_load (GsdSubscriptionManager *manager, GError **error)
+{
+       GsdSubscriptionManagerPrivate *priv = manager->priv;
+
+       priv->config = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
+
+       /* connect to all the interfaces on the *different* objects :| */
+       for (guint i = 0; i < _RHSM_INTERFACE_LAST; i++) {
+               const gchar *kind = _rhsm_interface_to_string (i);
+               g_autofree gchar *opath = g_strdup_printf ("/com/redhat/RHSM1/%s", kind);
+               g_autofree gchar *iface = g_strdup_printf ("com.redhat.RHSM1.%s", kind);
+               priv->proxies[i] =
+                       g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM,
+                                                      G_DBUS_PROXY_FLAGS_NONE,
+                                                      NULL,
+                                                      "com.redhat.RHSM1",
+                                                      opath, iface,
+                                                      NULL,
+                                                      error);
+               if (priv->proxies[i] == NULL)
+                       return FALSE;
+               /* we want to get notified if the status of the system changes */
+               g_signal_connect (priv->proxies[i], "g-signal",
+                                 G_CALLBACK (_subman_proxy_signal_cb), manager);
+       }
+
+       /* get initial status */
+       priv->userlang = "";
+       if (!_client_update_config (manager, error))
+               return FALSE;
+       if (!_client_subscription_status_update (manager, error))
+               return FALSE;
+       if (!_client_syspurpose_update (manager, error))
+               return FALSE;
+
+       /* success */
+       return TRUE;
+}
+
+gboolean
+gsd_subscription_manager_start (GsdSubscriptionManager *manager, GError **error)
+{
+       gboolean ret;
+       g_debug ("Starting subscription manager");
+       gnome_settings_profile_start (NULL);
+       ret = _client_load (manager, error);
+       _client_maybe__show_notification (manager);
+       gnome_settings_profile_end (NULL);
+       return ret;
+}
+
+void
+gsd_subscription_manager_stop (GsdSubscriptionManager *manager)
+{
+       g_debug ("Stopping subscription manager");
+       _client_unload (manager);
+}
+
+static void
+gsd_subscription_manager_class_init (GsdSubscriptionManagerClass *klass)
+{
+       GObjectClass *object_class = G_OBJECT_CLASS (klass);
+       object_class->finalize = gsd_subscription_manager_finalize;
+        notify_init ("gnome-settings-daemon");
+       g_type_class_add_private (klass, sizeof (GsdSubscriptionManagerPrivate));
+}
+
+static void
+emit_property_changed (GsdSubscriptionManager *manager,
+                      const gchar *property_name,
+                      GVariant *property_value)
+{
+       GsdSubscriptionManagerPrivate *priv = manager->priv;
+       GVariantBuilder builder;
+       GVariantBuilder invalidated_builder;
+
+       /* not yet connected */
+       if (priv->connection == NULL)
+               return;
+
+       /* build the dict */
+       g_variant_builder_init (&invalidated_builder, G_VARIANT_TYPE ("as"));
+       g_variant_builder_init (&builder, G_VARIANT_TYPE_ARRAY);
+       g_variant_builder_add (&builder,
+                              "{sv}",
+                              property_name,
+                              property_value);
+       g_dbus_connection_emit_signal (priv->connection,
+                                      NULL,
+                                      GSD_SUBSCRIPTION_DBUS_PATH,
+                                      "org.freedesktop.DBus.Properties",
+                                      "PropertiesChanged",
+                                      g_variant_new ("(sa{sv}as)",
+                                      GSD_SUBSCRIPTION_DBUS_INTERFACE,
+                                      &builder,
+                                      &invalidated_builder),
+                                      NULL);
+       g_variant_builder_clear (&builder);
+       g_variant_builder_clear (&invalidated_builder);
+}
+
+static void
+_launch_info_overview (void)
+{
+       const gchar *argv[] = { "gnome-control-center", "info-overview", NULL };
+       g_debug ("Running gnome-control-center info-overview");
+       g_spawn_async (NULL, (gchar **) argv, NULL, G_SPAWN_SEARCH_PATH,
+                      NULL, NULL, NULL, NULL);
+}
+
+static void
+_notify_closed_cb (NotifyNotification *notification, gpointer user_data)
+{
+       /* FIXME: only launch when clicking on the main body, not the window close */
+       if (notify_notification_get_closed_reason (notification) == 0x400)
+               _launch_info_overview ();
+}
+
+static void
+_notify_clicked_cb (NotifyNotification *notification, char *action, gpointer user_data)
+{
+       _launch_info_overview ();
+}
+
+static void
+gsd_subscription_manager_init (GsdSubscriptionManager *manager)
+{
+       GsdSubscriptionManagerPrivate *priv = manager->priv = GSD_SUBSCRIPTION_MANAGER_GET_PRIVATE (manager);
+
+       priv->timer_last_notified = g_timer_new ();
+
+       /* expired */
+       priv->notification_expired =
+               notify_notification_new (_("Subscription Has Expired"),
+                                        _("Add or renew a subscription to continue receiving software 
updates."),
+                                        NULL);
+       notify_notification_set_app_name (priv->notification_expired, _("Subscription"));
+       notify_notification_set_hint_string (priv->notification_expired, "desktop-entry", "subman-panel");
+       notify_notification_set_hint_string (priv->notification_expired, "x-gnome-privacy-scope", "system");
+       notify_notification_set_urgency (priv->notification_expired, NOTIFY_URGENCY_CRITICAL);
+       notify_notification_add_action (priv->notification_expired,
+                                       "info-overview", _("Subscribe System…"),
+                                       _notify_clicked_cb,
+                                       manager, NULL);
+       g_signal_connect (priv->notification_expired, "closed",
+                         G_CALLBACK (_notify_closed_cb), manager);
+
+       /* registered */
+       priv->notification_registered =
+               notify_notification_new (_("Registration Successful"),
+                                        _("The system has been registered and software updates have been 
enabled."),
+                                        NULL);
+       notify_notification_set_app_name (priv->notification_registered, _("Subscription"));
+       notify_notification_set_hint_string (priv->notification_registered, "desktop-entry", "subman-panel");
+       notify_notification_set_hint_string (priv->notification_registered, "x-gnome-privacy-scope", 
"system");
+       notify_notification_set_urgency (priv->notification_registered, NOTIFY_URGENCY_CRITICAL);
+       g_signal_connect (priv->notification_registered, "closed",
+                         G_CALLBACK (_notify_closed_cb), manager);
+
+       /* registration required */
+       priv->notification_registration_required =
+               notify_notification_new (_("System Not Registered"),
+                                        _("Please register your system to receive software updates."),
+                                        NULL);
+       notify_notification_set_app_name (priv->notification_registration_required, _("Subscription"));
+       notify_notification_set_hint_string (priv->notification_registration_required, "desktop-entry", 
"subman-panel");
+       notify_notification_set_hint_string (priv->notification_registration_required, 
"x-gnome-privacy-scope", "system");
+       notify_notification_set_urgency (priv->notification_registration_required, NOTIFY_URGENCY_CRITICAL);
+       notify_notification_add_action (priv->notification_registration_required,
+                                       "info-overview", _("Register System…"),
+                                       _notify_clicked_cb,
+                                       manager, NULL);
+       g_signal_connect (priv->notification_registration_required, "closed",
+                         G_CALLBACK (_notify_closed_cb), manager);
+}
+
+static void
+gsd_subscription_manager_finalize (GObject *object)
+{
+       GsdSubscriptionManager *manager;
+
+       g_return_if_fail (object != NULL);
+       g_return_if_fail (GSD_IS_SUBSCRIPTION_MANAGER (object));
+
+       manager = GSD_SUBSCRIPTION_MANAGER (object);
+
+       gsd_subscription_manager_stop (manager);
+
+       if (manager->priv->bus_cancellable != NULL) {
+               g_cancellable_cancel (manager->priv->bus_cancellable);
+               g_clear_object (&manager->priv->bus_cancellable);
+       }
+
+       g_clear_pointer (&manager->priv->introspection_data, g_dbus_node_info_unref);
+       g_clear_object (&manager->priv->connection);
+       g_clear_object (&manager->priv->notification_expired);
+       g_clear_object (&manager->priv->notification_registered);
+       g_timer_destroy (manager->priv->timer_last_notified);
+
+       if (manager->priv->name_id != 0) {
+               g_bus_unown_name (manager->priv->name_id);
+               manager->priv->name_id = 0;
+       }
+
+       if (manager->priv->check_registration_timeout_id)
+               g_source_remove (manager->priv->check_registration_timeout_id);
+
+       G_OBJECT_CLASS (gsd_subscription_manager_parent_class)->finalize (object);
+}
+
+static gboolean
+nlight_forced_timeout_cb (gpointer user_data)
+{
+       GsdSubscriptionManager *manager = GSD_SUBSCRIPTION_MANAGER (user_data);
+       GsdSubscriptionManagerPrivate *priv = manager->priv;
+
+       priv->check_registration_timeout_id = 0;
+       emit_property_changed (manager, "SubscriptionStatus",
+                              g_variant_new_boolean (TRUE));
+
+       return G_SOURCE_REMOVE;
+}
+
+static void
+handle_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)
+{
+       GsdSubscriptionManager *manager = GSD_SUBSCRIPTION_MANAGER (user_data);
+       GsdSubscriptionManagerPrivate *priv = manager->priv;
+       g_autoptr(GError) error = NULL;
+
+       if (g_strcmp0 (method_name, "Register") == 0) {
+               guint32 duration = 0;
+               const gchar *organisation = NULL;
+               const gchar *hostname = NULL;
+
+               if (FALSE) {
+                       g_dbus_method_invocation_return_error_literal (invocation,
+                                                                      G_IO_ERROR, G_IO_ERROR_NOT_INITIALIZED,
+                                                                      "Cannot register at this time");
+
+                       return;
+               }
+
+               g_autoptr(GVariantDict) dict = g_variant_dict_new (g_variant_get_child_value (parameters, 0));
+
+               const gchar *kind = NULL;
+               if (!g_variant_dict_lookup (dict, "kind", "&s", &kind)) {
+                       g_dbus_method_invocation_return_error_literal (invocation,
+                                                                      G_IO_ERROR, G_IO_ERROR_FAILED,
+                                                                      "No kind specified");
+
+                       return;
+               }
+               if (g_strcmp0 (kind, "username") == 0) {
+                       const gchar *username = NULL;
+                       const gchar *password = NULL;
+                       g_variant_dict_lookup (dict, "hostname", "&s", &hostname);
+                       g_variant_dict_lookup (dict, "organisation", "&s", &organisation);
+                       g_variant_dict_lookup (dict, "username", "&s", &username);
+                       g_variant_dict_lookup (dict, "password", "&s", &password);
+                       if (!_client_register (manager,
+                                                    hostname,
+                                                    organisation,
+                                                    username,
+                                                    password,
+                                                    &error)) {
+                               g_dbus_method_invocation_return_gerror (invocation, error);
+                               return;
+                       }
+               } else if (g_strcmp0 (kind, "key") == 0) {
+                       const gchar *activation_key = NULL;
+                       g_variant_dict_lookup (dict, "hostname", "&s", &hostname);
+                       g_variant_dict_lookup (dict, "organisation", "&s", &organisation);
+                       g_variant_dict_lookup (dict, "activation-key", "&s", &activation_key);
+                       if (!_client_register_with_keys (manager,
+                                                              hostname,
+                                                              organisation,
+                                                              activation_key,
+                                                              &error)) {
+                               g_dbus_method_invocation_return_gerror (invocation, error);
+                               return;
+                       }
+               } else {
+                       g_dbus_method_invocation_return_error_literal (invocation,
+                                                                      G_IO_ERROR, G_IO_ERROR_FAILED,
+                                                                      "Invalid kind specified");
+
+                       return;
+               }
+               if (priv->check_registration_timeout_id)
+                       g_source_remove (priv->check_registration_timeout_id);
+               priv->check_registration_timeout_id = g_timeout_add_seconds (duration, 
nlight_forced_timeout_cb, manager);
+
+               g_dbus_method_invocation_return_value (invocation, NULL);
+       } else if (g_strcmp0 (method_name, "Unregister") == 0) {
+               if (!_client_unregister (manager, &error)) {
+                       g_dbus_method_invocation_return_gerror (invocation, error);
+                       return;
+               }
+               g_dbus_method_invocation_return_value (invocation, NULL);
+       } else {
+               g_assert_not_reached ();
+       }
+}
+
+static GVariant *
+handle_get_property (GDBusConnection *connection,
+                    const gchar *sender,
+                    const gchar *object_path,
+                    const gchar *interface_name,
+                    const gchar *property_name,
+                    GError **error, gpointer user_data)
+{
+       GsdSubscriptionManager *manager = GSD_SUBSCRIPTION_MANAGER (user_data);
+       GsdSubscriptionManagerPrivate *priv = manager->priv;
+
+       if (g_strcmp0 (interface_name, GSD_SUBSCRIPTION_DBUS_INTERFACE) != 0) {
+               g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
+                            "No such interface: %s", interface_name);
+               return NULL;
+       }
+
+       if (g_strcmp0 (property_name, "SubscriptionStatus") == 0)
+               return g_variant_new_uint32 (priv->subscription_status);
+
+       g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
+                    "Failed to get property: %s", property_name);
+       return NULL;
+}
+
+static gboolean
+handle_set_property (GDBusConnection *connection,
+                    const gchar *sender,
+                    const gchar *object_path,
+                    const gchar *interface_name,
+                    const gchar *property_name,
+                    GVariant *value,
+                    GError **error, gpointer user_data)
+{
+       if (g_strcmp0 (interface_name, GSD_SUBSCRIPTION_DBUS_INTERFACE) != 0) {
+               g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
+                            "No such interface: %s", interface_name);
+               return FALSE;
+       }
+       g_set_error (error, G_DBUS_ERROR, G_DBUS_ERROR_FAILED,
+                    "No such property: %s", property_name);
+       return FALSE;
+}
+
+static const GDBusInterfaceVTable interface_vtable =
+{
+       handle_method_call,
+       handle_get_property,
+       handle_set_property
+};
+
+static void
+name_lost_handler_cb (GDBusConnection *connection, const gchar *name, gpointer user_data)
+{
+       g_debug ("lost name, so exiting");
+       gtk_main_quit ();
+}
+
+static void
+on_bus_gotten (GObject *source_object, GAsyncResult *res, GsdSubscriptionManager *manager)
+{
+       GsdSubscriptionManagerPrivate *priv = manager->priv;
+       GDBusConnection *connection;
+       g_autoptr(GError) error = NULL;
+
+       connection = g_bus_get_finish (res, &error);
+       if (connection == NULL) {
+               if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+                       g_warning ("Could not get session bus: %s", error->message);
+               return;
+       }
+
+       priv->connection = connection;
+       g_dbus_connection_register_object (connection,
+                                          GSD_SUBSCRIPTION_DBUS_PATH,
+                                          priv->introspection_data->interfaces[0],
+                                          &interface_vtable,
+                                          manager,
+                                          NULL,
+                                          NULL);
+       priv->name_id = g_bus_own_name_on_connection (connection,
+                                                     GSD_SUBSCRIPTION_DBUS_NAME,
+                                                     G_BUS_NAME_OWNER_FLAGS_NONE,
+                                                     NULL,
+                                                     name_lost_handler_cb,
+                                                     manager,
+                                                     NULL);
+}
+
+static void
+register_manager_dbus (GsdSubscriptionManager *manager)
+{
+       GsdSubscriptionManagerPrivate *priv = manager->priv;
+
+       priv->introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL);
+       g_assert (priv->introspection_data != NULL);
+       priv->bus_cancellable = g_cancellable_new ();
+
+       g_bus_get (G_BUS_TYPE_SESSION, priv->bus_cancellable,
+                  (GAsyncReadyCallback) on_bus_gotten, manager);
+}
+
+GsdSubscriptionManager *
+gsd_subscription_manager_new (void)
+{
+       if (manager_object != NULL) {
+               g_object_ref (manager_object);
+       } else {
+               manager_object = g_object_new (GSD_TYPE_SUBSCRIPTION_MANAGER, NULL);
+               g_object_add_weak_pointer (manager_object,
+                                          (gpointer *) &manager_object);
+               register_manager_dbus (manager_object);
+       }
+
+       return GSD_SUBSCRIPTION_MANAGER (manager_object);
+}
diff --git a/plugins/subman/gsd-subscription-manager.h b/plugins/subman/gsd-subscription-manager.h
new file mode 100644
index 00000000..6a524b1b
--- /dev/null
+++ b/plugins/subman/gsd-subscription-manager.h
@@ -0,0 +1,63 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 8 -*-
+ *
+ * Copyright (C) 2019 Richard Hughes <richard hughsie com>
+ *
+ * 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 2 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, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef __GSD_SUBSCRIPTION_MANAGER_H
+#define __GSD_SUBSCRIPTION_MANAGER_H
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GSD_TYPE_SUBSCRIPTION_MANAGER          (gsd_subscription_manager_get_type ())
+#define GSD_SUBSCRIPTION_MANAGER(o)            (G_TYPE_CHECK_INSTANCE_CAST ((o), 
GSD_TYPE_SUBSCRIPTION_MANAGER, GsdSubscriptionManager))
+#define GSD_SUBSCRIPTION_MANAGER_CLASS(k)      (G_TYPE_CHECK_CLASS_CAST((k), GSD_TYPE_SUBSCRIPTION_MANAGER, 
GsdSubscriptionManagerClass))
+#define GSD_IS_SUBSCRIPTION_MANAGER(o)         (G_TYPE_CHECK_INSTANCE_TYPE ((o), 
GSD_TYPE_SUBSCRIPTION_MANAGER))
+#define GSD_IS_SUBSCRIPTION_MANAGER_CLASS(k)   (G_TYPE_CHECK_CLASS_TYPE ((k), GSD_TYPE_SUBSCRIPTION_MANAGER))
+#define GSD_SUBSCRIPTION_MANAGER_GET_CLASS(o)  (G_TYPE_INSTANCE_GET_CLASS ((o), 
GSD_TYPE_SUBSCRIPTION_MANAGER, GsdSubscriptionManagerClass))
+#define GSD_SUBSCRIPTION_MANAGER_ERROR         (gsd_subscription_manager_error_quark ())
+
+typedef struct GsdSubscriptionManagerPrivate GsdSubscriptionManagerPrivate;
+
+typedef struct
+{
+       GObject                          parent;
+       GsdSubscriptionManagerPrivate   *priv;
+} GsdSubscriptionManager;
+
+typedef struct
+{
+       GObjectClass                    parent_class;
+} GsdSubscriptionManagerClass;
+
+enum
+{
+       GSD_SUBSCRIPTION_MANAGER_ERROR_FAILED
+};
+
+GType                  gsd_subscription_manager_get_type       (void);
+GQuark                 gsd_subscription_manager_error_quark    (void);
+
+GsdSubscriptionManager *gsd_subscription_manager_new           (void);
+gboolean               gsd_subscription_manager_start          (GsdSubscriptionManager *manager,
+                                                                GError                **error);
+void                   gsd_subscription_manager_stop           (GsdSubscriptionManager *manager);
+
+G_END_DECLS
+
+#endif /* __GSD_SUBSCRIPTION_MANAGER_H */
diff --git a/plugins/subman/main.c b/plugins/subman/main.c
new file mode 100644
index 00000000..28ac995b
--- /dev/null
+++ b/plugins/subman/main.c
@@ -0,0 +1,8 @@
+#define NEW gsd_subscription_manager_new
+#define START gsd_subscription_manager_start
+#define STOP gsd_subscription_manager_stop
+#define MANAGER GsdSubscriptionManager
+#define GDK_BACKEND "x11"
+#include "gsd-subscription-manager.h"
+
+#include "daemon-skeleton-gtk.h"
diff --git a/plugins/subman/meson.build b/plugins/subman/meson.build
new file mode 100644
index 00000000..bfd073b6
--- /dev/null
+++ b/plugins/subman/meson.build
@@ -0,0 +1,56 @@
+sources = files(
+  'gsd-subscription-manager.c',
+  'gsd-subman-common.c',
+  'main.c'
+)
+
+deps = plugins_deps + [
+  libnotify_dep,
+  gtk_dep,
+  jsonglib_dep,
+  m_dep,
+]
+
+cflags += ['-DBINDIR="@0@"'.format(gsd_bindir)]
+cflags += ['-DLIBEXECDIR="@0@"'.format(gsd_libexecdir)]
+
+executable(
+  'gsd-' + plugin_name,
+  sources,
+  include_directories: [top_inc, common_inc],
+  dependencies: deps,
+  c_args: cflags,
+  install: true,
+  install_rpath: gsd_pkglibdir,
+  install_dir: gsd_libexecdir
+)
+
+# .Register needs to be called from root as subman can't do PolicyKit...
+policy = 'org.gnome.settings-daemon.plugins.subman.policy'
+policy_in = configure_file(
+  input: policy + '.in.in',
+  output: policy + '.in',
+  configuration: plugins_conf
+)
+
+i18n.merge_file(
+  policy,
+  input: policy_in,
+  output: policy,
+  po_dir: po_dir,
+  install: true,
+  install_dir: join_paths(gsd_datadir, 'polkit-1', 'actions')
+)
+
+install_data('org.gnome.settings-daemon.plugins.subman.rules',
+             install_dir : join_paths(gsd_datadir, 'polkit-1', 'rules.d'))
+
+executable(
+  'gsd-subman-helper',
+  'gsd-subman-helper.c',
+  include_directories: top_inc,
+  dependencies: [gio_dep, jsonglib_dep],
+  install: true,
+  install_rpath: gsd_pkglibdir,
+  install_dir: gsd_libexecdir
+)
diff --git a/plugins/subman/org.gnome.SettingsDaemon.Subscription.desktop.in 
b/plugins/subman/org.gnome.SettingsDaemon.Subscription.desktop.in
new file mode 100644
index 00000000..b8b9511a
--- /dev/null
+++ b/plugins/subman/org.gnome.SettingsDaemon.Subscription.desktop.in
@@ -0,0 +1,9 @@
+[Desktop Entry]
+Type=Application
+Name=GNOME Settings Daemon's subscription manager plugin
+Exec=@libexecdir@/gsd-subscription
+OnlyShowIn=GNOME;
+NoDisplay=true
+X-GNOME-Autostart-Phase=Initialization
+X-GNOME-Autostart-Notify=true
+X-GNOME-AutoRestart=true
diff --git a/plugins/subman/org.gnome.settings-daemon.plugins.subman.policy.in.in 
b/plugins/subman/org.gnome.settings-daemon.plugins.subman.policy.in.in
new file mode 100644
index 00000000..cf010549
--- /dev/null
+++ b/plugins/subman/org.gnome.settings-daemon.plugins.subman.policy.in.in
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE policyconfig PUBLIC
+ "-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
+ "http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd";>
+<policyconfig>
+
+  <!--
+    Policy definitions for gnome-settings-daemon system-wide actions.
+    Copyright (c) 2019 Richard Hughes <richard hughsie com>
+  -->
+
+  <vendor>GNOME Settings Daemon</vendor>
+  <vendor_url>http://git.gnome.org/browse/gnome-settings-daemon</vendor_url>
+  <icon_name>emblem-synchronizing</icon_name>
+
+  <action id="org.gnome.settings-daemon.plugins.subman.register">
+    <description>Register the system</description>
+    <message>Authentication is required to register the system</message>
+    <defaults>
+      <allow_any>no</allow_any>
+      <allow_inactive>no</allow_inactive>
+      <allow_active>auth_admin_keep</allow_active>
+    </defaults>
+    <annotate key="org.freedesktop.policykit.exec.path">@libexecdir@/gsd-subman-helper</annotate>
+  </action>
+
+</policyconfig>
+
diff --git a/plugins/subman/org.gnome.settings-daemon.plugins.subman.rules 
b/plugins/subman/org.gnome.settings-daemon.plugins.subman.rules
new file mode 100644
index 00000000..1ed3a0ea
--- /dev/null
+++ b/plugins/subman/org.gnome.settings-daemon.plugins.subman.rules
@@ -0,0 +1,7 @@
+polkit.addRule(function(action, subject) {
+    if (action.id == "org.gnome.settings-daemon.plugins.subman.register" &&
+        subject.active == true && subject.local == true &&
+        subject.isInGroup("wheel")) {
+            return polkit.Result.YES;
+    }
+});


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]