/*
   ldb database module utility library

   Copyright (C) Andrew Bartlett <abartlet@samba.org> 2018

   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/>.
*/

/*
 * Common utility functions for SamDb audit logging.
 *
 */

#include "common/util.h"
#include "includes.h"
#include "ldb_module.h"
#include "lib/audit_logging/audit_logging.h"

#include "dsdb/samdb/samdb.h"
#include "dsdb/samdb/ldb_modules/util.h"
#include "libcli/security/dom_sid.h"
#include "libcli/security/security_token.h"
#include "auth/common_auth.h"
#include "param/param.h"
#include "dsdb/samdb/ldb_modules/audit_util_proto.h"

#define MAX_LENGTH 1024

/*
 * List of attributes considered secret or confidential the values of these
 * attributes should not be displayed in log messages.
 */
static const char * const secret_attributes[] = {
	DSDB_SECRET_ATTRIBUTES,
	NULL};
/*
 * List of attributes that contain a password, used to detect password changes
 */
static const char * const password_attributes[] = {
	DSDB_PASSWORD_ATTRIBUTES,
	NULL};

/*
 * List of attributes that while not secret, do control the authentication
 * of an account.
 */
static const char * const  authentication_attributes[] = {
	DSDB_AUTHENTICATION_ATTRIBUTES,
	NULL};

/*
 * @brief Should the value of the specified value be redacted.
 *
 * The values of secret or password attributes should not be displayed.
 *
 * @param name The attributes name.
 *
 * @return True if the attribute should be redacted
 */
bool dsdb_audit_redact_attribute(const char * name)
{

	if (ldb_attr_in_list(secret_attributes, name)) {
		return true;
	}

	if (ldb_attr_in_list(password_attributes, name)) {
		return true;
	}

	return false;
}

/*
 * @brief is the attribute a password attribute?
 *
 * Is the attribute a password attribute.
 *
 * @return True if the attribute is a "Password" attribute.
 */
bool dsdb_audit_is_password_attribute(const char * name)
{

	bool is_password = ldb_attr_in_list(password_attributes, name);
	return is_password;
}


/*
 * @brief is the attribute an authentication information attribute?
 *
 * Is the attribute a authentication information attribute.
 *
 * @return True if the attribute is an "Authentication Information" attribute.
 */
bool dsdb_audit_is_authentication_information(const char * name)
{

	bool is_auth_info = ldb_attr_in_list(authentication_attributes, name);
	return is_auth_info;
}

/*
 * @brief Get the remote address from the ldb context.
 *
 * The remote address is stored in the ldb opaque value "remoteAddress"
 * it is the responsibility of the higher level code to ensure that this
 * value is set.
 *
 * @param ldb the ldb_context.
 *
 * @return the remote address if known, otherwise NULL.
 */
const struct tsocket_address *dsdb_audit_get_remote_address(
	struct ldb_context *ldb)
{
	void *opaque_remote_address = NULL;
	struct tsocket_address *remote_address;

	opaque_remote_address = ldb_get_opaque(ldb,
					       "remoteAddress");
	if (opaque_remote_address == NULL) {
		return NULL;
	}

	remote_address = talloc_get_type(opaque_remote_address,
					 struct tsocket_address);
	return remote_address;
}

/*
 * @brief Get the actual user SID from ldb context.
 *
 * The actual user SID is stored in the ldb opaque value "networkSessionInfo"
 * it is the responsibility of the higher level code to ensure that this
 * value is set.
 *
 * @param ldb the ldb_context.
 *
 * @return the users actual sid.
 */
