53 lines
7.3 KiB
PHP
53 lines
7.3 KiB
PHP
<?php
|
|
// Only the two existing QA books. No server, database, PostgreSQL role or public grant.
|
|
$book=$argv[1]??'regression';require __DIR__.'/sgf-single-bootstrap.php';
|
|
$policy=json_decode(file_get_contents($repo.'/docs/integration/sgf-stock-final-access-proposal.json'),true);
|
|
if(($policy['approved']??false)!==true)throw new LogicException('Explicit approval required');
|
|
$profiles=json_decode(file_get_contents($s['root'].'/single-profiles.json'),true)['profiles'];$reviewer=$profiles['workshop_verify']['user_id'];$ledger=json_decode(file_get_contents($s['root'].'/single-native-ledger.json'),true);
|
|
$native->transaction(function()use($native,$policy,$tag){
|
|
// Legacy QA role mode ignores organization profiles. Preserve every existing role verbatim
|
|
// before enabling the native profile mechanism required by the approved seven functions.
|
|
if(!$native->table('organization_access')->where(['tenant_id'=>1,'active'=>true])->exists()){
|
|
foreach($native->table('users')->where('tenant_id',1)->get(['id','purchasing_role']) as $u){
|
|
$rights=$u->purchasing_role===null||$u->purchasing_role==='configured'?[]:[$u->purchasing_role];
|
|
if($rights===[])continue;$name='SGF-PRESERVE-QA-ROLE-'.$tag.'-'.$u->id;
|
|
$id=$native->table('organization_profiles')->where(['tenant_id'=>1,'name'=>$name])->value('id');
|
|
if(!$id){$id=$native->table('organization_profiles')->insertGetId(['tenant_id'=>1,'name'=>$name,'permissions'=>json_encode($rights),'version'=>1,'created_at'=>now(),'updated_at'=>now()]);$native->table('organization_memberships')->insert(['tenant_id'=>1,'user_id'=>$u->id,'profile_id'=>$id,'maintenance'=>false,'created_at'=>now(),'updated_at'=>now()]);}
|
|
}
|
|
$native->table('organization_access')->updateOrInsert(['tenant_id'=>1],['active'=>true,'version'=>1,'created_at'=>now(),'updated_at'=>now()]);
|
|
$native->table('audit_logs')->insert(['tenant_id'=>1,'user_id'=>6,'action'=>'sgf.stock.final.qa_profile_mode','before'=>json_encode(['active'=>false]),'after'=>json_encode(['active'=>true,'existing_roles_preserved'=>true,'approval'=>$policy['approval']]),'created_at'=>now()]);
|
|
}
|
|
if(!$native->getSchemaBuilder()->hasTable('sgf_stock_transit_events')){
|
|
$native->unprepared("CREATE TABLE sgf_stock_transit_events(id bigserial PRIMARY KEY,tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,transfer_key uuid NOT NULL,command_key uuid NOT NULL UNIQUE,request_hash text NOT NULL,action text NOT NULL CHECK(action IN ('dispatch','receive','return')),actor_id bigint NOT NULL,host_actor_id bigint NOT NULL,stock_item_id bigint NOT NULL REFERENCES stock_items(id),source_warehouse_id bigint NOT NULL REFERENCES warehouses(id),destination_warehouse_id bigint NOT NULL REFERENCES warehouses(id),quantity_milli bigint NOT NULL CHECK(quantity_milli>0),movement_id bigint NOT NULL UNIQUE REFERENCES stock_movements(id),reason text NOT NULL,snapshot jsonb NOT NULL,created_at timestamp NOT NULL,CHECK(source_warehouse_id<>destination_warehouse_id))");
|
|
$native->unprepared("CREATE UNIQUE INDEX sgf_transit_one_dispatch ON sgf_stock_transit_events(tenant_id,company_id,unit_id,transfer_key) WHERE action='dispatch'");
|
|
$native->unprepared('CREATE INDEX sgf_transit_scope ON sgf_stock_transit_events(tenant_id,company_id,unit_id,transfer_key,id)');
|
|
$native->unprepared('CREATE TRIGGER sgf_transit_immutable BEFORE UPDATE OR DELETE ON sgf_stock_transit_events FOR EACH ROW EXECUTE FUNCTION gl_append_only()');
|
|
$native->statement('REVOKE ALL ON TABLE sgf_stock_transit_events FROM PUBLIC');$native->statement('REVOKE ALL ON SEQUENCE sgf_stock_transit_events_id_seq FROM PUBLIC');
|
|
}
|
|
foreach($policy['existing_native_functions_to_grant_only_in_qa'] as $grant){
|
|
$name='SGF-STOCK-FINAL-QA-'.$tag.'-'.$grant['native_user_id'];$profile=$native->table('organization_profiles')->where(['tenant_id'=>1,'name'=>$name])->first();
|
|
if($profile){if(json_decode($profile->permissions,true)!==$grant['functions'])throw new LogicException('Preserve differing native profile');continue;}
|
|
$id=$native->table('organization_profiles')->insertGetId(['tenant_id'=>1,'name'=>$name,'permissions'=>json_encode($grant['functions']),'version'=>1,'created_at'=>now(),'updated_at'=>now()]);
|
|
$native->table('organization_memberships')->insert(['tenant_id'=>1,'user_id'=>$grant['native_user_id'],'profile_id'=>$id,'maintenance'=>false,'created_at'=>now(),'updated_at'=>now()]);
|
|
$native->table('audit_logs')->insert(['tenant_id'=>1,'user_id'=>$grant['native_user_id'],'action'=>'sgf.stock.final.qa_access','before'=>null,'after'=>json_encode(['approval'=>$policy['approval'],'functions'=>$grant['functions'],'real_access'=>false]),'created_at'=>now()]);
|
|
}
|
|
});
|
|
$host->transaction(function()use($host,$f,$reviewer,$policy){
|
|
$old=$host->selectOne("SELECT pg_get_constraintdef(oid) value FROM pg_constraint WHERE conrelid='permission_grants'::regclass AND conname='permission_grants_scope_check'")->value;
|
|
if(!str_contains($old,'stock.transfers.receive')){
|
|
if(!preg_match('/^CHECK \((.*)\)$/s',$old,$m))throw new LogicException('Grant schema changed');
|
|
$host->statement('ALTER TABLE permission_grants DROP CONSTRAINT permission_grants_scope_check');
|
|
$host->statement("ALTER TABLE permission_grants ADD CONSTRAINT permission_grants_scope_check CHECK ((".$m[1].") OR (unit_id IS NOT NULL AND permission IN ('stock.transfers.receive','stock.lots.review')))");
|
|
}
|
|
foreach($policy['new_host_rights'] as $grant){$values=$f['scope']+['user_id'=>$reviewer,'permission'=>$grant['permission']];if($host->table('permission_grants')->where($values)->exists())continue;
|
|
$host->table('permission_grants')->insert($values);App\Modules\Identity\Services\Audit::record('access.granted_on_creation',null,1,2,2,$reviewer,['permission'=>$grant['permission'],'reason'=>$policy['approval'],'limits'=>$grant,'real_access'=>false]);}
|
|
});
|
|
$bindingsFile=$s['root'].'/single-bindings.json';$b=json_decode(file_get_contents($bindingsFile),true);$key='1:2:2:'.$reviewer;copy($bindingsFile,$work.'/single-bindings-before.json');
|
|
if(!in_array($b['bindings'][$key]['actor_id'],[2,3],true))throw new LogicException('Unexpected reviewer binding');$b['bindings'][$key]['actor_id']=3;
|
|
file_put_contents($bindingsFile,json_encode($b,JSON_PRETTY_PRINT));
|
|
$value=['version'=>$policy['version'],'composition'=>$tag,'scope'=>$f['scope'],'dispatcher_host_id'=>$f['validator'],'reviewer_host_id'=>$reviewer,'dispatcher_native_id'=>6,'reviewer_native_id'=>3,'source_warehouse_id'=>$ledger['warehouse_id'],'destination_warehouse_id'=>$nf['warehouse']];
|
|
if(is_file($s['root'].'/stock-final-binding.json')){$old=json_decode(file_get_contents($s['root'].'/stock-final-binding.json'),true);foreach(['lot_item_id','transfer_item_id'] as $field)if(isset($old[$field]))$value[$field]=$old[$field];}
|
|
file_put_contents($s['root'].'/stock-final-binding.json',json_encode($value,JSON_PRETTY_PRINT));
|
|
$pins=json_decode(file_get_contents($s['root'].'/single-binding-pins.json'),true);foreach(['single-bindings.json','stock-final-binding.json'] as $name)$pins[$name]=hash_file('sha256',$s['root'].'/'.$name);file_put_contents($s['root'].'/single-binding-pins.json',json_encode($pins,JSON_PRETTY_PRINT));
|
|
echo json_encode(['passed'=>true,'book'=>$book,'approved_qa_rights'=>2,'native_functions'=>7,'new_pg_roles'=>0,'public_grants'=>0,'proof_directory'=>$work]).PHP_EOL;
|