98 lines
5.4 KiB
Python
98 lines
5.4 KiB
Python
"""Strict external C01 composition/principal bindings for D09 generations."""
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import pwd
|
|
import stat
|
|
|
|
from d09_store import read, atomic
|
|
from d09_wire import canonical, digest
|
|
|
|
LAUNCH = Path('/etc/archipelago-c01/launch.json')
|
|
GENERATION_RECORDS = Path('/var/lib/archipelago-c01/access/generations')
|
|
|
|
|
|
def hex_digest(value):
|
|
return type(value) is str and len(value)==64 and all(c in '0123456789abcdef' for c in value)
|
|
|
|
|
|
def validate(binding):
|
|
c=binding['c01']
|
|
if type(c) is not dict or set(c)!={'version','application','runtime','runtime_sha256','generations','transitions'} or type(c['version']) is not int or c['version']!=1:
|
|
raise ValueError('C01 composition fields/version')
|
|
for role in ('application','runtime'):
|
|
r=c[role]
|
|
if set(r)!={'name','uid','gid','home'} or type(r['uid']) is not int or type(r['gid']) is not int or r['uid']<=0 or r['gid']<=0:
|
|
raise ValueError('C01 named principal')
|
|
account=pwd.getpwnam(r['name'])
|
|
if (account.pw_uid,account.pw_gid,account.pw_dir)!=(r['uid'],r['gid'],r['home']) or os.getgrouplist(r['name'],r['gid'])!=[r['gid']]:
|
|
raise ValueError('C01 protected account binding')
|
|
if c['application']['uid']==c['runtime']['uid'] or c['application']['gid']==c['runtime']['gid']:
|
|
raise ValueError('C01 requester and workspace principals must differ')
|
|
runtime_raw=read(Path('/etc/archipelago-c01/runtime.json'))
|
|
import c01_runtime
|
|
runtime,runtime_digest=c01_runtime.load_config()
|
|
if not hex_digest(c['runtime_sha256']) or digest(runtime_raw)!=runtime_digest or runtime_digest!=c['runtime_sha256'] or runtime.get('version')!=2 or runtime.get('workspace_helper_sha256')!=binding['helper_digest']:
|
|
raise ValueError('C01 runtime workspace appointment')
|
|
if (runtime['manager_uid'],runtime['manager_gid'],runtime['runtime_uid'],runtime['runtime_gid'],runtime['home'])!=(
|
|
c['application']['uid'],c['application']['gid'],c['runtime']['uid'],c['runtime']['gid'],c['runtime']['home']):
|
|
raise ValueError('C01 runtime principal mapping')
|
|
if set(c['generations'])!={'old','new','restore'} or set(c['transitions'])!={'APPLY','RESTORE'}:
|
|
raise ValueError('C01 exact generation set')
|
|
expected={'old':binding['prerequisite']['backend_sha256'],'new':binding['artifacts']['backend'],'restore':binding['restore_artifacts']['backend']}
|
|
for generation, fields in c['generations'].items():
|
|
if set(fields)!={'native_sha256','launch_sha256'} or not all(hex_digest(x) for x in fields.values()) or fields['native_sha256']!=expected[generation]:
|
|
raise ValueError('C01 native/launch digest domain')
|
|
if not all(hex_digest(x) for x in c['transitions'].values()):
|
|
raise ValueError('C01 external access transition identity')
|
|
return c
|
|
|
|
|
|
def stage_records(binding, incoming):
|
|
import c01_access_generation as generation
|
|
import c01_access_policy as policy
|
|
c=validate(binding)
|
|
policy_value,policy_digest=policy.json_read(policy.CONFIG)
|
|
controller=generation.Generations(policy_value,policy_digest,*policy.json_read(policy.LAUNCH))
|
|
GENERATION_RECORDS.mkdir(mode=0o700,exist_ok=True)
|
|
for action, hashed in c['transitions'].items():
|
|
raw=read(incoming/('c01-generation-'+hashed+'.json'))
|
|
if digest(raw)!=hashed:
|
|
raise ValueError('C01 transition record changed')
|
|
record=json.loads(raw)
|
|
history=read(incoming/('c01-history-'+record['external_history_digest']+'.json'))
|
|
if digest(history)!=record['external_history_digest']:
|
|
raise ValueError('C01 external history binding')
|
|
facts=json.loads(history)
|
|
for key in ('transaction_id','action','grant_digest','from_launch_sha256','to_launch_sha256'):
|
|
if facts[key]!=record[key]: raise ValueError('C01 external latest history facts')
|
|
if record['transaction_id']!=binding['transaction_id'] or record['action']!=action:
|
|
raise ValueError('C01 transaction scope')
|
|
before,after=('old','new') if action=='APPLY' else ('new','restore')
|
|
if record['from_launch_sha256']!=c['generations'][before]['launch_sha256'] or record['to_launch_sha256']!=c['generations'][after]['launch_sha256']:
|
|
raise ValueError('C01 transition generation scope')
|
|
if record['from_workspace_digest']!=binding['objects']['config']['old' if before=='old' else 'new'] or record['to_workspace_digest']!=binding['objects']['config']['new' if after=='new' else 'restore']:
|
|
raise ValueError('C01 workspace object domain/scope')
|
|
target=GENERATION_RECORDS/(hashed+'.json')
|
|
if target.exists():
|
|
if read(target)!=raw: raise ValueError('C01 retained transition differs')
|
|
else:
|
|
atomic(target,raw)
|
|
controller.record(hashed) # full schema/invariant validation, no transition
|
|
|
|
|
|
def workspace(binding):
|
|
"""Observe the live runtime's actual workspace mapping; never run as app UID."""
|
|
from c01_runtime_view import RuntimeLease
|
|
c=validate(binding)
|
|
a,r=c['application'],c['runtime']
|
|
lease=RuntimeLease(a['uid'],a['gid'],r['uid'],r['gid'],c['runtime_sha256'])
|
|
try:
|
|
path=Path('/proc')/str(lease.identity['pid'])/'root'/r['home'].lstrip('/')/'Work'
|
|
meta=path.lstat()
|
|
if not stat.S_ISDIR(meta.st_mode) or meta.st_uid!=r['uid']:
|
|
raise ValueError('runtime Work mapping unavailable')
|
|
lease.check()
|
|
finally:
|
|
lease.close()
|