const struct dom_sid *dsdb_audit_get_actual_sid(struct ldb_context *ldb)
{
	void *opaque_session = NULL;
	struct auth_session_info *session = NULL;
	struct security_token *user_token = NULL;

	opaque_session = ldb_get_opaque(ldb, DSDB_NETWORK_SESSION_INFO);
	if (opaque_session == NULL) {
		return NULL;
	}

	session = talloc_get_type(opaque_session, struct auth_session_info);
	if (session == NULL) {
		return NULL;
	}

	user_token = session->security_token;
	if (user_token == NULL) {
		return NULL;
	}
	return &user_token->sids[PRIMARY_USER_SID_INDEX];
}
/*
 * @brief get the ldb error string.
 *
 * Get the ldb error string if set, otherwise get the generic error code
 * for the status code.
 *
 * @param ldb the ldb_context.
 * @param status the ldb_status code.
 *
 * @return a string describing the error.
 */
const char *dsdb_audit_get_ldb_error_string(
	struct ldb_module *module,
	int status)
{
	struct ldb_context *ldb = ldb_module_get_ctx(module);
	const char *err_string = ldb_errstring(ldb);

	if (err_string == NULL) {
		return ldb_strerror(status);
	}
	return err_string;
}

/*
 * @brief get the SID of the user performing the operation.
 *
 * Get the SID of the user performing the operation.
 *
 * @param module the ldb_module.
 *
 * @return the SID of the currently logged on user.
 */
const struct dom_sid *dsdb_audit_get_user_sid(const struct ldb_module *module)
{
	struct security_token *user_token = NULL;

	/*
	 * acl_user_token does not alter module so it's safe
	 * to discard the const.
	 */
	user_token = acl_user_token(discard_const(module));
	if (user_token == NULL) {
		return NULL;
	}
	return &user_token->sids[PRIMARY_USER_SID_INDEX];

}

/*
 * @brief is operation being performed using the system session.
 *
 * Is the operation being performed using the system session.
 *
 * @param module the ldb_module.
 *
 * @return true if the operation is being performed using the system session.
 */
bool dsdb_audit_is_system_session(const struct ldb_module *module)
{
	struct security_token *user_token = NULL;

	/*
	 * acl_user_token does not alter module and security_token_is_system
	 * does not alter the security token so it's safe to discard the const.
	 */
	user_token = acl_user_token(discard_const(module));
	if (user_token == NULL) {
		return false;
	}
	return security_token_is_system(user_token);;

}

/*
 * @brief get the session identifier GUID
 *
 * Get the GUID that uniquely identifies the current authenticated session.
 *
 * @param module the ldb_module.
 *
 * @return the unique session GUID
 */
const struct GUID *dsdb_audit_get_unique_session_token(
	const struct ldb_module *module)
{
	struct ldb_context *ldb = ldb_module_get_ctx(discard_const(module));
	struct auth_session_info *session_info
		= (struct auth_session_info *)ldb_get_opaque(
			ldb,
			DSDB_SESSION_INFO);
	if(!session_info) {
		return NULL;
	}
	return &session_info->unique_session_token;
}

/*
 * @brief get the actual user session identifier
 *
 * Get the GUID that uniquely identifies the current authenticated session.
 * This is the session of the connected user, as it may differ from the
 * session the operation is being performed as, i.e. for operations performed
 * under the system session.
 *
 * @param context the ldb_context.
 *
 * @return the unique session GUID
 */
const struct GUID *dsdb_audit_get_actual_unique_session_token(
	struct ldb_context *ldb)
{
	struct auth_session_info *session_info
		= (struct auth_session_info *)ldb_get_opaque(
			ldb,
			DSDB_NETWORK_SESSION_INFO);
	if(!session_info) {
		return NULL;
	}
	return &session_info->unique_session_token;
}

/*
 * @brief Get a printable string value for the remote host address.
 *
 * Get a printable string representation of the remote host, for display in the
 * the audit logs.
 *
 * @param ldb the ldb context.
 * @param mem_ctx the talloc memory context that will own the returned string.
 *
 * @return A string representation of the remote host address or "Unknown"
 *
 */
char *dsdb_audit_get_remote_host(struct ldb_context *ldb, TALLOC_CTX *mem_ctx)
{
	const struct tsocket_address *remote_address;
	char* remote_host = NULL;

	remote_address = dsdb_audit_get_remote_address(ldb);
	if (remote_address == NULL) {
		remote_host = talloc_asprintf(mem_ctx, "Unknown");
		return remote_host;
	}

	remote_host = tsocket_address_string(remote_address, mem_ctx);
	return remote_host;
}

