/*
 *
 *  Connection Manager
 *
 *  Copyright (C) 2012-2014  BMW Car IT GmbH.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  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 St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <errno.h>
#include <string.h>
#include <sys/inotify.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <pwd.h>
#include <grp.h>

#include <glib.h>

#include <gdbus.h>

#define CONNMAN_API_SUBJECT_TO_CHANGE
#include <connman/plugin.h>
#include <connman/log.h>
#include <connman/session.h>
#include <connman/dbus.h>
#include <connman/inotify.h>

#include "src/shared/util.h"

#define POLICYDIR STORAGEDIR "/session_policy_local"

#define MODE		(S_IRUSR | S_IWUSR | S_IXUSR | S_IRGRP | \
			S_IXGRP | S_IROTH | S_IXOTH)

static DBusConnection *connection;

static GHashTable *file_hash;    /* (filename, policy_file) */
static GHashTable *session_hash; /* (connman_session, policy_config) */

/* Global lookup tables for mapping sessions to policies */
static GHashTable *selinux_hash; /* (lsm context, policy_group) */
static GHashTable *uid_hash;     /* (uid, policy_group) */
static GHashTable *gid_hash;     /* (gid, policy_group) */

/*
 * A instance of struct policy_file is created per file in
 * POLICYDIR.
 */
struct policy_file {
	/*
	 * A valid file is a keyfile with one ore more groups. All
	 * groups are kept in this list.
	 */
	GSList *groups;
};

struct policy_group {
	char *selinux;
	char *uid;
	char *gid;

	/*
	 * Each policy_group owns a config and is not shared with
	 * sessions. Instead each session copies the valued from this
	 * object.
	 */
	struct connman_session_config *config;

	/* All 'users' of this policy. */
	GSList *sessions;
};

/* A struct policy_config object is created and owned by a session. */
struct policy_config {
	char *selinux;
	char *selinux_context;
	char *uid;
	GSList *gids;

	/* The policy config owned by the session */
	struct connman_session_config *config;

	/* To which policy belongs this policy_config */
	struct connman_session *session;
	/*
	 * Points to the policy_group when a config has been applied
	 * from a file.
	 */
	struct policy_group *group;
};

static void copy_session_config(struct connman_session_config *dst,
			struct connman_session_config *src)
{
	g_slist_free(dst->allowed_bearers);
	dst->allowed_bearers = g_slist_copy(src->allowed_bearers);
	dst->ecall = src->ecall;
	dst->type = src->type;
	dst->roaming_policy = src->roaming_policy;
	dst->priority = src->priority;
}

static void set_policy(struct policy_config *policy,
			struct policy_group *group)
{
	DBG("policy %p group %p", policy, group);

	group->sessions = g_slist_prepend(group->sessions, policy);
	policy->group = group;

	copy_session_config(policy->config, group->config);
}

static char *parse_selinux_type(const char *context)
{
	char *ident, **tokens;

	/*
	 * SELinux combines Role-Based Access Control (RBAC), Type
	 * Enforcement (TE) and optionally Multi-Level Security (MLS).
	 *
	 * When SELinux is enabled all processes and files are labeled
	 * with a contex that contains information such as user, role
	 * type (and optionally a level). E.g.
	 *
	 * $ ls -Z
	 * -rwxrwxr-x. wagi wagi unconfined_u:object_r:haifux_exec_t:s0 session_ui.py
	 *
	 * For identifyng application we (ab)using the type
	 * information. In the above example the haifux_exec_t type
	 * will be transferred to haifux_t as defined in the domain
	 * transition and thus we are able to identify the application
	 * as haifux_t.
	 */

	tokens = g_strsplit(context, ":", 0);
	if (g_strv_length(tokens) < 2) {
		g_strfreev(tokens);
		return NULL;
	}

	/* Use the SELinux type as identification token. */
	ident = g_strdup(tokens[2]);

	g_strfreev(tokens);

	return ident;
}

static void cleanup_config(gpointer user_data);

