815 lines
34 KiB
Diff
815 lines
34 KiB
Diff
From e05653cbff500c47b89e43e4a1c85b7cb30321ff Mon Sep 17 00:00:00 2001
|
|
From: Simon Pichugin <spichugi@redhat.com>
|
|
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
|
|
|