/*
 * @brief get a printable representation of the primary DN.
 *
 * Get a printable representation of the primary DN. The primary DN is the
 * DN of the object being added, deleted, modified or renamed.
 *
 * @param the ldb_request.
 *
 * @return a printable and linearized DN
 */
const char* dsdb_audit_get_primary_dn(const struct ldb_request *request)
{
	struct ldb_dn *dn = NULL;
	switch (request->operation) {
	case LDB_ADD:
		if (request->op.add.message != NULL) {
			dn = request->op.add.message->dn;
		}
		break;
	case LDB_MODIFY:
		if (request->op.mod.message != NULL) {
			dn = request->op.mod.message->dn;
		}
		break;
	case LDB_DELETE:
		dn = request->op.del.dn;
		break;
	case LDB_RENAME:
		dn = request->op.rename.olddn;
		break;
	default:
		dn = NULL;
		break;
	}
	if (dn == NULL) {
		return NULL;
	}
	return ldb_dn_get_linearized(dn);
}

/*
 * @brief Get the ldb_message from a request.
 *
 * Get the ldb_message for the request, returns NULL is there is no
 * associated ldb_message
 *
 * @param The request
 *
 * @return the message associated with this request, or NULL
 */
const struct ldb_message *dsdb_audit_get_message(
	const struct ldb_request *request)
{
	switch (request->operation) {
	case LDB_ADD:
		return request->op.add.message;
	case LDB_MODIFY:
		return request->op.mod.message;
	default:
		return NULL;
	}
}

/*
 * @brief get the secondary dn, i.e. the target dn for a rename.
 *
 * Get the secondary dn, i.e. the target for a rename. This is only applicable
 * got a rename operation, for the non rename operations this function returns
 * NULL.
 *
 * @param request the ldb_request.
 *
 * @return the secondary dn in a printable and linearized form.
 */
const char *dsdb_audit_get_secondary_dn(const struct ldb_request *request)
{
	switch (request->operation) {
	case LDB_RENAME:
		return ldb_dn_get_linearized(request->op.rename.newdn);
	default:
		return NULL;
	}
}

/*
 * @brief Map the request operation to a description.
 *
 * Get a description of the operation for logging
 *
 * @param request the ldb_request
 *
 * @return a string describing the operation, or "Unknown" if the operation
 *         is not known.
 */
const char *dsdb_audit_get_operation_name(const struct ldb_request *request)
{
	switch (request->operation) {
	case LDB_SEARCH:
		return "Search";
	case LDB_ADD:
		return "Add";
	case LDB_MODIFY:
		return "Modify";
	case LDB_DELETE:
		return "Delete";
	case LDB_RENAME:
		return "Rename";
	case LDB_EXTENDED:
		return "Extended";
	case LDB_REQ_REGISTER_CONTROL:
		return "Register Control";
	case LDB_REQ_REGISTER_PARTITION:
		return "Register Partition";
	default:
		return "Unknown";
	}
}

/*
 * @brief get a description of a modify action for logging.
 *
 * Get a brief description of the modification action suitable for logging.
 *
 * @param flags the ldb_attributes flags.
 *
 * @return a brief description, or "unknown".
 */
const char *dsdb_audit_get_modification_action(unsigned int flags)
{
	switch (LDB_FLAG_MOD_TYPE(flags)) {
	case LDB_FLAG_MOD_ADD:
		return "add";
	case LDB_FLAG_MOD_DELETE:
		return "delete";
	case LDB_FLAG_MOD_REPLACE:
		return "replace";
	default:
		return "unknown";
	}
}

