diff options
Diffstat (limited to 'src/settings/nm-agent-manager.c')
-rw-r--r-- | src/settings/nm-agent-manager.c | 1390 |
1 files changed, 1390 insertions, 0 deletions
diff --git a/src/settings/nm-agent-manager.c b/src/settings/nm-agent-manager.c new file mode 100644 index 000000000..dbc29cae4 --- /dev/null +++ b/src/settings/nm-agent-manager.c @@ -0,0 +1,1390 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: t; c-basic-offset: 4 -*- */ +/* NetworkManager -- Network link manager + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Copyright (C) 2010 - 2011 Red Hat, Inc. + */ + +#include <config.h> +#include <string.h> +#include <ctype.h> +#include <pwd.h> + +#include <glib.h> +#include <dbus/dbus-glib.h> +#include <dbus/dbus-glib-lowlevel.h> + +#include "NetworkManager.h" +#include "nm-logging.h" +#include "nm-agent-manager.h" +#include "nm-secret-agent.h" +#include "nm-manager-auth.h" +#include "nm-dbus-glib-types.h" +#include "nm-polkit-helpers.h" +#include "nm-manager-auth.h" +#include "nm-setting-vpn.h" +#include "nm-setting-connection.h" + +G_DEFINE_TYPE (NMAgentManager, nm_agent_manager, G_TYPE_OBJECT) + +#define NM_AGENT_MANAGER_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o), \ + NM_TYPE_AGENT_MANAGER, \ + NMAgentManagerPrivate)) + +typedef struct { + gboolean disposed; + + NMDBusManager *dbus_mgr; + NMSessionMonitor *session_monitor; + PolkitAuthority *authority; + + /* Hashed by owner name, not identifier, since two agents in different + * sessions can use the same identifier. + */ + GHashTable *agents; + + GHashTable *requests; +} NMAgentManagerPrivate; + +typedef struct _Request Request; + +static void request_add_agent (Request *req, + NMSecretAgent *agent, + NMSessionMonitor *session_monitor); + +static void request_remove_agent (Request *req, NMSecretAgent *agent); + +static void impl_agent_manager_register (NMAgentManager *self, + const char *identifier, + DBusGMethodInvocation *context); + +static void impl_agent_manager_unregister (NMAgentManager *self, + DBusGMethodInvocation *context); + +#include "nm-agent-manager-glue.h" + +/********************************************************************/ + +#define NM_AGENT_MANAGER_ERROR (nm_agent_manager_error_quark ()) +#define NM_TYPE_AGENT_MANAGER_ERROR (nm_agent_manager_error_get_type ()) + +typedef enum { + NM_AGENT_MANAGER_ERROR_SENDER_UNKNOWN = 0, + NM_AGENT_MANAGER_ERROR_PERMISSION_DENIED, + NM_AGENT_MANAGER_ERROR_SESSION_NOT_FOUND, + NM_AGENT_MANAGER_ERROR_INVALID_IDENTIFIER, + NM_AGENT_MANAGER_ERROR_NOT_REGISTERED, + NM_AGENT_MANAGER_ERROR_INTERNAL_ERROR, + NM_AGENT_MANAGER_ERROR_NO_SECRETS +} NMAgentManagerError; + +static GQuark +nm_agent_manager_error_quark (void) +{ + static GQuark ret = 0; + + if (G_UNLIKELY (ret == 0)) + ret = g_quark_from_static_string ("nm-agent-manager-error"); + return ret; +} + +#define ENUM_ENTRY(NAME, DESC) { NAME, "" #NAME "", DESC } + +static GType +nm_agent_manager_error_get_type (void) +{ + static GType etype = 0; + + if (etype == 0) { + static const GEnumValue values[] = { + /* Unable to determine caller's sender or UID */ + ENUM_ENTRY (NM_AGENT_MANAGER_ERROR_SENDER_UNKNOWN, "SenderUnknown"), + /* Permission for some operation was denied */ + ENUM_ENTRY (NM_AGENT_MANAGER_ERROR_PERMISSION_DENIED, "PermissionDenied"), + /* The caller's session could not be determined */ + ENUM_ENTRY (NM_AGENT_MANAGER_ERROR_SESSION_NOT_FOUND, "SessionNotFound"), + /* The identifier was invalid */ + ENUM_ENTRY (NM_AGENT_MANAGER_ERROR_INVALID_IDENTIFIER, "InvalidIdentifier"), + /* Request was not from a registered agent */ + ENUM_ENTRY (NM_AGENT_MANAGER_ERROR_NOT_REGISTERED, "NotRegistered"), + /* Some internal error occurred */ + ENUM_ENTRY (NM_AGENT_MANAGER_ERROR_INTERNAL_ERROR, "InternalError"), + /* No secrets were available */ + ENUM_ENTRY (NM_AGENT_MANAGER_ERROR_NO_SECRETS, "NoSecrets"), + { 0, 0, 0 } + }; + + etype = g_enum_register_static ("NMAgentManagerError", values); + } + return etype; +} + +/*************************************************************/ + +static gboolean +remove_agent (NMAgentManager *self, const char *owner) +{ + NMAgentManagerPrivate *priv = NM_AGENT_MANAGER_GET_PRIVATE (self); + NMSecretAgent *agent; + GHashTableIter iter; + gpointer data; + + g_return_val_if_fail (owner != NULL, FALSE); + + /* Make sure this agent has already registered */ + agent = g_hash_table_lookup (priv->agents, owner); + if (!agent) + return FALSE; + + nm_log_dbg (LOGD_AGENTS, "(%s) agent unregistered", + nm_secret_agent_get_description (agent)); + + /* Remove this agent to any in-progress secrets requests */ + g_hash_table_iter_init (&iter, priv->requests); + while (g_hash_table_iter_next (&iter, NULL, &data)) + request_remove_agent ((Request *) data, agent); + + /* And dispose of the agent */ + g_hash_table_remove (priv->agents, owner); + return TRUE; +} + +/*************************************************************/ + +static gboolean +validate_identifier (const char *identifier, GError **error) +{ + const char *p = identifier; + size_t id_len; + + if (!identifier) { + g_set_error_literal (error, + NM_AGENT_MANAGER_ERROR, + NM_AGENT_MANAGER_ERROR_INVALID_IDENTIFIER, + "No identifier was given"); + return FALSE; + } + + /* Length between 3 and 255 characters inclusive */ + id_len = strlen (identifier); + if (id_len < 3 || id_len > 255) { + g_set_error_literal (error, + NM_AGENT_MANAGER_ERROR, + NM_AGENT_MANAGER_ERROR_INVALID_IDENTIFIER, + "Identifier length not between 3 and 255 characters (inclusive)"); + return FALSE; + } + + if ((identifier[0] == '.') || (identifier[id_len - 1] == '.')) { + g_set_error_literal (error, + NM_AGENT_MANAGER_ERROR, + NM_AGENT_MANAGER_ERROR_INVALID_IDENTIFIER, + "Identifier must not start or end with '.'"); + return FALSE; + } + + /* FIXME: do complete validation here */ + while (p && *p) { + if (!isalnum (*p) && (*p != '_') && (*p != '-') && (*p != '.')) { + g_set_error (error, + NM_AGENT_MANAGER_ERROR, + NM_AGENT_MANAGER_ERROR_INVALID_IDENTIFIER, + "Identifier contains invalid character '%c'", *p); + return FALSE; + } + + if ((*p == '.') && (*(p + 1) == '.')) { + g_set_error_literal (error, + NM_AGENT_MANAGER_ERROR, + NM_AGENT_MANAGER_ERROR_INVALID_IDENTIFIER, + "Identifier contains two '.' characters in sequence"); + return FALSE; + } + p++; + } + + return TRUE; +} + +static void +impl_agent_manager_register (NMAgentManager *self, + const char *identifier, + DBusGMethodInvocation *context) +{ + NMAgentManagerPrivate *priv = NM_AGENT_MANAGER_GET_PRIVATE (self); + char *error_desc = NULL, *sender = NULL; + gulong sender_uid = G_MAXULONG; + GError *error = NULL, *local = NULL; + NMSecretAgent *agent; + GHashTableIter iter; + gpointer data; + + if (!nm_auth_get_caller_uid (context, + priv->dbus_mgr, + &sender_uid, + &error_desc)) { + error = g_error_new_literal (NM_AGENT_MANAGER_ERROR, + NM_AGENT_MANAGER_ERROR_SENDER_UNKNOWN, + error_desc); + g_free (error_desc); + goto done; + } + + if (!nm_session_monitor_uid_has_session (priv->session_monitor, + sender_uid, + NULL, + &local)) { + error = g_error_new_literal (NM_AGENT_MANAGER_ERROR, + NM_AGENT_MANAGER_ERROR_SESSION_NOT_FOUND, + local && local->message ? local->message : "Session not found"); + goto done; + } + + sender = dbus_g_method_get_sender (context); + if (!sender) { + error = g_error_new_literal (NM_AGENT_MANAGER_ERROR, + NM_AGENT_MANAGER_ERROR_SENDER_UNKNOWN, + "Failed to get D-Bus request sender"); + goto done; + } + + /* Validate the identifier */ + if (!validate_identifier (identifier, &error)) + goto done; + + /* Success, add the new agent */ + agent = nm_secret_agent_new (priv->dbus_mgr, sender, identifier, sender_uid); + if (!agent) { + error = g_error_new_literal (NM_AGENT_MANAGER_ERROR, + NM_AGENT_MANAGER_ERROR_INTERNAL_ERROR, + "Failed to initialize the agent"); + goto done; + } + + g_hash_table_insert (priv->agents, g_strdup (sender), agent); + nm_log_dbg (LOGD_AGENTS, "(%s) agent registered", + nm_secret_agent_get_description (agent)); + dbus_g_method_return (context); + + /* Add this agent to any in-progress secrets requests */ + g_hash_table_iter_init (&iter, priv->requests); + while (g_hash_table_iter_next (&iter, NULL, &data)) + request_add_agent ((Request *) data, agent, priv->session_monitor); + +done: + if (error) + dbus_g_method_return_error (context, error); + g_clear_error (&error); + g_clear_error (&local); + g_free (sender); +} + +static void +impl_agent_manager_unregister (NMAgentManager *self, + DBusGMethodInvocation *context) +{ + GError *error = NULL; + char *sender = NULL; + + sender = dbus_g_method_get_sender (context); + if (!sender) { + error = g_error_new_literal (NM_AGENT_MANAGER_ERROR, + NM_AGENT_MANAGER_ERROR_SENDER_UNKNOWN, + "Failed to get D-Bus request sender"); + goto done; + } + + /* Found the agent, unregister and remove it */ + if (!remove_agent (self, sender)) { + error = g_error_new_literal (NM_AGENT_MANAGER_ERROR, + NM_AGENT_MANAGER_ERROR_NOT_REGISTERED, + "Caller is not registered as an Agent"); + goto done; + } + + dbus_g_method_return (context); + +done: + if (error) + dbus_g_method_return_error (context, error); + g_clear_error (&error); + g_free (sender); +} + +/*************************************************************/ + +typedef void (*RequestCompleteFunc) (Request *req, + GHashTable *secrets, + const char *agent_dbus_owner, + const char *agent_username, + gboolean agent_has_modify, + GError *error, + gpointer user_data); +typedef void (*RequestNextFunc) (Request *req); +typedef void (*RequestCancelFunc) (Request *req); + +struct _Request { + guint32 reqid; + PolkitAuthority *authority; + NMAuthChain *chain; + + NMConnection *connection; + gboolean filter_by_uid; + gulong uid_filter; + char *setting_name; + NMSettingsGetSecretsFlags flags; + char *hint; + + /* Current agent being asked for secrets */ + NMSecretAgent *current; + gconstpointer current_call_id; + gboolean current_has_modify; + + /* Stores the sorted list of NMSecretAgents which will be asked for secrets */ + GSList *pending; + + /* Stores the list of NMSecretAgent hashes that we've already + * asked for secrets, so that we don't ask the same agent twice + * if it quits and re-registers during this secrets request. + */ + GSList *asked; + + guint32 idle_id; + + GHashTable *existing_secrets; + + NMAgentSecretsResultFunc callback; + gpointer callback_data; + gpointer other_data2; + gpointer other_data3; + + RequestCancelFunc cancel_callback; + RequestNextFunc next_callback; + RequestCompleteFunc complete_callback; + gpointer complete_callback_data; +}; + +static guint32 next_req_id = 1; + +static Request * +request_new_get (NMConnection *connection, + PolkitAuthority *authority, + gboolean filter_by_uid, + gulong uid_filter, + GHashTable *existing_secrets, + const char *setting_name, + NMSettingsGetSecretsFlags flags, + const char *hint, + NMAgentSecretsResultFunc callback, + gpointer callback_data, + gpointer other_data2, + gpointer other_data3, + RequestCompleteFunc complete_callback, + gpointer complete_callback_data, + RequestNextFunc next_callback, + RequestCancelFunc cancel_callback) +{ + Request *req; + + req = g_malloc0 (sizeof (Request)); + req->reqid = next_req_id++; + req->connection = g_object_ref (connection); + req->authority = g_object_ref (authority); + req->filter_by_uid = filter_by_uid; + req->uid_filter = uid_filter; + if (existing_secrets) + req->existing_secrets = g_hash_table_ref (existing_secrets); + req->setting_name = g_strdup (setting_name); + req->flags = flags; + req->hint = g_strdup (hint); + req->callback = callback; + req->callback_data = callback_data; + req->other_data2 = other_data2; + req->other_data3 = other_data3; + req->complete_callback = complete_callback; + req->complete_callback_data = complete_callback_data; + req->next_callback = next_callback; + req->cancel_callback = cancel_callback; + + return req; +} + +static Request * +request_new_other (NMConnection *connection, + gboolean filter_by_uid, + gulong uid_filter, + RequestCompleteFunc complete_callback, + gpointer complete_callback_data, + RequestNextFunc next_callback) +{ + Request *req; + + req = g_malloc0 (sizeof (Request)); + req->reqid = next_req_id++; + req->connection = g_object_ref (connection); + req->filter_by_uid = filter_by_uid; + req->uid_filter = uid_filter; + req->complete_callback = complete_callback; + req->complete_callback_data = complete_callback_data; + req->next_callback = next_callback; + + return req; +} + +static void +request_free (Request *req) +{ + if (req->idle_id) + g_source_remove (req->idle_id); + + if (req->cancel_callback) + req->cancel_callback (req); + + g_slist_free (req->pending); + g_slist_free (req->asked); + g_object_unref (req->connection); + g_free (req->setting_name); + g_free (req->hint); + if (req->existing_secrets) + g_hash_table_unref (req->existing_secrets); + if (req->chain) + nm_auth_chain_unref (req->chain); + if (req->authority) + g_object_unref (req->authority); + memset (req, 0, sizeof (Request)); + g_free (req); +} + +static void +req_complete_success (Request *req, + GHashTable *secrets, + const char *agent_dbus_owner, + const char *agent_uname, + gboolean agent_has_modify) +{ + req->complete_callback (req, + secrets, + agent_dbus_owner, + agent_uname, + agent_has_modify, + NULL, + req->complete_callback_data); +} + +static void +req_complete_error (Request *req, GError *error) +{ + req->complete_callback (req, NULL, NULL, NULL, FALSE, error, req->complete_callback_data); +} + +static gint +agent_compare_func (NMSecretAgent *a, NMSecretAgent *b, gpointer user_data) +{ + NMSessionMonitor *session_monitor = NM_SESSION_MONITOR (user_data); + gboolean a_active, b_active; + + if (a && !b) + return -1; + else if (a == b) + return 0; + else if (!a && b) + return 1; + + /* Prefer agents in active sessions */ + a_active = nm_session_monitor_uid_active (session_monitor, + nm_secret_agent_get_owner_uid (a), + NULL); + b_active = nm_session_monitor_uid_active (session_monitor, + nm_secret_agent_get_owner_uid (b), + NULL); + if (a_active && !b_active) + return -1; + else if (a_active == b_active) + return 0; + else if (!a_active && b_active) + return 1; + + return 0; +} + +static void +request_add_agent (Request *req, + NMSecretAgent *agent, + NMSessionMonitor *session_monitor) +{ + uid_t agent_uid; + + g_return_if_fail (req != NULL); + g_return_if_fail (agent != NULL); + + if (g_slist_find (req->asked, GUINT_TO_POINTER (nm_secret_agent_get_hash (agent)))) + return; + + /* Ensure the caller's username exists in the connection's permissions, + * or that the permissions is empty (ie, visible by everyone). + */ + agent_uid = nm_secret_agent_get_owner_uid (agent); + if (0 != agent_uid) { + if (!nm_auth_uid_in_acl (req->connection, session_monitor, agent_uid, NULL)) { + nm_log_dbg (LOGD_AGENTS, "(%s) agent ignored for secrets request %p/%s (not in ACL)", + nm_secret_agent_get_description (agent), + req, req->setting_name); + /* Connection not visible to this agent's user */ + return; + } + /* Caller is allowed to manipulate this connection */ + } + + /* If the request should filter agents by UID, do that now */ + if (req->filter_by_uid && (agent_uid != req->uid_filter)) { + nm_log_dbg (LOGD_AGENTS, "(%s) agent ignored for secrets request %p/%s " + "(uid %d not required %ld)", + nm_secret_agent_get_description (agent), + req, req->setting_name, agent_uid, req->uid_filter); + return; + } + + nm_log_dbg (LOGD_AGENTS, "(%s) agent allowed for secrets request %p/%s", + nm_secret_agent_get_description (agent), + req, req->setting_name); + + /* Add this agent to the list, preferring active sessions */ + req->pending = g_slist_insert_sorted_with_data (req->pending, + agent, + (GCompareDataFunc) agent_compare_func, + session_monitor); +} + +static void +request_add_agents (NMAgentManager *self, Request *req) +{ + NMAgentManagerPrivate *priv = NM_AGENT_MANAGER_GET_PRIVATE (self); + GHashTableIter iter; + gpointer data; + + g_hash_table_iter_init (&iter, priv->agents); + while (g_hash_table_iter_next (&iter, NULL, &data)) + request_add_agent (req, NM_SECRET_AGENT (data), priv->session_monitor); +} + +static void +request_remove_agent (Request *req, NMSecretAgent *agent) +{ + gboolean try_next = FALSE; + const char *detail = ""; + + g_return_if_fail (req != NULL); + g_return_if_fail (agent != NULL); + + /* If this agent is being asked right now, cancel the request */ + if (agent == req->current) { + req->cancel_callback (req); + req->current_has_modify = FALSE; + req->current = NULL; + req->current_call_id = NULL; + try_next = TRUE; + detail = " current"; + } + + nm_log_dbg (LOGD_AGENTS, "(%s)%s agent removed from secrets request %p/%s", + nm_secret_agent_get_description (agent), + detail, req, req->setting_name); + + req->pending = g_slist_remove (req->pending, agent); + + if (try_next) { + /* If the agent serving the in-progress secrets request went away then + * we need to send the request to the next agent. + */ + req->next_callback (req); + } +} + +static gboolean +next_generic (Request *req, const char *detail) +{ + GError *error = NULL; + gboolean success = FALSE; + + if (req->pending == NULL) { + /* No more secret agents are available to fulfill this secrets request */ + error = g_error_new_literal (NM_AGENT_MANAGER_ERROR, + NM_AGENT_MANAGER_ERROR_NO_SECRETS, + "No agents were available for this request."); + req_complete_error (req, error); + g_error_free (error); + } else { + /* Send a secrets request to the next agent */ + req->current_has_modify = FALSE; + req->current = req->pending->data; + req->pending = g_slist_remove (req->pending, req->current); + + nm_log_dbg (LOGD_AGENTS, "(%s) agent %s secrets for request %p/%s", + nm_secret_agent_get_description (req->current), + detail, req, req->setting_name); + success = TRUE; + } + + return success; +} + +static gboolean +start_generic (gpointer user_data) +{ + Request *req = user_data; + + req->idle_id = 0; + req->next_callback (req); + return FALSE; +} + + +/*************************************************************/ + +static void +get_done_cb (NMSecretAgent *agent, + gconstpointer call_id, + GHashTable *secrets, + GError *error, + gpointer user_data) +{ + Request *req = user_data; + GHashTable *setting_secrets; + const char *agent_dbus_owner; + gboolean agent_has_modify; + struct passwd *pw; + char *agent_uname = NULL; + + g_return_if_fail (call_id == req->current_call_id); + + agent_has_modify = req->current_has_modify; + req->current_has_modify = FALSE; + req->current = NULL; + req->current_call_id = NULL; + + if (error) { + nm_log_dbg (LOGD_AGENTS, "(%s) agent failed secrets request %p/%s: (%d) %s", + nm_secret_agent_get_description (agent), + req, req->setting_name, + error ? error->code : -1, + (error && error->message) ? error->message : "(unknown)"); + + /* Try the next agent */ + req->next_callback (req); + return; + } + + /* Ensure the setting we wanted secrets for got returned and has something in it */ + setting_secrets = g_hash_table_lookup (secrets, req->setting_name); + if (!setting_secrets || !g_hash_table_size (setting_secrets)) { + nm_log_dbg (LOGD_AGENTS, "(%s) agent returned no secrets for request %p/%s", + nm_secret_agent_get_description (agent), + req, req->setting_name); + + /* Try the next agent */ + req->next_callback (req); + return; + } + + nm_log_dbg (LOGD_AGENTS, "(%s) agent returned secrets for request %p/%s", + nm_secret_agent_get_description (agent), + req, req->setting_name); + + /* Get the agent's username */ + pw = getpwuid (nm_secret_agent_get_owner_uid (agent)); + if (pw && strlen (pw->pw_name)) { + /* Needs to be UTF-8 valid since it may be pushed through D-Bus */ + if (g_utf8_validate (pw->pw_name, -1, NULL)) + agent_uname = g_strdup (pw->pw_name); + } + + agent_dbus_owner = nm_secret_agent_get_dbus_owner (agent); + req_complete_success (req, secrets, agent_dbus_owner, agent_uname, agent_has_modify); + g_free (agent_uname); +} + +static void +set_secrets_not_required (NMConnection *connection, GHashTable *hash) +{ + GHashTableIter iter, setting_iter; + const char *setting_name = NULL; + GHashTable *setting_hash = NULL; + + /* Iterate through the settings hashes */ + g_hash_table_iter_init (&iter, hash); + while (g_hash_table_iter_next (&iter, + (gpointer *) &setting_name, + (gpointer *) &setting_hash)) { + const char *key_name = NULL; + NMSetting *setting; + GValue *val; + + setting = nm_connection_get_setting_by_name (connection, setting_name); + if (setting) { + /* Now through each secret in the setting and mark it as not required */ + g_hash_table_iter_init (&setting_iter, setting_hash); + while (g_hash_table_iter_next (&setting_iter, (gpointer *) &key_name, (gpointer *) &val)) { + /* For each secret, set the flag that it's not required; VPN + * secrets need slightly different treatment here since the + * "secrets" property is actually a hash table of secrets. + */ + if ( strcmp (setting_name, NM_SETTING_VPN_SETTING_NAME) == 0 + && strcmp (key_name, NM_SETTING_VPN_SECRETS) == 0 + && G_VALUE_HOLDS (val, DBUS_TYPE_G_MAP_OF_STRING)) { + GHashTableIter vpn_secret_iter; + const char *secret_name; + + g_hash_table_iter_init (&vpn_secret_iter, g_value_get_boxed (val)); + while (g_hash_table_iter_next (&vpn_secret_iter, (gpointer *) &secret_name, NULL)) + nm_setting_set_secret_flags (setting, secret_name, NM_SETTING_SECRET_FLAG_NOT_REQUIRED, NULL); + } else + nm_setting_set_secret_flags (setting, key_name, NM_SETTING_SECRET_FLAG_NOT_REQUIRED, NULL); + } + } + } +} + +static void +get_agent_request_secrets (Request *req, gboolean include_system_secrets) +{ + NMConnection *tmp; + + tmp = nm_connection_duplicate (req->connection); + nm_connection_clear_secrets (tmp); + if (include_system_secrets) { + if (req->existing_secrets) + nm_connection_update_secrets (tmp, req->setting_name, req->existing_secrets, NULL); + } else { + /* Update secret flags in the temporary connection to indicate that + * the system secrets we're not sending to the agent aren't required, + * so the agent can properly validate UI controls and such. + */ + if (req->existing_secrets) + set_secrets_not_required (tmp, req->existing_secrets); + } + + req->current_call_id = nm_secret_agent_get_secrets (NM_SECRET_AGENT (req->current), + tmp, + req->setting_name, + req->hint, + req->flags, + get_done_cb, + req); + if (req->current_call_id == NULL) { + /* Shouldn't hit this, but handle it anyway */ + g_warn_if_fail (req->current_call_id != NULL); + req->current_has_modify = FALSE; + req->current = NULL; + req->next_callback (req); + } + + g_object_unref (tmp); +} + +static void +get_agent_modify_auth_cb (NMAuthChain *chain, + GError *error, + DBusGMethodInvocation *context, + gpointer user_data) +{ + Request *req = user_data; + NMAuthCallResult result; + const char *perm; + + req->chain = NULL; + + if (error) { + nm_log_dbg (LOGD_AGENTS, "(%p/%s) agent MODIFY check error: (%d) %s", + req, req->setting_name, + error->code, error->message ? error->message : "(unknown)"); + + /* Try the next agent */ + req->next_callback (req); + } else { + /* If the agent obtained the 'modify' permission, we send all system secrets + * to it. If it didn't, we still ask it for secrets, but we don't send + * any system secrets. + */ + perm = nm_auth_chain_get_data (chain, "perm"); + g_assert (perm); + result = nm_auth_chain_get_result (chain, perm); + if (result == NM_AUTH_CALL_RESULT_YES) + req->current_has_modify = TRUE; + + nm_log_dbg (LOGD_AGENTS, "(%p/%s) agent MODIFY check result %d", + req, req->setting_name, result); + + get_agent_request_secrets (req, req->current_has_modify); + } + nm_auth_chain_unref (chain); +} + +static void +check_system_secrets_cb (NMSetting *setting, + const char *key, + const GValue *value, + GParamFlags flags, + gpointer user_data) +{ + NMSettingSecretFlags secret_flags = NM_SETTING_SECRET_FLAG_NONE; + gboolean *has_system = user_data; + + if (!(flags & NM_SETTING_PARAM_SECRET)) + return; + + /* Clear out system-owned or always-ask secrets */ + if (NM_IS_SETTING_VPN (setting) && !strcmp (key, NM_SETTING_VPN_SECRETS)) { + GHashTableIter iter; + const char *secret_name = NULL; + + /* VPNs are special; need to handle each secret separately */ + g_hash_table_iter_init (&iter, (GHashTable *) g_value_get_boxed (value)); + while (g_hash_table_iter_next (&iter, (gpointer *) &secret_name, NULL)) { + if (nm_setting_get_secret_flags (setting, secret_name, &secret_flags, NULL)) { + if (secret_flags == NM_SETTING_SECRET_FLAG_NONE) + *has_system = TRUE; + } + } + } else { + nm_setting_get_secret_flags (setting, key, &secret_flags, NULL); + if (secret_flags == NM_SETTING_SECRET_FLAG_NONE) + *has_system = TRUE; + } +} + +static gboolean +has_system_secrets (NMConnection *connection) +{ + gboolean has_system = FALSE; + + nm_connection_for_each_setting_value (connection, check_system_secrets_cb, &has_system); + return has_system; +} + +static void +get_next_cb (Request *req) +{ + NMSettingConnection *s_con; + const char *agent_dbus_owner, *perm; + + if (!next_generic (req, "getting")) + return; + + agent_dbus_owner = nm_secret_agent_get_dbus_owner (NM_SECRET_AGENT (req->current)); + + /* If the request flags allow user interaction, and there are existing + * system secrets (or blank secrets that are supposed to be system-owned), + * check whether the agent has the 'modify' permission before sending those + * secrets to the agent. We shouldn't leak system-owned secrets to + * unprivileged users. + */ + if ( (req->flags != NM_SETTINGS_GET_SECRETS_FLAG_NONE) + && (req->existing_secrets || has_system_secrets (req->connection))) { + nm_log_dbg (LOGD_AGENTS, "(%p/%s) request has system secrets; checking agent %s for MODIFY", + req, req->setting_name, agent_dbus_owner); + + req->chain = nm_auth_chain_new_dbus_sender (req->authority, + agent_dbus_owner, + get_agent_modify_auth_cb, + req); + g_assert (req->chain); + + /* If the caller is the only user in the connection's permissions, then + * we use the 'modify.own' permission instead of 'modify.system'. If the + * request affects more than just the caller, require 'modify.system'. + */ + s_con = (NMSettingConnection *) nm_connection_get_setting (req->connection, NM_TYPE_SETTING_CONNECTION); + g_assert (s_con); + if (nm_setting_connection_get_num_permissions (s_con) == 1) + perm = NM_AUTH_PERMISSION_SETTINGS_MODIFY_OWN; + else + perm = NM_AUTH_PERMISSION_SETTINGS_MODIFY_SYSTEM; + nm_auth_chain_set_data (req->chain, "perm", (gpointer) perm, NULL); + + nm_auth_chain_add_call (req->chain, perm, TRUE); + } else { + nm_log_dbg (LOGD_AGENTS, "(%p/%s) requesting user-owned secrets from agent %s", + req, req->setting_name, agent_dbus_owner); + + get_agent_request_secrets (req, FALSE); + } +} + +static gboolean +get_start (gpointer user_data) +{ + Request *req = user_data; + GHashTable *setting_secrets = NULL; + + req->idle_id = 0; + + /* Check if there are any existing secrets */ + if (req->existing_secrets) + setting_secrets = g_hash_table_lookup (req->existing_secrets, req->setting_name); + + if (setting_secrets && g_hash_table_size (setting_secrets)) { + NMConnection *tmp; + GError *error = NULL; + gboolean request_new = (req->flags & NM_SETTINGS_GET_SECRETS_FLAG_REQUEST_NEW); + + /* The connection already had secrets; check if any more are required. + * If no more are required, we're done. If secrets are still needed, + * ask a secret agent for more. This allows admins to provide generic + * secrets but allow additional user-specific ones as well. + */ + tmp = nm_connection_duplicate (req->connection); + g_assert (tmp); + + if (!nm_connection_update_secrets (tmp, req->setting_name, req->existing_secrets, &error)) { + req_complete_error (req, error); + g_clear_error (&error); + } else { + /* Do we have everything we need? */ + /* FIXME: handle second check for VPN connections */ + if ((nm_connection_need_secrets (tmp, NULL) == NULL) && (request_new == FALSE)) { + nm_log_dbg (LOGD_AGENTS, "(%p/%s) system settings secrets sufficient", + req, req->setting_name); + + /* Got everything, we're done */ + req_complete_success (req, req->existing_secrets, NULL, NULL, FALSE); + } else { + nm_log_dbg (LOGD_AGENTS, "(%p/%s) system settings secrets insufficient, asking agents", + req, req->setting_name); + + /* We don't, so ask some agents for additional secrets */ + req->next_callback (req); + } + } + g_object_unref (tmp); + } else { + /* Couldn't get secrets from system settings, so now we ask the + * agents for secrets. Let the Agent Manager handle which agents + * we'll ask and in which order. + */ + req->next_callback (req); + } + + return FALSE; +} + +static void +get_complete_cb (Request *req, + GHashTable *secrets, + const char *agent_dbus_owner, + const char *agent_username, + gboolean agent_has_modify, + GError *error, + gpointer user_data) +{ + NMAgentManager *self = NM_AGENT_MANAGER (user_data); + NMAgentManagerPrivate *priv = NM_AGENT_MANAGER_GET_PRIVATE (self); + + /* Send secrets back to the requesting object */ + req->callback (self, + req->reqid, + agent_dbus_owner, + agent_username, + agent_has_modify, + req->setting_name, + req->flags, + error ? NULL : secrets, + error, + req->callback_data, + req->other_data2, + req->other_data3); + + g_hash_table_remove (priv->requests, GUINT_TO_POINTER (req->reqid)); +} + +static void +get_cancel_cb (Request *req) +{ + if (req->current && req->current_call_id) + nm_secret_agent_cancel_secrets (req->current, req->current_call_id); +} + +guint32 +nm_agent_manager_get_secrets (NMAgentManager *self, + NMConnection *connection, + gboolean filter_by_uid, + gulong uid_filter, + GHashTable *existing_secrets, + const char *setting_name, + NMSettingsGetSecretsFlags flags, + const char *hint, + NMAgentSecretsResultFunc callback, + gpointer callback_data, + gpointer other_data2, + gpointer other_data3) +{ + NMAgentManagerPrivate *priv = NM_AGENT_MANAGER_GET_PRIVATE (self); + Request *req; + + g_return_val_if_fail (self != NULL, 0); + g_return_val_if_fail (connection != NULL, 0); + g_return_val_if_fail (NM_IS_CONNECTION (connection), 0); + g_return_val_if_fail (callback != NULL, 0); + + nm_log_dbg (LOGD_SETTINGS, + "Secrets requested for connection %s (%s)", + nm_connection_get_path (connection), + setting_name); + + /* NOTE: a few things in the Request handling depend on existing_secrets + * being NULL if there aren't any system-owned secrets for this connection. + * This in turn depends on nm_connection_to_hash() and nm_setting_to_hash() + * both returning NULL if they didn't hash anything. + */ + + req = request_new_get (connection, + priv->authority, + filter_by_uid, + uid_filter, + existing_secrets, + setting_name, + flags, + hint, + callback, + callback_data, + other_data2, + other_data3, + get_complete_cb, + self, + get_next_cb, + get_cancel_cb); + g_hash_table_insert (priv->requests, GUINT_TO_POINTER (req->reqid), req); + + /* Kick off the request */ + request_add_agents (self, req); + req->idle_id = g_idle_add (get_start, req); + + return req->reqid; +} + +void +nm_agent_manager_cancel_secrets (NMAgentManager *self, + guint32 request_id) +{ + g_return_if_fail (self != NULL); + g_return_if_fail (request_id > 0); + + g_hash_table_remove (NM_AGENT_MANAGER_GET_PRIVATE (self)->requests, + GUINT_TO_POINTER (request_id)); +} + +/*************************************************************/ + +static void +save_done_cb (NMSecretAgent *agent, + gconstpointer call_id, + GHashTable *secrets, + GError *error, + gpointer user_data) +{ + Request *req = user_data; + const char *agent_dbus_owner; + + g_return_if_fail (call_id == req->current_call_id); + + req->current = NULL; + req->current_call_id = NULL; + + if (error) { + nm_log_dbg (LOGD_AGENTS, "(%s) agent failed save secrets request %p/%s: (%d) %s", + nm_secret_agent_get_description (agent), + req, req->setting_name, + error ? error->code : -1, + (error && error->message) ? error->message : "(unknown)"); + + /* Try the next agent */ + req->next_callback (req); + return; + } + + nm_log_dbg (LOGD_AGENTS, "(%s) agent saved secrets for request %p/%s", + nm_secret_agent_get_description (agent), + req, req->setting_name); + + agent_dbus_owner = nm_secret_agent_get_dbus_owner (agent); + req_complete_success (req, NULL, NULL, agent_dbus_owner, FALSE); +} + +static void +save_next_cb (Request *req) +{ + if (!next_generic (req, "saving")) + return; + + req->current_call_id = nm_secret_agent_save_secrets (NM_SECRET_AGENT (req->current), + req->connection, + save_done_cb, + req); + if (req->current_call_id == NULL) { + /* Shouldn't hit this, but handle it anyway */ + g_warn_if_fail (req->current_call_id != NULL); + req->current = NULL; + req->next_callback (req); + } +} + +static void +save_complete_cb (Request *req, + GHashTable *secrets, + const char *agent_dbus_owner, + const char *agent_username, + gboolean agent_has_modify, + GError *error, + gpointer user_data) +{ + NMAgentManager *self = NM_AGENT_MANAGER (user_data); + NMAgentManagerPrivate *priv = NM_AGENT_MANAGER_GET_PRIVATE (self); + + g_hash_table_remove (priv->requests, GUINT_TO_POINTER (req->reqid)); +} + +guint32 +nm_agent_manager_save_secrets (NMAgentManager *self, + NMConnection *connection, + gboolean filter_by_uid, + gulong uid_filter) +{ + NMAgentManagerPrivate *priv = NM_AGENT_MANAGER_GET_PRIVATE (self); + Request *req; + + g_return_val_if_fail (self != NULL, 0); + g_return_val_if_fail (connection != NULL, 0); + g_return_val_if_fail (NM_IS_CONNECTION (connection), 0); + + nm_log_dbg (LOGD_SETTINGS, + "Saving secrets for connection %s", + nm_connection_get_path (connection)); + + req = request_new_other (connection, + filter_by_uid, + uid_filter, + save_complete_cb, + self, + save_next_cb); + g_hash_table_insert (priv->requests, GUINT_TO_POINTER (req->reqid), req); + + /* Kick off the request */ + request_add_agents (self, req); + req->idle_id = g_idle_add (start_generic, req); + + return req->reqid; +} + +/*************************************************************/ + +static void +delete_done_cb (NMSecretAgent *agent, + gconstpointer call_id, + GHashTable *secrets, + GError *error, + gpointer user_data) +{ + Request *req = user_data; + + g_return_if_fail (call_id == req->current_call_id); + + req->current = NULL; + req->current_call_id = NULL; + + if (error) { + nm_log_dbg (LOGD_AGENTS, "(%s) agent failed delete secrets request %p/%s: (%d) %s", + nm_secret_agent_get_description (agent), + req, req->setting_name, + error ? error->code : -1, + (error && error->message) ? error->message : "(unknown)"); + } else { + nm_log_dbg (LOGD_AGENTS, "(%s) agent deleted secrets for request %p/%s", + nm_secret_agent_get_description (agent), + req, req->setting_name); + } + + /* Tell the next agent to delete secrets */ + req->next_callback (req); +} + +static void +delete_next_cb (Request *req) +{ + if (!next_generic (req, "deleting")) + return; + + req->current_call_id = nm_secret_agent_delete_secrets (NM_SECRET_AGENT (req->current), + req->connection, + delete_done_cb, + req); + if (req->current_call_id == NULL) { + /* Shouldn't hit this, but handle it anyway */ + g_warn_if_fail (req->current_call_id != NULL); + req->current = NULL; + req->next_callback (req); + } +} + +static void +delete_complete_cb (Request *req, + GHashTable *secrets, + const char *agent_dbus_owner, + const char *agent_username, + gboolean agent_has_modify, + GError *error, + gpointer user_data) +{ + NMAgentManager *self = NM_AGENT_MANAGER (user_data); + NMAgentManagerPrivate *priv = NM_AGENT_MANAGER_GET_PRIVATE (self); + + g_hash_table_remove (priv->requests, GUINT_TO_POINTER (req->reqid)); +} + +guint32 +nm_agent_manager_delete_secrets (NMAgentManager *self, + NMConnection *connection, + gboolean filter_by_uid, + gulong uid_filter) +{ + NMAgentManagerPrivate *priv = NM_AGENT_MANAGER_GET_PRIVATE (self); + Request *req; + + g_return_val_if_fail (self != NULL, 0); + g_return_val_if_fail (connection != NULL, 0); + g_return_val_if_fail (NM_IS_CONNECTION (connection), 0); + + nm_log_dbg (LOGD_SETTINGS, + "Deleting secrets for connection %s", + nm_connection_get_path (connection)); + + req = request_new_other (connection, + filter_by_uid, + uid_filter, + delete_complete_cb, + self, + delete_next_cb); + g_hash_table_insert (priv->requests, GUINT_TO_POINTER (req->reqid), req); + + /* Kick off the request */ + request_add_agents (self, req); + req->idle_id = g_idle_add (start_generic, req); + + return req->reqid; +} + +/*************************************************************/ + +static void +name_owner_changed_cb (NMDBusManager *dbus_mgr, + const char *name, + const char *old_owner, + const char *new_owner, + gpointer user_data) +{ + if (old_owner) { + /* The agent quit, so remove it and let interested clients know */ + remove_agent (NM_AGENT_MANAGER (user_data), old_owner); + } +} + +/*************************************************************/ + +NMAgentManager * +nm_agent_manager_get (void) +{ + static NMAgentManager *singleton = NULL; + NMAgentManagerPrivate *priv; + DBusGConnection *connection; + + if (singleton) + return g_object_ref (singleton); + + singleton = (NMAgentManager *) g_object_new (NM_TYPE_AGENT_MANAGER, NULL); + g_assert (singleton); + + priv = NM_AGENT_MANAGER_GET_PRIVATE (singleton); + priv->session_monitor = nm_session_monitor_get (); + priv->dbus_mgr = nm_dbus_manager_get (); + + connection = nm_dbus_manager_get_connection (priv->dbus_mgr); + dbus_g_connection_register_g_object (connection, + NM_DBUS_PATH_AGENT_MANAGER, + G_OBJECT (singleton)); + + g_signal_connect (priv->dbus_mgr, + NM_DBUS_MANAGER_NAME_OWNER_CHANGED, + G_CALLBACK (name_owner_changed_cb), + singleton); + + return singleton; +} + +static void +nm_agent_manager_init (NMAgentManager *self) +{ + NMAgentManagerPrivate *priv = NM_AGENT_MANAGER_GET_PRIVATE (self); + GError *error = NULL; + + priv->authority = polkit_authority_get_sync (NULL, &error); + if (!priv->authority) { + nm_log_warn (LOGD_SETTINGS, "failed to create PolicyKit authority: (%d) %s", + error ? error->code : -1, + error && error->message ? error->message : "(unknown)"); + g_clear_error (&error); + } + + priv->agents = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_object_unref); + priv->requests = g_hash_table_new_full (g_direct_hash, + g_direct_equal, + NULL, + (GDestroyNotify) request_free); +} + +static void +dispose (GObject *object) +{ + NMAgentManagerPrivate *priv = NM_AGENT_MANAGER_GET_PRIVATE (object); + + if (!priv->disposed) { + priv->disposed = TRUE; + + g_hash_table_destroy (priv->agents); + g_hash_table_destroy (priv->requests); + + g_object_unref (priv->session_monitor); + g_object_unref (priv->dbus_mgr); + g_object_unref (priv->authority); + } + + G_OBJECT_CLASS (nm_agent_manager_parent_class)->dispose (object); +} + +static void +nm_agent_manager_class_init (NMAgentManagerClass *agent_manager_class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (agent_manager_class); + + g_type_class_add_private (agent_manager_class, sizeof (NMAgentManagerPrivate)); + + /* virtual methods */ + object_class->dispose = dispose; + + dbus_g_object_type_install_info (G_TYPE_FROM_CLASS (agent_manager_class), + &dbus_glib_nm_agent_manager_object_info); + + dbus_g_error_domain_register (NM_AGENT_MANAGER_ERROR, + NM_DBUS_INTERFACE_AGENT_MANAGER, + NM_TYPE_AGENT_MANAGER_ERROR); +} |