/*
 * Convert NFSv4 acls stored per http://www.suse.de/~agruen/nfs4acl/ to NT acls and vice versa.
 *
 * Copyright (C) Jiri Sasek, 2007
 * based on the foobar.c module which is copyrighted by Volker Lendecke
 * based on pvfs_acl_nfs4.c  Copyright (C) Andrew Tridgell 2006
 *
 * based on vfs_fake_acls:
 * Copyright (C) Tim Potter, 1999-2000
 * Copyright (C) Alexander Bokovoy, 2002
 * Copyright (C) Andrew Bartlett, 2002,2012
 * Copyright (C) Ralph Boehme 2017
 *
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 *
 */

#include "includes.h"
#include "system/filesys.h"
#include "smbd/smbd.h"
#include "libcli/security/security_token.h"
#include "libcli/security/dom_sid.h"
#include "nfs4_acls.h"
#include "librpc/gen_ndr/ndr_nfs4acl.h"
#include "nfs4acl_xattr.h"
#include "nfs4acl_xattr_ndr.h"
#include "nfs4acl_xattr_xdr.h"

#undef DBGC_CLASS
#define DBGC_CLASS DBGC_VFS

static const struct enum_list nfs4acl_encoding[] = {
	{NFS4ACL_ENCODING_NDR, "ndr"},
	{NFS4ACL_ENCODING_XDR, "xdr"},
};

/*
 * Check if someone changed the POSIX mode, for files we expect 0666, for
 * directories 0777. Discard the ACL blob if the mode is different.
 */
static bool nfs4acl_validate_blob(vfs_handle_struct *handle,
				  const struct smb_filename *smb_fname)
{
	struct nfs4acl_config *config = NULL;
	mode_t expected_mode;
	int saved_errno = 0;
	int ret;

	SMB_VFS_HANDLE_GET_DATA(handle, config,
				struct nfs4acl_config,
				return false);

	if (!VALID_STAT(smb_fname->st)) {
		/* might be a create */
		return true;
	}

	if (S_ISDIR(smb_fname->st.st_ex_mode)) {
		expected_mode = 0777;
	} else {
		expected_mode = 0666;
	}
	if ((smb_fname->st.st_ex_mode & expected_mode) == expected_mode) {
		return true;
	}

	become_root();
	ret = SMB_VFS_NEXT_REMOVEXATTR(handle,
				       smb_fname,
				       config->xattr_name);
	if (ret != 0) {
		saved_errno = errno;
	}
	unbecome_root();
	if (saved_errno != 0) {
		errno = saved_errno;
	}
	if (ret != 0 && errno != ENOATTR) {
		DBG_ERR("Removing NFS4 xattr failed: %s\n", strerror(errno));
		return false;
	}

	return true;
}

static NTSTATUS nfs4acl_get_blob(struct vfs_handle_struct *handle,
				 files_struct *fsp,
				 const struct smb_filename *smb_fname_in,
				 TALLOC_CTX *mem_ctx,
				 DATA_BLOB *blob)
{
	struct nfs4acl_config *config = NULL;
	const struct smb_filename *smb_fname = NULL;
	size_t allocsize = 256;
	ssize_t length;
	bool ok;

	SMB_VFS_HANDLE_GET_DATA(handle, config,
				struct nfs4acl_config,
				return NT_STATUS_INTERNAL_ERROR);

	*blob = data_blob_null;

	if (fsp == NULL && smb_fname_in == NULL) {
		return NT_STATUS_INTERNAL_ERROR;
	}
	smb_fname = smb_fname_in;
	if (smb_fname == NULL) {
		smb_fname = fsp->fsp_name;
	}
	if (smb_fname == NULL) {
		return NT_STATUS_INTERNAL_ERROR;
	}

	ok = nfs4acl_validate_blob(handle, smb_fname);
	if (!ok) {
		return NT_STATUS_INTERNAL_ERROR;
	}

	do {
		int saved_errno = 0;

		allocsize *= 4;
		ok = data_blob_realloc(mem_ctx, blob, allocsize);
		if (!ok) {
			return NT_STATUS_NO_MEMORY;
		}

		become_root();
		if (fsp != NULL && fsp->fh->fd != -1) {
			length = SMB_VFS_NEXT_FGETXATTR(handle,
							fsp,
							config->xattr_name,
							blob->data,
							blob->length);
		} else {
			length = SMB_VFS_NEXT_GETXATTR(handle,
						       smb_fname,
						       config->xattr_name,
						       blob->data,
						       blob->length);
		}
		if (length == -1) {
			saved_errno = errno;
		}
		unbecome_root();
		if (saved_errno != 0) {
			errno = saved_errno;
		}
	} while (length == -1 && errno == ERANGE && allocsize <= 65536);

	if (length == -1) {
		return map_nt_error_from_unix(errno);
	}

	return NT_STATUS_OK;
}