/*
 * @brief Add an ldb_value to a json object array
 *
 * Convert the current ldb_value to a JSON object and append it to array.
 * {
 *	"value":"xxxxxxxx",
 *	"base64":true
 *	"truncated":true
 * }
 *
 * value     is the JSON string representation of the ldb_val,
 *           will be null if the value is zero length. The value will be
 *           truncated if it is more than MAX_LENGTH bytes long. It will also
 *           be base64 encoded if it contains any non printable characters.
 *
 * base64    Indicates that the value is base64 encoded, will be absent if the
 *           value is not encoded.
 *
 * truncated Indicates that the length of the value exceeded MAX_LENGTH and was
 *           truncated.  Note that values are truncated and then base64 encoded.
 *           so an encoded value can be longer than MAX_LENGTH.
 *
 * @param array the JSON array to append the value to.
 * @param lv the ldb_val to convert and append to the array.
 *
 */
static int dsdb_audit_add_ldb_value(struct json_object *array,
				    const struct ldb_val lv)
{
	bool base64;
	int len;
	struct json_object value = json_empty_object;
	int rc = 0;

	json_assert_is_array(array);
	if (json_is_invalid(array)) {
		return -1;
	}

	if (lv.length == 0 || lv.data == NULL) {
		rc = json_add_object(array, NULL, NULL);
		if (rc != 0) {
			goto failure;
		}
		return 0;
	}

	base64 = ldb_should_b64_encode(NULL, &lv);
	len = MIN(lv.length, MAX_LENGTH);
	value = json_new_object();
	if (json_is_invalid(&value)) {
		goto failure;
	}

	if (lv.length > MAX_LENGTH) {
		rc = json_add_bool(&value, "truncated", true);
		if (rc != 0) {
			goto failure;
		}
	}
	if (base64) {
		TALLOC_CTX *ctx = talloc_new(NULL);
		char *encoded = ldb_base64_encode(
			ctx,
			(char*) lv.data,
			len);

		if (ctx == NULL) {
			goto failure;
		}

		rc = json_add_bool(&value, "base64", true);
		if (rc != 0) {
			TALLOC_FREE(ctx);
			goto failure;
		}
		rc = json_add_string(&value, "value", encoded);
		if (rc != 0) {
			TALLOC_FREE(ctx);
			goto failure;
		}
		TALLOC_FREE(ctx);
	} else {
		rc = json_add_stringn(&value, "value", (char *)lv.data, len);
		if (rc != 0) {
			goto failure;
		}
	}
	/*
	 * As array is a JSON array the element name is NULL
	 */
	rc = json_add_object(array, NULL, &value);
	if (rc != 0) {
		goto failure;
	}
	return 0;
failure:
	/*
	 * In the event of a failure value will not have been added to array
	 * so it needs to be freed to prevent a leak.
	 */
	json_free(&value);
	DBG_ERR("unable to add ldb value to JSON audit message\n");
	return -1;
}

/*
 * @brief Build a JSON object containing the attributes in an ldb_message.
 *
 * Build a JSON object containing all the attributes in an ldb_message.
 * The attributes are keyed by attribute name, the values of "secret attributes"
 * are suppressed.
 *
 * {
 * 	"password":{
 * 		"redacted":true,
 * 		"action":"delete"
 * 	},
 * 	"name":{
 * 		"values": [
 * 			{
 *				"value":"xxxxxxxx",
 *				"base64":true
 *				"truncated":true
 *			},
 * 		],
 * 		"action":"add",
 * 	}
 * }
 *
 * values is an array of json objects generated by add_ldb_value.
 * redacted indicates that the attribute is secret.
 * action is only set for modification operations.
 *
 * @param operation the ldb operation being performed
 * @param message the ldb_message to process.
 *
 * @return A populated json object.
 *
 */
