isClosed() ? self::WORKSPACE_POSTURE_CLOSED : self::WORKSPACE_POSTURE_OPEN; } public function tenantPosture(ManagedEnvironment $tenant): string { return $tenant->isRemovedFromWorkspace() ? self::TENANT_POSTURE_REMOVED : self::TENANT_POSTURE_ACTIVE; } public function assertWorkspaceMutationAllowed(Workspace $workspace): void { if (! $workspace->isClosed()) { return; } throw ValidationException::withMessages([ 'workspace' => 'This workspace is closed. Reopen it before making workspace or tenant changes.', ]); } public function closeWorkspace(Workspace $workspace, PlatformUser $actor, string $reason): Workspace { $this->authorizePlatformDirectoryManagement($actor); $reason = $this->normalizeReason($reason); return DB::transaction(function () use ($workspace, $actor, $reason): Workspace { $workspace = Workspace::query()->lockForUpdate()->findOrFail($workspace->getKey()); if ($workspace->isClosed()) { throw ValidationException::withMessages([ 'reason' => 'This workspace is already closed.', ]); } $workspace->forceFill([ 'closed_at' => now(), 'closed_by_platform_user_id' => (int) $actor->getKey(), 'closed_reason' => $reason, ])->save(); ManagedEnvironment::query() ->where('workspace_id', (int) $workspace->getKey()) ->where('is_current', true) ->update(['is_current' => false]); $this->auditLogger->log( workspace: $workspace, action: AuditActionId::WorkspaceClosed, context: [ 'reason' => $reason, 'before_status' => self::WORKSPACE_POSTURE_OPEN, 'after_status' => self::WORKSPACE_POSTURE_CLOSED, 'closed_at' => $workspace->closed_at?->toISOString(), ], actor: $actor, resourceType: 'workspace', resourceId: (string) $workspace->getKey(), targetLabel: (string) $workspace->name, summary: 'Workspace closed for '.$workspace->name, ); return $workspace; }); } public function reopenWorkspace(Workspace $workspace, PlatformUser $actor, string $reason): Workspace { $this->authorizePlatformDirectoryManagement($actor); $reason = $this->normalizeReason($reason); return DB::transaction(function () use ($workspace, $actor, $reason): Workspace { $workspace = Workspace::query()->lockForUpdate()->findOrFail($workspace->getKey()); if (! $workspace->isClosed()) { throw ValidationException::withMessages([ 'reason' => 'This workspace is already open.', ]); } $previousClosedAt = $workspace->closed_at; $previousReason = $workspace->closureReason(); $previousActorId = $workspace->closed_by_platform_user_id; $workspace->forceFill([ 'closed_at' => null, 'closed_by_platform_user_id' => null, 'closed_reason' => null, ])->save(); $this->auditLogger->log( workspace: $workspace, action: AuditActionId::WorkspaceReopened, context: [ 'reason' => $reason, 'before_status' => self::WORKSPACE_POSTURE_CLOSED, 'after_status' => self::WORKSPACE_POSTURE_OPEN, 'previous_closed_at' => $previousClosedAt?->toISOString(), 'previous_closed_reason' => $previousReason, 'previous_closed_by_platform_user_id' => $previousActorId, ], actor: $actor, resourceType: 'workspace', resourceId: (string) $workspace->getKey(), targetLabel: (string) $workspace->name, summary: 'Workspace reopened for '.$workspace->name, ); return $workspace; }); } public function removeTenantFromWorkspace(ManagedEnvironment $tenant, User $actor, string $reason): ManagedEnvironment { $reason = $this->normalizeReason($reason); return DB::transaction(function () use ($tenant, $actor, $reason): ManagedEnvironment { $tenant = ManagedEnvironment::query() ->with(['workspace']) ->withTrashed() ->lockForUpdate() ->findOrFail($tenant->getKey()); if (! $this->capabilityResolver->can($actor, $tenant, Capabilities::TENANT_DELETE)) { throw new AuthorizationException('You are not allowed to remove this tenant from the workspace.'); } if ($tenant->workspace?->isClosed()) { $this->assertWorkspaceMutationAllowed($tenant->workspace); } if ($tenant->isRemovedFromWorkspace()) { throw ValidationException::withMessages([ 'reason' => 'This tenant is already removed from the workspace.', ]); } $tenant->forceFill([ 'removed_from_workspace_at' => now(), 'removed_from_workspace_by_user_id' => (int) $actor->getKey(), 'removed_from_workspace_reason' => $reason, 'is_current' => false, ])->save(); app(WorkspaceContext::class)->clearRememberedTenantContext(); $this->auditLogger->logTenantLifecycleAction( tenant: $tenant, action: AuditActionId::TenantRemovedFromWorkspace, context: [ 'reason' => $reason, 'before_status' => self::TENANT_POSTURE_ACTIVE, 'after_status' => self::TENANT_POSTURE_REMOVED, 'removed_from_workspace_at' => $tenant->removed_from_workspace_at?->toISOString(), ], actor: $actor, summary: 'ManagedEnvironment removed from workspace for '.$tenant->name, ); return $tenant; }); } public function restoreTenantToWorkspace(ManagedEnvironment $tenant, User $actor, string $reason): ManagedEnvironment { $reason = $this->normalizeReason($reason); return DB::transaction(function () use ($tenant, $actor, $reason): ManagedEnvironment { $tenant = ManagedEnvironment::query() ->with(['workspace']) ->withTrashed() ->lockForUpdate() ->findOrFail($tenant->getKey()); if (! $this->capabilityResolver->can($actor, $tenant, Capabilities::TENANT_DELETE)) { throw new AuthorizationException('You are not allowed to restore this tenant to the workspace.'); } if ($tenant->workspace?->isClosed()) { $this->assertWorkspaceMutationAllowed($tenant->workspace); } if (! $tenant->isRemovedFromWorkspace()) { throw ValidationException::withMessages([ 'reason' => 'This tenant is not removed from the workspace.', ]); } $previousRemovedAt = $tenant->removed_from_workspace_at; $previousReason = $tenant->workspaceRemovalReason(); $previousActorId = $tenant->removed_from_workspace_by_user_id; $tenant->forceFill([ 'removed_from_workspace_at' => null, 'removed_from_workspace_by_user_id' => null, 'removed_from_workspace_reason' => null, ])->save(); $this->auditLogger->logTenantLifecycleAction( tenant: $tenant, action: AuditActionId::TenantRestoredToWorkspace, context: [ 'reason' => $reason, 'before_status' => self::TENANT_POSTURE_REMOVED, 'after_status' => self::TENANT_POSTURE_ACTIVE, 'previous_removed_from_workspace_at' => $previousRemovedAt?->toISOString(), 'previous_removed_from_workspace_reason' => $previousReason, 'previous_removed_from_workspace_by_user_id' => $previousActorId, ], actor: $actor, summary: 'ManagedEnvironment restored to workspace for '.$tenant->name, ); return $tenant; }); } private function authorizePlatformDirectoryManagement(PlatformUser $actor): void { if ($actor->hasCapability(PlatformCapabilities::DIRECTORY_MANAGE)) { return; } throw new AuthorizationException('You are not allowed to manage workspace lifecycle.'); } private function normalizeReason(string $reason): string { $reason = trim($reason); if (mb_strlen($reason) < 5) { throw ValidationException::withMessages([ 'reason' => 'Provide a reason with at least 5 characters.', ]); } if (mb_strlen($reason) > 2000) { throw ValidationException::withMessages([ 'reason' => 'Provide a reason with 2000 characters or fewer.', ]); } return $reason; } }