From e74faf44753799282be0e595ca7de1308af55c8b Mon Sep 17 00:00:00 2001 From: Jan-Michael Brummer Date: Tue, 19 Dec 2023 09:54:21 +0100 Subject: [PATCH] Add Microsoft 365 support Add Microsoft 365 provider to enable initial O365 integration (files). --- meson.build | 18 + meson_options.txt | 3 + src/goabackend/goamsgraphprovider.c | 597 ++++++++++++++++++++++++++++ src/goabackend/goamsgraphprovider.h | 43 ++ src/goabackend/goaoauth2handler.c | 6 + src/goabackend/goaoauth2provider.c | 123 ++++-- src/goabackend/goaprovider.c | 4 + src/goabackend/meson.build | 1 + 8 files changed, 765 insertions(+), 30 deletions(-) create mode 100644 src/goabackend/goamsgraphprovider.c create mode 100644 src/goabackend/goamsgraphprovider.h diff --git a/meson.build b/meson.build index 3fbf27a5..02517c9e 100644 --- a/meson.build +++ b/meson.build @@ -182,6 +182,15 @@ config_h.set_quoted('GOA_WINDOWS_LIVE_CLIENT_ID', windows_live_client_id) enable_windows_live = get_option('windows_live') config_h.set('GOA_WINDOWS_LIVE_ENABLED', enable_windows_live) +# Microsoft Graph API account +config_h.set_quoted('GOA_MS_GRAPH_NAME', 'ms_graph') + +ms_graph_client_id = get_option('ms_graph_client_id') +config_h.set_quoted('GOA_MS_GRAPH_CLIENT_ID', ms_graph_client_id) + +enable_ms_graph = get_option('ms_graph') +config_h.set('GOA_MS_GRAPH_ENABLED', enable_ms_graph) + # Optional timerfd support timerfd_support_src = ''' #include @@ -282,6 +291,7 @@ summary({ 'ownCloud': enable_owncloud, 'WebDAV': enable_webdav, 'Windows Live': enable_windows_live, + 'Microsoft 365': enable_ms_graph, }, bool_yn: true, section: 'Providers', @@ -303,3 +313,11 @@ if enable_windows_live section: 'Windows Live Provider OAuth 2.0', ) endif + +if enable_ms_graph + summary({ + 'id': ms_graph_client_id, + }, + section: 'Microsoft 365 Provider OAuth 2.0', + ) +endif diff --git a/meson_options.txt b/meson_options.txt index aa44f90f..ca83d2ce 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -19,6 +19,9 @@ option('webdav', type: 'boolean', value: true, description: 'Enable WebDAV provi option('windows_live', type: 'boolean', value: true, description: 'Enable Windows Live provider') option('windows_live_client_id', type: 'string', value: '0000000044067703', description: 'Windows Live OAuth 2.0 client id') +option('ms_graph', type: 'boolean', value: true, description: 'Enable Microsoft 365 provider') +option('ms_graph_client_id', type: 'string', value: '', description: 'Microsoft 365 client id') + option('gtk_doc', type: 'boolean', value: false, description: 'use gtk_doc to build documentation') option('introspection', type: 'boolean', value: true, description: 'Enable GObject Introspection (depends on GObject)') option('man', type: 'boolean', value: false, description: 'enable man pages') diff --git a/src/goabackend/goamsgraphprovider.c b/src/goabackend/goamsgraphprovider.c new file mode 100644 index 00000000..a44b7bd6 --- /dev/null +++ b/src/goabackend/goamsgraphprovider.c @@ -0,0 +1,597 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright © 2019 Vilém Hořínek + * Copyright © 2022-2023 Jan-Michael Brummer + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General + * Public License along with this library; if not, see . + */ + +#include "config.h" + +#include + +#include +#include + +#include "goaprovider.h" +#include "goaprovider-priv.h" +#include "goamsgraphprovider.h" +#include "goarestproxy.h" +#include "goaobjectskeletonutils.h" +#include "goautils.h" + +struct _GoaMsGraphProvider +{ + GoaOAuth2Provider parent_instance; + + gboolean setup_done; + char *client_id; + char *redirect_uri; + char *authorization_uri; + char *token_uri; +}; + +G_DEFINE_TYPE_WITH_CODE (GoaMsGraphProvider, goa_ms_graph_provider, GOA_TYPE_OAUTH2_PROVIDER, + goa_provider_ensure_extension_points_registered (); + g_io_extension_point_implement (GOA_PROVIDER_EXTENSION_POINT_NAME, + g_define_type_id, + GOA_MS_GRAPH_NAME, 0)); + +typedef struct +{ + GCancellable *cancellable; + + GtkDialog *dialog; + GMainLoop *loop; + + GtkWidget *grid; + GtkWidget *email_entry; + GtkWidget *expander; + GtkWidget *client_id_entry; + GtkWidget *connect_button; + GError *error; +} AddAccountData; + +/* -------------------------------------------------------------------------- */ + +static const gchar * +get_provider_type (GoaProvider *provider) +{ + return GOA_MS_GRAPH_NAME; +} + +static gchar * +get_provider_name (GoaProvider *provider, + GoaObject *object) +{ + return g_strdup (_("Microsoft 365")); +} + +static GIcon * +get_provider_icon (GoaProvider *provider, + GoaObject *object) +{ + return g_themed_icon_new_with_default_fallbacks ("goa-account-msn"); +} + +static GoaProviderGroup +get_provider_group (GoaProvider *provider) +{ + return GOA_PROVIDER_GROUP_BRANDED; +} + +static GoaProviderFeatures +get_provider_features (GoaProvider *provider) +{ + return GOA_PROVIDER_FEATURE_BRANDED | + GOA_PROVIDER_FEATURE_FILES; +} + +static const gchar * +get_authorization_uri (GoaOAuth2Provider *oauth2_provider) +{ + GoaMsGraphProvider *self = GOA_MS_GRAPH_PROVIDER (oauth2_provider); + return self->authorization_uri; +} + +static const gchar * +get_token_uri (GoaOAuth2Provider *oauth2_provider) +{ + GoaMsGraphProvider *self = GOA_MS_GRAPH_PROVIDER (oauth2_provider); + return self->token_uri; +} + +static const gchar * +get_redirect_uri (GoaOAuth2Provider *oauth2_provider) +{ + GoaMsGraphProvider *self = GOA_MS_GRAPH_PROVIDER (oauth2_provider); + return self->redirect_uri; +} + +static const gchar * +get_scope (GoaOAuth2Provider *oauth2_provider) +{ + return "offline_access files.readwrite files.readwrite.all sites.read.all sites.readwrite.all user.read mail.readwrite contacts.readwrite"; +} + +static const gchar * +get_client_id (GoaOAuth2Provider *oauth2_provider) +{ + GoaMsGraphProvider *self = GOA_MS_GRAPH_PROVIDER (oauth2_provider); + return self->client_id ? self->client_id : GOA_MS_GRAPH_CLIENT_ID; +} + +static const gchar * +get_client_secret (GoaOAuth2Provider *oauth2_provider) +{ + return ""; +} + +static gchar * +build_authorization_uri (GoaOAuth2Provider *self, + const gchar *authorization_uri, + const gchar *escaped_redirect_uri, + const gchar *escaped_client_id, + const gchar *escaped_scope) +{ + return g_strdup_printf ("%s" + "?client_id=%s" + "&response_type=code" + "&redirect_uri=%s" + "&response_mode=query" + "&scope=%s", + authorization_uri, + escaped_client_id, + escaped_redirect_uri, + escaped_scope); +} + +/* -------------------------------------------------------------------------- */ + +static gchar * +get_identity_sync (GoaOAuth2Provider *oauth2_provider, + const gchar *access_token, + gchar **out_presentation_identity, + GCancellable *cancellable, + GError **error) +{ + JsonParser *parser = NULL; + JsonObject *json_object = NULL; + RestProxy *proxy = NULL; + RestProxyCall *call = NULL; + GError *identity_error = NULL; + gchar *authorization = NULL; + gchar *presentation_identity = NULL; + gchar *id = NULL; + gchar *ret = NULL; + + authorization = g_strconcat ("Bearer ", access_token, NULL); + + proxy = goa_rest_proxy_new ("https://graph.microsoft.com/v1.0/me/drive", FALSE); + call = rest_proxy_new_call (proxy); + rest_proxy_call_set_method (call, "GET"); + rest_proxy_call_add_header (call, "Authorization", authorization); + + if (!rest_proxy_call_sync (call, error)) + { + goto out; + } + if (rest_proxy_call_get_status_code (call) != 200) + { + g_set_error (error, + GOA_ERROR, + GOA_ERROR_FAILED, + _("Expected status 200 when requesting your identity, instead got status %d (%s)"), + rest_proxy_call_get_status_code (call), + rest_proxy_call_get_status_message (call)); + goto out; + } + + parser = json_parser_new (); + if (!json_parser_load_from_data (parser, + rest_proxy_call_get_payload (call), + rest_proxy_call_get_payload_length (call), + &identity_error)) + { + g_debug ("json_parser_load_from_data() failed: %s (%s, %d)", + identity_error->message, + g_quark_to_string (identity_error->domain), + identity_error->code); + g_set_error (error, + GOA_ERROR, + GOA_ERROR_FAILED, + _("Could not parse response")); + goto out; + } + + json_object = json_node_get_object (json_parser_get_root (parser)); + if (!json_object_has_member (json_object, "owner")) + { + g_debug ("Did not find owner in JSON data"); + g_set_error (error, + GOA_ERROR, + GOA_ERROR_FAILED, + _("Could not parse response")); + goto out; + } + + json_object = json_object_get_object_member (json_object, "owner"); + if (!json_object_has_member (json_object, "user")) + { + g_debug ("Did not find user in JSON data"); + g_set_error (error, + GOA_ERROR, + GOA_ERROR_FAILED, + _("Could not parse response")); + goto out; + } + json_object = json_object_get_object_member (json_object, "user"); + + id = g_strdup (json_object_get_string_member (json_object, "id")); + + presentation_identity = g_strdup (json_object_get_string_member (json_object, "displayName")); + ret = id; + id = NULL; + if (out_presentation_identity != NULL) + { + *out_presentation_identity = presentation_identity; + presentation_identity = NULL; + } + +out: + g_clear_object (&parser); + g_clear_error (&identity_error); + g_clear_object (&call); + g_clear_object (&proxy); + g_free (authorization); + g_free (id); + g_free (presentation_identity); + return ret; +} + +static gboolean +build_object (GoaProvider *provider, + GoaObjectSkeleton *object, + GKeyFile *key_file, + const gchar *group, + GDBusConnection *connection, + gboolean just_added, + GError **error) +{ + GoaMsGraphProvider *self = GOA_MS_GRAPH_PROVIDER (provider); + GoaAccount *account = NULL; + const gchar *email_address = NULL; + gboolean files_enabled = FALSE; + gchar *uri_onedrive = NULL; + gboolean ret = FALSE; + + if (!GOA_PROVIDER_CLASS (goa_ms_graph_provider_parent_class)->build_object (provider, + object, + key_file, + group, + connection, + just_added, + error)) + goto out; + + account = goa_object_get_account (GOA_OBJECT (object)); + email_address = goa_account_get_identity (account); + + /* Files */ + files_enabled = g_key_file_get_boolean (key_file, group, "FilesEnabled", NULL); + uri_onedrive = g_strconcat ("onedrive://", email_address, "/", NULL); + goa_object_skeleton_attach_files (object, uri_onedrive, files_enabled, FALSE); + g_free (uri_onedrive); + + self->client_id = g_key_file_get_string (key_file, group, "client_id", NULL); + self->redirect_uri = g_key_file_get_string (key_file, group, "redirect_uri", NULL); + self->authorization_uri = g_key_file_get_string (key_file, group, "authorization_uri", NULL); + self->token_uri = g_key_file_get_string (key_file, group, "token_uri", NULL); + + if (just_added) + { + goa_account_set_files_disabled (account, !files_enabled); + + g_signal_connect (account, + "notify::files-disabled", + G_CALLBACK (goa_util_account_notify_property_cb), + (gpointer) "FilesEnabled"); + } + + ret = TRUE; + +out: + g_clear_object (&account); + return ret; +} + +static void +add_account_key_values (GoaOAuth2Provider *oauth2_provider, + GVariantBuilder *builder) +{ + GoaMsGraphProvider *self = GOA_MS_GRAPH_PROVIDER (oauth2_provider); + + g_variant_builder_add (builder, "{ss}", "FilesEnabled", "true"); + + g_variant_builder_add (builder, "{ss}", "OAuth2AuthorizationUri", self->authorization_uri); + g_variant_builder_add (builder, "{ss}", "OAuth2TokenUri", self->token_uri); + g_variant_builder_add (builder, "{ss}", "OAuth2ClientId", self->client_id); + g_variant_builder_add (builder, "{ss}", "OAuth2RedirectUri", self->redirect_uri); + g_variant_builder_add (builder, "{ss}", "OAuth2ClientSecret", ""); + +} + +static void +on_email_entry_changed (GtkWidget *entry, + gpointer user_data) +{ + AddAccountData *data = user_data; + const char *email = gtk_entry_get_text (GTK_ENTRY (entry)); + g_auto (GStrv) split = NULL; + gboolean ret = FALSE; + + split = g_strsplit (email, "@", -1); + if (g_strv_length (split) == 2 && strlen (split[1]) > 0) + ret = TRUE; + + gtk_widget_set_sensitive (data->connect_button, ret); +} + +static void +create_account_details_ui (GoaProvider *provider, + GtkDialog *dialog, + GtkBox *vbox, + gboolean new_account, + AddAccountData *data) +{ + GtkWidget *label; + GtkStyleContext *context; + GtkWidget *expander_grid; + + goa_utils_set_dialog_title (provider, dialog, new_account); + + data->grid = gtk_grid_new (); + gtk_container_set_border_width (GTK_CONTAINER (data->grid), 12); + gtk_orientable_set_orientation (GTK_ORIENTABLE (data->grid), GTK_ORIENTATION_VERTICAL); + gtk_grid_set_column_spacing (GTK_GRID (data->grid), 6); + gtk_grid_set_row_spacing (GTK_GRID (data->grid), 12); + gtk_container_add (GTK_CONTAINER (vbox), data->grid); + + label = gtk_label_new_with_mnemonic (_("E-Mail")); + context = gtk_widget_get_style_context (label); + gtk_style_context_add_class (context, GTK_STYLE_CLASS_DIM_LABEL); + gtk_widget_set_halign (label, GTK_ALIGN_END); + gtk_grid_attach (GTK_GRID (data->grid), label, 0, 0, 1, 1); + + data->email_entry = gtk_entry_new (); + g_signal_connect (G_OBJECT (data->email_entry), "changed", G_CALLBACK (on_email_entry_changed), data); + gtk_widget_set_hexpand (data->email_entry, TRUE); + gtk_grid_attach (GTK_GRID (data->grid), data->email_entry, 1, 0, 1, 1); + + data->expander = gtk_expander_new_with_mnemonic (_("_Custom")); + gtk_expander_set_expanded (GTK_EXPANDER (data->expander), FALSE); + gtk_expander_set_resize_toplevel (GTK_EXPANDER (data->expander), TRUE); + gtk_grid_attach (GTK_GRID (data->grid), data->expander, 0, 1, 2, 1); + + expander_grid = gtk_grid_new (); + gtk_grid_set_column_spacing (GTK_GRID (expander_grid), 6); + gtk_grid_set_row_spacing (GTK_GRID (expander_grid), 12); + gtk_container_add (GTK_CONTAINER (data->expander), expander_grid); + + label = gtk_label_new_with_mnemonic ("Client ID"); + context = gtk_widget_get_style_context (label); + gtk_style_context_add_class (context, GTK_STYLE_CLASS_DIM_LABEL); + gtk_widget_set_halign (label, GTK_ALIGN_END); + gtk_grid_attach (GTK_GRID (expander_grid), label, 0, 1, 1, 1); + + data->client_id_entry = gtk_entry_new (); + gtk_widget_set_hexpand (data->client_id_entry, TRUE); + gtk_entry_set_activates_default (GTK_ENTRY (data->client_id_entry), TRUE); + gtk_entry_set_text (GTK_ENTRY (data->client_id_entry), get_client_id (GOA_OAUTH2_PROVIDER (provider))); + gtk_grid_attach (GTK_GRID (expander_grid), data->client_id_entry, 1, 1, 1, 1); + + data->connect_button = gtk_dialog_add_button (data->dialog, _("C_onnect"), GTK_RESPONSE_OK); + gtk_dialog_set_default_response (data->dialog, GTK_RESPONSE_OK); + gtk_dialog_set_response_sensitive (data->dialog, GTK_RESPONSE_OK, FALSE); + + gtk_widget_set_sensitive (data->connect_button, FALSE); +} + +static void +dialog_response_cb (GtkDialog *dialog, gint response_id, gpointer user_data) +{ + AddAccountData *data = user_data; + + if (response_id == GTK_RESPONSE_CANCEL || response_id == GTK_RESPONSE_DELETE_EVENT) + g_cancellable_cancel (data->cancellable); +} + +static gboolean +setup_tenant (GoaMsGraphProvider *self, + const char *host) +{ + g_autofree char *config_url = NULL; + SoupSession *session; + SoupMessage *msg; + GInputStream *res; + JsonParser *parser; + g_autoptr (GError) error = NULL; + + /* First try to read openid configuration and extract known tenant configuration */ + config_url = g_strdup_printf ("https://login.microsoft.com/%s/v2.0/.well-known/openid-configuration", host); + + session = soup_session_new (); + soup_session_add_feature_by_type (session, SOUP_TYPE_AUTH_NTLM); + + msg = soup_message_new ("GET", config_url); + res = soup_session_send (session, msg, NULL, &error); + if (res) + { + char buffer[2048]; + gsize len; + GError *parse_error = NULL; + JsonObject *json_object; + + memset(buffer, 0, sizeof (buffer)); + g_input_stream_read_all (res, &buffer, sizeof (buffer) - 1, &len, NULL, NULL); + + parser = json_parser_new (); + if (!json_parser_load_from_data (parser, + buffer, + len, + &parse_error)) + { + g_debug ("json_parser_load_from_data() failed: %s (%s, %d)", + parse_error->message, + g_quark_to_string (parse_error->domain), + parse_error->code); + g_set_error (&error, + GOA_ERROR, + GOA_ERROR_FAILED, + _("Could not parse response")); + return FALSE; + } + + json_object = json_node_get_object (json_parser_get_root (parser)); + if (!json_object_has_member (json_object, "error")) + { + if (!json_object_has_member (json_object, "authorization_endpoint")) + { + g_debug ("Did not find authorization_endpoint in JSON data"); + g_set_error (&error, + GOA_ERROR, + GOA_ERROR_FAILED, + _("Could not parse response")); + return FALSE; + } + + char *url = g_strdup (json_object_get_string_member (json_object, "authorization_endpoint")); + self->authorization_uri = url; + + json_object = json_node_get_object (json_parser_get_root (parser)); + if (!json_object_has_member (json_object, "token_endpoint")) + { + g_debug ("Did not find token_endpoint in JSON data"); + g_set_error (&error, + GOA_ERROR, + GOA_ERROR_FAILED, + _("Could not parse response")); + return FALSE; + } + + url = g_strdup (json_object_get_string_member (json_object, "token_endpoint")); + self->token_uri = url; + } + } + + if (!self->authorization_uri) + { + g_debug ("No openid configuration, using defaults"); + self->authorization_uri = g_strdup ("https://login.microsoftonline.com/common/oauth2/v2.0/authorize"); + self->token_uri = g_strdup ("https://login.microsoftonline.com/common/oauth2/v2.0/token"); + } + g_debug ("%s: authorization_uri=%s", __FUNCTION__, self->authorization_uri); + g_debug ("%s: token_uri=%s", __FUNCTION__, self->token_uri); + + return TRUE; +} + +static GoaObject * +add_account (GoaProvider *provider, + GoaClient *client, + GtkDialog *dialog, + GtkBox *vbox, + GError **error) +{ + AddAccountData data; + GoaObject *ret = NULL; + int response; + GoaMsGraphProvider *self = GOA_MS_GRAPH_PROVIDER (provider); + const char *email; + g_auto (GStrv) split = NULL; + + memset (&data, 0, sizeof (AddAccountData)); + data.cancellable = g_cancellable_new (); + data.loop = g_main_loop_new (NULL, FALSE); + data.dialog = dialog; + data.error = NULL; + + create_account_details_ui (provider, dialog, vbox, TRUE, &data); + gtk_widget_show_all (GTK_WIDGET (vbox)); + g_signal_connect (dialog, "response", G_CALLBACK (dialog_response_cb), &data); + + response = gtk_dialog_run (dialog); + if (response != GTK_RESPONSE_OK) + { + g_set_error (&data.error, + GOA_ERROR, + GOA_ERROR_DIALOG_DISMISSED, + _("Dialog was dismissed")); + return ret; + } + + email = gtk_entry_get_text (GTK_ENTRY (data.email_entry)); + split = g_strsplit (email, "@", -1); + if (g_strv_length (split) != 2) { + return ret; + } + + /* Setup tenant based on host */ + setup_tenant(self, split[1]); + + self->client_id = g_strdup (gtk_entry_get_text (GTK_ENTRY (data.client_id_entry))); + self->redirect_uri = g_strdup_printf ("goa-oauth2://localhost/%s", self->client_id); + + gtk_widget_set_visible (data.grid, FALSE); + gtk_widget_set_no_show_all (data.grid, TRUE); + gtk_widget_set_sensitive (data.connect_button, FALSE); + + return GOA_PROVIDER_CLASS (goa_ms_graph_provider_parent_class)->add_account (provider, client, dialog, vbox, error); +} + +/* -------------------------------------------------------------------------- */ + +static void +goa_ms_graph_provider_init (GoaMsGraphProvider *self) +{ + self->setup_done = FALSE; +} + +static void +goa_ms_graph_provider_class_init (GoaMsGraphProviderClass *klass) +{ + GoaProviderClass *provider_class; + GoaOAuth2ProviderClass *oauth2_class; + + provider_class = GOA_PROVIDER_CLASS (klass); + provider_class->get_provider_type = get_provider_type; + provider_class->get_provider_name = get_provider_name; + provider_class->get_provider_icon = get_provider_icon; + provider_class->get_provider_group = get_provider_group; + provider_class->get_provider_features = get_provider_features; + provider_class->build_object = build_object; + provider_class->add_account = add_account; + + oauth2_class = GOA_OAUTH2_PROVIDER_CLASS (klass); + oauth2_class->get_authorization_uri = get_authorization_uri; + oauth2_class->get_client_id = get_client_id; + oauth2_class->get_client_secret = get_client_secret; + oauth2_class->get_identity_sync = get_identity_sync; + oauth2_class->get_redirect_uri = get_redirect_uri; + oauth2_class->get_scope = get_scope; + oauth2_class->get_token_uri = get_token_uri; + oauth2_class->add_account_key_values = add_account_key_values; + + oauth2_class->build_authorization_uri = build_authorization_uri; +} diff --git a/src/goabackend/goamsgraphprovider.h b/src/goabackend/goamsgraphprovider.h new file mode 100644 index 00000000..3ffbc6e8 --- /dev/null +++ b/src/goabackend/goamsgraphprovider.h @@ -0,0 +1,43 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright © 2019 Vilém Hořínek + * Copyright © 2022-2023 Jan-Michael Brummer + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General + * Public License along with this library; if not, see . + */ + +#if !defined (__GOA_BACKEND_INSIDE_GOA_BACKEND_H__) && !defined (GOA_BACKEND_COMPILATION) +#error "Only can be included directly." +#endif + +#ifndef __GOA_MS_GRAPH_PROVIDER_H__ +#define __GOA_MS_GRAPH_PROVIDER_H__ + +#include + +#include "goaoauth2provider.h" +#include "goaoauth2provider-priv.h" + +G_BEGIN_DECLS + +#define GOA_TYPE_MS_GRAPH_PROVIDER (goa_ms_graph_provider_get_type ()) +G_DECLARE_FINAL_TYPE (GoaMsGraphProvider, + goa_ms_graph_provider, + GOA, + MS_GRAPH_PROVIDER, + GoaOAuth2Provider); + +G_END_DECLS + +#endif /* __GOA_MS_GRAPH_PROVIDER_H__ */ diff --git a/src/goabackend/goaoauth2handler.c b/src/goabackend/goaoauth2handler.c index 3736fbbf..dea7382f 100644 --- a/src/goabackend/goaoauth2handler.c +++ b/src/goabackend/goaoauth2handler.c @@ -58,6 +58,12 @@ oauth2_providers[] = .client_id = GOA_WINDOWS_LIVE_CLIENT_ID, .provider = GOA_WINDOWS_LIVE_NAME, }, +#endif +#ifdef GOA_MS_GRAPH_ENABLED + { + .client_id = GOA_MS_GRAPH_CLIENT_ID, + .provider = GOA_MS_GRAPH_NAME, + }, #endif { NULL, NULL }, }; diff --git a/src/goabackend/goaoauth2provider.c b/src/goabackend/goaoauth2provider.c index 73537ac1..b1225b9c 100644 --- a/src/goabackend/goaoauth2provider.c +++ b/src/goabackend/goaoauth2provider.c @@ -61,6 +61,15 @@ * need to be implemented - this type implements these methods.. */ +typedef struct +{ + char *client_id; + char *client_secret; + char *token_uri; + char *authorization_uri; + char *redirect_uri; +} OAuth2Data; + struct _GoaOAuth2ProviderPrivate { GtkDialog *dialog; @@ -80,6 +89,8 @@ struct _GoaOAuth2ProviderPrivate gchar *password; gchar *request_uri; SecretCollection *session; + + OAuth2Data *data; }; G_LOCK_DEFINE_STATIC (provider_lock); @@ -451,6 +462,54 @@ goa_oauth2_provider_get_identity_sync (GoaOAuth2Provider *self, error); } +static void +oauth2_data_free (OAuth2Data *data) +{ + g_clear_pointer (&data->client_id, g_free); + g_clear_pointer (&data->client_secret, g_free); + g_clear_pointer (&data->token_uri, g_free); + g_clear_pointer (&data->authorization_uri, g_free); + g_clear_pointer (&data->redirect_uri, g_free); + g_free (data); +} + +static void +oauth2_data_update (GoaOAuth2Provider *self, + GoaObject *object) +{ + GoaOAuth2ProviderPrivate *priv = goa_oauth2_provider_get_instance_private (self); + char *tmp; + + g_clear_pointer (&priv->data, oauth2_data_free); + priv->data = g_new0 (OAuth2Data, 1); + + if (object == NULL || (tmp = goa_util_lookup_keyfile_string (object, "OAuth2ClientId")) == NULL) + tmp = g_strdup (goa_oauth2_provider_get_client_id (self)); + priv->data->client_id = tmp; + + if (object == NULL || (tmp = goa_util_lookup_keyfile_string (object, "OAuth2ClientSecret")) == NULL) + tmp = g_strdup (goa_oauth2_provider_get_client_secret (self)); + priv->data->client_secret = tmp; + + if (object == NULL || (tmp = goa_util_lookup_keyfile_string (object, "OAuth2TokenUri")) == NULL) + tmp = g_strdup (goa_oauth2_provider_get_token_uri (self)); + priv->data->token_uri = tmp; + + if (object == NULL || (tmp = goa_util_lookup_keyfile_string (object, "OAuth2AuthorizationUri")) == NULL) + tmp = g_strdup (goa_oauth2_provider_get_authorization_uri (self)); + priv->data->authorization_uri = tmp; + + if (object == NULL || (tmp = goa_util_lookup_keyfile_string (object, "OAuth2RedirectUri")) == NULL) + tmp = g_strdup (goa_oauth2_provider_get_redirect_uri (self)); + priv->data->redirect_uri = tmp; + + g_debug ("- client_id=%s", priv->data->client_id); + g_debug ("- client_secret=%s", priv->data->client_secret); + g_debug ("- token_uri=%s", priv->data->token_uri); + g_debug ("- authorization_uri=%s", priv->data->authorization_uri); + g_debug ("- redirect_uri=%s", priv->data->redirect_uri); +} + /* ---------------------------------------------------------------------------------------------------- */ static gchar * @@ -462,6 +521,7 @@ get_tokens_sync (GoaOAuth2Provider *self, GCancellable *cancellable, GError **error) { + GoaOAuth2ProviderPrivate *priv = goa_oauth2_provider_get_instance_private (self); GError *tokens_error = NULL; RestProxy *proxy; RestProxyCall *call; @@ -472,18 +532,16 @@ get_tokens_sync (GoaOAuth2Provider *self, gchar *ret_refresh_token = NULL; const gchar *payload; gsize payload_length; - const gchar *client_secret; - proxy = goa_rest_proxy_new (goa_oauth2_provider_get_token_uri (self), FALSE); + proxy = goa_rest_proxy_new (priv->data->token_uri, FALSE); call = rest_proxy_new_call (proxy); rest_proxy_call_set_method (call, "POST"); rest_proxy_call_add_header (call, "Content-Type", "application/x-www-form-urlencoded"); - rest_proxy_call_add_param (call, "client_id", goa_oauth2_provider_get_client_id (self)); + rest_proxy_call_add_param (call, "client_id", priv->data->client_id); - client_secret = goa_oauth2_provider_get_client_secret (self); - if (client_secret != NULL) - rest_proxy_call_add_param (call, "client_secret", client_secret); + if (priv->data->client_secret != NULL) + rest_proxy_call_add_param (call, "client_secret", priv->data->client_secret); if (refresh_token != NULL) { @@ -495,7 +553,7 @@ get_tokens_sync (GoaOAuth2Provider *self, { /* No refresh code.. request an access token using the authorization code instead */ rest_proxy_call_add_param (call, "grant_type", "authorization_code"); - rest_proxy_call_add_param (call, "redirect_uri", goa_oauth2_provider_get_redirect_uri (self)); + rest_proxy_call_add_param (call, "redirect_uri", priv->data->redirect_uri); rest_proxy_call_add_param (call, "code", authorization_code); } @@ -611,8 +669,8 @@ get_tokens_sync (GoaOAuth2Provider *self, /* ---------------------------------------------------------------------------------------------------- */ static gboolean -parse_requested_uri (GoaOAuth2Provider *self, - const char *requested_uri) +parse_requested_uri (GoaOAuth2Provider *self, + const char *requested_uri) { GoaOAuth2ProviderPrivate *priv = goa_oauth2_provider_get_instance_private (self); g_autoptr (GHashTable) key_value_pairs = NULL; @@ -620,12 +678,10 @@ parse_requested_uri (GoaOAuth2Provider *self, const gchar *fragment; const gchar *oauth2_error; const gchar *query; - const gchar *redirect_uri; g_assert (priv->error == NULL); - redirect_uri = goa_oauth2_provider_get_redirect_uri (self); - if (!g_str_has_prefix (requested_uri, redirect_uri)) + if (!g_str_has_prefix (requested_uri, priv->data->redirect_uri)) { g_set_error (&priv->error, GOA_ERROR, @@ -755,15 +811,13 @@ on_secrets_changed (SecretCollection *collection, GoaOAuth2Provider *self) { GoaOAuth2ProviderPrivate *priv = goa_oauth2_provider_get_instance_private (self); - const char *client_id = NULL; const char *provider_type = NULL; g_autofree char *requested_uri = NULL; GtkResponseType response_id = GTK_RESPONSE_NONE; - client_id = goa_oauth2_provider_get_client_id (self); provider_type = goa_provider_get_provider_type (GOA_PROVIDER (self)); requested_uri = secret_password_lookup_sync (&oauth2_schema, NULL, NULL, - "goa-oauth2-client", client_id, + "goa-oauth2-client", priv->data->client_id, "goa-oauth2-provider", provider_type, NULL); @@ -803,14 +857,12 @@ secret_service_get_cb (GObject *object, /* The session collection is an empty string (?) */ if (g_strcmp0 (label, "") == 0) { - const char *client_id = NULL; const char *provider_type = NULL; /* Ensure there's no dangling entry */ - client_id = goa_oauth2_provider_get_client_id (self); provider_type = goa_provider_get_provider_type (GOA_PROVIDER (self)); secret_password_clear_sync (&oauth2_schema, NULL, NULL, - "goa-oauth2-client", client_id, + "goa-oauth2-client", priv->data->client_id, "goa-oauth2-provider", provider_type, NULL); @@ -892,15 +944,15 @@ get_tokens_and_identity (GoaOAuth2Provider *self, g_clear_object (&priv->session); /* TODO: use oauth2_proxy_build_login_url_full() */ - escaped_redirect_uri = g_uri_escape_string (goa_oauth2_provider_get_redirect_uri (self), NULL, TRUE); - escaped_client_id = g_uri_escape_string (goa_oauth2_provider_get_client_id (self), NULL, TRUE); + escaped_redirect_uri = g_uri_escape_string (priv->data->redirect_uri, NULL, TRUE); + escaped_client_id = g_uri_escape_string (priv->data->client_id, NULL, TRUE); scope = goa_oauth2_provider_get_scope (self); if (scope != NULL) escaped_scope = g_uri_escape_string (goa_oauth2_provider_get_scope (self), NULL, TRUE); else escaped_scope = NULL; priv->request_uri = goa_oauth2_provider_build_authorization_uri (self, - goa_oauth2_provider_get_authorization_uri (self), + priv->data->authorization_uri, escaped_redirect_uri, escaped_client_id, escaped_scope); @@ -1073,11 +1125,11 @@ add_credentials_key_values (GoaOAuth2Provider *self, } static GoaObject * -goa_oauth2_provider_add_account (GoaProvider *provider, - GoaClient *client, - GtkDialog *dialog, - GtkBox *vbox, - GError **error) +goa_oauth2_provider_add_account (GoaProvider *provider, + GoaClient *client, + GtkDialog *dialog, + GtkBox *vbox, + GError **error) { GoaOAuth2Provider *self = GOA_OAUTH2_PROVIDER (provider); GoaOAuth2ProviderPrivate *priv; @@ -1094,6 +1146,8 @@ goa_oauth2_provider_add_account (GoaProvider *provider, priv = goa_oauth2_provider_get_instance_private (self); priv->loop = g_main_loop_new (NULL, FALSE); + oauth2_data_update (self, NULL); + if (!get_tokens_and_identity (self, TRUE, NULL, dialog, vbox)) goto out; @@ -1191,6 +1245,8 @@ goa_oauth2_provider_refresh_account (GoaProvider *provider, account = goa_object_peek_account (object); + oauth2_data_update (self, object); + /* We abuse presentation identity here because for some providers * identity can be a machine readable ID, which can not be used to * log in via the provider's web interface. @@ -1484,6 +1540,9 @@ goa_oauth2_provider_build_object (GoaProvider *provider, gboolean just_added, GError **error) { + GoaOAuth2Provider *self = GOA_OAUTH2_PROVIDER (provider); + GoaOAuth2ProviderPrivate *priv; + GoaOAuth2Based *oauth2_based; oauth2_based = goa_object_get_oauth2_based (GOA_OBJECT (object)); @@ -1491,10 +1550,12 @@ goa_oauth2_provider_build_object (GoaProvider *provider, goto out; oauth2_based = goa_oauth2_based_skeleton_new (); - goa_oauth2_based_set_client_id (oauth2_based, - goa_oauth2_provider_get_client_id (GOA_OAUTH2_PROVIDER (provider))); - goa_oauth2_based_set_client_secret (oauth2_based, - goa_oauth2_provider_get_client_secret (GOA_OAUTH2_PROVIDER (provider))); + priv = goa_oauth2_provider_get_instance_private (self); + + oauth2_data_update (self, GOA_OBJECT (object)); + + goa_oauth2_based_set_client_id (oauth2_based, priv->data->client_id); + goa_oauth2_based_set_client_secret (oauth2_based, priv->data->client_secret); /* Ensure D-Bus method invocations run in their own thread */ g_dbus_interface_skeleton_set_flags (G_DBUS_INTERFACE_SKELETON (oauth2_based), G_DBUS_INTERFACE_SKELETON_FLAGS_HANDLE_METHOD_INVOCATIONS_IN_THREAD); @@ -1526,6 +1587,7 @@ goa_oauth2_provider_ensure_credentials_sync (GoaProvider *provider, gboolean force_refresh = FALSE; again: + oauth2_data_update (self, object); access_token = goa_oauth2_provider_get_access_token_sync (self, object, force_refresh, @@ -1579,6 +1641,7 @@ goa_oauth2_provider_finalize (GObject *object) priv = goa_oauth2_provider_get_instance_private (self); g_clear_pointer (&priv->loop, g_main_loop_unref); + g_clear_pointer (&priv->data, oauth2_data_free); g_free (priv->account_object_path); g_free (priv->password); diff --git a/src/goabackend/goaprovider.c b/src/goabackend/goaprovider.c index 12de7572..c0834de1 100644 --- a/src/goabackend/goaprovider.c +++ b/src/goabackend/goaprovider.c @@ -28,6 +28,7 @@ #include "goaowncloudprovider.h" #include "goawebdavprovider.h" #include "goawindowsliveprovider.h" +#include "goamsgraphprovider.h" #ifdef GOA_FEDORA_ENABLED #include "goafedoraprovider.h" @@ -954,6 +955,9 @@ static struct #endif #ifdef GOA_KERBEROS_ENABLED { GOA_KERBEROS_NAME, goa_kerberos_provider_get_type }, +#endif +#ifdef GOA_MS_GRAPH_ENABLED + { GOA_MS_GRAPH_NAME, goa_ms_graph_provider_get_type }, #endif { NULL, NULL } }; diff --git a/src/goabackend/meson.build b/src/goabackend/meson.build index dda164df..b1df9f7b 100644 --- a/src/goabackend/meson.build +++ b/src/goabackend/meson.build @@ -13,6 +13,7 @@ libgoa_backend_sources = files( 'goaimapsmtpprovider.c', 'goamailauth.c', 'goamailclient.c', + 'goamsgraphprovider.c', 'goaobjectskeletonutils.c', 'goaowncloudprovider.c', 'goaprovider.c', -- GitLab