/* -*- 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 Red Hat, Inc.
 */

#include "nm-default.h"

#include "nm-auth-utils.h"

#include <string.h>

#include "nm-utils/nm-c-list.h"

#include "nm-setting-connection.h"
#include "nm-auth-subject.h"
#include "nm-auth-manager.h"
#include "nm-session-monitor.h"

/*****************************************************************************/

struct NMAuthChain {
	GHashTable *data_hash;

	CList auth_call_lst_head;

	GDBusMethodInvocation *context;
	NMAuthSubject *subject;

	NMAuthChainResultFunc done_func;
	gpointer user_data;

	guint32 refcount;

	bool done:1;
};

typedef struct {
	CList auth_call_lst;
	NMAuthChain *chain;
	NMAuthManagerCallId *call_id;
	char *permission;
} AuthCall;

/*****************************************************************************/

static void
_ASSERT_call (AuthCall *call)
{
	nm_assert (call);
	nm_assert (call->chain);
	nm_assert (nm_c_list_contains_entry (&call->chain->auth_call_lst_head, call, auth_call_lst));
}

/*****************************************************************************/

static void
auth_call_free (AuthCall *call)
{
	if (call->call_id)
		nm_auth_manager_check_authorization_cancel (call->call_id);
	c_list_unlink_stale (&call->auth_call_lst);
	g_free (call->permission);
	g_slice_free (AuthCall, call);
}

/*****************************************************************************/

typedef struct {

	/* must be the first field. */
	const char *tag;

	gpointer data;
	GDestroyNotify destroy;
	char tag_data[];
} ChainData;

static ChainData *
chain_data_new (const char *tag, gpointer data, GDestroyNotify destroy)
{
	ChainData *tmp;
	gsize l = strlen (tag);

	tmp = g_malloc (sizeof (ChainData) + l + 1);
	tmp->tag = &tmp->tag_data[0];
	tmp->data = data;
	tmp->destroy = destroy;
	memcpy (&tmp->tag_data[0], tag, l + 1);
	return tmp;
}

static void
chain_data_free (gpointer data)
{
	ChainData *tmp = data;

	if (tmp->destroy)
		tmp->destroy (tmp->data);
	g_free (tmp);
}

static gpointer
_get_data (NMAuthChain *self, const char *tag)
{
	ChainData *tmp;

	if (!self->data_hash)
		return NULL;
	tmp = g_hash_table_lookup (self->data_hash, &tag);
	return tmp ? tmp->data : NULL;
}

gpointer
nm_auth_chain_get_data (NMAuthChain *self, const char *tag)
{
	g_return_val_if_fail (self, NULL);
	g_return_val_if_fail (tag, NULL);

	return _get_data (self, tag);
}

/**
 * nm_auth_chain_steal_data:
 * @self: A #NMAuthChain.
 * @tag: A "tag" uniquely identifying the data to steal.
 *
 * Removes the datum assocated with @tag from the chain's data associations,
 * without invoking the association's destroy handler.  The caller assumes
 * ownership over the returned value.
 *
 * Returns: the datum originally associated with @tag
 */
gpointer
nm_auth_chain_steal_data (NMAuthChain *self, const char *tag)
{
	ChainData *tmp;
	gpointer value = NULL;

	g_return_val_if_fail (self, NULL);
	g_return_val_if_fail (tag, NULL);

	if (!self->data_hash)
		return NULL;

	tmp = g_hash_table_lookup (self->data_hash, &tag);
	if (!tmp)
		return NULL;

	value = tmp->data;

	/* Make sure the destroy handler isn't called when freeing */
	tmp->destroy = NULL;
	g_hash_table_remove (self->data_hash, tmp);
	return value;
}

void
nm_auth_chain_set_data (NMAuthChain *self,
                        const char *tag,
                        gpointer data,
                        GDestroyNotify data_destroy)
{
	g_return_if_fail (self);
	g_return_if_fail (tag);

	if (data == NULL) {
		if (self->data_hash)
			g_hash_table_remove (self->data_hash, &tag);
	} else {
		if (!self->data_hash) {
			self->data_hash = g_hash_table_new_full (nm_pstr_hash, nm_pstr_equal,
			                                         NULL, chain_data_free);
		}
		g_hash_table_add (self->data_hash,
		                  chain_data_new (tag, data, data_destroy));
	}
}

