TenantAtlas/apps/platform/app/Services/Auth/SupportAccessGrantResolver.php
Ahmed Darrazi 37105653a1
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m29s
feat(spec-276): implement support access governance — commit all changes
2026-05-05 22:17:14 +02:00

229 lines
8.3 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Auth;
use App\Models\SupportAccessGrant;
use App\Models\Workspace;
use App\Models\WorkspaceMembership;
use App\Services\Audit\WorkspaceAuditLogger;
use App\Support\Audit\AuditActionId;
use App\Support\Auth\WorkspaceRole;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Collection;
use InvalidArgumentException;
class SupportAccessGrantResolver
{
public function __construct(
private readonly WorkspaceAuditLogger $auditLogger,
) {}
public function maxTtlMinutes(): int
{
return max(1, (int) config('tenantpilot.support_access.max_ttl_minutes', 240));
}
public function validateScope(string $scope): string
{
$normalized = trim($scope);
if (! in_array($normalized, SupportAccessGrant::scopes(), true)) {
throw new InvalidArgumentException('Unsupported support-access scope.');
}
return $normalized;
}
public function approvalModeFor(Workspace $workspace, string $scope): string
{
$scope = $this->validateScope($scope);
if ($scope === SupportAccessGrant::SCOPE_AUDIT_VIEW) {
return SupportAccessGrant::APPROVAL_MODE_AUTO;
}
return $this->workspaceHasOwners($workspace)
? SupportAccessGrant::APPROVAL_MODE_OWNER_REQUIRED
: SupportAccessGrant::APPROVAL_MODE_OWNERLESS_WAIVER;
}
public function workspaceHasOwners(Workspace $workspace): bool
{
return WorkspaceMembership::query()
->where('workspace_id', (int) $workspace->getKey())
->where('role', WorkspaceRole::Owner->value)
->exists();
}
public function activeGrantFor(Workspace $workspace, string $scope): ?SupportAccessGrant
{
$scope = $this->validateScope($scope);
$this->expireStaleActiveGrants($workspace);
return SupportAccessGrant::query()
->with(['requester', 'approver'])
->where('workspace_id', (int) $workspace->getKey())
->where('scope', $scope)
->where('status', SupportAccessGrant::STATUS_ACTIVE)
->where('expires_at', '>', CarbonImmutable::now())
->latest('id')
->first();
}
public function activeRecoveryGrantFor(Workspace $workspace): ?SupportAccessGrant
{
return $this->activeGrantFor($workspace, SupportAccessGrant::SCOPE_WORKSPACE_RECOVERY);
}
public function hasActiveRecoveryGrant(Workspace $workspace): bool
{
return $this->activeRecoveryGrantFor($workspace) instanceof SupportAccessGrant;
}
public function currentOpenGrantFor(Workspace $workspace): ?SupportAccessGrant
{
$this->expireStaleActiveGrants($workspace);
return SupportAccessGrant::query()
->with(['requester', 'approver'])
->where('workspace_id', (int) $workspace->getKey())
->open()
->orderByRaw("CASE WHEN status = 'active' THEN 0 ELSE 1 END")
->orderByDesc('id')
->first();
}
/**
* @return Collection<int, SupportAccessGrant>
*/
public function pendingRecoveryRequestsFor(Workspace $workspace): Collection
{
$this->expireStaleActiveGrants($workspace);
return SupportAccessGrant::query()
->with(['requester'])
->where('workspace_id', (int) $workspace->getKey())
->where('scope', SupportAccessGrant::SCOPE_WORKSPACE_RECOVERY)
->where('status', SupportAccessGrant::STATUS_REQUESTED)
->orderBy('requested_at')
->get();
}
/**
* @return array<string, mixed>
*/
public function summaryFor(Workspace $workspace): array
{
$grant = $this->currentOpenGrantFor($workspace);
if (! $grant instanceof SupportAccessGrant) {
return [
'status' => 'none',
'status_label' => 'No active support access',
'status_color' => 'gray',
'scope' => null,
'scope_label' => null,
'grant_id' => null,
'reason' => null,
'requester_label' => null,
'approval_mode' => null,
'approval_label' => null,
'approver_label' => null,
'requested_ttl_label' => null,
'expires_at' => null,
'expires_label' => null,
'needs_break_glass' => false,
];
}
return [
'status' => (string) $grant->status,
'status_label' => SupportAccessGrant::statusLabel((string) $grant->status),
'status_color' => $grant->status === SupportAccessGrant::STATUS_ACTIVE ? 'success' : 'warning',
'scope' => (string) $grant->scope,
'scope_label' => SupportAccessGrant::scopeLabel((string) $grant->scope),
'grant_id' => (int) $grant->getKey(),
'reason' => (string) $grant->reason,
'requester_label' => $grant->requester?->name ?? $grant->requester?->email ?? 'Platform support',
'approval_mode' => (string) $grant->approval_mode,
'approval_label' => SupportAccessGrant::approvalModeLabel((string) $grant->approval_mode),
'approver_label' => $grant->approver?->name ?? $grant->approver?->email,
'requested_ttl_label' => $grant->ttl_minutes.' minutes',
'expires_at' => $grant->expires_at,
'expires_label' => $grant->expires_at?->diffForHumans(),
'needs_break_glass' => $grant->needsBreakGlass(),
];
}
public function expireStaleActiveGrants(?Workspace $workspace = null): void
{
$query = SupportAccessGrant::query()
->with(['workspace', 'requester'])
->where('status', SupportAccessGrant::STATUS_ACTIVE)
->whereNotNull('expires_at')
->where('expires_at', '<=', CarbonImmutable::now());
if ($workspace instanceof Workspace) {
$query->where('workspace_id', (int) $workspace->getKey());
}
$query->get()->each(function (SupportAccessGrant $grant): void {
$grant->forceFill([
'status' => SupportAccessGrant::STATUS_EXPIRED,
])->save();
$workspace = $grant->workspace;
if (! $workspace instanceof Workspace) {
return;
}
$this->auditLogger->log(
workspace: $workspace,
action: AuditActionId::SupportAccessExpired,
context: $this->auditContext($grant),
actor: null,
status: 'success',
resourceType: 'support_access_grant',
resourceId: (string) $grant->getKey(),
targetLabel: $this->targetLabel($grant),
summary: 'Support access expired for '.$workspace->name,
);
});
}
/**
* @return array<string, mixed>
*/
public function auditContext(SupportAccessGrant $grant, array $extra = []): array
{
return [
'support_access_grant_id' => (int) $grant->getKey(),
'workspace_id' => (int) $grant->workspace_id,
'scope' => (string) $grant->scope,
'scope_label' => SupportAccessGrant::scopeLabel((string) $grant->scope),
'status' => (string) $grant->status,
'approval_mode' => (string) $grant->approval_mode,
'reason' => (string) $grant->reason,
'waiver_reason' => $grant->waiver_reason,
'ttl_minutes' => (int) $grant->ttl_minutes,
'requested_by_platform_user_id' => (int) $grant->requested_by_platform_user_id,
'approved_by_user_id' => is_numeric($grant->approved_by_user_id) ? (int) $grant->approved_by_user_id : null,
'requested_at' => $grant->requested_at?->toISOString(),
'approved_at' => $grant->approved_at?->toISOString(),
'starts_at' => $grant->starts_at?->toISOString(),
'expires_at' => $grant->expires_at?->toISOString(),
'ended_at' => $grant->ended_at?->toISOString(),
'denied_at' => $grant->denied_at?->toISOString(),
] + $extra;
}
public function targetLabel(SupportAccessGrant $grant): string
{
return SupportAccessGrant::scopeLabel((string) $grant->scope).' #'.$grant->getKey();
}
}