379 lines
24 KiB
Python
379 lines
24 KiB
Python
#!/usr/bin/env python3
|
|
"""Task-scoped continuous authorization controller for the AIOS Candidate writer."""
|
|
from __future__ import annotations
|
|
import base64, fcntl, hashlib, json, os, pwd, grp, re, socket, sqlite3, stat, subprocess, sys, tempfile, time, urllib.error, urllib.parse, urllib.request
|
|
from datetime import datetime, timedelta, timezone
|
|
from pathlib import Path, PurePosixPath, PureWindowsPath
|
|
from typing import Any
|
|
|
|
TASK='AIOS-AUTONOMOUS-RELEASE-FULL-IMPLEMENTATION-20260726'
|
|
AUTH_ID='authz-aios-autonomous-release-20260726'
|
|
BRANCH='patch/chatgpt-autonomous-completion-contract-20260726'
|
|
BASE='875d41c3101093149414292067fd700dc214d4d3'
|
|
REPO='aios/aios'
|
|
WRITER_AUDIENCE='chatgpt-autonomous-release-writer'
|
|
ACTOR='chatgpt-gitops-bot'
|
|
WRITER_INSTANCE='writer-6e32f321-1240-46f8-9693-0b0631d9c77b'
|
|
CHILD_OPERATION_MAX_USES=20
|
|
ROOT=Path('/data/aios-gitops')
|
|
WORKTREE=ROOT/'worktrees/chatgpt-autonomous-completion-contract-20260726'
|
|
BARE=ROOT/'repo.git'
|
|
TASK_ROOT=ROOT/'runtime/tasks'/TASK
|
|
AUTH_DIR=Path('/etc/aios-gitops/task-authorizations')
|
|
PARENT=AUTH_DIR/'authz-aios-autonomous-release-20260726.json'
|
|
STATE_DIR=Path('/var/lib/aios-autonomous-release-authorization')
|
|
STATE=STATE_DIR/'state.json'
|
|
LOCK=STATE_DIR/'controller.lock'
|
|
AUDIT=TASK_ROOT/'continuous-authorization.audit.jsonl'
|
|
SOCKET_DIR=Path('/run/aios-autonomous-release-authorization')
|
|
SOCKET_PATH=SOCKET_DIR/'controller.sock'
|
|
PROJECTION=Path('/etc/aios-gitops/credentials/chatgpt-autonomous-release-writer')
|
|
REGISTRY=Path('/var/lib/aios-chatgpt-autonomous-release-writer/token-registry.sqlite3')
|
|
WRITER_LEDGER=Path('/var/lib/aios-chatgpt-autonomous-release-writer/operation-ledger.sqlite3')
|
|
ASKPASS=Path('/usr/libexec/aios-autonomous-release-authorization/gitea_askpass.py')
|
|
PATH_POLICY=Path('/etc/aios-gitops/policies-chatgpt-autonomous-release/autonomous-release-path-policy.json')
|
|
ROUTE_BINDING=Path('/etc/aios-gitops/action-route-bindings/chatgpt-autonomous-release-20260726.json')
|
|
OID_RE=re.compile(r'^[0-9a-f]{40}$')
|
|
OP_RE=re.compile(r'^[A-Za-z0-9][A-Za-z0-9._:-]{7,127}$')
|
|
TERMINAL={'COMPLETED','CANCELLED','FAILED_FINAL','REVOKED','REVOKED_IDLE_TIMEOUT','REVOKED_WALL_CLOCK','RECONCILIATION_REQUIRED'}
|
|
|
|
sys.path.insert(0,'/usr/libexec/aios-chatgpt-autonomous-release-writer-fullprofile-8ac2f90f')
|
|
from isolated_writer_token_registry import TokenRegistry
|
|
|
|
def now() -> datetime: return datetime.now(timezone.utc)
|
|
def stamp() -> str: return now().replace(microsecond=0).isoformat()
|
|
def canonical(value: Any) -> bytes: return json.dumps(value,sort_keys=True,separators=(',',':'),ensure_ascii=False).encode('utf-8')
|
|
def fail(code: str, status: int=503, **details: Any) -> None: raise ControlError(code,status,details)
|
|
class ControlError(RuntimeError):
|
|
def __init__(self,code:str,status:int=503,details:dict[str,Any]|None=None): self.code,self.status,self.details=code,status,details or {}; super().__init__(code)
|
|
|
|
def owned_regular(path:Path, mode:int) -> os.stat_result:
|
|
try: info=os.lstat(path)
|
|
except FileNotFoundError: fail('authorization_config_missing')
|
|
if not stat.S_ISREG(info.st_mode) or stat.S_ISLNK(info.st_mode) or info.st_uid!=0 or stat.S_IMODE(info.st_mode)&~mode: fail('authorization_config_permissions_rejected')
|
|
return info
|
|
|
|
def load_json(path:Path,mode:int=0o600) -> dict[str,Any]:
|
|
owned_regular(path,mode)
|
|
try: value=json.loads(path.read_text(encoding='utf-8'))
|
|
except Exception: fail('authorization_config_json_rejected')
|
|
if not isinstance(value,dict): fail('authorization_config_schema_rejected')
|
|
return value
|
|
|
|
def atomic_json(path:Path,value:dict[str,Any],mode:int=0o600) -> None:
|
|
path.parent.mkdir(parents=True,exist_ok=True)
|
|
raw=canonical(value)+b'\n'
|
|
fd,tmp=tempfile.mkstemp(prefix='.new-',dir=path.parent)
|
|
try:
|
|
os.fchmod(fd,mode); os.write(fd,raw); os.fsync(fd); os.close(fd); os.chown(tmp,0,0); os.replace(tmp,path)
|
|
os.chmod(path,mode)
|
|
finally:
|
|
try: os.close(fd)
|
|
except OSError: pass
|
|
if os.path.exists(tmp): os.unlink(tmp)
|
|
|
|
def append_audit(event:dict[str,Any]) -> None:
|
|
TASK_ROOT.mkdir(parents=True,exist_ok=True)
|
|
flags=os.O_WRONLY|os.O_CREAT|os.O_APPEND|getattr(os,'O_NOFOLLOW',0)
|
|
fd=os.open(AUDIT,flags,0o600)
|
|
try: os.write(fd,canonical({'at':stamp(),'task_id':TASK,'secrets_included':False,**event})+b'\n'); os.fsync(fd)
|
|
finally: os.close(fd)
|
|
|
|
def parent() -> dict[str,Any]:
|
|
p=load_json(PARENT)
|
|
required={'schema_version','development_authorization_id','authorization_type','task_id','actor_identity','audience','repository','exact_branch','approved_base_oid','allowed_path_roots','allowed_exact_paths','allowed_operations','created_at','valid_until','maximum_wall_clock_seconds','idle_timeout_seconds','manual_reapproval_per_commit','maximum_concurrent_writes','path_policy_sha256','writer_instance_id'}
|
|
if set(p)!=required or p.get('schema_version')!=1: fail('parent_authorization_schema_rejected')
|
|
fixed={'development_authorization_id':AUTH_ID,'authorization_type':'TASK_SCOPED_CONTINUOUS_DEVELOPMENT','task_id':TASK,'actor_identity':ACTOR,'audience':WRITER_AUDIENCE,'repository':REPO,'exact_branch':BRANCH,'approved_base_oid':BASE,'manual_reapproval_per_commit':False,'maximum_concurrent_writes':1,'writer_instance_id':WRITER_INSTANCE}
|
|
if any(p.get(k)!=v for k,v in fixed.items()): fail('parent_authorization_binding_rejected')
|
|
if p.get('maximum_wall_clock_seconds')!=86400 or p.get('idle_timeout_seconds')!=2700: fail('parent_authorization_lifetime_rejected')
|
|
if not isinstance(p.get('allowed_path_roots'),list) or not isinstance(p.get('allowed_exact_paths'),list) or not isinstance(p.get('allowed_operations'),list): fail('parent_authorization_scope_rejected')
|
|
raw=PATH_POLICY.read_bytes()
|
|
if hashlib.sha256(raw).hexdigest()!=p.get('path_policy_sha256'): fail('parent_authorization_path_policy_changed')
|
|
return p
|
|
|
|
def parse_time(v:Any) -> datetime:
|
|
try:
|
|
d=datetime.fromisoformat(str(v).replace('Z','+00:00'))
|
|
return d.astimezone(timezone.utc)
|
|
except Exception: fail('authorization_time_rejected')
|
|
|
|
def components(path:str)->tuple[str,...]:
|
|
if not isinstance(path,str) or not path or path!=path.strip() or '\\' in path or '\x00' in path: fail('authorization_path_rejected',403)
|
|
pp=PurePosixPath(path); wp=PureWindowsPath(path)
|
|
parts=tuple(path.split('/'))
|
|
if pp.is_absolute() or wp.is_absolute() or bool(wp.drive) or any(x in {'','.','..'} for x in parts): fail('authorization_path_rejected',403)
|
|
return parts
|
|
|
|
def path_allowed(p:dict[str,Any],path:str)->bool:
|
|
item=components(path)
|
|
for root in p['allowed_path_roots']:
|
|
r=components(str(root).rstrip('/'))
|
|
if item[:len(r)]==r: return True
|
|
return any(item==components(x) for x in p['allowed_exact_paths'])
|
|
|
|
def git(*args:str,cwd:Path|None=None,env:dict[str,str]|None=None)->str:
|
|
e=os.environ.copy(); e.update(env or {}); e['GIT_CONFIG_COUNT']='1'; e['GIT_CONFIG_KEY_0']='safe.directory'; e['GIT_CONFIG_VALUE_0']=str(WORKTREE)
|
|
r=subprocess.run(['git',*args],cwd=cwd,text=True,capture_output=True,env=e)
|
|
if r.returncode: fail('authorization_git_probe_failed',503)
|
|
return r.stdout.strip()
|
|
|
|
def credential_lines()->list[str]:
|
|
cp=Path(os.environ.get('CREDENTIALS_DIRECTORY',''))/'gitea_git_http'
|
|
try:
|
|
info=os.lstat(cp); lines=cp.read_text(encoding='utf-8').splitlines()
|
|
except OSError: fail('authorization_gitea_credential_unavailable')
|
|
if not stat.S_ISREG(info.st_mode) or stat.S_ISLNK(info.st_mode) or len(lines)!=2 or lines[0]!='gitops-bot' or not lines[1]: fail('authorization_gitea_credential_rejected')
|
|
return lines
|
|
|
|
def remote_oid()->str:
|
|
credential_lines()
|
|
e={'GIT_ASKPASS':str(ASKPASS),'GIT_TERMINAL_PROMPT':'0'}
|
|
out=git('ls-remote','--heads','http://127.0.0.1:3000/aios/aios.git',f'refs/heads/{BRANCH}',cwd=WORKTREE,env=e)
|
|
fields=out.split()
|
|
if len(fields)!=2 or not OID_RE.fullmatch(fields[0]): fail('authorization_git_ls_remote_rejected')
|
|
return fields[0]
|
|
|
|
def api_oid()->str:
|
|
user,password=credential_lines(); branch=urllib.parse.quote(BRANCH,safe='')
|
|
encoded=base64.b64encode((user+':'+password).encode()).decode()
|
|
request=urllib.request.Request('http://127.0.0.1:3000/api/v1/repos/aios/aios/branches/'+branch,headers={'Authorization':'Basic '+encoded,'Host':'g.cnnetmkt.top'})
|
|
try:
|
|
with urllib.request.urlopen(request,timeout=10) as response: value=json.loads(response.read())
|
|
except Exception: fail('authorization_gitea_api_unavailable')
|
|
oid=str(value.get('commit',{}).get('id',''))
|
|
if not OID_RE.fullmatch(oid): fail('authorization_gitea_api_oid_rejected')
|
|
return oid
|
|
|
|
def four_way(expected:str)->None:
|
|
local=git('rev-parse','HEAD',cwd=WORKTREE)
|
|
bare=git('--git-dir',str(BARE),'rev-parse',f'refs/heads/{BRANCH}')
|
|
remote=remote_oid(); api=api_oid()
|
|
if any(x!=expected for x in (local,bare,remote,api)): fail('RECONCILIATION_REQUIRED',409,current_head_oid=local)
|
|
if git('status','--porcelain',cwd=WORKTREE)!='': fail('authorization_worktree_dirty',409)
|
|
|
|
def state()->dict[str,Any]:
|
|
s=load_json(STATE)
|
|
required={'schema_version','development_authorization_id','task_id','status','current_head_oid','last_activity_at','writer_child','test_child','inflight','updated_at'}
|
|
if set(s)!=required or s.get('schema_version')!=1 or s.get('development_authorization_id')!=AUTH_ID or s.get('task_id')!=TASK or not OID_RE.fullmatch(str(s.get('current_head_oid',''))): fail('authorization_state_schema_rejected')
|
|
return s
|
|
|
|
def save_state(s:dict[str,Any])->None: s['updated_at']=stamp(); atomic_json(STATE,s)
|
|
|
|
def lock():
|
|
STATE_DIR.mkdir(parents=True,exist_ok=True); os.chmod(STATE_DIR,0o700)
|
|
f=open(LOCK,'a+b'); os.chmod(LOCK,0o600); fcntl.flock(f.fileno(),fcntl.LOCK_EX); return f
|
|
|
|
def route_binding_valid(p:dict[str,Any])->None:
|
|
b=load_json(ROUTE_BINDING,0o640)
|
|
identities=b.get('action_identities')
|
|
if b.get('task_id')!=TASK or b.get('writer_instance_id')!=WRITER_INSTANCE or b.get('repository')!=REPO or b.get('branch')!=BRANCH or b.get('approved_base_oid')!=BASE or b.get('path_policy_sha256')!=p.get('path_policy_sha256') or not isinstance(identities,list) or len(identities)!=1 or identities[0].get('account')!=ACTOR:
|
|
fail('authorization_route_binding_changed',409)
|
|
|
|
def task_live(p:dict[str,Any],s:dict[str,Any],*,touch:bool)->None:
|
|
route_binding_valid(p)
|
|
if s['status'] in TERMINAL: fail('authorization_task_terminal',409)
|
|
if now()>=parse_time(p['valid_until']):
|
|
s['status']='REVOKED_WALL_CLOCK'; save_state(s); cleanup(s,'wall_clock'); fail('authorization_task_terminal',409)
|
|
if (now()-parse_time(s['last_activity_at'])).total_seconds()>p['idle_timeout_seconds']:
|
|
s['status']='REVOKED_IDLE_TIMEOUT'; save_state(s); cleanup(s,'idle_timeout'); fail('authorization_task_terminal',409)
|
|
if touch: s['last_activity_at']=stamp(); save_state(s)
|
|
|
|
def projection_token()->str|None:
|
|
if not PROJECTION.exists(): return None
|
|
try:
|
|
info=os.lstat(PROJECTION)
|
|
if not stat.S_ISREG(info.st_mode) or stat.S_ISLNK(info.st_mode) or info.st_uid!=0 or stat.S_IMODE(info.st_mode)!=0o600: fail('authorization_projection_rejected')
|
|
token=PROJECTION.read_text(encoding='utf-8').strip()
|
|
return token if len(token.encode())>=32 else None
|
|
except OSError: fail('authorization_projection_rejected')
|
|
|
|
def write_projection(token:str)->None:
|
|
PROJECTION.parent.mkdir(parents=True,exist_ok=True)
|
|
fd,tmp=tempfile.mkstemp(prefix='.writer-',dir=PROJECTION.parent)
|
|
try:
|
|
os.fchmod(fd,0o600); os.write(fd,(token+'\n').encode()); os.fsync(fd); os.close(fd); os.chown(tmp,0,0); os.replace(tmp,PROJECTION); os.chmod(PROJECTION,0o600)
|
|
finally:
|
|
try: os.close(fd)
|
|
except OSError: pass
|
|
if os.path.exists(tmp): os.unlink(tmp)
|
|
|
|
def post_revoke_401(token:str)->None:
|
|
req=urllib.request.Request('http://127.0.0.1:19893/api/runtime-status',headers={'Authorization':'Bearer '+token})
|
|
try: urllib.request.urlopen(req,timeout=5)
|
|
except urllib.error.HTTPError as e:
|
|
if e.code==401:return
|
|
except Exception: pass
|
|
fail('authorization_old_credential_revocation_unverified')
|
|
|
|
def rotate_child(p:dict[str,Any],s:dict[str,Any],reason:str)->str:
|
|
old=projection_token(); registry=TokenRegistry(REGISTRY)
|
|
old_record=registry.find(old) if old else None
|
|
if old_record and (old_record.get('task_id')!=TASK or old_record.get('development_authorization_id')!=AUTH_ID):
|
|
if parse_time(old_record.get('expires_at'))>=now(): fail('authorization_existing_projection_binding_rejected')
|
|
if not registry.revoke(str(old_record['token_id'])): fail('authorization_historical_child_revoke_failed')
|
|
post_revoke_401(old)
|
|
append_audit({'event':'historical_expired_child_revoked','old_credential_http_status':401})
|
|
old_record=None
|
|
issued=now().replace(microsecond=0); expires=issued+timedelta(minutes=20)
|
|
prefixes=[str(x).rstrip('/')+'/' for x in p['allowed_path_roots']]
|
|
grant={'token_id':'continuous-child-'+issued.strftime('%Y%m%dT%H%M%SZ')+'-'+os.urandom(3).hex(),'credential_id':'credential-'+issued.strftime('%Y%m%dT%H%M%SZ')+'-'+os.urandom(3).hex(),'development_authorization_id':AUTH_ID,'audience':WRITER_AUDIENCE,'task_id':TASK,'actor_identity':ACTOR,'writer_instance_id':WRITER_INSTANCE,'repository_identity':REPO,'source_cidrs':['127.0.0.1/32'],'allowed_branch':BRANCH,'current_expected_head_oid':s['current_head_oid'],'approved_base_oid':BASE,'allowed_operations':p['allowed_operations'],'allowed_paths':p['allowed_exact_paths'],'allowed_path_prefixes':prefixes,'not_before':issued.isoformat(),'expires_at':expires.isoformat(),'renewable':False,'max_uses':{operation:CHILD_OPERATION_MAX_USES for operation in p['allowed_operations']},'issued_by':'task_authorization_controller'}
|
|
token,record=registry.issue(grant)
|
|
try:
|
|
write_projection(token)
|
|
if old_record:
|
|
if not registry.revoke(str(old_record['token_id'])): fail('authorization_old_child_revoke_failed')
|
|
post_revoke_401(old)
|
|
except Exception:
|
|
registry.revoke(str(record['token_id']))
|
|
try: PROJECTION.unlink()
|
|
except FileNotFoundError: pass
|
|
raise
|
|
s['writer_child']={'credential_id':record['credential_id'],'issued_at':record['not_before'],'expires_at':record['expires_at'],'current_expected_head_oid':s['current_head_oid'],'rotation_reason':reason}
|
|
save_state(s); append_audit({'event':'child_grant_rotated','reason':reason,'current_head_oid':s['current_head_oid'],'old_grant_revoked':bool(old_record),'old_credential_http_status':401 if old_record else None})
|
|
return token
|
|
|
|
def child_operation_quota_exhausted(rec:dict[str,Any],registry:TokenRegistry)->bool:
|
|
token_id=str(rec.get('token_id',''))
|
|
allowed=rec.get('allowed_operations')
|
|
limits=rec.get('max_uses')
|
|
if not token_id or not isinstance(allowed,list) or not isinstance(limits,dict): fail('authorization_child_quota_schema_rejected')
|
|
for operation in allowed:
|
|
maximum=limits.get(operation)
|
|
if not isinstance(operation,str) or not isinstance(maximum,int) or maximum<1: fail('authorization_child_quota_schema_rejected')
|
|
if registry.usage(token_id,operation)>=maximum: return True
|
|
return False
|
|
|
|
def ensure_child(p:dict[str,Any],s:dict[str,Any],reason:str)->str:
|
|
token=projection_token(); meta=s.get('writer_child') or {}
|
|
expired=True
|
|
try: expired=(parse_time(meta.get('expires_at'))-now()).total_seconds()<=300
|
|
except Exception: pass
|
|
if not token or expired or meta.get('current_expected_head_oid')!=s['current_head_oid']:
|
|
return rotate_child(p,s,reason)
|
|
registry=TokenRegistry(REGISTRY)
|
|
rec=registry.find(token)
|
|
if not rec or rec.get('revoked') or rec.get('development_authorization_id')!=AUTH_ID or rec.get('current_expected_head_oid')!=s['current_head_oid']:
|
|
return rotate_child(p,s,reason)
|
|
if child_operation_quota_exhausted(rec,registry):
|
|
return rotate_child(p,s,'operation_quota_exhausted')
|
|
return token
|
|
|
|
def ledger_verified(operation_id:str,commit_oid:str)->None:
|
|
with sqlite3.connect(WRITER_LEDGER) as db:
|
|
row=db.execute('select operation_id,branch,commit_oid,state,verification_status from writer_operations where operation_id=?',(operation_id,)).fetchone()
|
|
if not row or row[1]!=BRANCH or row[2]!=commit_oid or row[3]!='COMPLETED' or row[4]!='bare+gitea': fail('authorization_writer_ledger_rejected',409)
|
|
|
|
def reconcile(p:dict[str,Any],s:dict[str,Any],previous:str,new:str,operation_id:str)->dict[str,Any]:
|
|
inflight=s.get('inflight')
|
|
if not isinstance(inflight,dict) or inflight.get('previous_head_oid')!=previous or inflight.get('operation_id')!=operation_id: fail('authorization_inflight_rejected',409)
|
|
if previous!=s['current_head_oid'] or not OID_RE.fullmatch(new): fail('authorization_head_transition_rejected',409)
|
|
ledger_verified(operation_id,new); four_way(new)
|
|
s['current_head_oid']=new; s['inflight']=None; s['last_activity_at']=stamp(); save_state(s)
|
|
ensure_child(p,s,'head_advance')
|
|
append_audit({'event':'authorization_head_advanced','previous_head_oid':previous,'new_commit_oid':new,'four_way_ref_status':True,'worktree_clean':True})
|
|
return {'authorization_current_head_oid':new,'automatic_head_advance':True,'four_way_ref_status':True}
|
|
|
|
def prepare(p:dict[str,Any],s:dict[str,Any],data:dict[str,Any])->dict[str,Any]:
|
|
required={'expected_current_head_oid','operation_id','idempotency_key','exact_path','content_digest','commit_message_digest','branch','path','message'}
|
|
if set(data)!=required: fail('authorization_request_schema_rejected',403)
|
|
if data['branch']!=BRANCH or data['path']!=data['exact_path'] or not path_allowed(p,str(data['path'])): fail('authorization_path_rejected',403)
|
|
if data['expected_current_head_oid']!=s['current_head_oid'] or not OID_RE.fullmatch(str(data['expected_current_head_oid'])): fail('BRANCH_HEAD_CHANGED',409,current_head_oid=s['current_head_oid'])
|
|
if not OP_RE.fullmatch(str(data['operation_id'])) or not OP_RE.fullmatch(str(data['idempotency_key'])): fail('authorization_operation_id_rejected',403)
|
|
if not all(isinstance(data[k],str) and re.fullmatch(r'[0-9a-f]{64}',data[k]) for k in ('content_digest','commit_message_digest')): fail('authorization_digest_rejected',403)
|
|
if hashlib.sha256(data['message'].encode()).hexdigest()!=data['commit_message_digest']: fail('authorization_commit_message_digest_rejected',403)
|
|
inflight=s.get('inflight')
|
|
if inflight:
|
|
if inflight.get('operation_id')==data['operation_id'] and inflight.get('idempotency_key')==data['idempotency_key']: return {'expected_head_oid':s['current_head_oid'],'idempotency_key':data['idempotency_key']}
|
|
fail('BRANCH_HEAD_CHANGED',409,current_head_oid=s['current_head_oid'])
|
|
four_way(s['current_head_oid'])
|
|
s['inflight']={'operation_id':data['operation_id'],'idempotency_key':data['idempotency_key'],'previous_head_oid':s['current_head_oid'],'path':data['path'],'created_at':stamp()}; s['last_activity_at']=stamp(); save_state(s)
|
|
append_audit({'event':'write_prepared','operation_id':data['operation_id'],'path':data['path'],'expected_head_oid':s['current_head_oid']})
|
|
return {'expected_head_oid':s['current_head_oid'],'idempotency_key':data['idempotency_key']}
|
|
|
|
def test_authorize(p:dict[str,Any],s:dict[str,Any],operation:str)->dict[str,Any]:
|
|
allowed={'pytest_dormant','pytest_autonomous_release','compileall_autonomous_release','git_diff_check'}
|
|
if operation not in allowed: fail('candidate_test_executor_operation_rejected',403)
|
|
four_way(s['current_head_oid'])
|
|
child=s.get('test_child') or {}; refresh=True
|
|
try: refresh=(parse_time(child.get('expires_at'))-now()).total_seconds()<=300
|
|
except Exception: pass
|
|
if refresh:
|
|
issued=now().replace(microsecond=0); child={'test_authorization_id':'test-authz-aios-autonomous-release-20260726','grant_id':'test-child-'+issued.strftime('%Y%m%dT%H%M%SZ'),'issued_at':issued.isoformat(),'expires_at':(issued+timedelta(minutes=20)).isoformat()}; s['test_child']=child
|
|
s['last_activity_at']=stamp(); save_state(s); append_audit({'event':'test_authorized','operation':operation})
|
|
return {'test_authorization_id':child['test_authorization_id'],'operation':operation,'expires_at':child['expires_at']}
|
|
|
|
def cleanup(s:dict[str,Any],reason:str)->None:
|
|
old=projection_token()
|
|
if old:
|
|
reg=TokenRegistry(REGISTRY); rec=reg.find(old)
|
|
if rec: reg.revoke(str(rec['token_id']))
|
|
try: PROJECTION.unlink()
|
|
except FileNotFoundError: pass
|
|
try: post_revoke_401(old)
|
|
except ControlError: pass
|
|
for unit in ('aios-chatgpt-autonomous-release-route.service','aios-chatgpt-autonomous-release-writer.service'):
|
|
subprocess.run(['/usr/bin/systemctl','stop',unit],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,timeout=20)
|
|
subprocess.run(['/usr/bin/systemctl','stop','aios-autonomous-release-dormant-test-executor@*.service'],stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL,timeout=20)
|
|
try: ROUTE_BINDING.unlink()
|
|
except FileNotFoundError: pass
|
|
try: SOCKET_PATH.unlink()
|
|
except FileNotFoundError: pass
|
|
append_audit({'event':'authorization_cleanup','reason':reason,'projection_removed':not PROJECTION.exists(),'route_overlay_removed':not ROUTE_BINDING.exists(),'task_services_stop_requested':True})
|
|
|
|
def request(data:dict[str,Any])->dict[str,Any]:
|
|
op=data.get('op'); f=lock()
|
|
try:
|
|
p=parent(); s=state(); task_live(p,s,touch=False)
|
|
if op=='describe_authorization':
|
|
four_way(s['current_head_oid'])
|
|
return {'development_authorization_id':AUTH_ID,'task_id':TASK,'actor_identity':ACTOR,'audience':WRITER_AUDIENCE,'repository':REPO,'branch':BRANCH,'approved_base_oid':BASE,'authorization_status':s['status'],'current_head_oid':s['current_head_oid']}
|
|
if op=='get_writer_token':
|
|
four_way(s['current_head_oid']); token=ensure_child(p,s,'scheduled_or_requested_rotation'); return {'token':token,'current_head_oid':s['current_head_oid']}
|
|
if op=='prepare_write':
|
|
ensure_child(p,s,'pre_write_quota_check')
|
|
return prepare(p,s,data.get('request') if isinstance(data.get('request'),dict) else {})
|
|
if op=='reconcile_commit':
|
|
r=data.get('result') if isinstance(data.get('result'),dict) else {}; return reconcile(p,s,str(r.get('previous_head_oid','')),str(r.get('new_commit_oid','')),str(r.get('operation_id','')))
|
|
if op=='fail_write':
|
|
r=data.get('result') if isinstance(data.get('result'),dict) else {}; inflight=s.get('inflight')
|
|
if isinstance(inflight,dict) and inflight.get('operation_id')==r.get('operation_id'): s['inflight']=None; save_state(s)
|
|
return {'cleared':True}
|
|
if op=='authorize_test': return test_authorize(p,s,str(data.get('operation','')))
|
|
fail('authorization_operation_rejected',403)
|
|
finally:
|
|
fcntl.flock(f.fileno(),fcntl.LOCK_UN); f.close()
|
|
|
|
def peer_allowed(conn:socket.socket)->bool:
|
|
try:
|
|
import struct
|
|
_pid,uid,gid=struct.unpack('3i',conn.getsockopt(socket.SOL_SOCKET,socket.SO_PEERCRED,12))
|
|
return uid==pwd.getpwnam('aiman').pw_uid and gid==grp.getgrnam('gitops').gr_gid
|
|
except Exception: return False
|
|
|
|
def serve()->None:
|
|
SOCKET_DIR.mkdir(parents=True,exist_ok=True); os.chown(SOCKET_DIR,0,grp.getgrnam('gitops').gr_gid); os.chmod(SOCKET_DIR,0o750)
|
|
try: SOCKET_PATH.unlink()
|
|
except FileNotFoundError: pass
|
|
with socket.socket(socket.AF_UNIX,socket.SOCK_STREAM) as server:
|
|
server.bind(str(SOCKET_PATH)); os.chown(SOCKET_PATH,0,grp.getgrnam('gitops').gr_gid); os.chmod(SOCKET_PATH,0o660); server.listen(32); server.settimeout(1.0)
|
|
last=0.0
|
|
while True:
|
|
if time.monotonic()-last>30:
|
|
last=time.monotonic()
|
|
try:
|
|
f=lock(); p=parent(); s=state(); task_live(p,s,touch=False); four_way(s['current_head_oid']); ensure_child(p,s,'scheduled_rotation'); fcntl.flock(f.fileno(),fcntl.LOCK_UN); f.close()
|
|
except ControlError as e:
|
|
append_audit({'event':'scheduled_check_blocked','error':e.code})
|
|
if e.code=='authorization_task_terminal': return
|
|
except Exception: append_audit({'event':'scheduled_check_blocked','error':'authorization_controller_internal_error'})
|
|
try: conn,_=server.accept()
|
|
except socket.timeout: continue
|
|
with conn:
|
|
if not peer_allowed(conn): conn.sendall(canonical({'ok':False,'status':403,'error':'authorization_peer_rejected'})+b'\n'); continue
|
|
try:
|
|
raw=conn.recv(131072); data=json.loads(raw.decode()); result=request(data); conn.sendall(canonical({'ok':True,'result':result})+b'\n')
|
|
except ControlError as e: conn.sendall(canonical({'ok':False,'status':e.status,'error':e.code,**e.details})+b'\n')
|
|
except Exception as exc:
|
|
append_audit({'event':'controller_internal_error','error_type':type(exc).__name__})
|
|
conn.sendall(canonical({'ok':False,'status':503,'error':'authorization_controller_internal_error'})+b'\n')
|
|
if __name__=='__main__': serve() |