dsdb group audit tests: log_membership_changes extra tests
[samba.git] / source4 / dsdb / samdb / ldb_modules / tests / test_group_audit.c
index 2c32b658a21a4c2daa4cca5db74a7cf7141605ff..6ec218beb54e8db3037e9c1ad62cdff3a3dd75e8 100644 (file)
@@ -64,37 +64,151 @@ int dsdb_search_one(struct ldb_context *ldb,
 }
 
 /*
- * Mocking for audit_log_hr to capture the called parameters
+ * Mock version of audit_log_json
  */
-const char *audit_log_hr_prefix = NULL;
-const char *audit_log_hr_message = NULL;
-int audit_log_hr_debug_class = 0;
-int audit_log_hr_debug_level = 0;
 
-static void audit_log_hr_init(void)
+#define MAX_EXPECTED_MESSAGES 16
+static struct json_object messages[MAX_EXPECTED_MESSAGES];
+static size_t messages_sent = 0;
+
+void audit_message_send(
+       struct imessaging_context *msg_ctx,
+       const char *server_name,
+       uint32_t message_type,
+       struct json_object *message)
 {
-       audit_log_hr_prefix = NULL;
-       audit_log_hr_message = NULL;
-       audit_log_hr_debug_class = 0;
-       audit_log_hr_debug_level = 0;
+       messages[messages_sent].root = json_deep_copy(message->root);
+       messages[messages_sent].valid = message->valid;
+       messages_sent++;
 }
 
-void audit_log_human_text(
-       const char *prefix,
-       const char *message,
-       int debug_class,
-       int debug_level)
+#define check_group_change_message(m, u, a)\
+       _check_group_change_message(m, u, a, __FILE__, __LINE__);
+/*
+ * declare the internal cmocka cm_print_error so that we can output messages
+ * in sub unit format
+ */
+void cm_print_error(const char * const format, ...);
+
+/*
+ * Validate a group change JSON audit message
+ *
+ * It should contain 3 elements.
+ * Have a type of "groupChange"
+ * Have a groupChange element
+ *
+ * The group change element should have 10 elements.
+ *
+ * There should be a user element matching the expected value
+ * There should be an action matching the expected value
+ */
+static void _check_group_change_message(
+       const int message,
+       const char *user,
+       const char *action,
+       const char *file,
+       const int line)
 {
-       audit_log_hr_prefix = prefix;
-       audit_log_hr_message = message;
-       audit_log_hr_debug_class = debug_class;
-       audit_log_hr_debug_level = debug_level;
+       struct json_object json;
+       json_t *audit = NULL;
+       json_t *v = NULL;
+       const char* value;
+       json = messages[message];
+
+       /*
+        * Validate the root JSON element
+        * check the number of elements
+        */
+       if (json_object_size(json.root) != 3) {
+               cm_print_error(
+                   "Unexpected number of elements in root %zu != %d\n",
+                   json_object_size(json.root),
+                   3);
+               _fail(file, line);
+       }
+
+       /*
+        * Check the type element
+        */
+       v = json_object_get(json.root, "type");
+       if (v == NULL) {
+               cm_print_error( "No \"type\" element\n");
+               _fail(file, line);
+       }
+
+       value = json_string_value(v);
+       if (strncmp("groupChange", value, strlen("groupChange") != 0)) {
+               cm_print_error(
+                   "Unexpected type \"%s\" != \"groupChange\"\n",
+                   value);
+               _fail(file, line);
+       }
+
+
+       audit = json_object_get(json.root, "groupChange");
+       if (audit == NULL) {
+               cm_print_error("No groupChange element\n");
+               _fail(file, line);
+       }
+
+       /*
+        * Validate the groupChange element
+        */
+       if (json_object_size(audit) != 10) {
+               cm_print_error(
+                   "Unexpected number of elements in groupChange "
+                   "%zu != %d\n",
+                   json_object_size(audit),
+                   10);
+               _fail(file, line);
+       }
+       /*
+        * Validate the user element
+        */
+       v = json_object_get(audit, "user");
+       if (v == NULL) {
+               cm_print_error( "No user element\n");
+               _fail(file, line);
+       }
+
+       value = json_string_value(v);
+       if (strncmp(user, value, strlen(user) != 0)) {
+               cm_print_error(
+                   "Unexpected user name \"%s\" != \"%s\"\n",
+                   value,
+                   user);
+               _fail(file, line);
+       }
+
+       /*
+        * Validate the action element
+        */
+       v = json_object_get(audit, "action");
+       if (v == NULL) {
+               cm_print_error( "No action element\n");
+               _fail(file, line);
+       }
+
+       value = json_string_value(v);
+       if (strncmp(action, value, strlen(action) != 0)) {
+               print_error(
+                   "Unexpected action \"%s\" != \"%s\"\n",
+                   value,
+                   action);
+               _fail(file, line);
+       }
 }
 