struct json_object dsdb_audit_attributes_json(
	enum ldb_request_type operation,
	const struct ldb_message* message)
{

	unsigned int i, j;
	struct json_object attributes = json_new_object();

	if (json_is_invalid(&attributes)) {
		goto failure;
	}
	for (i=0;i<message->num_elements;i++) {
		struct json_object actions = json_empty_object;
		struct json_object attribute = json_empty_object;
		struct json_object action = json_empty_object;
		const char *name = message->elements[i].name;
		int rc = 0;

		action = json_new_object();
		if (json_is_invalid(&action)) {
			goto failure;
		}

		/*
		 * If this is a modify operation tag the attribute with
		 * the modification action.
		 */
		if (operation == LDB_MODIFY) {
			const char *act = NULL;
			const int flags =  message->elements[i].flags;
			act = dsdb_audit_get_modification_action(flags);
			rc = json_add_string(&action, "action", act);
			if (rc != 0) {
				json_free(&action);
				goto failure;
			}
		}
		if (operation == LDB_ADD) {
			rc = json_add_string(&action, "action", "add");
			if (rc != 0) {
				json_free(&action);
				goto failure;
			}
		}

		/*
		 * If the attribute is a secret attribute, tag it as redacted
		 * and don't include the values
		 */
		if (dsdb_audit_redact_attribute(name)) {
			rc = json_add_bool(&action, "redacted", true);
			if (rc != 0) {
				json_free(&action);
				goto failure;
			}
		} else {
			struct json_object values;
			/*
			 * Add the values for the action
			 */
			values = json_new_array();
			if (json_is_invalid(&values)) {
				json_free(&action);
				goto failure;
			}

			for (j=0;j<message->elements[i].num_values;j++) {
				rc = dsdb_audit_add_ldb_value(
				    &values, message->elements[i].values[j]);
				if (rc != 0) {
					json_free(&values);
					json_free(&action);
					goto failure;
				}
			}
			rc = json_add_object(&action, "values", &values);
			if (rc != 0) {
				json_free(&values);
				json_free(&action);
				goto failure;
			}
		}
		attribute = json_get_object(&attributes, name);
		if (json_is_invalid(&attribute)) {
			json_free(&action);
			goto failure;
		}
		actions = json_get_array(&attribute, "actions");
		if (json_is_invalid(&actions)) {
			json_free(&action);
			goto failure;
		}
		rc = json_add_object(&actions, NULL, &action);
		if (rc != 0) {
			json_free(&action);
			goto failure;
		}
		rc = json_add_object(&attribute, "actions", &actions);
		if (rc != 0) {
			json_free(&actions);
			goto failure;
		}
		rc = json_add_object(&attributes, name, &attribute);
		if (rc != 0) {
			json_free(&attribute);
			goto failure;
		}
	}
	return attributes;
failure:
	json_free(&attributes);
	DBG_ERR("Unable to create ldb attributes JSON audit message\n");
	return attributes;
}

/*
 * @brief Generate a human readable string, detailing attributes in a message
 *
 * For modify operations each attribute is prefixed with the action.
 * Normal values are enclosed in []
 * Base64 values are enclosed in {}
 * Truncated values are indicated by three trailing dots "..."
 *
 * @param[in]  ldb       the ldb_context
 * @param[out] buffer    the attributes will be appended to the buffer.
 *                       assumed to have been allocated via talloc.
 * @param[in]  operation the operation type
 * @param[in]  message   the message to process
 *
 * @return a pointer to buffer
 *
 */
