[gnome-software] snap: Use snap authentication



commit b25e3c05aaf53b42cb8c24ff909f81cb23d04ba8
Author: Robert Ancell <robert ancell canonical com>
Date:   Fri Jul 1 15:33:07 2016 +1200

    snap: Use snap authentication

 src/plugins/gs-plugin-snap.c |  235 ++++++++++++++++++++++++++++++++++++++++++
 src/plugins/gs-snapd.c       |   86 +++++++++++++++
 src/plugins/gs-snapd.h       |   15 +++
 3 files changed, 336 insertions(+), 0 deletions(-)
---
diff --git a/src/plugins/gs-plugin-snap.c b/src/plugins/gs-plugin-snap.c
index aad25c0..ab92912 100644
--- a/src/plugins/gs-plugin-snap.c
+++ b/src/plugins/gs-plugin-snap.c
@@ -26,22 +26,47 @@
 
 #include "gs-snapd.h"
 
+struct GsPluginData {
+       GsAuth          *auth;
+};
+
 typedef gboolean (*AppFilterFunc)(const gchar *id, JsonObject *object, gpointer data);
 
 void
 gs_plugin_initialize (GsPlugin *plugin)
 {
+       GsPluginData *priv = gs_plugin_alloc_data (plugin, sizeof(GsPluginData));
+
        if (!gs_snapd_exists ()) {
                g_debug ("disabling '%s' as snapd not running",
                         gs_plugin_get_name (plugin));
                gs_plugin_set_enabled (plugin, FALSE);
        }
 
+       priv->auth = gs_auth_new ("snapd");
+       gs_auth_set_provider_name (priv->auth, "Snap Store");
+       gs_auth_set_provider_schema (priv->auth, "com.ubuntu.UbuntuOne.GnomeSoftware");
+       gs_plugin_add_auth (plugin, priv->auth);
+
        gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "desktop-categories");
        gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_RUN_AFTER, "ubuntu-reviews");
        gs_plugin_add_rule (plugin, GS_PLUGIN_RULE_BETTER_THAN, "packagekit");
 }
 
+gboolean
+gs_plugin_setup (GsPlugin *plugin, GCancellable *cancellable, GError **error)
+{
+       GsPluginData *priv = gs_plugin_get_data (plugin);
+
+       /* load from disk */
+       gs_auth_add_metadata (priv->auth, "macaroon", NULL);
+       if (!gs_auth_load (priv->auth, cancellable, error))
+               return FALSE;
+
+       /* success */
+       return TRUE;
+}
+
 static JsonParser *
 parse_result (const gchar *response, const gchar *response_type, GError **error)
 {
@@ -84,12 +109,43 @@ parse_result (const gchar *response, const gchar *response_type, GError **error)
 }
 
 static void