+#define check_timestamp(b, t)\
+       _check_timestamp(b, t, __FILE__, __LINE__);
 /*
  * Test helper to check ISO 8601 timestamps for validity
  */
-static void check_timestamp(time_t before, const char *timestamp)
+static void _check_timestamp(
+       time_t before,
+       const char *timestamp,
+       const char *file,
+       const int line)
 {
        int rc;
        int usec, tz;
@@ -130,29 +244,97 @@ static void check_timestamp(time_t before, const char *timestamp)
        actual = mktime(&tm);
 
        /*
-        * The timestamp should be before <= actual <= after
+        * The time stamp should be before <= actual <= after
         */
-       assert_true(difftime(actual, before) >= 0);
-       assert_true(difftime(after, actual) >= 0);
+       if (difftime(actual, before) < 0) {
+               char buffer[40];
+               strftime(buffer,
+                        sizeof(buffer)-1,
+                        "%Y-%m-%dT%T",
+                        localtime(&before));
+               cm_print_error(
+                   "time stamp \"%s\" is before start time \"%s\"\n",
+                   timestamp,
+                   buffer);
+               _fail(file, line);
+       }
+       if (difftime(after, actual) < 0) {
+               char buffer[40];
+               strftime(buffer,
+                        sizeof(buffer)-1,
+                        "%Y-%m-%dT%T",
+                        localtime(&after));
+               cm_print_error(
+                   "time stamp \"%s\" is after finish time \"%s\"\n",
+                   timestamp,
+                   buffer);
+               _fail(file, line);
+       }
 }
 
+#define check_version(v, m, n)\
+       _check_version(v, m, n, __FILE__, __LINE__);
 /*
  * Test helper to validate a version object.
  */
-static void check_version(struct json_t *version, int major, int minor)
+static void _check_version(
+       struct json_t *version,
+       int major,
+       int minor,
+       const char* file,
+       const int line)
 {
        struct json_t *v = NULL;
+       int value;
 
-       assert_true(json_is_object(version));
-       assert_int_equal(2, json_object_size(version));
+       if (!json_is_object(version)) {
+               cm_print_error("version is not a JSON object\n");
+               _fail(file, line);
+       }
 
+       if (json_object_size(version) != 2) {
+               cm_print_error(
+                   "Unexpected number of elements in version %zu != %d\n",
+                   json_object_size(version),
+                   2);
+               _fail(file, line);
+       }
+
+       /*
+        * Validate the major version number element
+        */
        v = json_object_get(version, "major");
-       assert_non_null(v);
-       assert_int_equal(major, json_integer_value(v));
+       if (v == NULL) {
+               cm_print_error( "No major element\n");
+               _fail(file, line);
+       }
+
+       value = json_integer_value(v);
+       if (value != major) {
+               print_error(
+                   "Unexpected major version number \"%d\" != \"%d\"\n",
+                   value,
+                   major);
+               _fail(file, line);
+       }
 
+       /*
+        * Validate the minor version number element
+        */
        v = json_object_get(version, "minor");
-       assert_non_null(v);
-       assert_int_equal(minor, json_integer_value(v));
+       if (v == NULL) {
+               cm_print_error( "No minor element\n");
+               _fail(file, line);
+       }
+
+       value = json_integer_value(v);
+       if (value != minor) {
+               print_error(
+                   "Unexpected minor version number \"%d\" != \"%d\"\n",
+                   value,
+                   minor);
+               _fail(file, line);
+       }
 }
 
 /*
@@ -454,7 +636,7 @@ static void test_dn_compare(void **state)
        b->v = &bb;
 
        res = dn_compare(ctx, ldb, a, b);
-       assert_int_equal(GREATER_THAN, res);
+       assert_int_equal(LESS_THAN, res);
        /*
         * DN's should have been parsed
         */
