/* realmd -- Realm configuration service
*
* Copyright 2012 Red Hat Inc
*
* This program 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 licence or (at
* your option) any later version.
*
* See the included COPYING file for more information.
*
* Author: Stef Walter <stefw@gnome.org>
*/
#include "config.h"
#include "realm-daemon.h"
#include "realm-dbus-constants.h"
#include "realm-dbus-generated.h"
#include "realm-invocation.h"
#include <glib.h>
#include <glib/gi18n.h>
#include <polkit/polkit.h>
#include <string.h>
typedef struct {
const gchar *interface;
const gchar *method;
const gchar *action_id;
int options_param;
} InvocationMethod;
static InvocationMethod invocation_methods[] = {
{ REALM_DBUS_PROVIDER_INTERFACE, "Discover", "org.freedesktop.realmd.discover-realm", 2 },
{ REALM_DBUS_KERBEROS_MEMBERSHIP_INTERFACE, "Join", "org.freedesktop.realmd.configure-realm", 2 },
{ REALM_DBUS_KERBEROS_MEMBERSHIP_INTERFACE, "Leave", "org.freedesktop.realmd.deconfigure-realm", 2 },
{ REALM_DBUS_REALM_INTERFACE, "Deconfigure", "org.freedesktop.realmd.deconfigure-realm", 1 },
{ REALM_DBUS_REALM_INTERFACE, "ChangeLoginPolicy", "org.freedesktop.realmd.login-policy", 4 },
};
typedef struct {
GCancellable *cancellable;
gchar *identifier;
const gchar *operation;
const InvocationMethod *method;
} InvocationData;
typedef struct {
guint watch;
gchar *locale;
} InvocationClient;
static const GVariantType *asv_type = NULL;
static GObject *current_invocation = NULL;
static GQuark invocation_data_quark = 0;
/* These are protected by the mutex */
static GHashTable *invocation_clients = NULL;
static GHashTable *cancellables = NULL;
static PolkitAuthority *polkit_authority = NULL;
G_LOCK_DEFINE_STATIC (invocations);
#define PEER ":peer"
static void
on_cancellable_gone (gpointer user_data,
GObject *where_the_object_was)
{
gchar *invocation_id = user_data;
G_LOCK (invocations);
g_hash_table_remove (cancellables, invocation_id);
G_UNLOCK (invocations);
}
static void
invocation_data_free (void *data)
{
InvocationData *invo = data;
if (invo->cancellable)
g_object_unref (invo->cancellable);
g_free (invo->identifier);
g_free (invo);
}
static void
on_client_vanished (GDBusConnection *connection,
const gchar *name,
gpointer user_data)
{
g_debug ("client gone away: %s", name);
G_LOCK (invocations);
realm_daemon_release (name);
g_hash_table_remove (invocation_clients, name);
G_UNLOCK (invocations);
}
static InvocationClient *
lookup_or_register_client (const gchar *sender)
{
InvocationClient *client;
g_assert (sender != NULL);
client = g_hash_table_lookup (invocation_clients, sender);
if (!client) {
client = g_new0 (InvocationClient, 1);
if (!g_str_equal (sender, PEER)) {
client->watch = g_bus_watch_name (G_BUS_TYPE_SYSTEM, sender,
G_BUS_NAME_WATCHER_FLAGS_NONE,
NULL, on_client_vanished, NULL, NULL);
g_debug ("client using service: %s", sender);
}
g_hash_table_insert (invocation_clients, g_strdup (sender), client);
realm_daemon_hold (sender);
}
return client;
}
static gboolean
on_idle_setup_client (gpointer user_data)
{
gchar *sender = user_data;
G_LOCK (invocations);
lookup_or_register_client (sender);
G_UNLOCK (invocations);
return FALSE; /* don't call again */
}
static gchar *
extract_operation (GDBusMessage *message,
const InvocationMethod *method)
{
gchar *operation = NULL;
GVariant *params;
GVariant *options;
gint idx;
if (!method->options_param)
return NULL;
params = g_dbus_message_get_body (message);
idx = method->options_param - 1;
if (g_variant_n_children (params) <= idx)
return NULL;
options = g_variant_get_child_value (params, idx);
if (g_variant_is_of_type (options, asv_type)) {
if (!g_variant_lookup (options, REALM_DBUS_OPTION_OPERATION, "&s", &operation))
operation = NULL;
}
g_variant_unref (options);
return operation;
}
static void
prepare_method_in_dbus_worker_thread (GDBusMessage *message,
const gchar *sender)
{
const InvocationMethod *invo_method;
InvocationData *invo = NULL;
const gchar *interface;
const gchar *method;
gchar *operation;
gchar *key;
gint i;
g_assert (sender != NULL);
interface = g_dbus_message_get_interface (message);
/* Do no processing these interfaces */
if (g_str_equal (interface, REALM_DBUS_SERVICE_INTERFACE) ||
g_str_equal (interface, DBUS_PROPERTIES_INTERFACE) ||
g_str_equal (interface, DBUS_INTROSPECTABLE_INTERFACE) ||
g_str_equal (interface, DBUS_PEER_INTERFACE))
return;
method = g_dbus_message_get_member (message);
invo_method = NULL;
for (i = 0; i < G_N_ELEMENTS (invocation_methods); i++) {
if (g_str_equal (invocation_methods[i].interface, interface) &&
g_str_equal (invocation_methods[i].method, method)) {
invo_method = invocation_methods + i;
break;
}
}
/* Find the operation id for this message */
if (invo_method) {
invo = g_new0 (InvocationData, 1);
invo->method = invo_method;
operation = extract_operation (message, invo_method);
if (operation) {
g_debug ("Using '%s' operation for method '%s' invocation on '%s' interface",
operation, method, interface);
invo->identifier = g_strdup_printf ("%s %s", sender, operation);
invo->operation = strchr (invo->identifier, ' ');
g_assert (invo->operation != NULL);
invo->operation++;
} else {
invo->identifier = g_strdup (sender);
invo->operation = NULL;
}
}
g_object_set_qdata_full (G_OBJECT (message), invocation_data_quark,
invo, invocation_data_free);
G_LOCK (invocations);
/* Prepare a cancellable if desired */
if (invo && invo->operation) {
invo->cancellable = g_hash_table_lookup (cancellables, invo->identifier);
if (invo->cancellable == NULL) {
invo->cancellable = g_cancellable_new ();
key = g_strdup (invo->identifier);
g_hash_table_insert (cancellables, key, invo->cancellable);
g_debug ("Registered cancellable for operation '%s'", operation);
g_object_weak_ref (G_OBJECT (invo->cancellable), on_cancellable_gone, key);
} else {
g_object_ref (invo->cancellable);
}
}
/* Setup a client later if necessary */
if (g_hash_table_lookup (invocation_clients, sender) == NULL)
g_idle_add_full (G_PRIORITY_DEFAULT, on_idle_setup_client, g_strdup (sender), g_free);
G_UNLOCK (invocations);
}
static GDBusMessage *
on_connection_filter (GDBusConnection *connection,
GDBusMessage *message,
gboolean incoming,
gpointer user_data)
{
const gchar *own_name = user_data;
GDBusMessageType type;
const gchar *sender;
/* Each time we see an incoming function call, keep the service alive */
if (incoming) {
type = g_dbus_message_get_message_type (message);
if (type == G_DBUS_MESSAGE_TYPE_METHOD_CALL) {
sender = g_dbus_message_get_sender (message);
g_return_val_if_fail (sender != NULL || realm_daemon_is_dbus_peer (), message);
if (sender == NULL)
sender = PEER;
if (!own_name || g_strcmp0 (own_name, sender) != 0)
prepare_method_in_dbus_worker_thread (message, sender);
}
}
return message;
}
static gboolean
on_service_release (RealmDbusService *object,
GDBusMethodInvocation *invocation)
{
const char *sender;
sender = g_dbus_method_invocation_get_sender (invocation);
g_return_val_if_fail (sender != NULL || realm_daemon_is_dbus_peer (), FALSE);
if (sender == NULL)
sender = PEER;
g_debug ("explicitly releasing service: %s", sender);
G_LOCK (invocations);
g_hash_table_remove (invocation_clients, sender);
realm_daemon_release (sender);
G_UNLOCK (invocations);
realm_dbus_service_complete_release (object, invocation);
return TRUE;
}
static gboolean
on_service_cancel (RealmDbusService *object,
GDBusMethodInvocation *invocation,
const gchar *operation)
{
GCancellable *cancellable;
gchar *identifier;
const gchar *sender;
sender = g_dbus_method_invocation_get_sender (invocation);
g_return_val_if_fail (sender != NULL || realm_daemon_is_dbus_peer (), FALSE);
if (sender == NULL)
sender = PEER;
G_LOCK (invocations);
identifier = g_strdup_printf ("%s %s", sender, operation);
cancellable = g_hash_table_lookup (cancellables, identifier);
g_free (identifier);
if (cancellable)
g_object_ref (cancellable);
G_UNLOCK (invocations);
realm_dbus_service_complete_cancel (object, invocation);
if (cancellable) {
g_debug ("Cancelling operation '%s'", operation);
g_cancellable_cancel (cancellable);
g_object_unref (cancellable);
} else {
g_debug ("Nothing to cancel for '%s'", operation);
}
return TRUE;
}
static gboolean
on_service_set_locale (RealmDbusService *object,
GDBusMethodInvocation *invocation,
const gchar *arg_locale)
{
InvocationClient *client;
const gchar *sender;
sender = g_dbus_method_invocation_get_sender (invocation);
g_return_val_if_fail (sender != NULL || realm_daemon_is_dbus_peer (), FALSE);
if (sender == NULL)
sender = PEER;
G_LOCK (invocations);
client = lookup_or_register_client (sender);
g_free (client->locale);
client->locale = g_strdup (arg_locale);
G_UNLOCK (invocations);
realm_dbus_service_complete_set_locale (object, invocation);
return TRUE;
}
static void
unwatch_and_free_client (gpointer data)
{
InvocationClient *client = data;
g_assert (data != NULL);
if (client->watch)
g_bus_unwatch_name (client->watch);
g_free (client->locale);
g_free (client);
realm_daemon_poke ();
}
void
realm_invocation_initialize (GDBusConnection *connection)
{
RealmDbusService *service;
const gchar *self_name;
invocation_data_quark = g_quark_from_static_string ("realmd-invocation-data");
asv_type = G_VARIANT_TYPE ("a{sv}");
cancellables = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
invocation_clients = g_hash_table_new_full (g_str_hash, g_str_equal,
g_free, unwatch_and_free_client);
/* Add a filter which keeps service alive */
self_name = g_dbus_connection_get_unique_name (connection);
g_dbus_connection_add_filter (connection, on_connection_filter,
g_strdup (self_name), g_free);
service = realm_dbus_service_skeleton_new ();
g_signal_connect (service, "handle-release", G_CALLBACK (on_service_release), NULL);
g_signal_connect (service, "handle-set-locale", G_CALLBACK (on_service_set_locale), NULL);
g_signal_connect (service, "handle-cancel", G_CALLBACK (on_service_cancel), NULL);
g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (service),
connection, REALM_DBUS_SERVICE_PATH, NULL);
}
void
realm_invocation_cleanup (void)
{
g_hash_table_destroy (cancellables);
cancellables = NULL;
g_hash_table_destroy (invocation_clients);
invocation_clients = NULL;
g_clear_object (&polkit_authority);
}
static InvocationData *
lookup_invocation_data (GDBusMethodInvocation *invocation)
{
InvocationData *invo;
GDBusMessage *message;
invo = g_object_get_qdata (G_OBJECT (invocation), invocation_data_quark);
if (invo == NULL) {
message = g_dbus_method_invocation_get_message (invocation);
invo = g_object_get_qdata (G_OBJECT (message), invocation_data_quark);
if (invo != NULL)
g_object_set_qdata (G_OBJECT (invocation), invocation_data_quark, invo);
}
return invo;
}
static gboolean
check_dbus_action (const gchar *sender,
const gchar *action_id)
{
PolkitAuthorizationResult *result;
PolkitAuthority *authority;
PolkitSubject *subject;
GError *error = NULL;
gboolean ret;
/* If we're a dbus peer, just allow all calls */
if (realm_daemon_is_dbus_peer ())
return TRUE;
g_return_val_if_fail (sender != NULL, FALSE);
g_return_val_if_fail (action_id != NULL, FALSE);
G_LOCK (invocations);
authority = polkit_authority ? g_object_ref (polkit_authority) : NULL;
G_UNLOCK (invocations);
if (!authority) {
authority = polkit_authority_get_sync (NULL, &error);
if (authority == NULL) {
g_warning ("failure to get polkit authority: %s", error->message);
g_error_free (error);
return FALSE;
}
G_LOCK (invocations);
if (polkit_authority == NULL) {
polkit_authority = g_object_ref (authority);
} else {
g_object_unref (authority);
authority = g_object_ref (polkit_authority);
}
G_UNLOCK (invocations);
}
/* do authorization async */
subject = polkit_system_bus_name_new (sender);
result = polkit_authority_check_authorization_sync (authority, subject, action_id, NULL,
POLKIT_CHECK_AUTHORIZATION_FLAGS_ALLOW_USER_INTERACTION, NULL, &error);
g_object_unref (authority);
g_object_unref (subject);
/* failed */
if (result == NULL) {
g_warning ("couldn't check polkit authorization%s%s",
error ? ": " : "", error ? error->message : "");
g_error_free (error);
return FALSE;
}
ret = polkit_authorization_result_get_is_authorized (result);
g_object_unref (result);
return ret;
}
gboolean
realm_invocation_authorize (GDBusMethodInvocation *invocation)
{
const gchar *action_id = NULL;
const gchar *sender;
InvocationData *invo;
gboolean ret;
invo = lookup_invocation_data (invocation);
if (invo != NULL && invo->method != NULL)
action_id = invo->method->action_id;
if (action_id == NULL) {
g_warning ("Couldn't authorize unregistered method '%s' on interface '%s'",
g_dbus_method_invocation_get_method_name (invocation),
g_dbus_method_invocation_get_interface_name (invocation));
ret = FALSE;
} else {
sender = g_dbus_method_invocation_get_sender (invocation);
ret = check_dbus_action (sender, action_id);
}
if (ret == FALSE) {
g_debug ("rejecting access to method '%s' on interface '%s' at %s",
g_dbus_method_invocation_get_method_name (invocation),
g_dbus_method_invocation_get_interface_name (invocation),
g_dbus_method_invocation_get_object_path (invocation));
g_dbus_method_invocation_return_dbus_error (invocation, REALM_DBUS_ERROR_NOT_AUTHORIZED,
_("Not authorized to perform this action"));
}
return ret;
}
GCancellable *
realm_invocation_get_cancellable (GDBusMethodInvocation *invocation)
{
InvocationData *invo;
g_return_val_if_fail (invocation != NULL, NULL);
invo = lookup_invocation_data (invocation);
return invo ? invo->cancellable : NULL;
}
const gchar *
realm_invocation_get_operation (GDBusMethodInvocation *invocation)
{
InvocationData *invo;
g_return_val_if_fail (invocation != NULL, NULL);
invo = lookup_invocation_data (invocation);
return invo ? invo->operation : NULL;
}
const gchar *
realm_invocation_get_key (GDBusMethodInvocation *invocation)
{
InvocationData *invo;
g_return_val_if_fail (invocation != NULL, NULL);
invo = lookup_invocation_data (invocation);
return invo ? invo->identifier : NULL;
}
static void
on_invocation_gone (gpointer unused,
GObject *where_the_object_was)
{
g_warning ("a GDBusMethodInvocation was released but the invocation was "
"registered as part of a realm_invocation_lock_daemon()");
g_assert (where_the_object_was == current_invocation);
current_invocation = NULL;
}
gboolean
realm_invocation_lock_daemon (GDBusMethodInvocation *invocation)
{
g_return_val_if_fail (G_IS_DBUS_METHOD_INVOCATION (invocation), FALSE);
if (current_invocation)
return FALSE;
current_invocation = G_OBJECT (invocation);
g_object_weak_ref (current_invocation, on_invocation_gone, NULL);
/* Hold the daemon up while action */
realm_daemon_hold ("current-invocation");
return TRUE;
}
void
realm_invocation_unlock_daemon (GDBusMethodInvocation *invocation)
{
g_return_if_fail (G_IS_DBUS_METHOD_INVOCATION (invocation));
if (current_invocation != G_OBJECT (invocation)) {
g_warning ("trying to realm_invocation_unlock_daemon() with an invocation "
"that is not registered as the current locked action.");
return;
}
g_object_weak_unref (current_invocation, on_invocation_gone, NULL);
current_invocation = NULL;
/* Matches the hold in realm_invocation_lock_daemon() */
if (!realm_daemon_release ("current-invocation"))
g_warn_if_reached ();
}