feat: read the lockout threshold and window in the password policy

The password change must stay below the domain's lockout threshold. The policy now carries lockout_threshold and lockout_window, from the domain or from the fine-grained policy that applies. AD-2's read allow-list names the new attributes.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01N3t4WL7bd9bdHHNjEsxzqC
This commit is contained in:
2026-10-18 09:55:10 +02:00
co-authored by Claude Sonnet 5.5
parent ead56709bc
commit b87ffae94c
7 changed files with 24 additions and 1 deletions
+1
View File
@@ -86,6 +86,7 @@ def test_password_policy_is_returned(directory):
policy = directory.password_policy("alice")
assert policy.min_length == 8
assert policy.min_age == timedelta(0)
assert policy.lockout_threshold == 5
def test_password_change_needs_the_current_password(directory):
+1
View File
@@ -70,6 +70,7 @@ def test_other_attributes_are_refused(live):
def test_password_policy_is_returned(live):
policy = live.password_policy("ptest-user1")
assert policy.min_length >= 0
assert policy.lockout_threshold >= 0 # 0: the domain never locks accounts
def test_verify_password(live):
+7
View File
@@ -77,6 +77,8 @@ def world():
minPwdAge=str(-864000000000),
maxPwdAge=str(-77760000000000),
lockoutDuration=str(-18000000000),
lockoutThreshold="5",
lockoutObservationWindow=str(-18000000000),
),
)
]
@@ -201,6 +203,8 @@ def test_password_policy_converts_the_directory_units(directory):
assert policy.min_age == timedelta(days=1)
assert policy.max_age == timedelta(days=90)
assert policy.fine_grained is False
assert policy.lockout_threshold == 5
assert policy.lockout_window == timedelta(minutes=30)
def test_a_fine_grained_policy_is_used_when_readable(directory, world):
@@ -216,6 +220,8 @@ def test_a_fine_grained_policy_is_used_when_readable(directory, world):
"msDS-PasswordHistoryLength": "5",
"msDS-MinimumPasswordAge": "0",
"msDS-MaximumPasswordAge": str(-(2**63)),
"msDS-LockoutThreshold": "3",
"msDS-LockoutObservationWindow": str(-6000000000),
}
),
)
@@ -223,6 +229,7 @@ def test_a_fine_grained_policy_is_used_when_readable(directory, world):
policy = directory.password_policy("alice")
assert (policy.min_length, policy.complexity, policy.history_length) == (20, False, 5)
assert policy.max_age is None and policy.fine_grained is True
assert policy.lockout_threshold == 3 and policy.lockout_window == timedelta(minutes=10)
def test_an_unreadable_fine_grained_policy_is_flagged_not_hidden(directory, world, monkeypatch):