Files
compras/integrations/sgf/access/InitialAuthority.php
T
2026-11-02 08:08:05 +02:00

94 lines
6.0 KiB
PHP

<?php
namespace Tempo\Purchasing\Delivery;
use App\Modules\Identity\Services\Audit;
use App\Modules\Identity\Services\Permissions;
use App\Shared\Context\CompanyContext;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
/** Explicit initial delivery to nominated administrators, never automatic profile inheritance. */
final class InitialAuthority
{
public function options(Request $request): array
{
$context = app(CompanyContext::class);
$scope = $context->identifiers() + ['unit_id' => $context->unitId()];
$actor = $request->user()->id;
abort_unless($scope['unit_id'] !== null && $context->userId() === $actor, 403);
abort_unless(in_array(implode(':', $scope), config('purchasing-access.initialisation_scopes', []), true), 403);
$permissions = app(Permissions::class);
$context->selectFor($actor, $scope['tenant_id'], $scope['company_id'], $scope['unit_id']);
$permissions->require($actor, $scope['tenant_id'], $scope['company_id'], 'access.manage');
$members = DB::table('company_memberships as m')->join('users as u', 'u.id', '=', 'm.user_id')
->where('m.tenant_id', $scope['tenant_id'])->where('m.company_id', $scope['company_id'])->get(['u.id', 'u.name'])->filter(function ($member) use ($permissions, $scope) {
return $permissions->has($member->id, $scope['tenant_id'], $scope['company_id'], 'company.view')
&& $permissions->has($member->id, $scope['tenant_id'], $scope['company_id'], 'access.manage')
&& $permissions->has($member->id, $scope['tenant_id'], $scope['company_id'], 'unit.view', $scope['unit_id']);
})->values()->all();
return ['scope' => $scope, 'administrators' => $members, 'rights' => AccessOverlay::RIGHTS, 'labels' => AccessOverlay::LABELS,
'initialised' => DB::table('identity_audit')->where($scope)->where('action', 'purchasing.access_initialised')->exists()];
}
public function initialise(Request $request): array
{
$data = $request->validate([
'operation_key' => 'required|uuid', 'reason' => 'required|string|min:10|max:500',
'administrators' => 'required|array|min:1|max:20', 'administrators.*' => 'array:user_id,rights',
'administrators.*.user_id' => 'required|integer|distinct|min:1',
'administrators.*.rights' => 'required|array|min:1', 'administrators.*.rights.*' => 'required|string|distinct',
]);
abort_unless(array_diff(array_keys($request->all()), array_keys($data)) === [], 422);
$context = app(CompanyContext::class);
$scope = $context->identifiers() + ['unit_id' => $context->unitId()];
$actor = $request->user()->id;
abort_unless($scope['unit_id'] !== null && $context->userId() === $actor, 403);
// Installed configuration explicitly permits initialisation for this exact scope.
$scopeKey = implode(':', $scope);
abort_unless(in_array($scopeKey, config('purchasing-access.initialisation_scopes', []), true), 403);
foreach ($data['administrators'] as &$admin) {
abort_unless(array_diff($admin['rights'], AccessOverlay::RIGHTS) === [], 422);
sort($admin['rights']);
}
unset($admin);
usort($data['administrators'], fn ($a, $b) => $a['user_id'] <=> $b['user_id']);
$hash = hash('sha256', json_encode($data, JSON_THROW_ON_ERROR));
return DB::transaction(function () use ($request, $scope, $actor, $context, $data, $hash) {
DB::table('companies')->where('id', $scope['company_id'])->where('tenant_id', $scope['tenant_id'])->lockForUpdate()->firstOrFail();
$context->selectFor($actor, $scope['tenant_id'], $scope['company_id'], $scope['unit_id']);
$permissions = app(Permissions::class);
$permissions->require($actor, $scope['tenant_id'], $scope['company_id'], 'access.manage');
if ($request->hasSession()) {
$token = (string) $request->session()->get('company_context.token', '');
abort_unless($token !== '' && hash_equals($token, (string) $request->header('X-SGF-Context')), 409);
}
$old = DB::table('identity_audit')->where($scope)->where('action', 'purchasing.access_initialised')->first();
if ($old) {
$details = json_decode($old->details, true, 32, JSON_THROW_ON_ERROR);
abort_unless($old->actor_id === $actor && ($details['event_id'] ?? null) === $hash, 409, 'Autoridade inicial já entregue; use a gestão normal de acessos.');
return ['initialised' => true, 'replayed' => true];
}
$grants = [];
foreach ($data['administrators'] as $admin) {
foreach (['company.view', 'access.manage'] as $right) {
$permissions->require($admin['user_id'], $scope['tenant_id'], $scope['company_id'], $right);
}
$permissions->require($admin['user_id'], $scope['tenant_id'], $scope['company_id'], 'unit.view', $scope['unit_id']);
abort_unless(DB::table('company_memberships')->where(['user_id' => $admin['user_id'], 'tenant_id' => $scope['tenant_id'], 'company_id' => $scope['company_id']])->exists(), 403);
foreach ($admin['rights'] as $right) {
abort_unless(in_array($right, Permissions::UNIT, true), 503, 'Catálogo operacional não preparado.');
DB::table('permission_grants')->insertOrIgnore($scope + ['user_id' => $admin['user_id'], 'permission' => $right, 'created_at' => now(), 'updated_at' => now()]);
$grants[] = ['user_id' => $admin['user_id'], 'permission' => $right];
}
}
Audit::record('purchasing.access_initialised', $actor, $scope['tenant_id'], $scope['company_id'], $scope['unit_id'], details: ['reason' => $data['reason'], 'event_id' => $hash, 'grants' => $grants]);
return ['initialised' => true, 'replayed' => false];
});
}
}