Files
Investidor-V2/app/Services/DocumentManagementAccess.php
T

93 lines
3.9 KiB
PHP

<?php
namespace App\Services;
use App\Models\User;
use Illuminate\Database\Query\Builder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Str;
class DocumentManagementAccess
{
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(file_get_contents(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;
}
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),
]);
}
}