/*****************************************************************************/

NMAuthCallResult
nm_auth_chain_get_result (NMAuthChain *self, const char *permission)
{
	gpointer data;

	g_return_val_if_fail (self, NM_AUTH_CALL_RESULT_UNKNOWN);
	g_return_val_if_fail (permission, NM_AUTH_CALL_RESULT_UNKNOWN);

	data = _get_data (self, permission);
	return data ? GPOINTER_TO_UINT (data) : NM_AUTH_CALL_RESULT_UNKNOWN;
}

NMAuthSubject *
nm_auth_chain_get_subject (NMAuthChain *self)
{
	g_return_val_if_fail (self, NULL);

	return self->subject;
}

/*****************************************************************************/

static gboolean
auth_chain_finish (NMAuthChain *self)
{
	self->done = TRUE;

	/* Ensure we stay alive across the callback */
	nm_assert (self->refcount == 1);
	self->refcount++;
	self->done_func (self, NULL, self->context, self->user_data);
	nm_assert (NM_IN_SET (self->refcount, 1, 2));
	nm_auth_chain_destroy (self);
	return FALSE;
}

static void
auth_call_complete (AuthCall *call)
{
	NMAuthChain *self;

	_ASSERT_call (call);

	self = call->chain;

	nm_assert (!self->done);

	auth_call_free (call);

	if (c_list_is_empty (&self->auth_call_lst_head)) {
		/* we are on an idle-handler or a clean call-stack (non-reentrant). */
		auth_chain_finish (self);
	}
}

static void
pk_call_cb (NMAuthManager *auth_manager,
            NMAuthManagerCallId *call_id,
            gboolean is_authorized,
            gboolean is_challenge,
            GError *error,
            gpointer user_data)
{
	AuthCall *call;
	NMAuthCallResult call_result;

	if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
		return;

	call = user_data;

	nm_assert (call->call_id == call_id);

	call->call_id = NULL;

	call_result = nm_auth_call_result_eval (is_authorized, is_challenge, error);

	nm_auth_chain_set_data (call->chain, call->permission, GUINT_TO_POINTER (call_result), NULL);

	auth_call_complete (call);
}

void
nm_auth_chain_add_call (NMAuthChain *self,
                        const char *permission,
                        gboolean allow_interaction)
{
	AuthCall *call;
	NMAuthManager *auth_manager = nm_auth_manager_get ();

	g_return_if_fail (self);
	g_return_if_fail (self->subject);
	g_return_if_fail (!self->done);
	g_return_if_fail (permission && *permission);
	g_return_if_fail (nm_auth_subject_is_unix_process (self->subject) || nm_auth_subject_is_internal (self->subject));

	call = g_slice_new0 (AuthCall);
	call->chain = self;
	call->permission = g_strdup (permission);
	c_list_link_tail (&self->auth_call_lst_head, &call->auth_call_lst);
	call->call_id = nm_auth_manager_check_authorization (auth_manager,
	                                                     self->subject,
	                                                     permission,
	                                                     allow_interaction,
	                                                     pk_call_cb,
	                                                     call);
}

/*****************************************************************************/

/* Creates the NMAuthSubject automatically */
NMAuthChain *
nm_auth_chain_new_context (GDBusMethodInvocation *context,
                           NMAuthChainResultFunc done_func,
                           gpointer user_data)
{
	NMAuthSubject *subject;
	NMAuthChain *chain;

	g_return_val_if_fail (context, NULL);

	subject = nm_auth_subject_new_unix_process_from_context (context);
	if (!subject)
		return NULL;

	chain = nm_auth_chain_new_subject (subject,
	                                   context,
	                                   done_func,
	                                   user_data);
	g_object_unref (subject);
	return chain;
}

/* Requires an NMAuthSubject */
NMAuthChain *
nm_auth_chain_new_subject (NMAuthSubject *subject,
                           GDBusMethodInvocation *context,
                           NMAuthChainResultFunc done_func,
                           gpointer user_data)
{
	NMAuthChain *self;

	g_return_val_if_fail (NM_IS_AUTH_SUBJECT (subject), NULL);
	nm_assert (nm_auth_subject_is_unix_process (subject) || nm_auth_subject_is_internal (subject));

	self = g_slice_new0 (NMAuthChain);
	c_list_init (&self->auth_call_lst_head);
	self->refcount = 1;
	self->done_func = done_func;
	self->user_data = user_data;
	self->context = context ? g_object_ref (context) : NULL;
	self->subject = g_object_ref (subject);
	return self;
}

/**
 * nm_auth_chain_destroy:
 * @self: the auth-chain
 *
 * Destroys the auth-chain. By destroying the auth-chain, you also cancel
 * the receipt of the done-callback. IOW, the callback will not be invoked.
 *
 * The only exception is, if may call nm_auth_chain_destroy() from inside
 * the callback. In this case, @self stays alive until the callback returns.
 *
 * Note that you might only destroy an auth-chain exactly once, and never
 * after the callback was handled.
 */
void
nm_auth_chain_destroy (NMAuthChain *self)
{
	AuthCall *call;

	g_return_if_fail (self);
	g_return_if_fail (NM_IN_SET (self->refcount, 1, 2));

	if (--self->refcount > 0)
		return;

	nm_clear_g_object (&self->subject);
	nm_clear_g_object (&self->context);

	while ((call = c_list_first_entry (&self->auth_call_lst_head, AuthCall, auth_call_lst)))
		auth_call_free (call);

	nm_clear_pointer (&self->data_hash, g_hash_table_destroy);

	g_slice_free (NMAuthChain, self);
}

/******************************************************************************
 * utils
 *****************************************************************************/

gboolean
nm_auth_is_subject_in_acl (NMConnection *connection,
                           NMAuthSubject *subject,
                           char **out_error_desc)
{
	NMSettingConnection *s_con;
	const char *user = NULL;
	gulong uid;

	g_return_val_if_fail (connection, FALSE);
	g_return_val_if_fail (NM_IS_AUTH_SUBJECT (subject), FALSE);
	g_return_val_if_fail (nm_auth_subject_is_internal (subject) || nm_auth_subject_is_unix_process (subject), FALSE);

	if (nm_auth_subject_is_internal (subject))
		return TRUE;

	uid = nm_auth_subject_get_unix_process_uid (subject);

	/* Root gets a free pass */
	if (0 == uid)
		return TRUE;

	if (!nm_session_monitor_uid_to_user (uid, &user)) {
		NM_SET_OUT (out_error_desc,
		            g_strdup_printf ("Could not determine username for uid %lu", uid));
		return FALSE;
	}

	s_con = nm_connection_get_setting_connection (connection);
	if (!s_con) {
		/* This can only happen when called from AddAndActivate, so we know
		 * the user will be authorized when the connection is completed.
		 */
		return TRUE;
	}

	/* Match the username returned by the session check to a user in the ACL */
	if (!nm_setting_connection_permissions_user_allowed (s_con, user)) {
		NM_SET_OUT (out_error_desc,
		            g_strdup_printf ("uid %lu has no permission to perform this operation", uid));
		return FALSE;
	}

	return TRUE;
}

gboolean
nm_auth_is_subject_in_acl_set_error (NMConnection *connection,
                                     NMAuthSubject *subject,
                                     GQuark err_domain,
                                     int err_code,
                                     GError **error)
{
	char *error_desc = NULL;

	nm_assert (!error || !*error);

	if (nm_auth_is_subject_in_acl (connection,
	                               subject,
	                               error ? &error_desc : NULL))
		return TRUE;

	g_set_error_literal (error, err_domain, err_code, error_desc);
	g_free (error_desc);
	return FALSE;
}
