Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3
160 lines
7.2 KiB
Python
160 lines
7.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Attended, read-only reset-role classification; no credential discovery or grant."""
|
|
|
|
import argparse
|
|
import getpass
|
|
import hashlib
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
import re
|
|
import stat
|
|
import sys
|
|
import urllib.request
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
SPEC = importlib.util.spec_from_file_location('reset_observer_transport', HERE / 'lldap_admin.py')
|
|
transport = importlib.util.module_from_spec(SPEC)
|
|
SPEC.loader.exec_module(transport)
|
|
QUERY = 'query ResetRoles($id:String!){user(userId:$id){id groups{displayName}}}'
|
|
ROLES = ('lldap_admin', 'lldap_password_manager', 'lldap_strict_readonly')
|
|
# Coulomb's retained personal-reset acceptance, 2026-10-06. No caller-selected live subject.
|
|
ACCEPTED_SUBJECT = 'tegwick'
|
|
ROOT = HERE.parents[2]
|
|
SOURCES = (Path(__file__), HERE / 'lldap_admin.py',
|
|
ROOT / 'identity-provisioner/provisioner.py',
|
|
ROOT / 'identity-provisioner/group_provenance.py')
|
|
|
|
|
|
class Refused(Exception):
|
|
pass
|
|
|
|
|
|
class Parser(argparse.ArgumentParser):
|
|
def error(self, message):
|
|
raise Refused('invalid_arguments')
|
|
|
|
|
|
def identifier(value):
|
|
if not isinstance(value, str) or not re.fullmatch(r'[a-z][a-z0-9._-]{0,127}', value):
|
|
raise Refused('invalid_subject_identifier')
|
|
return value
|
|
|
|
|
|
class Observer:
|
|
def __init__(self, endpoint, opener=None):
|
|
self.base = transport.endpoint(endpoint)
|
|
self.opener = opener or transport.Transport(self.base)
|
|
|
|
def post(self, path, payload, token=None):
|
|
headers = {'Content-Type': 'application/json'}
|
|
if token:
|
|
headers['Authorization'] = 'Bearer ' + token
|
|
request = urllib.request.Request(self.base + path, data=json.dumps(payload).encode(),
|
|
headers=headers, method='POST')
|
|
return json.load(self.opener(request, timeout=15))
|
|
|
|
def classify(self, token, subject):
|
|
body = self.post('/api/graphql', {'query': QUERY, 'variables': {'id': subject}}, token)
|
|
if not isinstance(body, dict) or body.get('errors') or set(body) != {'data'}:
|
|
raise Refused('classification_unavailable')
|
|
data = body['data']
|
|
if not isinstance(data, dict) or set(data) != {'user'}:
|
|
raise Refused('invalid_classification_response')
|
|
user = data['user']
|
|
if not isinstance(user, dict) or set(user) != {'id', 'groups'} or user['id'] != subject:
|
|
raise Refused('subject_not_exactly_observed')
|
|
groups = user['groups']
|
|
if not isinstance(groups, list) or len(groups) > 1000:
|
|
raise Refused('invalid_group_metadata')
|
|
names = []
|
|
for group in groups:
|
|
if not isinstance(group, dict) or set(group) != {'displayName'}:
|
|
raise Refused('invalid_group_metadata')
|
|
name = group['displayName']
|
|
if (not isinstance(name, str) or not 1 <= len(name) <= 256
|
|
or any(ord(c) < 32 for c in name) or name in names):
|
|
raise Refused('invalid_group_metadata')
|
|
names.append(name)
|
|
builtin = {role: role in names for role in ROLES}
|
|
return {'builtin_roles': builtin, 'other_group_count': len(set(names) - set(ROLES)),
|
|
'matches_reviewed_service_name_filter':
|
|
subject.startswith('svc-') or subject == 'ld-admin',
|
|
'application_authority_assessed': False}
|
|
|
|
def observe(self, subject, password):
|
|
subject = identifier(subject)
|
|
if not isinstance(password, str) or not password or len(password.encode()) > 4096:
|
|
raise Refused('invalid_private_input')
|
|
body = self.post('/auth/simple/login', {'username': 'admin', 'password': password})
|
|
token = body.get('token') if isinstance(body, dict) else None
|
|
if (not isinstance(token, str) or not token or len(token) > 16384
|
|
or any(not 33 <= ord(c) <= 126 for c in token)):
|
|
raise Refused('admin_authentication_unavailable')
|
|
admin = self.classify(token, 'admin')
|
|
if not admin['builtin_roles']['lldap_admin']:
|
|
raise Refused('human_admin_role_not_observed')
|
|
bind = self.classify(token, 'svc-authelia')
|
|
selected = self.classify(token, subject)
|
|
return {'schema': 'netkingdom.reset-role-observation.v1',
|
|
'status': 'current_reset_roles_observed',
|
|
'observed_at': datetime.now(UTC).isoformat(),
|
|
'endpoint_sha256': hashlib.sha256(self.base.encode()).hexdigest(),
|
|
'selected_subject_sha256': hashlib.sha256(
|
|
('reset-role-subject-v1:' + subject).encode()).hexdigest(),
|
|
'human_admin_role_verified': True, 'bind': bind, 'selected_subject': selected,
|
|
'bind_has_multiple_builtin_roles': sum(bind['builtin_roles'].values()) > 1,
|
|
'membership_changed': False, 'password_reset_attempted': False,
|
|
'grant_approved': False, 'application_access_verified': False,
|
|
'credential_values_recorded': False,
|
|
'sources': [{'path': str(p.relative_to(ROOT)),
|
|
'sha256': hashlib.sha256(p.read_bytes()).hexdigest()}
|
|
for p in SOURCES]}
|
|
|
|
|
|
def receipt_path(value):
|
|
path = Path(os.path.abspath(Path(value).expanduser()))
|
|
if any(p.is_symlink() for p in (path, *path.parents)):
|
|
raise Refused('receipt_symlink_refused')
|
|
if any((p / '.git').exists() for p in path.parents):
|
|
raise Refused('receipt_must_be_outside_repository')
|
|
parent = path.parent.stat()
|
|
if not stat.S_ISDIR(parent.st_mode) or parent.st_uid != os.getuid() or parent.st_mode & 0o077:
|
|
raise Refused('private_owned_receipt_directory_required')
|
|
if path.exists():
|
|
raise Refused('fresh_receipt_required')
|
|
return path
|
|
|
|
|
|
def main(argv=None):
|
|
parser = Parser(description=__doc__)
|
|
parser.add_argument('--endpoint', default='https://lldap.coulomb.social')
|
|
parser.add_argument('--receipt', required=True)
|
|
try:
|
|
args = parser.parse_args(argv)
|
|
if not sys.stdin.isatty() or not sys.stderr.isatty():
|
|
raise Refused('human_terminal_required')
|
|
path = receipt_path(args.receipt)
|
|
observer = Observer(args.endpoint)
|
|
password = getpass.getpass('Password of the human admin account (hidden): ')
|
|
result = observer.observe(ACCEPTED_SUBJECT, password)
|
|
raw = (json.dumps(result, indent=2) + '\n').encode()
|
|
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600)
|
|
with os.fdopen(fd, 'wb') as stream:
|
|
stream.write(raw)
|
|
stream.flush()
|
|
os.fsync(stream.fileno())
|
|
print(raw.decode(), end='')
|
|
return 0
|
|
except Exception as error: # noqa: BLE001 -- every private-input failure must redact its body.
|
|
# Never include transport bodies, usernames, credentials or arbitrary exception text.
|
|
print(json.dumps({'status': 'observation_refused',
|
|
'reason': str(error) if isinstance(error, Refused)
|
|
else 'observation_unavailable'}), file=sys.stderr)
|
|
return 1
|
|
|
|
|
|
if __name__ == '__main__':
|
|
raise SystemExit(main())
|