577 lines
24 KiB
PHP
577 lines
24 KiB
PHP
<?php
|
|
|
|
namespace MintyPHP\Tests\Service\Access;
|
|
|
|
use MintyPHP\Service\Access\PermissionGateway;
|
|
use MintyPHP\Service\Access\PermissionService;
|
|
use MintyPHP\Service\Access\UserAuthorizationPolicy;
|
|
use MintyPHP\Service\Directory\DirectoryScopeGateway;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
class UserAuthorizationPolicyTest extends TestCase
|
|
{
|
|
public function testAdminViewRequiresUsersViewPermission(): void
|
|
{
|
|
$permissionGateway = $this->createMock(PermissionGateway::class);
|
|
$permissionGateway->expects($this->once())
|
|
->method('userHas')
|
|
->with(13, PermissionService::USERS_VIEW)
|
|
->willReturn(false);
|
|
|
|
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
|
$policy = new UserAuthorizationPolicy($permissionGateway, $scopeGateway);
|
|
$decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_VIEW, [
|
|
'actor_user_id' => 13,
|
|
]);
|
|
|
|
$this->assertFalse($decision->isAllowed());
|
|
$this->assertSame(403, $decision->status());
|
|
$this->assertSame('forbidden', $decision->error());
|
|
}
|
|
|
|
public function testAdminCreateRequiresUsersCreatePermission(): void
|
|
{
|
|
$permissionGateway = $this->createMock(PermissionGateway::class);
|
|
$permissionGateway->expects($this->once())
|
|
->method('userHas')
|
|
->with(13, PermissionService::USERS_CREATE)
|
|
->willReturn(false);
|
|
|
|
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
|
$policy = new UserAuthorizationPolicy($permissionGateway, $scopeGateway);
|
|
$decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_CREATE, [
|
|
'actor_user_id' => 13,
|
|
]);
|
|
|
|
$this->assertFalse($decision->isAllowed());
|
|
$this->assertSame(403, $decision->status());
|
|
}
|
|
|
|
public function testAdminActivateRequiresUsersUpdatePermission(): void
|
|
{
|
|
$permissionGateway = $this->createMock(PermissionGateway::class);
|
|
$permissionGateway->expects($this->once())
|
|
->method('userHas')
|
|
->with(11, PermissionService::USERS_UPDATE)
|
|
->willReturn(false);
|
|
|
|
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
|
$scopeGateway->expects($this->never())->method('canAccess');
|
|
|
|
$policy = new UserAuthorizationPolicy($permissionGateway, $scopeGateway);
|
|
$decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_ACTIVATE, [
|
|
'actor_user_id' => 11,
|
|
'target_user_id' => 22,
|
|
]);
|
|
|
|
$this->assertFalse($decision->isAllowed());
|
|
$this->assertSame(403, $decision->status());
|
|
$this->assertSame('forbidden', $decision->error());
|
|
}
|
|
|
|
public function testAdminActivateReturnsPermissionDeniedWhenTenantScopeFails(): void
|
|
{
|
|
$permissionGateway = $this->createMock(PermissionGateway::class);
|
|
$permissionGateway->method('userHas')
|
|
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 11 && $permission === PermissionService::USERS_UPDATE);
|
|
|
|
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
|
$scopeGateway->expects($this->once())
|
|
->method('canAccess')
|
|
->with('users', 22, 11)
|
|
->willReturn(false);
|
|
|
|
$policy = new UserAuthorizationPolicy($permissionGateway, $scopeGateway);
|
|
$decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_ACTIVATE, [
|
|
'actor_user_id' => 11,
|
|
'target_user_id' => 22,
|
|
]);
|
|
|
|
$this->assertFalse($decision->isAllowed());
|
|
$this->assertSame(403, $decision->status());
|
|
$this->assertSame('permission_denied', $decision->error());
|
|
}
|
|
|
|
public function testAdminDeactivateAllowsSelfWithoutScopeLookup(): void
|
|
{
|
|
$permissionGateway = $this->createMock(PermissionGateway::class);
|
|
$permissionGateway->method('userHas')
|
|
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 9 && $permission === PermissionService::USERS_UPDATE);
|
|
|
|
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
|
$scopeGateway->expects($this->never())->method('canAccess');
|
|
|
|
$policy = new UserAuthorizationPolicy($permissionGateway, $scopeGateway);
|
|
$decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_DEACTIVATE, [
|
|
'actor_user_id' => 9,
|
|
'target_user_id' => 9,
|
|
]);
|
|
|
|
$this->assertTrue($decision->isAllowed());
|
|
}
|
|
|
|
public function testAdminBulkDeleteRequiresUsersDeletePermission(): void
|
|
{
|
|
$permissionGateway = $this->createMock(PermissionGateway::class);
|
|
$permissionGateway->expects($this->once())
|
|
->method('userHas')
|
|
->with(7, PermissionService::USERS_DELETE)
|
|
->willReturn(false);
|
|
|
|
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
|
$policy = new UserAuthorizationPolicy($permissionGateway, $scopeGateway);
|
|
|
|
$decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_BULK, [
|
|
'actor_user_id' => 7,
|
|
'bulk_action' => 'delete',
|
|
]);
|
|
|
|
$this->assertFalse($decision->isAllowed());
|
|
$this->assertSame(403, $decision->status());
|
|
}
|
|
|
|
public function testAdminDeleteReturnsPermissionDeniedWhenTenantScopeFails(): void
|
|
{
|
|
$permissionGateway = $this->createMock(PermissionGateway::class);
|
|
$permissionGateway->method('userHas')
|
|
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 7 && $permission === PermissionService::USERS_DELETE);
|
|
|
|
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
|
$scopeGateway->expects($this->once())
|
|
->method('canAccess')
|
|
->with('users', 17, 7)
|
|
->willReturn(false);
|
|
|
|
$policy = new UserAuthorizationPolicy($permissionGateway, $scopeGateway);
|
|
$decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_DELETE, [
|
|
'actor_user_id' => 7,
|
|
'target_user_id' => 17,
|
|
]);
|
|
|
|
$this->assertFalse($decision->isAllowed());
|
|
$this->assertSame(403, $decision->status());
|
|
$this->assertSame('permission_denied', $decision->error());
|
|
}
|
|
|
|
public function testAdminAccessPdfBulkRequiresPermission(): void
|
|
{
|
|
$permissionGateway = $this->createMock(PermissionGateway::class);
|
|
$permissionGateway->expects($this->once())
|
|
->method('userHas')
|
|
->with(21, PermissionService::USERS_ACCESS_PDF)
|
|
->willReturn(false);
|
|
|
|
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
|
$scopeGateway->expects($this->never())->method('canAccess');
|
|
|
|
$policy = new UserAuthorizationPolicy($permissionGateway, $scopeGateway);
|
|
$decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_ACCESS_PDF_BULK, [
|
|
'actor_user_id' => 21,
|
|
]);
|
|
|
|
$this->assertFalse($decision->isAllowed());
|
|
$this->assertSame(403, $decision->status());
|
|
}
|
|
|
|
public function testAdminApiTokensManageRequiresScopeForOtherUsers(): void
|
|
{
|
|
$permissionGateway = $this->createMock(PermissionGateway::class);
|
|
$permissionGateway->method('userHas')
|
|
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 8 && $permission === PermissionService::API_TOKENS_MANAGE);
|
|
|
|
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
|
$scopeGateway->expects($this->once())
|
|
->method('canAccess')
|
|
->with('users', 18, 8)
|
|
->willReturn(false);
|
|
|
|
$policy = new UserAuthorizationPolicy($permissionGateway, $scopeGateway);
|
|
$decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_API_TOKENS_MANAGE, [
|
|
'actor_user_id' => 8,
|
|
'target_user_id' => 18,
|
|
]);
|
|
|
|
$this->assertFalse($decision->isAllowed());
|
|
$this->assertSame('permission_denied', $decision->error());
|
|
}
|
|
|
|
public function testAdminSendAccessAllowsSelfWithSelfUpdatePermission(): void
|
|
{
|
|
$permissionGateway = $this->createMock(PermissionGateway::class);
|
|
$permissionGateway->method('userHas')
|
|
->willReturnCallback(static function (int $userId, string $permission): bool {
|
|
if ($userId !== 30) {
|
|
return false;
|
|
}
|
|
|
|
if ($permission === PermissionService::USERS_SELF_UPDATE) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
|
$scopeGateway->expects($this->never())->method('canAccess');
|
|
|
|
$policy = new UserAuthorizationPolicy($permissionGateway, $scopeGateway);
|
|
$decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_SEND_ACCESS, [
|
|
'actor_user_id' => 30,
|
|
'target_user_id' => 30,
|
|
]);
|
|
|
|
$this->assertTrue($decision->isAllowed());
|
|
}
|
|
|
|
public function testEditContextAllowsOwnAccountWithUsersSelfUpdate(): void
|
|
{
|
|
$permissionGateway = $this->createMock(PermissionGateway::class);
|
|
$permissionGateway->method('userHas')
|
|
->willReturnCallback(static function (int $userId, string $permission): bool {
|
|
if ($userId !== 30) {
|
|
return false;
|
|
}
|
|
|
|
return $permission === PermissionService::USERS_SELF_UPDATE;
|
|
});
|
|
|
|
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
|
$scopeGateway->expects($this->never())->method('canAccess');
|
|
$scopeGateway->expects($this->once())
|
|
->method('getUserTenantIds')
|
|
->with(30)
|
|
->willReturn([2, 3]);
|
|
|
|
$policy = new UserAuthorizationPolicy($permissionGateway, $scopeGateway);
|
|
$decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_EDIT_CONTEXT, [
|
|
'actor_user_id' => 30,
|
|
'target_user_id' => 30,
|
|
]);
|
|
|
|
$capabilities = $decision->attribute('capabilities', []);
|
|
$this->assertTrue($decision->isAllowed());
|
|
$this->assertTrue($capabilities['is_own_account'] ?? false);
|
|
$this->assertTrue($capabilities['can_edit_user'] ?? false);
|
|
$this->assertTrue($capabilities['can_view_page'] ?? false);
|
|
$this->assertTrue($capabilities['can_view_security_artifacts'] ?? false);
|
|
$this->assertSame([2, 3], $capabilities['allowed_tenant_ids'] ?? []);
|
|
}
|
|
|
|
public function testEditContextReturnsPermissionDeniedWhenTargetIsOutOfScope(): void
|
|
{
|
|
$permissionGateway = $this->createMock(PermissionGateway::class);
|
|
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
|
$scopeGateway->expects($this->once())
|
|
->method('canAccess')
|
|
->with('users', 66, 33)
|
|
->willReturn(false);
|
|
|
|
$policy = new UserAuthorizationPolicy($permissionGateway, $scopeGateway);
|
|
$decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_EDIT_CONTEXT, [
|
|
'actor_user_id' => 33,
|
|
'target_user_id' => 66,
|
|
]);
|
|
|
|
$this->assertFalse($decision->isAllowed());
|
|
$this->assertSame(403, $decision->status());
|
|
$this->assertSame('permission_denied', $decision->error());
|
|
}
|
|
|
|
public function testEditContextHidesSecurityArtifactsWithoutAuditPermission(): void
|
|
{
|
|
$permissionGateway = $this->createMock(PermissionGateway::class);
|
|
$permissionGateway->method('userHas')
|
|
->willReturnCallback(static function (int $userId, string $permission): bool {
|
|
if ($userId !== 9) {
|
|
return false;
|
|
}
|
|
|
|
return $permission === PermissionService::USERS_VIEW;
|
|
});
|
|
|
|
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
|
$scopeGateway->expects($this->once())
|
|
->method('canAccess')
|
|
->with('users', 55, 9)
|
|
->willReturn(true);
|
|
$scopeGateway->expects($this->once())
|
|
->method('getUserTenantIds')
|
|
->with(9)
|
|
->willReturn([1]);
|
|
|
|
$policy = new UserAuthorizationPolicy($permissionGateway, $scopeGateway);
|
|
$decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_EDIT_CONTEXT, [
|
|
'actor_user_id' => 9,
|
|
'target_user_id' => 55,
|
|
]);
|
|
|
|
$this->assertTrue($decision->isAllowed());
|
|
$this->assertFalse((bool) ($decision->attribute('capabilities', [])['can_view_security_artifacts'] ?? true));
|
|
}
|
|
|
|
public function testEditContextShowsSecurityArtifactsWithAuditPermission(): void
|
|
{
|
|
$permissionGateway = $this->createMock(PermissionGateway::class);
|
|
$permissionGateway->method('userHas')
|
|
->willReturnCallback(static function (int $userId, string $permission): bool {
|
|
if ($userId !== 9) {
|
|
return false;
|
|
}
|
|
|
|
return in_array($permission, [PermissionService::USERS_VIEW, PermissionService::USERS_VIEW_AUDIT], true);
|
|
});
|
|
|
|
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
|
$scopeGateway->expects($this->once())
|
|
->method('canAccess')
|
|
->with('users', 55, 9)
|
|
->willReturn(true);
|
|
$scopeGateway->expects($this->once())
|
|
->method('getUserTenantIds')
|
|
->with(9)
|
|
->willReturn([1]);
|
|
|
|
$policy = new UserAuthorizationPolicy($permissionGateway, $scopeGateway);
|
|
$decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_EDIT_CONTEXT, [
|
|
'actor_user_id' => 9,
|
|
'target_user_id' => 55,
|
|
]);
|
|
|
|
$this->assertTrue($decision->isAllowed());
|
|
$this->assertTrue((bool) ($decision->attribute('capabilities', [])['can_view_security_artifacts'] ?? false));
|
|
}
|
|
|
|
public function testEditSubmitRejectsUserWithoutEditCapability(): void
|
|
{
|
|
$permissionGateway = $this->createMock(PermissionGateway::class);
|
|
$permissionGateway->method('userHas')
|
|
->willReturnCallback(static function (int $userId, string $permission): bool {
|
|
if ($userId !== 9) {
|
|
return false;
|
|
}
|
|
|
|
return $permission === PermissionService::USERS_VIEW;
|
|
});
|
|
|
|
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
|
$scopeGateway->expects($this->once())
|
|
->method('canAccess')
|
|
->with('users', 55, 9)
|
|
->willReturn(true);
|
|
$scopeGateway->expects($this->once())
|
|
->method('getUserTenantIds')
|
|
->with(9)
|
|
->willReturn([1]);
|
|
|
|
$policy = new UserAuthorizationPolicy($permissionGateway, $scopeGateway);
|
|
$decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_EDIT_SUBMIT, [
|
|
'actor_user_id' => 9,
|
|
'target_user_id' => 55,
|
|
]);
|
|
|
|
$this->assertFalse($decision->isAllowed());
|
|
$this->assertSame(403, $decision->status());
|
|
$this->assertSame('forbidden', $decision->error());
|
|
}
|
|
|
|
public function testAvatarUploadAllowsOwnAccountWithSelfUpdate(): void
|
|
{
|
|
$permissionGateway = $this->createMock(PermissionGateway::class);
|
|
$permissionGateway->method('userHas')
|
|
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 19 && $permission === PermissionService::USERS_SELF_UPDATE);
|
|
|
|
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
|
$scopeGateway->expects($this->never())->method('canAccess');
|
|
|
|
$policy = new UserAuthorizationPolicy($permissionGateway, $scopeGateway);
|
|
$decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_AVATAR_UPLOAD, [
|
|
'actor_user_id' => 19,
|
|
'target_user_id' => 19,
|
|
]);
|
|
|
|
$this->assertTrue($decision->isAllowed());
|
|
}
|
|
|
|
public function testAvatarDeleteReturnsPermissionDeniedOutOfScope(): void
|
|
{
|
|
$permissionGateway = $this->createMock(PermissionGateway::class);
|
|
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
|
$scopeGateway->expects($this->once())
|
|
->method('canAccess')
|
|
->with('users', 42, 7)
|
|
->willReturn(false);
|
|
|
|
$policy = new UserAuthorizationPolicy($permissionGateway, $scopeGateway);
|
|
$decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_AVATAR_DELETE, [
|
|
'actor_user_id' => 7,
|
|
'target_user_id' => 42,
|
|
]);
|
|
|
|
$this->assertFalse($decision->isAllowed());
|
|
$this->assertSame('permission_denied', $decision->error());
|
|
}
|
|
|
|
public function testAvatarViewAllowsInScopeAddressBookViewer(): void
|
|
{
|
|
$permissionGateway = $this->createMock(PermissionGateway::class);
|
|
$permissionGateway->method('userHas')
|
|
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 5 && $permission === PermissionService::ADDRESS_BOOK_VIEW);
|
|
|
|
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
|
$scopeGateway->expects($this->once())
|
|
->method('canAccess')
|
|
->with('users', 70, 5)
|
|
->willReturn(true);
|
|
|
|
$policy = new UserAuthorizationPolicy($permissionGateway, $scopeGateway);
|
|
$decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_AVATAR_VIEW, [
|
|
'actor_user_id' => 5,
|
|
'target_user_id' => 70,
|
|
]);
|
|
|
|
$this->assertTrue($decision->isAllowed());
|
|
}
|
|
|
|
public function testAvatarViewRejectsInScopeUserWithoutAnyRelevantPermission(): void
|
|
{
|
|
$permissionGateway = $this->createMock(PermissionGateway::class);
|
|
$permissionGateway->method('userHas')->willReturn(false);
|
|
|
|
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
|
$scopeGateway->expects($this->once())
|
|
->method('canAccess')
|
|
->with('users', 70, 5)
|
|
->willReturn(true);
|
|
|
|
$policy = new UserAuthorizationPolicy($permissionGateway, $scopeGateway);
|
|
$decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_AVATAR_VIEW, [
|
|
'actor_user_id' => 5,
|
|
'target_user_id' => 70,
|
|
]);
|
|
|
|
$this->assertFalse($decision->isAllowed());
|
|
$this->assertSame('forbidden', $decision->error());
|
|
}
|
|
|
|
public function testApiShowReturnsNotFoundWhenTenantScopeFails(): void
|
|
{
|
|
$permissionGateway = $this->createMock(PermissionGateway::class);
|
|
$permissionGateway->method('userHas')
|
|
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 4 && $permission === PermissionService::USERS_VIEW);
|
|
|
|
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
|
$scopeGateway->expects($this->once())
|
|
->method('canAccess')
|
|
->with('users', 44, 4)
|
|
->willReturn(false);
|
|
|
|
$policy = new UserAuthorizationPolicy($permissionGateway, $scopeGateway);
|
|
$decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_API_USERS_SHOW_GET, [
|
|
'actor_user_id' => 4,
|
|
'target_user_id' => 44,
|
|
'scoped_tenant_id' => null,
|
|
]);
|
|
|
|
$this->assertFalse($decision->isAllowed());
|
|
$this->assertSame(404, $decision->status());
|
|
$this->assertSame('not_found', $decision->error());
|
|
}
|
|
|
|
public function testApiShowReturnsNotFoundForScopedTokenCrossTenantResource(): void
|
|
{
|
|
$permissionGateway = $this->createMock(PermissionGateway::class);
|
|
$permissionGateway->method('userHas')
|
|
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 4 && $permission === PermissionService::USERS_VIEW);
|
|
|
|
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
|
$scopeGateway->expects($this->once())
|
|
->method('canAccess')
|
|
->with('users', 44, 4)
|
|
->willReturn(true);
|
|
$scopeGateway->expects($this->once())
|
|
->method('resourceBelongsToTenant')
|
|
->with('users', 44, 2)
|
|
->willReturn(false);
|
|
|
|
$policy = new UserAuthorizationPolicy($permissionGateway, $scopeGateway);
|
|
$decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_API_USERS_SHOW_GET, [
|
|
'actor_user_id' => 4,
|
|
'target_user_id' => 44,
|
|
'scoped_tenant_id' => 2,
|
|
]);
|
|
|
|
$this->assertFalse($decision->isAllowed());
|
|
$this->assertSame(404, $decision->status());
|
|
$this->assertSame('not_found', $decision->error());
|
|
}
|
|
|
|
public function testApiCreateForScopedTokenRejectsOutOfScopeTenantIds(): void
|
|
{
|
|
$permissionGateway = $this->createMock(PermissionGateway::class);
|
|
$permissionGateway->method('userHas')
|
|
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 5 && $permission === PermissionService::USERS_CREATE);
|
|
|
|
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
|
$policy = new UserAuthorizationPolicy($permissionGateway, $scopeGateway);
|
|
|
|
$decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_API_USERS_INDEX_POST, [
|
|
'actor_user_id' => 5,
|
|
'scoped_tenant_id' => 9,
|
|
'input' => ['tenant_ids' => [9, 10]],
|
|
]);
|
|
|
|
$this->assertFalse($decision->isAllowed());
|
|
$this->assertSame(403, $decision->status());
|
|
$this->assertSame('tenant_scoped_token_forbidden', $decision->error());
|
|
}
|
|
|
|
public function testApiCreateForScopedTokenEnforcesScopedTenantInInput(): void
|
|
{
|
|
$permissionGateway = $this->createMock(PermissionGateway::class);
|
|
$permissionGateway->method('userHas')
|
|
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 5 && $permission === PermissionService::USERS_CREATE);
|
|
|
|
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
|
$policy = new UserAuthorizationPolicy($permissionGateway, $scopeGateway);
|
|
|
|
$decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_API_USERS_INDEX_POST, [
|
|
'actor_user_id' => 5,
|
|
'scoped_tenant_id' => 9,
|
|
'input' => ['first_name' => 'Ada', 'tenant_ids' => [9]],
|
|
]);
|
|
|
|
$this->assertTrue($decision->isAllowed());
|
|
$this->assertSame([9], $decision->attribute('input')['tenant_ids'] ?? []);
|
|
$this->assertSame(9, $decision->attribute('input')['primary_tenant_id'] ?? null);
|
|
}
|
|
|
|
public function testApiUpdateForScopedTokenRejectsOutOfScopePrimaryTenant(): void
|
|
{
|
|
$permissionGateway = $this->createMock(PermissionGateway::class);
|
|
$permissionGateway->method('userHas')
|
|
->willReturnCallback(static fn (int $userId, string $permission): bool => $userId === 6 && $permission === PermissionService::USERS_UPDATE);
|
|
|
|
$scopeGateway = $this->createMock(DirectoryScopeGateway::class);
|
|
$scopeGateway->expects($this->once())
|
|
->method('canAccess')
|
|
->with('users', 42, 6)
|
|
->willReturn(true);
|
|
$scopeGateway->expects($this->once())
|
|
->method('resourceBelongsToTenant')
|
|
->with('users', 42, 2)
|
|
->willReturn(true);
|
|
|
|
$policy = new UserAuthorizationPolicy($permissionGateway, $scopeGateway);
|
|
$decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_API_USERS_SHOW_UPDATE, [
|
|
'actor_user_id' => 6,
|
|
'target_user_id' => 42,
|
|
'scoped_tenant_id' => 2,
|
|
'input' => ['primary_tenant_id' => 7],
|
|
]);
|
|
|
|
$this->assertFalse($decision->isAllowed());
|
|
$this->assertSame(403, $decision->status());
|
|
$this->assertSame('tenant_scoped_token_forbidden', $decision->error());
|
|
}
|
|
}
|