static NTSTATUS nfs4acl_xattr_default_sd(
	struct vfs_handle_struct *handle,
	const struct smb_filename *smb_fname,
	TALLOC_CTX *mem_ctx,
	struct security_descriptor **sd)
{
	struct nfs4acl_config *config = NULL;
	enum default_acl_style default_acl_style;
	mode_t required_mode;
	SMB_STRUCT_STAT sbuf = smb_fname->st;
	int ret;

	SMB_VFS_HANDLE_GET_DATA(handle, config,
				struct nfs4acl_config,
				return NT_STATUS_INTERNAL_ERROR);

	default_acl_style = config->default_acl_style;

	if (!VALID_STAT(sbuf)) {
		ret = vfs_stat_smb_basename(handle->conn,
					    smb_fname,
					    &sbuf);
		if (ret != 0) {
			return map_nt_error_from_unix(errno);
		}
	}

	if (S_ISDIR(sbuf.st_ex_mode)) {
		required_mode = 0777;
	} else {
		required_mode = 0666;
	}
	if ((sbuf.st_ex_mode & required_mode) != required_mode) {
		default_acl_style = DEFAULT_ACL_POSIX;
	}

	return make_default_filesystem_acl(mem_ctx,
					   default_acl_style,
					   smb_fname->base_name,
					   &sbuf,
					   sd);
}

static NTSTATUS nfs4acl_blob_to_smb4(struct vfs_handle_struct *handle,
				     DATA_BLOB *blob,
				     TALLOC_CTX *mem_ctx,
				     struct SMB4ACL_T **smb4acl)
{
	struct nfs4acl_config *config = NULL;
	NTSTATUS status;

	SMB_VFS_HANDLE_GET_DATA(handle, config,
				struct nfs4acl_config,
				return NT_STATUS_INTERNAL_ERROR);

	switch (config->encoding) {
	case NFS4ACL_ENCODING_NDR:
		status = nfs4acl_ndr_blob_to_smb4(handle, mem_ctx, blob, smb4acl);
		break;
	case NFS4ACL_ENCODING_XDR:
		status = nfs4acl_xdr_blob_to_smb4(handle, mem_ctx, blob, smb4acl);
		break;
	default:
		status = NT_STATUS_INTERNAL_ERROR;
		break;
	}

	return status;
}

static NTSTATUS nfs4acl_xattr_fget_nt_acl(struct vfs_handle_struct *handle,
				   struct files_struct *fsp,
				   uint32_t security_info,
				   TALLOC_CTX *mem_ctx,
				   struct security_descriptor **sd)
{
	struct SMB4ACL_T *smb4acl = NULL;
	TALLOC_CTX *frame = talloc_stackframe();
	DATA_BLOB blob;
	NTSTATUS status;

	status = nfs4acl_get_blob(handle, fsp, NULL, frame, &blob);
	if (NT_STATUS_EQUAL(status, NT_STATUS_NOT_FOUND)) {
		TALLOC_FREE(frame);
		return nfs4acl_xattr_default_sd(
			handle, fsp->fsp_name, mem_ctx, sd);
	}
	if (!NT_STATUS_IS_OK(status)) {
		TALLOC_FREE(frame);
		return status;
	}

	status = nfs4acl_blob_to_smb4(handle, &blob, frame, &smb4acl);
	if (!NT_STATUS_IS_OK(status)) {
		TALLOC_FREE(frame);
		return status;
	}

	status = smb_fget_nt_acl_nfs4(fsp, NULL, security_info, mem_ctx,
				      sd, smb4acl);
	TALLOC_FREE(frame);
	return status;
}

static NTSTATUS nfs4acl_xattr_get_nt_acl(struct vfs_handle_struct *handle,
				  const struct smb_filename *smb_fname,
				  uint32_t security_info,
				  TALLOC_CTX *mem_ctx,
				  struct security_descriptor **sd)
{
	struct SMB4ACL_T *smb4acl = NULL;
	TALLOC_CTX *frame = talloc_stackframe();
	DATA_BLOB blob;
	NTSTATUS status;

