229 lines
8.3 KiB
PHP
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();
|
|
}
|
|
}
|