/* AFS security handling
 *
 * Copyright (C) 2007, 2017 Red Hat, Inc. All Rights Reserved.
 * Written by David Howells (dhowells@redhat.com)
 *
 * 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.
 */

#include <linux/init.h>
#include <linux/slab.h>
#include <linux/fs.h>
#include <linux/ctype.h>
#include <linux/sched.h>
#include <linux/hashtable.h>
#include <keys/rxrpc-type.h>
#include "internal.h"

static DEFINE_HASHTABLE(afs_permits_cache, 10);
static DEFINE_SPINLOCK(afs_permits_lock);

/*
 * get a key
 */
struct key *afs_request_key(struct afs_cell *cell)
{
	struct key *key;

	_enter("{%x}", key_serial(cell->anonymous_key));

	_debug("key %s", cell->anonymous_key->description);
	key = request_key(&key_type_rxrpc, cell->anonymous_key->description,
			  NULL);
	if (IS_ERR(key)) {
		if (PTR_ERR(key) != -ENOKEY) {
			_leave(" = %ld", PTR_ERR(key));
			return key;
		}

		/* act as anonymous user */
		_leave(" = {%x} [anon]", key_serial(cell->anonymous_key));
		return key_get(cell->anonymous_key);
	} else {
		/* act as authorised user */
		_leave(" = {%x} [auth]", key_serial(key));
		return key;
	}
}

/*
 * Dispose of a list of permits.
 */
static void afs_permits_rcu(struct rcu_head *rcu)
{
	struct afs_permits *permits =
		container_of(rcu, struct afs_permits, rcu);
	int i;

	for (i = 0; i < permits->nr_permits; i++)
		key_put(permits->permits[i].key);
	kfree(permits);
}

/*
 * Discard a permission cache.
 */
void afs_put_permits(struct afs_permits *permits)
{
	if (permits && refcount_dec_and_test(&permits->usage)) {
		spin_lock(&afs_permits_lock);
		hash_del_rcu(&permits->hash_node);
		spin_unlock(&afs_permits_lock);
		call_rcu(&permits->rcu, afs_permits_rcu);
	}
}

/*
 * Clear a permit cache on callback break.
 */
void afs_clear_permits(struct afs_vnode *vnode)
{
	struct afs_permits *permits;

	spin_lock(&vnode->lock);
	permits = rcu_dereference_protected(vnode->permit_cache,
					    lockdep_is_held(&vnode->lock));
	RCU_INIT_POINTER(vnode->permit_cache, NULL);
	spin_unlock(&vnode->lock);

	afs_put_permits(permits);
}

/*
 * Hash a list of permits.  Use simple addition to make it easy to add an extra
 * one at an as-yet indeterminate position in the list.
 */
static void afs_hash_permits(struct afs_permits *permits)
{
	unsigned long h = permits->nr_permits;
	int i;

	for (i = 0; i < permits->nr_permits; i++) {
		h += (unsigned long)permits->permits[i].key / sizeof(void *);
		h += permits->permits[i].access;
	}

	permits->h = h;
}

/*
 * Cache the CallerAccess result obtained from doing a fileserver operation
 * that returned a vnode status for a particular key.  If a callback break
 * occurs whilst the operation was in progress then we have to ditch the cache
 * as the ACL *may* have changed.
 */
