authorizePlatformActor($actor); $scope = $this->resolver->validateScope($scope); $reason = $this->validatedText($reason, 'reason'); $ttlMinutes = $this->validatedTtl($ttlMinutes); $approvalMode = $this->resolver->approvalModeFor($workspace, $scope); $waiverReason = $waiverReason !== null ? trim($waiverReason) : null; if ($approvalMode === SupportAccessGrant::APPROVAL_MODE_OWNERLESS_WAIVER) { if (! $this->breakGlassSession->isActive()) { throw ValidationException::withMessages([ 'scope' => 'Ownerless workspace recovery requires active break-glass mode.', ]); } $waiverReason = $this->validatedText((string) $waiverReason, 'waiver_reason'); } if ($approvalMode !== SupportAccessGrant::APPROVAL_MODE_OWNERLESS_WAIVER) { $waiverReason = null; } $this->resolver->expireStaleActiveGrants($workspace); $existing = SupportAccessGrant::query() ->where('workspace_id', (int) $workspace->getKey()) ->where('requested_by_platform_user_id', (int) $actor->getKey()) ->where('scope', $scope) ->open() ->latest('id') ->first(); if ($existing instanceof SupportAccessGrant) { return $existing; } return DB::transaction(function () use ($workspace, $actor, $scope, $reason, $ttlMinutes, $approvalMode, $waiverReason): SupportAccessGrant { $now = CarbonImmutable::now(); $activeImmediately = in_array($approvalMode, [ SupportAccessGrant::APPROVAL_MODE_AUTO, SupportAccessGrant::APPROVAL_MODE_OWNERLESS_WAIVER, ], true); $grant = SupportAccessGrant::query()->create([ 'workspace_id' => (int) $workspace->getKey(), 'requested_by_platform_user_id' => (int) $actor->getKey(), 'approved_by_user_id' => null, 'scope' => $scope, 'status' => $activeImmediately ? SupportAccessGrant::STATUS_ACTIVE : SupportAccessGrant::STATUS_REQUESTED, 'approval_mode' => $approvalMode, 'reason' => $reason, 'waiver_reason' => $waiverReason, 'ttl_minutes' => $ttlMinutes, 'requested_at' => $now, 'approved_at' => null, 'starts_at' => $activeImmediately ? $now : null, 'expires_at' => $activeImmediately ? $now->addMinutes($ttlMinutes) : null, ]); $this->audit($grant, AuditActionId::SupportAccessRequested, $actor, 'Support access requested for '.$workspace->name); if ($approvalMode === SupportAccessGrant::APPROVAL_MODE_OWNERLESS_WAIVER) { $this->audit($grant, AuditActionId::SupportAccessOwnerlessWaiverUsed, $actor, 'Ownerless support-access waiver used for '.$workspace->name); } if ($activeImmediately) { $this->audit($grant, AuditActionId::SupportAccessActivated, $actor, 'Support access activated for '.$workspace->name); } return $grant; }); } public function approve(SupportAccessGrant $grant, User $actor): SupportAccessGrant { $this->authorizeWorkspaceOwnerApproval($grant, $actor); if ($grant->status !== SupportAccessGrant::STATUS_REQUESTED) { throw new DomainException('Only pending support-access requests may be approved.'); } if ($grant->scope !== SupportAccessGrant::SCOPE_WORKSPACE_RECOVERY) { throw new DomainException('Only recovery support-access requests require owner approval.'); } return DB::transaction(function () use ($grant, $actor): SupportAccessGrant { $now = CarbonImmutable::now(); $grant->forceFill([ 'status' => SupportAccessGrant::STATUS_ACTIVE, 'approved_by_user_id' => (int) $actor->getKey(), 'approved_at' => $now, 'starts_at' => $now, 'expires_at' => $now->addMinutes((int) $grant->ttl_minutes), ])->save(); $this->audit($grant->fresh(), AuditActionId::SupportAccessApproved, $actor, 'Support access approved for '.$grant->workspace->name); $this->audit($grant->fresh(), AuditActionId::SupportAccessActivated, $actor, 'Support access activated for '.$grant->workspace->name); return $grant->fresh(); }); } public function deny(SupportAccessGrant $grant, User $actor): SupportAccessGrant { $this->authorizeWorkspaceOwnerApproval($grant, $actor); if ($grant->status !== SupportAccessGrant::STATUS_REQUESTED) { throw new DomainException('Only pending support-access requests may be denied.'); } return DB::transaction(function () use ($grant, $actor): SupportAccessGrant { $grant->forceFill([ 'status' => SupportAccessGrant::STATUS_DENIED, 'denied_at' => CarbonImmutable::now(), ])->save(); $this->audit($grant->fresh(), AuditActionId::SupportAccessDenied, $actor, 'Support access denied for '.$grant->workspace->name); return $grant->fresh(); }); } public function end(SupportAccessGrant $grant, PlatformUser $actor): SupportAccessGrant { $this->authorizePlatformActor($actor); $this->resolver->expireStaleActiveGrants($grant->workspace); $grant = $grant->fresh(); if (! $grant instanceof SupportAccessGrant || $grant->status !== SupportAccessGrant::STATUS_ACTIVE) { throw new DomainException('Only active support-access grants may be ended.'); } return DB::transaction(function () use ($grant, $actor): SupportAccessGrant { $grant->forceFill([ 'status' => SupportAccessGrant::STATUS_ENDED, 'ended_at' => CarbonImmutable::now(), ])->save(); $this->audit($grant->fresh(), AuditActionId::SupportAccessEnded, $actor, 'Support access ended for '.$grant->workspace->name); return $grant->fresh(); }); } private function authorizePlatformActor(PlatformUser $actor): void { if (! $actor->hasCapability(PlatformCapabilities::SUPPORT_ACCESS_MANAGE)) { abort(403); } } private function authorizeWorkspaceOwnerApproval(SupportAccessGrant $grant, User $actor): void { $workspace = $grant->workspace; if (! $workspace instanceof Workspace) { abort(404); } if (! $this->workspaceCapabilities->isMember($actor, $workspace)) { abort(404); } if (! $this->workspaceCapabilities->can($actor, $workspace, Capabilities::WORKSPACE_SETTINGS_MANAGE)) { abort(403); } if ($this->workspaceCapabilities->getRole($actor, $workspace) !== WorkspaceRole::Owner) { abort(403); } } private function validatedText(string $value, string $field): string { $trimmed = trim($value); if (mb_strlen($trimmed) < 5) { throw ValidationException::withMessages([ $field => 'Enter at least 5 characters.', ]); } if (mb_strlen($trimmed) > 500) { throw ValidationException::withMessages([ $field => 'Enter no more than 500 characters.', ]); } return $trimmed; } private function validatedTtl(int $ttlMinutes): int { $max = $this->resolver->maxTtlMinutes(); if ($ttlMinutes < 1 || $ttlMinutes > $max) { throw ValidationException::withMessages([ 'ttl_minutes' => 'TTL must be between 1 and '.$max.' minutes.', ]); } return $ttlMinutes; } private function audit(SupportAccessGrant $grant, AuditActionId $action, User|PlatformUser $actor, string $summary): void { $workspace = $grant->workspace; if (! $workspace instanceof Workspace) { return; } $this->auditLogger->log( workspace: $workspace, action: $action, context: $this->resolver->auditContext($grant), actor: $actor, status: 'success', resourceType: 'support_access_grant', resourceId: (string) $grant->getKey(), actorType: $actor instanceof PlatformUser ? AuditActorType::Platform : null, targetLabel: $this->resolver->targetLabel($grant), summary: $summary, ); } }