	status = nfs4acl_get_blob(handle, NULL, smb_fname, frame, &blob);
	if (NT_STATUS_EQUAL(status, NT_STATUS_NOT_FOUND)) {
		TALLOC_FREE(frame);
		return nfs4acl_xattr_default_sd(
			handle,	smb_fname, mem_ctx, sd);
	}
	if (!NT_STATUS_IS_OK(status)) {
		TALLOC_FREE(frame);
		return status;
	}

	status = nfs4acl_blob_to_smb4(handle, &blob, frame, &smb4acl);
	if (!NT_STATUS_IS_OK(status)) {
		TALLOC_FREE(frame);
		return status;
	}

	status = smb_get_nt_acl_nfs4(handle->conn, smb_fname, NULL,
				     security_info, mem_ctx, sd,
				     smb4acl);
	TALLOC_FREE(frame);
	return status;
}

static bool nfs4acl_smb4acl_set_fn(vfs_handle_struct *handle,
				   files_struct *fsp,
				   struct SMB4ACL_T *smb4acl)
{
	struct nfs4acl_config *config = NULL;
	DATA_BLOB blob;
	NTSTATUS status;
	int saved_errno = 0;
	int ret;

	SMB_VFS_HANDLE_GET_DATA(handle, config,
				struct nfs4acl_config,
				return false);

	switch (config->encoding) {
	case NFS4ACL_ENCODING_NDR:
		status = nfs4acl_smb4acl_to_ndr_blob(handle, talloc_tos(),
						     smb4acl, &blob);
		break;
	case NFS4ACL_ENCODING_XDR:
		status = nfs4acl_smb4acl_to_xdr_blob(handle, talloc_tos(),
						     smb4acl, &blob);
		break;
	default:
		status = NT_STATUS_INTERNAL_ERROR;
		break;
	}
	if (!NT_STATUS_IS_OK(status)) {
		return false;
	}

	become_root();
	if (fsp->fh->fd != -1) {
		ret = SMB_VFS_NEXT_FSETXATTR(handle, fsp, config->xattr_name,
					     blob.data, blob.length, 0);
	} else {
		ret = SMB_VFS_NEXT_SETXATTR(handle, fsp->fsp_name,
					    config->xattr_name,
					    blob.data, blob.length, 0);
	}
	if (ret != 0) {
		saved_errno = errno;
	}
	unbecome_root();
	data_blob_free(&blob);
	if (saved_errno != 0) {
		errno = saved_errno;
	}
	if (ret != 0) {
		DBG_ERR("can't store acl in xattr: %s\n", strerror(errno));
		return false;
	}

	return true;
}