void afs_cache_permit(struct afs_vnode *vnode, struct key *key,
		      unsigned int cb_break)
{
	struct afs_permits *permits, *xpermits, *replacement, *zap, *new = NULL;
	afs_access_t caller_access = READ_ONCE(vnode->status.caller_access);
	size_t size = 0;
	bool changed = false;
	int i, j;

	_enter("{%x:%u},%x,%x",
	       vnode->fid.vid, vnode->fid.vnode, key_serial(key), caller_access);

	rcu_read_lock();

	/* Check for the common case first: We got back the same access as last
	 * time we tried and already have it recorded.
	 */
	permits = rcu_dereference(vnode->permit_cache);
	if (permits) {
		if (!permits->invalidated) {
			for (i = 0; i < permits->nr_permits; i++) {
				if (permits->permits[i].key < key)
					continue;
				if (permits->permits[i].key > key)
					break;
				if (permits->permits[i].access != caller_access) {
					changed = true;
					break;
				}

				if (cb_break != afs_cb_break_sum(vnode, vnode->cb_interest)) {
					changed = true;
					break;
				}

				/* The cache is still good. */
				rcu_read_unlock();
				return;
			}
		}

		changed |= permits->invalidated;
		size = permits->nr_permits;

		/* If this set of permits is now wrong, clear the permits
		 * pointer so that no one tries to use the stale information.
		 */
		if (changed) {
			spin_lock(&vnode->lock);
			if (permits != rcu_access_pointer(vnode->permit_cache))
				goto someone_else_changed_it_unlock;
			RCU_INIT_POINTER(vnode->permit_cache, NULL);
			spin_unlock(&vnode->lock);

			afs_put_permits(permits);
			permits = NULL;
			size = 0;
		}
	}

	if (cb_break != afs_cb_break_sum(vnode, vnode->cb_interest))
		goto someone_else_changed_it;

	/* We need a ref on any permits list we want to copy as we'll have to
	 * drop the lock to do memory allocation.
	 */
	if (permits && !refcount_inc_not_zero(&permits->usage))
		goto someone_else_changed_it;

	rcu_read_unlock();

	/* Speculatively create a new list with the revised permission set.  We
	 * discard this if we find an extant match already in the hash, but
	 * it's easier to compare with memcmp this way.
	 *
	 * We fill in the key pointers at this time, but we don't get the refs
	 * yet.
	 */
	size++;
	new = kzalloc(sizeof(struct afs_permits) +
		      sizeof(struct afs_permit) * size, GFP_NOFS);
	if (!new)
		goto out_put;

	refcount_set(&new->usage, 1);
	new->nr_permits = size;
	i = j = 0;
	if (permits) {
		for (i = 0; i < permits->nr_permits; i++) {
			if (j == i && permits->permits[i].key > key) {
				new->permits[j].key = key;
				new->permits[j].access = caller_access;
				j++;
			}
			new->permits[j].key = permits->permits[i].key;
			new->permits[j].access = permits->permits[i].access;
			j++;
		}
	}

	if (j == i) {
		new->permits[j].key = key;
		new->permits[j].access = caller_access;
	}

	afs_hash_permits(new);

	/* Now see if the permit list we want is actually already available */
	spin_lock(&afs_permits_lock);

	hash_for_each_possible(afs_permits_cache, xpermits, hash_node, new->h) {
		if (xpermits->h != new->h ||
		    xpermits->invalidated ||
		    xpermits->nr_permits != new->nr_permits ||
		    memcmp(xpermits->permits, new->permits,
			   new->nr_permits * sizeof(struct afs_permit)) != 0)
			continue;

		if (refcount_inc_not_zero(&xpermits->usage)) {
			replacement = xpermits;
			goto found;
		}

		break;
	}

	for (i = 0; i < new->nr_permits; i++)
		key_get(new->permits[i].key);
	hash_add_rcu(afs_permits_cache, &new->hash_node, new->h);
	replacement = new;
	new = NULL;

found:
	spin_unlock(&afs_permits_lock);

	kfree(new);

	spin_lock(&vnode->lock);
	zap = rcu_access_pointer(vnode->permit_cache);
	if (cb_break == afs_cb_break_sum(vnode, vnode->cb_interest) &&
	    zap == permits)
		rcu_assign_pointer(vnode->permit_cache, replacement);
	else
		zap = replacement;
	spin_unlock(&vnode->lock);
	afs_put_permits(zap);
out_put:
	afs_put_permits(permits);
	return;

someone_else_changed_it_unlock:
	spin_unlock(&vnode->lock);
someone_else_changed_it:
	/* Someone else changed the cache under us - don't recheck at this
	 * time.
	 */
	rcu_read_unlock();
	return;
}

/*
 * check with the fileserver to see if the directory or parent directory is
 * permitted to be accessed with this authorisation, and if so, what access it
 * is granted
 */
