TenantAtlas/apps/platform/tests/Unit/System/SupportAccessGrantResolverTest.php
ahmido 1e0f21365b PR: 276-support-access-governance → platform-dev (#332)
Automated PR created via MCP by Copilot on user request: "pr gegen platform-dev".

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #332
2026-05-05 21:54:26 +00:00

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();
});