static void finish_create(struct policy_config *policy,
				connman_session_config_func_t cb,
				void *user_data)
{
	struct policy_group *group = NULL;
	GSList *list;

	if (policy->selinux)
		group = g_hash_table_lookup(selinux_hash, policy->selinux);

	if (group) {
		set_policy(policy, group);

		policy->config->id_type = CONNMAN_SESSION_ID_TYPE_LSM;
		policy->config->id = g_strdup(policy->selinux_context);
		goto done;
	}

	if (policy->uid)
		group = g_hash_table_lookup(uid_hash, policy->uid);

	if (group) {
		set_policy(policy, group);

		policy->config->id_type = CONNMAN_SESSION_ID_TYPE_UID;
		policy->config->id = g_strdup(policy->uid);
		goto done;
	}

	for (list = policy->gids; list; list = list->next) {
		char *gid = list->data;

		group = g_hash_table_lookup(gid_hash, gid);
		if (!group)
			continue;

		set_policy(policy, group);

		policy->config->id_type = CONNMAN_SESSION_ID_TYPE_GID;
		policy->config->id = g_strdup(gid);
		break;
	}
done:
	g_hash_table_replace(session_hash, policy->session, policy);

	(*cb)(policy->session, policy->config, user_data, 0);
}

static void failed_create(struct policy_config *policy,
			connman_session_config_func_t cb,
			void *user_data, int err)
{
	(*cb)(policy->session, NULL, user_data, err);

	cleanup_config(policy);
}

static void selinux_context_reply(const unsigned char *context, void *user_data,
					int err)
{
	struct cb_data *cbd = user_data;
	connman_session_config_func_t cb = cbd->cb;
	struct policy_config *policy = cbd->data;
	char *ident = NULL;

	DBG("session %p", policy->session);

	if (err == -EIO) {
		/* No SELinux support, drop back to UID/GID only mode */
		finish_create(policy, cb, cbd->user_data);
		goto done;
	}

	if (err < 0) {
		failed_create(policy, cb, cbd->user_data, err);
		goto done;
	}

	DBG("SELinux context %s", context);

	policy->selinux_context = g_strdup((const char *)context);
	ident = parse_selinux_type(policy->selinux_context);
	if (ident)
		policy->selinux = g_strdup(ident);

	finish_create(policy, cb, cbd->user_data);

done:
	g_free(cbd);
	g_free(ident);
}

static void get_uid_reply(unsigned int uid, void *user_data, int err)
{
	struct cb_data *cbd = user_data;
	connman_session_config_func_t cb = cbd->cb;
	struct policy_config *policy = cbd->data;
	const char *owner;
	struct passwd *pwd;
	struct group *grp;
	gid_t *groups = NULL;
	int nrgroups, i;

	DBG("session %p uid %d", policy->session, uid);

	if (err < 0)
		goto err;

	pwd = getpwuid((uid_t)uid);
	if (!pwd) {
		if (errno != 0)
			err = -errno;
		else
			err = -EINVAL;
		goto err;
	}

	policy->uid = g_strdup(pwd->pw_name);

	nrgroups = 0;
	getgrouplist(pwd->pw_name, pwd->pw_gid, NULL, &nrgroups);
	groups = g_try_new0(gid_t, nrgroups);
	if (!groups) {
		err = -ENOMEM;
		goto err;
	}

	err = getgrouplist(pwd->pw_name, pwd->pw_gid, groups, &nrgroups);
	if (err < 0)
		goto err;

	for (i = 0; i < nrgroups; i++) {
		grp = getgrgid(groups[i]);
		if (!grp) {
			if (errno != 0)
				err = -errno;
			else
				err = -EINVAL;
			goto err;
		}

		policy->gids = g_slist_prepend(policy->gids,
					g_strdup(grp->gr_name));
	}
	g_free(groups);

	owner = connman_session_get_owner(policy->session);

	err = connman_dbus_get_selinux_context(connection, owner,
						selinux_context_reply, cbd);
	if (err == 0) {
		/*
		 * We are able to ask for a SELinux context. Let's defer the
		 * creation of the session config until we get the answer
		 * from D-Bus.
		 */
		return;
	}

	finish_create(policy, cb, cbd->user_data);
	g_free(cbd);

	return;

err:
	failed_create(policy, cb, cbd->user_data, err);
	g_free(cbd);
	g_free(groups);
}

