Files
SGF/scripts/functional-access-apply-remote.py
2026-10-30 23:20:27 +02:00

42 lines
7.6 KiB
Python

import pathlib,subprocess,json,os,hashlib,datetime
plan=PLAN
expected=EXPECTED_HASHES
root=pathlib.Path('/var/www/html/sgf.tempo.co.mz')
for f,h in expected.items():
if hashlib.sha256((root/f).read_bytes()).hexdigest()!=h:raise RuntimeError('Authorisation source changed; stop before writing: '+f)
assert plan['target']['id']==1 and plan['target']['email']=='nala@tempo.co.mz' and len(plan['entries'])==82
php=r'''
chdir('/var/www/html/sgf.tempo.co.mz');require 'vendor/autoload.php';$p=json_decode(stream_get_contents(STDIN),true,64,JSON_THROW_ON_ERROR);$a=require 'bootstrap/cache/config.php';$c=$a['database']['connections'][$a['database']['default']];if($c['database']!=='sgf_db'||$a['app']['env']!=='production'||$a['app']['url']!=='https://sgf.tempo.co.mz')throw new Exception('Destination changed');
$db=new PDO('pgsql:host='.$c['host'].';port='.$c['port'].';dbname='.$c['database'],$c['username'],$c['password'],[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION]);$db->beginTransaction();$db->exec('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ');$db->exec("SET LOCAL lock_timeout='5s'");$db->exec("SET LOCAL statement_timeout='30s'");$db->exec('SELECT pg_advisory_xact_lock(72644101)');$db->exec('LOCK TABLE permission_grants IN SHARE ROW EXCLUSIVE MODE');
$q=$db->query('select id,name,email,auth_version from users where id=1 for update');$u=$q->fetch(PDO::FETCH_ASSOC);if(!$u||$u['email']!=='nala@tempo.co.mz'||$u['name']!=='Osmane Nala')throw new Exception('Identity changed');
foreach(['tenants'=>['name'=>'Tempo'],'companies'=>['name'=>'Tempo, Lda.','tenant_id'=>1],'units'=>['name'=>'SEGI','tenant_id'=>1,'company_id'=>1]] as $t=>$fields){$row=$db->query('select * from '.$t.' where id=1 for update')->fetch(PDO::FETCH_ASSOC);foreach($fields as $k=>$v)if(!$row||$row[$k]!=$v)throw new Exception('Scope changed');}
if(!$db->query('select 1 from company_memberships where user_id=1 and tenant_id=1 and company_id=1')->fetchColumn())throw new Exception('Membership changed');
$allBefore=$db->query('select * from permission_grants order by id')->fetchAll(PDO::FETCH_ASSOC);$before=array_values(array_filter($allBefore,fn($g)=>$g['user_id']==1&&$g['tenant_id']==1&&$g['company_id']==1));if(count($before)!==6)throw new Exception('Expected six original rights; review changed state before retry');
$wanted=[];foreach($p['entries'] as $e){if($e['tenant_id']!==1||$e['company_id']!==1||!in_array($e['unit_id'],[null,1],true))throw new Exception('Out-of-scope manifest');$k=$e['permission'].'@'.($e['unit_id']??'company');if(isset($wanted[$k]))throw new Exception('Duplicate manifest right');$wanted[$k]=$e;}
foreach($before as $g){$k=$g['permission'].'@'.($g['unit_id']??'company');if(!isset($wanted[$k])||!$wanted[$k]['already_granted'])throw new Exception('Original grant changed');}
if(getenv('SGF_ACCESS_MODE')==='preview'){$db->rollBack();echo json_encode(['preview_passed'=>true,'existing'=>6,'expected_additions'=>76,'target'=>$u,'effective_database'=>'sgf_db']);exit;}
$tables=$db->query("select tablename from pg_tables where schemaname='public' order by tablename")->fetchAll(PDO::FETCH_COLUMN);$count=function()use($db,$tables){$r=[];foreach($tables as $t)$r[$t]=(int)$db->query('select count(*) from "'.str_replace('"','""',$t).'"')->fetchColumn();return $r;};$countsBefore=$count();$q=$db->prepare('insert into permission_grants (user_id,tenant_id,company_id,unit_id,permission,created_at,updated_at) values (1,1,1,?,?,now(),now()) returning id');$added=[];
foreach($wanted as $k=>$e){$matches=array_filter($before,fn($g)=>$g['permission']===$e['permission']&&($g['unit_id']===null?null:(int)$g['unit_id'])===$e['unit_id']);if($matches)continue;$q->execute([$e['unit_id'],$e['permission']]);$added[]=['id'=>(int)$q->fetchColumn(),'permission'=>$e['permission'],'unit_id'=>$e['unit_id']];}if(count($added)!==76)throw new Exception('Unexpected addition count');
$q=$db->prepare('insert into identity_audit (action,actor_id,tenant_id,company_id,unit_id,subject_id,details,created_at) values (?,?,?,?,?,?,?,now()) returning id');$q->execute(['identity.functional_access_package_granted',1,1,1,1,1,json_encode(['operator'=>'Tempo — confirmação expressa do pacote de 82 direitos em 30/10/2026','reason'=>'SGF-FUNCTIONAL-DEVELOPMENT-ACCESS-PROPOSAL-20261030; conservar seis direitos e acrescentar 76; sem levantar guardas fiscais, bancárias ou de QA; manifesto SHA256 '.$p['approved_manifest_sha256'],'grants'=>array_map(fn($e)=>['permission'=>$e['permission'],'unit_id'=>$e['unit_id']],$added)],JSON_THROW_ON_ERROR)]);$audit=(int)$q->fetchColumn();
$allAfter=$db->query('select * from permission_grants order by id')->fetchAll(PDO::FETCH_ASSOC);$byId=array_column($allAfter,null,'id');foreach($allBefore as $g)if(($byId[$g['id']]??null)!==$g)throw new Exception('Previous grant changed');$actual=array_values(array_filter($allAfter,fn($g)=>$g['user_id']==1&&$g['tenant_id']==1&&$g['company_id']==1));if(count($actual)!==82)throw new Exception('Final package size mismatch');foreach($actual as $g)if(!isset($wanted[$g['permission'].'@'.($g['unit_id']??'company')]))throw new Exception('Extra grant detected');
$afterCounts=$count();foreach($countsBefore as $t=>$n){$delta=$t==='permission_grants'?76:($t==='identity_audit'?1:0);if($afterCounts[$t]!==$n+$delta)throw new Exception('Unexpected table effect');}
$db->commit();echo json_encode(['applied'=>true,'database'=>'sgf_db','user'=>['id'=>1,'name'=>$u['name'],'email'=>$u['email']],'tenant_id'=>1,'company_id'=>1,'unit_id'=>1,'existing_preserved'=>6,'added'=>76,'final'=>82,'added_grants'=>$added,'audit_id'=>$audit,'all_previous_grants_preserved'=>true,'other_table_counts_preserved'=>true,'public_tables_checked'=>count($tables),'schema_changes'=>0,'credential_changes'=>0,'qa_guards_removed'=>false,'public_payables_enabled'=>false,'fiscal_real_enabled'=>false,'bank_execution_enabled'=>false]);
'''
body=dict(plan);body['approved_manifest_sha256']=MANIFEST_SHA
payload=json.dumps(body);env=dict(os.environ);env['SGF_ACCESS_MODE']='preview'
r=subprocess.run(['php','-r',php],input=payload,capture_output=True,text=True,env=env)
if r.returncode:raise RuntimeError('Readiness verification failed; no apply attempted; output withheld')
preview=json.loads(r.stdout)
if MODE=='preview':print(json.dumps(preview));raise SystemExit(0)
backup=pathlib.Path('/var/backups/sgf-functional-access-20261030');backup.mkdir(mode=0o700,exist_ok=True);os.chmod(backup,0o700);archive=backup/'permissions-audit-before.dump'
if archive.exists():raise RuntimeError('Existing operation backup; inspect receipt before any retry')
r=subprocess.run(['runuser','-u','postgres','--','pg_dump','-Fc','--dbname=sgf_db','-t','public.permission_grants','-t','public.identity_audit'],capture_output=True,check=True);fd=os.open(archive,os.O_WRONLY|os.O_CREAT|os.O_EXCL,0o600)
with os.fdopen(fd,'wb') as f:f.write(r.stdout)
subprocess.run(['pg_restore','--list',str(archive)],capture_output=True,check=True)
# Validate restore syntax and content readability; do not restore over live history.
subprocess.run(['pg_restore','--file=-',str(archive)],capture_output=True,check=True)
env['SGF_ACCESS_MODE']='apply';r=subprocess.run(['php','-r',php],input=payload,capture_output=True,text=True,env=env)
if r.returncode:raise RuntimeError('Transaction refused/rolled back; inspect current grants before retry; sensitive output withheld')
receipt=json.loads(r.stdout);receipt.update(checked_at=datetime.datetime.now(datetime.timezone.utc).isoformat(),manifest_sha256=MANIFEST_SHA,release=str(root.resolve()),backup=str(archive),backup_catalogue_and_sql_readable=True,restore_over_live_history=False)
f=backup/'receipt.json';f.write_text(json.dumps(receipt,ensure_ascii=False,indent=2));os.chmod(f,0o600);print(json.dumps(receipt))