/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
/*
* Copyright © 2012 – 2017 Red Hat, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General
* Public License along with this library; if not, see <http://www.gnu.org/licenses/>.
*/
#include "config.h"
#include "goakerberosidentitymanager.h"
#include "goaidentitymanager.h"
#include "goaidentitymanagererror.h"
#include "goaidentitymanagerprivate.h"
#include "goakerberosidentityinquiry.h"
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <glib/gi18n.h>
#include <glib/gstdio.h>
#include <gio/gio.h>
#include <krb5.h>
struct _GoaKerberosIdentityManagerPrivate
{
GHashTable *identities;
GHashTable *expired_identities;
GHashTable *identities_by_realm;
GAsyncQueue *pending_operations;
GCancellable *scheduler_cancellable;
krb5_context kerberos_context;
GFileMonitor *credentials_cache_monitor;
gulong credentials_cache_changed_signal_id;
char *credentials_cache_type;
GMutex scheduler_job_lock;
GCond scheduler_job_unblocked;
gboolean is_blocking_scheduler_job;
volatile int pending_refresh_count;
guint polling_timeout_id;
};
typedef enum
{
OPERATION_TYPE_REFRESH,
OPERATION_TYPE_GET_IDENTITY,
OPERATION_TYPE_LIST,
OPERATION_TYPE_RENEW,
OPERATION_TYPE_SIGN_IN,
OPERATION_TYPE_SIGN_OUT,
OPERATION_TYPE_STOP_JOB
} OperationType;
typedef struct
{
GCancellable *cancellable;
GoaKerberosIdentityManager *manager;
OperationType type;
GSimpleAsyncResult *result;
GIOSchedulerJob *job;
union
{
GoaIdentity *identity;
struct
{
const char *identifier;
gconstpointer initial_password;
char *preauth_source;
GoaIdentitySignInFlags sign_in_flags;
GoaIdentityInquiry *inquiry;
GoaIdentityInquiryFunc inquiry_func;
gpointer inquiry_data;
GMutex inquiry_lock;
GCond inquiry_finished_condition;
volatile gboolean is_inquiring;
};
};
} Operation;
typedef struct
{
GoaKerberosIdentityManager *manager;
GoaIdentity *identity;
} IdentitySignalWork;
static GoaIdentityManager *goa_kerberos_identity_manager_singleton;
static void identity_manager_interface_init (GoaIdentityManagerInterface *
interface);
static void initable_interface_init (GInitableIface *interface);
static void on_identity_expired (GoaIdentity *identity,
GoaKerberosIdentityManager *self);
G_DEFINE_TYPE_WITH_CODE (GoaKerberosIdentityManager,
goa_kerberos_identity_manager,
G_TYPE_OBJECT,
G_IMPLEMENT_INTERFACE (GOA_TYPE_IDENTITY_MANAGER,
identity_manager_interface_init)
G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE,
initable_interface_init));
#define FALLBACK_POLLING_INTERVAL 5
static Operation *
operation_new (GoaKerberosIdentityManager *self,
GCancellable *cancellable,
OperationType type,
GSimpleAsyncResult *result)
{
Operation *operation;
operation = g_slice_new0 (Operation);
operation->manager = self;
operation->type = type;
if (cancellable == NULL)
cancellable = g_cancellable_new ();
else
g_object_ref (cancellable);
operation->cancellable = cancellable;
if (result != NULL)
g_object_ref (result);
operation->result = result;
operation->identity = NULL;
return operation;
}
static void
operation_free (Operation *operation)
{
g_clear_object (&operation->cancellable);
if (operation->type != OPERATION_TYPE_SIGN_IN &&
operation->type != OPERATION_TYPE_GET_IDENTITY)
{
g_clear_object (&operation->identity);
}
else
{
g_clear_pointer (&operation->identifier, g_free);
g_clear_pointer (&operation->preauth_source, g_free);
}
g_clear_object (&operation->result);
g_slice_free (Operation, operation);
}
static void
schedule_refresh (GoaKerberosIdentityManager *self)
{
Operation *operation;
g_atomic_int_inc (&self->priv->pending_refresh_count);
operation = operation_new (self, NULL, OPERATION_TYPE_REFRESH, NULL);
g_async_queue_push (self->priv->pending_operations, operation);
}
static IdentitySignalWork *
identity_signal_work_new (GoaKerberosIdentityManager *self,
GoaIdentity *identity)
{
IdentitySignalWork *work;
work = g_slice_new (IdentitySignalWork);
work->manager = self;
work->identity = g_object_ref (identity);
return work;
}
static void
identity_signal_work_free (IdentitySignalWork *work)
{
g_object_unref (work->identity);
g_slice_free (IdentitySignalWork, work);
}
static void
on_identity_expired (GoaIdentity *identity,
GoaKerberosIdentityManager *self)
{
_goa_identity_manager_emit_identity_expired (GOA_IDENTITY_MANAGER (self),
identity);
}
static void
on_identity_unexpired (GoaIdentity *identity,
GoaKerberosIdentityManager *self)
{
g_debug ("GoaKerberosIdentityManager: identity unexpired");
/* If an identity is now unexpired, that means some sort of weird
* clock skew happened and we should just do a full refresh, since it's
* probably affected more than one identity
*/
schedule_refresh (self);
}
static void
on_identity_expiring (GoaIdentity *identity,
GoaKerberosIdentityManager *self)
{
g_debug ("GoaKerberosIdentityManager: identity about to expire");
_goa_identity_manager_emit_identity_expiring (GOA_IDENTITY_MANAGER (self),
identity);
}
static void
on_identity_needs_renewal (GoaIdentity *identity,
GoaKerberosIdentityManager *self)
{
g_debug ("GoaKerberosIdentityManager: identity needs renewal");
_goa_identity_manager_emit_identity_needs_renewal (GOA_IDENTITY_MANAGER (self),
identity);
}
static void
on_identity_needs_refresh (GoaIdentity *identity,
GoaKerberosIdentityManager *self)
{
g_debug ("GoaKerberosIdentityManager: needs refresh");
schedule_refresh (self);
}
static void
watch_for_identity_expiration (GoaKerberosIdentityManager *self,
GoaIdentity *identity)
{
g_signal_handlers_disconnect_by_func (G_OBJECT (identity),
G_CALLBACK (on_identity_expired),
self);
g_signal_connect (G_OBJECT (identity),
"expired",
G_CALLBACK (on_identity_expired),
self);
g_signal_handlers_disconnect_by_func (G_OBJECT (identity),
G_CALLBACK (on_identity_unexpired),
self);
g_signal_connect (G_OBJECT (identity),
"unexpired",
G_CALLBACK (on_identity_unexpired),
self);
g_signal_handlers_disconnect_by_func (G_OBJECT (identity),
G_CALLBACK (on_identity_expiring),
self);
g_signal_connect (G_OBJECT (identity),
"expiring",
G_CALLBACK (on_identity_expiring),
self);
g_signal_handlers_disconnect_by_func (G_OBJECT (identity),
G_CALLBACK (on_identity_needs_renewal),
self);
g_signal_connect (G_OBJECT (identity),
"needs-renewal",
G_CALLBACK (on_identity_needs_renewal),
self);
g_signal_handlers_disconnect_by_func (G_OBJECT (identity),
G_CALLBACK (on_identity_needs_refresh),
self);
g_signal_connect (G_OBJECT (identity),
"needs-refresh",
G_CALLBACK (on_identity_needs_refresh),
self);
}
static void
do_identity_signal_added_work (IdentitySignalWork *work)
{
GoaKerberosIdentityManager *self = work->manager;
GoaIdentity *identity = work->identity;
watch_for_identity_expiration (self, identity);
_goa_identity_manager_emit_identity_added (GOA_IDENTITY_MANAGER (self), identity);
}
static void
do_identity_signal_removed_work (IdentitySignalWork *work)
{
GoaKerberosIdentityManager *self = work->manager;
GoaIdentity *identity = work->identity;
_goa_identity_manager_emit_identity_removed (GOA_IDENTITY_MANAGER (self),
identity);
}
static void
do_identity_signal_renamed_work (IdentitySignalWork *work)
{
GoaKerberosIdentityManager *self = work->manager;
GoaIdentity *identity = work->identity;
_goa_identity_manager_emit_identity_renamed (GOA_IDENTITY_MANAGER (self),
identity);
}
static void
do_identity_signal_refreshed_work (IdentitySignalWork *work)
{
GoaKerberosIdentityManager *self = work->manager;
GoaIdentity *identity = work->identity;
watch_for_identity_expiration (self, identity);
_goa_identity_manager_emit_identity_refreshed (GOA_IDENTITY_MANAGER (self),
identity);
}
static void
remove_identity (GoaKerberosIdentityManager *self,
Operation *operation,
GoaIdentity *identity)
{
IdentitySignalWork *work;
const char *identifier;
char *name;
GList *other_identities = NULL;
identifier = goa_identity_get_identifier (identity);
name = goa_kerberos_identity_get_realm_name (GOA_KERBEROS_IDENTITY (identity));
if (name != NULL)
{
other_identities = g_hash_table_lookup (self->priv->identities_by_realm, name);
g_hash_table_remove (self->priv->identities_by_realm, name);
other_identities = g_list_remove (other_identities, identity);
}
if (other_identities != NULL)
{
g_hash_table_replace (self->priv->identities_by_realm,
g_strdup (name), other_identities);
}
g_free (name);
work = identity_signal_work_new (self, identity);
g_hash_table_remove (self->priv->expired_identities, identifier);
g_hash_table_remove (self->priv->identities, identifier);
g_io_scheduler_job_send_to_mainloop (operation->job,
(GSourceFunc)
do_identity_signal_removed_work,
work,
(GDestroyNotify) identity_signal_work_free);
/* If there's only one identity for this realm now, then we can
* rename that identity to just the realm name
*/
if (other_identities != NULL && other_identities->next == NULL)
{
GoaIdentity *other_identity = other_identities->data;
work = identity_signal_work_new (self, other_identity);
g_io_scheduler_job_send_to_mainloop (operation->job,
(GSourceFunc)
do_identity_signal_renamed_work,
work,
(GDestroyNotify)
identity_signal_work_free);
}
}
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->priv->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->priv->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);
g_io_scheduler_job_send_to_mainloop (operation->job,
(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->priv->identities,
g_strdup (identifier), g_object_ref (identity));
if (!goa_identity_is_signed_in (identity))
{
g_hash_table_replace (self->priv->expired_identities,
g_strdup (identifier), identity);
}
work = identity_signal_work_new (self, identity);
g_io_scheduler_job_send_to_mainloop (operation->job,
(GSourceFunc)
do_identity_signal_added_work,
work,
(GDestroyNotify) identity_signal_work_free);
}
static void
refresh_identity (GoaKerberosIdentityManager *self,
Operation *operation,
GHashTable *refreshed_identities,
GoaIdentity *identity)
{
const char *identifier;
GoaIdentity *old_identity;
identifier = goa_identity_get_identifier (identity);
if (identifier == NULL)
return;
old_identity = g_hash_table_lookup (self->priv->identities, identifier);
if (old_identity != NULL)
{
g_debug ("GoaKerberosIdentityManager: refreshing identity '%s'", identifier);
update_identity (self, operation, old_identity, identity);
/* 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;
}
else
{
g_debug ("GoaKerberosIdentityManager: adding new identity '%s'", identifier);
add_identity (self, operation, identity, identifier);
}
/* Track refreshed identities so we can emit 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;
/* If we have more refreshes queued up, don't bother doing this one
*/
if (!g_atomic_int_dec_and_test (&self->priv->pending_refresh_count))
{
return FALSE;
}
g_debug ("GoaKerberosIdentityManager: Refreshing identities");
refreshed_identities = g_hash_table_new_full (g_str_hash,
g_str_equal,
(GDestroyNotify)
g_free,
(GDestroyNotify) g_object_unref);
error_code = krb5_cccol_cursor_new (self->priv->kerberos_context, &cursor);
if (error_code != 0)
{
error_message =
krb5_get_error_message (self->priv->kerberos_context, error_code);
g_debug ("GoaKerberosIdentityManager: Error looking up available credential caches: %s",
error_message);
krb5_free_error_message (self->priv->kerberos_context, error_message);
goto done;
}
error_code = krb5_cccol_cursor_next (self->priv->kerberos_context, cursor, &cache);
while (error_code == 0 && cache != NULL)
{
GoaIdentity *identity;
identity = goa_kerberos_identity_new (self->priv->kerberos_context,
cache, NULL);
if (identity != NULL)
{
refresh_identity (self, operation, refreshed_identities, identity);
g_object_unref (identity);
}
krb5_cc_close (self->priv->kerberos_context, cache);
error_code = krb5_cccol_cursor_next (self->priv->kerberos_context,
cursor, &cache);
}
if (error_code != 0)
{
error_message =
krb5_get_error_message (self->priv->kerberos_context, error_code);
g_debug ("GoaKerberosIdentityManager: Error iterating over available credential caches: %s",
error_message);
krb5_free_error_message (self->priv->kerberos_context, error_message);
}
krb5_cccol_cursor_free (self->priv->kerberos_context, &cursor);
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->priv->identities);
identities = g_list_sort (identities, (GCompareFunc) identity_sort_func);
g_list_foreach (identities, (GFunc) g_object_ref, NULL);
g_simple_async_result_set_op_res_gpointer (operation->result,
identities,
(GDestroyNotify) free_identity_list);
}
static void
renew_identity (GoaKerberosIdentityManager *self,
Operation *operation)
{
GError *error;
gboolean was_renewed;
char *identity_name;
identity_name =
goa_kerberos_identity_get_principal_name (GOA_KERBEROS_IDENTITY
(operation->identity));
g_debug ("GoaKerberosIdentityManager: renewing identity %s", identity_name);
g_free (identity_name);
error = NULL;
was_renewed =
goa_kerberos_identity_renew (GOA_KERBEROS_IDENTITY (operation->identity),
&error);
if (!was_renewed)
{
g_debug ("GoaKerberosIdentityManager: could not renew identity: %s",
error->message);
g_simple_async_result_set_from_error (operation->result, error);
}
g_simple_async_result_set_op_res_gboolean (operation->result, was_renewed);
}
static void
do_identity_inquiry (Operation *operation)
{
if (operation->inquiry_func == NULL)
{
return;
}
operation->inquiry_func (operation->inquiry,
operation->cancellable,
operation->inquiry_data);
}
static void
stop_waiting_on_inquiry (Operation *operation)
{
g_mutex_lock (&operation->inquiry_lock);
if (operation->is_inquiring)
{
operation->is_inquiring = FALSE;
g_cond_signal (&operation->inquiry_finished_condition);
}
g_mutex_unlock (&operation->inquiry_lock);
}
static void
on_kerberos_identity_inquiry_complete (GoaIdentityInquiry *inquiry,
Operation *operation)
{
stop_waiting_on_inquiry (operation);
}
static void
start_inquiry (Operation *operation,
GoaIdentityInquiry *inquiry)
{
operation->is_inquiring = TRUE;
g_signal_connect (G_OBJECT (inquiry),
"complete",
G_CALLBACK (on_kerberos_identity_inquiry_complete),
operation);
operation->inquiry = inquiry;
g_io_scheduler_job_send_to_mainloop (operation->job,
(GSourceFunc)
do_identity_inquiry,
operation, (GDestroyNotify) NULL);
}
static void
wait_for_inquiry_to_complete (Operation *operation,
GoaKerberosIdentityInquiry *inquiry)
{
g_mutex_lock (&operation->inquiry_lock);
while (operation->is_inquiring)
g_cond_wait (&operation->inquiry_finished_condition,
&operation->inquiry_lock);
g_mutex_unlock (&operation->inquiry_lock);
}
static void
on_sign_in_operation_cancelled (GCancellable *cancellable,
Operation *operation)
{
stop_waiting_on_inquiry (operation);
}
static void
on_kerberos_identity_inquiry (GoaKerberosIdentityInquiry *inquiry,
GCancellable *cancellable,
Operation *operation)
{
gulong handler_id;
start_inquiry (operation, GOA_IDENTITY_INQUIRY (inquiry));
handler_id = g_cancellable_connect (cancellable,
G_CALLBACK (on_sign_in_operation_cancelled),
operation, NULL);
if ((operation->cancellable == NULL) ||
!g_cancellable_is_cancelled (operation->cancellable))
wait_for_inquiry_to_complete (operation, inquiry);
g_cancellable_disconnect (cancellable, handler_id);
}
static void
get_identity (GoaKerberosIdentityManager *self,
Operation *operation)
{
GoaIdentity *identity;
g_debug ("GoaKerberosIdentityManager: get identity %s", operation->identifier);
identity = g_hash_table_lookup (self->priv->identities, operation->identifier);
if (identity == NULL)
{
g_simple_async_result_set_error (operation->result,
GOA_IDENTITY_MANAGER_ERROR,
GOA_IDENTITY_MANAGER_ERROR_IDENTITY_NOT_FOUND,
_("Could not find identity"));
g_simple_async_result_set_op_res_gpointer (operation->result, NULL, NULL);
return;
}
g_simple_async_result_set_op_res_gpointer (operation->result,
g_object_ref (identity),
(GDestroyNotify) 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->priv->credentials_cache_type, "FILE") == 0)
{
g_debug ("GoaKerberosIdentityManager: credential cache type %s doesn't supports cache collections",
self->priv->credentials_cache_type);
supports_multiple_identities = FALSE;
}
else if (g_strcmp0 (self->priv->credentials_cache_type, "DIR") == 0 ||
g_strcmp0 (self->priv->credentials_cache_type, "KEYRING") == 0)
{
g_debug ("GoaKerberosIdentityManager: credential cache type %s supports cache collections",
self->priv->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->priv->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->priv->identities) == 0)
{
error_code = krb5_cc_default (self->priv->kerberos_context, credentials_cache);
}
else
{
error_code = krb5_cc_new_unique (self->priv->kerberos_context,
self->priv->credentials_cache_type,
NULL,
credentials_cache);
}
return error_code;
}
static void
sign_in_identity (GoaKerberosIdentityManager *self,
Operation *operation)
{
GoaIdentity *identity;
GError *error;
krb5_error_code error_code;
gboolean is_new_identity = FALSE;
g_debug ("GoaKerberosIdentityManager: signing in identity %s",
operation->identifier);
error = NULL;
identity = g_hash_table_lookup (self->priv->identities, operation->identifier);
if (identity == NULL)
{
krb5_ccache credentials_cache;
error_code = get_new_credentials_cache (self, &credentials_cache);
if (error_code != 0)
{
const char *error_message;
error_message =
krb5_get_error_message (self->priv->kerberos_context, error_code);
g_debug ("GoaKerberosIdentityManager: Error creating new cache for identity credentials: %s",
error_message);
krb5_free_error_message (self->priv->kerberos_context, error_message);
g_simple_async_result_set_error (operation->result,
GOA_IDENTITY_MANAGER_ERROR,
GOA_IDENTITY_MANAGER_ERROR_CREATING_IDENTITY,
_("Could not create credential cache for identity"));
g_simple_async_result_set_op_res_gpointer (operation->result, NULL, NULL);
return;
}
identity = goa_kerberos_identity_new (self->priv->kerberos_context,
credentials_cache,
&error);
if (identity == NULL)
{
krb5_cc_destroy (self->priv->kerberos_context, credentials_cache);
g_simple_async_result_take_error (operation->result, error);
g_simple_async_result_set_op_res_gpointer (operation->result,
NULL,
NULL);
return;
}
krb5_cc_close (self->priv->kerberos_context, credentials_cache);
is_new_identity = TRUE;
}
else
{
g_object_ref (identity);
}
if (!goa_kerberos_identity_sign_in (GOA_KERBEROS_IDENTITY (identity),
operation->identifier,
operation->initial_password,
operation->preauth_source,
operation->sign_in_flags,
(GoaIdentityInquiryFunc)
on_kerberos_identity_inquiry,
operation,
NULL,
operation->cancellable,
&error))
{
if (is_new_identity)
goa_kerberos_identity_erase (GOA_KERBEROS_IDENTITY (identity), NULL);
g_simple_async_result_set_from_error (operation->result, error);
g_simple_async_result_set_op_res_gpointer (operation->result,
NULL,
NULL);
}
else
{
g_simple_async_result_set_op_res_gpointer (operation->result,
g_object_ref (identity),
(GDestroyNotify)
g_object_unref);
g_hash_table_replace (self->priv->identities,
g_strdup (operation->identifier),
g_object_ref (identity));
}
g_object_unref (identity);
}
static void
sign_out_identity (GoaKerberosIdentityManager *self,
Operation *operation)
{
GError *error;
gboolean was_signed_out;
char *identity_name;
identity_name =
goa_kerberos_identity_get_principal_name (GOA_KERBEROS_IDENTITY
(operation->identity));
g_debug ("GoaKerberosIdentityManager: signing out identity %s", identity_name);
g_free (identity_name);
error = NULL;
was_signed_out =
goa_kerberos_identity_erase (GOA_KERBEROS_IDENTITY (operation->identity),
&error);
if (!was_signed_out)
{
g_debug ("GoaKerberosIdentityManager: could not sign out identity: %s",
error->message);
g_error_free (error);
}
}
static void
block_scheduler_job (GoaKerberosIdentityManager *self)
{
g_mutex_lock (&self->priv->scheduler_job_lock);
while (self->priv->is_blocking_scheduler_job)
g_cond_wait (&self->priv->scheduler_job_unblocked,
&self->priv->scheduler_job_lock);
self->priv->is_blocking_scheduler_job = TRUE;
g_mutex_unlock (&self->priv->scheduler_job_lock);
}
static void
stop_blocking_scheduler_job (GoaKerberosIdentityManager *self)
{
g_mutex_lock (&self->priv->scheduler_job_lock);
self->priv->is_blocking_scheduler_job = FALSE;
g_cond_signal (&self->priv->scheduler_job_unblocked);
g_mutex_unlock (&self->priv->scheduler_job_lock);
}
static void
wait_for_scheduler_job_to_become_unblocked (GoaKerberosIdentityManager *self)
{
g_mutex_lock (&self->priv->scheduler_job_lock);
while (self->priv->is_blocking_scheduler_job)
g_cond_wait (&self->priv->scheduler_job_unblocked,
&self->priv->scheduler_job_lock);
g_mutex_unlock (&self->priv->scheduler_job_lock);
}
static void
on_job_cancelled (GCancellable *cancellable,
GoaKerberosIdentityManager *self)
{
Operation *operation;
operation = operation_new (self, cancellable, OPERATION_TYPE_STOP_JOB, NULL);
g_async_queue_push (self->priv->pending_operations, operation);
stop_blocking_scheduler_job (self);
}
static gboolean
on_job_scheduled (GIOSchedulerJob *job,
GCancellable *cancellable,
GoaKerberosIdentityManager *self)
{
GAsyncQueue *pending_operations;
g_assert (cancellable != NULL);
g_cancellable_connect (cancellable, G_CALLBACK (on_job_cancelled), self, NULL);
/* Take ownership of queue, since we may out live the identity manager */
pending_operations = g_async_queue_ref (self->priv->pending_operations);
while (!g_cancellable_is_cancelled (cancellable))
{
Operation *operation;
gboolean processed_operation = FALSE;
GError *error = NULL;
operation = g_async_queue_pop (pending_operations);
if (operation->result != NULL &&
g_cancellable_set_error_if_cancelled (operation->cancellable,
&error))
{
g_simple_async_result_take_error (operation->result, error);
g_simple_async_result_complete_in_idle (operation->result);
g_clear_object (&operation->result);
continue;
}
operation->job = job;
switch (operation->type)
{
case OPERATION_TYPE_STOP_JOB:
/* do nothing, loop will exit next iteration since cancellable
* is cancelled
*/
g_assert (g_cancellable_is_cancelled (cancellable));
operation_free (operation);
continue;
case OPERATION_TYPE_REFRESH:
processed_operation = refresh_identities (operation->manager, operation);
break;
case OPERATION_TYPE_GET_IDENTITY:
get_identity (operation->manager, operation);
processed_operation = TRUE;
break;
case OPERATION_TYPE_LIST:
list_identities (operation->manager, operation);
processed_operation = TRUE;
/* We want to block refreshes (and their associated "added"
* and "removed" signals) until the caller has had
* a chance to look at the batch of
* results we already processed
*/
g_assert (operation->result != NULL);
g_debug
("GoaKerberosIdentityManager: Blocking until identities list processed");
block_scheduler_job (self);
g_object_weak_ref (G_OBJECT (operation->result),
(GWeakNotify) stop_blocking_scheduler_job, self);
g_debug ("GoaKerberosIdentityManager: Continuing");
break;
case OPERATION_TYPE_SIGN_IN:
sign_in_identity (operation->manager, operation);
processed_operation = TRUE;
break;
case OPERATION_TYPE_SIGN_OUT:
sign_out_identity (operation->manager, operation);
processed_operation = TRUE;
break;
case OPERATION_TYPE_RENEW:
renew_identity (operation->manager, operation);
processed_operation = TRUE;
break;
default:
break;
}
operation->job = NULL;
if (operation->result != NULL)
{
g_simple_async_result_complete_in_idle (operation->result);
g_clear_object (&operation->result);
}
operation_free (operation);
wait_for_scheduler_job_to_become_unblocked (self);
/* Don't bother saying "Waiting for next operation" if this operation
* was a no-op, since the debug spew probably already says the message
*/
if (processed_operation)
g_debug ("GoaKerberosIdentityManager: Waiting for next operation");
}
g_async_queue_unref (pending_operations);
return FALSE;
}
static void
goa_kerberos_identity_manager_get_identity (GoaIdentityManager *manager,
const char *identifier,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GoaKerberosIdentityManager *self = GOA_KERBEROS_IDENTITY_MANAGER (manager);
GSimpleAsyncResult *result;
Operation *operation;
result = g_simple_async_result_new (G_OBJECT (self),
callback,
user_data,
goa_kerberos_identity_manager_get_identity);
operation = operation_new (self, cancellable, OPERATION_TYPE_GET_IDENTITY, result);
g_object_unref (result);
operation->identifier = g_strdup (identifier);
g_async_queue_push (self->priv->pending_operations, operation);
}
static GoaIdentity *
goa_kerberos_identity_manager_get_identity_finish (GoaIdentityManager *self,
GAsyncResult *result,
GError **error)
{
GoaIdentity *identity;
if (g_simple_async_result_propagate_error (G_SIMPLE_ASYNC_RESULT (result),
error))
return NULL;
identity =
g_simple_async_result_get_op_res_gpointer (G_SIMPLE_ASYNC_RESULT (result));
return g_object_ref (identity);
}
static void
goa_kerberos_identity_manager_list_identities (GoaIdentityManager *manager,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GoaKerberosIdentityManager *self = GOA_KERBEROS_IDENTITY_MANAGER (manager);
GSimpleAsyncResult *result;
Operation *operation;
result = g_simple_async_result_new (G_OBJECT (self),
callback,
user_data,
goa_kerberos_identity_manager_list_identities);
operation = operation_new (self, cancellable, OPERATION_TYPE_LIST, result);
g_object_unref (result);
g_async_queue_push (self->priv->pending_operations, operation);
}
static GList *
goa_kerberos_identity_manager_list_identities_finish (GoaIdentityManager *manager,
GAsyncResult *result,
GError **error)
{
GList *identities;
if (g_simple_async_result_propagate_error (G_SIMPLE_ASYNC_RESULT (result),
error))
return NULL;
identities =
g_simple_async_result_get_op_res_gpointer (G_SIMPLE_ASYNC_RESULT (result));
return identities;
}
static void
goa_kerberos_identity_manager_renew_identity (GoaIdentityManager *manager,
GoaIdentity *identity,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GoaKerberosIdentityManager *self = GOA_KERBEROS_IDENTITY_MANAGER (manager);
GSimpleAsyncResult *result;
Operation *operation;
result = g_simple_async_result_new (G_OBJECT (self),
callback,
user_data,
goa_kerberos_identity_manager_renew_identity);
operation = operation_new (self, cancellable, OPERATION_TYPE_RENEW, result);
g_object_unref (result);
operation->identity = g_object_ref (identity);
g_async_queue_push (self->priv->pending_operations, operation);
}
static void
goa_kerberos_identity_manager_renew_identity_finish (GoaIdentityManager *self,
GAsyncResult *result,
GError **error)
{
if (g_simple_async_result_propagate_error (G_SIMPLE_ASYNC_RESULT (result),
error))
return;
}
static void
goa_kerberos_identity_manager_sign_identity_in (GoaIdentityManager *manager,
const char *identifier,
gconstpointer initial_password,
const char *preauth_source,
GoaIdentitySignInFlags flags,
GoaIdentityInquiryFunc inquiry_func,
gpointer inquiry_data,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GoaKerberosIdentityManager *self = GOA_KERBEROS_IDENTITY_MANAGER (manager);
GSimpleAsyncResult *result;
Operation *operation;
result = g_simple_async_result_new (G_OBJECT (self),
callback,
user_data,
goa_kerberos_identity_manager_sign_identity_in);
operation = operation_new (self, cancellable, OPERATION_TYPE_SIGN_IN, result);
g_object_unref (result);
operation->identifier = g_strdup (identifier);
/* Not duped. Caller is responsible for ensuring it stays alive
* for duration of operation
*/
operation->initial_password = initial_password;
operation->preauth_source = g_strdup (preauth_source);
operation->sign_in_flags = flags;
operation->inquiry_func = inquiry_func;
operation->inquiry_data = inquiry_data;
g_mutex_init (&operation->inquiry_lock);
g_cond_init (&operation->inquiry_finished_condition);
operation->is_inquiring = FALSE;
g_async_queue_push (self->priv->pending_operations, operation);
}
static GoaIdentity *
goa_kerberos_identity_manager_sign_identity_in_finish (GoaIdentityManager *self,
GAsyncResult *result,
GError **error)
{
GoaIdentity *identity;
if (g_simple_async_result_propagate_error (G_SIMPLE_ASYNC_RESULT (result), error))
return NULL;
identity =
g_simple_async_result_get_op_res_gpointer (G_SIMPLE_ASYNC_RESULT (result));
return identity;
}
static void
goa_kerberos_identity_manager_sign_identity_out (GoaIdentityManager *manager,
GoaIdentity *identity,
GCancellable *cancellable,
GAsyncReadyCallback callback,
gpointer user_data)
{
GoaKerberosIdentityManager *self = GOA_KERBEROS_IDENTITY_MANAGER (manager);
GSimpleAsyncResult *result;
Operation *operation;
result = g_simple_async_result_new (G_OBJECT (self),
callback,
user_data,
goa_kerberos_identity_manager_sign_identity_out);
operation = operation_new (self, cancellable, OPERATION_TYPE_SIGN_OUT, result);
g_object_unref (result);
operation->identity = g_object_ref (identity);
g_async_queue_push (self->priv->pending_operations, operation);
}
static void
goa_kerberos_identity_manager_sign_identity_out_finish (GoaIdentityManager *self,
GAsyncResult *result,
GError **error)
{
if (g_simple_async_result_propagate_error (G_SIMPLE_ASYNC_RESULT (result), error))
return;
return;
}
static char *
goa_kerberos_identity_manager_name_identity (GoaIdentityManager *manager,
GoaIdentity *identity)
{
GoaKerberosIdentityManager *self = GOA_KERBEROS_IDENTITY_MANAGER (manager);
char *name;
GList *other_identities;
gboolean other_identity_needs_rename;
name = goa_kerberos_identity_get_realm_name (GOA_KERBEROS_IDENTITY (identity));
if (name == NULL)
return NULL;
other_identities = g_hash_table_lookup (self->priv->identities_by_realm, name);
/* If there was already exactly one identity for this realm before,
* then it was going by just the realm name, so we need to rename it
* to use the full principle name
*/
if (other_identities != NULL &&
other_identities->next == NULL && other_identities->data != identity)
other_identity_needs_rename = TRUE;
other_identities = g_list_remove (other_identities, identity);
other_identities = g_list_prepend (other_identities, identity);
g_hash_table_replace (self->priv->identities_by_realm,
g_strdup (name),
other_identities);
if (other_identities->next != NULL)
{
g_free (name);
name = goa_kerberos_identity_get_principal_name (GOA_KERBEROS_IDENTITY (identity));
if (other_identity_needs_rename)
{
GoaIdentity *other_identity = other_identities->next->data;
_goa_identity_manager_emit_identity_renamed (GOA_IDENTITY_MANAGER (self),
other_identity);
}
}
return name;
}
static void
identity_manager_interface_init (GoaIdentityManagerInterface *interface)
{
interface->get_identity = goa_kerberos_identity_manager_get_identity;
interface->get_identity_finish = goa_kerberos_identity_manager_get_identity_finish;
interface->list_identities = goa_kerberos_identity_manager_list_identities;
interface->list_identities_finish = goa_kerberos_identity_manager_list_identities_finish;
interface->sign_identity_in = goa_kerberos_identity_manager_sign_identity_in;
interface->sign_identity_in_finish = goa_kerberos_identity_manager_sign_identity_in_finish;
interface->sign_identity_out = goa_kerberos_identity_manager_sign_identity_out;
interface->sign_identity_out_finish = goa_kerberos_identity_manager_sign_identity_out_finish;
interface->renew_identity = goa_kerberos_identity_manager_renew_identity;
interface->renew_identity_finish = goa_kerberos_identity_manager_renew_identity_finish;
interface->name_identity = goa_kerberos_identity_manager_name_identity;
}
static void
on_credentials_cache_changed (GFileMonitor *monitor,
GFile *file,
GFile *other_file,
GFileMonitorEvent *event_type,
GoaKerberosIdentityManager *self)
{
schedule_refresh (self);
}
static gboolean
on_polling_timeout (GoaKerberosIdentityManager *self)
{
schedule_refresh (self);
return G_SOURCE_CONTINUE;
}
static gboolean
monitor_credentials_cache (GoaKerberosIdentityManager *self,
GError **error)
{
krb5_ccache default_cache;
const char *cache_type;
const char *cache_path;
GFileMonitor *monitor = NULL;
krb5_error_code error_code;
GError *monitoring_error = NULL;
gboolean can_monitor = TRUE;
error_code = krb5_cc_default (self->priv->kerberos_context, &default_cache);
if (error_code != 0)
{
const char *error_message;
error_message =
krb5_get_error_message (self->priv->kerberos_context, error_code);
g_set_error_literal (error,
GOA_IDENTITY_MANAGER_ERROR,
GOA_IDENTITY_MANAGER_ERROR_ACCESSING_CREDENTIALS,
error_message);
krb5_free_error_message (self->priv->kerberos_context, error_message);
return FALSE;
}
cache_type = krb5_cc_get_type (self->priv->kerberos_context, default_cache);
g_assert (cache_type != NULL);
if (strcmp (cache_type, "FILE") != 0 && strcmp (cache_type, "DIR") != 0)
{
g_warning ("GoaKerberosIdentityManager: Using polling for change notification for credential cache type '%s'",
cache_type);
can_monitor = FALSE;
}
g_free (self->priv->credentials_cache_type);
self->priv->credentials_cache_type = g_strdup (cache_type);
/* If we're using a FILE type credential cache, then the
* default cache file is the only cache we care about,
* and its path is what we want to monitor.
*
* If we're using a DIR type credential cache, then the default
* cache file is one of many possible cache files, all in the
* same directory. We want to monitor that directory.
*/
cache_path = krb5_cc_get_name (self->priv->kerberos_context, default_cache);
/* The cache name might have a : in front of it.
* See goakerberosidentity.c (fetch_raw_credentials) for similar code
* FIXME: figure out if that behavior is by design, or some
* odd bug.
*/
if (cache_path[0] == ':')
cache_path++;
if (can_monitor)
{
GFile *file;
file = g_file_new_for_path (cache_path);
monitoring_error = NULL;
if (strcmp (cache_type, "FILE") == 0)
{
monitor = g_file_monitor_file (file,
G_FILE_MONITOR_NONE,
NULL,
&monitoring_error);
}
else if (strcmp (cache_type, "DIR") == 0)
{
GFile *directory;
directory = g_file_get_parent (file);
monitor = g_file_monitor_directory (directory,
G_FILE_MONITOR_NONE,
NULL,
&monitoring_error);
g_object_unref (directory);
}
g_object_unref (file);
}
if (monitor == NULL)
{
if (monitoring_error != NULL)
{
g_warning ("GoaKerberosIdentityManager: Could not monitor credentials for %s (type %s), reverting to "
"polling: %s",
cache_path,
cache_type,
monitoring_error != NULL? monitoring_error->message : "");
g_clear_error (&monitoring_error);
}
can_monitor = FALSE;
}
else
{
self->priv->credentials_cache_changed_signal_id =
g_signal_connect (G_OBJECT (monitor), "changed",
G_CALLBACK (on_credentials_cache_changed), self);
self->priv->credentials_cache_monitor = monitor;
}
if (!can_monitor)
self->priv->polling_timeout_id = g_timeout_add_seconds (FALLBACK_POLLING_INTERVAL,
(GSourceFunc) on_polling_timeout,
self);
krb5_cc_close (self->priv->kerberos_context, default_cache);
return TRUE;
}
static void
stop_watching_credentials_cache (GoaKerberosIdentityManager *self)
{
if (self->priv->credentials_cache_monitor != NULL)
{
if (!g_file_monitor_is_cancelled (self->priv->credentials_cache_monitor))
g_file_monitor_cancel (self->priv->credentials_cache_monitor);
g_clear_object (&self->priv->credentials_cache_monitor);
}
if (self->priv->polling_timeout_id != 0)
{
g_source_remove (self->priv->polling_timeout_id);
self->priv->polling_timeout_id = 0;
}
}
static gboolean
goa_kerberos_identity_manager_initable_init (GInitable *initable,
GCancellable *cancellable,
GError **error)
{
GoaKerberosIdentityManager *self = GOA_KERBEROS_IDENTITY_MANAGER (initable);
krb5_error_code error_code;
GError *monitoring_error;
if (g_cancellable_set_error_if_cancelled (cancellable, error))
return FALSE;
error_code = krb5_init_context (&self->priv->kerberos_context);
if (error_code != 0)
{
const char *error_message;
error_message =
krb5_get_error_message (self->priv->kerberos_context, error_code);
g_set_error_literal (error,
GOA_IDENTITY_MANAGER_ERROR,
GOA_IDENTITY_MANAGER_ERROR_INITIALIZING, error_message);
krb5_free_error_message (self->priv->kerberos_context, error_message);
return FALSE;
}
monitoring_error = NULL;
if (!monitor_credentials_cache (self, &monitoring_error))
{
g_warning ("GoaKerberosIdentityManager: Could not monitor credentials: %s",
monitoring_error->message);
g_error_free (monitoring_error);
}
schedule_refresh (self);
return TRUE;
}
static void
initable_interface_init (GInitableIface *interface)
{
interface->init = goa_kerberos_identity_manager_initable_init;
}
static void
goa_kerberos_identity_manager_init (GoaKerberosIdentityManager *self)
{
self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
GOA_TYPE_KERBEROS_IDENTITY_MANAGER,
GoaKerberosIdentityManagerPrivate);
self->priv->identities = g_hash_table_new_full (g_str_hash,
g_str_equal,
(GDestroyNotify)
g_free,
(GDestroyNotify) g_object_unref);
self->priv->expired_identities = g_hash_table_new_full (g_str_hash,
g_str_equal,
(GDestroyNotify)
g_free, NULL);
self->priv->identities_by_realm = g_hash_table_new_full (g_str_hash,
g_str_equal,
(GDestroyNotify)
g_free, NULL);
self->priv->pending_operations = g_async_queue_new ();
g_mutex_init (&self->priv->scheduler_job_lock);
g_cond_init (&self->priv->scheduler_job_unblocked);
self->priv->scheduler_cancellable = g_cancellable_new ();
g_io_scheduler_push_job ((GIOSchedulerJobFunc)
on_job_scheduled,
self,
NULL,
G_PRIORITY_DEFAULT,
self->priv->scheduler_cancellable);
}
static void
cancel_pending_operations (GoaKerberosIdentityManager *self)
{
Operation *operation;
operation = g_async_queue_try_pop (self->priv->pending_operations);
while (operation != NULL)
{
g_cancellable_cancel (operation->cancellable);
operation_free (operation);
operation = g_async_queue_try_pop (self->priv->pending_operations);
}
}
static void
goa_kerberos_identity_manager_dispose (GObject *object)
{
GoaKerberosIdentityManager *self = GOA_KERBEROS_IDENTITY_MANAGER (object);
if (self->priv->identities_by_realm != NULL)
{
g_hash_table_unref (self->priv->identities_by_realm);
self->priv->identities_by_realm = NULL;
}
if (self->priv->expired_identities != NULL)
{
g_hash_table_unref (self->priv->expired_identities);
self->priv->expired_identities = NULL;
}
if (self->priv->identities != NULL)
{
g_hash_table_unref (self->priv->identities);
self->priv->identities = NULL;
}
stop_watching_credentials_cache (self);
if (self->priv->pending_operations != NULL)
cancel_pending_operations (self);
if (self->priv->scheduler_cancellable != NULL)
{
if (!g_cancellable_is_cancelled (self->priv->scheduler_cancellable))
{
g_cancellable_cancel (self->priv->scheduler_cancellable);
}
g_clear_object (&self->priv->scheduler_cancellable);
}
/* Note, other thread may still be holding a local reference to queue
* while it shuts down from cancelled scheduler_cancellable above
*/
if (self->priv->pending_operations != NULL)
{
g_async_queue_unref (self->priv->pending_operations);
self->priv->pending_operations = NULL;
}
G_OBJECT_CLASS (goa_kerberos_identity_manager_parent_class)->dispose (object);
}
static void
goa_kerberos_identity_manager_finalize (GObject *object)
{
GoaKerberosIdentityManager *self = GOA_KERBEROS_IDENTITY_MANAGER (object);
g_free (self->priv->credentials_cache_type);
g_cond_clear (&self->priv->scheduler_job_unblocked);
krb5_free_context (self->priv->kerberos_context);
G_OBJECT_CLASS (goa_kerberos_identity_manager_parent_class)->finalize (object);
}
static void
goa_kerberos_identity_manager_class_init (GoaKerberosIdentityManagerClass *klass)
{
GObjectClass *object_class = G_OBJECT_CLASS (klass);
object_class->dispose = goa_kerberos_identity_manager_dispose;
object_class->finalize = goa_kerberos_identity_manager_finalize;
g_type_class_add_private (klass, sizeof (GoaKerberosIdentityManagerPrivate));
}
GoaIdentityManager *
goa_kerberos_identity_manager_new (GCancellable * cancellable, GError ** error)
{
if (goa_kerberos_identity_manager_singleton == NULL)
{
GObject *object;
object = g_object_new (GOA_TYPE_KERBEROS_IDENTITY_MANAGER, NULL);
goa_kerberos_identity_manager_singleton = GOA_IDENTITY_MANAGER (object);
g_object_add_weak_pointer (object,
(gpointer *) &
goa_kerberos_identity_manager_singleton);
if (!g_initable_init (G_INITABLE (object), cancellable, error))
{
g_object_unref (object);
return NULL;
}
}
else
{
if (g_cancellable_set_error_if_cancelled (cancellable, error))
return NULL;
g_object_ref (goa_kerberos_identity_manager_singleton);
}
return goa_kerberos_identity_manager_singleton;
}