static int policy_local_create(struct connman_session *session,
				connman_session_config_func_t cb,
				void *user_data)
{
	struct cb_data *cbd = cb_data_new(cb, user_data);
	struct policy_config *policy;
	const char *owner;
	int err;

	DBG("session %p", session);

	policy = g_new0(struct policy_config, 1);
	policy->config = connman_session_create_default_config();
	policy->session = session;

	cbd->data = policy;

	owner = connman_session_get_owner(session);

	err = connman_dbus_get_connection_unix_user(connection, owner,
						get_uid_reply, cbd);
	if (err < 0) {
		connman_error("Could not get UID");
		cleanup_config(policy);
		g_free(cbd);
		return err;
	}

	return 0;
}

static void policy_local_destroy(struct connman_session *session)
{
	struct policy_data *policy;

	DBG("session %p", session);

	policy = g_hash_table_lookup(session_hash, session);
	if (!policy)
		return;

	g_hash_table_remove(session_hash, session);
}

static struct connman_session_policy session_policy_local = {
	.name = "session local policy configuration",
	.priority = CONNMAN_SESSION_POLICY_PRIORITY_DEFAULT,
	.create = policy_local_create,
	.destroy = policy_local_destroy,
};

static int load_keyfile(const char *pathname, GKeyFile **keyfile)
{
	GError *error = NULL;
	int err;

	*keyfile = g_key_file_new();

	if (!g_key_file_load_from_file(*keyfile, pathname, 0, &error))
		goto err;

	return 0;

err:
	/*
	 * The fancy G_FILE_ERROR_* codes are identical to the native
	 * error codes.
	 */
	err = -error->code;

	DBG("Unable to load %s: %s", pathname, error->message);
	g_clear_error(&error);

	g_key_file_free(*keyfile);
	*keyfile = NULL;

	return err;
}

static int load_policy(GKeyFile *keyfile, const char *groupname,
			struct policy_group *group)
{
	struct connman_session_config *config = group->config;
	char *str, **tokens;
	int i, err = 0;

	group->selinux = g_key_file_get_string(keyfile, groupname,
						"selinux", NULL);

	group->gid = g_key_file_get_string(keyfile, groupname,
						"gid", NULL);

	group->uid = g_key_file_get_string(keyfile, groupname,
						"uid", NULL);

	if (!group->selinux && !group->gid && !group->uid)
		return -EINVAL;

	config->priority = g_key_file_get_boolean(keyfile, groupname,
						"Priority", NULL);

	str = g_key_file_get_string(keyfile, groupname, "RoamingPolicy",
				NULL);
	if (str) {
		config->roaming_policy = connman_session_parse_roaming_policy(str);
		g_free(str);
	}

	str = g_key_file_get_string(keyfile, groupname, "ConnectionType",
				NULL);
	if (str) {
		config->type = connman_session_parse_connection_type(str);
		g_free(str);
	}

	config->ecall = g_key_file_get_boolean(keyfile, groupname,
						"EmergencyCall", NULL);

	str = g_key_file_get_string(keyfile, groupname, "AllowedBearers",
				NULL);
	if (str) {
		g_slist_free(config->allowed_bearers);
		config->allowed_bearers = NULL;
		tokens = g_strsplit(str, " ", 0);

		for (i = 0; tokens[i]; i++) {
			err = connman_session_parse_bearers(tokens[i],
					&config->allowed_bearers);
			if (err < 0)
				break;
		}

		g_free(str);
		g_strfreev(tokens);
	}

	DBG("group %p selinux %s uid %s gid %s", group, group->selinux,
		group->uid, group->gid);

	return err;
}

static void update_session(struct policy_config *policy)
{
	DBG("policy %p session %p", policy, policy->session);

	if (!policy->session)
		return;

	if (connman_session_config_update(policy->session) < 0)
		connman_session_destroy(policy->session);
}

static void set_default_config(gpointer user_data)
{
	struct policy_config *policy = user_data;

	connman_session_set_default_config(policy->config);
	policy->group = NULL;
	update_session(policy);
}