char *dsdb_audit_log_attributes(
	struct ldb_context *ldb,
	char *buffer,
	enum ldb_request_type operation,
	const struct ldb_message *message)
{
	size_t i, j;
	for (i=0;i<message->num_elements;i++) {
		if (i > 0) {
			buffer = talloc_asprintf_append_buffer(buffer, " ");
		}

		if (message->elements[i].name == NULL) {
			ldb_debug(
				ldb,
				LDB_DEBUG_ERROR,
				"Error: Invalid element name (NULL) at "
				"position %zu", i);
			return NULL;
		}

		if (operation == LDB_MODIFY) {
			const char *action =NULL;
			action = dsdb_audit_get_modification_action(
				message->elements[i].flags);
			buffer = talloc_asprintf_append_buffer(
				buffer,
				"%s: %s ",
				action,
				message->elements[i].name);
		} else {
			buffer = talloc_asprintf_append_buffer(
				buffer,
				"%s ",
				message->elements[i].name);
		}

		if (dsdb_audit_redact_attribute(message->elements[i].name)) {
			/*
			 * Do not log the value of any secret or password
			 * attributes
			 */
			buffer = talloc_asprintf_append_buffer(
				buffer,
				"[REDACTED SECRET ATTRIBUTE]");
			continue;
		}

		for (j=0;j<message->elements[i].num_values;j++) {
			struct ldb_val v;
			bool use_b64_encode = false;
			size_t length;
			if (j > 0) {
				buffer = talloc_asprintf_append_buffer(
					buffer,
					" ");
			}

			v = message->elements[i].values[j];
			length = MIN(MAX_LENGTH, v.length);
			use_b64_encode = ldb_should_b64_encode(ldb, &v);
			if (use_b64_encode) {
				const char *encoded = ldb_base64_encode(
					buffer,
					(char *)v.data,
					length);
				buffer = talloc_asprintf_append_buffer(
					buffer,
				        "{%s%s}",
					encoded,
					(v.length > MAX_LENGTH ? "..." : ""));
			} else {
				buffer = talloc_asprintf_append_buffer(
					buffer,
					"[%*.*s%s]",
					(int)length,
					(int)length,
					(char *)v.data,
					(v.length > MAX_LENGTH ? "..." : ""));
			}
		}
	}
	return buffer;
}

/*
 * @brief generate a human readable log entry detailing an ldb operation.
 *
 * Generate a human readable log entry detailing an ldb operation.
 *
 * @param[in] mem_ctx the talloc context owning the returned string.
 * @param[in] module  the ldb module
 * @param[in] request the request
 * @param[in] reply   the result of the operation
 *
 * @return the log entry.
 *
 */
char *dsdb_audit_operation_human_readable(
	TALLOC_CTX *mem_ctx,
	struct ldb_module *module,
	const struct ldb_request *request,
	const struct ldb_reply *reply)
{
	struct ldb_context *ldb = NULL;
	const char *remote_host = NULL;
	const struct tsocket_address *remote = NULL;
	const struct dom_sid *sid = NULL;
	struct dom_sid_buf user_sid;
	const char *timestamp = NULL;
	const char *op_name = NULL;
	char *log_entry = NULL;
	const char *dn = NULL;
	const char *new_dn = NULL;
	const struct ldb_message *message = NULL;

	TALLOC_CTX *ctx = talloc_new(NULL);

	ldb = ldb_module_get_ctx(module);

	remote_host = dsdb_audit_get_remote_host(ldb, ctx);
	remote = dsdb_audit_get_remote_address(ldb);
	if (remote != NULL && dsdb_audit_is_system_session(module)) {
		sid = dsdb_audit_get_actual_sid(ldb);
	} else {
		sid = dsdb_audit_get_user_sid(module);
	}
	timestamp = audit_get_timestamp(ctx);
	op_name = dsdb_audit_get_operation_name(request);
	dn = dsdb_audit_get_primary_dn(request);
	new_dn = dsdb_audit_get_secondary_dn(request);

	message = dsdb_audit_get_message(request);

	log_entry = talloc_asprintf(
		mem_ctx,
		"[%s] at [%s] status [%s] "
		"remote host [%s] SID [%s] DN [%s]",
		op_name,
		timestamp,
		ldb_strerror(reply->error),
		remote_host,
		dom_sid_str_buf(sid, &user_sid),
		dn);
	if (new_dn != NULL) {
		log_entry = talloc_asprintf_append_buffer(
			log_entry,
			" New DN [%s]",
			new_dn);
	}
	if (message != NULL) {
		log_entry = talloc_asprintf_append_buffer(log_entry,
							  " attributes [");
		log_entry = dsdb_audit_log_attributes(ldb,
						      log_entry,
						      request->operation,
						      message);
		log_entry = talloc_asprintf_append_buffer(log_entry, "]");
	}
	TALLOC_FREE(ctx);
	return log_entry;
}
