/* -*- 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 "goaidentity.h"
#include "goakerberosidentity.h"
#include "goakerberosidentityinquiry.h"
#include "goaalarm.h"
#include <netinet/in.h>
#include <arpa/nameser.h>
#include <resolv.h>
#include <string.h>
#include <glib/gi18n.h>
#include <gio/gio.h>
typedef enum
{
VERIFICATION_LEVEL_UNVERIFIED,
VERIFICATION_LEVEL_ERROR,
VERIFICATION_LEVEL_EXISTS,
VERIFICATION_LEVEL_SIGNED_IN
} VerificationLevel;
struct _GoaKerberosIdentityPrivate
{
krb5_context kerberos_context;
krb5_ccache credentials_cache;
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,
};
enum
{
PROP_0,
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
goa_kerberos_identity_dispose (GObject *object)
{
GoaKerberosIdentity *self = GOA_KERBEROS_IDENTITY (object);
G_LOCK (identity_lock);
clear_alarms (self);
g_clear_pointer (&self->priv->preauth_identity_source,
g_free);
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->priv->identifier);
if (self->priv->credentials_cache != NULL)
krb5_cc_close (self->priv->kerberos_context, self->priv->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->priv->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->priv->start_time);
G_UNLOCK (identity_lock);
break;
case PROP_RENEWAL_TIMESTAMP:
G_LOCK (identity_lock);
g_value_set_int64 (value, (gint64) self->priv->renewal_time);
G_UNLOCK (identity_lock);
break;
case PROP_EXPIRATION_TIMESTAMP:
G_LOCK (identity_lock);
g_value_set_int64 (value, (gint64) self->priv->expiration_time);
G_UNLOCK (identity_lock);
break;
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, param_spec);
break;
}
}
static void
goa_kerberos_identity_class_init (GoaKerberosIdentityClass *klass)
{
GObjectClass *object_class;
object_class = G_OBJECT_CLASS (klass);
object_class->dispose = goa_kerberos_identity_dispose;
object_class->finalize = goa_kerberos_identity_finalize;
object_class->get_property = goa_kerberos_identity_get_property;
g_type_class_add_private (klass, sizeof (GoaKerberosIdentityPrivate));
signals[EXPIRING] = g_signal_new ("expiring",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
0,
NULL,
NULL,
NULL,
G_TYPE_NONE,
0);
signals[EXPIRED] = g_signal_new ("expired",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
0,
NULL,
NULL,
NULL,
G_TYPE_NONE,
0);
signals[UNEXPIRED] = g_signal_new ("unexpired",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
0,
NULL,
NULL,
NULL,
G_TYPE_NONE,
0);
signals[NEEDS_RENEWAL] = g_signal_new ("needs-renewal",
G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
0,
NULL,
NULL,
NULL,
G_TYPE_NONE,
0);
signals[NEEDS_REFRESH] = g_signal_new ("needs-refresh",
G_TYPE_FROM_CLASS (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;
if (self->priv->credentials_cache == NULL)
return NULL;
error_code = krb5_cc_get_principal (self->priv->kerberos_context,
self->priv->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->priv->kerberos_context,
principal,
0,
&unparsed_name);
if (error_code != 0)
{
const char *error_message;
error_message =
krb5_get_error_message (self->priv->kerberos_context, error_code);
g_debug ("GoaKerberosIdentity: Error parsing principal identity name: %s",
error_message);
krb5_free_error_message (self->priv->kerberos_context, error_message);
goto out;
}
identifier = g_strdup (unparsed_name);
krb5_free_unparsed_name (self->priv->kerberos_context, unparsed_name);
out:
krb5_free_principal (self->priv->kerberos_context, principal);
return identifier;
}
static void
goa_kerberos_identity_init (GoaKerberosIdentity *self)
{
self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self,
GOA_TYPE_KERBEROS_IDENTITY,
GoaKerberosIdentityPrivate);
}
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->priv->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->priv->kerberos_context, error_message);
}
char *
goa_kerberos_identity_get_principal_name (GoaKerberosIdentity *self)
{
krb5_principal principal;
krb5_error_code error_code;
char *unparsed_name;
char *principal_name;
int flags;
if (self->priv->identifier == NULL)
return NULL;
error_code = krb5_parse_name (self->priv->kerberos_context,
self->priv->identifier,
&principal);
if (error_code != 0)
{
const char *error_message;
error_message =
krb5_get_error_message (self->priv->kerberos_context, error_code);
g_debug
("GoaKerberosIdentity: Error parsing identity %s into kerberos principal: %s",
self->priv->identifier, error_message);
krb5_free_error_message (self->priv->kerberos_context, error_message);
return NULL;
}
flags = KRB5_PRINCIPAL_UNPARSE_DISPLAY;
error_code = krb5_unparse_name_flags (self->priv->kerberos_context,
principal, flags, &unparsed_name);
if (error_code != 0)
{
const char *error_message;
error_message =
krb5_get_error_message (self->priv->kerberos_context, error_code);
g_debug ("GoaKerberosIdentity: Error parsing principal identity name: %s",
error_message);
krb5_free_error_message (self->priv->kerberos_context, error_message);
return NULL;
}
principal_name = g_strdup (unparsed_name);
krb5_free_unparsed_name (self->priv->kerberos_context, unparsed_name);
return principal_name;
}
char *
goa_kerberos_identity_get_realm_name (GoaKerberosIdentity *self)
{
krb5_principal principal;
krb5_error_code error_code;
krb5_data *realm;
char *realm_name;
if (self->priv->identifier == NULL)
return NULL;
error_code = krb5_parse_name (self->priv->kerberos_context,
self->priv->identifier, &principal);
if (error_code != 0)
{
const char *error_message;
error_message =
krb5_get_error_message (self->priv->kerberos_context, error_code);
g_debug
("GoaKerberosIdentity: Error parsing identity %s into kerberos principal: %s",
self->priv->identifier, error_message);
krb5_free_error_message (self->priv->kerberos_context, error_message);
return NULL;
}
realm = krb5_princ_realm (self->priv->kerberos_context, principal);
realm_name = g_strndup (realm->data, realm->length);
krb5_free_principal (self->priv->kerberos_context, principal);
return realm_name;
}
char *
goa_kerberos_identity_get_preauthentication_source (GoaKerberosIdentity *self)
{
return g_strdup (self->priv->preauth_identity_source);
}
static const char *
goa_kerberos_identity_get_identifier (GoaIdentity *identity)
{
GoaKerberosIdentity *self = GOA_KERBEROS_IDENTITY (identity);
return self->priv->identifier;
}
static gboolean
credentials_validate_existence (GoaKerberosIdentity *self,
krb5_principal principal, krb5_creds * credentials)
{
/* Checks if default principal associated with the cache has a valid
* ticket granting ticket in the passed in credentials
*/
if (krb5_is_config_principal (self->priv->kerberos_context, credentials->server))
return FALSE;
/* looking for the krbtgt / REALM pair, so it should be exactly 2 items */
if (krb5_princ_size (self->priv->kerberos_context, credentials->server) != 2)
return FALSE;
if (!krb5_realm_compare (self->priv->kerberos_context,
credentials->server, principal))
{
/* credentials are from some other realm */
return FALSE;
}
if (strncmp (credentials->server->data[0].data,
KRB5_TGS_NAME, credentials->server->data[0].length) != 0)
{
/* credentials aren't for ticket granting */
return FALSE;
}
if (credentials->server->data[1].length != principal->realm.length ||
memcmp (credentials->server->data[1].data,
principal->realm.data, principal->realm.length) != 0)
{
/* credentials are for some other realm */
return FALSE;
}
return TRUE;
}
static gboolean
snoop_preauth_identity_from_credentials (GoaKerberosIdentity *self,
krb5_creds *credentials,
char **identity_source)
{
GRegex *regex;
GMatchInfo *match_info = NULL;
gboolean identity_source_exposed = FALSE;
if (!krb5_is_config_principal (self->priv->kerberos_context, credentials->server))
return FALSE;
regex = g_regex_new ("\"X509_user_identity\":\"(?P<identity_source>[^\"]*)\"",
G_REGEX_MULTILINE | G_REGEX_CASELESS | G_REGEX_RAW,
0,
NULL);
if (regex == NULL)
return FALSE;
g_regex_match_full (regex, credentials->ticket.data, credentials->ticket.length, 0, 0, &match_info, NULL);
if (match_info != NULL && g_match_info_matches (match_info))
{
if (identity_source)
{
g_free (*identity_source);
*identity_source = g_match_info_fetch_named (match_info, "identity_source");
}
identity_source_exposed = TRUE;
}
g_match_info_free (match_info);
g_regex_unref (regex);
return identity_source_exposed;
}
static krb5_timestamp
get_current_time (GoaKerberosIdentity *self)
{
krb5_timestamp current_time;
krb5_error_code error_code;
error_code = krb5_timeofday (self->priv->kerberos_context, ¤t_time);
if (error_code != 0)
{
const char *error_message;
error_message =
krb5_get_error_message (self->priv->kerberos_context, error_code);
g_debug ("GoaKerberosIdentity: Error getting current time: %s", error_message);
krb5_free_error_message (self->priv->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)
{
*request->idle_id = 0;
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,
(GSourceFunc)
on_notify_queued,
request,
(GDestroyNotify)
clear_idle_id);
}
static gboolean
set_start_time (GoaKerberosIdentity *self,
krb5_timestamp start_time)
{
if (self->priv->start_time != start_time)
{
self->priv->start_time = start_time;
queue_notify (self,
&self->priv->start_time_idle_id,
"start-timestamp");
return TRUE;
}
return FALSE;
}
static gboolean
set_renewal_time (GoaKerberosIdentity *self,
krb5_timestamp renewal_time)
{
if (self->priv->renewal_time != renewal_time)
{
self->priv->renewal_time = renewal_time;
queue_notify (self,
&self->priv->renewal_time_idle_id,
"renewal-timestamp");
return TRUE;
}
return FALSE;
}
static gboolean
set_expiration_time (GoaKerberosIdentity *self,
krb5_timestamp expiration_time)
{
if (self->priv->expiration_time != expiration_time)
{
self->priv->expiration_time = expiration_time;
queue_notify (self,
&self->priv->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->priv->start_time == 0)
*start_time = credentials_start_time;
else
*start_time = MIN (self->priv->start_time,
credentials_start_time);
*expiration_time = MAX (credentials->times.endtime,
self->priv->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)
{
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->priv->credentials_cache == NULL)
goto out;
error_code = krb5_cc_get_principal (self->priv->kerberos_context,
self->priv->credentials_cache,
&principal);
if (error_code != 0)
{
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->priv->kerberos_context,
self->priv->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->priv->kerberos_context,
self->priv->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,
&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->priv->kerberos_context, &credentials);
error_code = krb5_cc_next_cred (self->priv->kerberos_context,
self->priv->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->priv->kerberos_context,
self->priv->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:
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->priv->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->priv->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));
g_debug ("GoaKerberosIdentity: expiration alarm fired for identity %s",
goa_identity_get_identifier (GOA_IDENTITY (self)));
g_signal_emit (G_OBJECT (self), signals[NEEDS_REFRESH], 0);
}
static void
on_expiration_alarm_rearmed (GoaAlarm *alarm,
GoaKerberosIdentity *self)
{
g_return_if_fail (GOA_IS_ALARM (alarm));
g_return_if_fail (GOA_IS_KERBEROS_IDENTITY (self));
g_debug ("GoaKerberosIdentity: expiration alarm rearmed");
g_signal_emit (G_OBJECT (self), signals[NEEDS_REFRESH], 0);
}
static void
on_renewal_alarm_rearmed (GoaAlarm *alarm,
GoaKerberosIdentity *self)
{
g_return_if_fail (GOA_IS_ALARM (alarm));
g_return_if_fail (GOA_IS_KERBEROS_IDENTITY (self));
g_debug ("GoaKerberosIdentity: renewal alarm rearmed");
}
static void
on_renewal_alarm_fired (GoaAlarm *alarm,
GoaKerberosIdentity *self)
{
g_return_if_fail (GOA_IS_ALARM (alarm));
g_return_if_fail (GOA_IS_KERBEROS_IDENTITY (self));
if (self->priv->cached_verification_level == VERIFICATION_LEVEL_SIGNED_IN)
{
g_debug ("GoaKerberosIdentity: renewal alarm fired for signed-in identity");
g_signal_emit (G_OBJECT (self), signals[NEEDS_RENEWAL], 0);
}
}
static void
on_expiring_alarm_rearmed (GoaAlarm *alarm,
GoaKerberosIdentity *self)
{
g_return_if_fail (GOA_IS_ALARM (alarm));
g_return_if_fail (GOA_IS_KERBEROS_IDENTITY (self));
g_debug ("GoaKerberosIdentity: expiring alarm rearmed");
}
static void
on_expiring_alarm_fired (GoaAlarm *alarm,
GoaKerberosIdentity *self)
{
g_return_if_fail (GOA_IS_ALARM (alarm));
g_return_if_fail (GOA_IS_KERBEROS_IDENTITY (self));
if (self->priv->cached_verification_level == VERIFICATION_LEVEL_SIGNED_IN)
{
g_debug ("GoaKerberosIdentity: expiring alarm fired for signed-in identity");
g_signal_emit (G_OBJECT (self), signals[EXPIRING], 0);
}
}
static gboolean
unref_alarm (GoaAlarm *alarm)
{
g_object_unref (G_OBJECT (alarm));
return G_SOURCE_REMOVE;
}
static void
clear_alarm_and_unref_on_idle (GoaKerberosIdentity *self,
GoaAlarm **alarm)
{
if (!*alarm)
return;
g_idle_add ((GSourceFunc) unref_alarm, *alarm);
*alarm = NULL;
}
static void
reset_alarm (GoaKerberosIdentity *self,
GoaAlarm **alarm,
GDateTime *alarm_time)
{
GDateTime *old_alarm_time = NULL;
G_LOCK (identity_lock);
if (*alarm)
old_alarm_time = goa_alarm_get_time (*alarm);
if (old_alarm_time == NULL || !g_date_time_equal (alarm_time, old_alarm_time))
{
clear_alarm_and_unref_on_idle (self, alarm);
*alarm = goa_alarm_new (alarm_time);
}
G_UNLOCK (identity_lock);
}
static void
disconnect_alarm_signals (GoaKerberosIdentity *self)
{
if (self->priv->renewal_alarm)
{
g_signal_handlers_disconnect_by_func (G_OBJECT (self->priv->renewal_alarm),
G_CALLBACK (on_renewal_alarm_fired),
self);
g_signal_handlers_disconnect_by_func (G_OBJECT (self->priv->renewal_alarm),
G_CALLBACK (on_renewal_alarm_rearmed),
self);
}
if (self->priv->expiring_alarm)
{
g_signal_handlers_disconnect_by_func (G_OBJECT (self->priv->expiring_alarm),
G_CALLBACK (on_expiring_alarm_fired),
self);
g_signal_handlers_disconnect_by_func (G_OBJECT (self->priv->expiring_alarm),
G_CALLBACK (on_expiring_alarm_rearmed),
self);
}
if (self->priv->expiration_alarm)
{
g_signal_handlers_disconnect_by_func (G_OBJECT (self->priv->expiration_alarm),
G_CALLBACK (on_expiration_alarm_rearmed),
self);
g_signal_handlers_disconnect_by_func (G_OBJECT (self->priv->expiration_alarm),
G_CALLBACK (on_expiration_alarm_fired),
self);
}
}
static void
connect_alarm_signals (GoaKerberosIdentity *self)
{
if (self->priv->renewal_alarm)
{
g_signal_connect (G_OBJECT (self->priv->renewal_alarm),
"fired",
G_CALLBACK (on_renewal_alarm_fired),
self);
g_signal_connect (G_OBJECT (self->priv->renewal_alarm),
"rearmed",
G_CALLBACK (on_renewal_alarm_rearmed),
self);
}
g_signal_connect (G_OBJECT (self->priv->expiring_alarm),
"fired",
G_CALLBACK (on_expiring_alarm_fired),
self);
g_signal_connect (G_OBJECT (self->priv->expiring_alarm),
"rearmed",
G_CALLBACK (on_expiring_alarm_rearmed),
self);
g_signal_connect (G_OBJECT (self->priv->expiration_alarm),
"fired",
G_CALLBACK (on_expiration_alarm_fired),
self);
g_signal_connect (G_OBJECT (self->priv->expiration_alarm),
"rearmed",
G_CALLBACK (on_expiration_alarm_rearmed),
self);
}
static void
reset_alarms (GoaKerberosIdentity *self)
{
GDateTime *start_time = NULL;
GDateTime *expiration_time = NULL;
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->priv->start_time);
if (self->priv->renewal_time != 0)
latest_possible_renewal_time = g_date_time_new_from_unix_local (self->priv->renewal_time);
expiration_time = g_date_time_new_from_unix_local (self->priv->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->priv->renewal_alarm, renewal_time);
reset_alarm (self, &self->priv->expiring_alarm, expiring_time);
reset_alarm (self, &self->priv->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->priv->renewal_alarm);
clear_alarm_and_unref_on_idle (self, &self->priv->expiring_alarm);
clear_alarm_and_unref_on_idle (self, &self->priv->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->priv->identifier == NULL)
{
self->priv->identifier = get_identifier (self, error);
if (self->priv->identifier != NULL)
queue_notify (self, &self->priv->identifier_idle_id, "identifier");
}
verification_error = NULL;
self->priv->cached_verification_level =
verify_identity (self, &self->priv->preauth_identity_source, &verification_error);
switch (self->priv->cached_verification_level)
{
case VERIFICATION_LEVEL_EXISTS:
case VERIFICATION_LEVEL_SIGNED_IN:
reset_alarms (self);
queue_notify (self, &self->priv->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;
}
}
static void
initable_interface_init (GInitableIface *interface)
{
interface->init = goa_kerberos_identity_initable_init;
}
typedef struct
{
GoaKerberosIdentity *identity;
GoaIdentityInquiryFunc inquiry_func;
gpointer inquiry_data;
GDestroyNotify destroy_notify;
GCancellable *cancellable;
} SignInOperation;
static krb5_error_code
on_kerberos_inquiry (krb5_context kerberos_context,
SignInOperation *operation,
const char *name,
const char *banner,
int number_of_prompts,
krb5_prompt prompts[])
{
GoaIdentityInquiry *inquiry;
krb5_error_code error_code;
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;
else
error_code = 0;
g_object_unref (inquiry);
return error_code;
}
static gboolean
create_credential_cache (GoaKerberosIdentity *self,
GError **error)
{
krb5_ccache default_cache;
const char *cache_type;
krb5_error_code error_code;
error_code = krb5_cc_default (self->priv->kerberos_context, &default_cache);
if (error_code == 0)
{
cache_type = krb5_cc_get_type (self->priv->kerberos_context, default_cache);
error_code = krb5_cc_new_unique (self->priv->kerberos_context,
cache_type,
NULL,
&self->priv->credentials_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;
}
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;
if (self->priv->credentials_cache == NULL)
{
if (!create_credential_cache (self, error))
{
krb5_free_cred_contents (self->priv->kerberos_context, new_credentials);
goto out;
}
}
error_code = krb5_cc_initialize (self->priv->kerberos_context,
self->priv->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->priv->kerberos_context, new_credentials);
goto out;
}
error_code = krb5_cc_store_cred (self->priv->kerberos_context,
self->priv->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->priv->kerberos_context, new_credentials);
goto out;
}
krb5_free_cred_contents (self->priv->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;
operation->inquiry_data = inquiry_data;
operation->destroy_notify = destroy_notify;
if (cancellable == NULL)
operation->cancellable = g_cancellable_new ();
else
operation->cancellable = g_object_ref (cancellable);
return operation;
}
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->priv->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->priv->identifier, principal_name) != 0)
{
g_free (self->priv->identifier);
self->priv->identifier = g_strdup (principal_name);
}
error_code = krb5_parse_name (self->priv->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)
krb5_get_init_creds_opt_set_proxiable (options, TRUE);
if ((flags & GOA_IDENTITY_SIGN_IN_FLAGS_DISALLOW_RENEWAL) == 0)
krb5_get_init_creds_opt_set_renew_life (options, G_MAXINT);
if (preauth_source != NULL)
{
krb5_get_init_creds_opt_set_pa (self->priv->kerberos_context,
options,
"X509_user_identity", preauth_source);
}
/* Poke glibc in case the network changed
*/
res_init ();
start_time = 0;
service_name = NULL;
error_code = krb5_get_init_creds_password (self->priv->kerberos_context,
&new_credentials,
principal,
(char *)
initial_password,
(krb5_prompter_fct)
on_kerberos_inquiry,
operation,
start_time,
service_name,
options);
if (error_code == KRB5_LIBOS_PWDINTR)
g_cancellable_cancel (operation->cancellable);
if (g_cancellable_set_error_if_cancelled (cancellable, error))
{
if (destroy_notify)
destroy_notify (inquiry_data);
sign_in_operation_free (operation);
krb5_free_principal (self->priv->kerberos_context, principal);
goto done;
}
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->priv->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->priv->kerberos_context, principal);
goto done;
}
krb5_free_principal (self->priv->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->priv->identifier, new_identifier) != 0 && new_identifier != NULL)
{
g_free (self->priv->identifier);
self->priv->identifier = new_identifier;
queue_notify (self, &self->priv->identifier_idle_id, "identifier");
}
else
{
g_free (new_identifier);
}
}
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;
if (self->priv->credentials_cache != NULL)
krb5_cc_close (self->priv->kerberos_context, self->priv->credentials_cache);
krb5_cc_dup (new_identity->priv->kerberos_context,
new_identity->priv->credentials_cache,
&self->priv->credentials_cache);
G_LOCK (identity_lock);
update_identifier (self, new_identity);
time_changed |= set_start_time (self, new_identity->priv->start_time);
time_changed |= set_renewal_time (self, new_identity->priv->renewal_time);
time_changed |= set_expiration_time (self, new_identity->priv->expiration_time);
old_verification_level = self->priv->cached_verification_level;
new_verification_level = new_identity->priv->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->priv->preauth_identity_source);
self->priv->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->priv->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->priv->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->priv->cached_verification_level = new_verification_level;
G_UNLOCK (identity_lock);
}
queue_notify (self, &self->priv->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->priv->credentials_cache == NULL)
{
g_set_error (error,
GOA_IDENTITY_ERROR,
GOA_IDENTITY_ERROR_CREDENTIALS_UNAVAILABLE,
_("Could not renew identity: Not signed in"));
goto out;
}
error_code = krb5_cc_get_principal (self->priv->kerberos_context,
self->priv->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: "));
goto out;
}
name = goa_kerberos_identity_get_principal_name (self);
error_code = krb5_get_renewed_creds (self->priv->kerberos_context,
&new_credentials,
principal,
self->priv->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->priv->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->priv->credentials_cache != NULL)
{
error_code = krb5_cc_destroy (self->priv->kerberos_context,
self->priv->credentials_cache);
self->priv->credentials_cache = NULL;
}
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: "));
return FALSE;
}
return TRUE;
}
GoaIdentity *
goa_kerberos_identity_new (krb5_context context, krb5_ccache cache, GError **error)
{
GoaKerberosIdentity *self;
self = GOA_KERBEROS_IDENTITY (g_object_new (GOA_TYPE_KERBEROS_IDENTITY, NULL));
krb5_cc_dup (context, cache, &self->priv->credentials_cache);
self->priv->kerberos_context = context;
error = NULL;
if (!g_initable_init (G_INITABLE (self), NULL, error))
{
g_object_unref (self);
return NULL;
}
return GOA_IDENTITY (self);
}