/*
 * fs/tomoyo_env.c
 *
 * Implementation of the Domain-Based Mandatory Access Control.
 *
 * Copyright (C) 2005-2008  NTT DATA CORPORATION
 *
 * Version: 1.6.2   2008/06/25
 *
 * This file is applicable to both 2.4.30 and 2.6.11 and later.
 * See README.ccs for ChangeLog.
 *
 */

#include <linux/ccs_common.h>
#include <linux/tomoyo.h>
#include <linux/realpath.h>

/**
 * audit_env_log - Audit environment variable name log.
 *
 * @env:        The name of environment variable.
 * @is_granted: True if this is a granted log.
 * @profile:    Profile number used.
 * @mode:       Access control mode used.
 *
 * Returns 0 on success, negative value otherwise.
 */
static int audit_env_log(const char *env, const bool is_granted,
			 const u8 profile, const u8 mode)
{
	char *buf;
	int len;
	int len2;
	if (ccs_can_save_audit_log(is_granted) < 0)
		return -ENOMEM;
	len = strlen(env) + 64;
	buf = ccs_init_audit_log(&len, profile, mode, NULL);
	if (!buf)
		return -ENOMEM;
	len2 = strlen(buf);
	snprintf(buf + len2, len - len2 - 1, KEYWORD_ALLOW_ENV "%s\n", env);
	return ccs_write_audit_log(buf, is_granted);
}

/* Structure for "allow_env" keyword. */
struct globally_usable_env_entry {
	struct list1_head list;
	const struct path_info *env;
	bool is_deleted;
};

/* The list for "struct globally_usable_env_entry". */
static LIST1_HEAD(globally_usable_env_list);

/**
 * update_globally_usable_env_entry - Update "struct globally_usable_env_entry" list.
 *
 * @env:       The name of environment variable.
 * @is_delete: True if it is a delete request.
 *
 * Returns 0 on success, negative value otherwise.
 */
static int update_globally_usable_env_entry(const char *env,
					    const bool is_delete)
{
	struct globally_usable_env_entry *new_entry;
	struct globally_usable_env_entry *ptr;
	static DEFINE_MUTEX(lock);
	const struct path_info *saved_env;
	int error = -ENOMEM;
	if (!ccs_is_correct_path(env, 0, 0, 0, __func__) || strchr(env, '='))
		return -EINVAL;
	saved_env = ccs_save_name(env);
	if (!saved_env)
		return -ENOMEM;
	mutex_lock(&lock);
	list1_for_each_entry(ptr, &globally_usable_env_list, list) {
		if (ptr->env != saved_env)
			continue;
		ptr->is_deleted = is_delete;
		error = 0;
		goto out;
	}
	if (is_delete) {
		error = -ENOENT;
		goto out;
	}
	new_entry = ccs_alloc_element(sizeof(*new_entry));
	if (!new_entry)
		goto out;
	new_entry->env = saved_env;
	list1_add_tail_mb(&new_entry->list, &globally_usable_env_list);
	error = 0;
 out:
	mutex_unlock(&lock);
	ccs_update_counter(CCS_UPDATES_COUNTER_EXCEPTION_POLICY);
	return error;
}

/**
 * is_globally_usable_env - Check whether the given environment variable is acceptable for all domains.
 *
 * @env: The name of environment variable.
 *
 * Returns true if @env is globally permitted environment variable's name,
 * false otherwise.
 */
static bool is_globally_usable_env(const struct path_info *env)
{
	struct globally_usable_env_entry *ptr;
	list1_for_each_entry(ptr, &globally_usable_env_list, list) {
		if (!ptr->is_deleted && ccs_path_matches_pattern(env, ptr->env))
			return true;
	}
	return false;
}

/**
 * ccs_write_globally_usable_env_policy - Write "struct globally_usable_env_entry" list.
 *
 * @data:      String to parse.
 * @is_delete: True if it is a delete request.
 *
 * Returns 0 on success, negative value otherwise.
 */
int ccs_write_globally_usable_env_policy(char *data, const bool is_delete)
{
	return update_globally_usable_env_entry(data, is_delete);
}

/**
 * ccs_read_globally_usable_env_policy - Read "struct globally_usable_env_entry" list.
 *
 * @head: Pointer to "struct ccs_io_buffer".
 *
 * Returns 0 on success, false otherwise.
 */
bool ccs_read_globally_usable_env_policy(struct ccs_io_buffer *head)
{
	struct list1_head *pos;
	list1_for_each_cookie(pos, head->read_var2,
			      &globally_usable_env_list) {
		struct globally_usable_env_entry *ptr;
		ptr = list1_entry(pos, struct globally_usable_env_entry, list);
		if (ptr->is_deleted)
			continue;
		if (!ccs_io_printf(head, KEYWORD_ALLOW_ENV "%s\n",
				   ptr->env->name))
			goto out;
	}
	return true;
 out:
	return false;
}

