Some checks failed
Use-case regression gate / usecase-regressions (push) Failing after 6m5s
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a18e2d-f513-7742-b8e7-12af0b744ff3
193 lines
11 KiB
Python
193 lines
11 KiB
Python
"""Synthetic native peers only; stdin is private, stdout is bounded metadata."""
|
|
|
|
import json
|
|
import sys
|
|
import time
|
|
import urllib.error
|
|
import urllib.parse
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
|
|
def run(v):
|
|
assert {p.name for p in Path('/sys/class/net').iterdir()} == {'lo'}
|
|
assert len(Path('/proc/net/route').read_text().splitlines()) == 1
|
|
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
|
cookie = ''
|
|
target = v.get('target', 'alice')
|
|
|
|
def call(port, path, data=None, *, token=None, auth=False):
|
|
nonlocal cookie
|
|
assert port in {17170, 9091} and path.startswith('/')
|
|
headers = {'Content-Type': 'application/json'}
|
|
if token:
|
|
headers['Authorization'] = 'Bearer ' + token
|
|
if auth:
|
|
headers.update({'X-Forwarded-Proto': 'https',
|
|
'X-Forwarded-Host': 'auth.recovery.test',
|
|
'X-Forwarded-For': '127.0.0.1', 'Cookie': cookie})
|
|
request = urllib.request.Request(
|
|
f'http://127.0.0.1:{port}' + path,
|
|
data=json.dumps(data).encode() if data is not None else None, headers=headers)
|
|
try:
|
|
response = opener.open(request, timeout=10)
|
|
except urllib.error.HTTPError as error:
|
|
response = error
|
|
with response:
|
|
if auth and response.headers.get('Set-Cookie'):
|
|
cookie = response.headers['Set-Cookie'].split(';', 1)[0]
|
|
raw = response.read()
|
|
return response.status, ({} if path == '/health' or response.status >= 400
|
|
else json.loads(raw or b'{}'))
|
|
|
|
def ready(port, path):
|
|
for _ in range(120):
|
|
try:
|
|
if call(port, path, auth=port == 9091)[0] == 200:
|
|
return
|
|
except (OSError, ValueError):
|
|
pass
|
|
time.sleep(.25)
|
|
raise RuntimeError('peer_startup_' + str(port))
|
|
|
|
def login(user, password):
|
|
code, body = call(17170, '/auth/simple/login', {'username': user, 'password': password})
|
|
return body.get('token') if code == 200 else None
|
|
|
|
def graphql(token, query, variables=None):
|
|
code, body = call(17170, '/api/graphql', {'query': query, 'variables': variables or {}},
|
|
token=token)
|
|
assert code == 200 and not body.get('errors'), 'directory_mutation_refused'
|
|
return body['data']
|
|
|
|
ready(17170, '/health')
|
|
admin = login('admin', v['admin_password'])
|
|
assert admin, 'synthetic_admin_login'
|
|
if v['mode'] == 'bootstrap':
|
|
for user in (['alice', 'svc-auth', 'bob', 'operator', 'appoperator', 'svc-other',
|
|
'victim'] if v.get('authority_matrix') else ['alice', 'svc-auth']):
|
|
graphql(admin, 'mutation($u:CreateUserInput!){createUser(user:$u){id}}', {
|
|
'u': {'id': user, 'email': user + '@example.test', 'displayName': user}})
|
|
if v.get('authority_matrix'):
|
|
groups = graphql(admin, '{groups{id displayName}}')['groups']
|
|
admin_id = next(g['id'] for g in groups if g['displayName'] == 'lldap_admin')
|
|
for user, group in [('operator', admin_id), ('svc-other', next(
|
|
g['id'] for g in groups if g['displayName'] == 'lldap_strict_readonly'))]:
|
|
graphql(admin, 'mutation($u:String!,$g:Int!){addUserToGroup('
|
|
'userId:$u,groupId:$g){ok}}', {'u': user, 'g': group})
|
|
app_id = graphql(admin, 'mutation{createGroup(name:"application-admins"){id}}')
|
|
graphql(admin, 'mutation($u:String!,$g:Int!){addUserToGroup('
|
|
'userId:$u,groupId:$g){ok}}',
|
|
{'u': 'appoperator', 'g': app_id['createGroup']['id']})
|
|
graphql(admin, 'mutation{createGroup(name:"victim-group"){id}}')
|
|
return {'status': 'prepared', 'admin_token': admin}
|
|
if v['mode'] == 'bind_role':
|
|
groups = graphql(admin, '{groups{id displayName}}')['groups']
|
|
memberships = {g['id'] for g in graphql(admin,
|
|
'query($u:String!){user(userId:$u){groups{id}}}',
|
|
{'u': 'svc-auth'})['user']['groups']}
|
|
for group in ['lldap_strict_readonly', 'lldap_password_manager']:
|
|
group_id = next(g['id'] for g in groups if g['displayName'] == group)
|
|
if (group == v['role']) == (group_id in memberships):
|
|
continue
|
|
operation = 'addUserToGroup' if group == v['role'] else 'removeUserFromGroup'
|
|
graphql(admin, 'mutation($u:String!,$g:Int!){' + operation
|
|
+ '(userId:$u,groupId:$g){ok}}', {'u': 'svc-auth', 'g': group_id})
|
|
service = login('svc-auth', v['bind_password'])
|
|
assert service, 'service_directory_login'
|
|
code, body = call(17170, '/api/graphql', {
|
|
'query': 'mutation{createGroup(name:"unexpected-admin-rights"){id}}'}, token=service)
|
|
assert code >= 400 or body.get('errors'), 'directory_group_creation_refused'
|
|
return {'status': 'role_set', 'directory_group_creation_refused': True}
|
|
if v['mode'] == 'authority_context':
|
|
service = login('svc-auth', v['bind_password'])
|
|
assert service, 'service_directory_login'
|
|
return {'status': 'authority_context', 'service_token': service,
|
|
'admin_token': admin}
|
|
if v['mode'] == 'directory_authentication':
|
|
return {'old_password_accepted': bool(login(v['target'], v['old_password'])),
|
|
'new_password_accepted': bool(login(v['target'], v['new_password']))}
|
|
if v['mode'] == 'write_boundaries':
|
|
service = login('svc-auth', v['bind_password'])
|
|
assert service, 'service_directory_login'
|
|
# Positive controls distinguish an authorization refusal from invalid queries.
|
|
create_user = 'mutation($u:CreateUserInput!){createUser(user:$u){id}}'
|
|
update_user = 'mutation($u:UpdateUserInput!){updateUser(user:$u){ok}}'
|
|
delete_user = 'mutation($u:String!){deleteUser(userId:$u){ok}}'
|
|
create_group = 'mutation($n:String!){createGroup(name:$n){id}}'
|
|
delete_group = 'mutation($g:Int!){deleteGroup(groupId:$g){ok}}'
|
|
add = 'mutation($u:String!,$g:Int!){addUserToGroup(userId:$u,groupId:$g){ok}}'
|
|
remove = 'mutation($u:String!,$g:Int!){removeUserFromGroup(userId:$u,groupId:$g){ok}}'
|
|
graphql(admin, create_user, {'u': {'id': 'control', 'email': 'control@example.test'}})
|
|
graphql(admin, update_user, {'u': {'id': 'control', 'email': 'changed@example.test'}})
|
|
gid = graphql(admin, create_group, {'n': 'control-group'})['createGroup']['id']
|
|
graphql(admin, add, {'u': 'control', 'g': gid})
|
|
graphql(admin, remove, {'u': 'control', 'g': gid})
|
|
graphql(admin, delete_group, {'g': gid})
|
|
graphql(admin, delete_user, {'u': 'control'})
|
|
snapshot_query = '{users{id email displayName groups{id}} groups{id displayName}}'
|
|
before = graphql(admin, snapshot_query)
|
|
groups = before['groups']
|
|
admin_id = next(g['id'] for g in groups if g['displayName'] == 'lldap_admin')
|
|
victim_id = next(g['id'] for g in groups if g['displayName'] == 'victim-group')
|
|
mutations = [
|
|
('create_user', create_user, {'u': {'id': 'unexpected', 'email': 'x@example.test'}}),
|
|
('edit_other_user', update_user, {'u': {'id': 'victim', 'email': 'x@example.test'}}),
|
|
('delete_user', delete_user, {'u': 'victim'}),
|
|
('create_group', create_group, {'n': 'unexpected'}),
|
|
('delete_group', delete_group, {'g': victim_id}),
|
|
('self_promote_admin', add, {'u': 'svc-auth', 'g': admin_id}),
|
|
('remove_admin_membership', remove, {'u': 'operator', 'g': admin_id})]
|
|
denials = []
|
|
for label, query, variables in mutations:
|
|
code, body = call(17170, '/api/graphql', {'query': query, 'variables': variables},
|
|
token=service)
|
|
assert code >= 400 or body.get('errors'), 'unexpected_directory_write_' + label
|
|
denials.append(label)
|
|
assert graphql(admin, snapshot_query) == before, 'directory_metadata_changed'
|
|
return {'positive_controls': len(mutations), 'refused_mutations': denials,
|
|
'directory_metadata_unchanged': True}
|
|
ready(9091, '/api/health')
|
|
if v['mode'] == 'reset_finish':
|
|
code, body = call(9091, '/api/reset-password', {'password': v['new_password']}, auth=True)
|
|
assert code != 200 or body.get('status') == 'KO', 'reset_without_challenge_refused'
|
|
code, body = call(9091, '/api/reset-password/identity/finish',
|
|
{'token': v['reset_token']}, auth=True)
|
|
assert code == 200 and body.get('status') == 'OK', 'identity_finish'
|
|
replay, body = call(9091, '/api/reset-password/identity/finish',
|
|
{'token': v['reset_token']}, auth=True)
|
|
assert replay != 200 or body.get('status') != 'OK', 'identity_replay_refused'
|
|
reset, body = call(9091, '/api/reset-password', {'password': v['new_password']}, auth=True)
|
|
return {'status': ('reset_completed' if reset == 200 and body.get('status') in {None, 'OK'}
|
|
else 'reset_refused'),
|
|
'reset_http_status': reset, 'reset_response_status': body.get('status'),
|
|
'native_identity_finish': True,
|
|
'reset_without_verified_challenge_refused': True,
|
|
'consumed_identity_token_replay_refused': True,
|
|
'old_password_accepted_after': bool(login(target, v['old_password'])),
|
|
'new_password_accepted_after': bool(login(target, v['new_password']))}
|
|
assert login(target, v['old_password']), 'initial_password'
|
|
assert not login(target, v['new_password']), 'replacement_not_initially_accepted'
|
|
code, body = call(9091, '/api/firstfactor', {
|
|
'username': 'svc-auth', 'password': v['bind_password'], 'keepMeLoggedIn': False}, auth=True)
|
|
assert code != 200 or body.get('status') != 'OK', 'interactive_service_login_refused'
|
|
code, body = call(9091, '/api/firstfactor', {
|
|
'username': target, 'password': v['old_password'], 'keepMeLoggedIn': False}, auth=True)
|
|
assert code == 200 and body.get('status') == 'OK', 'native_first_factor'
|
|
cookie = ''
|
|
code, _ = call(9091, '/api/reset-password/identity/start', {'username': target}, auth=True)
|
|
assert code == 200, 'reset_start'
|
|
return {'status': 'reset_requested', 'only_loopback_interface': True,
|
|
'external_routes_absent': True, 'old_password_initially_accepted': True,
|
|
'new_password_initially_refused': True, 'native_first_factor': True,
|
|
'interactive_service_login_refused': True}
|
|
|
|
|
|
if __name__ == '__main__':
|
|
try:
|
|
print(json.dumps(run(json.load(sys.stdin))))
|
|
except Exception as exc:
|
|
print(json.dumps({'status': 'probe_failed', 'failure_type': type(exc).__name__,
|
|
'stage': (str(exc) if isinstance(exc, (AssertionError, RuntimeError))
|
|
else 'native_probe')}))
|
|
raise SystemExit(1)
|