+get_macaroon (GsPlugin *plugin, gchar **macaroon, gchar ***discharges)
+{
+       GsAuth *auth;
+       gchar *serialized_macaroon;
+       g_autoptr(GVariant) macaroon_variant = NULL;
+       g_autoptr (GError) error_local = NULL;
+
+       *macaroon = NULL;
+       *discharges = NULL;
+
+       auth = gs_plugin_get_auth_by_id (plugin, "snapd");
+       if (auth == NULL)
+               return;
+       serialized_macaroon = gs_auth_get_metadata_item (auth, "macaroon");
+       if (serialized_macaroon == NULL)
+               return;
+       macaroon_variant = g_variant_parse (G_VARIANT_TYPE ("(sas)"),
+                                           serialized_macaroon,
+                                           NULL,
+                                           NULL,
+                                           NULL);
+       if (macaroon_variant == NULL)
+               return;
+       g_variant_get (macaroon_variant, "(s^as)", macaroon, discharges);
+}
+
+static void
 refine_app (GsPlugin *plugin, GsApp *app, JsonObject *package, gboolean from_search, GCancellable 
*cancellable)
 {
+       g_autofree gchar *macaroon = NULL;
+       g_auto(GStrv) discharges = NULL;
        const gchar *status, *icon_url, *launch_name = NULL;
        g_autoptr(GdkPixbuf) icon_pixbuf = NULL;
        gint64 size = -1;
 
+       get_macaroon (plugin, &macaroon, &discharges);
+
        status = json_object_get_string_member (package, "status");
        if (g_strcmp0 (status, "installed") == 0 || g_strcmp0 (status, "active") == 0) {
                const gchar *update_available;
@@ -128,6 +184,7 @@ refine_app (GsPlugin *plugin, GsApp *app, JsonObject *package, gboolean from_sea
                gsize icon_response_length;
 
                if (gs_snapd_request ("GET", icon_url, NULL,
+                                     macaroon, discharges,
                                      NULL, NULL,
                                      NULL, &icon_response, &icon_response_length,
                                      cancellable, NULL)) {
@@ -194,6 +251,8 @@ get_apps (GsPlugin *plugin,
          GCancellable *cancellable,
          GError **error)
 {
+       g_autofree gchar *macaroon = NULL;
+       g_auto(GStrv) discharges = NULL;
        guint status_code;
        GPtrArray *query_fields;
        g_autoptr (GString) path = NULL;
@@ -204,6 +263,8 @@ get_apps (GsPlugin *plugin,
        GList *snaps;
        GList *l;
 
+       get_macaroon (plugin, &macaroon, &discharges);
+
        /* Get all the apps */
        query_fields = g_ptr_array_new_with_free_func (g_free);
        if (sources != NULL) {
@@ -237,6 +298,7 @@ get_apps (GsPlugin *plugin,
        }
        g_ptr_array_free (query_fields, TRUE);
        if (!gs_snapd_request ("GET", path->str, NULL,
+                              macaroon, discharges,
                               &status_code, &reason_phrase,
                               &response_type, &response, NULL,
                               cancellable, error))
@@ -285,6 +347,8 @@ get_apps (GsPlugin *plugin,
 static gboolean
 get_app (GsPlugin *plugin, GsApp *app, GCancellable *cancellable, GError **error)
 {
+       g_autofree gchar *macaroon = NULL;
+       g_auto(GStrv) discharges = NULL;
        guint status_code;
        g_autofree gchar *path = NULL;
        g_autofree gchar *reason_phrase = NULL;
@@ -294,8 +358,11 @@ get_app (GsPlugin *plugin, GsApp *app, GCancellable *cancellable, GError **error
        JsonObject *root;
        JsonObject *result;
 
+       get_macaroon (plugin, &macaroon, &discharges);
+
        path = g_strdup_printf ("/v2/snaps/%s", gs_app_get_id (app));
        if (!gs_snapd_request ("GET", path, NULL,
+                              macaroon, discharges,
                               &status_code, &reason_phrase,
                               &response_type, &response, NULL,
                               cancellable, error))
@@ -331,6 +398,8 @@ get_app (GsPlugin *plugin, GsApp *app, GCancellable *cancellable, GError **error
 void
 gs_plugin_destroy (GsPlugin *plugin)
 {
+       GsPluginData *priv = gs_plugin_get_data (plugin);
+       g_clear_object (&priv->auth);
 }
 
 static gboolean
@@ -382,6 +451,8 @@ send_package_action (GsPlugin *plugin,
                     GCancellable *cancellable,
                     GError **error)
 {
+       g_autofree gchar *macaroon = NULL;
+       g_auto(GStrv) discharges = NULL;
        g_autofree gchar *content = NULL, *path = NULL;
        guint status_code;
        g_autofree gchar *reason_phrase = NULL;
@@ -397,14 +468,25 @@ send_package_action (GsPlugin *plugin,
        const gchar *type;
        const gchar *change_id;
 
+       get_macaroon (plugin, &macaroon, &discharges);
+
        content = g_strdup_printf ("{\"action\": \"%s\"}", action);
        path = g_strdup_printf ("/v2/snaps/%s", id);
        if (!gs_snapd_request ("POST", path, content,
+                              macaroon, discharges,
                               &status_code, &reason_phrase,
                               &response_type, &response, NULL,
                               cancellable, error))
                return FALSE;
 
+       if (status_code == SOUP_STATUS_UNAUTHORIZED) {
+               g_set_error_literal (error,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_AUTH_REQUIRED,
+                                    "Requires authentication with @snapd");
+               return FALSE;
+       }
+
        if (status_code != SOUP_STATUS_ACCEPTED) {
                g_set_error (error,
                             GS_PLUGIN_ERROR,
@@ -435,6 +517,7 @@ send_package_action (GsPlugin *plugin,
                        g_usleep (100 * 1000);
 
                        if (!gs_snapd_request ("GET", resource_path, NULL,
+                                              macaroon, discharges,
                                               &status_code, &status_reason_phrase,
                                               &status_response_type, &status_response, NULL,
                                               cancellable, error)) {
@@ -571,3 +654,155 @@ gs_plugin_app_remove (GsPlugin *plugin,
        gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
        return TRUE;
 }
+
+gboolean
+gs_plugin_auth_login (GsPlugin *plugin, GsAuth *auth,
+                     GCancellable *cancellable, GError **error)
+{
+       GsPluginData *priv = gs_plugin_get_data (plugin);
+       g_autoptr(JsonBuilder) builder = NULL;
+       g_autoptr(JsonNode) json_root = NULL;
+       g_autoptr(JsonGenerator) json_generator = NULL;
+       g_autofree gchar *data = NULL;
+       guint status_code;
+       g_autofree gchar *reason_phrase = NULL;
+       g_autofree gchar *response_type = NULL;
+       g_autofree gchar *response = NULL;
+       g_autoptr(JsonObject) result = NULL;
+       JsonArray *discharges;
+       guint i;
+       g_autoptr(GVariantBuilder) b = NULL;
+       g_autoptr(GVariant) macaroon_variant = NULL;
+       g_autofree gchar *serialized_macaroon = NULL;
+
+       if (auth != priv->auth)
+               return TRUE;
+
+       builder = json_builder_new ();
+       json_builder_begin_object (builder);
+       json_builder_set_member_name (builder, "username");
+       json_builder_add_string_value (builder, gs_auth_get_username (auth));
+       json_builder_set_member_name (builder, "password");
+       json_builder_add_string_value (builder, gs_auth_get_password (auth));
+       if (gs_auth_get_pin (auth)) {
+               json_builder_set_member_name (builder, "otp");
+               json_builder_add_string_value (builder, gs_auth_get_pin (auth));
+       }
+       json_builder_end_object (builder);
+
+       json_root = json_builder_get_root (builder);
+       json_generator = json_generator_new ();
+       json_generator_set_pretty (json_generator, TRUE);
+       json_generator_set_root (json_generator, json_root);
+       data = json_generator_to_data (json_generator, NULL);
+       if (data == NULL) {
+               g_set_error_literal (error,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_FAILED,
+                                    "Failed to generate JSON request");
+               return FALSE;
+       }
+
+       if (!gs_snapd_request ("POST", "/v2/login", data,
+                              NULL, NULL,
+                              &status_code, &reason_phrase,
+                              &response_type, &response, NULL,
+                              cancellable, error))
+               return FALSE;
+
+       if (status_code != SOUP_STATUS_OK) {
+               g_autofree gchar *error_message = NULL;
+               g_autofree gchar *error_kind = NULL;
+
+               if (!gs_snapd_parse_error (response_type, response, &error_message, &error_kind, error))
+                       return FALSE;
+
+               if (g_strcmp0 (error_kind, "two-factor-required") == 0) {
+                       g_set_error_literal (error,
+                                            GS_PLUGIN_ERROR,
+                                            GS_PLUGIN_ERROR_PIN_REQUIRED,
+                                            error_message);
+               }
+               else {
+                       g_set_error_literal (error,
+                                            GS_PLUGIN_ERROR,
+                                            GS_PLUGIN_ERROR_FAILED,
+                                            error_message);
+               }
+               return FALSE;
+       }
+
+       if (!gs_snapd_parse_result (response_type, response, &result, error))
+               return FALSE;
+
+       if (!json_object_has_member (result, "macaroon")) {
+               g_set_error_literal (error,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_FAILED,
+                                    "Login response missing macaroon");
+               return FALSE;
+       }
+       discharges = json_object_get_array_member (result, "discharges");
+       b = g_variant_builder_new (G_VARIANT_TYPE ("as"));
+       for (i = 0; i < json_array_get_length (discharges); i++) {
+               JsonNode *node;
+               node = json_array_get_element (discharges, i);
+               if (!JSON_NODE_HOLDS_VALUE (node) && json_node_get_value_type (node) != G_TYPE_STRING) {
+                       g_set_error_literal (error,
+                                            GS_PLUGIN_ERROR,
+                                            GS_PLUGIN_ERROR_FAILED,
+                                            "Macaroon discharge contains unexpected value");
+                       return FALSE;
+               }
+               g_variant_builder_add (b, "s", json_node_get_string (node));
+       }
+       macaroon_variant = g_variant_new ("(sas)",
+                                         json_object_get_string_member (result, "macaroon"),
+                                         b);
+       serialized_macaroon = g_variant_print (macaroon_variant, FALSE);
+       gs_auth_add_metadata (auth, "macaroon", serialized_macaroon);
+
+       /* store */
+       if (!gs_auth_save (auth, cancellable, error))
+               return FALSE;
+
+       gs_auth_add_flags (priv->auth, GS_AUTH_FLAG_VALID);
+
+       return TRUE;
+}
+
+gboolean
+gs_plugin_auth_lost_password (GsPlugin *plugin, GsAuth *auth,
+                             GCancellable *cancellable, GError **error)
+{
+       GsPluginData *priv = gs_plugin_get_data (plugin);
+
+       if (auth != priv->auth)
+               return TRUE;
+
+       // FIXME: snapd might not be using Ubuntu One accounts
+       // https://bugs.launchpad.net/bugs/1598667
+       g_set_error_literal (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_AUTH_INVALID,
+                            "do online using @https://login.ubuntu.com/+forgot_password";);
+       return FALSE;
+}
+
+gboolean
+gs_plugin_auth_register (GsPlugin *plugin, GsAuth *auth,
+                        GCancellable *cancellable, GError **error)
+{
+       GsPluginData *priv = gs_plugin_get_data (plugin);
+
+       if (auth != priv->auth)
+               return TRUE;
+
+       // FIXME: snapd might not be using Ubuntu One accounts
+       // https://bugs.launchpad.net/bugs/1598667
+       g_set_error_literal (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_AUTH_INVALID,
+                            "do online using @https://login.ubuntu.com/+login";);
+       return FALSE;
+}
diff --git a/src/plugins/gs-snapd.c b/src/plugins/gs-snapd.c
index 3a6a959..1a36d8d 100644
--- a/src/plugins/gs-snapd.c
+++ b/src/plugins/gs-snapd.c
@@ -95,6 +95,8 @@ gboolean
 gs_snapd_request (const gchar  *method,
                  const gchar  *path,
                  const gchar  *content,
+                 const gchar  *macaroon,
+                 gchar       **discharges,
                  guint        *status_code,
                  gchar       **reason_phrase,
                  gchar       **response_type,
@@ -123,6 +125,14 @@ gs_snapd_request (const gchar  *method,
        request = g_string_new ("");
        g_string_append_printf (request, "%s %s HTTP/1.1\r\n", method, path);
        g_string_append (request, "Host:\r\n");
+       if (macaroon != NULL) {
+               gint i;
+
+               g_string_append_printf (request, "Authorization: Macaroon root=\"%s\"", macaroon);
+               for (i = 0; discharges[i] != NULL; i++)
+                       g_string_append_printf (request, ",discharge=\"%s\"", discharges[i]);
+               g_string_append (request, "\r\n");
+       }
        if (content)
                g_string_append_printf (request, "Content-Length: %zi\r\n", strlen (content));
        g_string_append (request, "\r\n");
@@ -288,3 +298,79 @@ gs_snapd_request (const gchar  *method,
 
        return TRUE;
 }
+
+gboolean
+gs_snapd_parse_result (const gchar     *response_type,
+                      const gchar      *response,
+                      JsonObject       **result,
+                      GError           **error)
+{
+       g_autoptr(JsonParser) parser = NULL;
+       g_autoptr(GError) error_local = NULL;
+       JsonObject *root;
+
+       if (response_type == NULL) {
+               g_set_error_literal (error,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_FAILED,
+                                    "snapd returned no content type");
+               return FALSE;
+       }
+       if (g_strcmp0 (response_type, "application/json") != 0) {
+               g_set_error (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_FAILED,
+                            "snapd returned unexpected content type %s", response_type);
+               return FALSE;
+       }
+
+       parser = json_parser_new ();
+       if (!json_parser_load_from_data (parser, response, -1, &error_local)) {
+               g_set_error (error,
+                            GS_PLUGIN_ERROR,
+                            GS_PLUGIN_ERROR_FAILED,
+                            "Unable to parse snapd response: %s",
+                            error_local->message);
+               return FALSE;
+       }
+
+       if (!JSON_NODE_HOLDS_OBJECT (json_parser_get_root (parser))) {
+               g_set_error_literal (error,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_FAILED,
+                                    "snapd response does is not a valid JSON object");
+               return FALSE;
+       }
+       root = json_node_get_object (json_parser_get_root (parser));
+       if (!json_object_has_member (root, "result")) {
+               g_set_error_literal (error,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_FAILED,
+                                    "snapd response does not contain a \"result\" field");
+               return FALSE;
+       }
+       if (result != NULL)
+               *result = json_object_ref (json_object_get_object_member (root, "result"));
+
+       return TRUE;
+}
+
+gboolean
+gs_snapd_parse_error (const gchar      *response_type,
+                     const gchar       *response,
+                     gchar             **message,
+                     gchar             **kind,
+                     GError            **error)
+{
+       g_autoptr(JsonObject) result = NULL;
+
+       if (!gs_snapd_parse_result (response_type, response, &result, error))
+               return FALSE;
+
+       if (message != NULL)
+               *message = g_strdup (json_object_get_string_member (result, "message"));
+       if (kind != NULL)
+               *kind = json_object_has_member (result, "kind") ? g_strdup (json_object_get_string_member 
(result, "kind")) : NULL;
+
+       return TRUE;
+}
diff --git a/src/plugins/gs-snapd.h b/src/plugins/gs-snapd.h
index e84d36b..7c0eb9c 100644
--- a/src/plugins/gs-snapd.h
+++ b/src/plugins/gs-snapd.h
@@ -23,12 +23,15 @@
 #define __GS_SNAPD_H__
 
 #include <gio/gio.h>
+#include <json-glib/json-glib.h>
 
 gboolean gs_snapd_exists       (void);
 
 gboolean gs_snapd_request      (const gchar    *method,
                                 const gchar    *path,
                                 const gchar    *content,
+                                const gchar    *macaroon,
+                                gchar          **discharges,
                                 guint          *status_code,
                                 gchar          **reason_phrase,
                                 gchar          **response_type,
@@ -37,4 +40,16 @@ gboolean gs_snapd_request    (const gchar    *method,
                                 GCancellable   *cancellable,
                                 GError         **error);
 
+gboolean gs_snapd_parse_result (const gchar    *response_type,
+                                const gchar    *response,
+                                JsonObject     **result,
+                                GError         **error);
+
+gboolean gs_snapd_parse_error  (const gchar    *response_type,
+                                const gchar    *response,
+                                gchar          **message,
+                                gchar          **kind,
+                                GError         **error);
+
+
 #endif /* __GS_SNAPD_H__ */


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