105 lines
4.4 KiB
PHP
105 lines
4.4 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\User;
|
|
use App\Support\CheckedFile;
|
|
use Illuminate\Database\Query\Builder;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Gate;
|
|
use Illuminate\Support\Str;
|
|
|
|
class DocumentManagementAccess
|
|
{
|
|
/** Existing editorial publication authority; it does not grant financial decision powers. */
|
|
public function catalogueEditor(?User $actor): bool
|
|
{
|
|
return $actor && $actor->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<array-key, mixed> $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),
|
|
]);
|
|
}
|
|
}
|