Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a0e747-8f27-7242-8df8-8bc44f88c929
101 lines
5.4 KiB
Python
101 lines
5.4 KiB
Python
"""User Engine account-authority HTTP reader; no credential issuance or fallback."""
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import hashlib
|
|
import json
|
|
import time
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
import httpx
|
|
|
|
from hub_core.security.facts import AccountObservation
|
|
from hub_core.security.identity import AccessFailure, require_https
|
|
from hub_core.security.policy import parse_json
|
|
|
|
STATUSES = {'invited', 'active', 'disabled', 'suspended', 'deletion_pending', 'deleted'}
|
|
GRANT = {'scope_type': 'tenant', 'scope_id': 'platform',
|
|
'relation': 'platform-root-custodian', 'owner_system': 'net-kingdom-custody'}
|
|
|
|
|
|
def timestamp(value):
|
|
if not isinstance(value, str):
|
|
raise ValueError('owner timestamp required')
|
|
result = datetime.fromisoformat(value.replace('Z', '+00:00'))
|
|
if result.utcoffset() is None:
|
|
raise ValueError('owner timestamp timezone required')
|
|
return result.timestamp()
|
|
|
|
|
|
def normalize_account(body, *, issuer, subject, tenant, now):
|
|
if not isinstance(body, dict) or (body['issuer'], body['subject'], body['tenant']) != (issuer, subject, tenant):
|
|
raise ValueError('account binding mismatch')
|
|
for key in ('identity_id', 'user_id', 'account_id', 'decision_id'):
|
|
if not isinstance(body[key], str) or not body[key].strip():
|
|
raise ValueError('owner evidence missing')
|
|
if body['account_status'] not in STATUSES or body['tenant_account_status'] not in STATUSES | {None}:
|
|
raise ValueError('unknown account state')
|
|
observed = timestamp(body['observed_at'])
|
|
if not 0 <= now-observed <= 5:
|
|
raise ValueError('stale account observation')
|
|
# An explicit null is a valid no-grant result. Missing field is an old or
|
|
# incomplete authority contract, not permission to infer entitlement.
|
|
grant = body['root_grant']
|
|
deadline = None
|
|
if grant is not None:
|
|
if tenant != 'platform' or set(grant) != {'subject','grant','source','observed_at','expires_at'}:
|
|
raise ValueError('unsupported grant profile')
|
|
if grant['subject'] != {'issuer':issuer,'subject':subject} or grant['grant'] != GRANT:
|
|
raise ValueError('grant identity or scope mismatch')
|
|
source = grant['source']
|
|
if (set(source) != {'record_id','revision'} or source['record_id'] != 'nk-custody:platform-root'
|
|
or type(source['revision']) is not int or source['revision'] < 1):
|
|
raise ValueError('invalid custody provenance')
|
|
grant_time = timestamp(grant['observed_at'])
|
|
deadline = timestamp(grant['expires_at'])
|
|
if not 0 <= now-grant_time <= 5 or not now < deadline <= grant_time+900:
|
|
raise ValueError('stale custody observation')
|
|
observed = min(observed, grant_time)
|
|
evidence = {key: body[key] for key in ('issuer','subject','tenant','identity_id','user_id',
|
|
'account_id','decision_id','account_status','tenant_account_status','observed_at','root_grant')}
|
|
digest = hashlib.sha256(json.dumps(evidence,sort_keys=True,separators=(',',':')).encode()).hexdigest()
|
|
return AccountObservation(issuer,subject,tenant,
|
|
body['account_status'] == body['tenant_account_status'] == 'active',
|
|
grant is not None, observed, 'user-engine:sha256:'+digest, valid_until=deadline)
|
|
|
|
|
|
class UserEngineAccountReader:
|
|
def __init__(self, *, base_url: str, token_file: Path, client: httpx.AsyncClient):
|
|
require_https(base_url)
|
|
if not isinstance(token_file,Path) or not token_file.is_absolute():
|
|
raise ValueError('absolute dedicated workload token path required')
|
|
self.base_url, self.token_file, self.client = base_url.rstrip('/'), token_file, client
|
|
|
|
async def read(self, *, issuer: str, subject: str, tenant: str) -> AccountObservation:
|
|
try:
|
|
async with asyncio.timeout(3):
|
|
with self.token_file.open('rb') as stream:
|
|
raw = stream.read(16385)
|
|
if len(raw) > 16384:
|
|
raise ValueError('oversized credential')
|
|
token = raw.decode('ascii').strip()
|
|
if not token or any(ord(c) < 33 or ord(c) > 126 for c in token):
|
|
raise ValueError('invalid credential')
|
|
async with self.client.stream('POST',self.base_url+'/api/v1/platform/account-authority/lookup',
|
|
json={'issuer':issuer,'subject':subject,'tenant':tenant},
|
|
headers={'Authorization':'Bearer '+token,'Cache-Control':'no-cache',
|
|
'Accept-Encoding':'identity'},timeout=3,follow_redirects=False) as response:
|
|
if response.status_code != 200 or response.headers.get('age','0') != '0':
|
|
raise ValueError('owner lookup refused or cached')
|
|
if response.headers.get('content-encoding','identity') != 'identity':
|
|
raise ValueError('encoded authority response')
|
|
data = bytearray()
|
|
async for chunk in response.aiter_bytes():
|
|
data.extend(chunk)
|
|
if len(data) > 65536:
|
|
raise ValueError('oversized authority response')
|
|
return normalize_account(parse_json(bytes(data)),issuer=issuer,subject=subject,
|
|
tenant=tenant,now=time.time())
|
|
except Exception as exc:
|
|
raise AccessFailure(503,'account_authority_unavailable_or_untrusted') from exc
|