From e05653cbff500c47b89e43e4a1c85b7cb30321ff Mon Sep 17 00:00:00 2001 From: Simon Pichugin Date: Mon, 28 Jul 2025 15:41:29 -0700 Subject: [PATCH] Issue 6884 - Mask password hashes in audit logs (#6885) Description: Fix the audit log functionality to mask password hash values for userPassword, nsslapd-rootpw, nsmultiplexorcredentials, nsds5ReplicaCredentials, and nsds5ReplicaBootstrapCredentials attributes in ADD and MODIFY operations. Update auditlog.c to detect password attributes and replace their values with asterisks (**********************) in both LDIF and JSON audit log formats. Add a comprehensive test suite audit_password_masking_test.py to verify password masking works correctly across all log formats and operation types. Fixes: https://github.com/389ds/389-ds-base/issues/6884 Reviewed by: @mreynolds389, @vashirov (Thanks!!) --- .../logging/audit_password_masking_test.py | 501 ++++++++++++++++++ ldap/servers/slapd/auditlog.c | 170 +++++- ldap/servers/slapd/slapi-private.h | 1 + src/lib389/lib389/chaining.py | 3 +- 4 files changed, 652 insertions(+), 23 deletions(-) create mode 100644 dirsrvtests/tests/suites/logging/audit_password_masking_test.py diff --git a/dirsrvtests/tests/suites/logging/audit_password_masking_test.py b/dirsrvtests/tests/suites/logging/audit_password_masking_test.py new file mode 100644 index 000000000..3b6a54849 --- /dev/null +++ b/dirsrvtests/tests/suites/logging/audit_password_masking_test.py @@ -0,0 +1,501 @@ +# --- BEGIN COPYRIGHT BLOCK --- +# Copyright (C) 2025 Red Hat, Inc. +# All rights reserved. +# +# License: GPL (version 3 or any later version). +# See LICENSE for details. +# --- END COPYRIGHT BLOCK --- +# +import logging +import pytest +import os +import re +import time +import ldap +from lib389._constants import DEFAULT_SUFFIX, DN_DM, PW_DM +from lib389.topologies import topology_m2 as topo +from lib389.idm.user import UserAccounts +from lib389.dirsrv_log import DirsrvAuditJSONLog +from lib389.plugins import ChainingBackendPlugin +from lib389.chaining import ChainingLinks +from lib389.agreement import Agreements +from lib389.replica import ReplicationManager, Replicas +from lib389.idm.directorymanager import DirectoryManager + +log = logging.getLogger(__name__) + +MASKED_PASSWORD = "**********************" +TEST_PASSWORD = "MySecret123" +TEST_PASSWORD_2 = "NewPassword789" +TEST_PASSWORD_3 = "NewPassword101" + + +def setup_audit_logging(inst, log_format='default', display_attrs=None): + """Configure audit logging settings""" + inst.config.replace('nsslapd-auditlog-logbuffering', 'off') + inst.config.replace('nsslapd-auditlog-logging-enabled', 'on') + inst.config.replace('nsslapd-auditlog-log-format', log_format) + + if display_attrs is not None: + inst.config.replace('nsslapd-auditlog-display-attrs', display_attrs) + + inst.deleteAuditLogs() + + +def check_password_masked(inst, log_format, expected_password, actual_password): + """Helper function to check password masking in audit logs""" + + time.sleep(1) # Allow log to flush + + # List of all password/credential attributes that should be masked + password_attributes = [ + 'userPassword', + 'nsslapd-rootpw', + 'nsmultiplexorcredentials', + 'nsDS5ReplicaCredentials', + 'nsDS5ReplicaBootstrapCredentials' + ] + + # Get password schemes to check for hash leakage + user_password_scheme = inst.config.get_attr_val_utf8('passwordStorageScheme') + root_password_scheme = inst.config.get_attr_val_utf8('nsslapd-rootpwstoragescheme') + + if log_format == 'json': + # Check JSON format logs + audit_log = DirsrvAuditJSONLog(inst) + log_lines = audit_log.readlines() + + found_masked = False + found_actual = False + found_hashed = False + + for line in log_lines: + # Check if any password attribute is present in the line + for attr in password_attributes: + if attr in line: + if expected_password in line: + found_masked = True + if actual_password in line: + found_actual = True + # Check for password scheme indicators (hashed passwords) + if user_password_scheme and f'{{{user_password_scheme}}}' in line: + found_hashed = True + if root_password_scheme and f'{{{root_password_scheme}}}' in line: + found_hashed = True + break # Found a password attribute, no need to check others for this line + + else: + # Check LDIF format logs + found_masked = False + found_actual = False + found_hashed = False + + # Check each password attribute for masked password + for attr in password_attributes: + if inst.ds_audit_log.match(f"{attr}: {re.escape(expected_password)}"): + found_masked = True + if inst.ds_audit_log.match(f"{attr}: {actual_password}"): + found_actual = True + + # Check for hashed passwords in LDIF format + if user_password_scheme: + if inst.ds_audit_log.match(f"userPassword: {{{user_password_scheme}}}"): + found_hashed = True + if root_password_scheme: + if inst.ds_audit_log.match(f"nsslapd-rootpw: {{{root_password_scheme}}}"): + found_hashed = True + + # Delete audit logs to avoid interference with other tests + # We need to reset the root password to default as deleteAuditLogs() + # opens a new connection with the default password + dm = DirectoryManager(inst) + dm.change_password(PW_DM) + inst.deleteAuditLogs() + + return found_masked, found_actual, found_hashed + + +@pytest.mark.parametrize("log_format,display_attrs", [ + ("default", None), + ("default", "*"), + ("default", "userPassword"), + ("json", None), + ("json", "*"), + ("json", "userPassword") +]) +def test_password_masking_add_operation(topo, log_format, display_attrs): + """Test password masking in ADD operations + + :id: 4358bd75-bcc7-401c-b492-d3209b10412d + :parametrized: yes + :setup: Standalone Instance + :steps: + 1. Configure audit logging format + 2. Add user with password + 3. Check that password is masked in audit log + 4. Verify actual password does not appear in log + :expectedresults: + 1. Success + 2. Success + 3. Password should be masked with asterisks + 4. Actual password should not be found in log + """ + inst = topo.ms['supplier1'] + setup_audit_logging(inst, log_format, display_attrs) + + users = UserAccounts(inst, DEFAULT_SUFFIX) + user = None + + try: + user = users.create(properties={ + 'uid': 'test_add_pwd_mask', + 'cn': 'Test Add User', + 'sn': 'User', + 'uidNumber': '1000', + 'gidNumber': '1000', + 'homeDirectory': '/home/test_add', + 'userPassword': TEST_PASSWORD + }) + + found_masked, found_actual, found_hashed = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD) + + assert found_masked, f"Masked password not found in {log_format} ADD operation" + assert not found_actual, f"Actual password found in {log_format} ADD log (should be masked)" + assert not found_hashed, f"Hashed password found in {log_format} ADD log (should be masked)" + + finally: + if user is not None: + try: + user.delete() + except: + pass + + +@pytest.mark.parametrize("log_format,display_attrs", [ + ("default", None), + ("default", "*"), + ("default", "userPassword"), + ("json", None), + ("json", "*"), + ("json", "userPassword") +]) +def test_password_masking_modify_operation(topo, log_format, display_attrs): + """Test password masking in MODIFY operations + + :id: e6963aa9-7609-419c-aae2-1d517aa434bd + :parametrized: yes + :setup: Standalone Instance + :steps: + 1. Configure audit logging format + 2. Add user without password + 3. Add password via MODIFY operation + 4. Check that password is masked in audit log + 5. Modify password to new value + 6. Check that new password is also masked + 7. Verify actual passwords do not appear in log + :expectedresults: + 1. Success + 2. Success + 3. Success + 4. Password should be masked with asterisks + 5. Success + 6. New password should be masked with asterisks + 7. No actual password values should be found in log + """ + inst = topo.ms['supplier1'] + setup_audit_logging(inst, log_format, display_attrs) + + users = UserAccounts(inst, DEFAULT_SUFFIX) + user = None + + try: + user = users.create(properties={ + 'uid': 'test_modify_pwd_mask', + 'cn': 'Test Modify User', + 'sn': 'User', + 'uidNumber': '2000', + 'gidNumber': '2000', + 'homeDirectory': '/home/test_modify' + }) + + user.replace('userPassword', TEST_PASSWORD) + + found_masked, found_actual, found_hashed = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD) + assert found_masked, f"Masked password not found in {log_format} MODIFY operation (first password)" + assert not found_actual, f"Actual password found in {log_format} MODIFY log (should be masked)" + assert not found_hashed, f"Hashed password found in {log_format} MODIFY log (should be masked)" + + user.replace('userPassword', TEST_PASSWORD_2) + + found_masked_2, found_actual_2, found_hashed_2 = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD_2) + assert found_masked_2, f"Masked password not found in {log_format} MODIFY operation (second password)" + assert not found_actual_2, f"Second actual password found in {log_format} MODIFY log (should be masked)" + assert not found_hashed_2, f"Second hashed password found in {log_format} MODIFY log (should be masked)" + + finally: + if user is not None: + try: + user.delete() + except: + pass + + +@pytest.mark.parametrize("log_format,display_attrs", [ + ("default", None), + ("default", "*"), + ("default", "nsslapd-rootpw"), + ("json", None), + ("json", "*"), + ("json", "nsslapd-rootpw") +]) +def test_password_masking_rootpw_modify_operation(topo, log_format, display_attrs): + """Test password masking for nsslapd-rootpw MODIFY operations + + :id: ec8c9fd4-56ba-4663-ab65-58efb3b445e4 + :parametrized: yes + :setup: Standalone Instance + :steps: + 1. Configure audit logging format + 2. Modify nsslapd-rootpw in configuration + 3. Check that root password is masked in audit log + 4. Modify root password to new value + 5. Check that new root password is also masked + 6. Verify actual root passwords do not appear in log + :expectedresults: + 1. Success + 2. Success + 3. Root password should be masked with asterisks + 4. Success + 5. New root password should be masked with asterisks + 6. No actual root password values should be found in log + """ + inst = topo.ms['supplier1'] + setup_audit_logging(inst, log_format, display_attrs) + dm = DirectoryManager(inst) + + try: + dm.change_password(TEST_PASSWORD) + dm.rebind(TEST_PASSWORD) + + found_masked, found_actual, found_hashed = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD) + assert found_masked, f"Masked root password not found in {log_format} MODIFY operation (first root password)" + assert not found_actual, f"Actual root password found in {log_format} MODIFY log (should be masked)" + assert not found_hashed, f"Hashed root password found in {log_format} MODIFY log (should be masked)" + + dm.change_password(TEST_PASSWORD_2) + dm.rebind(TEST_PASSWORD_2) + + found_masked_2, found_actual_2, found_hashed_2 = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD_2) + assert found_masked_2, f"Masked root password not found in {log_format} MODIFY operation (second root password)" + assert not found_actual_2, f"Second actual root password found in {log_format} MODIFY log (should be masked)" + assert not found_hashed_2, f"Second hashed root password found in {log_format} MODIFY log (should be masked)" + + finally: + dm.change_password(PW_DM) + dm.rebind(PW_DM) + + +@pytest.mark.parametrize("log_format,display_attrs", [ + ("default", None), + ("default", "*"), + ("default", "nsmultiplexorcredentials"), + ("json", None), + ("json", "*"), + ("json", "nsmultiplexorcredentials") +]) +def test_password_masking_multiplexor_credentials(topo, log_format, display_attrs): + """Test password masking for nsmultiplexorcredentials in chaining/multiplexor configurations + + :id: 161a9498-b248-4926-90be-a696a36ed36e + :parametrized: yes + :setup: Standalone Instance + :steps: + 1. Configure audit logging format + 2. Create a chaining backend configuration entry with nsmultiplexorcredentials + 3. Check that multiplexor credentials are masked in audit log + 4. Modify the credentials + 5. Check that updated credentials are also masked + 6. Verify actual credentials do not appear in log + :expectedresults: + 1. Success + 2. Success + 3. Multiplexor credentials should be masked with asterisks + 4. Success + 5. Updated credentials should be masked with asterisks + 6. No actual credential values should be found in log + """ + inst = topo.ms['supplier1'] + setup_audit_logging(inst, log_format, display_attrs) + + # Enable chaining plugin and create chaining link + chain_plugin = ChainingBackendPlugin(inst) + chain_plugin.enable() + + chains = ChainingLinks(inst) + chain = None + + try: + # Create chaining link with multiplexor credentials + chain = chains.create(properties={ + 'cn': 'testchain', + 'nsfarmserverurl': 'ldap://localhost:389/', + 'nsslapd-suffix': 'dc=example,dc=com', + 'nsmultiplexorbinddn': 'cn=manager', + 'nsmultiplexorcredentials': TEST_PASSWORD, + 'nsCheckLocalACI': 'on', + 'nsConnectionLife': '30', + }) + + found_masked, found_actual, found_hashed = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD) + assert found_masked, f"Masked multiplexor credentials not found in {log_format} ADD operation" + assert not found_actual, f"Actual multiplexor credentials found in {log_format} ADD log (should be masked)" + assert not found_hashed, f"Hashed multiplexor credentials found in {log_format} ADD log (should be masked)" + + # Modify the credentials + chain.replace('nsmultiplexorcredentials', TEST_PASSWORD_2) + + found_masked_2, found_actual_2, found_hashed_2 = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD_2) + assert found_masked_2, f"Masked multiplexor credentials not found in {log_format} MODIFY operation" + assert not found_actual_2, f"Actual multiplexor credentials found in {log_format} MODIFY log (should be masked)" + assert not found_hashed_2, f"Hashed multiplexor credentials found in {log_format} MODIFY log (should be masked)" + + finally: + chain_plugin.disable() + if chain is not None: + inst.delete_branch_s(chain.dn, ldap.SCOPE_ONELEVEL) + chain.delete() + + +@pytest.mark.parametrize("log_format,display_attrs", [ + ("default", None), + ("default", "*"), + ("default", "nsDS5ReplicaCredentials"), + ("json", None), + ("json", "*"), + ("json", "nsDS5ReplicaCredentials") +]) +def test_password_masking_replica_credentials(topo, log_format, display_attrs): + """Test password masking for nsDS5ReplicaCredentials in replication agreements + + :id: 7bf9e612-1b7c-49af-9fc0-de4c7df84b2a + :parametrized: yes + :setup: Standalone Instance + :steps: + 1. Configure audit logging format + 2. Create a replication agreement entry with nsDS5ReplicaCredentials + 3. Check that replica credentials are masked in audit log + 4. Modify the credentials + 5. Check that updated credentials are also masked + 6. Verify actual credentials do not appear in log + :expectedresults: + 1. Success + 2. Success + 3. Replica credentials should be masked with asterisks + 4. Success + 5. Updated credentials should be masked with asterisks + 6. No actual credential values should be found in log + """ + inst = topo.ms['supplier2'] + setup_audit_logging(inst, log_format, display_attrs) + agmt = None + + try: + replicas = Replicas(inst) + replica = replicas.get(DEFAULT_SUFFIX) + agmts = replica.get_agreements() + agmt = agmts.create(properties={ + 'cn': 'testagmt', + 'nsDS5ReplicaHost': 'localhost', + 'nsDS5ReplicaPort': '389', + 'nsDS5ReplicaBindDN': 'cn=replication manager,cn=config', + 'nsDS5ReplicaCredentials': TEST_PASSWORD, + 'nsDS5ReplicaRoot': DEFAULT_SUFFIX + }) + + found_masked, found_actual, found_hashed = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD) + assert found_masked, f"Masked replica credentials not found in {log_format} ADD operation" + assert not found_actual, f"Actual replica credentials found in {log_format} ADD log (should be masked)" + assert not found_hashed, f"Hashed replica credentials found in {log_format} ADD log (should be masked)" + + # Modify the credentials + agmt.replace('nsDS5ReplicaCredentials', TEST_PASSWORD_2) + + found_masked_2, found_actual_2, found_hashed_2 = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD_2) + assert found_masked_2, f"Masked replica credentials not found in {log_format} MODIFY operation" + assert not found_actual_2, f"Actual replica credentials found in {log_format} MODIFY log (should be masked)" + assert not found_hashed_2, f"Hashed replica credentials found in {log_format} MODIFY log (should be masked)" + + finally: + if agmt is not None: + agmt.delete() + + +@pytest.mark.parametrize("log_format,display_attrs", [ + ("default", None), + ("default", "*"), + ("default", "nsDS5ReplicaBootstrapCredentials"), + ("json", None), + ("json", "*"), + ("json", "nsDS5ReplicaBootstrapCredentials") +]) +def test_password_masking_bootstrap_credentials(topo, log_format, display_attrs): + """Test password masking for nsDS5ReplicaCredentials and nsDS5ReplicaBootstrapCredentials in replication agreements + + :id: 248bd418-ffa4-4733-963d-2314c60b7c5b + :parametrized: yes + :setup: Standalone Instance + :steps: + 1. Configure audit logging format + 2. Create a replication agreement entry with both nsDS5ReplicaCredentials and nsDS5ReplicaBootstrapCredentials + 3. Check that both credentials are masked in audit log + 4. Modify both credentials + 5. Check that both updated credentials are also masked + 6. Verify actual credentials do not appear in log + :expectedresults: + 1. Success + 2. Success + 3. Both credentials should be masked with asterisks + 4. Success + 5. Both updated credentials should be masked with asterisks + 6. No actual credential values should be found in log + """ + inst = topo.ms['supplier2'] + setup_audit_logging(inst, log_format, display_attrs) + agmt = None + + try: + replicas = Replicas(inst) + replica = replicas.get(DEFAULT_SUFFIX) + agmts = replica.get_agreements() + agmt = agmts.create(properties={ + 'cn': 'testbootstrapagmt', + 'nsDS5ReplicaHost': 'localhost', + 'nsDS5ReplicaPort': '389', + 'nsDS5ReplicaBindDN': 'cn=replication manager,cn=config', + 'nsDS5ReplicaCredentials': TEST_PASSWORD, + 'nsDS5replicabootstrapbinddn': 'cn=bootstrap manager,cn=config', + 'nsDS5ReplicaBootstrapCredentials': TEST_PASSWORD_2, + 'nsDS5ReplicaRoot': DEFAULT_SUFFIX + }) + + found_masked_bootstrap, found_actual_bootstrap, found_hashed_bootstrap = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD_2) + assert found_masked_bootstrap, f"Masked bootstrap credentials not found in {log_format} ADD operation" + assert not found_actual_bootstrap, f"Actual bootstrap credentials found in {log_format} ADD log (should be masked)" + assert not found_hashed_bootstrap, f"Hashed bootstrap credentials found in {log_format} ADD log (should be masked)" + + agmt.replace('nsDS5ReplicaBootstrapCredentials', TEST_PASSWORD_3) + + found_masked_bootstrap_2, found_actual_bootstrap_2, found_hashed_bootstrap_2 = check_password_masked(inst, log_format, MASKED_PASSWORD, TEST_PASSWORD_3) + assert found_masked_bootstrap_2, f"Masked bootstrap credentials not found in {log_format} MODIFY operation" + assert not found_actual_bootstrap_2, f"Actual bootstrap credentials found in {log_format} MODIFY log (should be masked)" + assert not found_hashed_bootstrap_2, f"Hashed bootstrap credentials found in {log_format} MODIFY log (should be masked)" + + finally: + if agmt is not None: + agmt.delete() + + + +if __name__ == '__main__': + CURRENT_FILE = os.path.realpath(__file__) + pytest.main(["-s", CURRENT_FILE]) \ No newline at end of file diff --git a/ldap/servers/slapd/auditlog.c b/ldap/servers/slapd/auditlog.c index 3945b0533..3a34959f6 100644 --- a/ldap/servers/slapd/auditlog.c +++ b/ldap/servers/slapd/auditlog.c @@ -39,6 +39,89 @@ static void write_audit_file(Slapi_PBlock *pb, Slapi_Entry *entry, int logtype, static const char *modrdn_changes[4]; +/* Helper function to check if an attribute is a password that needs masking */ +static int +is_password_attribute(const char *attr_name) +{ + return (strcasecmp(attr_name, SLAPI_USERPWD_ATTR) == 0 || + strcasecmp(attr_name, CONFIG_ROOTPW_ATTRIBUTE) == 0 || + strcasecmp(attr_name, SLAPI_MB_CREDENTIALS) == 0 || + strcasecmp(attr_name, SLAPI_REP_CREDENTIALS) == 0 || + strcasecmp(attr_name, SLAPI_REP_BOOTSTRAP_CREDENTIALS) == 0); +} + +/* Helper function to create a masked string representation of an entry */ +static char * +create_masked_entry_string(Slapi_Entry *original_entry, int *len) +{ + Slapi_Attr *attr = NULL; + char *entry_str = NULL; + char *current_pos = NULL; + char *line_start = NULL; + char *next_line = NULL; + char *colon_pos = NULL; + int has_password_attrs = 0; + + if (original_entry == NULL) { + return NULL; + } + + /* Single pass through attributes to check for password attributes */ + for (slapi_entry_first_attr(original_entry, &attr); attr != NULL; + slapi_entry_next_attr(original_entry, attr, &attr)) { + + char *attr_name = NULL; + slapi_attr_get_type(attr, &attr_name); + + if (is_password_attribute(attr_name)) { + has_password_attrs = 1; + break; + } + } + + /* If no password attributes, return original string - no masking needed */ + entry_str = slapi_entry2str(original_entry, len); + if (!has_password_attrs) { + return entry_str; + } + + /* Process the string in-place, replacing password values */ + current_pos = entry_str; + while ((line_start = current_pos) != NULL && *line_start != '\0') { + /* Find the end of current line */ + next_line = strchr(line_start, '\n'); + if (next_line != NULL) { + *next_line = '\0'; /* Temporarily terminate line */ + current_pos = next_line + 1; + } else { + current_pos = NULL; /* Last line */ + } + + /* Find the colon that separates attribute name from value */ + colon_pos = strchr(line_start, ':'); + if (colon_pos != NULL) { + char saved_colon = *colon_pos; + *colon_pos = '\0'; /* Temporarily null-terminate attribute name */ + + /* Check if this is a password attribute that needs masking */ + if (is_password_attribute(line_start)) { + strcpy(colon_pos + 1, " **********************"); + } + + *colon_pos = saved_colon; /* Restore colon */ + } + + /* Restore newline if it was there */ + if (next_line != NULL) { + *next_line = '\n'; + } + } + + /* Update length since we may have shortened the string */ + *len = strlen(entry_str); + return entry_str; /* Return the modified original string */ +} + void write_audit_log_entry(Slapi_PBlock *pb) { @@ -279,10 +362,31 @@ add_entry_attrs_ext(Slapi_Entry *entry, lenstr *l, PRBool use_json, json_object { slapi_entry_attr_find(entry, req_attr, &entry_attr); if (entry_attr) { - if (use_json) { - log_entry_attr_json(entry_attr, req_attr, id_list); + if (strcmp(req_attr, PSEUDO_ATTR_UNHASHEDUSERPASSWORD) == 0) { + /* Do not write the unhashed clear-text password */ + continue; + } + + /* Check if this is a password attribute that needs masking */ + if (is_password_attribute(req_attr)) { + /* userpassword/rootdn password - mask the value */ + if (use_json) { + json_object *secret_obj = json_object_new_object(); + json_object_object_add(secret_obj, req_attr, + json_object_new_string("**********************")); + json_object_array_add(id_list, secret_obj); + } else { + addlenstr(l, "#"); + addlenstr(l, req_attr); + addlenstr(l, ": **********************\n"); + } } else { - log_entry_attr(entry_attr, req_attr, l); + /* Regular attribute - log normally */ + if (use_json) { + log_entry_attr_json(entry_attr, req_attr, id_list); + } else { + log_entry_attr(entry_attr, req_attr, l); + } } } } @@ -297,9 +401,7 @@ add_entry_attrs_ext(Slapi_Entry *entry, lenstr *l, PRBool use_json, json_object continue; } - if (strcasecmp(attr, SLAPI_USERPWD_ATTR) == 0 || - strcasecmp(attr, CONFIG_ROOTPW_ATTRIBUTE) == 0) - { + if (is_password_attribute(attr)) { /* userpassword/rootdn password - mask the value */ if (use_json) { json_object *secret_obj = json_object_new_object(); @@ -309,7 +411,7 @@ add_entry_attrs_ext(Slapi_Entry *entry, lenstr *l, PRBool use_json, json_object } else { addlenstr(l, "#"); addlenstr(l, attr); - addlenstr(l, ": ****************************\n"); + addlenstr(l, ": **********************\n"); } continue; } @@ -478,6 +580,9 @@ write_audit_file_json(Slapi_PBlock *pb, Slapi_Entry *entry, int logtype, } } + /* Check if this is a password attribute that needs masking */ + int is_password_attr = is_password_attribute(mods[j]->mod_type); + mod = json_object_new_object(); switch (operationtype) { case LDAP_MOD_ADD: @@ -502,7 +607,12 @@ write_audit_file_json(Slapi_PBlock *pb, Slapi_Entry *entry, int logtype, json_object *val_list = NULL; val_list = json_object_new_array(); for (size_t i = 0; mods[j]->mod_bvalues != NULL && mods[j]->mod_bvalues[i] != NULL; i++) { - json_object_array_add(val_list, json_object_new_string(mods[j]->mod_bvalues[i]->bv_val)); + if (is_password_attr) { + /* Mask password values */ + json_object_array_add(val_list, json_object_new_string("**********************")); + } else { + json_object_array_add(val_list, json_object_new_string(mods[j]->mod_bvalues[i]->bv_val)); + } } json_object_object_add(mod, "values", val_list); } @@ -514,8 +624,11 @@ write_audit_file_json(Slapi_PBlock *pb, Slapi_Entry *entry, int logtype, case SLAPI_OPERATION_ADD: int len; + e = change; - tmp = slapi_entry2str(e, &len); + + /* Create a masked string representation for password attributes */ + tmp = create_masked_entry_string(e, &len); tmpsave = tmp; while ((tmp = strchr(tmp, '\n')) != NULL) { tmp++; @@ -662,6 +775,10 @@ write_audit_file( break; } } + + /* Check if this is a password attribute that needs masking */ + int is_password_attr = is_password_attribute(mods[j]->mod_type); + switch (operationtype) { case LDAP_MOD_ADD: addlenstr(l, "add: "); @@ -686,18 +803,27 @@ write_audit_file( break; } if (operationtype != LDAP_MOD_IGNORE) { - for (i = 0; mods[j]->mod_bvalues != NULL && mods[j]->mod_bvalues[i] != NULL; i++) { - char *buf, *bufp; - len = strlen(mods[j]->mod_type); - len = LDIF_SIZE_NEEDED(len, mods[j]->mod_bvalues[i]->bv_len) + 1; - buf = slapi_ch_malloc(len); - bufp = buf; - slapi_ldif_put_type_and_value_with_options(&bufp, mods[j]->mod_type, - mods[j]->mod_bvalues[i]->bv_val, - mods[j]->mod_bvalues[i]->bv_len, 0); - *bufp = '\0'; - addlenstr(l, buf); - slapi_ch_free((void **)&buf); + if (is_password_attr) { + /* Add masked password */ + for (i = 0; mods[j]->mod_bvalues != NULL && mods[j]->mod_bvalues[i] != NULL; i++) { + addlenstr(l, mods[j]->mod_type); + addlenstr(l, ": **********************\n"); + } + } else { + /* Add actual values for non-password attributes */ + for (i = 0; mods[j]->mod_bvalues != NULL && mods[j]->mod_bvalues[i] != NULL; i++) { + char *buf, *bufp; + len = strlen(mods[j]->mod_type); + len = LDIF_SIZE_NEEDED(len, mods[j]->mod_bvalues[i]->bv_len) + 1; + buf = slapi_ch_malloc(len); + bufp = buf; + slapi_ldif_put_type_and_value_with_options(&bufp, mods[j]->mod_type, + mods[j]->mod_bvalues[i]->bv_val, + mods[j]->mod_bvalues[i]->bv_len, 0); + *bufp = '\0'; + addlenstr(l, buf); + slapi_ch_free((void **)&buf); + } } } addlenstr(l, "-\n"); @@ -708,7 +834,7 @@ write_audit_file( e = change; addlenstr(l, attr_changetype); addlenstr(l, ": add\n"); - tmp = slapi_entry2str(e, &len); + tmp = create_masked_entry_string(e, &len); tmpsave = tmp; while ((tmp = strchr(tmp, '\n')) != NULL) { tmp++; diff --git a/ldap/servers/slapd/slapi-private.h b/ldap/servers/slapd/slapi-private.h index 7a3eb3fdf..fb88488b1 100644 --- a/ldap/servers/slapd/slapi-private.h +++ b/ldap/servers/slapd/slapi-private.h @@ -848,6 +848,7 @@ void task_cleanup(void); /* for reversible encyrption */ #define SLAPI_MB_CREDENTIALS "nsmultiplexorcredentials" #define SLAPI_REP_CREDENTIALS "nsds5ReplicaCredentials" +#define SLAPI_REP_BOOTSTRAP_CREDENTIALS "nsds5ReplicaBootstrapCredentials" int pw_rever_encode(Slapi_Value **vals, char *attr_name); int pw_rever_decode(char *cipher, char **plain, const char *attr_name); diff --git a/src/lib389/lib389/chaining.py b/src/lib389/lib389/chaining.py index 533b83ebf..33ae78c8b 100644 --- a/src/lib389/lib389/chaining.py +++ b/src/lib389/lib389/chaining.py @@ -134,7 +134,7 @@ class ChainingLink(DSLdapObject): """ # Create chaining entry - super(ChainingLink, self).create(rdn, properties, basedn) + link = super(ChainingLink, self).create(rdn, properties, basedn) # Create mapping tree entry dn_comps = ldap.explode_dn(properties['nsslapd-suffix'][0]) @@ -149,6 +149,7 @@ class ChainingLink(DSLdapObject): self._mts.ensure_state(properties=mt_properties) except ldap.ALREADY_EXISTS: pass + return link class ChainingLinks(DSLdapObjects): -- 2.49.0