@@ -482,7 +664,7 @@ static void test_dn_compare(void **state)
        b->v = &bb;
 
        res = dn_compare(ctx, ldb, a, b);
-       assert_int_equal(LESS_THAN, res);
+       assert_int_equal(GREATER_THAN, res);
        /*
         * DN's should have been parsed
         */
@@ -551,60 +733,6 @@ static void test_get_primary_group_dn(void **state)
        TALLOC_FREE(ctx);
 }
 
-/*
- * Mocking for audit_log_json to capture the called parameters
- */
-const char *audit_log_json_prefix = NULL;
-struct json_object *audit_log_json_message = NULL;
-int audit_log_json_debug_class = 0;
-int audit_log_json_debug_level = 0;
-
-static void audit_log_json_init(void)
-{
-       audit_log_json_prefix = NULL;
-       audit_log_json_message = NULL;
-       audit_log_json_debug_class = 0;
-       audit_log_json_debug_level = 0;
-}
-
-void audit_log_json(
-       const char* prefix,
-       struct json_object* message,
-       int debug_class,
-       int debug_level)
-{
-       audit_log_json_prefix = prefix;
-       audit_log_json_message = message;
-       audit_log_json_debug_class = debug_class;
-       audit_log_json_debug_level = debug_level;
-}
-
-/*
- * Mocking for audit_message_send to capture the called parameters
- */
-struct imessaging_context *audit_message_send_msg_ctx = NULL;
-const char *audit_message_send_server_name = NULL;
-uint32_t audit_message_send_message_type = 0;
-struct json_object *audit_message_send_message = NULL;
-
-static void audit_message_send_init(void) {
-       audit_message_send_msg_ctx = NULL;
-       audit_message_send_server_name = NULL;
-       audit_message_send_message_type = 0;
-       audit_message_send_message = NULL;
-}
-void audit_message_send(
-       struct imessaging_context *msg_ctx,
-       const char *server_name,
-       uint32_t message_type,
-       struct json_object *message)
-{
-       audit_message_send_msg_ctx = msg_ctx;
-       audit_message_send_server_name = server_name;
-       audit_message_send_message_type = message_type;
-       audit_message_send_message = message;
-}
-
 static void test_audit_group_json(void **state)
 {
        struct ldb_context *ldb = NULL;
@@ -700,14 +828,481 @@ static void test_audit_group_json(void **state)
 
        json_free(&json);
        TALLOC_FREE(ctx);
+}
+
+static void setup_ldb(
+       TALLOC_CTX *ctx,
+       struct ldb_context **ldb,
+       struct ldb_module **module,
+       const char *ip,
+       const char *session,
+       const char *sid)
+{
+       struct tsocket_address *ts = NULL;
+       struct audit_context *context = NULL;
+
+       *ldb = ldb_init(ctx, NULL);
+       ldb_register_samba_handlers(*ldb);
+
+
+       *module = talloc_zero(ctx, struct ldb_module);
+       (*module)->ldb = *ldb;
+
+       context = talloc_zero(*module, struct audit_context);
+       context->send_events = true;
+       context->msg_ctx = (struct imessaging_context *) 0x01;
+
+       ldb_module_set_private(*module, context);
+
+       tsocket_address_inet_from_strings(ctx, "ip", "127.0.0.1", 0, &ts);
+       ldb_set_opaque(*ldb, "remoteAddress", ts);
+
+       add_session_data(ctx, *ldb, session, sid);
+}
+
+/*
+ * Test the removal of a user from a group.
+ *
+ * The new element contains one group member
+ * The old element contains two group member
+ *
+ * Expect to see the removed entry logged.
+ *
+ * This test confirms bug 13664
+ * https://bugzilla.samba.org/show_bug.cgi?id=13664
+ */
+static void test_log_membership_changes_removed(void **state)
+{
+       struct ldb_context *ldb = NULL;
+       struct ldb_module  *module = NULL;
+       const char * const SID = "S-1-5-21-2470180966-3899876309-2637894779";
+       const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773";
+       const char * const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773";
+       const char * const IP = "127.0.0.1";
+       struct ldb_request *req = NULL;
+       struct ldb_message_element *new_el = NULL;
+       struct ldb_message_element *old_el = NULL;
+       int status = 0;
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       setup_ldb(ctx, &ldb, &module, IP, SESSION, SID);
+
+       /*
+        * Build the ldb_request
+        */
+       req = talloc_zero(ctx, struct ldb_request);
+       req->operation =  LDB_ADD;
+       add_transaction_id(req, TRANSACTION);
+
+       /*
+        * Populate the new elements, containing one entry.
+        * Indicating that one element has been removed
+        */
+       new_el = talloc_zero(ctx, struct ldb_message_element);
+       new_el->num_values = 1;
+       new_el->values = talloc_zero_array(ctx, DATA_BLOB, 1);
+       new_el->values[0] = data_blob_string_const(
+               "<GUID=081519b5-a709-44a0-bc95-dd4bfe809bf8>;"
+               "CN=testuser131953,CN=Users,DC=addom,DC=samba,"
+               "DC=example,DC=com");
+
+       /*
+        * Populate the old elements, with two elements
+        * The first is the same as the one in new elements.
+        */
+       old_el = talloc_zero(ctx, struct ldb_message_element);
+       old_el->num_values = 2;
+       old_el->values = talloc_zero_array(ctx, DATA_BLOB, 2);
+       old_el->values[0] = data_blob_string_const(
+               "<GUID=cb8c2777-dcf5-419c-ab57-f645dbdf681b>;"
+               "cn=grpadttstuser01,cn=users,DC=addom,"
+               "DC=samba,DC=example,DC=com");
+       old_el->values[1] = data_blob_string_const(
+               "<GUID=081519b5-a709-44a0-bc95-dd4bfe809bf8>;"
+               "CN=testuser131953,CN=Users,DC=addom,DC=samba,"
+               "DC=example,DC=com");
+
+       /*
+        * call log_membership_changes
+        */
+       messages_sent = 0;
+       log_membership_changes(module, req, new_el, old_el, status);
+
+       /*
+        * Check the results
+        */
+       assert_int_equal(1, messages_sent);
+
+       check_group_change_message(
+           0,
+           "cn=grpadttstuser01,cn=users,DC=addom,DC=samba,DC=example,DC=com",
+           "Removed");
+
+       /*
+        * Clean up
+        */
+       json_free(&messages[0]);
+       TALLOC_FREE(ctx);
+}
+
+/* test log_membership_changes
+ *
+ * old contains 2 user dn's
+ * new contains 0 user dn's
+ *
+ * Expect to see both dn's logged as deleted.
+ */
+static void test_log_membership_changes_remove_all(void **state)
+{
+       struct ldb_context *ldb = NULL;
+       struct ldb_module  *module = NULL;
+       const char * const SID = "S-1-5-21-2470180966-3899876309-2637894779";
+       const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773";
+       const char * const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773";
+       const char * const IP = "127.0.0.1";
+       struct ldb_request *req = NULL;
+       struct ldb_message_element *new_el = NULL;
+       struct ldb_message_element *old_el = NULL;
+       int status = 0;
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       setup_ldb(ctx, &ldb, &module, IP, SESSION, SID);
+
+       /*
+        * Build the ldb_request
+        */
+       req = talloc_zero(ctx, struct ldb_request);
+       req->operation =  LDB_ADD;
+       add_transaction_id(req, TRANSACTION);
+
+       /*
+        * Populate the new elements, containing no entries.
+        * Indicating that all elements have been removed
+        */
+       new_el = talloc_zero(ctx, struct ldb_message_element);
+       new_el->num_values = 0;
+       new_el->values = NULL;
+
+       /*
+        * Populate the old elements, with two elements
+        */
+       old_el = talloc_zero(ctx, struct ldb_message_element);
+       old_el->num_values = 2;
+       old_el->values = talloc_zero_array(ctx, DATA_BLOB, 2);
+       old_el->values[0] = data_blob_string_const(
+               "<GUID=cb8c2777-dcf5-419c-ab57-f645dbdf681b>;"
+               "cn=grpadttstuser01,cn=users,DC=addom,"
+               "DC=samba,DC=example,DC=com");
+       old_el->values[1] = data_blob_string_const(
+               "<GUID=081519b5-a709-44a0-bc95-dd4bfe809bf8>;"
+               "CN=testuser131953,CN=Users,DC=addom,DC=samba,"
+               "DC=example,DC=com");
+
+       /*
+        * call log_membership_changes
+        */
+       messages_sent = 0;
+       log_membership_changes( module, req, new_el, old_el, status);
+
+       /*
+        * Check the results
+        */
+       assert_int_equal(2, messages_sent);
+
+       check_group_change_message(
+           0,
+           "cn=grpadttstuser01,cn=users,DC=addom,DC=samba,DC=example,DC=com",
+           "Removed");
+
+       check_group_change_message(
+           1,
+           "CN=testuser131953,CN=Users,DC=addom,DC=samba,DC=example,DC=com",
+           "Removed");
+
+       /*
+        * Clean up
+        */
+       json_free(&messages[0]);
+       json_free(&messages[1]);
+       TALLOC_FREE(ctx);
+}
+
+/* test log_membership_changes
+ *
+ * Add an entry.
+ *
+ * Old entries contains a single user dn
+ * New entries contains 2 user dn's, one matching the dn in old entries
+ *
+ * Should see a single new entry logged.
+ */
+static void test_log_membership_changes_added(void **state)
+{
+       struct ldb_context *ldb = NULL;
+       struct ldb_module  *module = NULL;
+       const char * const SID = "S-1-5-21-2470180966-3899876309-2637894779";
+       const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773";
+       const char * const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773";
+       const char * const IP = "127.0.0.1";
+       struct ldb_request *req = NULL;
+       struct ldb_message_element *new_el = NULL;
+       struct ldb_message_element *old_el = NULL;
+       int status = 0;
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       setup_ldb(ctx, &ldb, &module, IP, SESSION, SID);
+
+       /*
+        * Build the ldb_request
+        */
+       req = talloc_zero(ctx, struct ldb_request);
+       req->operation =  LDB_ADD;
+       add_transaction_id(req, TRANSACTION);
+
+       /*
+        * Populate the old elements adding a single entry.
+        */
+       old_el = talloc_zero(ctx, struct ldb_message_element);
+       old_el->num_values = 1;
+       old_el->values = talloc_zero_array(ctx, DATA_BLOB, 1);
+       old_el->values[0] = data_blob_string_const(
+               "<GUID=081519b5-a709-44a0-bc95-dd4bfe809bf8>;"
+               "CN=testuser131953,CN=Users,DC=addom,DC=samba,"
+               "DC=example,DC=com");
+
+       /*
+        * Populate the new elements adding two entries. One matches the entry
+        * in old elements. We expect to see the other element logged as Added
+        */
+       new_el = talloc_zero(ctx, struct ldb_message_element);
+       new_el->num_values = 2;
+       new_el->values = talloc_zero_array(ctx, DATA_BLOB, 2);
+       new_el->values[0] = data_blob_string_const(
+               "<GUID=cb8c2777-dcf5-419c-ab57-f645dbdf681b>;"
+               "cn=grpadttstuser01,cn=users,DC=addom,"
+               "DC=samba,DC=example,DC=com");
+       new_el->values[1] = data_blob_string_const(
+               "<GUID=081519b5-a709-44a0-bc95-dd4bfe809bf8>;"
+               "CN=testuser131953,CN=Users,DC=addom,DC=samba,"
+               "DC=example,DC=com");
+
+       /*
+        * call log_membership_changes
+        */
+       messages_sent = 0;
+       log_membership_changes( module, req, new_el, old_el, status);
+
+       /*
+        * Check the results
+        */
+       assert_int_equal(1, messages_sent);
+
+       check_group_change_message(
+           0,
+           "cn=grpadttstuser01,cn=users,DC=addom,DC=samba,DC=example,DC=com",
+           "Added");
+
+       /*
+        * Clean up
+        */
+       json_free(&messages[0]);
+       TALLOC_FREE(ctx);
+}
+
+/*
+ * test log_membership_changes.
+ *
+ * Old entries is empty
+ * New entries contains 2 user dn's
+ *
+ * Expect to see log messages for two added users
+ */
+static void test_log_membership_changes_add_to_empty(void **state)
+{
+       struct ldb_context *ldb = NULL;
+       struct ldb_module  *module = NULL;
+       const char * const SID = "S-1-5-21-2470180966-3899876309-2637894779";
+       const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773";
+       const char * const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773";
+       const char * const IP = "127.0.0.1";
+       struct ldb_request *req = NULL;
+       struct ldb_message_element *new_el = NULL;
+       struct ldb_message_element *old_el = NULL;
+       int status = 0;
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       /*
+        * Set up the ldb and module structures
+        */
+       setup_ldb(ctx, &ldb, &module, IP, SESSION, SID);
+
+       /*
+        * Build the request structure
+        */
+       req = talloc_zero(ctx, struct ldb_request);
+       req->operation =  LDB_ADD;
+       add_transaction_id(req, TRANSACTION);
+
+       /*
+        * Build the element containing the old values
+        */
+       old_el = talloc_zero(ctx, struct ldb_message_element);
+       old_el->num_values = 0;
+       old_el->values = NULL;
 
+       /*
+        * Build the element containing the new values
+        */
+       new_el = talloc_zero(ctx, struct ldb_message_element);
+       new_el->num_values = 2;
+       new_el->values = talloc_zero_array(ctx, DATA_BLOB, 2);
+       new_el->values[0] = data_blob_string_const(
+               "<GUID=cb8c2777-dcf5-419c-ab57-f645dbdf681b>;"
+               "cn=grpadttstuser01,cn=users,DC=addom,"
+               "DC=samba,DC=example,DC=com");
+       new_el->values[1] = data_blob_string_const(
+               "<GUID=081519b5-a709-44a0-bc95-dd4bfe809bf8>;"
+               "CN=testuser131953,CN=Users,DC=addom,DC=samba,"
+               "DC=example,DC=com");
+
+       /*
+        * Run log membership changes
+        */
+       messages_sent = 0;
+       log_membership_changes( module, req, new_el, old_el, status);
+       assert_int_equal(2, messages_sent);
+
+       check_group_change_message(
+           0,
+           "cn=grpadttstuser01,cn=users,DC=addom,DC=samba,DC=example,DC=com",
+           "Added");
+
+       check_group_change_message(
+           1,
+            "CN=testuser131953,CN=Users,DC=addom,DC=samba,DC=example,DC=com",
+           "Added");
+
+       json_free(&messages[0]);
+       json_free(&messages[1]);
+       TALLOC_FREE(ctx);
 }
 