/**
 * update_env_entry - Update "struct env_acl_record" list.
 *
 * @env:       The name of environment variable.
 * @domain:    Pointer to "struct domain_info".
 * @condition: Pointer to "struct condition_list". May be NULL.
 * @is_delete: True if it is a delete request.
 *
 * Returns 0 on success, negative value otherwise.
 */
static int update_env_entry(const char *env, struct domain_info *domain,
			    const struct condition_list *condition,
			    const bool is_delete)
{
	struct acl_info *ptr;
	struct env_acl_record *acl;
	const struct path_info *saved_env;
	int error = -ENOMEM;
	if (!ccs_is_correct_path(env, 0, 0, 0, __func__) || strchr(env, '='))
		return -EINVAL;
	saved_env = ccs_save_name(env);
	if (!saved_env)
		return -ENOMEM;

	mutex_lock(&domain_acl_lock);
	if (is_delete)
		goto delete;
	list1_for_each_entry(ptr, &domain->acl_info_list, list) {
		if (ccs_acl_type1(ptr) != TYPE_ENV_ACL)
			continue;
		if (ccs_get_condition_part(ptr) != condition)
			continue;
		acl = container_of(ptr, struct env_acl_record, head);
		if (acl->env != saved_env)
			continue;
		error = ccs_add_domain_acl(NULL, ptr);
		goto out;
	}
	/* Not found. Append it to the tail. */
	acl = ccs_alloc_acl_element(TYPE_ENV_ACL, condition);
	if (!acl)
		goto out;
	acl->env = saved_env;
	error = ccs_add_domain_acl(domain, &acl->head);
	goto out;
 delete:
	error = -ENOENT;
	list1_for_each_entry(ptr, &domain->acl_info_list, list) {
		if (ccs_acl_type2(ptr) != TYPE_ENV_ACL)
			continue;
		if (ccs_get_condition_part(ptr) != condition)
			continue;
		acl = container_of(ptr, struct env_acl_record, head);
		if (acl->env != saved_env)
			continue;
		error = ccs_del_domain_acl(ptr);
		break;
	}
 out:
	mutex_unlock(&domain_acl_lock);
	return error;
}

/**
 * check_env_acl - Check permission for environment variable's name.
 *
 * @environ: The name of environment variable.
 *
 * Returns 0 on success, negative value otherwise.
 */
static int check_env_acl(const char *environ)
{
	const struct domain_info *domain = current->domain_info;
	int error = -EPERM;
	struct acl_info *ptr;
	struct path_info env;
	env.name = environ;
	ccs_fill_path_info(&env);
	list1_for_each_entry(ptr, &domain->acl_info_list, list) {
		struct env_acl_record *acl;
		if (ccs_acl_type2(ptr) != TYPE_ENV_ACL)
			continue;
		acl = container_of(ptr, struct env_acl_record, head);
		if (!ccs_check_condition(ptr, NULL) ||
		    !ccs_path_matches_pattern(&env, acl->env))
			continue;
		ccs_update_condition(ptr);
		error = 0;
		break;
	}
	if (error &&
	    (domain->flags & DOMAIN_FLAGS_IGNORE_GLOBAL_ALLOW_ENV) == 0 &&
	    is_globally_usable_env(&env))
		error = 0;
	return error;
}

/**
 * ccs_check_env_perm - Check permission for environment variable's name.
 *
 * @env:     The name of environment variable.
 * @profile: Profile number.
 * @mode:    Access control mode.
 *
 * Returns 0 on success, negative value otherwise.
 */
int ccs_check_env_perm(const char *env, const u8 profile, const u8 mode)
{
	int error = 0;
	struct domain_info * const domain = current->domain_info;
	const bool is_enforce = (mode == 3);
	if (!env || !*env)
		return 0;
	error = check_env_acl(env);
	audit_env_log(env, !error, profile, mode);
	if (!error)
		return 0;
	if (ccs_verbose_mode())
		printk(KERN_WARNING "TOMOYO-%s: Environ %s denied for %s\n",
		       ccs_get_msg(is_enforce), env, ccs_get_last_name(domain));
	if (is_enforce)
		return ccs_check_supervisor(NULL, KEYWORD_ALLOW_ENV "%s\n",
					    env);
	if (mode == 1 && ccs_check_domain_quota(domain))
		update_env_entry(env, domain, NULL, false);
	return 0;
}

/**
 * ccs_write_env_policy - Write "struct env_acl_record" list.
 *
 * @data:      String to parse.
 * @domain:    Pointer to "struct domain_info".
 * @condition: Pointer to "struct condition_list". May be NULL.
 * @is_delete: True if it is a delete request.
 *
 * Returns 0 on success, negative value otherwise.
 */
int ccs_write_env_policy(char *data, struct domain_info *domain,
			 const struct condition_list *condition,
			 const bool is_delete)
{
	return update_env_entry(data, domain, condition, is_delete);
}
