Preview one uid's directory groups for sign-in diagnosis (NK-WP-0051-T04).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 1041694@bnt-lap001 Assistant-Session: 7af0bb68-ac4a-4ce3-8639-24ddd84c0b52
This commit is contained in:
parent
106d97f4a8
commit
2cc09ceb05
4 changed files with 60 additions and 5 deletions
|
|
@ -70,6 +70,21 @@ suspension (`netkingdom-`) and LLDAP administrative groups are refused by the
|
||||||
existing service-group validator before login. Use their respective owners for
|
existing service-group validator before login. Use their respective owners for
|
||||||
those operations; the old unrestricted membership helper is not a fallback.
|
those operations; the old unrestricted membership helper is not a fallback.
|
||||||
|
|
||||||
|
## Sign-in diagnosis: one uid's groups
|
||||||
|
|
||||||
|
To see which directory groups a login will carry (NK-WP-0051,
|
||||||
|
[`docs/sign-in-diagnosis.md`](../../../docs/sign-in-diagnosis.md)):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 lldap_admin.py groups --user signin-probe-negative \
|
||||||
|
--root /absolute/private/custody
|
||||||
|
```
|
||||||
|
|
||||||
|
It is read-only and has no `--apply`. It prints `uid` and `groups` (sorted
|
||||||
|
display names, never DNs, mail or credentials). An unknown uid is refused, not
|
||||||
|
reported as an empty list. Matching the names to an OpenBao role stays a
|
||||||
|
metadata read in ops-mason.
|
||||||
|
|
||||||
## Preservation, interruption and recovery
|
## Preservation, interruption and recovery
|
||||||
|
|
||||||
The digest binds origin, operation, target and the observed directory users,
|
The digest binds origin, operation, target and the observed directory users,
|
||||||
|
|
|
||||||
|
|
@ -147,8 +147,13 @@ def snapshot(provider, token):
|
||||||
|
|
||||||
|
|
||||||
def validate_operation(operation, action=None, group=None, user=None):
|
def validate_operation(operation, action=None, group=None, user=None):
|
||||||
if operation not in {'bootstrap', 'membership', 'verify'}:
|
if operation not in {'bootstrap', 'membership', 'verify', 'groups'}:
|
||||||
raise Refused()
|
raise Refused()
|
||||||
|
if operation == 'groups':
|
||||||
|
# Read-only sign-in diagnosis preview (docs/sign-in-diagnosis.md).
|
||||||
|
if action is not None or group is not None or not isinstance(user, str) or not re.fullmatch(r'[a-z][a-z0-9._-]{0,127}', user):
|
||||||
|
raise Refused()
|
||||||
|
return
|
||||||
if operation != 'membership':
|
if operation != 'membership':
|
||||||
if any(v is not None for v in (action, group, user)):
|
if any(v is not None for v in (action, group, user)):
|
||||||
raise Refused()
|
raise Refused()
|
||||||
|
|
@ -167,7 +172,7 @@ def validate_operation(operation, action=None, group=None, user=None):
|
||||||
|
|
||||||
def maintain(provider, base, operation, *, action=None, group=None, user=None, apply=False, expected_digest=None):
|
def maintain(provider, base, operation, *, action=None, group=None, user=None, apply=False, expected_digest=None):
|
||||||
validate_operation(operation, action, group, user)
|
validate_operation(operation, action, group, user)
|
||||||
if (apply and (operation == 'verify' or action == 'list')) or (expected_digest is not None and not apply):
|
if (apply and (operation in ('verify', 'groups') or action == 'list')) or (expected_digest is not None and not apply):
|
||||||
raise Refused()
|
raise Refused()
|
||||||
if apply and (not isinstance(expected_digest, str) or not re.fullmatch(r'[0-9a-f]{64}', expected_digest)):
|
if apply and (not isinstance(expected_digest, str) or not re.fullmatch(r'[0-9a-f]{64}', expected_digest)):
|
||||||
raise Refused()
|
raise Refused()
|
||||||
|
|
@ -182,6 +187,11 @@ def maintain(provider, base, operation, *, action=None, group=None, user=None, a
|
||||||
'native_acceptance': 'unverified',
|
'native_acceptance': 'unverified',
|
||||||
'remaining': ['governed_lifecycle', 'actual_login_and_mfa', 'issued_claims_and_consumer_access',
|
'remaining': ['governed_lifecycle', 'actual_login_and_mfa', 'issued_claims_and_consumer_access',
|
||||||
'session_revocation', 'independent_recovery']}
|
'session_revocation', 'independent_recovery']}
|
||||||
|
if operation == 'groups':
|
||||||
|
if user not in before['users']:
|
||||||
|
raise Refused()
|
||||||
|
names = sorted(name for name, item in before['groups'].items() if user in item['members'])
|
||||||
|
return {'result': 'groups_observed', 'uid': user, 'groups': names, 'native_acceptance': 'unverified'}
|
||||||
if operation == 'membership' and (group not in before['groups'] or (user is not None and user not in before['users'])):
|
if operation == 'membership' and (group not in before['groups'] or (user is not None and user not in before['users'])):
|
||||||
raise Refused()
|
raise Refused()
|
||||||
binding = {'base': endpoint(base), 'operation': operation, 'action': action, 'group': group, 'user': user, 'before': before}
|
binding = {'base': endpoint(base), 'operation': operation, 'action': action, 'group': group, 'user': user, 'before': before}
|
||||||
|
|
@ -234,11 +244,13 @@ def main(argv=None):
|
||||||
try:
|
try:
|
||||||
parser = Parser(description=__doc__)
|
parser = Parser(description=__doc__)
|
||||||
sub = parser.add_subparsers(dest='operation', required=True, parser_class=Parser)
|
sub = parser.add_subparsers(dest='operation', required=True, parser_class=Parser)
|
||||||
for operation in ('bootstrap', 'membership', 'verify'):
|
for operation in ('bootstrap', 'membership', 'verify', 'groups'):
|
||||||
command = sub.add_parser(operation)
|
command = sub.add_parser(operation)
|
||||||
command.add_argument('--root', required=True, help='Explicit private custody directory outside Git')
|
command.add_argument('--root', required=True, help='Explicit private custody directory outside Git')
|
||||||
command.add_argument('--lldap-url', default='https://lldap.coulomb.social')
|
command.add_argument('--lldap-url', default='https://lldap.coulomb.social')
|
||||||
if operation != 'verify':
|
if operation == 'groups':
|
||||||
|
command.add_argument('--user', required=True)
|
||||||
|
elif operation != 'verify':
|
||||||
command.add_argument('--apply', action='store_true')
|
command.add_argument('--apply', action='store_true')
|
||||||
command.add_argument('--expected-digest')
|
command.add_argument('--expected-digest')
|
||||||
if operation == 'membership':
|
if operation == 'membership':
|
||||||
|
|
|
||||||
|
|
@ -186,6 +186,34 @@ def test_list_is_nonmutating_and_verification_does_not_claim_acceptance():
|
||||||
assert provider.writes == []
|
assert provider.writes == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_groups_preview_lists_display_names_for_one_uid_without_writes():
|
||||||
|
provider = Directory()
|
||||||
|
result = admin.maintain(provider, 'https://lldap.coulomb.social', 'groups', user='tegwick')
|
||||||
|
assert result == {'result': 'groups_observed', 'uid': 'tegwick',
|
||||||
|
'groups': ['activity-core-operators', 'lldap_admin'], 'native_acceptance': 'unverified'}
|
||||||
|
assert provider.writes == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('kwargs', [{'user': 'nobody'}, {'user': 'Bad uid'}, {'user': None},
|
||||||
|
{'user': 'tegwick', 'group': 'lldap_admin'},
|
||||||
|
{'user': 'tegwick', 'apply': True, 'expected_digest': '0' * 64}])
|
||||||
|
def test_groups_preview_refuses_unknown_or_malformed_requests(kwargs):
|
||||||
|
provider = Directory()
|
||||||
|
with pytest.raises(admin.Refused):
|
||||||
|
admin.maintain(provider, 'https://lldap.coulomb.social', 'groups', **kwargs)
|
||||||
|
assert provider.writes == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_groups_cli_has_no_apply_and_prints_only_diagnosis_fields():
|
||||||
|
out = io.StringIO()
|
||||||
|
with patch.object(admin, 'private_password', return_value='fixture'), patch.object(admin, 'LLDAPProvisioner', return_value=Directory()), contextlib.redirect_stdout(out):
|
||||||
|
assert admin.main(['groups', '--root', '/fixture', '--user', 'agent-codex']) == 0
|
||||||
|
assert json.loads(out.getvalue()) == {'groups': ['tenant:one:users'], 'native_acceptance': 'unverified',
|
||||||
|
'result': 'groups_observed', 'uid': 'agent-codex'}
|
||||||
|
with contextlib.redirect_stderr(io.StringIO()):
|
||||||
|
assert admin.main(['groups', '--root', '/fixture', '--user', 'agent-codex', '--apply']) == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('payload', [{}, {'users': [], 'groups': None}, {'users': [{'id': 'same'}, {'id': 'same'}], 'groups': []},
|
@pytest.mark.parametrize('payload', [{}, {'users': [], 'groups': None}, {'users': [{'id': 'same'}, {'id': 'same'}], 'groups': []},
|
||||||
{'users': [], 'groups': [{'id': True, 'displayName': 'a', 'users': []}]},
|
{'users': [], 'groups': [{'id': True, 'displayName': 'a', 'users': []}]},
|
||||||
{'users': [], 'groups': [{'id': 1, 'displayName': 'a', 'users': [{'id': 'unknown'}]}]},
|
{'users': [], 'groups': [{'id': 1, 'displayName': 'a', 'users': [{'id': 'unknown'}]}]},
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,7 @@ This repo only points at that requirement from `docs/sign-in-diagnosis.md`.
|
||||||
|
|
||||||
```task
|
```task
|
||||||
id: NK-WP-0051-T04
|
id: NK-WP-0051-T04
|
||||||
status: todo
|
status: done
|
||||||
depends_on: [NK-WP-0051-T01]
|
depends_on: [NK-WP-0051-T01]
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "65f85a11-8512-5700-b258-feb2ead84372"
|
state_hub_task_id: "65f85a11-8512-5700-b258-feb2ead84372"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue