diff options
Diffstat (limited to 'source4/dsdb/samdb/ldb_modules/group_audit.c')
-rw-r--r-- | source4/dsdb/samdb/ldb_modules/group_audit.c | 1555 |
1 files changed, 1555 insertions, 0 deletions
diff --git a/source4/dsdb/samdb/ldb_modules/group_audit.c b/source4/dsdb/samdb/ldb_modules/group_audit.c new file mode 100644 index 0000000..a6ca25e --- /dev/null +++ b/source4/dsdb/samdb/ldb_modules/group_audit.c @@ -0,0 +1,1555 @@ +/* + ldb database 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/>. +*/ + +/* + * Provide an audit log of changes made to group memberships + * + */ + +#include "includes.h" +#include "ldb_module.h" +#include "lib/audit_logging/audit_logging.h" +#include "librpc/gen_ndr/windows_event_ids.h" + +#include "dsdb/samdb/samdb.h" +#include "dsdb/samdb/ldb_modules/util.h" +#include "dsdb/samdb/ldb_modules/audit_util_proto.h" +#include "libcli/security/dom_sid.h" +#include "auth/common_auth.h" +#include "param/param.h" + +#define AUDIT_JSON_TYPE "groupChange" +#define AUDIT_HR_TAG "Group Change" +#define AUDIT_MAJOR 1 +#define AUDIT_MINOR 1 +#define GROUP_LOG_LVL 5 + +static const char *const group_attrs[] = {"member", "groupType", NULL}; +static const char *const group_type_attr[] = {"groupType", NULL}; +static const char * const primary_group_attr[] = { + "primaryGroupID", + "objectSID", + NULL}; + +struct audit_context { + bool send_events; + struct imessaging_context *msg_ctx; +}; + +struct audit_callback_context { + struct ldb_request *request; + struct ldb_module *module; + struct ldb_message_element *members; + uint32_t primary_group; + void (*log_changes)( + struct audit_callback_context *acc, + const int status); +}; + +/* + * @brief get the transaction id. + * + * Get the id of the transaction that the current request is contained in. + * + * @param req the request. + * + * @return the transaction id GUID, or NULL if it is not there. + */ +static struct GUID *get_transaction_id( + const struct ldb_request *request) +{ + struct ldb_control *control; + struct dsdb_control_transaction_identifier *transaction_id; + + control = ldb_request_get_control( + discard_const(request), + DSDB_CONTROL_TRANSACTION_IDENTIFIER_OID); + if (control == NULL) { + return NULL; + } + transaction_id = talloc_get_type( + control->data, + struct dsdb_control_transaction_identifier); + if (transaction_id == NULL) { + return NULL; + } + return &transaction_id->transaction_guid; +} + +/* + * @brief generate a JSON log entry for a group change. + * + * Generate a JSON object containing details of a users group change. + * + * @param module the ldb module + * @param request the ldb_request + * @param action the change action being performed + * @param user the user name + * @param group the group name + * @param status the ldb status code for the ldb operation. + * + * @return A json object containing the details. + * NULL if an error was detected + */ +static struct json_object audit_group_json(const struct ldb_module *module, + const struct ldb_request *request, + const char *action, + const char *user, + const char *group, + const enum event_id_type event_id, + const int status) +{ + struct ldb_context *ldb = NULL; + const struct dom_sid *sid = NULL; + struct json_object wrapper = json_empty_object; + struct json_object audit = json_empty_object; + const struct tsocket_address *remote = NULL; + const struct GUID *unique_session_token = NULL; + struct GUID *transaction_id = NULL; + int rc = 0; + + ldb = ldb_module_get_ctx(discard_const(module)); + + remote = dsdb_audit_get_remote_address(ldb); + sid = dsdb_audit_get_user_sid(module); + unique_session_token = dsdb_audit_get_unique_session_token(module); + transaction_id = get_transaction_id(request); + + audit = json_new_object(); + if (json_is_invalid(&audit)) { + goto failure; + } + rc = json_add_version(&audit, AUDIT_MAJOR, AUDIT_MINOR); + if (rc != 0) { + goto failure; + } + if (event_id != EVT_ID_NONE) { + rc = json_add_int(&audit, "eventId", event_id); + if (rc != 0) { + goto failure; + } + } + rc = json_add_int(&audit, "statusCode", status); + if (rc != 0) { + goto failure; + } + rc = json_add_string(&audit, "status", ldb_strerror(status)); + if (rc != 0) { + goto failure; + } + rc = json_add_string(&audit, "action", action); + if (rc != 0) { + goto failure; + } + rc = json_add_address(&audit, "remoteAddress", remote); + if (rc != 0) { + goto failure; + } + rc = json_add_sid(&audit, "userSid", sid); + if (rc != 0) { + goto failure; + } + rc = json_add_string(&audit, "group", group); + if (rc != 0) { + goto failure; + } + rc = json_add_guid(&audit, "transactionId", transaction_id); + if (rc != 0) { + goto failure; + } + rc = json_add_guid(&audit, "sessionId", unique_session_token); + if (rc != 0) { + goto failure; + } + rc = json_add_string(&audit, "user", user); + if (rc != 0) { + goto failure; + } + + wrapper = json_new_object(); + if (json_is_invalid(&wrapper)) { + goto failure; + } + rc = json_add_timestamp(&wrapper); + if (rc != 0) { + goto failure; + } + rc = json_add_string(&wrapper, "type", AUDIT_JSON_TYPE); + if (rc != 0) { + goto failure; + } + rc = json_add_object(&wrapper, AUDIT_JSON_TYPE, &audit); + if (rc != 0) { + goto failure; + } + + return wrapper; +failure: + /* + * On a failure audit will not have been added to wrapper so it + * needs to free it to avoid a leak. + * + * wrapper is freed to invalidate it as it will have only been + * partially constructed and may be inconsistent. + * + * All the json manipulation routines handle a freed object correctly + */ + json_free(&audit); + json_free(&wrapper); + DBG_ERR("Failed to create group change JSON log message\n"); + return wrapper; +} + +/* + * @brief generate a human readable log entry for a group change. + * + * Generate a human readable log entry containing details of a users group + * change. + * + * @param ctx the talloc context owning the returned log entry + * @param module the ldb module + * @param request the ldb_request + * @param action the change action being performed + * @param user the user name + * @param group the group name + * @param status the ldb status code for the ldb operation. + * + * @return A human readable log line. + */ +static char *audit_group_human_readable( + TALLOC_CTX *mem_ctx, + const struct ldb_module *module, + const struct ldb_request *request, + const char *action, + const char *user, + const char *group, + const int status) +{ + struct ldb_context *ldb = NULL; + const char *remote_host = NULL; + const struct dom_sid *sid = NULL; + const char *user_sid = NULL; + const char *timestamp = NULL; + char *log_entry = NULL; + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_module_get_ctx(discard_const(module)); + + remote_host = dsdb_audit_get_remote_host(ldb, ctx); + sid = dsdb_audit_get_user_sid(module); + user_sid = dom_sid_string(ctx, sid); + timestamp = audit_get_timestamp(ctx); + + log_entry = talloc_asprintf( + mem_ctx, + "[%s] at [%s] status [%s] " + "Remote host [%s] SID [%s] Group [%s] User [%s]", + action, + timestamp, + ldb_strerror(status), + remote_host, + user_sid, + group, + user); + TALLOC_FREE(ctx); + return log_entry; +} + +/* + * @brief generate an array of parsed_dns, deferring the actual parsing. + * + * Get an array of 'struct parsed_dns' without the parsing. + * The parsed_dns are parsed only when needed to avoid the expense of parsing. + * + * This procedure assumes that the dn's are sorted in GUID order and contains + * no duplicates. This should be valid as the module sits below repl_meta_data + * which ensures this. + * + * @param mem_ctx The memory context that will own the generated array + * @param el The message element used to generate the array. + * + * @return an array of struct parsed_dns, or NULL in the event of an error + */ +static struct parsed_dn *get_parsed_dns( + TALLOC_CTX *mem_ctx, + struct ldb_message_element *el) +{ + int ret; + struct parsed_dn *pdn = NULL; + + if (el == NULL || el->num_values == 0) { + return NULL; + } + + ret = get_parsed_dns_trusted(mem_ctx, el, &pdn); + if (ret == LDB_ERR_OPERATIONS_ERROR) { + DBG_ERR("Out of memory\n"); + return NULL; + } + return pdn; + +} + +enum dn_compare_result { + LESS_THAN, + BINARY_EQUAL, + EQUAL, + GREATER_THAN +}; +/* + * @brief compare parsed_dn, using GUID ordering + * + * Compare two parsed_dn structures, using GUID ordering. + * To avoid the overhead of parsing the DN's this function does a binary + * compare first. The DN's tre only parsed if they are not equal at a binary + * level. + * + * @param ctx talloc context that will own the parsed dsdb_dn + * @param ldb ldb_context + * @param dn1 The first dn + * @param dn2 The second dn + * + * @return BINARY_EQUAL values are equal at a binary level + * EQUAL DN's are equal but the meta data is different + * LESS_THAN dn1's GUID is less than dn2's GUID + * GREATER_THAN dn1's GUID is greater than dn2's GUID + * + */ +static enum dn_compare_result dn_compare( + TALLOC_CTX *mem_ctx, + struct ldb_context *ldb, + struct parsed_dn *dn1, + struct parsed_dn *dn2) { + + int res = 0; + + /* + * Do a binary compare first to avoid unnecessary parsing + */ + if (data_blob_cmp(dn1->v, dn2->v) == 0) { + /* + * Values are equal at a binary level so no need + * for further processing + */ + return BINARY_EQUAL; + } + /* + * Values not equal at the binary level, so lets + * do a GUID ordering compare. To do this we will need to ensure + * that the dn's have been parsed. + */ + if (dn1->dsdb_dn == NULL) { + really_parse_trusted_dn( + mem_ctx, + ldb, + dn1, + LDB_SYNTAX_DN); + } + if (dn2->dsdb_dn == NULL) { + really_parse_trusted_dn( + mem_ctx, + ldb, + dn2, + LDB_SYNTAX_DN); + } + + res = ndr_guid_compare(&dn1->guid, &dn2->guid); + if (res < 0) { + return LESS_THAN; + } else if (res == 0) { + return EQUAL; + } else { + return GREATER_THAN; + } +} + +/* + * @brief Get the DN of a users primary group as a printable string. + * + * Get the DN of a users primary group as a printable string. + * + * @param mem_ctx Talloc context the the returned string will be allocated on. + * @param module The ldb module + * @param account_sid The SID for the uses account. + * @param primary_group_rid The RID for the users primary group. + * + * @return a formatted DN, or null if there is an error. + */ +static const char *get_primary_group_dn( + TALLOC_CTX *mem_ctx, + struct ldb_module *module, + struct dom_sid *account_sid, + uint32_t primary_group_rid) +{ + NTSTATUS status; + + struct ldb_context *ldb = NULL; + struct dom_sid *domain_sid = NULL; + struct dom_sid *primary_group_sid = NULL; + char *sid = NULL; + struct ldb_dn *dn = NULL; + struct ldb_message *msg = NULL; + int rc; + + ldb = ldb_module_get_ctx(module); + + status = dom_sid_split_rid(mem_ctx, account_sid, &domain_sid, NULL); + if (!NT_STATUS_IS_OK(status)) { + return NULL; + } + + primary_group_sid = dom_sid_add_rid( + mem_ctx, + domain_sid, + primary_group_rid); + if (!primary_group_sid) { + return NULL; + } + + sid = dom_sid_string(mem_ctx, primary_group_sid); + if (sid == NULL) { + return NULL; + } + + dn = ldb_dn_new_fmt(mem_ctx, ldb, "<SID=%s>", sid); + if(dn == NULL) { + return sid; + } + rc = dsdb_search_one( + ldb, + mem_ctx, + &msg, + dn, + LDB_SCOPE_BASE, + NULL, + 0, + NULL); + if (rc != LDB_SUCCESS) { + return NULL; + } + + return ldb_dn_get_linearized(msg->dn); +} + +/* + * @brief Log details of a change to a users primary group. + * + * Log details of a change to a users primary group. + * There is no windows event id associated with a Primary Group change. + * However for a new user we generate an added to group event. + * + * @param module The ldb module. + * @param request The request being logged. + * @param action Description of the action being performed. + * @param group The linearized for of the group DN + * @param status the LDB status code for the processing of the request. + * + */ +static void log_primary_group_change( + struct ldb_module *module, + const struct ldb_request *request, + const char *action, + const char *group, + const int status) +{ + const char *user = NULL; + + struct audit_context *ac = + talloc_get_type( + ldb_module_get_private(module), + struct audit_context); + + TALLOC_CTX *ctx = talloc_new(NULL); + + user = dsdb_audit_get_primary_dn(request); + if (CHECK_DEBUGLVLC(DBGC_DSDB_GROUP_AUDIT, GROUP_LOG_LVL)) { + char *message = NULL; + message = audit_group_human_readable( + ctx, + module, + request, + action, + user, + group, + status); + audit_log_human_text( + AUDIT_HR_TAG, + message, + DBGC_DSDB_GROUP_AUDIT, + GROUP_LOG_LVL); + TALLOC_FREE(message); + } + + if (CHECK_DEBUGLVLC(DBGC_DSDB_GROUP_AUDIT_JSON, GROUP_LOG_LVL) || + (ac->msg_ctx && ac->send_events)) { + + struct json_object json; + json = audit_group_json( + module, request, action, user, group, EVT_ID_NONE, status); + audit_log_json( + &json, + DBGC_DSDB_GROUP_AUDIT_JSON, + GROUP_LOG_LVL); + if (ac->send_events) { + audit_message_send( + ac->msg_ctx, + DSDB_GROUP_EVENT_NAME, + MSG_GROUP_LOG, + &json); + } + json_free(&json); + if (request->operation == LDB_ADD) { + /* + * Have just added a user, generate a groupChange + * message indicating the user has been added to thier + * new PrimaryGroup. + */ + } + } + TALLOC_FREE(ctx); +} + +/* + * @brief Log details of a single change to a users group membership. + * + * Log details of a change to a users group membership, except for changes + * to their primary group which is handled by log_primary_group_change. + * + * @param module The ldb module. + * @param request The request being logged. + * @param action Description of the action being performed. + * @param user The linearized form of the users DN + * @param status the LDB status code for the processing of the request. + * + */ +static void log_membership_change(struct ldb_module *module, + const struct ldb_request *request, + const char *action, + const char *user, + const enum event_id_type event_id, + const int status) +{ + const char *group = NULL; + struct audit_context *ac = + talloc_get_type( + ldb_module_get_private(module), + struct audit_context); + + TALLOC_CTX *ctx = talloc_new(NULL); + group = dsdb_audit_get_primary_dn(request); + if (CHECK_DEBUGLVLC(DBGC_DSDB_GROUP_AUDIT, GROUP_LOG_LVL)) { + char *message = NULL; + message = audit_group_human_readable( + ctx, + module, + request, + action, + user, + group, + status); + audit_log_human_text( + AUDIT_HR_TAG, + message, + DBGC_DSDB_GROUP_AUDIT, + GROUP_LOG_LVL); + TALLOC_FREE(message); + } + + if (CHECK_DEBUGLVLC(DBGC_DSDB_GROUP_AUDIT_JSON, GROUP_LOG_LVL) || + (ac->msg_ctx && ac->send_events)) { + struct json_object json; + json = audit_group_json( + module, request, action, user, group, event_id, status); + audit_log_json( + &json, + DBGC_DSDB_GROUP_AUDIT_JSON, + GROUP_LOG_LVL); + if (ac->send_events) { + audit_message_send( + ac->msg_ctx, + DSDB_GROUP_EVENT_NAME, + MSG_GROUP_LOG, + &json); + } + json_free(&json); + } + TALLOC_FREE(ctx); +} + +/* + * @brief Get the windows event type id for removing a user from a group type. + * + * @param group_type the type of the current group, see libds/common/flags.h + * + * @return the Windows Event Id + * + */ +static enum event_id_type get_remove_member_event(uint32_t group_type) +{ + + switch (group_type) { + case GTYPE_SECURITY_BUILTIN_LOCAL_GROUP: + return EVT_ID_USER_REMOVED_FROM_LOCAL_SEC_GROUP; + case GTYPE_SECURITY_GLOBAL_GROUP: + return EVT_ID_USER_REMOVED_FROM_GLOBAL_SEC_GROUP; + case GTYPE_SECURITY_DOMAIN_LOCAL_GROUP: + return EVT_ID_USER_REMOVED_FROM_LOCAL_SEC_GROUP; + case GTYPE_SECURITY_UNIVERSAL_GROUP: + return EVT_ID_USER_REMOVED_FROM_UNIVERSAL_SEC_GROUP; + case GTYPE_DISTRIBUTION_GLOBAL_GROUP: + return EVT_ID_USER_REMOVED_FROM_GLOBAL_GROUP; + case GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP: + return EVT_ID_USER_REMOVED_FROM_LOCAL_GROUP; + case GTYPE_DISTRIBUTION_UNIVERSAL_GROUP: + return EVT_ID_USER_REMOVED_FROM_UNIVERSAL_GROUP; + default: + return EVT_ID_NONE; + } +} + +/* + * @brief Get the windows event type id for adding a user to a group type. + * + * @param group_type the type of the current group, see libds/common/flags.h + * + * @return the Windows Event Id + * + */ +static enum event_id_type get_add_member_event(uint32_t group_type) +{ + + switch (group_type) { + case GTYPE_SECURITY_BUILTIN_LOCAL_GROUP: + return EVT_ID_USER_ADDED_TO_LOCAL_SEC_GROUP; + case GTYPE_SECURITY_GLOBAL_GROUP: + return EVT_ID_USER_ADDED_TO_GLOBAL_SEC_GROUP; + case GTYPE_SECURITY_DOMAIN_LOCAL_GROUP: + return EVT_ID_USER_ADDED_TO_LOCAL_SEC_GROUP; + case GTYPE_SECURITY_UNIVERSAL_GROUP: + return EVT_ID_USER_ADDED_TO_UNIVERSAL_SEC_GROUP; + case GTYPE_DISTRIBUTION_GLOBAL_GROUP: + return EVT_ID_USER_ADDED_TO_GLOBAL_GROUP; + case GTYPE_DISTRIBUTION_DOMAIN_LOCAL_GROUP: + return EVT_ID_USER_ADDED_TO_LOCAL_GROUP; + case GTYPE_DISTRIBUTION_UNIVERSAL_GROUP: + return EVT_ID_USER_ADDED_TO_UNIVERSAL_GROUP; + default: + return EVT_ID_NONE; + } +} + +/* + * @brief Log all the changes to a users group membership. + * + * Log details of a change to a users group memberships, except for changes + * to their primary group which is handled by log_primary_group_change. + * + * @param module The ldb module. + * @param request The request being logged. + * @param action Description of the action being performed. + * @param user The linearized form of the users DN + * @param status the LDB status code for the processing of the request. + * + */ +static void log_membership_changes(struct ldb_module *module, + const struct ldb_request *request, + struct ldb_message_element *el, + struct ldb_message_element *old_el, + uint32_t group_type, + int status) +{ + unsigned int i, old_i, new_i; + unsigned int old_num_values; + unsigned int max_num_values; + unsigned int new_num_values; + struct parsed_dn *old_val = NULL; + struct parsed_dn *new_val = NULL; + struct parsed_dn *new_values = NULL; + struct parsed_dn *old_values = NULL; + struct ldb_context *ldb = NULL; + + TALLOC_CTX *ctx = talloc_new(NULL); + + old_num_values = old_el ? old_el->num_values : 0; + new_num_values = el ? el->num_values : 0; + max_num_values = old_num_values + new_num_values; + + if (max_num_values == 0) { + /* + * There is nothing to do! + */ + TALLOC_FREE(ctx); + return; + } + + old_values = get_parsed_dns(ctx, old_el); + new_values = get_parsed_dns(ctx, el); + ldb = ldb_module_get_ctx(module); + + old_i = 0; + new_i = 0; + for (i = 0; i < max_num_values; i++) { + enum dn_compare_result cmp; + if (old_i < old_num_values && new_i < new_num_values) { + /* + * Both list have values, so compare the values + */ + old_val = &old_values[old_i]; + new_val = &new_values[new_i]; + cmp = dn_compare(ctx, ldb, old_val, new_val); + } else if (old_i < old_num_values) { + /* + * the new list is empty, read the old list + */ + old_val = &old_values[old_i]; + new_val = NULL; + cmp = LESS_THAN; + } else if (new_i < new_num_values) { + /* + * the old list is empty, read new list + */ + old_val = NULL; + new_val = &new_values[new_i]; + cmp = GREATER_THAN; + } else { + break; + } + + if (cmp == LESS_THAN) { + /* + * Have an entry in the original record that is not in + * the new record. So it's been deleted + */ + const char *user = NULL; + enum event_id_type event_id; + if (old_val->dsdb_dn == NULL) { + really_parse_trusted_dn( + ctx, + ldb, + old_val, + LDB_SYNTAX_DN); + } + user = ldb_dn_get_linearized(old_val->dsdb_dn->dn); + event_id = get_remove_member_event(group_type); + log_membership_change( + module, request, "Removed", user, event_id, status); + old_i++; + } else if (cmp == BINARY_EQUAL) { + /* + * DN's unchanged at binary level so nothing to do. + */ + old_i++; + new_i++; + } else if (cmp == EQUAL) { + /* + * DN is unchanged now need to check the flags to + * determine if a record has been deleted or undeleted + */ + uint32_t old_flags; + uint32_t new_flags; + if (old_val->dsdb_dn == NULL) { + really_parse_trusted_dn( + ctx, + ldb, + old_val, + LDB_SYNTAX_DN); + } + if (new_val->dsdb_dn == NULL) { + really_parse_trusted_dn( + ctx, + ldb, + new_val, + LDB_SYNTAX_DN); + } + + dsdb_get_extended_dn_uint32( + old_val->dsdb_dn->dn, + &old_flags, + "RMD_FLAGS"); + dsdb_get_extended_dn_uint32( + new_val->dsdb_dn->dn, + &new_flags, + "RMD_FLAGS"); + if (new_flags == old_flags) { + /* + * No changes to the Repl meta data so can + * no need to log the change + */ + old_i++; + new_i++; + continue; + } + if (new_flags & DSDB_RMD_FLAG_DELETED) { + /* + * DN has been deleted. + */ + const char *user = NULL; + enum event_id_type event_id; + user = ldb_dn_get_linearized( + old_val->dsdb_dn->dn); + event_id = get_remove_member_event(group_type); + log_membership_change(module, + request, + "Removed", + user, + event_id, + status); + } else { + /* + * DN has been re-added + */ + const char *user = NULL; + enum event_id_type event_id; + user = ldb_dn_get_linearized( + new_val->dsdb_dn->dn); + event_id = get_add_member_event(group_type); + log_membership_change(module, + request, + "Added", + user, + event_id, + status); + } + old_i++; + new_i++; + } else { + /* + * Member in the updated record that's not in the + * original, so it must have been added. + */ + const char *user = NULL; + enum event_id_type event_id; + if ( new_val->dsdb_dn == NULL) { + really_parse_trusted_dn( + ctx, + ldb, + new_val, + LDB_SYNTAX_DN); + } + user = ldb_dn_get_linearized(new_val->dsdb_dn->dn); + event_id = get_add_member_event(group_type); + log_membership_change( + module, request, "Added", user, event_id, status); + new_i++; + } + } + + TALLOC_FREE(ctx); +} + +/* + * @brief log a group change message for a newly added user. + * + * When a user is added we need to generate a GroupChange Add message to + * log that the user has been added to their PrimaryGroup + */ +static void log_new_user_added_to_primary_group( + TALLOC_CTX *ctx, + struct audit_callback_context *acc, + const char *group, + const int status) +{ + uint32_t group_type; + enum event_id_type event_id = EVT_ID_NONE; + struct ldb_result *res = NULL; + struct ldb_dn *group_dn = NULL; + struct ldb_context *ldb = NULL; + int ret; + + ldb = ldb_module_get_ctx(acc->module); + group_dn = ldb_dn_new(ctx, ldb, group); + ret = dsdb_module_search_dn(acc->module, + ctx, + &res, + group_dn, + group_type_attr, + DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_REVEAL_INTERNALS | + DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT, + NULL); + if (ret == LDB_SUCCESS) { + const char *user = NULL; + group_type = + ldb_msg_find_attr_as_uint(res->msgs[0], "groupType", 0); + event_id = get_add_member_event(group_type); + user = dsdb_audit_get_primary_dn(acc->request); + log_membership_change( + acc->module, acc->request, "Added", user, event_id, status); + } +} + +/* + * @brief Log the details of a primary group change. + * + * Retrieve the users primary groupo after the operation has completed + * and call log_primary_group_change to log the actual changes. + * + * @param acc details of the primary group before the operation. + * @param status The status code returned by the operation. + * + * @return an LDB status code. + */ +static void log_user_primary_group_change( + struct audit_callback_context *acc, + const int status) +{ + TALLOC_CTX *ctx = talloc_new(NULL); + uint32_t new_rid = UINT32_MAX; + struct dom_sid *account_sid = NULL; + int ret; + const struct ldb_message *msg = dsdb_audit_get_message(acc->request); + + if (status == LDB_SUCCESS && msg != NULL) { + struct ldb_result *res = NULL; + ret = dsdb_module_search_dn( + acc->module, + ctx, + &res, + msg->dn, + primary_group_attr, + DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_REVEAL_INTERNALS | + DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT, + NULL); + if (ret == LDB_SUCCESS) { + new_rid = ldb_msg_find_attr_as_uint( + msg, + "primaryGroupID", + ~0); + account_sid = samdb_result_dom_sid( + ctx, + res->msgs[0], + "objectSid"); + } + } + /* + * If we don't have a new value then the user has been deleted + * which we currently do not log. + * Otherwise only log if the primary group has actually changed. + */ + if (account_sid != NULL && + new_rid != UINT32_MAX && + acc->primary_group != new_rid) { + const char* group = get_primary_group_dn( + ctx, + acc->module, + account_sid, + new_rid); + log_primary_group_change( + acc->module, + acc->request, + "PrimaryGroup", + group, + status); + /* + * Are we adding a new user with the primaryGroupID + * set. If so and we're generating JSON audit logs, will need to + * generate an "Add" message with the appropriate windows + * event id. + */ + if (acc->request->operation == LDB_ADD) { + log_new_user_added_to_primary_group( + ctx, acc, group, status); + } + } + TALLOC_FREE(ctx); +} + +/* + * @brief log the changes to users group membership. + * + * Retrieve the users group memberships after the operation has completed + * and call log_membership_changes to log the actual changes. + * + * @param acc details of the group memberships before the operation. + * @param status The status code returned by the operation. + * + */ +static void log_group_membership_changes( + struct audit_callback_context *acc, + const int status) +{ + TALLOC_CTX *ctx = talloc_new(NULL); + struct ldb_message_element *new_val = NULL; + int ret; + uint32_t group_type = 0; + const struct ldb_message *msg = dsdb_audit_get_message(acc->request); + if (status == LDB_SUCCESS && msg != NULL) { + struct ldb_result *res = NULL; + ret = dsdb_module_search_dn( + acc->module, + ctx, + &res, + msg->dn, + group_attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_REVEAL_INTERNALS | + DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT, + NULL); + if (ret == LDB_SUCCESS) { + new_val = ldb_msg_find_element(res->msgs[0], "member"); + group_type = ldb_msg_find_attr_as_uint( + res->msgs[0], "groupType", 0); + log_membership_changes(acc->module, + acc->request, + new_val, + acc->members, + group_type, + status); + TALLOC_FREE(ctx); + return; + } + } + /* + * If we get here either + * one of the lower level modules failed and the group record did + * not get updated + * or + * the updated group record could not be read. + * + * In both cases it does not make sense to log individual membership + * changes so we log a group membership change "Failure" message. + * + */ + log_membership_change(acc->module, + acc->request, + "Failure", + "", + EVT_ID_NONE, + status); + TALLOC_FREE(ctx); +} + +/* + * @brief call back function to log changes to the group memberships. + * + * Call back function to log changes to the uses broup memberships. + * + * @param req the ldb request. + * @param ares the ldb result + * + * @return am LDB status code. + */ +static int group_audit_callback( + struct ldb_request *req, + struct ldb_reply *ares) +{ + struct audit_callback_context *ac = NULL; + + ac = talloc_get_type( + req->context, + struct audit_callback_context); + + if (!ares) { + return ldb_module_done( + ac->request, NULL, NULL, + LDB_ERR_OPERATIONS_ERROR); + } + + /* pass on to the callback */ + switch (ares->type) { + case LDB_REPLY_ENTRY: + return ldb_module_send_entry( + ac->request, + ares->message, + ares->controls); + + case LDB_REPLY_REFERRAL: + return ldb_module_send_referral( + ac->request, + ares->referral); + + case LDB_REPLY_DONE: + /* + * Log on DONE now we have a result code + */ + ac->log_changes(ac, ares->error); + return ldb_module_done( + ac->request, + ares->controls, + ares->response, + ares->error); + break; + + default: + /* Can't happen */ + return LDB_ERR_OPERATIONS_ERROR; + } +} + +/* + * @brief Does this request change the primary group. + * + * Does the request change the primary group, i.e. does it contain the + * primaryGroupID attribute. + * + * @param req the request to examine. + * + * @return True if the request modifies the primary group. + */ +static bool has_primary_group_id(struct ldb_request *req) +{ + struct ldb_message_element *el = NULL; + const struct ldb_message *msg = NULL; + + msg = dsdb_audit_get_message(req); + el = ldb_msg_find_element(msg, "primaryGroupID"); + + return (el != NULL); +} + +/* + * @brief Does this request change group membership. + * + * Does the request change the ses group memberships, i.e. does it contain the + * member attribute. + * + * @param req the request to examine. + * + * @return True if the request modifies the users group memberships. + */ +static bool has_group_membership_changes(struct ldb_request *req) +{ + struct ldb_message_element *el = NULL; + const struct ldb_message *msg = NULL; + + msg = dsdb_audit_get_message(req); + el = ldb_msg_find_element(msg, "member"); + + return (el != NULL); +} + + + +/* + * @brief Install the callback function to log an add request. + * + * Install the callback function to log an add request changing the users + * group memberships. As we want to log the returned status code, we need to + * register a callback function that will be called once the operation has + * completed. + * + * This function reads the current user record so that we can log the before + * and after state. + * + * @param module The ldb module. + * @param req The modify request. + * + * @return and LDB status code. + */ +static int set_group_membership_add_callback( + struct ldb_module *module, + struct ldb_request *req) +{ + struct audit_callback_context *context = NULL; + struct ldb_request *new_req = NULL; + struct ldb_context *ldb = NULL; + int ret; + /* + * Adding group memberships so will need to log the changes. + */ + ldb = ldb_module_get_ctx(module); + context = talloc_zero(req, struct audit_callback_context); + + if (context == NULL) { + return ldb_oom(ldb); + } + context->request = req; + context->module = module; + context->log_changes = log_group_membership_changes; + /* + * We want to log the return code status, so we need to register + * a callback function to get the actual result. + * We need to take a new copy so that we don't alter the callers copy + */ + ret = ldb_build_add_req( + &new_req, + ldb, + req, + req->op.add.message, + req->controls, + context, + group_audit_callback, + req); + if (ret != LDB_SUCCESS) { + return ret; + } + return ldb_next_request(module, new_req); +} + + +/* + * @brief Install the callback function to log a modify request. + * + * Install the callback function to log a modify request changing the primary + * group . As we want to log the returned status code, we need to register a + * callback function that will be called once the operation has completed. + * + * This function reads the current user record so that we can log the before + * and after state. + * + * @param module The ldb module. + * @param req The modify request. + * + * @return and LDB status code. + */ +static int set_primary_group_modify_callback( + struct ldb_module *module, + struct ldb_request *req) +{ + struct audit_callback_context *context = NULL; + struct ldb_request *new_req = NULL; + struct ldb_context *ldb = NULL; + const struct ldb_message *msg = NULL; + struct ldb_result *res = NULL; + int ret; + + TALLOC_CTX *ctx = talloc_new(NULL); + + ldb = ldb_module_get_ctx(module); + + context = talloc_zero(req, struct audit_callback_context); + if (context == NULL) { + ret = ldb_oom(ldb); + goto exit; + } + context->request = req; + context->module = module; + context->log_changes = log_user_primary_group_change; + + msg = dsdb_audit_get_message(req); + ret = dsdb_module_search_dn( + module, + ctx, + &res, + msg->dn, + primary_group_attr, + DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_REVEAL_INTERNALS | + DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT, + NULL); + if (ret == LDB_SUCCESS) { + uint32_t pg; + pg = ldb_msg_find_attr_as_uint( + res->msgs[0], + "primaryGroupID", + ~0); + context->primary_group = pg; + } + /* + * We want to log the return code status, so we need to register + * a callback function to get the actual result. + * We need to take a new copy so that we don't alter the callers copy + */ + ret = ldb_build_mod_req( + &new_req, + ldb, + req, + req->op.add.message, + req->controls, + context, + group_audit_callback, + req); + if (ret != LDB_SUCCESS) { + goto exit; + } + ret = ldb_next_request(module, new_req); +exit: + TALLOC_FREE(ctx); + return ret; +} + +/* + * @brief Install the callback function to log an add request. + * + * Install the callback function to log an add request changing the primary + * group . As we want to log the returned status code, we need to register a + * callback function that will be called once the operation has completed. + * + * This function reads the current user record so that we can log the before + * and after state. + * + * @param module The ldb module. + * @param req The modify request. + * + * @return and LDB status code. + */ +static int set_primary_group_add_callback( + struct ldb_module *module, + struct ldb_request *req) +{ + struct audit_callback_context *context = NULL; + struct ldb_request *new_req = NULL; + struct ldb_context *ldb = NULL; + int ret; + /* + * Adding a user with a primary group. + */ + ldb = ldb_module_get_ctx(module); + context = talloc_zero(req, struct audit_callback_context); + + if (context == NULL) { + return ldb_oom(ldb); + } + context->request = req; + context->module = module; + context->log_changes = log_user_primary_group_change; + /* + * We want to log the return code status, so we need to register + * a callback function to get the actual result. + * We need to take a new copy so that we don't alter the callers copy + */ + ret = ldb_build_add_req( + &new_req, + ldb, + req, + req->op.add.message, + req->controls, + context, + group_audit_callback, + req); + if (ret != LDB_SUCCESS) { + return ret; + } + return ldb_next_request(module, new_req); +} + +/* + * @brief Module handler for add operations. + * + * Inspect the current add request, and if needed log any group membership + * changes. + * + * @param module The ldb module. + * @param req The modify request. + * + * @return and LDB status code. + */ +static int group_add( + struct ldb_module *module, + struct ldb_request *req) +{ + + struct audit_context *ac = + talloc_get_type( + ldb_module_get_private(module), + struct audit_context); + /* + * Currently we don't log replicated group changes + */ + if (ldb_request_get_control(req, DSDB_CONTROL_REPLICATED_UPDATE_OID)) { + return ldb_next_request(module, req); + } + + if (CHECK_DEBUGLVLC(DBGC_DSDB_GROUP_AUDIT, GROUP_LOG_LVL) || + CHECK_DEBUGLVLC(DBGC_DSDB_GROUP_AUDIT_JSON, GROUP_LOG_LVL) || + (ac->msg_ctx && ac->send_events)) { + /* + * Avoid the overheads of logging unless it has been + * enabled + */ + if (has_group_membership_changes(req)) { + return set_group_membership_add_callback(module, req); + } + if (has_primary_group_id(req)) { + return set_primary_group_add_callback(module, req); + } + } + return ldb_next_request(module, req); +} + +/* + * @brief Module handler for delete operations. + * + * Currently there is no logging for delete operations. + * + * @param module The ldb module. + * @param req The modify request. + * + * @return and LDB status code. + */ +static int group_delete( + struct ldb_module *module, + struct ldb_request *req) +{ + return ldb_next_request(module, req); +} + +/* + * @brief Install the callback function to log a modify request. + * + * Install the callback function to log a modify request. As we want to log the + * returned status code, we need to register a callback function that will be + * called once the operation has completed. + * + * This function reads the current user record so that we can log the before + * and after state. + * + * @param module The ldb module. + * @param req The modify request. + * + * @return and LDB status code. + */ +static int set_group_modify_callback( + struct ldb_module *module, + struct ldb_request *req) +{ + struct audit_callback_context *context = NULL; + struct ldb_request *new_req = NULL; + struct ldb_context *ldb = NULL; + struct ldb_result *res = NULL; + int ret; + + ldb = ldb_module_get_ctx(module); + context = talloc_zero(req, struct audit_callback_context); + + if (context == NULL) { + return ldb_oom(ldb); + } + context->request = req; + context->module = module; + context->log_changes = log_group_membership_changes; + + /* + * About to change the group memberships need to read + * the current state from the database. + */ + ret = dsdb_module_search_dn( + module, + context, + &res, + req->op.add.message->dn, + group_attrs, + DSDB_FLAG_NEXT_MODULE | + DSDB_SEARCH_REVEAL_INTERNALS | + DSDB_SEARCH_SHOW_DN_IN_STORAGE_FORMAT, + NULL); + if (ret == LDB_SUCCESS) { + context->members = ldb_msg_find_element(res->msgs[0], "member"); + } + + ret = ldb_build_mod_req( + &new_req, + ldb, + req, + req->op.mod.message, + req->controls, + context, + group_audit_callback, + req); + if (ret != LDB_SUCCESS) { + return ret; + } + return ldb_next_request(module, new_req); +} + +/* + * @brief Module handler for modify operations. + * + * Inspect the current modify request, and if needed log any group membership + * changes. + * + * @param module The ldb module. + * @param req The modify request. + * + * @return and LDB status code. + */ +static int group_modify( + struct ldb_module *module, + struct ldb_request *req) +{ + + struct audit_context *ac = + talloc_get_type( + ldb_module_get_private(module), + struct audit_context); + /* + * Currently we don't log replicated group changes + */ + if (ldb_request_get_control(req, DSDB_CONTROL_REPLICATED_UPDATE_OID)) { + return ldb_next_request(module, req); + } + + if (CHECK_DEBUGLVLC(DBGC_DSDB_GROUP_AUDIT, GROUP_LOG_LVL) || + CHECK_DEBUGLVLC(DBGC_DSDB_GROUP_AUDIT_JSON, GROUP_LOG_LVL) || + (ac->msg_ctx && ac->send_events)) { + /* + * Avoid the overheads of logging unless it has been + * enabled + */ + if (has_group_membership_changes(req)) { + return set_group_modify_callback(module, req); + } + if (has_primary_group_id(req)) { + return set_primary_group_modify_callback(module, req); + } + } + return ldb_next_request(module, req); +} + +/* + * @brief ldb module initialisation + * + * Initialise the module, loading the private data etc. + * + * @param module The ldb module to initialise. + * + * @return An LDB status code. + */ +static int group_init(struct ldb_module *module) +{ + + struct ldb_context *ldb = ldb_module_get_ctx(module); + struct audit_context *context = NULL; + struct loadparm_context *lp_ctx + = talloc_get_type_abort( + ldb_get_opaque(ldb, "loadparm"), + struct loadparm_context); + struct tevent_context *ev = ldb_get_event_context(ldb); + + context = talloc_zero(module, struct audit_context); + if (context == NULL) { + return ldb_module_oom(module); + } + + if (lp_ctx && lpcfg_dsdb_group_change_notification(lp_ctx)) { + context->send_events = true; + context->msg_ctx = imessaging_client_init(context, + lp_ctx, + ev); + } + + ldb_module_set_private(module, context); + return ldb_next_init(module); +} + +static const struct ldb_module_ops ldb_group_audit_log_module_ops = { + .name = "group_audit_log", + .add = group_add, + .modify = group_modify, + .del = group_delete, + .init_context = group_init, +}; + +int ldb_group_audit_log_module_init(const char *version) +{ + LDB_MODULE_CHECK_VERSION(version); + return ldb_register_module(&ldb_group_audit_log_module_ops); +} |