static NTSTATUS nfs4acl_xattr_fset_nt_acl(vfs_handle_struct *handle,
			 files_struct *fsp,
			 uint32_t security_info_sent,
			 const struct security_descriptor *psd)
{
	struct nfs4acl_config *config = NULL;
	const struct security_token *token = NULL;
	mode_t existing_mode;
	mode_t expected_mode;
	mode_t restored_mode;
	bool chown_needed = false;
	struct dom_sid_buf buf;
	NTSTATUS status;
	int ret;

	SMB_VFS_HANDLE_GET_DATA(handle, config,
				struct nfs4acl_config,
				return NT_STATUS_INTERNAL_ERROR);

	if (!VALID_STAT(fsp->fsp_name->st)) {
		DBG_ERR("Invalid stat info on [%s]\n", fsp_str_dbg(fsp));
		return NT_STATUS_INTERNAL_ERROR;
	}

	existing_mode = fsp->fsp_name->st.st_ex_mode;
	if (S_ISDIR(existing_mode)) {
		expected_mode = 0777;
	} else {
		expected_mode = 0666;
	}
	if ((existing_mode & expected_mode) != expected_mode) {
		int saved_errno = 0;

		restored_mode = existing_mode | expected_mode;

		become_root();
		if (fsp->fh->fd != -1) {
			ret = SMB_VFS_NEXT_FCHMOD(handle,
						  fsp,
						  restored_mode);
		} else {
			ret = SMB_VFS_NEXT_CHMOD(handle,
						 fsp->fsp_name,
						 restored_mode);
		}
		if (ret != 0) {
			saved_errno = errno;
		}
		unbecome_root();
		if (saved_errno != 0) {
			errno = saved_errno;
		}
		if (ret != 0) {
			DBG_ERR("Resetting POSIX mode on [%s] from [0%o]: %s\n",
				fsp_str_dbg(fsp), existing_mode,
				strerror(errno));
			return map_nt_error_from_unix(errno);
		}
	}

	status = smb_set_nt_acl_nfs4(handle,
				     fsp,
				     &config->nfs4_params,
				     security_info_sent,
				     psd,
				     nfs4acl_smb4acl_set_fn);
	if (NT_STATUS_IS_OK(status)) {
		return NT_STATUS_OK;
	}

	/*
	 * We got access denied. If we're already root, or we didn't
	 * need to do a chown, or the fsp isn't open with WRITE_OWNER
	 * access, just return.
	 */

	if ((security_info_sent & SECINFO_OWNER) &&
	    (psd->owner_sid != NULL))
	{
		chown_needed = true;
	}
	if ((security_info_sent & SECINFO_GROUP) &&
	    (psd->group_sid != NULL))
	{
		chown_needed = true;
	}

	if (get_current_uid(handle->conn) == 0 ||
	    chown_needed == false ||
	    !(fsp->access_mask & SEC_STD_WRITE_OWNER))
	{
		return NT_STATUS_ACCESS_DENIED;
	}

	/*
	 * Only allow take-ownership, not give-ownership. That's the way Windows
	 * implements SEC_STD_WRITE_OWNER. MS-FSA 2.1.5.16 just states: If
	 * InputBuffer.OwnerSid is not a valid owner SID for a file in the
	 * objectstore, as determined in an implementation specific manner, the
	 * object store MUST return STATUS_INVALID_OWNER.
	 */
	token = get_current_nttok(fsp->conn);
	if (!security_token_is_sid(token, psd->owner_sid)) {
		return NT_STATUS_INVALID_OWNER;
	}

	DBG_DEBUG("overriding chown on file %s for sid %s\n",
		  fsp_str_dbg(fsp),
		  dom_sid_str_buf(psd->owner_sid, &buf));

	become_root();
	status = smb_set_nt_acl_nfs4(handle,
				     fsp,
				     &config->nfs4_params,
				     security_info_sent,
				     psd,
				     nfs4acl_smb4acl_set_fn);
	unbecome_root();
	return status;
}

static int nfs4acl_connect(struct vfs_handle_struct *handle,
			   const char *service,
			   const char *user)
{
	struct nfs4acl_config *config = NULL;
	const struct enum_list *default_acl_style_list = NULL;
	const char *default_xattr_name = NULL;
	int enumval;
	unsigned nfs_version;
	int ret;

	default_acl_style_list = get_default_acl_style_list();

	config = talloc_zero(handle->conn, struct nfs4acl_config);
	if (config == NULL) {
		DBG_ERR("talloc_zero() failed\n");
		return -1;
	}

	ret = SMB_VFS_NEXT_CONNECT(handle, service, user);
	if (ret < 0) {
		TALLOC_FREE(config);
		return ret;
	}

	ret = smbacl4_get_vfs_params(handle->conn, &config->nfs4_params);
	if (ret < 0) {
		TALLOC_FREE(config);
		return ret;
	}

	enumval = lp_parm_enum(SNUM(handle->conn),
			       "nfs4acl_xattr",
			       "encoding",
			       nfs4acl_encoding,
			       NFS4ACL_ENCODING_NDR);
	if (enumval == -1) {
		DBG_ERR("Invalid \"nfs4acl_xattr:encoding\" parameter\n");
		return -1;
	}
	config->encoding = (enum nfs4acl_encoding)enumval;

	switch (config->encoding) {
	case NFS4ACL_ENCODING_XDR:
		default_xattr_name = NFS4ACL_XDR_XATTR_NAME;
		break;
	case NFS4ACL_ENCODING_NDR:
	default:
		default_xattr_name = NFS4ACL_NDR_XATTR_NAME;
		break;
	}

	nfs_version = (unsigned)lp_parm_int(SNUM(handle->conn),
					    "nfs4acl_xattr",
					    "version",
					    41);
	switch (nfs_version) {
	case 40:
		config->nfs_version = ACL4_XATTR_VERSION_40;
		break;
	case 41:
		config->nfs_version = ACL4_XATTR_VERSION_41;
		break;
	default:
		config->nfs_version = ACL4_XATTR_VERSION_DEFAULT;
		break;
	}

	config->default_acl_style = lp_parm_enum(SNUM(handle->conn),
						 "nfs4acl_xattr",
						 "default acl style",
						 default_acl_style_list,
						 DEFAULT_ACL_EVERYONE);

	config->xattr_name = lp_parm_talloc_string(config,
						   SNUM(handle->conn),
						   "nfs4acl_xattr",
						   "xattr_name",
						   default_xattr_name);

	SMB_VFS_HANDLE_SET_DATA(handle, config, NULL, struct nfs4acl_config,
				return -1);

	/*
	 * Ensure we have the parameters correct if we're using this module.
	 */
	DBG_NOTICE("Setting 'inherit acls = true', "
		   "'dos filemode = true', "
		   "'force unknown acl user = true', "
		   "'create mask = 0666', "
		   "'directory mask = 0777' and "
		   "'store dos attributes = yes' "
		   "for service [%s]\n", service);

	lp_do_parameter(SNUM(handle->conn), "inherit acls", "true");
	lp_do_parameter(SNUM(handle->conn), "dos filemode", "true");
	lp_do_parameter(SNUM(handle->conn), "force unknown acl user", "true");
	lp_do_parameter(SNUM(handle->conn), "create mask", "0666");
	lp_do_parameter(SNUM(handle->conn), "directory mask", "0777");
	lp_do_parameter(SNUM(handle->conn), "store dos attributes", "yes");

	return 0;
}

