From 8888746f18ad5662a07a48e7e8bf0a4b2d75273d Mon Sep 17 00:00:00 2001 From: Debarshi Ray Date: Wed, 12 Oct 2022 22:59:34 +0200 Subject: [PATCH 01/16] 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 caed5ae2..4fcb132c 100644 --- a/src/goaidentity/goakerberosidentitymanager.c +++ b/src/goaidentity/goakerberosidentitymanager.c @@ -807,61 +807,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.0 From a25eeeabda86b281665941842d4904b47112f85e Mon Sep 17 00:00:00 2001 From: Debarshi Ray Date: Thu, 13 Oct 2022 18:46:46 +0200 Subject: [PATCH 02/16] 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.0 From 0cd016658de5f30ddf9a8fb749899067d57a3570 Mon Sep 17 00:00:00 2001 From: Debarshi Ray Date: Thu, 13 Oct 2022 19:02:59 +0200 Subject: [PATCH 03/16] 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.0 From 2e777d41cc6545da48f9e10083f1468887e2f880 Mon Sep 17 00:00:00 2001 From: Debarshi Ray Date: Thu, 13 Oct 2022 22:14:07 +0200 Subject: [PATCH 04/16] 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.0 From 9f66a5c92a0183a94c28fe24fad615631bc85b1b Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Thu, 27 Oct 2022 09:18:53 -0400 Subject: [PATCH 05/16] 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.0 From 6c2b4c40af149f92e446c06af8cb1091370c0504 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Thu, 13 Oct 2022 16:11:54 -0400 Subject: [PATCH 06/16] 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.0 From aca400799c225a84e5d0fc90efb206c8f1d48bc3 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Mon, 28 Nov 2022 14:16:09 -0500 Subject: [PATCH 07/16] 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.0 From 5f61f503de118c716809a18484e60a6d5a55b6e3 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Mon, 28 Nov 2022 15:58:09 -0500 Subject: [PATCH 08/16] 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.0 From e3821a2746b3a1f01dedece09f33f8b4d7573713 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 29 Nov 2022 12:21:09 -0500 Subject: [PATCH 09/16] 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.0 From 9eaf73c81528aae5c89ac9b3366f48d30bca4794 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Wed, 30 Nov 2022 13:53:41 -0500 Subject: [PATCH 10/16] 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.0 From 54f40b8c5696cd15e371f428eccc9090c37ebe65 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Thu, 15 Dec 2022 14:46:01 -0500 Subject: [PATCH 11/16] 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.0 From 09856fdebf7c6dc70a6d07fd76197eb42fdb9262 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Thu, 15 Dec 2022 14:46:01 -0500 Subject: [PATCH 12/16] 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.0 From 2f80c78f7b607fe45ac0c9cd5a9aa4dabecc9538 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Thu, 15 Dec 2022 16:27:27 -0500 Subject: [PATCH 13/16] 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.0 From 592aecd7bf439a7b5b276354d5262a1ce807d8e1 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Thu, 15 Dec 2022 15:35:49 -0500 Subject: [PATCH 14/16] 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.0 From 5cedc418bb4eabd94085660f224ebe78532d3ed4 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Mon, 16 Jan 2023 15:00:36 -0500 Subject: [PATCH 15/16] 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.0 From 8910cc096115f51bc1b13839d3909557276e7145 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Thu, 19 Jan 2023 11:31:14 -0500 Subject: [PATCH 16/16] 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 4fcb132c..f2d01571 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.0