int afs_check_permit(struct afs_vnode *vnode, struct key *key,
		     afs_access_t *_access)
{
	struct afs_permits *permits;
	bool valid = false;
	int i, ret;

	_enter("{%x:%u},%x",
	       vnode->fid.vid, vnode->fid.vnode, key_serial(key));

	/* check the permits to see if we've got one yet */
	if (key == vnode->volume->cell->anonymous_key) {
		_debug("anon");
		*_access = vnode->status.anon_access;
		valid = true;
	} else {
		rcu_read_lock();
		permits = rcu_dereference(vnode->permit_cache);
		if (permits) {
			for (i = 0; i < permits->nr_permits; i++) {
				if (permits->permits[i].key < key)
					continue;
				if (permits->permits[i].key > key)
					break;

				*_access = permits->permits[i].access;
				valid = !permits->invalidated;
				break;
			}
		}
		rcu_read_unlock();
	}

	if (!valid) {
		/* Check the status on the file we're actually interested in
		 * (the post-processing will cache the result).
		 */
		_debug("no valid permit");

		ret = afs_fetch_status(vnode, key, false);
		if (ret < 0) {
			*_access = 0;
			_leave(" = %d", ret);
			return ret;
		}
		*_access = vnode->status.caller_access;
	}

	_leave(" = 0 [access %x]", *_access);
	return 0;
}

/*
 * check the permissions on an AFS file
 * - AFS ACLs are attached to directories only, and a file is controlled by its
 *   parent directory's ACL
 */
int afs_permission(struct inode *inode, int mask)
{
	struct afs_vnode *vnode = AFS_FS_I(inode);
	afs_access_t uninitialized_var(access);
	struct key *key;
	int ret;

	if (mask & MAY_NOT_BLOCK)
		return -ECHILD;

	_enter("{{%x:%u},%lx},%x,",
	       vnode->fid.vid, vnode->fid.vnode, vnode->flags, mask);

	key = afs_request_key(vnode->volume->cell);
	if (IS_ERR(key)) {
		_leave(" = %ld [key]", PTR_ERR(key));
		return PTR_ERR(key);
	}

	ret = afs_validate(vnode, key);
	if (ret < 0)
		goto error;

	/* check the permits to see if we've got one yet */
	ret = afs_check_permit(vnode, key, &access);
	if (ret < 0)
		goto error;

	/* interpret the access mask */
	_debug("REQ %x ACC %x on %s",
	       mask, access, S_ISDIR(inode->i_mode) ? "dir" : "file");

	if (S_ISDIR(inode->i_mode)) {
		if (mask & (MAY_EXEC | MAY_READ | MAY_CHDIR)) {
			if (!(access & AFS_ACE_LOOKUP))
				goto permission_denied;
		}
		if (mask & MAY_WRITE) {
			if (!(access & (AFS_ACE_DELETE | /* rmdir, unlink, rename from */
					AFS_ACE_INSERT))) /* create, mkdir, symlink, rename to */
				goto permission_denied;
		}
	} else {
		if (!(access & AFS_ACE_LOOKUP))
			goto permission_denied;
		if ((mask & MAY_EXEC) && !(inode->i_mode & S_IXUSR))
			goto permission_denied;
		if (mask & (MAY_EXEC | MAY_READ)) {
			if (!(access & AFS_ACE_READ))
				goto permission_denied;
			if (!(inode->i_mode & S_IRUSR))
				goto permission_denied;
		} else if (mask & MAY_WRITE) {
			if (!(access & AFS_ACE_WRITE))
				goto permission_denied;
			if (!(inode->i_mode & S_IWUSR))
				goto permission_denied;
		}
	}

	key_put(key);
	_leave(" = %d", ret);
	return ret;

permission_denied:
	ret = -EACCES;
error:
	key_put(key);
	_leave(" = %d", ret);
	return ret;
}

void __exit afs_clean_up_permit_cache(void)
{
	int i;

	for (i = 0; i < HASH_SIZE(afs_permits_cache); i++)
		WARN_ON_ONCE(!hlist_empty(&afs_permits_cache[i]));

}