static void cleanup_config(gpointer user_data)
{
	struct policy_config *policy = user_data;

	DBG("policy %p group %p", policy, policy->group);

	if (policy->group)
		policy->group->sessions =
			g_slist_remove(policy->group->sessions, policy);

	g_slist_free(policy->config->allowed_bearers);
	g_free(policy->config->id);
	g_free(policy->config);
	g_free(policy->selinux_context);
	g_free(policy->selinux);
	g_free(policy->uid);
	g_slist_free_full(policy->gids, g_free);
	g_free(policy);
}

static void cleanup_group(gpointer user_data)
{
	struct policy_group *group = user_data;

	DBG("group %p", group);

	g_slist_free_full(group->sessions, set_default_config);

	g_slist_free(group->config->allowed_bearers);
	g_free(group->config->id);
	g_free(group->config);
	if (group->selinux)
		g_hash_table_remove(selinux_hash, group->selinux);
	if (group->uid)
		g_hash_table_remove(uid_hash, group->uid);
	if (group->gid)
		g_hash_table_remove(gid_hash, group->gid);
	g_free(group->selinux);
	g_free(group->uid);
	g_free(group->gid);
	g_free(group);
}

static void cleanup_file(gpointer user_data)
{
	struct policy_file *file = user_data;

	DBG("file %p", file);

	g_slist_free_full(file->groups, cleanup_group);
	g_free(file);
}

static void recheck_sessions(void)
{
	GHashTableIter iter;
	gpointer value, key;
	struct policy_group *group = NULL;
	GSList *list;

	g_hash_table_iter_init(&iter, session_hash);
	while (g_hash_table_iter_next(&iter, &key, &value)) {
		struct policy_config *policy = value;

		if (policy->group)
			continue;

		if (policy->selinux)
			group = g_hash_table_lookup(selinux_hash,
							policy->selinux);
		if (group) {
			policy->config->id_type = CONNMAN_SESSION_ID_TYPE_LSM;
			g_free(policy->config->id);
			policy->config->id = g_strdup(policy->selinux_context);
			update_session(policy);
			continue;
		}

		group = g_hash_table_lookup(uid_hash, policy->uid);
		if (group) {
			set_policy(policy, group);

			policy->config->id_type = CONNMAN_SESSION_ID_TYPE_UID;
			g_free(policy->config->id);
			policy->config->id = g_strdup(policy->uid);
			update_session(policy);
			continue;
		}

		for (list = policy->gids; list; list = list->next) {
			char *gid = list->data;
			group = g_hash_table_lookup(gid_hash, gid);
			if (group) {
				set_policy(policy, group);

				policy->config->id_type = CONNMAN_SESSION_ID_TYPE_GID;
				g_free(policy->config->id);
				policy->config->id = g_strdup(gid);
				update_session(policy);
			}
		}
	}
}

static int load_file(const char *filename, struct policy_file *file)
{
	GKeyFile *keyfile;
	struct policy_group *group;
	char **groupnames;
	char *pathname;
	int err = 0, i;

	DBG("%s", filename);

	pathname = g_strdup_printf("%s/%s", POLICYDIR, filename);
	err = load_keyfile(pathname, &keyfile);
	g_free(pathname);

	if (err < 0)
		return err;

	groupnames = g_key_file_get_groups(keyfile, NULL);

	for (i = 0; groupnames[i]; i++) {
		group = g_new0(struct policy_group, 1);
		group->config = connman_session_create_default_config();

		err = load_policy(keyfile, groupnames[i], group);
		if (err < 0) {
			g_free(group->config);
			g_free(group);
			break;
		}
		if (group->selinux)
			g_hash_table_replace(selinux_hash, group->selinux, group);

		if (group->uid)
			g_hash_table_replace(uid_hash, group->uid, group);

		if (group->gid)
			g_hash_table_replace(gid_hash, group->gid, group);

		file->groups = g_slist_prepend(file->groups, group);
	}

	g_strfreev(groupnames);

	if (err < 0)
		g_slist_free_full(file->groups, cleanup_group);

	g_key_file_free(keyfile);

	return err;
}

