Prepare restricted seven-source reuse reader and bounded recovery
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a137ca-5c77-7a71-b9ae-23ad9b0b5684
This commit is contained in:
parent
748fe6c7db
commit
a570bc2f5b
12 changed files with 1367 additions and 0 deletions
383
scripts/provision-reuse-surface-forgejo-source.py
Executable file
383
scripts/provision-reuse-surface-forgejo-source.py
Executable file
|
|
@ -0,0 +1,383 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Silent attended CCR-2026-0046 bootstrap; never runs without exact approval.
|
||||
|
||||
Only this seven-repository identity/PAT/KV path is mutable. No policy, ESO,
|
||||
consumer rollout, or registration mutations occur here. A receipt is evidence,
|
||||
not authorization. Uncertain requests are recorded and reconciled, never retried
|
||||
in the same execution; orphan PAT drift requires attended recovery.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import argparse
|
||||
import base64
|
||||
import datetime as dt
|
||||
import fcntl
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import secrets
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import yaml
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
CCR = 'CCR-2026-0046'
|
||||
CCR_FILE = 'credential-change-requests/CCR-2026-0046-reuse-surface-forgejo-source-read.yaml'
|
||||
PLAN_FILE = 'docs/proposals/2026-10-15-reuse-private-reader/bootstrap-contract.json'
|
||||
SCRIPT_FILE = 'scripts/provision-reuse-surface-forgejo-source.py'
|
||||
API = 'https://forgejo.coulomb.social/api/v1'
|
||||
USER = 'reuse-surface-source'
|
||||
TEAM = 'reuse-surface-source-readers'
|
||||
ORG = 'coulomb'
|
||||
KV = 'platform/workloads/reuse/reuse-surface/forgejo-source-read'
|
||||
FIELD = 'FORGEJO_SOURCE_TOKEN'
|
||||
PREFIX = 'reuse-surface-source-read-'
|
||||
REPOS = frozenset('rapp-openbao rapp-policy-nexus rapp-postgres rapp-sbom-nexus rapp-telemetry rapp-tenant-engine rapp-user-engine'.split())
|
||||
SCOPES = ['read:repository']
|
||||
UNITS = {'repo.code': 'read', **{x: 'none' for x in ('repo.actions', 'repo.packages', 'repo.issues', 'repo.ext_issues', 'repo.wiki', 'repo.pulls', 'repo.releases', 'repo.projects', 'repo.ext_wiki')}}
|
||||
|
||||
class Refused(Exception):
|
||||
"""Messages must be fixed non-secret codes, never provider text."""
|
||||
|
||||
class NoRedirect(urllib.request.HTTPRedirectHandler):
|
||||
def redirect_request(self, *args, **kwargs):
|
||||
raise Refused('redirect_refused')
|
||||
|
||||
class Journal:
|
||||
def __init__(self, path):
|
||||
flags = os.O_RDWR | os.O_CREAT | os.O_APPEND | os.O_NOFOLLOW
|
||||
self.fd = os.open(path, flags, 0o600)
|
||||
if os.fstat(self.fd).st_mode & 0o077:
|
||||
os.close(self.fd)
|
||||
raise Refused('receipt_permissions')
|
||||
fcntl.flock(self.fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
self.attempt = secrets.token_hex(12)
|
||||
def put(self, event, **fields):
|
||||
# Only locally authored operation tags, scalar status/IDs are accepted.
|
||||
allowed = {'operation', 'status', 'http_status', 'token_id', 'team_id', 'outcome', 'error_code'}
|
||||
if set(fields) - allowed:
|
||||
raise Refused('receipt_fields')
|
||||
record = {'at': dt.datetime.now(dt.timezone.utc).isoformat(), 'ccr': CCR,
|
||||
'attempt': self.attempt, 'event': event, **fields}
|
||||
data = (json.dumps(record, sort_keys=True) + '\n').encode()
|
||||
while data:
|
||||
written = os.write(self.fd, data)
|
||||
if written <= 0:
|
||||
raise Refused('receipt_write_failed')
|
||||
data = data[written:]
|
||||
os.fsync(self.fd)
|
||||
def close(self):
|
||||
os.close(self.fd)
|
||||
|
||||
class Client:
|
||||
def __init__(self, admin, journal):
|
||||
self.admin, self.journal = admin, journal
|
||||
self.opener = urllib.request.build_opener(NoRedirect())
|
||||
def request(self, method, path, payload=None, expected=(200,), *, token=None, sudo=False, basic=None, operation=None):
|
||||
if not path.startswith('/') or '://' in path:
|
||||
raise Refused('invalid_api_path')
|
||||
headers = {'Authorization': basic or 'token ' + (self.admin if token is None else token), 'Accept': 'application/json'}
|
||||
if sudo:
|
||||
headers['Sudo'] = USER
|
||||
if payload is not None:
|
||||
headers['Content-Type'] = 'application/json'
|
||||
if method != 'GET':
|
||||
if not operation:
|
||||
raise Refused('mutation_without_intent')
|
||||
self.journal.put('intent', operation=operation)
|
||||
req = urllib.request.Request(API + path, data=None if payload is None else json.dumps(payload).encode(), headers=headers, method=method)
|
||||
self.last_total_count = None
|
||||
try:
|
||||
with self.opener.open(req, timeout=30) as response:
|
||||
status, body = response.status, response.read(2_000_001)
|
||||
raw_total = response.headers.get('X-Total-Count')
|
||||
if raw_total is not None:
|
||||
if not re.fullmatch(r'[0-9]+', raw_total):
|
||||
raise Refused('invalid_total_count')
|
||||
self.last_total_count = int(raw_total)
|
||||
except urllib.error.HTTPError as error:
|
||||
status, body = error.code, b''
|
||||
except Exception:
|
||||
if operation:
|
||||
self.journal.put('outcome', operation=operation, status='ambiguous')
|
||||
raise Refused('request_observation_failed') from None
|
||||
if operation:
|
||||
self.journal.put('outcome', operation=operation, status='observed', http_status=status)
|
||||
if status not in expected:
|
||||
raise Refused('unexpected_http_status')
|
||||
if len(body) > 2_000_000:
|
||||
raise Refused('response_bound_exceeded')
|
||||
if not body:
|
||||
return status, None
|
||||
try:
|
||||
return status, json.loads(body)
|
||||
except Exception:
|
||||
raise Refused('invalid_json') from None
|
||||
def pages(self, path, *, sudo=False):
|
||||
"""Page until observed empty; requested size may be server-clamped.
|
||||
|
||||
Forgejo11.0.3 default MaxResponseItems=50, so a response shorter than
|
||||
our requested100 proves nothing. Detect unstable/duplicate IDs and
|
||||
reconcile X-Total-Count whenever available; exceeding bound refuses.
|
||||
"""
|
||||
items, seen, expected_total = [], set(), None
|
||||
for page in range(1, 21):
|
||||
separator = '&' if '?' in path else '?'
|
||||
_, batch = self.request('GET', path + separator + f'limit=100&page={page}', sudo=sudo)
|
||||
if not isinstance(batch, list) or any(not isinstance(x, dict) or type(x.get('id')) is not int or x['id'] <= 0 for x in batch):
|
||||
raise Refused('invalid_list')
|
||||
observed_total = getattr(self, 'last_total_count', None)
|
||||
if observed_total is not None:
|
||||
if expected_total is not None and observed_total != expected_total:
|
||||
raise Refused('unstable_total_count')
|
||||
expected_total = observed_total
|
||||
if not batch:
|
||||
if expected_total is not None and len(items) != expected_total:
|
||||
raise Refused('total_count_mismatch')
|
||||
return items
|
||||
for item in batch:
|
||||
if item['id'] in seen:
|
||||
raise Refused('duplicate_page_id')
|
||||
seen.add(item['id'])
|
||||
items.extend(batch)
|
||||
if expected_total is not None and len(items) > expected_total:
|
||||
raise Refused('total_count_mismatch')
|
||||
raise Refused('pagination_bound_exceeded')
|
||||
|
||||
class Bao:
|
||||
@staticmethod
|
||||
def run(args, stdin=None):
|
||||
try:
|
||||
return subprocess.run(['bao', *args], input=stdin, capture_output=True, timeout=60, check=False, cwd=ROOT)
|
||||
except Exception:
|
||||
raise Refused('bao_observation_failed') from None
|
||||
def get(self):
|
||||
result = self.run(['kv', 'get', '-format=json', KV])
|
||||
if result.returncode != 0:
|
||||
# Distinguish an actual not-found response from denied/unavailable.
|
||||
# Never include captured raw errors in any evidence.
|
||||
if re.search(rb'Code:\s*404\b', result.stderr or b'') or b'No value found at ' in (result.stderr or b''):
|
||||
return None
|
||||
raise Refused('bao_preflight_failed')
|
||||
try:
|
||||
data = json.loads(result.stdout)['data']['data']
|
||||
except Exception:
|
||||
raise Refused('bao_invalid_record') from None
|
||||
if set(data) != {FIELD, 'API_USER', 'API_BASE_URL', 'TOKEN_SCOPES', 'GENERATED_AT', 'CCR_ID'} or data['API_USER'] != USER or data['API_BASE_URL'] != 'https://forgejo.coulomb.social' or data['TOKEN_SCOPES'] != 'read:repository' or data['CCR_ID'] != CCR or not isinstance(data[FIELD], str) or not data[FIELD]:
|
||||
raise Refused('bao_record_conflict')
|
||||
return data
|
||||
def put(self, token, journal):
|
||||
journal.put('intent', operation='kv_create')
|
||||
timestamp = dt.datetime.now(dt.timezone.utc).isoformat()
|
||||
try:
|
||||
result = self.run(['kv', 'put', '-cas=0', KV, FIELD + '=-', 'API_USER=' + USER,
|
||||
'API_BASE_URL=https://forgejo.coulomb.social', 'TOKEN_SCOPES=read:repository',
|
||||
'GENERATED_AT=' + timestamp, 'CCR_ID=' + CCR], token.encode())
|
||||
except Refused:
|
||||
journal.put('outcome', operation='kv_create', status='ambiguous')
|
||||
raise
|
||||
journal.put('outcome', operation='kv_create', status='observed' if result.returncode == 0 else 'ambiguous')
|
||||
# Readback reconciles even a response failure after successful write.
|
||||
stored = self.get()
|
||||
if stored is None or not secrets.compare_digest(stored[FIELD], token):
|
||||
raise Refused('bao_write_requires_reconciliation')
|
||||
journal.put('verified', operation='kv_create', status='matched')
|
||||
|
||||
|
||||
def valid_user(user):
|
||||
return isinstance(user, dict) and user.get('login') == USER and user.get('restricted') is True and user.get('is_admin') is False and user.get('visibility') == 'private'
|
||||
|
||||
def valid_team(team):
|
||||
return isinstance(team, dict) and isinstance(team.get('id'), int) and team.get('name') == TEAM and team.get('permission') == 'read' and team.get('includes_all_repositories') is False and team.get('can_create_org_repo') is False and team.get('units_map') == UNITS
|
||||
|
||||
def repo_names(items):
|
||||
names = set()
|
||||
for repo in items:
|
||||
if repo.get('owner', {}).get('login') != ORG or repo.get('name') not in REPOS:
|
||||
raise Refused('unexpected_team_repository')
|
||||
names.add(repo['name'])
|
||||
if len(names) != len(items):
|
||||
raise Refused('duplicate_repository')
|
||||
return names
|
||||
|
||||
def inspect(client, bao):
|
||||
"""Read-only preflight before mutations.
|
||||
|
||||
Forgejo11.0.3 ListMyRepos uses OwnerID plus Collaborate=None; model
|
||||
ValueOrDefault(true) includes direct access and team grants (not only owned).
|
||||
Deployed-server API compatibility and live scope proof remain separate gates.
|
||||
"""
|
||||
for repo in sorted(REPOS):
|
||||
_, meta = client.request('GET', f'/repos/{ORG}/{repo}')
|
||||
if not isinstance(meta, dict) or meta.get('private') is not True or meta.get('full_name') != ORG + '/' + repo:
|
||||
raise Refused('source_repository_conflict')
|
||||
_, probe = client.request('GET', '/repos/coulomb/rapp-core-hub')
|
||||
if not isinstance(probe, dict) or probe.get('private') is not True:
|
||||
raise Refused('negative_probe_not_private')
|
||||
_, user = client.request('GET', '/users/' + USER, expected=(200, 404))
|
||||
if user is not None and not valid_user(user):
|
||||
raise Refused('user_conflict')
|
||||
teams = [x for x in client.pages(f'/orgs/{ORG}/teams') if x.get('name') == TEAM]
|
||||
if len(teams) > 1:
|
||||
raise Refused('duplicate_team')
|
||||
team = teams[0] if teams else None
|
||||
members, repos = [], set()
|
||||
if team is not None:
|
||||
if not valid_team(team):
|
||||
raise Refused('team_conflict')
|
||||
members = client.pages('/teams/' + str(team['id']) + '/members')
|
||||
if len(members) > 1 or any(x.get('login') != USER for x in members):
|
||||
raise Refused('team_members_conflict')
|
||||
repos = repo_names(client.pages('/teams/' + str(team['id']) + '/repos'))
|
||||
tokens = []
|
||||
if user is not None:
|
||||
orgs = client.pages('/user/orgs', sudo=True)
|
||||
if any(x.get('username') != ORG for x in orgs):
|
||||
raise Refused('extra_organization_affiliation')
|
||||
grants = client.pages('/user/teams', sudo=True)
|
||||
if any(x.get('name') != TEAM or x.get('organization', {}).get('username') != ORG for x in grants):
|
||||
raise Refused('extra_team_grant')
|
||||
for meta in client.pages('/user/repos?order_by=id', sudo=True):
|
||||
if meta.get('private') is True and meta.get('full_name') not in {ORG + '/' + x for x in REPOS}:
|
||||
raise Refused('extra_private_repository_grant')
|
||||
if meta.get('owner', {}).get('login') == USER:
|
||||
raise Refused('service_user_owned_repository')
|
||||
tokens = client.pages('/users/' + USER + '/tokens')
|
||||
if len(tokens) > 1 or any(not str(x.get('name', '')).startswith(PREFIX) or x.get('scopes') != SCOPES or not isinstance(x.get('id'), int) for x in tokens):
|
||||
raise Refused('token_conflict')
|
||||
stored = bao.get()
|
||||
if bool(tokens) != bool(stored):
|
||||
raise Refused('orphan_token_or_kv_requires_recovery')
|
||||
if tokens and (not isinstance(tokens[0].get('token_last_eight'), str) or len(tokens[0]['token_last_eight']) != 8 or not secrets.compare_digest(tokens[0]['token_last_eight'], stored[FIELD][-8:])):
|
||||
raise Refused('pat_kv_binding_unverifiable')
|
||||
if tokens and (not team or repos != REPOS or len(members) != 1):
|
||||
raise Refused('active_lane_grant_drift')
|
||||
return user, team, repos, members, tokens, stored
|
||||
|
||||
def verify(client, source_token, plan):
|
||||
for source in plan['sources']:
|
||||
_, repo = client.request('GET', '/repos/' + ORG + '/' + source['repo'], token=source_token)
|
||||
if repo.get('permissions') != {'admin': False, 'push': False, 'pull': True}:
|
||||
raise Refused('effective_permission_not_read')
|
||||
# Exact consented bytes fetched quietly; raw output is never retained.
|
||||
path = f"/repos/{ORG}/{source['repo']}/contents/registry/indexes/capabilities.yaml?ref={source['consented_commit']}"
|
||||
_, content = client.request('GET', path, token=source_token)
|
||||
try:
|
||||
raw = base64.b64decode(content['content'], validate=False)
|
||||
except Exception:
|
||||
raise Refused('invalid_index_encoding') from None
|
||||
if hashlib.sha256(raw).hexdigest() != source['index_sha256']:
|
||||
raise Refused('index_digest_mismatch')
|
||||
client.request('GET', f"/repos/{ORG}/{source['repo']}/actions/secrets?limit=1", token=source_token, expected=(403, 404))
|
||||
client.request('GET', '/admin/users', token=source_token, expected=(403, 404))
|
||||
# A known private excluded source is selected by reviewed contract.
|
||||
probe = plan['negative_private_probe']
|
||||
_, meta = client.request('GET', '/repos/' + ORG + '/' + probe)
|
||||
if not isinstance(meta, dict) or meta.get('private') is not True:
|
||||
raise Refused('negative_probe_not_private')
|
||||
client.request('GET', '/repos/' + ORG + '/' + probe, token=source_token, expected=(403, 404))
|
||||
client.journal.put('verified', operation='reader_scope', status='seven_exact_no_extra_private_grants')
|
||||
|
||||
def provision(client, bao, plan):
|
||||
user, team, repos, members, tokens, stored = inspect(client, bao)
|
||||
if tokens:
|
||||
verify(client, stored[FIELD], plan)
|
||||
client.journal.put('terminal', outcome='verified_noop')
|
||||
return 'noop'
|
||||
if user is None:
|
||||
_, user = client.request('POST', '/admin/users', {'username': USER, 'email': USER + '@coulomb.social', 'full_name': 'Reuse Surface seven-source reader', 'password': secrets.token_urlsafe(48), 'restricted': True, 'must_change_password': False, 'send_notify': False, 'visibility': 'private'}, expected=(201,), operation='user_create')
|
||||
if not valid_user(user):
|
||||
raise Refused('created_user_contract_failed')
|
||||
if team is None:
|
||||
_, team = client.request('POST', f'/orgs/{ORG}/teams', {'name': TEAM, 'description': CCR + ' exact seven consented source repos', 'permission': 'read', 'includes_all_repositories': False, 'can_create_org_repo': False, 'units': ['repo.code'], 'units_map': UNITS}, expected=(201,), operation='team_create')
|
||||
if not valid_team(team):
|
||||
raise Refused('created_team_contract_failed')
|
||||
team_id = team['id']
|
||||
for repo in sorted(REPOS - repos):
|
||||
client.request('PUT', f'/teams/{team_id}/repos/{ORG}/{repo}', expected=(204,), operation='team_repo_' + repo)
|
||||
if not members:
|
||||
client.request('PUT', f'/teams/{team_id}/members/{USER}', expected=(204,), operation='team_member_add')
|
||||
# Recheck complete grants before creating any PAT; refuse implicit grants.
|
||||
_, _, actual_repos, actual_members, _, _ = inspect(client, bao)
|
||||
if actual_repos != REPOS or len(actual_members) != 1:
|
||||
raise Refused('incomplete_team_scope')
|
||||
password = secrets.token_urlsafe(48)
|
||||
client.request('PATCH', '/admin/users/' + USER, {'password': password, 'restricted': True, 'admin': False, 'allow_create_organization': False, 'allow_git_hook': False, 'allow_import_local': False, 'max_repo_creation': 0, 'must_change_password': False, 'active': True, 'visibility': 'private'}, operation='service_password_randomize')
|
||||
basic = 'Basic ' + base64.b64encode((USER + ':' + password).encode()).decode()
|
||||
_, record = client.request('POST', '/users/' + USER + '/tokens', {'name': PREFIX + client.journal.attempt, 'scopes': SCOPES}, expected=(201,), basic=basic, operation='pat_create')
|
||||
if not isinstance(record, dict) or not isinstance(record.get('id'), int) or record.get('scopes') != SCOPES or not isinstance(record.get('sha1'), str) or not record['sha1']:
|
||||
raise Refused('created_pat_requires_recovery')
|
||||
client.journal.put('identified', operation='pat_create', token_id=record['id'], team_id=team_id)
|
||||
token = record['sha1']
|
||||
verify(client, token, plan)
|
||||
bao.put(token, client.journal)
|
||||
# Repeat complete grant inventory after custody; verify exactly one PAT.
|
||||
inspect(client, bao)
|
||||
client.journal.put('terminal', outcome='provisioned_verified_delivery_pending')
|
||||
return 'minted'
|
||||
|
||||
def approval(path):
|
||||
try:
|
||||
receipt = json.loads(Path(path).read_text())
|
||||
ccr = yaml.safe_load((ROOT / CCR_FILE).read_text())
|
||||
if receipt['ccr_id'] != CCR or receipt['decision'] != 'approved' or not receipt['supervisor'] or not receipt['decision_ref'] or ccr['status'] != 'approved':
|
||||
raise Refused('approval_missing')
|
||||
files = {CCR_FILE, PLAN_FILE, SCRIPT_FILE, 'openbao/policies/workload-kv-read-reuse-surface-runtime-eso.hcl'}
|
||||
if set(receipt['approved_artifacts']) != files:
|
||||
raise Refused('approval_artifact_set')
|
||||
for rel in files:
|
||||
if hashlib.sha256((ROOT / rel).read_bytes()).hexdigest() != receipt['approved_artifacts'][rel]:
|
||||
raise Refused('approval_revision_mismatch')
|
||||
plan = json.loads((ROOT / PLAN_FILE).read_text())
|
||||
if {x['repo'] for x in plan['sources']} != REPOS or len(plan['sources']) != 7 or plan['negative_private_probe'] != 'rapp-core-hub' or any(not re.fullmatch(r'[a-f0-9]{40}', x['consented_commit']) or not re.fullmatch(r'[a-f0-9]{64}', x['index_sha256']) for x in plan['sources']):
|
||||
raise Refused('plan_scope_conflict')
|
||||
return plan
|
||||
except Refused:
|
||||
raise
|
||||
except Exception:
|
||||
raise Refused('approval_invalid') from None
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--approval', required=True)
|
||||
parser.add_argument('--receipt', required=True)
|
||||
parser.add_argument('--inner', action='store_true', help=argparse.SUPPRESS)
|
||||
args = parser.parse_args(argv)
|
||||
journal = None
|
||||
lane_lock = None
|
||||
try:
|
||||
plan = approval(args.approval)
|
||||
if not args.inner:
|
||||
# Governed OpenBao attended lane surrounds this command. Warden
|
||||
# supplies only Forgejo API_TOKEN to inner child, not via argv/files.
|
||||
result = subprocess.run(['warden', 'access', 'forgejo-admin-api-token', '--field', 'API_TOKEN', '--exec', '--', sys.executable, str(ROOT / SCRIPT_FILE), '--inner', '--approval', str(Path(args.approval).resolve()), '--receipt', str(Path(args.receipt).resolve())], capture_output=True, timeout=900, check=False, cwd=ROOT)
|
||||
return 0 if result.returncode == 0 else 1
|
||||
lane_lock = os.open('/tmp/' + CCR + '-reuse-reader-' + str(os.getuid()) + '.lock', os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW, 0o600)
|
||||
if os.fstat(lane_lock).st_uid != os.getuid() or os.fstat(lane_lock).st_mode & 0o077:
|
||||
raise Refused('lane_lock_owner_or_permissions')
|
||||
fcntl.flock(lane_lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
journal = Journal(args.receipt)
|
||||
admin = os.environ.get('API_TOKEN')
|
||||
if not admin:
|
||||
raise Refused('admin_route_missing')
|
||||
provision(Client(admin, journal), Bao(), plan)
|
||||
return 0
|
||||
except Exception as error:
|
||||
if journal:
|
||||
journal.put('terminal', outcome='stopped_reconcile_before_retry', error_code=str(error) if isinstance(error, Refused) else 'internal_failure')
|
||||
return 1
|
||||
finally:
|
||||
if journal:
|
||||
journal.close()
|
||||
if lane_lock is not None:
|
||||
os.close(lane_lock)
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
result = main()
|
||||
except Exception:
|
||||
result = 1
|
||||
raise SystemExit(result)
|
||||
109
scripts/revoke-reuse-surface-forgejo-source.py
Executable file
109
scripts/revoke-reuse-surface-forgejo-source.py
Executable file
|
|
@ -0,0 +1,109 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Silent separately approved exact-PAT revocation; preserve KV and delivery.
|
||||
|
||||
Forgejo11.0.3 DELETE token requires Basic auth. Reset only the dedicated user
|
||||
password to process-memory random bytes, revoke the reviewed token and reconcile
|
||||
inventory absence. Never remint, rotate/write KV, or touch another identity.
|
||||
"""
|
||||
import argparse
|
||||
import base64
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SCRIPT = 'scripts/revoke-reuse-surface-forgejo-source.py'
|
||||
spec = importlib.util.spec_from_file_location('reuse_reader_bootstrap', ROOT / 'scripts/provision-reuse-surface-forgejo-source.py')
|
||||
p = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(p)
|
||||
|
||||
|
||||
def approved_target(path):
|
||||
try:
|
||||
record = json.loads(Path(path).read_text())
|
||||
expected_files = {SCRIPT, p.SCRIPT_FILE, p.CCR_FILE, p.PLAN_FILE}
|
||||
if record['ccr_id'] != p.CCR or record['decision'] != 'approved' or not record['supervisor'] or not record['decision_ref'] or record['action'] != 'revoke_exact_pat' or set(record['approved_artifacts']) != expected_files:
|
||||
raise p.Refused('recovery_approval_invalid')
|
||||
for rel in expected_files:
|
||||
if hashlib.sha256((ROOT / rel).read_bytes()).hexdigest() != record['approved_artifacts'][rel]:
|
||||
raise p.Refused('recovery_approved_revision_mismatch')
|
||||
target = record['target']
|
||||
if set(target) != {'id', 'name'} or type(target['id']) is not int or target['id'] <= 0 or not isinstance(target['name'], str) or not target['name'].startswith(p.PREFIX) or len(target['name']) > 100:
|
||||
raise p.Refused('recovery_target_invalid')
|
||||
return target
|
||||
except p.Refused:
|
||||
raise
|
||||
except Exception:
|
||||
raise p.Refused('recovery_approval_invalid') from None
|
||||
|
||||
|
||||
def recover(client, target):
|
||||
_, user = client.request('GET', '/users/' + p.USER)
|
||||
if not p.valid_user(user):
|
||||
raise p.Refused('recovery_user_conflict')
|
||||
tokens = client.pages('/users/' + p.USER + '/tokens')
|
||||
if not tokens:
|
||||
# Absence reconciles a prior ambiguous revocation without new writes.
|
||||
client.journal.put('terminal', outcome='revoke_verified_absent_noop')
|
||||
return
|
||||
if len(tokens) != 1 or tokens[0].get('id') != target['id'] or tokens[0].get('name') != target['name'] or tokens[0].get('scopes') != p.SCOPES:
|
||||
raise p.Refused('recovery_token_inventory_conflict')
|
||||
password = secrets.token_urlsafe(48)
|
||||
client.request('PATCH', '/admin/users/' + p.USER, {'password': password, 'must_change_password': False}, operation='recovery_password_randomize')
|
||||
basic = 'Basic ' + base64.b64encode((p.USER + ':' + password).encode()).decode()
|
||||
try:
|
||||
client.request('DELETE', '/users/' + p.USER + '/tokens/' + str(target['id']), expected=(204, 404), basic=basic, operation='exact_pat_revoke')
|
||||
except p.Refused:
|
||||
# Observation failure does not mean DELETE did not apply. Read-only
|
||||
# reconciliation follows; never issue another DELETE in this execution.
|
||||
pass
|
||||
remaining = client.pages('/users/' + p.USER + '/tokens')
|
||||
if remaining:
|
||||
raise p.Refused('revoke_unverified_stop')
|
||||
client.journal.put('verified', operation='exact_pat_revoke', token_id=target['id'], status='inventory_absent')
|
||||
client.journal.put('terminal', outcome='revoked_verified_kv_preserved')
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--approval', required=True)
|
||||
parser.add_argument('--receipt', required=True)
|
||||
parser.add_argument('--inner', action='store_true', help=argparse.SUPPRESS)
|
||||
args = parser.parse_args(argv)
|
||||
journal, lock = None, None
|
||||
try:
|
||||
target = approved_target(args.approval)
|
||||
if not args.inner:
|
||||
result = subprocess.run(['warden', 'access', 'forgejo-admin-api-token', '--field', 'API_TOKEN', '--exec', '--', sys.executable, str(ROOT / SCRIPT), '--inner', '--approval', str(Path(args.approval).resolve()), '--receipt', str(Path(args.receipt).resolve())], capture_output=True, check=False, timeout=900, cwd=ROOT)
|
||||
return 0 if result.returncode == 0 else 1
|
||||
lock = os.open('/tmp/' + p.CCR + '-reuse-reader-' + str(os.getuid()) + '.lock', os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW, 0o600)
|
||||
if os.fstat(lock).st_uid != os.getuid() or os.fstat(lock).st_mode & 0o077:
|
||||
raise p.Refused('lane_lock_owner_or_permissions')
|
||||
p.fcntl.flock(lock, p.fcntl.LOCK_EX | p.fcntl.LOCK_NB)
|
||||
journal = p.Journal(args.receipt)
|
||||
admin = os.environ.get('API_TOKEN')
|
||||
if not admin:
|
||||
raise p.Refused('admin_route_missing')
|
||||
recover(p.Client(admin, journal), target)
|
||||
return 0
|
||||
except Exception as error:
|
||||
if journal:
|
||||
journal.put('terminal', outcome='recovery_stopped_reconcile_before_retry', error_code=str(error) if isinstance(error, p.Refused) else 'internal_failure')
|
||||
return 1
|
||||
finally:
|
||||
if journal:
|
||||
journal.close()
|
||||
if lock is not None:
|
||||
os.close(lock)
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
result = main()
|
||||
except Exception:
|
||||
result = 1
|
||||
raise SystemExit(result)
|
||||
Loading…
Add table
Add a link
Reference in a new issue