166 lines
6.3 KiB
PHP
166 lines
6.3 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Models\AuditLog;
|
|
use App\Models\PlatformUser;
|
|
use App\Models\SupportAccessGrant;
|
|
use App\Models\Tenant;
|
|
use App\Models\User;
|
|
use App\Models\Workspace;
|
|
use App\Models\WorkspaceMembership;
|
|
use App\Services\Auth\BreakGlassSession;
|
|
use App\Services\Auth\SupportAccessGrantManager;
|
|
use App\Services\Auth\SupportAccessGrantResolver;
|
|
use App\Support\Audit\AuditActionId;
|
|
use App\Support\Auth\PlatformCapabilities;
|
|
use App\Support\Auth\WorkspaceRole;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function (): void {
|
|
Tenant::factory()->create([
|
|
'tenant_id' => null,
|
|
'external_id' => 'platform',
|
|
'name' => 'Platform',
|
|
]);
|
|
|
|
config()->set('tenantpilot.break_glass.enabled', true);
|
|
config()->set('tenantpilot.break_glass.ttl_minutes', 15);
|
|
config()->set('tenantpilot.support_access.max_ttl_minutes', 120);
|
|
});
|
|
|
|
function spec276_resolver_platform_user(): PlatformUser
|
|
{
|
|
return PlatformUser::factory()->create([
|
|
'capabilities' => [
|
|
PlatformCapabilities::ACCESS_SYSTEM_PANEL,
|
|
PlatformCapabilities::DIRECTORY_VIEW,
|
|
PlatformCapabilities::SUPPORT_ACCESS_MANAGE,
|
|
PlatformCapabilities::USE_BREAK_GLASS,
|
|
],
|
|
]);
|
|
}
|
|
|
|
it('auto activates audit-view support access and dedupes overlapping requests from the same platform actor', function (): void {
|
|
$workspace = Workspace::factory()->create();
|
|
$platformUser = spec276_resolver_platform_user();
|
|
|
|
$grant = app(SupportAccessGrantManager::class)->request(
|
|
workspace: $workspace,
|
|
actor: $platformUser,
|
|
scope: SupportAccessGrant::SCOPE_AUDIT_VIEW,
|
|
reason: 'Review support case audit history',
|
|
ttlMinutes: 45,
|
|
);
|
|
|
|
$duplicate = app(SupportAccessGrantManager::class)->request(
|
|
workspace: $workspace,
|
|
actor: $platformUser,
|
|
scope: SupportAccessGrant::SCOPE_AUDIT_VIEW,
|
|
reason: 'Review support case audit history again',
|
|
ttlMinutes: 60,
|
|
);
|
|
|
|
expect($grant->status)->toBe(SupportAccessGrant::STATUS_ACTIVE)
|
|
->and($grant->approval_mode)->toBe(SupportAccessGrant::APPROVAL_MODE_AUTO)
|
|
->and($grant->starts_at)->not->toBeNull()
|
|
->and($grant->expires_at)->not->toBeNull()
|
|
->and($duplicate->getKey())->toBe($grant->getKey())
|
|
->and(SupportAccessGrant::query()->count())->toBe(1);
|
|
|
|
expect(AuditLog::query()
|
|
->where('workspace_id', (int) $workspace->getKey())
|
|
->whereIn('action', [
|
|
AuditActionId::SupportAccessRequested->value,
|
|
AuditActionId::SupportAccessActivated->value,
|
|
])
|
|
->count())->toBe(2);
|
|
});
|
|
|
|
it('keeps workspace-recovery support access pending until a workspace owner approves it', function (): void {
|
|
$workspace = Workspace::factory()->create();
|
|
$platformUser = spec276_resolver_platform_user();
|
|
$owner = User::factory()->create();
|
|
|
|
WorkspaceMembership::factory()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'user_id' => (int) $owner->getKey(),
|
|
'role' => WorkspaceRole::Owner->value,
|
|
]);
|
|
|
|
$grant = app(SupportAccessGrantManager::class)->request(
|
|
workspace: $workspace,
|
|
actor: $platformUser,
|
|
scope: SupportAccessGrant::SCOPE_WORKSPACE_RECOVERY,
|
|
reason: 'Recover workspace ownership safely',
|
|
ttlMinutes: 30,
|
|
);
|
|
|
|
expect($grant->status)->toBe(SupportAccessGrant::STATUS_REQUESTED)
|
|
->and($grant->approval_mode)->toBe(SupportAccessGrant::APPROVAL_MODE_OWNER_REQUIRED)
|
|
->and($grant->starts_at)->toBeNull()
|
|
->and($grant->expires_at)->toBeNull();
|
|
|
|
$approved = app(SupportAccessGrantManager::class)->approve($grant, $owner);
|
|
|
|
expect($approved->status)->toBe(SupportAccessGrant::STATUS_ACTIVE)
|
|
->and($approved->approved_by_user_id)->toBe((int) $owner->getKey())
|
|
->and($approved->expires_at)->not->toBeNull()
|
|
->and(app(SupportAccessGrantResolver::class)->activeRecoveryGrantFor($workspace)?->getKey())->toBe($grant->getKey());
|
|
});
|
|
|
|
it('expires stale active grants before returning active support access', function (): void {
|
|
$workspace = Workspace::factory()->create();
|
|
$grant = SupportAccessGrant::factory()->expired()->create([
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'scope' => SupportAccessGrant::SCOPE_WORKSPACE_RECOVERY,
|
|
]);
|
|
|
|
expect(app(SupportAccessGrantResolver::class)->activeRecoveryGrantFor($workspace))->toBeNull()
|
|
->and($grant->fresh()->status)->toBe(SupportAccessGrant::STATUS_EXPIRED);
|
|
|
|
expect(AuditLog::query()
|
|
->where('workspace_id', (int) $workspace->getKey())
|
|
->where('action', AuditActionId::SupportAccessExpired->value)
|
|
->exists())->toBeTrue();
|
|
});
|
|
|
|
it('requires break-glass and a waiver reason for ownerless workspace recovery', function (): void {
|
|
$workspace = Workspace::factory()->create();
|
|
$platformUser = spec276_resolver_platform_user();
|
|
|
|
expect(fn () => app(SupportAccessGrantManager::class)->request(
|
|
workspace: $workspace,
|
|
actor: $platformUser,
|
|
scope: SupportAccessGrant::SCOPE_WORKSPACE_RECOVERY,
|
|
reason: 'Recover ownerless workspace',
|
|
ttlMinutes: 30,
|
|
waiverReason: 'No owners exist',
|
|
))->toThrow(ValidationException::class);
|
|
|
|
$this->actingAs($platformUser, 'platform');
|
|
app(BreakGlassSession::class)->start($platformUser, 'Recover ownerless workspace');
|
|
|
|
$grant = app(SupportAccessGrantManager::class)->request(
|
|
workspace: $workspace,
|
|
actor: $platformUser,
|
|
scope: SupportAccessGrant::SCOPE_WORKSPACE_RECOVERY,
|
|
reason: 'Recover ownerless workspace',
|
|
ttlMinutes: 30,
|
|
waiverReason: 'Validated no workspace owners remain',
|
|
);
|
|
|
|
expect($grant->status)->toBe(SupportAccessGrant::STATUS_ACTIVE)
|
|
->and($grant->approval_mode)->toBe(SupportAccessGrant::APPROVAL_MODE_OWNERLESS_WAIVER)
|
|
->and($grant->waiver_reason)->toBe('Validated no workspace owners remain');
|
|
|
|
expect(AuditLog::query()
|
|
->where('workspace_id', (int) $workspace->getKey())
|
|
->where('action', AuditActionId::SupportAccessOwnerlessWaiverUsed->value)
|
|
->whereJsonContains('metadata->waiver_reason', 'Validated no workspace owners remain')
|
|
->exists())->toBeTrue();
|
|
});
|