static bool is_filename_valid(const char *filename)
{
	if (!filename)
		return false;

	if (filename[0] == '.')
		return false;

	return g_str_has_suffix(filename, ".policy");
}

static int read_policies(void)
{
	GDir *dir;
	const gchar *filename;
	struct policy_file *file;

	DBG("");

	dir = g_dir_open(POLICYDIR, 0, NULL);
	if (!dir)
		return -EINVAL;

	while ((filename = g_dir_read_name(dir))) {
		if (!is_filename_valid(filename))
			continue;

		file = g_new0(struct policy_file, 1);
		if (load_file(filename, file) < 0) {
			g_free(file);
			continue;
		}

		g_hash_table_replace(file_hash, g_strdup(filename), file);
	}

	g_dir_close(dir);

	return 0;
}


static void notify_handler(struct inotify_event *event,
                                        const char *filename)
{
	struct policy_file *file;

	DBG("event %x file %s", event->mask, filename);

	if (event->mask & IN_CREATE)
		return;

	if (!is_filename_valid(filename))
		return;

	/*
	 * load_file() will modify the global selinux/uid/gid hash
	 * tables. We need to remove the old entries first before
	 * else the table points to the wrong entries.
	 */
	g_hash_table_remove(file_hash, filename);

	if (event->mask & (IN_DELETE | IN_MOVED_FROM))
		return;

	if (event->mask & (IN_MOVED_TO | IN_MODIFY)) {
		connman_info("Policy update for '%s'", filename);

		file = g_new0(struct policy_file, 1);
		if (load_file(filename, file) < 0) {
			g_free(file);
			return;
		}

		g_hash_table_replace(file_hash, g_strdup(filename), file);
		recheck_sessions();
	}
}

static int session_policy_local_init(void)
{
	int err;

	DBG("");

	/* If the dir doesn't exist, create it */
	if (!g_file_test(POLICYDIR, G_FILE_TEST_IS_DIR)) {
		if (mkdir(POLICYDIR, MODE) < 0) {
			if (errno != EEXIST)
				return -errno;
		}
	}

	connection = connman_dbus_get_connection();
	if (!connection)
		return -EIO;

	file_hash = g_hash_table_new_full(g_str_hash, g_str_equal,
					g_free, cleanup_file);
	session_hash = g_hash_table_new_full(g_direct_hash, g_direct_equal,
						NULL, cleanup_config);
	selinux_hash = g_hash_table_new_full(g_str_hash, g_str_equal,
					NULL, NULL);
	uid_hash = g_hash_table_new_full(g_str_hash, g_str_equal,
					NULL, NULL);
	gid_hash = g_hash_table_new_full(g_str_hash, g_str_equal,
					NULL, NULL);

	err = connman_inotify_register(POLICYDIR, notify_handler);
	if (err < 0)
		goto err;

	err = connman_session_policy_register(&session_policy_local);
	if (err < 0)
		goto err_notify;

	read_policies();

	return 0;

err_notify:

	connman_inotify_unregister(POLICYDIR, notify_handler);

err:
	if (file_hash)
		g_hash_table_destroy(file_hash);

	if (session_hash)
		g_hash_table_destroy(session_hash);

	if (selinux_hash)
		g_hash_table_destroy(selinux_hash);

	if (uid_hash)
		g_hash_table_destroy(uid_hash);

	if (gid_hash)
		g_hash_table_destroy(gid_hash);

	connman_session_policy_unregister(&session_policy_local);

	dbus_connection_unref(connection);

	return err;
}

static void session_policy_local_exit(void)
{
	DBG("");

	g_hash_table_destroy(file_hash);
	g_hash_table_destroy(session_hash);
	g_hash_table_destroy(selinux_hash);
	g_hash_table_destroy(uid_hash);
	g_hash_table_destroy(gid_hash);

	connman_session_policy_unregister(&session_policy_local);

	dbus_connection_unref(connection);

	connman_inotify_unregister(POLICYDIR, notify_handler);
}

CONNMAN_PLUGIN_DEFINE(session_policy_local,
		"Session local file policy configuration plugin",
		VERSION, CONNMAN_PLUGIN_PRIORITY_DEFAULT,
		session_policy_local_init, session_policy_local_exit)