-static void test_place_holder(void **state)
+/* test log_membership_changes
+ *
+ * Test Replication Meta Data flag handling.
+ *
+ * 4 entries in old and new entries with their RMD_FLAGS set as below:
+ *    old   new
+ * 1)  0     0    Not logged
+ * 2)  1     1    Both deleted, no change not logged
+ * 3)  0     1    New tagged as deleted, log as deleted
+ * 4)  1     0    Has been undeleted, log as an add
+ *
+ * Should see a single new entry logged.
+ */
+static void test_log_membership_changes_rmd_flags(void **state)
 {
-       audit_log_json_init();
-       audit_log_hr_init();
-       audit_message_send_init();
+       struct ldb_context *ldb = NULL;
+       struct ldb_module  *module = NULL;
+       const char * const SID = "S-1-5-21-2470180966-3899876309-2637894779";
+       const char * const SESSION = "7130cb06-2062-6a1b-409e-3514c26b1773";
+       const char * const TRANSACTION = "7130cb06-2062-6a1b-409e-3514c26b1773";
+       const char * const IP = "127.0.0.1";
+       struct ldb_request *req = NULL;
+       struct ldb_message_element *new_el = NULL;
+       struct ldb_message_element *old_el = NULL;
+       int status = 0;
+       TALLOC_CTX *ctx = talloc_new(NULL);
+
+       setup_ldb(ctx, &ldb, &module, IP, SESSION, SID);
+
+       /*
+        * Build the ldb_request
+        */
+       req = talloc_zero(ctx, struct ldb_request);
+       req->operation =  LDB_ADD;
+       add_transaction_id(req, TRANSACTION);
+
+       /*
+        * Populate the old elements.
+        */
+       old_el = talloc_zero(ctx, struct ldb_message_element);
+       old_el->num_values = 4;
+       old_el->values = talloc_zero_array(ctx, DATA_BLOB, 4);
+       old_el->values[0] = data_blob_string_const(
+               "<GUID=cb8c2777-dcf5-419c-ab57-f645dbdf681b>;"
+               "<RMD_FLAGS=0>;"
+               "cn=grpadttstuser01,cn=users,DC=addom,"
+               "DC=samba,DC=example,DC=com");
+       old_el->values[1] = data_blob_string_const(
+               "<GUID=cb8c2777-dcf5-419c-ab57-f645dbdf681c>;"
+               "<RMD_FLAGS=1>;"
+               "cn=grpadttstuser02,cn=users,DC=addom,"
+               "DC=samba,DC=example,DC=com");
+       old_el->values[2] = data_blob_string_const(
+               "<GUID=cb8c2777-dcf5-419c-ab57-f645dbdf681d>;"
+               "<RMD_FLAGS=0>;"
+               "cn=grpadttstuser03,cn=users,DC=addom,"
+               "DC=samba,DC=example,DC=com");
+       old_el->values[3] = data_blob_string_const(
+               "<GUID=cb8c2777-dcf5-419c-ab57-f645dbdf681e>;"
+               "<RMD_FLAGS=1>;"
+               "cn=grpadttstuser04,cn=users,DC=addom,"
+               "DC=samba,DC=example,DC=com");
+
+       /*
+        * Populate the new elements.
+        */
+       new_el = talloc_zero(ctx, struct ldb_message_element);
+       new_el->num_values = 4;
+       new_el->values = talloc_zero_array(ctx, DATA_BLOB, 4);
+       new_el->values[0] = data_blob_string_const(
+               "<GUID=cb8c2777-dcf5-419c-ab57-f645dbdf681b>;"
+               "<RMD_FLAGS=0>;"
+               "cn=grpadttstuser01,cn=users,DC=addom,"
+               "DC=samba,DC=example,DC=com");
+       new_el->values[1] = data_blob_string_const(
+               "<GUID=cb8c2777-dcf5-419c-ab57-f645dbdf681c>;"
+               "<RMD_FLAGS=1>;"
+               "cn=grpadttstuser02,cn=users,DC=addom,"
+               "DC=samba,DC=example,DC=com");
+       new_el->values[2] = data_blob_string_const(
+               "<GUID=cb8c2777-dcf5-419c-ab57-f645dbdf681d>;"
+               "<RMD_FLAGS=1>;"
+               "cn=grpadttstuser03,cn=users,DC=addom,"
+               "DC=samba,DC=example,DC=com");
+       new_el->values[3] = data_blob_string_const(
+               "<GUID=cb8c2777-dcf5-419c-ab57-f645dbdf681e>;"
+               "<RMD_FLAGS=0>;"
+               "cn=grpadttstuser04,cn=users,DC=addom,"
+               "DC=samba,DC=example,DC=com");
+
+       /*
+        * call log_membership_changes
+        */
+       messages_sent = 0;
+       log_membership_changes( module, req, new_el, old_el, status);
+
+       /*
+        * Check the results
+        */
+       assert_int_equal(2, messages_sent);
+
+       check_group_change_message(
+           0,
+           "cn=grpadttstuser03,cn=users,DC=addom,DC=samba,DC=example,DC=com",
+           "Removed");
+       check_group_change_message(
+           1,
+           "cn=grpadttstuser04,cn=users,DC=addom,DC=samba,DC=example,DC=com",
+           "Added");
+
+       /*
+        * Clean up
+        */
+       json_free(&messages[0]);
+       json_free(&messages[1]);
+       TALLOC_FREE(ctx);
 }
 
 /*
@@ -720,13 +1315,16 @@ static void test_place_holder(void **state)
 int main(void) {
        const struct CMUnitTest tests[] = {
                cmocka_unit_test(test_audit_group_json),
-               cmocka_unit_test(test_place_holder),
                cmocka_unit_test(test_get_transaction_id),
                cmocka_unit_test(test_audit_group_hr),
                cmocka_unit_test(test_get_parsed_dns),
                cmocka_unit_test(test_dn_compare),
                cmocka_unit_test(test_get_primary_group_dn),
-
+               cmocka_unit_test(test_log_membership_changes_removed),
+               cmocka_unit_test(test_log_membership_changes_remove_all),
+               cmocka_unit_test(test_log_membership_changes_added),
+               cmocka_unit_test(test_log_membership_changes_add_to_empty),
+               cmocka_unit_test(test_log_membership_changes_rmd_flags),
        };
 
        cmocka_set_message_output(CM_OUTPUT_SUBUNIT);