hasVerifiedEmail() && ! app(PortalProfileAccess::class)->has($actor, 'auditor') && ($this->full($actor) || $this->editorial($actor)); } public function full(User $actor): bool { return Gate::forUser($actor)->allows('manage-portal'); } public function limited(User $actor): bool { // Explicit Tempo authorisation; registration itself never grants access. return $actor->hasVerifiedEmail() && in_array(strtolower(trim($actor->email)), config('portal.document_managers', []), true) && app(PortalProfileAccess::class)->mfa($actor); } /** Restore the approved editorial operation without a global management grant. */ public function editorial(User $actor): bool { // Tempo's named editorial designation does not depend on whether the // site's technical reviewer reservation is enabled. return in_array(strtolower(trim($actor->email)), config('homologation_editorial.accounts', []), true) && app(PortalProfileAccess::class)->superuser($actor) && ! app(PortalProfileAccess::class)->has($actor, 'auditor') && app(PortalProfileAccess::class)->mfa($actor); } public function editorialResources(User $actor): Builder { abort_unless($this->editorial($actor), 403); return DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id') ->whereNull('d.deleted_at')->where('d.status', '!=', 'deleted') ->where(function ($query) use ($actor) { $query->where(function ($own) use ($actor) { $own->where('r.created_by', $actor->id)->where('d.uploaded_by', $actor->id) ->where('d.source_system', 'portal-services'); }); foreach (config('homologation_editorial.resources', []) as $item) { $query->orWhere(function ($listed) use ($item) { $listed->where('r.id', $item['id'])->where('d.sha256', $item['sha256']) ->where('d.external_id', $item['external_id']); }); } foreach (json_decode(CheckedFile::read(config_path('legacy_editorial_resources.json')), true, 512, JSON_THROW_ON_ERROR) as $item) { // These exact original bytes contain the legacy identity/source. // Destination upload IDs differ; the approved file scope does not. $query->orWhere('d.sha256', $item['sha256']); } }); } public function assertEditableResource(User $actor, int $id): void { abort_unless($this->full($actor) || $this->editorialResources($actor)->where('r.id', $id)->exists(), 403); } public function documents(User $actor): Builder { $query = DB::table('documents')->whereNull('deleted_at'); if (! $this->full($actor)) { $editorial = $this->editorial($actor); abort_unless($this->limited($actor) || $editorial, 403); $query->where(function ($scope) use ($actor, $editorial) { $scope->where(fn ($own) => $own->where('uploaded_by', $actor->id)->where('source_system', 'portal-services')); if ($editorial) { $scope->orWhereIn('id', $this->editorialResources($actor)->select('d.id')); } }); } return $query; } /** * @param array $data */ public function audit(User $actor, string $action, array $data): void { DB::table('audit_events')->insert([ 'source_system' => 'document-access-v1', 'entity_type' => 'authorization', 'actor_user_id' => $actor->id, 'action' => $action, 'data_classification' => 'restricted', 'correlation_id' => (string) Str::uuid(), 'after_data' => json_encode($data, JSON_THROW_ON_ERROR), ]); } }