Files
compras/tests/Support/sgf-access-proof.php
T
2026-11-02 08:08:05 +02:00

142 lines
8.5 KiB
PHP

<?php
use App\Models\User;
use App\Modules\Identity\Services\IdentityWorkflow;
use App\Modules\Identity\Services\Permissions;
use App\Modules\Purchasing\BudgetAdapter;
use App\Shared\Context\CompanyContext;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Http\Request;
use Illuminate\Session\ArraySessionHandler;
use Illuminate\Session\Store;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Tempo\Purchasing\Delivery\InitialAuthority;
use Tempo\Purchasing\SgfCandidate\Schema;
$host = getenv('IR_SGF_HOST');
$root = getenv('IR_CANDIDATE_ROOT');
$overlay = getenv('IR_ACCESS_OVERLAY');
require $host.'/vendor/autoload.php';
// Class replacements exist only in this isolated process, not in the installed host.
require $overlay.'/app/Modules/Identity/Services/Permissions.php';
require $overlay.'/app/Modules/Identity/Services/IdentityWorkflow.php';
require $overlay.'/AccessOverlay.php';
require $overlay.'/InitialAuthority.php';
$app = require $host.'/bootstrap/app.php';
$app->make(Kernel::class)->bootstrap();
BudgetAdapter::guard();
require $host.'/packages/budget-control/tests/sgf-schema.php';
require $host.'/packages/budget-control/tests/host-support.php';
$f = budgetQaFixture('sgf');
require $root.'/integrations/sgf/internal-requisitions/Schema.php';
Schema::install(DB::connection());
$scope = $f['scope'];
$actor = $f['actor'];
$target = $f['validator'];
$checks = [];
$check = function (bool $ok, string $name) use (&$checks) {
if (! $ok) {
throw new RuntimeException($name);
}
$checks[] = $name;
};
$deny = function (callable $work, int $status) use ($check) {
try {
$work();
throw new RuntimeException('Unexpected permission success');
} catch (HttpException $e) {
$check($e->getStatusCode() === $status, 'denied '.$status);
}
};
$grant = function ($user, $right, $unit = null) use ($scope) {
DB::table('permission_grants')->insertOrIgnore(['tenant_id' => $scope['tenant_id'], 'company_id' => $scope['company_id'], 'unit_id' => $unit, 'user_id' => $user, 'permission' => $right, 'created_at' => now(), 'updated_at' => now()]);
};
$ctx = app(CompanyContext::class);
$ctx->selectFor($actor, $scope['tenant_id'], $scope['company_id'], $scope['unit_id']);
$request = function (array $data) use ($actor) {
$r = Request::create('/api/access', 'POST', $data);
$r->setUserResolver(fn () => User::findOrFail($actor));
$session = new Store('access-proof', new ArraySessionHandler(120));
$session->start();
$session->put('company_context.token', 'isolated-context-token');
$r->setLaravelSession($session);
$r->headers->set('X-SGF-Context', 'isolated-context-token');
return $r;
};
$p = app(Permissions::class);
$workflow = app(IdentityWorkflow::class);
$grant($actor, 'access.manage');
$grant($actor, 'unit.create');
$grant($target, 'access.manage');
$check(in_array('purchasing_requisitions.configure', $workflow->accessList($request([]))['catalogue']['unit'], true), 'existing administration lists new catalogue');
$unit = $workflow->createUnit($request(['name' => 'Synthetic new unit']));
$check(DB::table('permission_grants')->where('unit_id', $unit)->count() === 2, 'unit creation grants only legacy rights');
$initial = ['operation_key' => (string) Str::uuid(), 'reason' => 'Entrega inicial sintética explicitamente escolhida.', 'administrators' => [['user_id' => $actor, 'rights' => ['purchasing_requisitions.view', 'purchasing_requisitions.configure']]]];
$service = new InitialAuthority;
$deny(fn () => $service->initialise($request($initial)), 403);
config(['purchasing-access.initialisation_scopes' => [implode(':', $scope)]]);
$stale = $request($initial);
$stale->headers->set('X-SGF-Context', 'old-token');
$deny(fn () => $service->initialise($stale), 409);
$options = $service->options($request([]));
$check(count($options['administrators']) === 2 && ! $options['initialised'], 'nominative authorised initialisation choices');
$render = function (array $options, Request $request) use ($overlay) {
ob_start();
require $overlay.'/screen.php';
return ob_get_clean();
};
$html = $render($options, $request([]));
$check(str_contains($html, 'name="rights"') && str_contains($html, 'X-SGF-Context') && str_contains($html, 'X-CSRF-TOKEN'), 'initial authority screen selects explicit rights and includes context and CSRF');
$check(! str_contains($html, ' checked') && str_contains($html, 'crypto.randomUUID()'), 'screen has no selected grants and retains operation key on retry');
$deny(fn () => $service->initialise($request([...$initial, 'administrators' => [['user_id' => $actor, 'rights' => ['company.edit']]]])), 422);
$failAudit = true;
DB::listen(function ($query) use (&$failAudit) {
if ($failAudit && str_contains($query->sql, 'insert into "identity_audit"') && in_array('purchasing.access_initialised', $query->bindings, true)) {
$failAudit = false;
throw new RuntimeException('Injected authority audit failure');
}
});
try {
$service->initialise($request($initial));
throw new RuntimeException('Expected audit failure');
} catch (RuntimeException $error) {
if ($error->getMessage() !== 'Injected authority audit failure') {
throw $error;
}
}
$check(! $p->has($actor, $scope['tenant_id'], $scope['company_id'], 'purchasing_requisitions.configure', $scope['unit_id']), 'audit failure rolls back initial grants');
$before = DB::table('identity_audit')->count();
$check($service->initialise($request($initial))['initialised'], 'explicit nominated administrator initialisation');
$check(DB::table('identity_audit')->count() === $before + 1, 'initial authority audited');
$check(! $p->has($actor, $scope['tenant_id'], $scope['company_id'], 'purchasing_requisitions.approve.finance', $scope['unit_id']), 'unselected rights absent');
$check($service->initialise($request($initial))['replayed'], 'initialisation retry idempotent');
$options = $service->options($request([]));
$check($options['initialised'] && ! str_contains($render($options, $request([])), '<form'), 'initialised screen cannot offer a second initial grant');
$change = ['email' => User::findOrFail($target)->email, 'permission' => 'purchasing_requisitions.configure', 'unit_id' => $scope['unit_id'], 'reason' => 'Delegação sintética explícita.'];
$workflow->changeAccess($request($change), true);
$check($p->has($target, $scope['tenant_id'], $scope['company_id'], $change['permission'], $scope['unit_id']), 'normal administrative grant');
$workflow->changeAccess($request($change), false);
$check(! $p->has($target, $scope['tenant_id'], $scope['company_id'], $change['permission'], $scope['unit_id']), 'normal administrative revocation');
$deny(fn () => $workflow->changeAccess($request([...$change, 'permission' => 'purchasing_requisitions.approve.finance']), true), 403);
$grant($target, 'purchasing_requisitions.approve.finance', $scope['unit_id']);
$deny(fn () => $workflow->changeAccess($request([...$change, 'permission' => 'unit.view']), false), 403);
$check($p->has($target, $scope['tenant_id'], $scope['company_id'], 'unit.view', $scope['unit_id']), 'failed cascade preserves unit access');
$grant($actor, 'purchasing_requisitions.approve.finance', $scope['unit_id']);
// Fixture also assigned budget rights; actor needs each affected right to revoke the full unit.
foreach (DB::table('permission_grants')->where($scope)->where('user_id', $target)->pluck('permission') as $right) {
$grant($actor, $right, $scope['unit_id']);
}
$workflow->changeAccess($request([...$change, 'permission' => 'unit.view']), false);
$check(DB::table('permission_grants')->where($scope)->where('user_id', $target)->count() === 0, 'unit revocation removes derived catalogue rights');
DB::table('permission_grants')->where($scope)->where('user_id', $actor)->where('permission', 'purchasing_requisitions.configure')->delete();
$check($service->initialise($request($initial))['replayed'] && ! $p->has($actor, $scope['tenant_id'], $scope['company_id'], 'purchasing_requisitions.configure', $scope['unit_id']), 'initialisation replay never resurrects revoked rights');
$deny(fn () => $service->initialise($request([...$initial, 'operation_key' => (string) Str::uuid()])), 409);
DB::table('permission_grants')->where('tenant_id', $scope['tenant_id'])->where('company_id', $scope['company_id'])->where('user_id', $actor)->where('permission', 'access.manage')->delete();
$deny(fn () => $service->initialise($request($initial)), 403);
$deny(fn () => $service->options($request([])), 403);
echo json_encode(['passed' => true, 'checks' => $checks, 'installed' => false], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);