/*
   As long as Samba does not support an exiplicit method for a module
   to define conflicting vfs methods, we should override all conflicting
   methods here.  That way, we know we are using the NFSv4 storage

   Function declarations taken from vfs_solarisacl
*/

static SMB_ACL_T nfs4acl_xattr_fail__sys_acl_get_file(vfs_handle_struct *handle,
					const struct smb_filename *smb_fname,
					SMB_ACL_TYPE_T type,
					TALLOC_CTX *mem_ctx)
{
	return (SMB_ACL_T)NULL;
}

static SMB_ACL_T nfs4acl_xattr_fail__sys_acl_get_fd(vfs_handle_struct *handle,
						    files_struct *fsp,
						    TALLOC_CTX *mem_ctx)
{
	return (SMB_ACL_T)NULL;
}

static int nfs4acl_xattr_fail__sys_acl_set_file(vfs_handle_struct *handle,
					 const struct smb_filename *smb_fname,
					 SMB_ACL_TYPE_T type,
					 SMB_ACL_T theacl)
{
	return -1;
}

static int nfs4acl_xattr_fail__sys_acl_set_fd(vfs_handle_struct *handle,
				       files_struct *fsp,
				       SMB_ACL_T theacl)
{
	return -1;
}

static int nfs4acl_xattr_fail__sys_acl_delete_def_file(vfs_handle_struct *handle,
			const struct smb_filename *smb_fname)
{
	return -1;
}

static int nfs4acl_xattr_fail__sys_acl_blob_get_file(vfs_handle_struct *handle,
			const struct smb_filename *smb_fname,
			TALLOC_CTX *mem_ctx,
			char **blob_description,
			DATA_BLOB *blob)
{
	return -1;
}

static int nfs4acl_xattr_fail__sys_acl_blob_get_fd(vfs_handle_struct *handle, files_struct *fsp, TALLOC_CTX *mem_ctx, char **blob_description, DATA_BLOB *blob)
{
	return -1;
}

/* VFS operations structure */

static struct vfs_fn_pointers nfs4acl_xattr_fns = {
	.connect_fn = nfs4acl_connect,
	.fget_nt_acl_fn = nfs4acl_xattr_fget_nt_acl,
	.get_nt_acl_fn = nfs4acl_xattr_get_nt_acl,
	.fset_nt_acl_fn = nfs4acl_xattr_fset_nt_acl,

	.sys_acl_get_file_fn = nfs4acl_xattr_fail__sys_acl_get_file,
	.sys_acl_get_fd_fn = nfs4acl_xattr_fail__sys_acl_get_fd,
	.sys_acl_blob_get_file_fn = nfs4acl_xattr_fail__sys_acl_blob_get_file,
	.sys_acl_blob_get_fd_fn = nfs4acl_xattr_fail__sys_acl_blob_get_fd,
	.sys_acl_set_file_fn = nfs4acl_xattr_fail__sys_acl_set_file,
	.sys_acl_set_fd_fn = nfs4acl_xattr_fail__sys_acl_set_fd,
	.sys_acl_delete_def_file_fn = nfs4acl_xattr_fail__sys_acl_delete_def_file,
};

static_decl_vfs;
NTSTATUS vfs_nfs4acl_xattr_init(TALLOC_CTX *ctx)
{
	return smb_register_vfs(SMB_VFS_INTERFACE_VERSION, "nfs4acl_xattr",
				&nfs4acl_xattr_fns);
}
