From eb41a7c7c237797d2902d2c7b05f9d2d46fac070 Mon Sep 17 00:00:00 2001 From: Debarshi Ray Date: Wed, 12 Oct 2022 22:59:34 +0200 Subject: [PATCH 01/22] kerberos-identity-manager: Clarify an ambiguous debug log about KCM Kerberos KCM credential caches do support multiple identities and a lot of users use KCM these days because it's the default on Fedora. Hence, it's better not to have a debug log that implies that the code wasn't expecting KCM and is making assumptions about it - KCM is definitely a supported Kerberos cache type. https://gitlab.gnome.org/GNOME/gnome-online-accounts/-/merge_requests/104 --- src/goaidentity/goakerberosidentitymanager.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/goaidentity/goakerberosidentitymanager.c b/src/goaidentity/goakerberosidentitymanager.c index c35aa8b6..d4ff2de4 100644 --- a/src/goaidentity/goakerberosidentitymanager.c +++ b/src/goaidentity/goakerberosidentitymanager.c @@ -806,61 +806,63 @@ get_identity (GoaKerberosIdentityManager *self, GoaIdentity *identity; g_debug ("GoaKerberosIdentityManager: get identity %s", operation->identifier); identity = g_hash_table_lookup (self->identities, operation->identifier); if (identity == NULL) { g_task_return_new_error (operation->task, GOA_IDENTITY_MANAGER_ERROR, GOA_IDENTITY_MANAGER_ERROR_IDENTITY_NOT_FOUND, _("Could not find identity")); return; } g_task_return_pointer (operation->task, g_object_ref (identity), g_object_unref); } static krb5_error_code get_new_credentials_cache (GoaKerberosIdentityManager *self, krb5_ccache *credentials_cache) { krb5_error_code error_code; gboolean supports_multiple_identities; if (g_strcmp0 (self->credentials_cache_type, "FILE") == 0) { g_debug ("GoaKerberosIdentityManager: credential cache type %s doesn't supports cache collections", self->credentials_cache_type); supports_multiple_identities = FALSE; } - else if (g_strcmp0 (self->credentials_cache_type, "DIR") == 0 || g_strcmp0 (self->credentials_cache_type, "KEYRING") == 0) + else if (g_strcmp0 (self->credentials_cache_type, "DIR") == 0 + || g_strcmp0 (self->credentials_cache_type, "KCM") == 0 + || g_strcmp0 (self->credentials_cache_type, "KEYRING") == 0) { g_debug ("GoaKerberosIdentityManager: credential cache type %s supports cache collections", self->credentials_cache_type); supports_multiple_identities = TRUE; } else { g_debug ("GoaKerberosIdentityManager: don't know if credential cache type %s supports cache collections, " "assuming yes", self->credentials_cache_type); supports_multiple_identities = TRUE; } /* If we're configured for FILE based credentials, then we only * have one ccache, and we need to use it always. * * If we're configured for DIR or KEYRING based credentials, then we * can have multiple ccache's so we should use the default one first * (so it gets selected automatically) and then fallback to unique * ccache names for subsequent tickets. * */ if (!supports_multiple_identities || g_hash_table_size (self->identities) == 0) error_code = krb5_cc_default (self->kerberos_context, credentials_cache); else error_code = krb5_cc_new_unique (self->kerberos_context, self->credentials_cache_type, NULL, credentials_cache); return error_code; } static void -- 2.39.3 From 3bd1d5df6a25899651d2af72a5c226c3696e9f7c Mon Sep 17 00:00:00 2001 From: Debarshi Ray Date: Thu, 13 Oct 2022 18:46:46 +0200 Subject: [PATCH 02/22] kerberos-identity: Clarify and remove redundancy from the renewal errors The "Could not renew identity" segment in the error strings from goa_kerberos_identity_renew is redundant because the caller already knows that this function is about renewing an identity and any failure means just that. In fact, it's callers in GoaIdentityService and GoaKerberosIdentityManager already prepend a similar segment when using the error strings. Debug logs are already verbose enough. It's better not to make it even more difficult to follow them through needless redundancy. When reporting errors from krb5_cc_get_principal, it's good to make the error string match the documentation [1] of that function because it makes the code self-documenting. [1] https://web.mit.edu/kerberos/krb5-devel/doc/appdev/refs/api/krb5_cc_get_principal.html https://gitlab.gnome.org/GNOME/gnome-online-accounts/-/issues/160 --- src/goaidentity/goakerberosidentity.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/goaidentity/goakerberosidentity.c b/src/goaidentity/goakerberosidentity.c index 45d54f4d..3d2fe25c 100644 --- a/src/goaidentity/goakerberosidentity.c +++ b/src/goaidentity/goakerberosidentity.c @@ -1403,71 +1403,72 @@ goa_kerberos_identity_update (GoaKerberosIdentity *self, G_LOCK (identity_lock); self->cached_verification_level = new_verification_level; G_UNLOCK (identity_lock); g_signal_emit (G_OBJECT (self), signals[UNEXPIRED], 0); } else { G_LOCK (identity_lock); self->cached_verification_level = new_verification_level; G_UNLOCK (identity_lock); } queue_notify (self, &self->is_signed_in_idle_id, "is-signed-in"); } } gboolean goa_kerberos_identity_renew (GoaKerberosIdentity *self, GError **error) { krb5_error_code error_code = 0; krb5_principal principal; krb5_creds new_credentials; gboolean renewed = FALSE; char *name = NULL; if (self->credentials_cache == NULL) { g_set_error (error, GOA_IDENTITY_ERROR, GOA_IDENTITY_ERROR_CREDENTIALS_UNAVAILABLE, - _("Could not renew identity: Not signed in")); + _("Not signed in")); goto out; } error_code = krb5_cc_get_principal (self->kerberos_context, self->credentials_cache, &principal); if (error_code != 0) { set_and_prefix_error_from_krb5_error_code (self, error, GOA_IDENTITY_ERROR_CREDENTIALS_UNAVAILABLE, - error_code, _("Could not renew identity: ")); + error_code, + _("Could not get the default principal: ")); goto out; } name = goa_kerberos_identity_get_principal_name (self); error_code = krb5_get_renewed_creds (self->kerberos_context, &new_credentials, principal, self->credentials_cache, NULL); if (error_code != 0) { set_and_prefix_error_from_krb5_error_code (self, error, GOA_IDENTITY_ERROR_RENEWING, error_code, _("Could not get new credentials to renew identity %s: "), name); goto free_principal; } if (!goa_kerberos_identity_update_credentials (self, principal, &new_credentials, error)) { goto free_principal; } g_debug ("GoaKerberosIdentity: identity %s renewed", name); renewed = TRUE; free_principal: krb5_free_principal (self->kerberos_context, principal); -- 2.39.3 From a2e8e45b8e44201b9704ee31b9d762f71379be95 Mon Sep 17 00:00:00 2001 From: Debarshi Ray Date: Thu, 13 Oct 2022 19:02:59 +0200 Subject: [PATCH 03/22] kerberos-identity: Clarify the error when talking to the KDC failed When krb5_get_renewed_creds fails to talk to the Kerberos Key Distribution Centre (or KDC) because of network problems, krb5_get_error_message translates the krb5_error_code as: Resource temporarily unavailable This ends up in the debug logs as: GoaKerberosIdentityManager: could not renew identity: Could not get new credentials to renew identity FOO@BAR.ORG: Resource temporarily unavailable GoaIdentityService: could not renew identity: Could not get new credentials to renew identity FOO@BAR.ORG: Resource temporarily unavailable It's not immediately clear that the 'resource' that's 'temporarily unavailable' is actually the KDC, which would make it easier to understand that there is a network problem. Therefore, mention KDC in the error string from krb5_get_renewed_creds and make it match the documentation [1] of that function because it makes the code self-documenting. [1] https://web.mit.edu/kerberos/krb5-devel/doc/appdev/refs/api/krb5_get_renewed_creds.html https://gitlab.gnome.org/GNOME/gnome-online-accounts/-/issues/160 --- src/goaidentity/goakerberosidentity.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/goaidentity/goakerberosidentity.c b/src/goaidentity/goakerberosidentity.c index 3d2fe25c..57ab616f 100644 --- a/src/goaidentity/goakerberosidentity.c +++ b/src/goaidentity/goakerberosidentity.c @@ -1427,61 +1427,61 @@ goa_kerberos_identity_renew (GoaKerberosIdentity *self, GError **error) if (self->credentials_cache == NULL) { g_set_error (error, GOA_IDENTITY_ERROR, GOA_IDENTITY_ERROR_CREDENTIALS_UNAVAILABLE, _("Not signed in")); goto out; } error_code = krb5_cc_get_principal (self->kerberos_context, self->credentials_cache, &principal); if (error_code != 0) { set_and_prefix_error_from_krb5_error_code (self, error, GOA_IDENTITY_ERROR_CREDENTIALS_UNAVAILABLE, error_code, _("Could not get the default principal: ")); goto out; } name = goa_kerberos_identity_get_principal_name (self); error_code = krb5_get_renewed_creds (self->kerberos_context, &new_credentials, principal, self->credentials_cache, NULL); if (error_code != 0) { set_and_prefix_error_from_krb5_error_code (self, error, GOA_IDENTITY_ERROR_RENEWING, error_code, - _("Could not get new credentials to renew identity %s: "), + _("Could not get renewed credentials from the KDC for identity %s: "), name); goto free_principal; } if (!goa_kerberos_identity_update_credentials (self, principal, &new_credentials, error)) { goto free_principal; } g_debug ("GoaKerberosIdentity: identity %s renewed", name); renewed = TRUE; free_principal: krb5_free_principal (self->kerberos_context, principal); out: g_free (name); return renewed; } gboolean goa_kerberos_identity_erase (GoaKerberosIdentity *self, GError **error) { krb5_error_code error_code = 0; if (self->credentials_cache != NULL) -- 2.39.3 From 4b4719ea24e8a8503ccd015659eb82e0fbdbb7de Mon Sep 17 00:00:00 2001 From: Debarshi Ray Date: Thu, 13 Oct 2022 22:14:07 +0200 Subject: [PATCH 04/22] kerberos-identity: Fail initialization if an identifier can't be found The inability to get an identifier already leads to an error. Continuing beyond that point can lead to the verification_error trying to clobber it. https://gitlab.gnome.org/GNOME/gnome-online-accounts/-/merge_requests/107 --- src/goaidentity/goakerberosidentity.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/goaidentity/goakerberosidentity.c b/src/goaidentity/goakerberosidentity.c index 57ab616f..b72ce6ab 100644 --- a/src/goaidentity/goakerberosidentity.c +++ b/src/goaidentity/goakerberosidentity.c @@ -958,63 +958,64 @@ reset_alarms (GoaKerberosIdentity *self) g_clear_pointer (&expiration_time, g_date_time_unref); g_clear_pointer (&latest_possible_renewal_time, g_date_time_unref); g_clear_pointer (&start_time, g_date_time_unref); connect_alarm_signals (self); } static void clear_alarms (GoaKerberosIdentity *self) { disconnect_alarm_signals (self); clear_alarm_and_unref_on_idle (self, &self->renewal_alarm); clear_alarm_and_unref_on_idle (self, &self->expiring_alarm); clear_alarm_and_unref_on_idle (self, &self->expiration_alarm); } static gboolean goa_kerberos_identity_initable_init (GInitable *initable, GCancellable *cancellable, GError **error) { GoaKerberosIdentity *self = GOA_KERBEROS_IDENTITY (initable); GError *verification_error; if (g_cancellable_set_error_if_cancelled (cancellable, error)) return FALSE; if (self->identifier == NULL) { self->identifier = get_identifier (self, error); + if (self->identifier == NULL) + return FALSE; - if (self->identifier != NULL) - queue_notify (self, &self->identifier_idle_id, "identifier"); + queue_notify (self, &self->identifier_idle_id, "identifier"); } verification_error = NULL; self->cached_verification_level = verify_identity (self, &self->preauth_identity_source, &verification_error); switch (self->cached_verification_level) { case VERIFICATION_LEVEL_EXISTS: case VERIFICATION_LEVEL_SIGNED_IN: reset_alarms (self); queue_notify (self, &self->is_signed_in_idle_id, "is-signed-in"); return TRUE; case VERIFICATION_LEVEL_UNVERIFIED: return TRUE; case VERIFICATION_LEVEL_ERROR: default: if (verification_error != NULL) { g_propagate_error (error, verification_error); return FALSE; } g_set_error (error, GOA_IDENTITY_ERROR, GOA_IDENTITY_ERROR_VERIFYING, _("No associated identification found")); return FALSE; -- 2.39.3 From 10da18e2f5ab562ce7c0ea52701956adb80a118f Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Thu, 27 Oct 2022 09:18:53 -0400 Subject: [PATCH 05/22] kerberos-identity: Ensure idles queued to main thread are property synchronized Kerberos identities are refreshed on a helper thread, and the state of those identities are exported over the user bus on the main thread. Since the main consumer of an identity's properties is the bus service running on the main thread, to simplify things, property notifications are dispatched from the main thread as well (even though the underlying state is changed on a worker thread). The mechanism to dispatch property notifies to the main thread is an idle handler. The logic for doing the dispatch has a concurrency bug however. In order to coaelsce multiple notifies that happen in quick succession, the dispatch code checks for a preexisting idle id associated with the given property. That idle id is set from the worker thread when the idle is queued, and it's cleared from the main thread when the idle is dispatched. The bug is that the main thread could in theory clear the idle id immediately after the worker thread decided there was already a notify queued, leading to a notify getting completely dropped. This commit addresses the bug by adding appropriate locking. Closes https://gitlab.gnome.org/GNOME/gnome-online-accounts/-/issues/160 --- src/goaidentity/goakerberosidentity.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/goaidentity/goakerberosidentity.c b/src/goaidentity/goakerberosidentity.c index b72ce6ab..695396bf 100644 --- a/src/goaidentity/goakerberosidentity.c +++ b/src/goaidentity/goakerberosidentity.c @@ -493,61 +493,64 @@ snoop_preauth_identity_from_credentials (GoaKerberosIdentity *self, static krb5_timestamp get_current_time (GoaKerberosIdentity *self) { krb5_timestamp current_time; krb5_error_code error_code; error_code = krb5_timeofday (self->kerberos_context, ¤t_time); if (error_code != 0) { const char *error_message; error_message = krb5_get_error_message (self->kerberos_context, error_code); g_debug ("GoaKerberosIdentity: Error getting current time: %s", error_message); krb5_free_error_message (self->kerberos_context, error_message); return 0; } return current_time; } typedef struct { GoaKerberosIdentity *self; guint *idle_id; const char *property_name; } NotifyRequest; static void clear_idle_id (NotifyRequest *request) { + G_LOCK (identity_lock); *request->idle_id = 0; + G_UNLOCK (identity_lock); + g_object_unref (request->self); g_slice_free (NotifyRequest, request); } static gboolean on_notify_queued (NotifyRequest *request) { g_object_notify (G_OBJECT (request->self), request->property_name); return FALSE; } static void queue_notify (GoaKerberosIdentity *self, guint *idle_id, const char *property_name) { NotifyRequest *request; if (*idle_id != 0) { return; } request = g_slice_new0 (NotifyRequest); request->self = g_object_ref (self); request->idle_id = idle_id; request->property_name = property_name; *idle_id = g_idle_add_full (G_PRIORITY_DEFAULT_IDLE, -- 2.39.3 From b8cb0f6df2742e0a79f764edf8f99ffa40a4347d Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Thu, 13 Oct 2022 16:11:54 -0400 Subject: [PATCH 06/22] identity: Don't add temporary accounts for expired credentials The identity service creates a "temporary" kerberos account when a user manually does kinit, to handle automatic renewal, etc. Unfortunately, it also picks up expired cruft that builds up in KCM based credential caches, and creates temporary accounts for that as well. This commit tries to avoid that. Closes https://gitlab.gnome.org/GNOME/gnome-online-accounts/-/issues/32 --- src/goaidentity/goaidentityservice.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/goaidentity/goaidentityservice.c b/src/goaidentity/goaidentityservice.c index 3dd27060..a25de416 100644 --- a/src/goaidentity/goaidentityservice.c +++ b/src/goaidentity/goaidentityservice.c @@ -1070,61 +1070,61 @@ add_temporary_account (GoaIdentityService *self, g_object_ref (identity)); } else { add_temporary_account_as_kerberos (self, identity, NULL, on_temporary_account_added_as_kerberos, g_object_ref (identity)); } g_free (realm); } /* ---------------------------------------------------------------------------------------------------- */ static void on_identity_added (GoaIdentityManager *identity_manager, GoaIdentity *identity, GoaIdentityService *self) { GoaObject *object; const char *identifier; export_identity (self, identity); identifier = goa_identity_get_identifier (identity); object = find_object_with_principal (self, identifier, FALSE); - if (object == NULL) + if (object == NULL && goa_identity_is_signed_in (identity)) add_temporary_account (self, identity); g_clear_object (&object); } static void on_identity_removed (GoaIdentityManager *identity_manager, GoaIdentity *identity, GoaIdentityService *self) { GoaObject *object; const char *identifier; identifier = goa_identity_get_identifier (identity); object = find_object_with_principal (self, identifier, FALSE); if (object != NULL) ensure_account_credentials (self, object); unexport_identity (self, identity); g_clear_object (&object); } static void on_identity_refreshed (GoaIdentityManager *identity_manager, GoaIdentity *identity, GoaIdentityService *self) { GoaObject *object; const char *identifier; -- 2.39.3 From bf61b7ed9842be060d50c7ef0fa6f5987d05d67b Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Mon, 28 Nov 2022 14:16:09 -0500 Subject: [PATCH 07/22] kerberos-identity: Attempt to cope with multiple credential caches per identity At the moment the identity service assumes there will just be one credential cache collection for any given prinicipal. This isn't necessarily true though, and the service gets quite confused when that assumption doesn't hold up. This commit attempts to make it cope with the situation better, by maintaining a hash table of collections per identity. It deems one of the collections the "active" one and relegates the rest to be backup if the active one expires and can't be renewed. Closes: https://gitlab.gnome.org/GNOME/gnome-online-accounts/-/issues/79 --- src/goaidentity/goakerberosidentity.c | 340 ++++++++++++++++++++++---- 1 file changed, 287 insertions(+), 53 deletions(-) diff --git a/src/goaidentity/goakerberosidentity.c b/src/goaidentity/goakerberosidentity.c index 695396bf..dbb5991d 100644 --- a/src/goaidentity/goakerberosidentity.c +++ b/src/goaidentity/goakerberosidentity.c @@ -6,73 +6,76 @@ * 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 "goaidentity.h" #include "goakerberosidentity.h" #include "goakerberosidentityinquiry.h" #include "goaalarm.h" #include #include #include #include #include #include typedef enum { + VERIFICATION_LEVEL_ERROR = -1, VERIFICATION_LEVEL_UNVERIFIED, - VERIFICATION_LEVEL_ERROR, VERIFICATION_LEVEL_EXISTS, VERIFICATION_LEVEL_SIGNED_IN } VerificationLevel; struct _GoaKerberosIdentity { GObject parent; krb5_context kerberos_context; krb5_ccache credentials_cache; + GHashTable *credentials_caches; + char *active_credentials_cache_name; + char *identifier; guint identifier_idle_id; char *preauth_identity_source; krb5_timestamp start_time; guint start_time_idle_id; krb5_timestamp renewal_time; guint renewal_time_idle_id; krb5_timestamp expiration_time; guint expiration_time_idle_id; GoaAlarm *expiration_alarm; GoaAlarm *expiring_alarm; GoaAlarm *renewal_alarm; VerificationLevel cached_verification_level; guint is_signed_in_idle_id; }; enum { EXPIRING, EXPIRED, UNEXPIRED, NEEDS_RENEWAL, NEEDS_REFRESH, NUMBER_OF_SIGNALS, }; @@ -82,84 +85,99 @@ enum PROP_IDENTIFIER, PROP_IS_SIGNED_IN, PROP_START_TIMESTAMP, PROP_RENEWAL_TIMESTAMP, PROP_EXPIRATION_TIMESTAMP }; static guint signals[NUMBER_OF_SIGNALS] = { 0 }; static void identity_interface_init (GoaIdentityInterface *interface); static void initable_interface_init (GInitableIface *interface); static void reset_alarms (GoaKerberosIdentity *self); static void clear_alarms (GoaKerberosIdentity *self); static gboolean goa_kerberos_identity_is_signed_in (GoaIdentity *identity); static void set_and_prefix_error_from_krb5_error_code (GoaKerberosIdentity *self, GError **error, gint code, krb5_error_code error_code, const char *format, ...) G_GNUC_PRINTF (5, 6); G_LOCK_DEFINE_STATIC (identity_lock); G_DEFINE_TYPE_WITH_CODE (GoaKerberosIdentity, goa_kerberos_identity, G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_interface_init) G_IMPLEMENT_INTERFACE (GOA_TYPE_IDENTITY, identity_interface_init)); + +static void +close_credentials_caches (GoaKerberosIdentity *self) +{ + GHashTableIter iter; + const char *name; + krb5_ccache credentials_cache; + + g_hash_table_iter_init (&iter, self->credentials_caches); + while (g_hash_table_iter_next (&iter, (gpointer *) &name, (gpointer*) &credentials_cache)) + { + krb5_cc_close (self->kerberos_context, credentials_cache); + } + g_clear_pointer (&self->active_credentials_cache_name, g_free); +} + static void goa_kerberos_identity_dispose (GObject *object) { GoaKerberosIdentity *self = GOA_KERBEROS_IDENTITY (object); G_LOCK (identity_lock); clear_alarms (self); g_clear_pointer (&self->preauth_identity_source, g_free); + close_credentials_caches (self); + g_clear_pointer (&self->credentials_caches, g_hash_table_unref); G_UNLOCK (identity_lock); G_OBJECT_CLASS (goa_kerberos_identity_parent_class)->dispose (object); } static void goa_kerberos_identity_finalize (GObject *object) { GoaKerberosIdentity *self = GOA_KERBEROS_IDENTITY (object); g_free (self->identifier); - if (self->credentials_cache != NULL) - krb5_cc_close (self->kerberos_context, self->credentials_cache); - G_OBJECT_CLASS (goa_kerberos_identity_parent_class)->finalize (object); } static void goa_kerberos_identity_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *param_spec) { GoaKerberosIdentity *self = GOA_KERBEROS_IDENTITY (object); switch (property_id) { case PROP_IDENTIFIER: G_LOCK (identity_lock); g_value_set_string (value, self->identifier); G_UNLOCK (identity_lock); break; case PROP_IS_SIGNED_IN: g_value_set_boolean (value, goa_kerberos_identity_is_signed_in (GOA_IDENTITY (self))); break; case PROP_START_TIMESTAMP: G_LOCK (identity_lock); g_value_set_int64 (value, (gint64) self->start_time); G_UNLOCK (identity_lock); break; case PROP_RENEWAL_TIMESTAMP: G_LOCK (identity_lock); g_value_set_int64 (value, (gint64) self->renewal_time); @@ -228,109 +246,114 @@ goa_kerberos_identity_class_init (GoaKerberosIdentityClass *klass) G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); g_object_class_override_property (object_class, PROP_IDENTIFIER, "identifier"); g_object_class_override_property (object_class, PROP_IS_SIGNED_IN, "is-signed-in"); g_object_class_override_property (object_class, PROP_START_TIMESTAMP, "start-timestamp"); g_object_class_override_property (object_class, PROP_RENEWAL_TIMESTAMP, "renewal-timestamp"); g_object_class_override_property (object_class, PROP_EXPIRATION_TIMESTAMP, "expiration-timestamp"); } static char * get_identifier (GoaKerberosIdentity *self, GError **error) { krb5_principal principal; krb5_error_code error_code; char *unparsed_name; char *identifier = NULL; + krb5_ccache credentials_cache; - if (self->credentials_cache == NULL) + if (self->active_credentials_cache_name == NULL) return NULL; - error_code = krb5_cc_get_principal (self->kerberos_context, self->credentials_cache, &principal); + credentials_cache = (krb5_ccache) g_hash_table_lookup (self->credentials_caches, + self->active_credentials_cache_name); + + error_code = krb5_cc_get_principal (self->kerberos_context, credentials_cache, &principal); if (error_code != 0) { if (error_code == KRB5_CC_END) { set_and_prefix_error_from_krb5_error_code (self, error, GOA_IDENTITY_ERROR_CREDENTIALS_UNAVAILABLE, error_code, _("Could not find identity in credential cache: ")); } else { set_and_prefix_error_from_krb5_error_code (self, error, GOA_IDENTITY_ERROR_ENUMERATING_CREDENTIALS, error_code, _("Could not find identity in credential cache: ")); } return NULL; } error_code = krb5_unparse_name_flags (self->kerberos_context, principal, 0, &unparsed_name); if (error_code != 0) { const char *error_message; error_message = krb5_get_error_message (self->kerberos_context, error_code); g_debug ("GoaKerberosIdentity: Error parsing principal identity name: %s", error_message); krb5_free_error_message (self->kerberos_context, error_message); goto out; } identifier = g_strdup (unparsed_name); krb5_free_unparsed_name (self->kerberos_context, unparsed_name); out: krb5_free_principal (self->kerberos_context, principal); return identifier; } static void goa_kerberos_identity_init (GoaKerberosIdentity *self) { + self->credentials_caches = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); } static void set_and_prefix_error_from_krb5_error_code (GoaKerberosIdentity *self, GError **error, gint code, krb5_error_code error_code, const char *format, ...) { const char *error_message; char *literal_prefix; va_list args; error_message = krb5_get_error_message (self->kerberos_context, error_code); g_set_error_literal (error, GOA_IDENTITY_ERROR, code, error_message); va_start (args, format); literal_prefix = g_strdup_vprintf (format, args); va_end (args); g_prefix_error (error, "%s", literal_prefix); g_free (literal_prefix); krb5_free_error_message (self->kerberos_context, error_message); } char * goa_kerberos_identity_get_principal_name (GoaKerberosIdentity *self) { @@ -613,169 +636,280 @@ examine_credentials (GoaKerberosIdentity *self, krb5_timestamp current_time; G_LOCK (identity_lock); if (credentials->times.starttime != 0) credentials_start_time = credentials->times.starttime; else credentials_start_time = credentials->times.authtime; *renewal_time = credentials->times.renew_till; credentials_end_time = credentials->times.endtime; if (self->start_time == 0) *start_time = credentials_start_time; else *start_time = MIN (self->start_time, credentials_start_time); *expiration_time = MAX (credentials->times.endtime, self->expiration_time); G_UNLOCK (identity_lock); current_time = get_current_time (self); if (current_time < credentials_start_time || credentials_end_time <= current_time) *are_expired = TRUE; else *are_expired = FALSE; } static VerificationLevel -verify_identity (GoaKerberosIdentity *self, - char **preauth_identity_source, - GError **error) +verify_identity_in_credentials_cache (GoaKerberosIdentity *self, + char **preauth_identity_source, + krb5_ccache credentials_cache, + krb5_timestamp *start_time, + krb5_timestamp *renewal_time, + krb5_timestamp *expiration_time, + GError **error) { krb5_principal principal = NULL; krb5_cc_cursor cursor; krb5_creds credentials; krb5_error_code error_code; - krb5_timestamp start_time = 0; - krb5_timestamp renewal_time = 0; - krb5_timestamp expiration_time = 0; VerificationLevel verification_level = VERIFICATION_LEVEL_UNVERIFIED; - if (self->credentials_cache == NULL) - goto out; + g_debug ("GoaKerberosIdentity: Verifying identity in credentials cache '%s'", + krb5_cc_get_name (self->kerberos_context, credentials_cache)); - error_code = krb5_cc_get_principal (self->kerberos_context, self->credentials_cache, &principal); + error_code = krb5_cc_get_principal (self->kerberos_context, credentials_cache, &principal); if (error_code != 0) { + if (error_code == KRB5_CC_END) + g_debug ("GoaKerberosIdentity: Credentials cache empty"); + else if (error_code == KRB5_FCC_NOFILE) + g_debug ("GoaKerberosIdentity: Credentials cache missing"); + if (error_code == KRB5_CC_END || error_code == KRB5_FCC_NOFILE) goto out; set_and_prefix_error_from_krb5_error_code (self, error, GOA_IDENTITY_ERROR_NOT_FOUND, error_code, _("Could not find identity in credential cache: ")); verification_level = VERIFICATION_LEVEL_ERROR; goto out; } - error_code = krb5_cc_start_seq_get (self->kerberos_context, self->credentials_cache, &cursor); + error_code = krb5_cc_start_seq_get (self->kerberos_context, credentials_cache, &cursor); if (error_code != 0) { set_and_prefix_error_from_krb5_error_code (self, error, GOA_IDENTITY_ERROR_CREDENTIALS_UNAVAILABLE, error_code, _("Could not find identity credentials in cache: ")); verification_level = VERIFICATION_LEVEL_ERROR; goto out; } verification_level = VERIFICATION_LEVEL_UNVERIFIED; - error_code = krb5_cc_next_cred (self->kerberos_context, self->credentials_cache, &cursor, &credentials); + error_code = krb5_cc_next_cred (self->kerberos_context, credentials_cache, &cursor, &credentials); while (error_code == 0) { if (credentials_validate_existence (self, principal, &credentials)) { gboolean credentials_are_expired = TRUE; - examine_credentials (self, &credentials, - &start_time, - &renewal_time, - &expiration_time, + examine_credentials (self, + &credentials, + start_time, + renewal_time, + expiration_time, &credentials_are_expired); if (!credentials_are_expired) verification_level = VERIFICATION_LEVEL_SIGNED_IN; else verification_level = VERIFICATION_LEVEL_EXISTS; } else { snoop_preauth_identity_from_credentials (self, &credentials, preauth_identity_source); } krb5_free_cred_contents (self->kerberos_context, &credentials); - error_code = krb5_cc_next_cred (self->kerberos_context, self->credentials_cache, &cursor, &credentials); + error_code = krb5_cc_next_cred (self->kerberos_context, credentials_cache, &cursor, &credentials); } if (error_code != KRB5_CC_END) { verification_level = VERIFICATION_LEVEL_ERROR; set_and_prefix_error_from_krb5_error_code (self, error, GOA_IDENTITY_ERROR_ENUMERATING_CREDENTIALS, error_code, _("Could not sift through identity credentials in cache: ")); - goto end_sequence; } - end_sequence: - error_code = krb5_cc_end_seq_get (self->kerberos_context, self->credentials_cache, &cursor); + error_code = krb5_cc_end_seq_get (self->kerberos_context, credentials_cache, &cursor); if (error_code != 0) { verification_level = VERIFICATION_LEVEL_ERROR; set_and_prefix_error_from_krb5_error_code (self, error, GOA_IDENTITY_ERROR_ENUMERATING_CREDENTIALS, error_code, _("Could not finish up sifting through " "identity credentials in cache: ")); goto out; } + out: + switch (verification_level) + { + case VERIFICATION_LEVEL_EXISTS: + g_debug ("GoaKerberosIdentity: Credentials in credentials cache '%s' are out of date", + krb5_cc_get_name (self->kerberos_context, credentials_cache)); + break; + + case VERIFICATION_LEVEL_SIGNED_IN: + g_debug ("GoaKerberosIdentity: Credentials in credentials cache '%s' are valid", + krb5_cc_get_name (self->kerberos_context, credentials_cache)); + break; + + case VERIFICATION_LEVEL_UNVERIFIED: + g_debug ("GoaKerberosIdentity: Credentials in credentials cache '%s' are missing", + krb5_cc_get_name (self->kerberos_context, credentials_cache)); + break; + + case VERIFICATION_LEVEL_ERROR: + default: + g_debug ("GoaKerberosIdentity: Credentials in credentials cache '%s' could not be validated", + krb5_cc_get_name (self->kerberos_context, credentials_cache)); + break; + } + + if (principal != NULL) + krb5_free_principal (self->kerberos_context, principal); + return verification_level; +} + +static VerificationLevel +verify_identity (GoaKerberosIdentity *self, + char **preauth_identity_source, + GError **error) +{ + krb5_ccache credentials_cache; + const char *name; + krb5_timestamp start_time = 0; + krb5_timestamp renewal_time = 0; + krb5_timestamp expiration_time = 0; + VerificationLevel verification_level = VERIFICATION_LEVEL_UNVERIFIED; + GHashTableIter iter; + + if (self->active_credentials_cache_name != NULL) + { + credentials_cache = (krb5_ccache) g_hash_table_lookup (self->credentials_caches, + self->active_credentials_cache_name); + + verification_level = verify_identity_in_credentials_cache (self, + preauth_identity_source, + credentials_cache, + &start_time, + &renewal_time, + &expiration_time, + error); + if (verification_level == VERIFICATION_LEVEL_SIGNED_IN) + goto out; + + if (verification_level == VERIFICATION_LEVEL_UNVERIFIED) + { + krb5_cc_close (self->kerberos_context, credentials_cache); + g_hash_table_remove (self->credentials_caches, self->active_credentials_cache_name); + g_clear_pointer (&self->active_credentials_cache_name, g_free); + } + } + + self->start_time = 0; + self->renewal_time = 0; + self->expiration_time = 0; + + g_hash_table_iter_init (&iter, self->credentials_caches); + while (g_hash_table_iter_next (&iter, (gpointer *) &name, (gpointer*) &credentials_cache)) + { + krb5_timestamp new_start_time = 0; + krb5_timestamp new_renewal_time = 0; + krb5_timestamp new_expiration_time = 0; + + if (g_strcmp0 (name, self->active_credentials_cache_name) == 0) + continue; + + g_clear_pointer (preauth_identity_source, g_free); + verification_level = verify_identity_in_credentials_cache (self, + preauth_identity_source, + credentials_cache, + &new_start_time, + &new_renewal_time, + &new_expiration_time, + error); + + if (verification_level == VERIFICATION_LEVEL_SIGNED_IN || + self->active_credentials_cache_name == NULL) + { + g_clear_pointer (&self->active_credentials_cache_name, g_free); + self->active_credentials_cache_name = g_strdup (name); + start_time = new_start_time; + renewal_time = new_renewal_time; + expiration_time = new_expiration_time; + + if (verification_level == VERIFICATION_LEVEL_SIGNED_IN) + break; + } + else if (verification_level == VERIFICATION_LEVEL_UNVERIFIED) + { + krb5_cc_close (self->kerberos_context, credentials_cache); + g_hash_table_iter_remove (&iter); + } + } +out: G_LOCK (identity_lock); set_start_time (self, start_time); set_renewal_time (self, renewal_time); set_expiration_time (self, expiration_time); G_UNLOCK (identity_lock); - if (principal != NULL) - krb5_free_principal (self->kerberos_context, principal); return verification_level; } static gboolean goa_kerberos_identity_is_signed_in (GoaIdentity *identity) { GoaKerberosIdentity *self = GOA_KERBEROS_IDENTITY (identity); gboolean is_signed_in = FALSE; G_LOCK (identity_lock); if (self->cached_verification_level == VERIFICATION_LEVEL_SIGNED_IN) is_signed_in = TRUE; G_UNLOCK (identity_lock); return is_signed_in; } static void identity_interface_init (GoaIdentityInterface *interface) { interface->get_identifier = goa_kerberos_identity_get_identifier; interface->is_signed_in = goa_kerberos_identity_is_signed_in; } static void on_expiration_alarm_fired (GoaAlarm *alarm, GoaKerberosIdentity *self) { g_return_if_fail (GOA_IS_ALARM (alarm)); g_return_if_fail (GOA_IS_KERBEROS_IDENTITY (self)); @@ -1052,121 +1186,154 @@ on_kerberos_inquiry (krb5_context kerberos_context, GoaIdentityInquiry *inquiry; krb5_error_code error_code = 0; if (number_of_prompts == 0) goto out; inquiry = goa_kerberos_identity_inquiry_new (operation->identity, name, banner, prompts, number_of_prompts); operation->inquiry_func (inquiry, operation->cancellable, operation->inquiry_data); if (goa_identity_inquiry_is_failed (inquiry)) error_code = KRB5_LIBOS_CANTREADPWD; else if (!goa_identity_inquiry_is_complete (inquiry)) g_cancellable_cancel (operation->cancellable); if (g_cancellable_is_cancelled (operation->cancellable)) error_code = KRB5_LIBOS_PWDINTR; g_object_unref (inquiry); out: return error_code; } +static void +goa_kerberos_identity_add_credentials_cache (GoaKerberosIdentity *self, + krb5_ccache credentials_cache) +{ + const char *cache_name; + + cache_name = krb5_cc_get_name (self->kerberos_context, credentials_cache); + + if (g_hash_table_contains (self->credentials_caches, cache_name)) + { + krb5_ccache old_credentials_cache; + + old_credentials_cache = (krb5_ccache) g_hash_table_lookup (self->credentials_caches, cache_name); + + krb5_cc_close (self->kerberos_context, old_credentials_cache); + } + + g_hash_table_replace (self->credentials_caches, g_strdup (cache_name), credentials_cache); + + if (self->active_credentials_cache_name == NULL) + { + self->active_credentials_cache_name = g_strdup (cache_name); + } +} + static gboolean -create_credential_cache (GoaKerberosIdentity *self, - GError **error) +create_credentials_cache (GoaKerberosIdentity *self, + GError **error) { krb5_ccache default_cache; + krb5_ccache new_cache; const char *cache_type; krb5_error_code error_code; error_code = krb5_cc_default (self->kerberos_context, &default_cache); if (error_code == 0) { cache_type = krb5_cc_get_type (self->kerberos_context, default_cache); - error_code = krb5_cc_new_unique (self->kerberos_context, cache_type, NULL, &self->credentials_cache); + error_code = krb5_cc_new_unique (self->kerberos_context, cache_type, NULL, &new_cache); } if (error_code != 0) { set_and_prefix_error_from_krb5_error_code (self, error, GOA_IDENTITY_ERROR_ALLOCATING_CREDENTIALS, error_code, _("Could not create credential cache: ")); return FALSE; } + goa_kerberos_identity_add_credentials_cache (self, new_cache); + return TRUE; } static gboolean goa_kerberos_identity_update_credentials (GoaKerberosIdentity *self, krb5_principal principal, krb5_creds *new_credentials, GError **error) { krb5_error_code error_code; + krb5_ccache credentials_cache; - if (self->credentials_cache == NULL) + + if (self->active_credentials_cache_name == NULL) { - if (!create_credential_cache (self, error)) + if (!create_credentials_cache (self, error)) { krb5_free_cred_contents (self->kerberos_context, new_credentials); goto out; } } - error_code = krb5_cc_initialize (self->kerberos_context, self->credentials_cache, principal); + credentials_cache = (krb5_ccache) g_hash_table_lookup (self->credentials_caches, + self->active_credentials_cache_name); + + error_code = krb5_cc_initialize (self->kerberos_context, credentials_cache, principal); if (error_code != 0) { set_and_prefix_error_from_krb5_error_code (self, error, GOA_IDENTITY_ERROR_ALLOCATING_CREDENTIALS, error_code, _("Could not initialize credentials cache: ")); krb5_free_cred_contents (self->kerberos_context, new_credentials); goto out; } - error_code = krb5_cc_store_cred (self->kerberos_context, self->credentials_cache, new_credentials); + error_code = krb5_cc_store_cred (self->kerberos_context, credentials_cache, new_credentials); if (error_code != 0) { set_and_prefix_error_from_krb5_error_code (self, error, GOA_IDENTITY_ERROR_SAVING_CREDENTIALS, error_code, _("Could not store new credentials in credentials cache: ")); krb5_free_cred_contents (self->kerberos_context, new_credentials); goto out; } krb5_free_cred_contents (self->kerberos_context, new_credentials); return TRUE; out: return FALSE; } static SignInOperation * sign_in_operation_new (GoaKerberosIdentity *identity, GoaIdentityInquiryFunc inquiry_func, gpointer inquiry_data, GDestroyNotify destroy_notify, GCancellable *cancellable) { SignInOperation *operation; operation = g_slice_new0 (SignInOperation); operation->identity = g_object_ref (identity); operation->inquiry_func = inquiry_func; @@ -1327,201 +1494,268 @@ goa_kerberos_identity_sign_in (GoaKerberosIdentity *self, krb5_free_principal (self->kerberos_context, principal); goto done; } krb5_free_principal (self->kerberos_context, principal); g_debug ("GoaKerberosIdentity: identity signed in"); signed_in = TRUE; done: return signed_in; } static void update_identifier (GoaKerberosIdentity *self, GoaKerberosIdentity *new_identity) { char *new_identifier; new_identifier = get_identifier (self, NULL); if (g_strcmp0 (self->identifier, new_identifier) != 0 && new_identifier != NULL) { g_free (self->identifier); self->identifier = new_identifier; queue_notify (self, &self->identifier_idle_id, "identifier"); } else { g_free (new_identifier); } } +static int +goa_kerberos_identity_compare (GoaKerberosIdentity *self, + GoaKerberosIdentity *new_identity) +{ + if (self->cached_verification_level < new_identity->cached_verification_level) + return -100; + + if (self->cached_verification_level > new_identity->cached_verification_level) + return 100; + + if (self->cached_verification_level != VERIFICATION_LEVEL_SIGNED_IN) + return 50; + + if (self->expiration_time < new_identity->expiration_time) + return -10; + + if (self->expiration_time > new_identity->expiration_time) + return 10; + + if (self->start_time > new_identity->start_time) + return -5; + + if (self->start_time < new_identity->start_time) + return 5; + + if (self->renewal_time < new_identity->renewal_time) + return -1; + + if (self->renewal_time > new_identity->renewal_time) + return 1; + + return 0; +} + void goa_kerberos_identity_update (GoaKerberosIdentity *self, GoaKerberosIdentity *new_identity) { VerificationLevel old_verification_level, new_verification_level; gboolean time_changed = FALSE; char *preauth_identity_source = NULL; + int comparison; + + comparison = goa_kerberos_identity_compare (self, new_identity); + + if (new_identity->active_credentials_cache_name != NULL) + { + krb5_ccache credentials_cache; + krb5_ccache copied_cache; - if (self->credentials_cache != NULL) - krb5_cc_close (self->kerberos_context, self->credentials_cache); + credentials_cache = (krb5_ccache) g_hash_table_lookup (new_identity->credentials_caches, + new_identity->active_credentials_cache_name); + krb5_cc_dup (new_identity->kerberos_context, credentials_cache, &copied_cache); - krb5_cc_dup (new_identity->kerberos_context, new_identity->credentials_cache, &self->credentials_cache); + if (comparison < 0) + g_clear_pointer (&self->active_credentials_cache_name, &g_free); + + goa_kerberos_identity_add_credentials_cache (self, copied_cache); + } + + if (comparison >= 0) + return; G_LOCK (identity_lock); update_identifier (self, new_identity); - time_changed |= set_start_time (self, new_identity->start_time); time_changed |= set_renewal_time (self, new_identity->renewal_time); time_changed |= set_expiration_time (self, new_identity->expiration_time); old_verification_level = self->cached_verification_level; new_verification_level = new_identity->cached_verification_level; G_UNLOCK (identity_lock); if (time_changed) { if (new_verification_level == VERIFICATION_LEVEL_SIGNED_IN) reset_alarms (self); else clear_alarms (self); } G_LOCK (identity_lock); g_free (self->preauth_identity_source); self->preauth_identity_source = preauth_identity_source; G_UNLOCK (identity_lock); if (new_verification_level != old_verification_level) { if (old_verification_level == VERIFICATION_LEVEL_SIGNED_IN && new_verification_level == VERIFICATION_LEVEL_EXISTS) { G_LOCK (identity_lock); self->cached_verification_level = new_verification_level; G_UNLOCK (identity_lock); g_signal_emit (G_OBJECT (self), signals[EXPIRED], 0); } else if (old_verification_level == VERIFICATION_LEVEL_EXISTS && new_verification_level == VERIFICATION_LEVEL_SIGNED_IN) { G_LOCK (identity_lock); self->cached_verification_level = new_verification_level; G_UNLOCK (identity_lock); g_signal_emit (G_OBJECT (self), signals[UNEXPIRED], 0); } else { G_LOCK (identity_lock); self->cached_verification_level = new_verification_level; G_UNLOCK (identity_lock); } queue_notify (self, &self->is_signed_in_idle_id, "is-signed-in"); } } gboolean goa_kerberos_identity_renew (GoaKerberosIdentity *self, GError **error) { krb5_error_code error_code = 0; krb5_principal principal; krb5_creds new_credentials; + krb5_ccache credentials_cache; gboolean renewed = FALSE; char *name = NULL; - if (self->credentials_cache == NULL) + if (self->active_credentials_cache_name == NULL) { g_set_error (error, GOA_IDENTITY_ERROR, GOA_IDENTITY_ERROR_CREDENTIALS_UNAVAILABLE, _("Not signed in")); goto out; } - error_code = krb5_cc_get_principal (self->kerberos_context, self->credentials_cache, &principal); + credentials_cache = (krb5_ccache) g_hash_table_lookup (self->credentials_caches, + self->active_credentials_cache_name); + error_code = krb5_cc_get_principal (self->kerberos_context, credentials_cache, &principal); if (error_code != 0) { set_and_prefix_error_from_krb5_error_code (self, error, GOA_IDENTITY_ERROR_CREDENTIALS_UNAVAILABLE, error_code, _("Could not get the default principal: ")); goto out; } name = goa_kerberos_identity_get_principal_name (self); - error_code = krb5_get_renewed_creds (self->kerberos_context, &new_credentials, principal, self->credentials_cache, NULL); + error_code = krb5_get_renewed_creds (self->kerberos_context, &new_credentials, principal, credentials_cache, NULL); if (error_code != 0) { set_and_prefix_error_from_krb5_error_code (self, error, GOA_IDENTITY_ERROR_RENEWING, error_code, _("Could not get renewed credentials from the KDC for identity %s: "), name); goto free_principal; } if (!goa_kerberos_identity_update_credentials (self, principal, &new_credentials, error)) { goto free_principal; } g_debug ("GoaKerberosIdentity: identity %s renewed", name); renewed = TRUE; free_principal: krb5_free_principal (self->kerberos_context, principal); out: g_free (name); return renewed; } gboolean goa_kerberos_identity_erase (GoaKerberosIdentity *self, GError **error) { + GHashTableIter iter; + const char *name; + krb5_ccache credentials_cache; krb5_error_code error_code = 0; - if (self->credentials_cache != NULL) + if (self->active_credentials_cache_name != NULL) { - error_code = krb5_cc_destroy (self->kerberos_context, self->credentials_cache); - self->credentials_cache = NULL; + credentials_cache = (krb5_ccache) g_hash_table_lookup (self->credentials_caches, + self->active_credentials_cache_name); + g_debug ("GoaKerberosIdentity: Destroying active credentials cache %s", self->active_credentials_cache_name); + error_code = krb5_cc_destroy (self->kerberos_context, credentials_cache); + g_clear_pointer (&self->active_credentials_cache_name, g_free); + + if (error_code != 0) + { + set_and_prefix_error_from_krb5_error_code (self, + error, + GOA_IDENTITY_ERROR_REMOVING_CREDENTIALS, + error_code, _("Could not erase identity: ")); + } } - if (error_code != 0) + g_hash_table_iter_init (&iter, self->credentials_caches); + while (g_hash_table_iter_next (&iter, (gpointer *) &name, (gpointer*) &credentials_cache)) { - set_and_prefix_error_from_krb5_error_code (self, - error, - GOA_IDENTITY_ERROR_REMOVING_CREDENTIALS, - error_code, _("Could not erase identity: ")); - return FALSE; + g_debug ("GoaKerberosIdentity: Destroying inactive credentials cache %s", name); + krb5_cc_destroy (self->kerberos_context, credentials_cache); } + g_hash_table_remove_all (self->credentials_caches); - return TRUE; + return error_code == 0; } GoaIdentity * goa_kerberos_identity_new (krb5_context context, krb5_ccache cache, GError **error) { GoaKerberosIdentity *self; + krb5_ccache copied_cache; self = GOA_KERBEROS_IDENTITY (g_object_new (GOA_TYPE_KERBEROS_IDENTITY, NULL)); - - krb5_cc_dup (context, cache, &self->credentials_cache); self->kerberos_context = context; + krb5_cc_dup (self->kerberos_context, cache, &copied_cache); + goa_kerberos_identity_add_credentials_cache (self, copied_cache); + error = NULL; if (!g_initable_init (G_INITABLE (self), NULL, error)) { g_object_unref (self); return NULL; } return GOA_IDENTITY (self); } -- 2.39.3 From 02b8df618e1e26ed70b586b3214d1c8f255f9909 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Mon, 28 Nov 2022 15:58:09 -0500 Subject: [PATCH 08/22] kerberos-identity: Clear alarms on temporary identity When the identity service does a refresh, it creates a new temporary identity object to check the credentials, then it merges that temporary identity into the preexisting identity object (so the pointers don't change). This has the unfortunate side-effect of arming expiration alarms in the temporary object, that can then fire immediately before the object is thrown out. This commit disarms those alarms so they don't fire needlessly. --- src/goaidentity/goakerberosidentity.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/goaidentity/goakerberosidentity.c b/src/goaidentity/goakerberosidentity.c index dbb5991d..6006385b 100644 --- a/src/goaidentity/goakerberosidentity.c +++ b/src/goaidentity/goakerberosidentity.c @@ -1554,60 +1554,62 @@ goa_kerberos_identity_compare (GoaKerberosIdentity *self, return 0; } void goa_kerberos_identity_update (GoaKerberosIdentity *self, GoaKerberosIdentity *new_identity) { VerificationLevel old_verification_level, new_verification_level; gboolean time_changed = FALSE; char *preauth_identity_source = NULL; int comparison; comparison = goa_kerberos_identity_compare (self, new_identity); if (new_identity->active_credentials_cache_name != NULL) { krb5_ccache credentials_cache; krb5_ccache copied_cache; credentials_cache = (krb5_ccache) g_hash_table_lookup (new_identity->credentials_caches, new_identity->active_credentials_cache_name); krb5_cc_dup (new_identity->kerberos_context, credentials_cache, &copied_cache); if (comparison < 0) g_clear_pointer (&self->active_credentials_cache_name, &g_free); goa_kerberos_identity_add_credentials_cache (self, copied_cache); } + clear_alarms (new_identity); + if (comparison >= 0) return; G_LOCK (identity_lock); update_identifier (self, new_identity); time_changed |= set_start_time (self, new_identity->start_time); time_changed |= set_renewal_time (self, new_identity->renewal_time); time_changed |= set_expiration_time (self, new_identity->expiration_time); old_verification_level = self->cached_verification_level; new_verification_level = new_identity->cached_verification_level; G_UNLOCK (identity_lock); if (time_changed) { if (new_verification_level == VERIFICATION_LEVEL_SIGNED_IN) reset_alarms (self); else clear_alarms (self); } G_LOCK (identity_lock); g_free (self->preauth_identity_source); self->preauth_identity_source = preauth_identity_source; G_UNLOCK (identity_lock); if (new_verification_level != old_verification_level) { if (old_verification_level == VERIFICATION_LEVEL_SIGNED_IN && new_verification_level == VERIFICATION_LEVEL_EXISTS) { -- 2.39.3 From c0f70c84a8fafbcaf3245765be51a7a027b312e3 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 29 Nov 2022 12:21:09 -0500 Subject: [PATCH 09/22] kerberos-identity: Add missing locking commit c492cbfd861bc773cf8b4c15bc722380355fc4b3 introduced some code to goa_kerberos_identity_update that's not protected by the identity lock. It really should be. This commit fixes that. --- src/goaidentity/goakerberosidentity.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/goaidentity/goakerberosidentity.c b/src/goaidentity/goakerberosidentity.c index 6006385b..b51c9920 100644 --- a/src/goaidentity/goakerberosidentity.c +++ b/src/goaidentity/goakerberosidentity.c @@ -1537,76 +1537,78 @@ goa_kerberos_identity_compare (GoaKerberosIdentity *self, if (self->expiration_time < new_identity->expiration_time) return -10; if (self->expiration_time > new_identity->expiration_time) return 10; if (self->start_time > new_identity->start_time) return -5; if (self->start_time < new_identity->start_time) return 5; if (self->renewal_time < new_identity->renewal_time) return -1; if (self->renewal_time > new_identity->renewal_time) return 1; return 0; } void goa_kerberos_identity_update (GoaKerberosIdentity *self, GoaKerberosIdentity *new_identity) { VerificationLevel old_verification_level, new_verification_level; gboolean time_changed = FALSE; char *preauth_identity_source = NULL; int comparison; + G_LOCK (identity_lock); comparison = goa_kerberos_identity_compare (self, new_identity); if (new_identity->active_credentials_cache_name != NULL) { krb5_ccache credentials_cache; krb5_ccache copied_cache; credentials_cache = (krb5_ccache) g_hash_table_lookup (new_identity->credentials_caches, new_identity->active_credentials_cache_name); krb5_cc_dup (new_identity->kerberos_context, credentials_cache, &copied_cache); if (comparison < 0) g_clear_pointer (&self->active_credentials_cache_name, &g_free); goa_kerberos_identity_add_credentials_cache (self, copied_cache); } + G_UNLOCK (identity_lock); clear_alarms (new_identity); if (comparison >= 0) return; G_LOCK (identity_lock); update_identifier (self, new_identity); time_changed |= set_start_time (self, new_identity->start_time); time_changed |= set_renewal_time (self, new_identity->renewal_time); time_changed |= set_expiration_time (self, new_identity->expiration_time); old_verification_level = self->cached_verification_level; new_verification_level = new_identity->cached_verification_level; G_UNLOCK (identity_lock); if (time_changed) { if (new_verification_level == VERIFICATION_LEVEL_SIGNED_IN) reset_alarms (self); else clear_alarms (self); } G_LOCK (identity_lock); g_free (self->preauth_identity_source); self->preauth_identity_source = preauth_identity_source; G_UNLOCK (identity_lock); if (new_verification_level != old_verification_level) { -- 2.39.3 From 4d8bba0ec4df1c5fd8533c4b77b62002d5c99ece Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Wed, 30 Nov 2022 13:53:41 -0500 Subject: [PATCH 10/22] kerberos-identity: Drop the weird ampersand This commit drops an unnecessary and non-idiomatic ampersand. --- src/goaidentity/goakerberosidentity.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/goaidentity/goakerberosidentity.c b/src/goaidentity/goakerberosidentity.c index b51c9920..55288d24 100644 --- a/src/goaidentity/goakerberosidentity.c +++ b/src/goaidentity/goakerberosidentity.c @@ -1550,61 +1550,61 @@ goa_kerberos_identity_compare (GoaKerberosIdentity *self, return -1; if (self->renewal_time > new_identity->renewal_time) return 1; return 0; } void goa_kerberos_identity_update (GoaKerberosIdentity *self, GoaKerberosIdentity *new_identity) { VerificationLevel old_verification_level, new_verification_level; gboolean time_changed = FALSE; char *preauth_identity_source = NULL; int comparison; G_LOCK (identity_lock); comparison = goa_kerberos_identity_compare (self, new_identity); if (new_identity->active_credentials_cache_name != NULL) { krb5_ccache credentials_cache; krb5_ccache copied_cache; credentials_cache = (krb5_ccache) g_hash_table_lookup (new_identity->credentials_caches, new_identity->active_credentials_cache_name); krb5_cc_dup (new_identity->kerberos_context, credentials_cache, &copied_cache); if (comparison < 0) - g_clear_pointer (&self->active_credentials_cache_name, &g_free); + g_clear_pointer (&self->active_credentials_cache_name, g_free); goa_kerberos_identity_add_credentials_cache (self, copied_cache); } G_UNLOCK (identity_lock); clear_alarms (new_identity); if (comparison >= 0) return; G_LOCK (identity_lock); update_identifier (self, new_identity); time_changed |= set_start_time (self, new_identity->start_time); time_changed |= set_renewal_time (self, new_identity->renewal_time); time_changed |= set_expiration_time (self, new_identity->expiration_time); old_verification_level = self->cached_verification_level; new_verification_level = new_identity->cached_verification_level; G_UNLOCK (identity_lock); if (time_changed) { if (new_verification_level == VERIFICATION_LEVEL_SIGNED_IN) reset_alarms (self); else clear_alarms (self); } G_LOCK (identity_lock); g_free (self->preauth_identity_source); self->preauth_identity_source = preauth_identity_source; -- 2.39.3 From 1d947c23ae037ea9063064338250251cc52f0b0c Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Thu, 15 Dec 2022 14:46:01 -0500 Subject: [PATCH 11/22] kerberos-identity: Unbreak handling of fresh caches commit 4acfcc323e986526975ede981673dd173be4e267 attempted to avoid an error variable getting stomped all over by returning FALSE when encountering the error. Unfortunately, it's actual legitimate for an error to happen in that path and we should proceed anyway. That can happen when a credential cache is new and not yet initialized, so it won't have a principal associated with it yet. This commit changes the problematic code to just pass NULL for the error variable, since we don't need it. --- src/goaidentity/goakerberosidentity.c | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/goaidentity/goakerberosidentity.c b/src/goaidentity/goakerberosidentity.c index 55288d24..a20c0438 100644 --- a/src/goaidentity/goakerberosidentity.c +++ b/src/goaidentity/goakerberosidentity.c @@ -1094,65 +1094,64 @@ reset_alarms (GoaKerberosIdentity *self) g_clear_pointer (&renewal_time, g_date_time_unref); g_clear_pointer (&expiration_time, g_date_time_unref); g_clear_pointer (&latest_possible_renewal_time, g_date_time_unref); g_clear_pointer (&start_time, g_date_time_unref); connect_alarm_signals (self); } static void clear_alarms (GoaKerberosIdentity *self) { disconnect_alarm_signals (self); clear_alarm_and_unref_on_idle (self, &self->renewal_alarm); clear_alarm_and_unref_on_idle (self, &self->expiring_alarm); clear_alarm_and_unref_on_idle (self, &self->expiration_alarm); } static gboolean goa_kerberos_identity_initable_init (GInitable *initable, GCancellable *cancellable, GError **error) { GoaKerberosIdentity *self = GOA_KERBEROS_IDENTITY (initable); GError *verification_error; if (g_cancellable_set_error_if_cancelled (cancellable, error)) return FALSE; if (self->identifier == NULL) { - self->identifier = get_identifier (self, error); - if (self->identifier == NULL) - return FALSE; + self->identifier = get_identifier (self, NULL); - queue_notify (self, &self->identifier_idle_id, "identifier"); + if (self->identifier != NULL) + queue_notify (self, &self->identifier_idle_id, "identifier"); } verification_error = NULL; self->cached_verification_level = verify_identity (self, &self->preauth_identity_source, &verification_error); switch (self->cached_verification_level) { case VERIFICATION_LEVEL_EXISTS: case VERIFICATION_LEVEL_SIGNED_IN: reset_alarms (self); queue_notify (self, &self->is_signed_in_idle_id, "is-signed-in"); return TRUE; case VERIFICATION_LEVEL_UNVERIFIED: return TRUE; case VERIFICATION_LEVEL_ERROR: default: if (verification_error != NULL) { g_propagate_error (error, verification_error); return FALSE; } g_set_error (error, GOA_IDENTITY_ERROR, GOA_IDENTITY_ERROR_VERIFYING, _("No associated identification found")); return FALSE; -- 2.39.3 From 712feaacacf435f807a9f11fc5fa6066d6a052af Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Thu, 15 Dec 2022 14:46:01 -0500 Subject: [PATCH 12/22] kerberos-identity: Fix buglet in update_identity The update_identity function is supposed to transfer the identity form one object to another. In practice, this is currently always a noop because only objects with the same identities get copied to each other. Nevertheless, there is a bug in the function. It grabs the identity from the target object instead of from the source object. This commit fixes that. --- src/goaidentity/goakerberosidentity.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/goaidentity/goakerberosidentity.c b/src/goaidentity/goakerberosidentity.c index a20c0438..bc607966 100644 --- a/src/goaidentity/goakerberosidentity.c +++ b/src/goaidentity/goakerberosidentity.c @@ -1480,61 +1480,61 @@ goa_kerberos_identity_sign_in (GoaKerberosIdentity *self, krb5_free_principal (self->kerberos_context, principal); goto done; } if (destroy_notify) destroy_notify (inquiry_data); sign_in_operation_free (operation); if (!goa_kerberos_identity_update_credentials (self, principal, &new_credentials, error)) { krb5_free_principal (self->kerberos_context, principal); goto done; } krb5_free_principal (self->kerberos_context, principal); g_debug ("GoaKerberosIdentity: identity signed in"); signed_in = TRUE; done: return signed_in; } static void update_identifier (GoaKerberosIdentity *self, GoaKerberosIdentity *new_identity) { char *new_identifier; - new_identifier = get_identifier (self, NULL); + new_identifier = get_identifier (new_identity, NULL); if (g_strcmp0 (self->identifier, new_identifier) != 0 && new_identifier != NULL) { g_free (self->identifier); self->identifier = new_identifier; queue_notify (self, &self->identifier_idle_id, "identifier"); } else { g_free (new_identifier); } } static int goa_kerberos_identity_compare (GoaKerberosIdentity *self, GoaKerberosIdentity *new_identity) { if (self->cached_verification_level < new_identity->cached_verification_level) return -100; if (self->cached_verification_level > new_identity->cached_verification_level) return 100; if (self->cached_verification_level != VERIFICATION_LEVEL_SIGNED_IN) return 50; if (self->expiration_time < new_identity->expiration_time) return -10; if (self->expiration_time > new_identity->expiration_time) return 10; -- 2.39.3 From 4f3d89260a4b600e26a67a671039ea03692fb684 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Thu, 15 Dec 2022 16:27:27 -0500 Subject: [PATCH 13/22] goakerberosidentity: Fix crash when erasing credentials Right now when erasing an identity we erase the active credentials first and then the inactive ones. We neglect to take the active one out of the hash table, though, so it gets destroyed twice. This commit fixes that. --- src/goaidentity/goakerberosidentity.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/goaidentity/goakerberosidentity.c b/src/goaidentity/goakerberosidentity.c index bc607966..46a6fb22 100644 --- a/src/goaidentity/goakerberosidentity.c +++ b/src/goaidentity/goakerberosidentity.c @@ -1692,60 +1692,62 @@ goa_kerberos_identity_renew (GoaKerberosIdentity *self, GError **error) { goto free_principal; } g_debug ("GoaKerberosIdentity: identity %s renewed", name); renewed = TRUE; free_principal: krb5_free_principal (self->kerberos_context, principal); out: g_free (name); return renewed; } gboolean goa_kerberos_identity_erase (GoaKerberosIdentity *self, GError **error) { GHashTableIter iter; const char *name; krb5_ccache credentials_cache; krb5_error_code error_code = 0; if (self->active_credentials_cache_name != NULL) { credentials_cache = (krb5_ccache) g_hash_table_lookup (self->credentials_caches, self->active_credentials_cache_name); g_debug ("GoaKerberosIdentity: Destroying active credentials cache %s", self->active_credentials_cache_name); error_code = krb5_cc_destroy (self->kerberos_context, credentials_cache); + g_hash_table_remove (self->credentials_caches, self->active_credentials_cache_name); + g_clear_pointer (&self->active_credentials_cache_name, g_free); if (error_code != 0) { set_and_prefix_error_from_krb5_error_code (self, error, GOA_IDENTITY_ERROR_REMOVING_CREDENTIALS, error_code, _("Could not erase identity: ")); } } g_hash_table_iter_init (&iter, self->credentials_caches); while (g_hash_table_iter_next (&iter, (gpointer *) &name, (gpointer*) &credentials_cache)) { g_debug ("GoaKerberosIdentity: Destroying inactive credentials cache %s", name); krb5_cc_destroy (self->kerberos_context, credentials_cache); } g_hash_table_remove_all (self->credentials_caches); return error_code == 0; } GoaIdentity * goa_kerberos_identity_new (krb5_context context, krb5_ccache cache, GError **error) { GoaKerberosIdentity *self; krb5_ccache copied_cache; self = GOA_KERBEROS_IDENTITY (g_object_new (GOA_TYPE_KERBEROS_IDENTITY, NULL)); self->kerberos_context = context; -- 2.39.3 From d36f9eb09ee157a01ad66f3950076a46cda94094 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Thu, 15 Dec 2022 15:35:49 -0500 Subject: [PATCH 14/22] goakerberosidentity: Explicitly switch to credentials cache when needed If we're updating a credentials cache and decide it should be the new default for an identity, and the old credentials cache was the default cache for the cache collection then we should make the new credential cache the default cache for the collection, too. This commit adds that. It also makes the new credentials cache the default if there wasn't a valid default set already. This brings consistency to differences in behavior from different kerberos ccache types. --- src/goaidentity/goakerberosidentity.c | 63 +++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 3 deletions(-) diff --git a/src/goaidentity/goakerberosidentity.c b/src/goaidentity/goakerberosidentity.c index 46a6fb22..4fe4b70b 100644 --- a/src/goaidentity/goakerberosidentity.c +++ b/src/goaidentity/goakerberosidentity.c @@ -1527,100 +1527,157 @@ goa_kerberos_identity_compare (GoaKerberosIdentity *self, if (self->cached_verification_level < new_identity->cached_verification_level) return -100; if (self->cached_verification_level > new_identity->cached_verification_level) return 100; if (self->cached_verification_level != VERIFICATION_LEVEL_SIGNED_IN) return 50; if (self->expiration_time < new_identity->expiration_time) return -10; if (self->expiration_time > new_identity->expiration_time) return 10; if (self->start_time > new_identity->start_time) return -5; if (self->start_time < new_identity->start_time) return 5; if (self->renewal_time < new_identity->renewal_time) return -1; if (self->renewal_time > new_identity->renewal_time) return 1; return 0; } +static char * +get_default_cache_name (GoaKerberosIdentity *self) +{ + int error_code; + krb5_ccache default_cache; + krb5_principal principal; + char *default_cache_name; + char *principal_name; + + error_code = krb5_cc_default (self->kerberos_context, &default_cache); + + if (error_code != 0) + return NULL; + + /* Return NULL if the default cache doesn't pass basic sanity checks + */ + error_code = krb5_cc_get_principal (self->kerberos_context, default_cache, &principal); + + if (error_code != 0) + return NULL; + + error_code = krb5_unparse_name_flags (self->kerberos_context, principal, 0, &principal_name); + krb5_free_principal (self->kerberos_context, principal); + + if (error_code != 0) + return NULL; + + krb5_free_unparsed_name (self->kerberos_context, principal_name); + + default_cache_name = g_strdup (krb5_cc_get_name (self->kerberos_context, default_cache)); + krb5_cc_close (self->kerberos_context, default_cache); + + return default_cache_name; +} + void goa_kerberos_identity_update (GoaKerberosIdentity *self, GoaKerberosIdentity *new_identity) { VerificationLevel old_verification_level, new_verification_level; gboolean time_changed = FALSE; char *preauth_identity_source = NULL; int comparison; G_LOCK (identity_lock); + + old_verification_level = self->cached_verification_level; + new_verification_level = new_identity->cached_verification_level; + comparison = goa_kerberos_identity_compare (self, new_identity); if (new_identity->active_credentials_cache_name != NULL) { + g_autofree char *default_cache_name = NULL; krb5_ccache credentials_cache; krb5_ccache copied_cache; + gboolean should_switch_to_new_credentials_cache = FALSE; + + default_cache_name = get_default_cache_name (self); + + if (default_cache_name == NULL) + should_switch_to_new_credentials_cache = TRUE; credentials_cache = (krb5_ccache) g_hash_table_lookup (new_identity->credentials_caches, new_identity->active_credentials_cache_name); krb5_cc_dup (new_identity->kerberos_context, credentials_cache, &copied_cache); + if (g_strcmp0 (default_cache_name, self->active_credentials_cache_name) == 0) + { + if ((comparison < 0) || + (comparison == 0 && old_verification_level != VERIFICATION_LEVEL_SIGNED_IN)) + should_switch_to_new_credentials_cache = TRUE; + } + if (comparison < 0) - g_clear_pointer (&self->active_credentials_cache_name, g_free); + { + g_clear_pointer (&self->active_credentials_cache_name, g_free); + self->active_credentials_cache_name = g_strdup (new_identity->active_credentials_cache_name); + } goa_kerberos_identity_add_credentials_cache (self, copied_cache); + + if (should_switch_to_new_credentials_cache) + krb5_cc_switch (self->kerberos_context, copied_cache); } G_UNLOCK (identity_lock); clear_alarms (new_identity); if (comparison >= 0) return; G_LOCK (identity_lock); update_identifier (self, new_identity); time_changed |= set_start_time (self, new_identity->start_time); time_changed |= set_renewal_time (self, new_identity->renewal_time); time_changed |= set_expiration_time (self, new_identity->expiration_time); - old_verification_level = self->cached_verification_level; - new_verification_level = new_identity->cached_verification_level; G_UNLOCK (identity_lock); if (time_changed) { if (new_verification_level == VERIFICATION_LEVEL_SIGNED_IN) reset_alarms (self); else clear_alarms (self); } G_LOCK (identity_lock); g_free (self->preauth_identity_source); self->preauth_identity_source = preauth_identity_source; G_UNLOCK (identity_lock); if (new_verification_level != old_verification_level) { if (old_verification_level == VERIFICATION_LEVEL_SIGNED_IN && new_verification_level == VERIFICATION_LEVEL_EXISTS) { G_LOCK (identity_lock); self->cached_verification_level = new_verification_level; G_UNLOCK (identity_lock); g_signal_emit (G_OBJECT (self), signals[EXPIRED], 0); } else if (old_verification_level == VERIFICATION_LEVEL_EXISTS && new_verification_level == VERIFICATION_LEVEL_SIGNED_IN) { G_LOCK (identity_lock); -- 2.39.3 From bef93518e9f23d69c01c714ec9c8a6f2c012a08c Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Mon, 16 Jan 2023 15:00:36 -0500 Subject: [PATCH 15/22] goakerberosidentity: Fix automatic reinitialization The identity service has the ability to automatically fetch a new ticket granting ticket from the KDC when the existing one expires, provided the user keeps their kerberos password in GNOME keyring. Unfortunately, commit aca400799c225a84e5d0fc90efb206c8f1d48bc3 inadvertently broke this feature in some cases. When deciding whether or not to make a new credentials cache for a principal the active one it looks at various characteristics of the competing credentials to decide which cache is better. For instance, if one credentials cache has a ticket that's valid and signed in, but the other credentials cache only has an expired ticket, then obviously the one that's valid and signed in gets picked to be active. Likewise, if one is expiring in 10 minutes and one is expiring in 24 hours, the one that expires in 24 hours will be treated as better. This comparison, only makes sense, though when looking at two different credentials caches. If we're updating a preexisting credentials cache, then we're actually just comparing up to date data with out of date data. In that case, we need to proceed even if new newer view of the credentials look worse than the older view of those credentials. Unfortunately, the buggy commit neglected to account for that. This commit fixes that problem and related problems, by more thoroughly and systematically checking all the permutations of credentials in the old credentials cache for the identity, the new credentials cache for the identity, and the default credentials cache. It also adds a lot more logging for clarity. --- src/goaidentity/goakerberosidentity.c | 198 ++++++++++++++++++++++++-- 1 file changed, 183 insertions(+), 15 deletions(-) diff --git a/src/goaidentity/goakerberosidentity.c b/src/goaidentity/goakerberosidentity.c index 4fe4b70b..d046a8a4 100644 --- a/src/goaidentity/goakerberosidentity.c +++ b/src/goaidentity/goakerberosidentity.c @@ -1562,130 +1562,298 @@ get_default_cache_name (GoaKerberosIdentity *self) krb5_principal principal; char *default_cache_name; char *principal_name; error_code = krb5_cc_default (self->kerberos_context, &default_cache); if (error_code != 0) return NULL; /* Return NULL if the default cache doesn't pass basic sanity checks */ error_code = krb5_cc_get_principal (self->kerberos_context, default_cache, &principal); if (error_code != 0) return NULL; error_code = krb5_unparse_name_flags (self->kerberos_context, principal, 0, &principal_name); krb5_free_principal (self->kerberos_context, principal); if (error_code != 0) return NULL; krb5_free_unparsed_name (self->kerberos_context, principal_name); default_cache_name = g_strdup (krb5_cc_get_name (self->kerberos_context, default_cache)); krb5_cc_close (self->kerberos_context, default_cache); return default_cache_name; } +static char * +get_default_principal (GoaKerberosIdentity *self) +{ + int error_code; + krb5_ccache default_cache; + krb5_principal principal; + char *unparsed_principal, *principal_name; + + error_code = krb5_cc_default (self->kerberos_context, &default_cache); + + if (error_code != 0) + return NULL; + + /* Return NULL if the default cache doesn't pass basic sanity checks + */ + error_code = krb5_cc_get_principal (self->kerberos_context, default_cache, &principal); + + if (error_code != 0) + return NULL; + + error_code = krb5_unparse_name_flags (self->kerberos_context, principal, 0, &unparsed_principal); + krb5_free_principal (self->kerberos_context, principal); + + if (error_code != 0) + return NULL; + + principal_name = g_strdup (unparsed_principal); + krb5_free_unparsed_name (self->kerberos_context, unparsed_principal); + + krb5_cc_close (self->kerberos_context, default_cache); + + return principal_name; +} + void goa_kerberos_identity_update (GoaKerberosIdentity *self, GoaKerberosIdentity *new_identity) { VerificationLevel old_verification_level, new_verification_level; + gboolean should_set_cache_active = FALSE; gboolean time_changed = FALSE; char *preauth_identity_source = NULL; + g_autofree char *default_principal = NULL; int comparison; G_LOCK (identity_lock); + g_debug ("GoaKerberosIdentity: Evaluating updated credentials for identity %s " + "(old credentials cache name: %s, new credentials cache name: %s)", + self->identifier, + self->active_credentials_cache_name, + new_identity->active_credentials_cache_name); old_verification_level = self->cached_verification_level; new_verification_level = new_identity->cached_verification_level; + default_principal = get_default_principal (self); comparison = goa_kerberos_identity_compare (self, new_identity); if (new_identity->active_credentials_cache_name != NULL) { g_autofree char *default_cache_name = NULL; krb5_ccache credentials_cache; krb5_ccache copied_cache; - gboolean should_switch_to_new_credentials_cache = FALSE; + gboolean should_set_cache_as_default = FALSE; + gboolean is_default_principal = FALSE, is_default_cache = FALSE; + gboolean cache_already_active = FALSE; + + is_default_principal = g_strcmp0 (default_principal, self->identifier) == 0; default_cache_name = get_default_cache_name (self); + is_default_cache = g_strcmp0 (default_cache_name, self->active_credentials_cache_name) == 0; + cache_already_active = g_strcmp0 (self->active_credentials_cache_name, new_identity->active_credentials_cache_name) == 0; - if (default_cache_name == NULL) - should_switch_to_new_credentials_cache = TRUE; + g_debug ("GoaKerberosIdentity: Default credentials cache is '%s' (is %sus, is %sactive)", default_cache_name, is_default_cache? "" : "not ", cache_already_active? "" : "not "); - credentials_cache = (krb5_ccache) g_hash_table_lookup (new_identity->credentials_caches, - new_identity->active_credentials_cache_name); - krb5_cc_dup (new_identity->kerberos_context, credentials_cache, &copied_cache); + if (default_principal == NULL) + { + should_set_cache_as_default = TRUE; + should_set_cache_active = TRUE; - if (g_strcmp0 (default_cache_name, self->active_credentials_cache_name) == 0) + g_debug ("GoaKerberosIdentity: Setting default credentials cache to '%s' (principal %s) " + "because there is no active default", + new_identity->active_credentials_cache_name, + self->identifier); + } + else if (!is_default_principal) + { + g_debug ("GoaKerberosIdentity: Not switching default credentials cache from '%s' to '%s' (principal %s) " + "because identity is currently not default (credentials already active? %s)", + default_cache_name, + new_identity->active_credentials_cache_name, + self->identifier, + cache_already_active? "yes" : "no"); + should_set_cache_as_default = FALSE; + + if (comparison < 0) + { + should_set_cache_active = TRUE; + + g_debug ("GoaKerberosIdentity: Switching identity %s from credentials cache '%s' to credentials cache '%s' " + "because it has better credentials", + self->identifier, + self->active_credentials_cache_name, + new_identity->active_credentials_cache_name); + } + else if (comparison == 0 && old_verification_level != VERIFICATION_LEVEL_SIGNED_IN) + { + should_set_cache_active = TRUE; + + g_debug ("GoaKerberosIdentity: Switching identity %s from credentials cache '%s' to " + "'%s' because it is newer and is otherwise just as good", + self->identifier, + self->active_credentials_cache_name, + new_identity->active_credentials_cache_name); + } + else + { + should_set_cache_active = FALSE; + + g_debug ("GoaKerberosIdentity: Not switching identity %s from credentials cache '%s' to '%s' " + "because it has less good credentials", + self->identifier, + default_cache_name, + new_identity->active_credentials_cache_name); + } + } + else if (cache_already_active) + { + if (is_default_cache) + { + should_set_cache_as_default = FALSE; + should_set_cache_active = FALSE; + + g_debug ("GoaKerberosIdentity: Not setting default credentials cache to '%s' " + "because cache is already active for identity %s and identity is default", + new_identity->active_credentials_cache_name, + self->identifier); + } + else + { + should_set_cache_as_default = TRUE; + should_set_cache_active = TRUE; + + g_debug ("GoaKerberosIdentity: Switching default credentials cache from '%s' to '%s' " + "because identity %s is default and cache is supposed to be active already but isn't", + default_cache_name, + new_identity->active_credentials_cache_name, + self->identifier); + } + } + else { - if ((comparison < 0) || - (comparison == 0 && old_verification_level != VERIFICATION_LEVEL_SIGNED_IN)) - should_switch_to_new_credentials_cache = TRUE; + if (comparison < 0) + { + should_set_cache_as_default = TRUE; + should_set_cache_active = TRUE; + + g_debug ("GoaKerberosIdentity: Switching default credentials cache from '%s' to '%s' " + "because identity %s is default and the cache has better credentials than those " + "in '%s'", + default_cache_name, + new_identity->active_credentials_cache_name, + self->identifier, + self->active_credentials_cache_name); + } + else if (comparison == 0 && old_verification_level != VERIFICATION_LEVEL_SIGNED_IN) + { + should_set_cache_as_default = TRUE; + should_set_cache_active = TRUE; + + g_debug ("GoaKerberosIdentity: Switching default credentials cache from '%s' to '%s' " + "because identity %s is default, and the cache has newer, and otherwise " + "just as good credentials as those in '%s'", + default_cache_name, + new_identity->active_credentials_cache_name, + self->identifier, + self->active_credentials_cache_name); + } + else + { + should_set_cache_as_default = FALSE; + should_set_cache_active = FALSE; + + g_debug ("GoaKerberosIdentity: Not switching default credentials cache from '%s' to '%s' " + "because identity %s is default but newer credentials aren't as good as those in '%s'", + default_cache_name, + new_identity->active_credentials_cache_name, + self->identifier, + self->active_credentials_cache_name); + } } + credentials_cache = (krb5_ccache) g_hash_table_lookup (new_identity->credentials_caches, + new_identity->active_credentials_cache_name); + krb5_cc_dup (new_identity->kerberos_context, credentials_cache, &copied_cache); - if (comparison < 0) + if (should_set_cache_active) { g_clear_pointer (&self->active_credentials_cache_name, g_free); self->active_credentials_cache_name = g_strdup (new_identity->active_credentials_cache_name); } goa_kerberos_identity_add_credentials_cache (self, copied_cache); - if (should_switch_to_new_credentials_cache) + if (should_set_cache_as_default) krb5_cc_switch (self->kerberos_context, copied_cache); } G_UNLOCK (identity_lock); clear_alarms (new_identity); - if (comparison >= 0) + if (!should_set_cache_active) return; G_LOCK (identity_lock); + g_debug ("GoaKerberosIdentity: Setting identity %s to use updated credentials in credentials cache '%s'", + self->identifier, self->active_credentials_cache_name); update_identifier (self, new_identity); time_changed |= set_start_time (self, new_identity->start_time); time_changed |= set_renewal_time (self, new_identity->renewal_time); time_changed |= set_expiration_time (self, new_identity->expiration_time); G_UNLOCK (identity_lock); if (time_changed) { if (new_verification_level == VERIFICATION_LEVEL_SIGNED_IN) - reset_alarms (self); + { + g_debug ("GoaKerberosIdentity: identity %s credentials have updated times, resetting alarms", self->identifier); + reset_alarms (self); + } else - clear_alarms (self); + { + g_debug ("GoaKerberosIdentity: identity %s credentials are now expired, clearing alarms", self->identifier); + clear_alarms (self); + } + } + else + { + g_debug ("GoaKerberosIdentity: identity %s credentials do not have updated times, so not adjusting alarms", self->identifier); } G_LOCK (identity_lock); g_free (self->preauth_identity_source); self->preauth_identity_source = preauth_identity_source; G_UNLOCK (identity_lock); if (new_verification_level != old_verification_level) { if (old_verification_level == VERIFICATION_LEVEL_SIGNED_IN && new_verification_level == VERIFICATION_LEVEL_EXISTS) { G_LOCK (identity_lock); self->cached_verification_level = new_verification_level; G_UNLOCK (identity_lock); g_signal_emit (G_OBJECT (self), signals[EXPIRED], 0); } else if (old_verification_level == VERIFICATION_LEVEL_EXISTS && new_verification_level == VERIFICATION_LEVEL_SIGNED_IN) { G_LOCK (identity_lock); self->cached_verification_level = new_verification_level; G_UNLOCK (identity_lock); g_signal_emit (G_OBJECT (self), signals[UNEXPIRED], 0); } else { G_LOCK (identity_lock); -- 2.39.3 From 829c24d0ecbac452c38d5217b8465457ea0cfb43 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Thu, 19 Jan 2023 11:31:14 -0500 Subject: [PATCH 16/22] goakerberosidentity: Fall back to stale credentials if active credentials get destroyed At the moment, the identity service doesn't recognize when a credentials cache gets kdestroy'd explicitly by the user. It knows when a principal is purged from all credential caches, but it doesn't know when a specific cache is removed. This means it doesn't fall back properly to an older credential crash if the active cache gets destroyed. This commit addresses that problem by reachitecting things a bit. Previously, cache updates were processed by creating a new transient GoaKerberosIdentity and then merging it into an existing identity using goa_kerberos_identity_update. Using a full blown GoaKerberosIdentity object as a wrapper around a lone credentials cache is kind of a weird pattern and doesn't facillate processing cache removal, since we can't create a transient identity for what's not there. This commit exports goa_kerberos_identity_add_credentials_cache as public api as an alternative to the merging transient identity flow. It also also adds a goa_kerberos_identity_refresh function to be called after goa_kerberos_identity_add_credentials_cache is preformed for all new caches. It handles merging in the new credentials from the updated credentials caches, and also handles cache removal. A benefit of this new flow is much of the guts of goa_kerberos_identity_update have now been moved to verify_identity allowing for some code deduplication. Previously verify_identity was only called at object construction time, though, so this commit adds more locking to accomodate it get called while the identity is in use by other threads. --- src/goaidentity/goakerberosidentity.c | 706 +++++++++---------- src/goaidentity/goakerberosidentity.h | 8 +- src/goaidentity/goakerberosidentitymanager.c | 132 ++-- 3 files changed, 419 insertions(+), 427 deletions(-) diff --git a/src/goaidentity/goakerberosidentity.c b/src/goaidentity/goakerberosidentity.c index d046a8a4..b5cbcecd 100644 --- a/src/goaidentity/goakerberosidentity.c +++ b/src/goaidentity/goakerberosidentity.c @@ -619,65 +619,62 @@ set_expiration_time (GoaKerberosIdentity *self, self->expiration_time = expiration_time; queue_notify (self, &self->expiration_time_idle_id, "expiration-timestamp"); return TRUE; } return FALSE; } static void examine_credentials (GoaKerberosIdentity *self, krb5_creds *credentials, krb5_timestamp *start_time, krb5_timestamp *renewal_time, krb5_timestamp *expiration_time, gboolean *are_expired) { krb5_timestamp credentials_start_time; krb5_timestamp credentials_end_time; krb5_timestamp current_time; G_LOCK (identity_lock); if (credentials->times.starttime != 0) credentials_start_time = credentials->times.starttime; else credentials_start_time = credentials->times.authtime; *renewal_time = credentials->times.renew_till; credentials_end_time = credentials->times.endtime; - if (self->start_time == 0) - *start_time = credentials_start_time; - else - *start_time = MIN (self->start_time, credentials_start_time); - *expiration_time = MAX (credentials->times.endtime, self->expiration_time); + *start_time = credentials_start_time; + *expiration_time = credentials->times.endtime; G_UNLOCK (identity_lock); current_time = get_current_time (self); if (current_time < credentials_start_time || credentials_end_time <= current_time) *are_expired = TRUE; else *are_expired = FALSE; } static VerificationLevel verify_identity_in_credentials_cache (GoaKerberosIdentity *self, char **preauth_identity_source, krb5_ccache credentials_cache, krb5_timestamp *start_time, krb5_timestamp *renewal_time, krb5_timestamp *expiration_time, GError **error) { krb5_principal principal = NULL; krb5_cc_cursor cursor; krb5_creds credentials; krb5_error_code error_code; VerificationLevel verification_level = VERIFICATION_LEVEL_UNVERIFIED; g_debug ("GoaKerberosIdentity: Verifying identity in credentials cache '%s'", krb5_cc_get_name (self->kerberos_context, credentials_cache)); error_code = krb5_cc_get_principal (self->kerberos_context, credentials_cache, &principal); @@ -771,146 +768,395 @@ verify_identity_in_credentials_cache (GoaKerberosIdentity *self, out: switch (verification_level) { case VERIFICATION_LEVEL_EXISTS: g_debug ("GoaKerberosIdentity: Credentials in credentials cache '%s' are out of date", krb5_cc_get_name (self->kerberos_context, credentials_cache)); break; case VERIFICATION_LEVEL_SIGNED_IN: g_debug ("GoaKerberosIdentity: Credentials in credentials cache '%s' are valid", krb5_cc_get_name (self->kerberos_context, credentials_cache)); break; case VERIFICATION_LEVEL_UNVERIFIED: g_debug ("GoaKerberosIdentity: Credentials in credentials cache '%s' are missing", krb5_cc_get_name (self->kerberos_context, credentials_cache)); break; case VERIFICATION_LEVEL_ERROR: default: g_debug ("GoaKerberosIdentity: Credentials in credentials cache '%s' could not be validated", krb5_cc_get_name (self->kerberos_context, credentials_cache)); break; } if (principal != NULL) krb5_free_principal (self->kerberos_context, principal); return verification_level; } +static char * +get_default_principal (GoaKerberosIdentity *self) +{ + int error_code; + krb5_ccache default_cache; + krb5_principal principal; + char *unparsed_principal, *principal_name; + + error_code = krb5_cc_default (self->kerberos_context, &default_cache); + + if (error_code != 0) + return NULL; + + /* Return NULL if the default cache doesn't pass basic sanity checks + */ + error_code = krb5_cc_get_principal (self->kerberos_context, default_cache, &principal); + + if (error_code != 0) + return NULL; + + error_code = krb5_unparse_name_flags (self->kerberos_context, principal, 0, &unparsed_principal); + krb5_free_principal (self->kerberos_context, principal); + + if (error_code != 0) + return NULL; + + principal_name = g_strdup (unparsed_principal); + krb5_free_unparsed_name (self->kerberos_context, unparsed_principal); + + krb5_cc_close (self->kerberos_context, default_cache); + + return principal_name; +} + +static char * +get_default_cache_name (GoaKerberosIdentity *self) +{ + int error_code; + krb5_ccache default_cache; + krb5_principal principal; + char *default_cache_name; + char *principal_name; + + error_code = krb5_cc_default (self->kerberos_context, &default_cache); + + if (error_code != 0) + return NULL; + + /* Return NULL if the default cache doesn't pass basic sanity checks + */ + error_code = krb5_cc_get_principal (self->kerberos_context, default_cache, &principal); + + if (error_code != 0) + return NULL; + + error_code = krb5_unparse_name_flags (self->kerberos_context, principal, 0, &principal_name); + krb5_free_principal (self->kerberos_context, principal); + + if (error_code != 0) + return NULL; + + krb5_free_unparsed_name (self->kerberos_context, principal_name); + + default_cache_name = g_strdup (krb5_cc_get_name (self->kerberos_context, default_cache)); + krb5_cc_close (self->kerberos_context, default_cache); + + return default_cache_name; +} + static VerificationLevel verify_identity (GoaKerberosIdentity *self, char **preauth_identity_source, GError **error) { krb5_ccache credentials_cache; + g_autofree char *default_principal = NULL; + g_autofree char *default_credentials_cache_name = NULL; + gboolean is_default_principal; + gboolean is_default_credentials_cache; + gboolean should_switch_default_credentials_cache = FALSE; + gboolean time_changed = FALSE; const char *name; - krb5_timestamp start_time = 0; - krb5_timestamp renewal_time = 0; - krb5_timestamp expiration_time = 0; - VerificationLevel verification_level = VERIFICATION_LEVEL_UNVERIFIED; + krb5_timestamp best_start_time = 0; + krb5_timestamp best_renewal_time = 0; + krb5_timestamp best_expiration_time = 0; + g_autofree char *best_preauth_identity_source = NULL; + g_autofree char *best_credentials_cache_name = NULL; + VerificationLevel old_verification_level = VERIFICATION_LEVEL_UNVERIFIED; + VerificationLevel best_verification_level = VERIFICATION_LEVEL_UNVERIFIED; GHashTableIter iter; if (self->active_credentials_cache_name != NULL) { + G_LOCK (identity_lock); credentials_cache = (krb5_ccache) g_hash_table_lookup (self->credentials_caches, self->active_credentials_cache_name); + G_UNLOCK (identity_lock); - verification_level = verify_identity_in_credentials_cache (self, - preauth_identity_source, - credentials_cache, - &start_time, - &renewal_time, - &expiration_time, - error); - if (verification_level == VERIFICATION_LEVEL_SIGNED_IN) + best_verification_level = verify_identity_in_credentials_cache (self, + &best_preauth_identity_source, + credentials_cache, + &best_start_time, + &best_renewal_time, + &best_expiration_time, + error); + G_LOCK (identity_lock); + best_credentials_cache_name = g_strdup (self->active_credentials_cache_name); + G_UNLOCK (identity_lock); + + if (best_verification_level == VERIFICATION_LEVEL_SIGNED_IN) goto out; - if (verification_level == VERIFICATION_LEVEL_UNVERIFIED) + if (best_verification_level == VERIFICATION_LEVEL_UNVERIFIED || + best_verification_level == VERIFICATION_LEVEL_ERROR) { - krb5_cc_close (self->kerberos_context, credentials_cache); - g_hash_table_remove (self->credentials_caches, self->active_credentials_cache_name); - g_clear_pointer (&self->active_credentials_cache_name, g_free); + g_clear_pointer (&best_credentials_cache_name, g_free); + + G_LOCK (identity_lock); + if (self->identifier != NULL) + { + krb5_cc_close (self->kerberos_context, credentials_cache); + g_hash_table_remove (self->credentials_caches, self->active_credentials_cache_name); + g_clear_pointer (&self->active_credentials_cache_name, g_free); + } + G_UNLOCK (identity_lock); } } - self->start_time = 0; - self->renewal_time = 0; - self->expiration_time = 0; + G_LOCK (identity_lock); + old_verification_level = self->cached_verification_level; + G_UNLOCK (identity_lock); + G_LOCK (identity_lock); g_hash_table_iter_init (&iter, self->credentials_caches); while (g_hash_table_iter_next (&iter, (gpointer *) &name, (gpointer*) &credentials_cache)) { krb5_timestamp new_start_time = 0; krb5_timestamp new_renewal_time = 0; krb5_timestamp new_expiration_time = 0; + g_autofree char *new_preauth_identity_source = NULL; + VerificationLevel verification_level = VERIFICATION_LEVEL_UNVERIFIED; + gboolean has_better_credentials = FALSE; if (g_strcmp0 (name, self->active_credentials_cache_name) == 0) continue; - g_clear_pointer (preauth_identity_source, g_free); + G_UNLOCK (identity_lock); + + if (preauth_identity_source != NULL) + g_clear_pointer (preauth_identity_source, g_free); + verification_level = verify_identity_in_credentials_cache (self, - preauth_identity_source, + &new_preauth_identity_source, credentials_cache, &new_start_time, &new_renewal_time, &new_expiration_time, error); - if (verification_level == VERIFICATION_LEVEL_SIGNED_IN || - self->active_credentials_cache_name == NULL) + if (verification_level == VERIFICATION_LEVEL_UNVERIFIED || + verification_level == VERIFICATION_LEVEL_ERROR) { - g_clear_pointer (&self->active_credentials_cache_name, g_free); - self->active_credentials_cache_name = g_strdup (name); - start_time = new_start_time; - renewal_time = new_renewal_time; - expiration_time = new_expiration_time; - - if (verification_level == VERIFICATION_LEVEL_SIGNED_IN) - break; + G_LOCK (identity_lock); + if (self->identifier != NULL) + { + krb5_cc_close (self->kerberos_context, credentials_cache); + g_hash_table_iter_remove (&iter); + } + + /* Note: The lock is held while iterating */ + continue; } - else if (verification_level == VERIFICATION_LEVEL_UNVERIFIED) + + if (best_verification_level < verification_level) + has_better_credentials = TRUE; + else if (best_verification_level > verification_level) + has_better_credentials = FALSE; + else if (best_expiration_time < new_expiration_time) + has_better_credentials = TRUE; + else if (best_expiration_time > new_expiration_time) + has_better_credentials = FALSE; + else if (best_start_time > new_start_time) + has_better_credentials = TRUE; + else if (best_start_time > new_start_time) + has_better_credentials = FALSE; + else if (best_renewal_time < new_renewal_time) + has_better_credentials = TRUE; + else if (best_renewal_time > new_renewal_time) + has_better_credentials = FALSE; + else + has_better_credentials = FALSE; + + if (has_better_credentials) { - krb5_cc_close (self->kerberos_context, credentials_cache); - g_hash_table_iter_remove (&iter); + best_verification_level = verification_level; + best_start_time = new_start_time; + best_renewal_time = new_renewal_time; + best_expiration_time = new_expiration_time; + + g_clear_pointer (&best_preauth_identity_source, g_free); + best_preauth_identity_source = g_steal_pointer (&new_preauth_identity_source); + + g_clear_pointer (&best_credentials_cache_name, g_free); + best_credentials_cache_name = g_strdup (name); } + + G_LOCK (identity_lock); + } + G_UNLOCK (identity_lock); + + if (best_credentials_cache_name == NULL) + { + g_hash_table_iter_init (&iter, self->credentials_caches); + if (g_hash_table_iter_next (&iter, (gpointer *) &name, (gpointer*) &credentials_cache)) + best_credentials_cache_name = g_strdup (name); } out: + G_LOCK (identity_lock); - set_start_time (self, start_time); - set_renewal_time (self, renewal_time); - set_expiration_time (self, expiration_time); + g_clear_pointer (&self->active_credentials_cache_name, g_free); + self->active_credentials_cache_name = g_steal_pointer (&best_credentials_cache_name); G_UNLOCK (identity_lock); - return verification_level; + *preauth_identity_source = g_steal_pointer (&best_preauth_identity_source); + + if (best_verification_level > VERIFICATION_LEVEL_UNVERIFIED) + { + G_LOCK (identity_lock); + time_changed |= set_start_time (self, best_start_time); + time_changed |= set_renewal_time (self, best_renewal_time); + time_changed |= set_expiration_time (self, best_expiration_time); + G_UNLOCK (identity_lock); + + if (time_changed) + { + if (best_verification_level == VERIFICATION_LEVEL_SIGNED_IN) + { + g_debug ("GoaKerberosIdentity: identity %s credentials have updated times, resetting alarms", self->identifier); + reset_alarms (self); + } + else + { + g_debug ("GoaKerberosIdentity: identity %s credentials are now expired, clearing alarms", self->identifier); + clear_alarms (self); + } + } + else + { + g_debug ("GoaKerberosIdentity: identity %s credentials do not have updated times, so not adjusting alarms", self->identifier); + } + } + else + { + g_debug ("GoaKerberosIdentity: identity is unverified, clearing alarms"); + clear_alarms (self); + } + + if (best_verification_level != old_verification_level) + { + if (old_verification_level == VERIFICATION_LEVEL_SIGNED_IN && + best_verification_level == VERIFICATION_LEVEL_EXISTS) + { + G_LOCK (identity_lock); + self->cached_verification_level = best_verification_level; + G_UNLOCK (identity_lock); + + g_signal_emit (G_OBJECT (self), signals[EXPIRED], 0); + } + else if (old_verification_level == VERIFICATION_LEVEL_EXISTS && + best_verification_level == VERIFICATION_LEVEL_SIGNED_IN) + { + G_LOCK (identity_lock); + self->cached_verification_level = best_verification_level; + G_UNLOCK (identity_lock); + + g_signal_emit (G_OBJECT (self), signals[UNEXPIRED], 0); + } + else + { + G_LOCK (identity_lock); + self->cached_verification_level = best_verification_level; + G_UNLOCK (identity_lock); + } + queue_notify (self, &self->is_signed_in_idle_id, "is-signed-in"); + } + + default_principal = get_default_principal (self); + is_default_principal = g_strcmp0 (default_principal, self->identifier) == 0; + + default_credentials_cache_name = get_default_cache_name (self); + is_default_credentials_cache = g_strcmp0 (default_credentials_cache_name, self->active_credentials_cache_name) == 0; + + if (self->active_credentials_cache_name == NULL) + { + g_debug ("GoaKerberosIdentity: Not switching default credentials cache because identity %s has no active credentials cache to switch to", self->identifier); + should_switch_default_credentials_cache = FALSE; + } + else if (self->identifier == NULL) + { + g_debug ("GoaKerberosIdentity: Not switching default credentials cache to '%s' because it is not yet initialized", self->active_credentials_cache_name); + should_switch_default_credentials_cache = FALSE; + } + else if (default_principal == NULL) + { + g_debug ("GoaKerberosIdentity: Switching default credentials cache to '%s' (identity %s) because there is currently no default", self->active_credentials_cache_name, self->identifier); + should_switch_default_credentials_cache = TRUE; + } + else if (!is_default_principal) + { + g_debug ("GoaKerberosIdentity: Not switching default credentials cache because identity %s is not the default identity", self->identifier); + should_switch_default_credentials_cache = FALSE; + } + else if (!is_default_credentials_cache) + { + g_debug ("GoaKerberosIdentity: Switching default credentials cache from '%s' to '%s' because identity %s is the default, and that credentials cache is supposed to be the active cache for that identity", + default_credentials_cache_name, self->active_credentials_cache_name, self->identifier); + should_switch_default_credentials_cache = TRUE; + } + else + { + g_debug ("GoaKerberosIdentity: Not switching default credentials cache to '%s' for identity %s because it's already the default", self->active_credentials_cache_name, self->identifier); + should_switch_default_credentials_cache = FALSE; + } + + if (should_switch_default_credentials_cache) + { + G_LOCK (identity_lock); + credentials_cache = (krb5_ccache) g_hash_table_lookup (self->credentials_caches, + self->active_credentials_cache_name); + krb5_cc_switch (self->kerberos_context, credentials_cache); + G_UNLOCK (identity_lock); + } + + return best_verification_level; } static gboolean goa_kerberos_identity_is_signed_in (GoaIdentity *identity) { GoaKerberosIdentity *self = GOA_KERBEROS_IDENTITY (identity); gboolean is_signed_in = FALSE; G_LOCK (identity_lock); if (self->cached_verification_level == VERIFICATION_LEVEL_SIGNED_IN) is_signed_in = TRUE; G_UNLOCK (identity_lock); return is_signed_in; } static void identity_interface_init (GoaIdentityInterface *interface) { interface->get_identifier = goa_kerberos_identity_get_identifier; interface->is_signed_in = goa_kerberos_identity_is_signed_in; } static void on_expiration_alarm_fired (GoaAlarm *alarm, GoaKerberosIdentity *self) { g_return_if_fail (GOA_IS_ALARM (alarm)); g_return_if_fail (GOA_IS_KERBEROS_IDENTITY (self)); @@ -1059,60 +1305,62 @@ reset_alarms (GoaKerberosIdentity *self) GDateTime *expiring_time = NULL; GDateTime *latest_possible_renewal_time = NULL; GDateTime *renewal_time = NULL; G_LOCK (identity_lock); start_time = g_date_time_new_from_unix_local (self->start_time); if (self->renewal_time != 0) latest_possible_renewal_time = g_date_time_new_from_unix_local (self->renewal_time); expiration_time = g_date_time_new_from_unix_local (self->expiration_time); G_UNLOCK (identity_lock); /* Let the user reauthenticate 10 min before expiration */ expiring_time = g_date_time_add_minutes (expiration_time, -10); if (latest_possible_renewal_time != NULL) { GTimeSpan lifespan; lifespan = g_date_time_difference (expiration_time, start_time); /* Try to quietly auto-renew halfway through so in ideal configurations * the ticket is never more than halfway to unrenewable */ renewal_time = g_date_time_add (start_time, lifespan / 2); } disconnect_alarm_signals (self); if (renewal_time != NULL) reset_alarm (self, &self->renewal_alarm, renewal_time); + else if (self->renewal_alarm != NULL) + clear_alarm_and_unref_on_idle (self, &self->renewal_alarm); reset_alarm (self, &self->expiring_alarm, expiring_time); reset_alarm (self, &self->expiration_alarm, expiration_time); g_clear_pointer (&expiring_time, g_date_time_unref); g_clear_pointer (&renewal_time, g_date_time_unref); g_clear_pointer (&expiration_time, g_date_time_unref); g_clear_pointer (&latest_possible_renewal_time, g_date_time_unref); g_clear_pointer (&start_time, g_date_time_unref); connect_alarm_signals (self); } static void clear_alarms (GoaKerberosIdentity *self) { disconnect_alarm_signals (self); clear_alarm_and_unref_on_idle (self, &self->renewal_alarm); clear_alarm_and_unref_on_idle (self, &self->expiring_alarm); clear_alarm_and_unref_on_idle (self, &self->expiration_alarm); } static gboolean goa_kerberos_identity_initable_init (GInitable *initable, GCancellable *cancellable, GError **error) { GoaKerberosIdentity *self = GOA_KERBEROS_IDENTITY (initable); GError *verification_error; @@ -1185,114 +1433,137 @@ on_kerberos_inquiry (krb5_context kerberos_context, GoaIdentityInquiry *inquiry; krb5_error_code error_code = 0; if (number_of_prompts == 0) goto out; inquiry = goa_kerberos_identity_inquiry_new (operation->identity, name, banner, prompts, number_of_prompts); operation->inquiry_func (inquiry, operation->cancellable, operation->inquiry_data); if (goa_identity_inquiry_is_failed (inquiry)) error_code = KRB5_LIBOS_CANTREADPWD; else if (!goa_identity_inquiry_is_complete (inquiry)) g_cancellable_cancel (operation->cancellable); if (g_cancellable_is_cancelled (operation->cancellable)) error_code = KRB5_LIBOS_PWDINTR; g_object_unref (inquiry); out: return error_code; } -static void +gboolean +goa_kerberos_identity_has_credentials_cache (GoaKerberosIdentity *self, + krb5_ccache credentials_cache) +{ + const char *cache_name; + + cache_name = krb5_cc_get_name (self->kerberos_context, credentials_cache); + + return g_hash_table_contains (self->credentials_caches, cache_name); +} + +void goa_kerberos_identity_add_credentials_cache (GoaKerberosIdentity *self, krb5_ccache credentials_cache) { const char *cache_name; + krb5_ccache copied_cache; cache_name = krb5_cc_get_name (self->kerberos_context, credentials_cache); if (g_hash_table_contains (self->credentials_caches, cache_name)) { krb5_ccache old_credentials_cache; + g_debug ("GoaKerberosIdentity: Updating credentials in credentials cache '%s' for identity %s ", cache_name, self->identifier); + old_credentials_cache = (krb5_ccache) g_hash_table_lookup (self->credentials_caches, cache_name); krb5_cc_close (self->kerberos_context, old_credentials_cache); } + else + { + if (self->identifier != NULL) + g_debug ("GoaKerberosIdentity: Associating identity %s with new credentials cache '%s'", self->identifier, cache_name); + else + g_debug ("GoaKerberosIdentity: Associating new identity with new credentials cache '%s'", cache_name); + } - g_hash_table_replace (self->credentials_caches, g_strdup (cache_name), credentials_cache); + krb5_cc_dup (self->kerberos_context, credentials_cache, &copied_cache); + g_hash_table_replace (self->credentials_caches, g_strdup (cache_name), copied_cache); if (self->active_credentials_cache_name == NULL) { self->active_credentials_cache_name = g_strdup (cache_name); } } static gboolean create_credentials_cache (GoaKerberosIdentity *self, GError **error) { krb5_ccache default_cache; krb5_ccache new_cache; const char *cache_type; krb5_error_code error_code; error_code = krb5_cc_default (self->kerberos_context, &default_cache); if (error_code == 0) { cache_type = krb5_cc_get_type (self->kerberos_context, default_cache); error_code = krb5_cc_new_unique (self->kerberos_context, cache_type, NULL, &new_cache); } if (error_code != 0) { set_and_prefix_error_from_krb5_error_code (self, error, GOA_IDENTITY_ERROR_ALLOCATING_CREDENTIALS, error_code, _("Could not create credential cache: ")); return FALSE; } goa_kerberos_identity_add_credentials_cache (self, new_cache); + krb5_cc_close (self->kerberos_context, new_cache); return TRUE; } static gboolean goa_kerberos_identity_update_credentials (GoaKerberosIdentity *self, krb5_principal principal, krb5_creds *new_credentials, GError **error) { krb5_error_code error_code; krb5_ccache credentials_cache; if (self->active_credentials_cache_name == NULL) { if (!create_credentials_cache (self, error)) { krb5_free_cred_contents (self->kerberos_context, new_credentials); goto out; } } credentials_cache = (krb5_ccache) g_hash_table_lookup (self->credentials_caches, self->active_credentials_cache_name); error_code = krb5_cc_initialize (self->kerberos_context, credentials_cache, principal); if (error_code != 0) { set_and_prefix_error_from_krb5_error_code (self, @@ -1348,81 +1619,78 @@ sign_in_operation_new (GoaKerberosIdentity *identity, } static void sign_in_operation_free (SignInOperation *operation) { g_object_unref (operation->identity); g_object_unref (operation->cancellable); g_slice_free (SignInOperation, operation); } gboolean goa_kerberos_identity_sign_in (GoaKerberosIdentity *self, const char *principal_name, gconstpointer initial_password, const char *preauth_source, GoaIdentitySignInFlags flags, GoaIdentityInquiryFunc inquiry_func, gpointer inquiry_data, GDestroyNotify destroy_notify, GCancellable *cancellable, GError **error) { SignInOperation *operation; krb5_principal principal; krb5_error_code error_code; krb5_creds new_credentials; krb5_get_init_creds_opt *options; krb5_deltat start_time; char *service_name; - gboolean signed_in; if (g_cancellable_set_error_if_cancelled (cancellable, error)) return FALSE; error_code = krb5_get_init_creds_opt_alloc (self->kerberos_context, &options); if (error_code != 0) { set_and_prefix_error_from_krb5_error_code (self, error, GOA_IDENTITY_ERROR_ALLOCATING_CREDENTIALS, error_code, "%s", ""); /* Silence -Wformat-zero-length */ if (destroy_notify) destroy_notify (inquiry_data); return FALSE; } - signed_in = FALSE; - operation = sign_in_operation_new (self, inquiry_func, inquiry_data, destroy_notify, cancellable); if (g_strcmp0 (self->identifier, principal_name) != 0) { g_free (self->identifier); self->identifier = g_strdup (principal_name); } error_code = krb5_parse_name (self->kerberos_context, principal_name, &principal); if (error_code != 0) { set_and_prefix_error_from_krb5_error_code (self, error, GOA_IDENTITY_ERROR_PARSING_IDENTIFIER, error_code, "%s", ""); /* Silence -Wformat-zero-length */ if (destroy_notify) destroy_notify (inquiry_data); return FALSE; } if ((flags & GOA_IDENTITY_SIGN_IN_FLAGS_DISALLOW_FORWARDING) == 0) krb5_get_init_creds_opt_set_forwardable (options, TRUE); if ((flags & GOA_IDENTITY_SIGN_IN_FLAGS_DISALLOW_PROXYING) == 0) @@ -1468,426 +1736,128 @@ goa_kerberos_identity_sign_in (GoaKerberosIdentity *self, if (error_code != 0) { set_and_prefix_error_from_krb5_error_code (self, error, GOA_IDENTITY_ERROR_AUTHENTICATION_FAILED, error_code, "%s", ""); /* Silence -Wformat-zero-length */ if (destroy_notify) destroy_notify (inquiry_data); sign_in_operation_free (operation); krb5_free_principal (self->kerberos_context, principal); goto done; } if (destroy_notify) destroy_notify (inquiry_data); sign_in_operation_free (operation); if (!goa_kerberos_identity_update_credentials (self, principal, &new_credentials, error)) { krb5_free_principal (self->kerberos_context, principal); goto done; } krb5_free_principal (self->kerberos_context, principal); - g_debug ("GoaKerberosIdentity: identity signed in"); - signed_in = TRUE; done: - return signed_in; -} - -static void -update_identifier (GoaKerberosIdentity *self, GoaKerberosIdentity *new_identity) -{ - char *new_identifier; + goa_kerberos_identity_refresh (self); - new_identifier = get_identifier (new_identity, NULL); - if (g_strcmp0 (self->identifier, new_identifier) != 0 && new_identifier != NULL) - { - g_free (self->identifier); - self->identifier = new_identifier; - queue_notify (self, &self->identifier_idle_id, "identifier"); - } - else + if (self->cached_verification_level != VERIFICATION_LEVEL_SIGNED_IN) { - g_free (new_identifier); + g_debug ("GoaKerberosIdentity: Identity '%s' could not be signed in", principal_name); + return FALSE; } -} - -static int -goa_kerberos_identity_compare (GoaKerberosIdentity *self, - GoaKerberosIdentity *new_identity) -{ - if (self->cached_verification_level < new_identity->cached_verification_level) - return -100; - - if (self->cached_verification_level > new_identity->cached_verification_level) - return 100; - - if (self->cached_verification_level != VERIFICATION_LEVEL_SIGNED_IN) - return 50; - - if (self->expiration_time < new_identity->expiration_time) - return -10; - - if (self->expiration_time > new_identity->expiration_time) - return 10; - - if (self->start_time > new_identity->start_time) - return -5; - - if (self->start_time < new_identity->start_time) - return 5; - - if (self->renewal_time < new_identity->renewal_time) - return -1; - - if (self->renewal_time > new_identity->renewal_time) - return 1; - - return 0; -} - -static char * -get_default_cache_name (GoaKerberosIdentity *self) -{ - int error_code; - krb5_ccache default_cache; - krb5_principal principal; - char *default_cache_name; - char *principal_name; - - error_code = krb5_cc_default (self->kerberos_context, &default_cache); - - if (error_code != 0) - return NULL; - - /* Return NULL if the default cache doesn't pass basic sanity checks - */ - error_code = krb5_cc_get_principal (self->kerberos_context, default_cache, &principal); - - if (error_code != 0) - return NULL; - - error_code = krb5_unparse_name_flags (self->kerberos_context, principal, 0, &principal_name); - krb5_free_principal (self->kerberos_context, principal); - - if (error_code != 0) - return NULL; - - krb5_free_unparsed_name (self->kerberos_context, principal_name); - - default_cache_name = g_strdup (krb5_cc_get_name (self->kerberos_context, default_cache)); - krb5_cc_close (self->kerberos_context, default_cache); - - return default_cache_name; -} - -static char * -get_default_principal (GoaKerberosIdentity *self) -{ - int error_code; - krb5_ccache default_cache; - krb5_principal principal; - char *unparsed_principal, *principal_name; - - error_code = krb5_cc_default (self->kerberos_context, &default_cache); - - if (error_code != 0) - return NULL; - - /* Return NULL if the default cache doesn't pass basic sanity checks - */ - error_code = krb5_cc_get_principal (self->kerberos_context, default_cache, &principal); - - if (error_code != 0) - return NULL; - - error_code = krb5_unparse_name_flags (self->kerberos_context, principal, 0, &unparsed_principal); - krb5_free_principal (self->kerberos_context, principal); - - if (error_code != 0) - return NULL; - - principal_name = g_strdup (unparsed_principal); - krb5_free_unparsed_name (self->kerberos_context, unparsed_principal); - - krb5_cc_close (self->kerberos_context, default_cache); - return principal_name; + g_debug ("GoaKerberosIdentity: Identity '%s' signed in", principal_name); + return TRUE; } void -goa_kerberos_identity_update (GoaKerberosIdentity *self, - GoaKerberosIdentity *new_identity) +goa_kerberos_identity_refresh (GoaKerberosIdentity *self) { VerificationLevel old_verification_level, new_verification_level; - gboolean should_set_cache_active = FALSE; - gboolean time_changed = FALSE; - char *preauth_identity_source = NULL; - g_autofree char *default_principal = NULL; - int comparison; + g_autofree char *preauth_identity_source = NULL; + g_autoptr (GError) error = NULL; - G_LOCK (identity_lock); - - g_debug ("GoaKerberosIdentity: Evaluating updated credentials for identity %s " - "(old credentials cache name: %s, new credentials cache name: %s)", + g_debug ("GoaKerberosIdentity: Refreshing identity %s (active credentials cache: %s)", self->identifier, - self->active_credentials_cache_name, - new_identity->active_credentials_cache_name); - old_verification_level = self->cached_verification_level; - new_verification_level = new_identity->cached_verification_level; - - default_principal = get_default_principal (self); - comparison = goa_kerberos_identity_compare (self, new_identity); - - if (new_identity->active_credentials_cache_name != NULL) - { - g_autofree char *default_cache_name = NULL; - krb5_ccache credentials_cache; - krb5_ccache copied_cache; - gboolean should_set_cache_as_default = FALSE; - gboolean is_default_principal = FALSE, is_default_cache = FALSE; - gboolean cache_already_active = FALSE; - - is_default_principal = g_strcmp0 (default_principal, self->identifier) == 0; - - default_cache_name = get_default_cache_name (self); - is_default_cache = g_strcmp0 (default_cache_name, self->active_credentials_cache_name) == 0; - cache_already_active = g_strcmp0 (self->active_credentials_cache_name, new_identity->active_credentials_cache_name) == 0; - - g_debug ("GoaKerberosIdentity: Default credentials cache is '%s' (is %sus, is %sactive)", default_cache_name, is_default_cache? "" : "not ", cache_already_active? "" : "not "); - - if (default_principal == NULL) - { - should_set_cache_as_default = TRUE; - should_set_cache_active = TRUE; - - g_debug ("GoaKerberosIdentity: Setting default credentials cache to '%s' (principal %s) " - "because there is no active default", - new_identity->active_credentials_cache_name, - self->identifier); - } - else if (!is_default_principal) - { - g_debug ("GoaKerberosIdentity: Not switching default credentials cache from '%s' to '%s' (principal %s) " - "because identity is currently not default (credentials already active? %s)", - default_cache_name, - new_identity->active_credentials_cache_name, - self->identifier, - cache_already_active? "yes" : "no"); - should_set_cache_as_default = FALSE; - - if (comparison < 0) - { - should_set_cache_active = TRUE; - - g_debug ("GoaKerberosIdentity: Switching identity %s from credentials cache '%s' to credentials cache '%s' " - "because it has better credentials", - self->identifier, - self->active_credentials_cache_name, - new_identity->active_credentials_cache_name); - } - else if (comparison == 0 && old_verification_level != VERIFICATION_LEVEL_SIGNED_IN) - { - should_set_cache_active = TRUE; - - g_debug ("GoaKerberosIdentity: Switching identity %s from credentials cache '%s' to " - "'%s' because it is newer and is otherwise just as good", - self->identifier, - self->active_credentials_cache_name, - new_identity->active_credentials_cache_name); - } - else - { - should_set_cache_active = FALSE; - - g_debug ("GoaKerberosIdentity: Not switching identity %s from credentials cache '%s' to '%s' " - "because it has less good credentials", - self->identifier, - default_cache_name, - new_identity->active_credentials_cache_name); - } - } - else if (cache_already_active) - { - if (is_default_cache) - { - should_set_cache_as_default = FALSE; - should_set_cache_active = FALSE; - - g_debug ("GoaKerberosIdentity: Not setting default credentials cache to '%s' " - "because cache is already active for identity %s and identity is default", - new_identity->active_credentials_cache_name, - self->identifier); - } - else - { - should_set_cache_as_default = TRUE; - should_set_cache_active = TRUE; - - g_debug ("GoaKerberosIdentity: Switching default credentials cache from '%s' to '%s' " - "because identity %s is default and cache is supposed to be active already but isn't", - default_cache_name, - new_identity->active_credentials_cache_name, - self->identifier); - } - } - else - { - if (comparison < 0) - { - should_set_cache_as_default = TRUE; - should_set_cache_active = TRUE; - - g_debug ("GoaKerberosIdentity: Switching default credentials cache from '%s' to '%s' " - "because identity %s is default and the cache has better credentials than those " - "in '%s'", - default_cache_name, - new_identity->active_credentials_cache_name, - self->identifier, - self->active_credentials_cache_name); - } - else if (comparison == 0 && old_verification_level != VERIFICATION_LEVEL_SIGNED_IN) - { - should_set_cache_as_default = TRUE; - should_set_cache_active = TRUE; - - g_debug ("GoaKerberosIdentity: Switching default credentials cache from '%s' to '%s' " - "because identity %s is default, and the cache has newer, and otherwise " - "just as good credentials as those in '%s'", - default_cache_name, - new_identity->active_credentials_cache_name, - self->identifier, - self->active_credentials_cache_name); - } - else - { - should_set_cache_as_default = FALSE; - should_set_cache_active = FALSE; - - g_debug ("GoaKerberosIdentity: Not switching default credentials cache from '%s' to '%s' " - "because identity %s is default but newer credentials aren't as good as those in '%s'", - default_cache_name, - new_identity->active_credentials_cache_name, - self->identifier, - self->active_credentials_cache_name); - } - } - credentials_cache = (krb5_ccache) g_hash_table_lookup (new_identity->credentials_caches, - new_identity->active_credentials_cache_name); - krb5_cc_dup (new_identity->kerberos_context, credentials_cache, &copied_cache); - - if (should_set_cache_active) - { - g_clear_pointer (&self->active_credentials_cache_name, g_free); - self->active_credentials_cache_name = g_strdup (new_identity->active_credentials_cache_name); - } + self->active_credentials_cache_name); - goa_kerberos_identity_add_credentials_cache (self, copied_cache); - - if (should_set_cache_as_default) - krb5_cc_switch (self->kerberos_context, copied_cache); - } + G_LOCK (identity_lock); + old_verification_level = self->cached_verification_level; G_UNLOCK (identity_lock); - clear_alarms (new_identity); - - if (!should_set_cache_active) - return; + new_verification_level = verify_identity (self, &preauth_identity_source, &error); G_LOCK (identity_lock); - g_debug ("GoaKerberosIdentity: Setting identity %s to use updated credentials in credentials cache '%s'", - self->identifier, self->active_credentials_cache_name); - update_identifier (self, new_identity); - time_changed |= set_start_time (self, new_identity->start_time); - time_changed |= set_renewal_time (self, new_identity->renewal_time); - time_changed |= set_expiration_time (self, new_identity->expiration_time); - G_UNLOCK (identity_lock); - - if (time_changed) - { - if (new_verification_level == VERIFICATION_LEVEL_SIGNED_IN) - { - g_debug ("GoaKerberosIdentity: identity %s credentials have updated times, resetting alarms", self->identifier); - reset_alarms (self); - } - else - { - g_debug ("GoaKerberosIdentity: identity %s credentials are now expired, clearing alarms", self->identifier); - clear_alarms (self); - } - } - else + if (g_strcmp0 (self->preauth_identity_source, preauth_identity_source) != 0) { - g_debug ("GoaKerberosIdentity: identity %s credentials do not have updated times, so not adjusting alarms", self->identifier); + g_free (self->preauth_identity_source); + self->preauth_identity_source = g_steal_pointer (&preauth_identity_source); } - - G_LOCK (identity_lock); - g_free (self->preauth_identity_source); - self->preauth_identity_source = preauth_identity_source; G_UNLOCK (identity_lock); if (new_verification_level != old_verification_level) { - if (old_verification_level == VERIFICATION_LEVEL_SIGNED_IN && + if ((old_verification_level == VERIFICATION_LEVEL_SIGNED_IN) && new_verification_level == VERIFICATION_LEVEL_EXISTS) { G_LOCK (identity_lock); self->cached_verification_level = new_verification_level; G_UNLOCK (identity_lock); g_signal_emit (G_OBJECT (self), signals[EXPIRED], 0); } else if (old_verification_level == VERIFICATION_LEVEL_EXISTS && new_verification_level == VERIFICATION_LEVEL_SIGNED_IN) { G_LOCK (identity_lock); self->cached_verification_level = new_verification_level; G_UNLOCK (identity_lock); g_signal_emit (G_OBJECT (self), signals[UNEXPIRED], 0); } else { G_LOCK (identity_lock); self->cached_verification_level = new_verification_level; G_UNLOCK (identity_lock); } + G_LOCK (identity_lock); queue_notify (self, &self->is_signed_in_idle_id, "is-signed-in"); + G_UNLOCK (identity_lock); } } gboolean goa_kerberos_identity_renew (GoaKerberosIdentity *self, GError **error) { krb5_error_code error_code = 0; krb5_principal principal; krb5_creds new_credentials; krb5_ccache credentials_cache; gboolean renewed = FALSE; char *name = NULL; if (self->active_credentials_cache_name == NULL) { g_set_error (error, GOA_IDENTITY_ERROR, GOA_IDENTITY_ERROR_CREDENTIALS_UNAVAILABLE, _("Not signed in")); goto out; } credentials_cache = (krb5_ccache) g_hash_table_lookup (self->credentials_caches, self->active_credentials_cache_name); error_code = krb5_cc_get_principal (self->kerberos_context, credentials_cache, &principal); if (error_code != 0) { set_and_prefix_error_from_krb5_error_code (self, error, GOA_IDENTITY_ERROR_CREDENTIALS_UNAVAILABLE, @@ -1945,47 +1915,45 @@ goa_kerberos_identity_erase (GoaKerberosIdentity *self, GError **error) g_debug ("GoaKerberosIdentity: Destroying active credentials cache %s", self->active_credentials_cache_name); error_code = krb5_cc_destroy (self->kerberos_context, credentials_cache); g_hash_table_remove (self->credentials_caches, self->active_credentials_cache_name); g_clear_pointer (&self->active_credentials_cache_name, g_free); if (error_code != 0) { set_and_prefix_error_from_krb5_error_code (self, error, GOA_IDENTITY_ERROR_REMOVING_CREDENTIALS, error_code, _("Could not erase identity: ")); } } g_hash_table_iter_init (&iter, self->credentials_caches); while (g_hash_table_iter_next (&iter, (gpointer *) &name, (gpointer*) &credentials_cache)) { g_debug ("GoaKerberosIdentity: Destroying inactive credentials cache %s", name); krb5_cc_destroy (self->kerberos_context, credentials_cache); } g_hash_table_remove_all (self->credentials_caches); return error_code == 0; } GoaIdentity * goa_kerberos_identity_new (krb5_context context, krb5_ccache cache, GError **error) { GoaKerberosIdentity *self; - krb5_ccache copied_cache; self = GOA_KERBEROS_IDENTITY (g_object_new (GOA_TYPE_KERBEROS_IDENTITY, NULL)); self->kerberos_context = context; - krb5_cc_dup (self->kerberos_context, cache, &copied_cache); - goa_kerberos_identity_add_credentials_cache (self, copied_cache); + goa_kerberos_identity_add_credentials_cache (self, cache); error = NULL; if (!g_initable_init (G_INITABLE (self), NULL, error)) { g_object_unref (self); return NULL; } return GOA_IDENTITY (self); } diff --git a/src/goaidentity/goakerberosidentity.h b/src/goaidentity/goakerberosidentity.h index de0752cd..70cd4e3f 100644 --- a/src/goaidentity/goakerberosidentity.h +++ b/src/goaidentity/goakerberosidentity.h @@ -14,54 +14,58 @@ * * You should have received a copy of the GNU Lesser General * Public License along with this library; if not, see . */ #ifndef __GOA_KERBEROS_IDENTITY_H__ #define __GOA_KERBEROS_IDENTITY_H__ #include #include #include #include "goaidentityinquiry.h" G_BEGIN_DECLS #define GOA_TYPE_KERBEROS_IDENTITY (goa_kerberos_identity_get_type ()) G_DECLARE_FINAL_TYPE (GoaKerberosIdentity, goa_kerberos_identity, GOA, KERBEROS_IDENTITY, GObject); typedef enum { GOA_KERBEROS_IDENTITY_DESCRIPTION_REALM, GOA_KERBEROS_IDENTITY_DESCRIPTION_USERNAME_AND_REALM, GOA_KERBEROS_IDENTITY_DESCRIPTION_USERNAME_ROLE_AND_REALM } GoaKerberosIdentityDescriptionLevel; GoaIdentity *goa_kerberos_identity_new (krb5_context kerberos_context, krb5_ccache cache, GError **error); +gboolean goa_kerberos_identity_has_credentials_cache (GoaKerberosIdentity *self, + krb5_ccache credentials_cache); +void goa_kerberos_identity_add_credentials_cache (GoaKerberosIdentity *self, + krb5_ccache cache); + gboolean goa_kerberos_identity_sign_in (GoaKerberosIdentity *self, const char *principal_name, gconstpointer initial_password, const char *preauth_source, GoaIdentitySignInFlags flags, GoaIdentityInquiryFunc inquiry_func, gpointer inquiry_data, GDestroyNotify destroy_notify, GCancellable *cancellable, GError **error); -void goa_kerberos_identity_update (GoaKerberosIdentity *identity, - GoaKerberosIdentity *new_identity); +void goa_kerberos_identity_refresh (GoaKerberosIdentity *identity); gboolean goa_kerberos_identity_renew (GoaKerberosIdentity *self, GError **error); gboolean goa_kerberos_identity_erase (GoaKerberosIdentity *self, GError **error); char *goa_kerberos_identity_get_principal_name (GoaKerberosIdentity *self); char *goa_kerberos_identity_get_realm_name (GoaKerberosIdentity *self); char *goa_kerberos_identity_get_preauthentication_source (GoaKerberosIdentity *self); G_END_DECLS #endif /* __GOA_KERBEROS_IDENTITY_H__ */ diff --git a/src/goaidentity/goakerberosidentitymanager.c b/src/goaidentity/goakerberosidentitymanager.c index d4ff2de4..7785b891 100644 --- a/src/goaidentity/goakerberosidentitymanager.c +++ b/src/goaidentity/goakerberosidentitymanager.c @@ -471,207 +471,227 @@ static void drop_stale_identities (GoaKerberosIdentityManager *self, Operation *operation, GHashTable *known_identities) { GList *stale_identity_ids; GList *node; stale_identity_ids = g_hash_table_get_keys (self->identities); node = stale_identity_ids; while (node != NULL) { GoaIdentity *identity; const char *identifier = node->data; identity = g_hash_table_lookup (known_identities, identifier); if (identity == NULL) { identity = g_hash_table_lookup (self->identities, identifier); if (identity != NULL) { remove_identity (self, operation, identity); } } node = node->next; } g_list_free (stale_identity_ids); } -static void -update_identity (GoaKerberosIdentityManager *self, - Operation *operation, - GoaIdentity *identity, - GoaIdentity *new_identity) -{ - - goa_kerberos_identity_update (GOA_KERBEROS_IDENTITY (identity), - GOA_KERBEROS_IDENTITY (new_identity)); - - if (goa_identity_is_signed_in (identity)) - { - IdentitySignalWork *work; - - /* if it's not expired, send out a refresh signal */ - g_debug ("GoaKerberosIdentityManager: identity '%s' refreshed", - goa_identity_get_identifier (identity)); - - work = identity_signal_work_new (self, identity); - goa_kerberos_identify_manager_send_to_context (operation->context, - (GSourceFunc) - do_identity_signal_refreshed_work, - work, - (GDestroyNotify) - identity_signal_work_free); - } -} - static void add_identity (GoaKerberosIdentityManager *self, Operation *operation, GoaIdentity *identity, const char *identifier) { IdentitySignalWork *work; g_hash_table_replace (self->identities, g_strdup (identifier), g_object_ref (identity)); if (!goa_identity_is_signed_in (identity)) g_hash_table_replace (self->expired_identities, g_strdup (identifier), identity); work = identity_signal_work_new (self, identity); goa_kerberos_identify_manager_send_to_context (operation->context, (GSourceFunc) do_identity_signal_added_work, work, (GDestroyNotify) identity_signal_work_free); } +static char * +get_principal_from_cache (GoaKerberosIdentityManager *self, + krb5_ccache cache) +{ + int error_code; + krb5_principal principal; + char *unparsed_name; + char *principal_name; + + error_code = krb5_cc_get_principal (self->kerberos_context, cache, &principal); + + error_code = krb5_unparse_name_flags (self->kerberos_context, principal, 0, &unparsed_name); + krb5_free_principal (self->kerberos_context, principal); + + if (error_code != 0) + return NULL; + + principal_name = g_strdup (unparsed_name); + + krb5_free_unparsed_name (self->kerberos_context, unparsed_name); + + return principal_name; +} + static void -refresh_identity (GoaKerberosIdentityManager *self, - Operation *operation, - GHashTable *refreshed_identities, - GoaIdentity *identity) +import_credentials_cache (GoaKerberosIdentityManager *self, + Operation *operation, + GHashTable *refreshed_identities, + krb5_ccache cache) { - const char *identifier; - GoaIdentity *old_identity; + g_autofree char *identifier = NULL; + GoaIdentity *identity = NULL; - identifier = goa_identity_get_identifier (identity); + identifier = get_principal_from_cache (self, cache); if (identifier == NULL) return; - old_identity = g_hash_table_lookup (self->identities, identifier); + identity = g_hash_table_lookup (self->identities, identifier); - if (old_identity != NULL) + if (identity == NULL) { - g_debug ("GoaKerberosIdentityManager: refreshing identity '%s'", identifier); - update_identity (self, operation, old_identity, identity); + g_autoptr(GError) error = NULL; - /* Reuse the old identity, so any object data set up on it doesn't - * disappear spurriously - */ - identifier = goa_identity_get_identifier (old_identity); - identity = old_identity; + g_debug ("GoaKerberosIdentityManager: Adding new identity '%s'", identifier); + identity = goa_kerberos_identity_new (self->kerberos_context, cache, &error); + + if (error != NULL) + { + g_debug ("GoaKerberosIdentityManager: Could not track identity %s: %s", + identifier, error->message); + return; + } + + add_identity (self, operation, identity, identifier); } else { - g_debug ("GoaKerberosIdentityManager: adding new identity '%s'", identifier); - add_identity (self, operation, identity, identifier); + if (!goa_kerberos_identity_has_credentials_cache (GOA_KERBEROS_IDENTITY (identity), cache)) + goa_kerberos_identity_add_credentials_cache (GOA_KERBEROS_IDENTITY (identity), cache); } - /* Track refreshed identities so we can emit removals when we're done fully + /* Track refreshed identities so we can emit refreshes and removals when we're done fully * enumerating the collection of credential caches */ g_hash_table_replace (refreshed_identities, g_strdup (identifier), g_object_ref (identity)); } static gboolean refresh_identities (GoaKerberosIdentityManager *self, Operation *operation) { krb5_error_code error_code; krb5_ccache cache; krb5_cccol_cursor cursor; const char *error_message; GHashTable *refreshed_identities; + GHashTableIter iter; + const char *name; + GoaIdentity *identity; /* If we have more refreshes queued up, don't bother doing this one */ if (!g_atomic_int_dec_and_test (&self->pending_refresh_count)) { return FALSE; } g_debug ("GoaKerberosIdentityManager: Refreshing identities"); refreshed_identities = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref); error_code = krb5_cccol_cursor_new (self->kerberos_context, &cursor); if (error_code != 0) { error_message = krb5_get_error_message (self->kerberos_context, error_code); g_debug ("GoaKerberosIdentityManager: Error looking up available credential caches: %s", error_message); krb5_free_error_message (self->kerberos_context, error_message); goto done; } error_code = krb5_cccol_cursor_next (self->kerberos_context, cursor, &cache); while (error_code == 0 && cache != NULL) { - GoaIdentity *identity; - - identity = goa_kerberos_identity_new (self->kerberos_context, cache, NULL); - - if (identity != NULL) - { - refresh_identity (self, operation, refreshed_identities, identity); - g_object_unref (identity); - } + import_credentials_cache (self, operation, refreshed_identities, cache); krb5_cc_close (self->kerberos_context, cache); error_code = krb5_cccol_cursor_next (self->kerberos_context, cursor, &cache); } if (error_code != 0) { error_message = krb5_get_error_message (self->kerberos_context, error_code); g_debug ("GoaKerberosIdentityManager: Error iterating over available credential caches: %s", error_message); krb5_free_error_message (self->kerberos_context, error_message); } krb5_cccol_cursor_free (self->kerberos_context, &cursor); + + g_hash_table_iter_init (&iter, self->identities); + while (g_hash_table_iter_next (&iter, (gpointer *) &name, (gpointer*) &identity)) + { + goa_kerberos_identity_refresh (GOA_KERBEROS_IDENTITY (identity)); + + if (goa_identity_is_signed_in (identity)) + { + IdentitySignalWork *work; + + /* if it's not expired, send out a refresh signal */ + g_debug ("GoaKerberosIdentityManager: identity '%s' refreshed", + goa_identity_get_identifier (identity)); + + work = identity_signal_work_new (self, identity); + goa_kerberos_identify_manager_send_to_context (operation->context, + (GSourceFunc) + do_identity_signal_refreshed_work, + work, + (GDestroyNotify) + identity_signal_work_free); + } + } + done: drop_stale_identities (self, operation, refreshed_identities); g_hash_table_unref (refreshed_identities); return TRUE; } static int identity_sort_func (GoaIdentity *a, GoaIdentity *b) { return g_strcmp0 (goa_identity_get_identifier (a), goa_identity_get_identifier (b)); } static void free_identity_list (GList *list) { g_list_free_full (list, g_object_unref); } static void list_identities (GoaKerberosIdentityManager *self, Operation *operation) { GList *identities; g_debug ("GoaKerberosIdentityManager: Listing identities"); identities = g_hash_table_get_values (self->identities); -- 2.39.3 From 7b78149d1f402412ea73b587c141e3f0ec8e87a3 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Thu, 9 Feb 2023 12:05:16 -0500 Subject: [PATCH 17/22] goakerberosidentity: Ensure credentials of expired identities at startup If the identity service is started later than goa-daemon then it won't currently notify goa-daemon about tickets that expired before it was running. This commit fixes the problem by manually calling EnsureCredentials on all signed out identities after their initial enumeration. --- src/goaidentity/goaidentityservice.c | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/goaidentity/goaidentityservice.c b/src/goaidentity/goaidentityservice.c index a25de416..7c2e389b 100644 --- a/src/goaidentity/goaidentityservice.c +++ b/src/goaidentity/goaidentityservice.c @@ -1442,82 +1442,88 @@ sign_in (GoaIdentityService *self, on_identity_inquiry, self, cancellable, (GAsyncReadyCallback) on_identity_signed_in, g_object_ref (operation_result)); g_object_unref (operation_result); } static void on_identity_expiring (GoaIdentityManager *identity_manager, GoaIdentity *identity, GoaIdentityService *self) { const char *principal; GoaObject *object; principal = goa_identity_get_identifier (identity); g_debug ("GoaIdentityService: identity %s expiring", principal); object = find_object_with_principal (self, principal, TRUE); if (object == NULL) return; ensure_account_credentials (self, object); g_clear_object (&object); } - static void -on_identity_expired (GoaIdentityManager *identity_manager, - GoaIdentity *identity, - GoaIdentityService *self) +handle_identity_expired (GoaIdentityService *self, + GoaIdentity *identity) { const char *principal; GoaObject *object; principal = goa_identity_get_identifier (identity); g_debug ("GoaIdentityService: identity %s expired", principal); object = find_object_with_principal (self, principal, TRUE); if (object == NULL) return; ensure_account_credentials (self, object); g_clear_object (&object); } +static void +on_identity_expired (GoaIdentityManager *identity_manager, + GoaIdentity *identity, + GoaIdentityService *self) +{ + handle_identity_expired (self, identity); +} + static void on_sign_out_for_account_change_done (GoaIdentityService *self, GAsyncResult *result) { GError *error = NULL; gboolean had_error; /* Workaround for bgo#764163 */ had_error = g_task_had_error (G_TASK (result)); g_task_propagate_boolean (G_TASK (result), &error); if (had_error) { g_debug ("Log out failed: %s", error->message); g_error_free (error); } else { g_debug ("Log out complete"); } } static void on_ticketing_done (GoaIdentityService *self, GAsyncResult *result) { GoaObject *object; object = g_task_get_task_data (G_TASK (result)); ensure_account_credentials (self, object); } @@ -1678,60 +1684,66 @@ on_identities_listed (GoaIdentityManager *manager, if (identities == NULL) { if (error != NULL) { g_warning ("Could not list identities: %s", error->message); g_error_free (error); } goto out; } for (node = identities; node != NULL; node = node->next) { GoaIdentity *identity = node->data; const char *principal; GoaObject *object; char *object_path; object_path = export_identity (self, identity); principal = goa_identity_get_identifier (identity); object = find_object_with_principal (self, principal, TRUE); if (object == NULL) add_temporary_account (self, identity); else g_object_unref (object); g_free (object_path); + + /* Treat identities that started out expired as if they just expired, in case + * the identity service is started long after goa-daemon + */ + if (!goa_identity_is_signed_in (identity)) + handle_identity_expired (self, identity); } out: g_object_unref (self); } static void ensure_credentials_for_accounts (GoaIdentityService *self) { GDBusObjectManager *object_manager; GList *accounts; GList *node; object_manager = goa_client_get_object_manager (self->client); g_signal_connect (object_manager, "interface-added", G_CALLBACK (on_account_interface_added), self); g_signal_connect (object_manager, "interface-removed", G_CALLBACK (on_account_interface_removed), self); accounts = goa_client_get_accounts (self->client); for (node = accounts; node != NULL; node = node->next) { GoaObject *object = GOA_OBJECT (node->data); GoaAccount *account; GoaTicketing *ticketing; const char *provider_type; account = goa_object_peek_account (object); if (account == NULL) -- 2.39.3 From 4003fdbed262f2da2e0affb39bda68c7ae1ccf18 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 21 Feb 2023 12:10:46 -0500 Subject: [PATCH 18/22] goakerberosidentity: Don't send "expired" and "unexpired" signals from two parts of the code Since commit e869642bd079aec2098542a3c8f1b296694499ab verify_identity handles sending "expired" and "unexpired" signals on its own. Unfortunately that commit neglected to make goa_kerberos_identity_refresh stop sending the signals. This commit removes the duplicated logic. --- src/goaidentity/goakerberosidentity.c | 42 ++++----------------------- 1 file changed, 6 insertions(+), 36 deletions(-) diff --git a/src/goaidentity/goakerberosidentity.c b/src/goaidentity/goakerberosidentity.c index b5cbcecd..20add727 100644 --- a/src/goaidentity/goakerberosidentity.c +++ b/src/goaidentity/goakerberosidentity.c @@ -1753,112 +1753,82 @@ goa_kerberos_identity_sign_in (GoaKerberosIdentity *self, destroy_notify (inquiry_data); sign_in_operation_free (operation); if (!goa_kerberos_identity_update_credentials (self, principal, &new_credentials, error)) { krb5_free_principal (self->kerberos_context, principal); goto done; } krb5_free_principal (self->kerberos_context, principal); done: goa_kerberos_identity_refresh (self); if (self->cached_verification_level != VERIFICATION_LEVEL_SIGNED_IN) { g_debug ("GoaKerberosIdentity: Identity '%s' could not be signed in", principal_name); return FALSE; } g_debug ("GoaKerberosIdentity: Identity '%s' signed in", principal_name); return TRUE; } void goa_kerberos_identity_refresh (GoaKerberosIdentity *self) { - VerificationLevel old_verification_level, new_verification_level; g_autofree char *preauth_identity_source = NULL; g_autoptr (GError) error = NULL; g_debug ("GoaKerberosIdentity: Refreshing identity %s (active credentials cache: %s)", self->identifier, self->active_credentials_cache_name); - G_LOCK (identity_lock); - old_verification_level = self->cached_verification_level; - G_UNLOCK (identity_lock); + verify_identity (self, &preauth_identity_source, &error); - new_verification_level = verify_identity (self, &preauth_identity_source, &error); + if (error != NULL) + { + g_debug ("GoaKerberosIdentity: Could not verify identity %s: %s", self->identifier, error->message); + return; + } G_LOCK (identity_lock); if (g_strcmp0 (self->preauth_identity_source, preauth_identity_source) != 0) { g_free (self->preauth_identity_source); self->preauth_identity_source = g_steal_pointer (&preauth_identity_source); } G_UNLOCK (identity_lock); - - if (new_verification_level != old_verification_level) - { - if ((old_verification_level == VERIFICATION_LEVEL_SIGNED_IN) && - new_verification_level == VERIFICATION_LEVEL_EXISTS) - { - G_LOCK (identity_lock); - self->cached_verification_level = new_verification_level; - G_UNLOCK (identity_lock); - - g_signal_emit (G_OBJECT (self), signals[EXPIRED], 0); - } - else if (old_verification_level == VERIFICATION_LEVEL_EXISTS && - new_verification_level == VERIFICATION_LEVEL_SIGNED_IN) - { - G_LOCK (identity_lock); - self->cached_verification_level = new_verification_level; - G_UNLOCK (identity_lock); - - g_signal_emit (G_OBJECT (self), signals[UNEXPIRED], 0); - } - else - { - G_LOCK (identity_lock); - self->cached_verification_level = new_verification_level; - G_UNLOCK (identity_lock); - } - G_LOCK (identity_lock); - queue_notify (self, &self->is_signed_in_idle_id, "is-signed-in"); - G_UNLOCK (identity_lock); - } } gboolean goa_kerberos_identity_renew (GoaKerberosIdentity *self, GError **error) { krb5_error_code error_code = 0; krb5_principal principal; krb5_creds new_credentials; krb5_ccache credentials_cache; gboolean renewed = FALSE; char *name = NULL; if (self->active_credentials_cache_name == NULL) { g_set_error (error, GOA_IDENTITY_ERROR, GOA_IDENTITY_ERROR_CREDENTIALS_UNAVAILABLE, _("Not signed in")); goto out; } credentials_cache = (krb5_ccache) g_hash_table_lookup (self->credentials_caches, self->active_credentials_cache_name); error_code = krb5_cc_get_principal (self->kerberos_context, credentials_cache, &principal); if (error_code != 0) { set_and_prefix_error_from_krb5_error_code (self, error, GOA_IDENTITY_ERROR_CREDENTIALS_UNAVAILABLE, error_code, -- 2.39.3 From 967384abe9044cfeacaf99f5df2c2985d3b2e357 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Mon, 20 Feb 2023 14:30:24 -0500 Subject: [PATCH 19/22] goakerberosidentity: Ensure properties are updated in a timely fashion At the moment property notifications of identity objects are deferred to a lower priority idle handler. This is suboptimal because it means there can be a bit of a delay updating the status of, e.g., IsSignedIn, over the bus. This commit changes the notification to queue at normal priority. --- src/goaidentity/goakerberosidentity.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/goaidentity/goakerberosidentity.c b/src/goaidentity/goakerberosidentity.c index 20add727..de646d5f 100644 --- a/src/goaidentity/goakerberosidentity.c +++ b/src/goaidentity/goakerberosidentity.c @@ -549,61 +549,61 @@ clear_idle_id (NotifyRequest *request) g_object_unref (request->self); g_slice_free (NotifyRequest, request); } static gboolean on_notify_queued (NotifyRequest *request) { g_object_notify (G_OBJECT (request->self), request->property_name); return FALSE; } static void queue_notify (GoaKerberosIdentity *self, guint *idle_id, const char *property_name) { NotifyRequest *request; if (*idle_id != 0) { return; } request = g_slice_new0 (NotifyRequest); request->self = g_object_ref (self); request->idle_id = idle_id; request->property_name = property_name; - *idle_id = g_idle_add_full (G_PRIORITY_DEFAULT_IDLE, + *idle_id = g_idle_add_full (G_PRIORITY_DEFAULT, (GSourceFunc) on_notify_queued, request, (GDestroyNotify) clear_idle_id); } static gboolean set_start_time (GoaKerberosIdentity *self, krb5_timestamp start_time) { if (self->start_time != start_time) { self->start_time = start_time; queue_notify (self, &self->start_time_idle_id, "start-timestamp"); return TRUE; } return FALSE; } static gboolean set_renewal_time (GoaKerberosIdentity *self, krb5_timestamp renewal_time) { if (self->renewal_time != renewal_time) { self->renewal_time = renewal_time; queue_notify (self, &self->renewal_time_idle_id, "renewal-timestamp"); return TRUE; } -- 2.39.3 From 345403d4842005b6666fb059a32da8701964f95d Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Mon, 20 Feb 2023 14:32:52 -0500 Subject: [PATCH 20/22] goakerberosidentity: Queue is-signed-in notify before emitting expired signal Right now the "expired" signal is emitted before the "is-signed-in" property notification is queued. This notification is used to update the "IsSignedIn" property of the object on the bus. The "expired" signal emission leads to a corresponding "identity-expired" signal on the manager object. A handler for that signal calls into gnome-online-accounts to check identity status. In response, gnome-online-accounts will check the "IsSignedIn" property. This commit makes sure the "is-signed-in" notification is queued first, before the "expired" signal is emitted. Of course queuing the notify before "expired" is emitted isn't enough to ensure the notify happens in time. A future commit will make sure the "identity-expired" signal itself is deferred to the main thread, which should resolve that problem. --- src/goaidentity/goakerberosidentity.c | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/goaidentity/goakerberosidentity.c b/src/goaidentity/goakerberosidentity.c index de646d5f..0eb0a9ca 100644 --- a/src/goaidentity/goakerberosidentity.c +++ b/src/goaidentity/goakerberosidentity.c @@ -1028,85 +1028,75 @@ out: time_changed |= set_renewal_time (self, best_renewal_time); time_changed |= set_expiration_time (self, best_expiration_time); G_UNLOCK (identity_lock); if (time_changed) { if (best_verification_level == VERIFICATION_LEVEL_SIGNED_IN) { g_debug ("GoaKerberosIdentity: identity %s credentials have updated times, resetting alarms", self->identifier); reset_alarms (self); } else { g_debug ("GoaKerberosIdentity: identity %s credentials are now expired, clearing alarms", self->identifier); clear_alarms (self); } } else { g_debug ("GoaKerberosIdentity: identity %s credentials do not have updated times, so not adjusting alarms", self->identifier); } } else { g_debug ("GoaKerberosIdentity: identity is unverified, clearing alarms"); clear_alarms (self); } if (best_verification_level != old_verification_level) { + G_LOCK (identity_lock); + self->cached_verification_level = best_verification_level; + queue_notify (self, &self->is_signed_in_idle_id, "is-signed-in"); + G_UNLOCK (identity_lock); + if (old_verification_level == VERIFICATION_LEVEL_SIGNED_IN && best_verification_level == VERIFICATION_LEVEL_EXISTS) { - G_LOCK (identity_lock); - self->cached_verification_level = best_verification_level; - G_UNLOCK (identity_lock); - g_signal_emit (G_OBJECT (self), signals[EXPIRED], 0); } else if (old_verification_level == VERIFICATION_LEVEL_EXISTS && best_verification_level == VERIFICATION_LEVEL_SIGNED_IN) { - G_LOCK (identity_lock); - self->cached_verification_level = best_verification_level; - G_UNLOCK (identity_lock); - g_signal_emit (G_OBJECT (self), signals[UNEXPIRED], 0); } - else - { - G_LOCK (identity_lock); - self->cached_verification_level = best_verification_level; - G_UNLOCK (identity_lock); - } - queue_notify (self, &self->is_signed_in_idle_id, "is-signed-in"); } default_principal = get_default_principal (self); is_default_principal = g_strcmp0 (default_principal, self->identifier) == 0; default_credentials_cache_name = get_default_cache_name (self); is_default_credentials_cache = g_strcmp0 (default_credentials_cache_name, self->active_credentials_cache_name) == 0; if (self->active_credentials_cache_name == NULL) { g_debug ("GoaKerberosIdentity: Not switching default credentials cache because identity %s has no active credentials cache to switch to", self->identifier); should_switch_default_credentials_cache = FALSE; } else if (self->identifier == NULL) { g_debug ("GoaKerberosIdentity: Not switching default credentials cache to '%s' because it is not yet initialized", self->active_credentials_cache_name); should_switch_default_credentials_cache = FALSE; } else if (default_principal == NULL) { g_debug ("GoaKerberosIdentity: Switching default credentials cache to '%s' (identity %s) because there is currently no default", self->active_credentials_cache_name, self->identifier); should_switch_default_credentials_cache = TRUE; } else if (!is_default_principal) { g_debug ("GoaKerberosIdentity: Not switching default credentials cache because identity %s is not the default identity", self->identifier); should_switch_default_credentials_cache = FALSE; } else if (!is_default_credentials_cache) { -- 2.39.3 From 3a3a9fbbb84adc7c8797027a2c8c9b3fada03e0b Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Mon, 20 Feb 2023 14:57:57 -0500 Subject: [PATCH 21/22] goakerberosidentitymanager: Ensure identity-expired signal is emitted from main loop thread Right now most of the the identity manager signals get emitted from the main thread. This makes sense, and is what a caller would typically expect (given they connect their callbacks in the main thread as well). The one exception is "identity-expired" which gets emitted from the worker thread. This commit fixes that. --- src/goaidentity/goakerberosidentitymanager.c | 21 ++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/goaidentity/goakerberosidentitymanager.c b/src/goaidentity/goakerberosidentitymanager.c index 7785b891..9a89917e 100644 --- a/src/goaidentity/goakerberosidentitymanager.c +++ b/src/goaidentity/goakerberosidentitymanager.c @@ -254,66 +254,83 @@ static void schedule_refresh (GoaKerberosIdentityManager *self) { Operation *operation; g_atomic_int_inc (&self->pending_refresh_count); operation = operation_new (self, NULL, OPERATION_TYPE_REFRESH, NULL); g_thread_pool_push (self->thread_pool, operation, NULL); } static IdentitySignalWork * identity_signal_work_new (GoaKerberosIdentityManager *self, GoaIdentity *identity) { IdentitySignalWork *work; work = g_slice_new (IdentitySignalWork); work->manager = self; work->identity = g_object_ref (identity); return work; } static void identity_signal_work_free (IdentitySignalWork *work) { g_object_unref (work->identity); g_slice_free (IdentitySignalWork, work); } +static void +do_identity_signal_expired_work (IdentitySignalWork *work) +{ + GoaKerberosIdentityManager *self = work->manager; + GoaIdentity *identity = work->identity; + + g_debug ("GoaKerberosIdentityManager: identity expired"); + _goa_identity_manager_emit_identity_expired (GOA_IDENTITY_MANAGER (self), identity); +} + static void on_identity_expired (GoaIdentity *identity, GoaKerberosIdentityManager *self) { - _goa_identity_manager_emit_identity_expired (GOA_IDENTITY_MANAGER (self), - identity); + IdentitySignalWork *work; + + work = identity_signal_work_new (self, identity); + goa_kerberos_identify_manager_send_to_context (g_main_context_default (), + (GSourceFunc) + do_identity_signal_expired_work, + work, + (GDestroyNotify) + identity_signal_work_free); } static void on_identity_unexpired (GoaIdentity *identity, GoaKerberosIdentityManager *self) { g_debug ("GoaKerberosIdentityManager: identity unexpired"); /* If an identity is now unexpired, that means some sort of weird * clock skew happened and we should just do a full refresh, since it's * probably affected more than one identity */ schedule_refresh (self); } static void on_identity_expiring (GoaIdentity *identity, GoaKerberosIdentityManager *self) { g_debug ("GoaKerberosIdentityManager: identity about to expire"); _goa_identity_manager_emit_identity_expiring (GOA_IDENTITY_MANAGER (self), identity); } static void on_identity_needs_renewal (GoaIdentity *identity, GoaKerberosIdentityManager *self) { g_debug ("GoaKerberosIdentityManager: identity needs renewal"); _goa_identity_manager_emit_identity_needs_renewal (GOA_IDENTITY_MANAGER (self), identity); -- 2.39.3 From 792600480e19f9e973fd7261fa5973be836f8470 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 21 Feb 2023 12:07:04 -0500 Subject: [PATCH 22/22] goakerberosidentity: Ensure old_verification_level is initialized verify_identity has a short-circuit at the top of the file if the active credentials cache is already known. In that case it bypasses the search for a credentials cache. Unfortunately it also inadvertently bypasses initialization of old_verification_level, leading to the code thinking the identity is always going unexpired. This commit fixes that. --- src/goaidentity/goakerberosidentity.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/goaidentity/goakerberosidentity.c b/src/goaidentity/goakerberosidentity.c index 0eb0a9ca..e4f09e14 100644 --- a/src/goaidentity/goakerberosidentity.c +++ b/src/goaidentity/goakerberosidentity.c @@ -859,101 +859,101 @@ get_default_cache_name (GoaKerberosIdentity *self) krb5_free_unparsed_name (self->kerberos_context, principal_name); default_cache_name = g_strdup (krb5_cc_get_name (self->kerberos_context, default_cache)); krb5_cc_close (self->kerberos_context, default_cache); return default_cache_name; } static VerificationLevel verify_identity (GoaKerberosIdentity *self, char **preauth_identity_source, GError **error) { krb5_ccache credentials_cache; g_autofree char *default_principal = NULL; g_autofree char *default_credentials_cache_name = NULL; gboolean is_default_principal; gboolean is_default_credentials_cache; gboolean should_switch_default_credentials_cache = FALSE; gboolean time_changed = FALSE; const char *name; krb5_timestamp best_start_time = 0; krb5_timestamp best_renewal_time = 0; krb5_timestamp best_expiration_time = 0; g_autofree char *best_preauth_identity_source = NULL; g_autofree char *best_credentials_cache_name = NULL; VerificationLevel old_verification_level = VERIFICATION_LEVEL_UNVERIFIED; VerificationLevel best_verification_level = VERIFICATION_LEVEL_UNVERIFIED; GHashTableIter iter; + G_LOCK (identity_lock); + old_verification_level = self->cached_verification_level; + G_UNLOCK (identity_lock); + if (self->active_credentials_cache_name != NULL) { G_LOCK (identity_lock); credentials_cache = (krb5_ccache) g_hash_table_lookup (self->credentials_caches, self->active_credentials_cache_name); G_UNLOCK (identity_lock); best_verification_level = verify_identity_in_credentials_cache (self, &best_preauth_identity_source, credentials_cache, &best_start_time, &best_renewal_time, &best_expiration_time, error); G_LOCK (identity_lock); best_credentials_cache_name = g_strdup (self->active_credentials_cache_name); G_UNLOCK (identity_lock); if (best_verification_level == VERIFICATION_LEVEL_SIGNED_IN) goto out; if (best_verification_level == VERIFICATION_LEVEL_UNVERIFIED || best_verification_level == VERIFICATION_LEVEL_ERROR) { g_clear_pointer (&best_credentials_cache_name, g_free); G_LOCK (identity_lock); if (self->identifier != NULL) { krb5_cc_close (self->kerberos_context, credentials_cache); g_hash_table_remove (self->credentials_caches, self->active_credentials_cache_name); g_clear_pointer (&self->active_credentials_cache_name, g_free); } G_UNLOCK (identity_lock); } } - G_LOCK (identity_lock); - old_verification_level = self->cached_verification_level; - G_UNLOCK (identity_lock); - G_LOCK (identity_lock); g_hash_table_iter_init (&iter, self->credentials_caches); while (g_hash_table_iter_next (&iter, (gpointer *) &name, (gpointer*) &credentials_cache)) { krb5_timestamp new_start_time = 0; krb5_timestamp new_renewal_time = 0; krb5_timestamp new_expiration_time = 0; g_autofree char *new_preauth_identity_source = NULL; VerificationLevel verification_level = VERIFICATION_LEVEL_UNVERIFIED; gboolean has_better_credentials = FALSE; if (g_strcmp0 (name, self->active_credentials_cache_name) == 0) continue; G_UNLOCK (identity_lock); if (preauth_identity_source != NULL) g_clear_pointer (preauth_identity_source, g_free); verification_level = verify_identity_in_credentials_cache (self, &new_preauth_identity_source, credentials_cache, &new_start_time, &new_renewal_time, &new_expiration_time, error); if (verification_level == VERIFICATION_LEVEL_UNVERIFIED || verification_level == VERIFICATION_LEVEL_ERROR) { -- 2.39.3