Files
SGF/app/Shared/Installation/SyntheticProfiles.php
2026-11-11 09:00:17 +02:00

85 lines
7.4 KiB
PHP

<?php
namespace App\Shared\Installation;
use App\Models\User;
use App\Modules\Identity\Services\Audit;
use Illuminate\Support\Facades\DB;
/** Explicitly approved fixture installation. No runtime grants, role creation or legacy assignment. */
final class SyntheticProfiles
{
public static function policy():array
{
$p=json_decode(file_get_contents(base_path('docs/integration/sgf-8085-access-proposal.json')),true,64,JSON_THROW_ON_ERROR);
abort_unless(($p['approved']??false)===true&&($p['authorization']['decision']??null)==='autorizo'&&($p['scope']??null)===['tenant_id'=>1,'company_id'=>2,'unit_id'=>2],403);
return $p;
}
public static function fingerprint(array $p):string
{
return hash('sha256',json_encode(array_intersect_key($p,array_flip(['version','scope','profiles','existing_stock_profile','new_right_names','financial_source_policy'])),JSON_THROW_ON_ERROR));
}
public function install():array
{
CompositionSchema::identity();$p=self::policy();$pin=self::fingerprint($p);$scope=$p['scope'];
$known=[];
foreach([base_path('app/Modules/Services/DocumentPreview.php'),base_path('app/Modules/Purchasing/ReceiptRuntime.php'),...glob(database_path('migrations/*.php'))] as $file){preg_match_all("/'([a-z_]+\\.[a-z_.]+)'/",file_get_contents($file),$matches);$known=[...$known,...$matches[1]];}
foreach(\App\Modules\Services\ServicePermissions::AREAS as $area)foreach(\App\Modules\Services\ServicePermissions::ACTIONS as $action)$known[]=$area.'.'.$action;
foreach(['view','prepare','validate','post','correct_prepare','correct_validate','correct_post','period_prepare','period_validate','period_close'] as $action)$known[]='general_ledger.'.$action;
foreach(['view','prepare','confirm'] as $action)$known[]='workshop_stock.'.$action;
$known=[...$known,'stock.view','stock.operations.prepare','stock.operations.confirm'];
foreach($p['profiles'] as $profile)foreach($profile['rights'] as $right)abort_unless(in_array($right['permission'],$known,true),422,'Direito fora do catálogo existente/aprovado: '.$right['permission']);
foreach($p['existing_stock_profile']['rights'] as $right)abort_unless(in_array($right,$known,true),422);
return DB::transaction(function()use($p,$pin,$scope){
DB::selectOne("SELECT pg_advisory_xact_lock(hashtext('sgf-approved-presentation-profiles'))");
DB::table('companies')->where('id',$scope['company_id'])->where('tenant_id',$scope['tenant_id'])->lockForUpdate()->firstOrFail();
abort_unless(DB::table('units')->where(['tenant_id'=>$scope['tenant_id'],'company_id'=>$scope['company_id'],'id'=>$scope['unit_id']])->exists(),403);
if(!DB::getSchemaBuilder()->hasTable('sgf_presentation_profiles')){
DB::unprepared('CREATE TABLE sgf_presentation_profiles(profile_key varchar(80) PRIMARY KEY,user_id bigint NOT NULL UNIQUE REFERENCES users(id),tenant_id bigint NOT NULL,company_id bigint NOT NULL,unit_id bigint NOT NULL,label text NOT NULL,source_hash char(64) NOT NULL,created_at timestamptz NOT NULL DEFAULT now())');
DB::statement('REVOKE ALL ON TABLE sgf_presentation_profiles FROM PUBLIC');
DB::statement('CREATE TRIGGER sgf_presentation_profiles_append_only BEFORE UPDATE OR DELETE ON sgf_presentation_profiles FOR EACH ROW EXECUTE FUNCTION sgf_append_only_audit()');
}
$existing=DB::table('sgf_presentation_profiles')->get();
if($existing->isNotEmpty()){
abort_unless($existing->count()===count($p['profiles'])&&$existing->every(fn($v)=>hash_equals($v->source_hash,$pin)),409,'A matriz de perfis mudou; não ampliar acessos por inferência.');
return ['profiles'=>$existing->keyBy('profile_key')->map(fn($r)=>['user_id'=>(int)$r->user_id,'label'=>$r->label])->all(),'replayed'=>true,'source_hash'=>$pin];
}
$company=[];$unit=[];
foreach($p['profiles'] as $profile)foreach($profile['rights'] as $r){
abort_unless(preg_match('/^[a-z][a-z_.]+$/D',$r['permission'])&&in_array($r['scope'],['company','unit'],true),422);
if($r['scope']==='company')$company[]=$r['permission'];else $unit[]=$r['permission'];
}
$unit=[...$unit,...$p['existing_stock_profile']['rights']];
$old=DB::selectOne("SELECT pg_get_constraintdef(oid) value FROM pg_constraint WHERE conrelid='permission_grants'::regclass AND conname='permission_grants_scope_check'")->value;
if(!preg_match('/^CHECK \((.*)\)$/s',$old,$m))throw new \LogicException('Permission schema changed');
$literal=fn($values)=>implode(',',array_map(fn($v)=>"'".$v."'",array_unique($values)));
DB::statement('ALTER TABLE permission_grants DROP CONSTRAINT permission_grants_scope_check');
DB::statement('ALTER TABLE permission_grants ADD CONSTRAINT permission_grants_scope_check CHECK (('.$m[1].') OR (unit_id IS NULL AND permission IN ('.$literal($company).')) OR (unit_id IS NOT NULL AND permission IN ('.$literal($unit).')))');
$profiles=[];
foreach($p['profiles'] as $profile){
abort_if(User::where('email','sgf-'.$profile['id'].'-'.CompositionRuntime::physical()['composition'].'@example.test')->exists(),409,'Conta existente não registada nesta matriz.');
$user=User::create(['name'=>$profile['label'],'email'=>'sgf-'.$profile['id'].'-'.CompositionRuntime::physical()['composition'].'@example.test','password'=>bin2hex(random_bytes(32))]);
DB::table('company_memberships')->insert(['user_id'=>$user->id,'tenant_id'=>$scope['tenant_id'],'company_id'=>$scope['company_id']]);
foreach($profile['rights'] as $right)$this->grant($user->id,$scope,$right['permission'],$right['scope']==='unit');
DB::table('sgf_presentation_profiles')->insert(['profile_key'=>$profile['id'],'user_id'=>$user->id]+$scope+['label'=>$profile['label'],'source_hash'=>$pin]);
Audit::record('access.synthetic_profile_created',null,$scope['tenant_id'],$scope['company_id'],$scope['unit_id'],$user->id,['operator'=>'Autorização expressa do proprietário em10/11/2026','reason'=>'Perfil exclusivamente sintético da matriz aprovada.']);
$profiles[$profile['id']]=['user_id'=>$user->id,'label'=>$profile['label']];
}
$stock=(int)$p['existing_stock_profile']['user_id'];
abort_unless(DB::table('company_memberships')->where(['user_id'=>$stock,'tenant_id'=>$scope['tenant_id'],'company_id'=>$scope['company_id']])->exists(),403);
foreach($p['existing_stock_profile']['rights'] as $right)$this->grant($stock,$scope,$right,true);
return ['profiles'=>$profiles,'replayed'=>false,'source_hash'=>$pin];
});
}
private function grant(int $user,array $scope,string $right,bool $unit):void
{
$values=['tenant_id'=>$scope['tenant_id'],'company_id'=>$scope['company_id'],'unit_id'=>$unit?$scope['unit_id']:null,'user_id'=>$user,'permission'=>$right];
if(DB::table('permission_grants')->where($values)->exists())return;
DB::table('permission_grants')->insert($values);
Audit::record('access.granted_on_creation',null,$scope['tenant_id'],$scope['company_id'],$values['unit_id'],$user,['permission'=>$right,'operator'=>'Autorização10/11/2026','reason'=>'Apenas perfil QA e âmbito da matriz aprovada; sem acesso real.']);
}
}