45 lines
5.2 KiB
PHP
45 lines
5.2 KiB
PHP
<?php
|
|
|
|
// Runs only inside the disposable reproduction after its authorised lifecycle.
|
|
// Inherits the explicit test scope/clients; never creates grants in the host.
|
|
$catalogue=$call($actor,'/api/accesses');
|
|
$check($catalogue['status']===200&&in_array('purchasing_requisitions.prepare',$catalogue['data']['catalogue']['unit'],true),'native administration receives complete unit catalogue');
|
|
$check(($catalogue['data']['catalogue_labels']['purchasing_requisitions.prepare']??'')==='Preparar e submeter requisições','native administration receives canonical server labels');
|
|
$check($call($head,'/api/accesses')['status']===403,'reviewer cannot inspect administrative catalogue without management right');
|
|
$check($call($actor,'/api/accesses','POST',['email'=>\App\Models\User::findOrFail($head)->email,'unit_id'=>$scope['unit_id'],'permission'=>'purchasing_budget.consume','reason'=>'Forbidden delegation outside own synthetic authority'])['status']===403,'forged delegation of an unowned catalogue right refused');
|
|
|
|
$uiManifest=json_decode(file_get_contents($clone.'/public/build/manifest.json'),true,32,JSON_THROW_ON_ERROR);
|
|
foreach($uiManifest as $chunk)foreach([$chunk['file'],...($chunk['css']??[])] as $file){
|
|
$asset=$anonymous->get('http://127.0.0.1:'.$port.'/build/'.$file);
|
|
$check($asset->getStatusCode()===200&&strlen((string)$asset->getBody())>100,'packaged frontend dependency served before authentication');
|
|
}
|
|
$main=$anonymous->get('http://127.0.0.1:'.$port.'/');
|
|
$check($main->getStatusCode()===200&&str_contains((string)$main->getBody(),'/build/modules/internal-requisitions/assets/'),'native SGF entry renders the packaged frontend manifest');
|
|
$check($anonymous->get('http://127.0.0.1:'.$port.'/build/modules/internal-requisitions/assets/missing.js')->getStatusCode()===404,'undeclared frontend asset refused');
|
|
|
|
// Reproduce destination-style broad inherited ACLs on NEW objects, then prove
|
|
// the proposed map narrows them without touching any of the 87 prior tables.
|
|
$beforeAcl=[];
|
|
foreach($inventory['tables'] as $table)$beforeAcl[$table]=$db->selectOne('SELECT relacl::text AS acl FROM pg_class WHERE oid=?::regclass',['public.'.$table])->acl;
|
|
foreach(\Tempo\Purchasing\Delivery\SqlPrivileges::TABLES as $table=>$rights)$db->statement('GRANT SELECT,INSERT,UPDATE,DELETE ON TABLE public.'.$q($table).' TO '.$q($runtime));
|
|
foreach(\Tempo\Purchasing\Delivery\SqlPrivileges::plan($runtime)['statements'] as $sql)$db->statement($sql);
|
|
$check(!$rt->selectOne("SELECT has_table_privilege(current_user,'cb_centres','UPDATE') AS allowed")->allowed&&!$rt->selectOne("SELECT has_table_privilege(current_user,'cb_audit','DELETE') AS allowed")->allowed,'minimal plan removes inherited update and delete on new append-only sources');
|
|
$check(!$rt->selectOne("SELECT has_table_privilege(current_user,'ir_requests','TRUNCATE') AS allowed")->allowed,'runtime cannot truncate requisitions');
|
|
foreach($beforeAcl as $table=>$acl)$check($db->selectOne('SELECT relacl::text AS acl FROM pg_class WHERE oid=?::regclass',['public.'.$table])->acl===$acl,'existing table ACL preserved '.$table);
|
|
try{$rt->transaction(fn()=>$rt->statement('CREATE TABLE forbidden_runtime_ddl (id integer)'));throw new LogicException('Unexpected runtime DDL');}
|
|
catch(\Illuminate\Database\QueryException $e){$check($e->errorInfo[0]==='42501','actual runtime CREATE denied');}
|
|
try{$rt->transaction(fn()=>$rt->statement('ALTER TABLE permission_grants ADD COLUMN forbidden_runtime integer'));throw new LogicException('Unexpected runtime ALTER');}
|
|
catch(\Illuminate\Database\QueryException $e){$check($e->errorInfo[0]==='42501','actual runtime ALTER of identity catalogue denied');}
|
|
|
|
$badCentre=['operation_key'=>(string)\Illuminate\Support\Str::uuid(),'code'=>'NOT-ALLOWED','reason'=>'Validation in the isolated reproduction only'];
|
|
$check($call($head,'/api/internal-requisitions/centres','POST',$badCentre)['status']===403,'centre creation requires own configuration and budget rights');
|
|
$check($call($actor,'/api/internal-requisitions/centres','POST',$badCentre+['company_id'=>999999])['status']===422,'centre context cannot be forged');
|
|
$check(\Illuminate\Support\Facades\DB::table('cb_centres')->count()===1,'refused centre operations leave original source intact');
|
|
foreach(['equipment','part'] as $unsupported){
|
|
$r=$call($actor,'/api/internal-requisitions/save','POST',['operation_key'=>(string)\Illuminate\Support\Str::uuid(),'cost_center_id'=>$f['centre'],'priority'=>'normal','needed_by'=>date('Y-m-d',time()+864000),'justification'=>'Unsupported type must remain outside this increment.','items'=>[['description'=>'Unsupported source','category'=>'Excluded','purchase_type'=>$unsupported,'quantity'=>'1.000','unit'=>'UN','unit_price'=>'1.00','tax_rate'=>'0.00']]]);
|
|
$check($r['status']===422,'unsupported requisition type refused '.$unsupported);
|
|
}
|
|
$data=json_decode(\Illuminate\Support\Facades\DB::table('ir_requests')->where('id',$id)->value('data'),true,32,JSON_THROW_ON_ERROR);
|
|
$check(array_column($data['items'],'purchase_type')===['consumable','service','other'],'approved lifecycle covers the three supported basic types');
|
|
$check(\Illuminate\Support\Facades\DB::table('ir_requests')->count()===1&&\Illuminate\Support\Facades\DB::table('cb_budget_entries')->where('kind','reserve')->count()===1,'unsupported types create neither a second request nor reservation');
|