2026-02-24 08:49:40 +01:00
|
|
|
<?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
|
|
|
|
|
{
|
2026-03-04 15:56:58 +01:00
|
|
|
use AuthorizationPolicyTestSupport;
|
|
|
|
|
|
2026-02-24 08:49:40 +01:00
|
|
|
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,
|
|
|
|
|
]);
|
|
|
|
|
|
2026-03-04 15:56:58 +01:00
|
|
|
$this->assertForbiddenDecision($decision);
|
2026-02-24 08:49:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
]);
|
|
|
|
|
|
2026-03-04 15:56:58 +01:00
|
|
|
$this->assertDeniedDecision($decision, 403, 'forbidden');
|
2026-02-24 08:49:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
]);
|
|
|
|
|
|
2026-03-04 15:56:58 +01:00
|
|
|
$this->assertForbiddenDecision($decision);
|
2026-02-24 08:49:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testAdminActivateReturnsPermissionDeniedWhenTenantScopeFails(): void
|
|
|
|
|
{
|
2026-03-04 15:56:58 +01:00
|
|
|
$permissionGateway = $this->permissionGatewayAllowing([
|
|
|
|
|
11 => [PermissionService::USERS_UPDATE],
|
|
|
|
|
]);
|
2026-02-24 08:49:40 +01:00
|
|
|
|
|
|
|
|
$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,
|
|
|
|
|
]);
|
|
|
|
|
|
2026-03-04 15:56:58 +01:00
|
|
|
$this->assertDeniedDecision($decision, 403, 'permission_denied');
|
2026-02-24 08:49:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testAdminDeactivateAllowsSelfWithoutScopeLookup(): void
|
|
|
|
|
{
|
2026-03-04 15:56:58 +01:00
|
|
|
$permissionGateway = $this->permissionGatewayAllowing([
|
|
|
|
|
9 => [PermissionService::USERS_UPDATE],
|
|
|
|
|
]);
|
2026-02-24 08:49:40 +01:00
|
|
|
|
|
|
|
|
$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',
|
|
|
|
|
]);
|
|
|
|
|
|
2026-03-04 15:56:58 +01:00
|
|
|
$this->assertDeniedDecision($decision, 403, 'forbidden');
|
2026-02-24 08:49:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testAdminDeleteReturnsPermissionDeniedWhenTenantScopeFails(): void
|
|
|
|
|
{
|
2026-03-04 15:56:58 +01:00
|
|
|
$permissionGateway = $this->permissionGatewayAllowing([
|
|
|
|
|
7 => [PermissionService::USERS_DELETE],
|
|
|
|
|
]);
|
2026-02-24 08:49:40 +01:00
|
|
|
|
|
|
|
|
$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,
|
|
|
|
|
]);
|
|
|
|
|
|
2026-03-04 15:56:58 +01:00
|
|
|
$this->assertDeniedDecision($decision, 403, 'permission_denied');
|
2026-02-24 08:49:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
]);
|
|
|
|
|
|
2026-03-04 15:56:58 +01:00
|
|
|
$this->assertDeniedDecision($decision, 403, 'forbidden');
|
2026-02-24 08:49:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testAdminApiTokensManageRequiresScopeForOtherUsers(): void
|
|
|
|
|
{
|
2026-03-04 15:56:58 +01:00
|
|
|
$permissionGateway = $this->permissionGatewayAllowing([
|
|
|
|
|
8 => [PermissionService::API_TOKENS_MANAGE],
|
|
|
|
|
]);
|
2026-02-24 08:49:40 +01:00
|
|
|
|
|
|
|
|
$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,
|
|
|
|
|
]);
|
|
|
|
|
|
2026-03-04 15:56:58 +01:00
|
|
|
$this->assertDeniedDecision($decision, 403, 'permission_denied');
|
2026-02-24 08:49:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testAdminSendAccessAllowsSelfWithSelfUpdatePermission(): void
|
|
|
|
|
{
|
2026-03-04 15:56:58 +01:00
|
|
|
$permissionGateway = $this->permissionGatewayAllowing([
|
|
|
|
|
30 => [PermissionService::USERS_SELF_UPDATE],
|
|
|
|
|
]);
|
2026-02-24 08:49:40 +01:00
|
|
|
|
|
|
|
|
$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
|
|
|
|
|
{
|
2026-03-04 15:56:58 +01:00
|
|
|
$permissionGateway = $this->permissionGatewayAllowing([
|
|
|
|
|
30 => [PermissionService::USERS_SELF_UPDATE],
|
|
|
|
|
]);
|
2026-02-24 08:49:40 +01:00
|
|
|
|
|
|
|
|
$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,
|
|
|
|
|
]);
|
|
|
|
|
|
2026-03-04 15:56:58 +01:00
|
|
|
$this->assertDeniedDecision($decision, 403, 'permission_denied');
|
2026-02-24 08:49:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testEditContextHidesSecurityArtifactsWithoutAuditPermission(): void
|
|
|
|
|
{
|
2026-03-04 15:56:58 +01:00
|
|
|
$permissionGateway = $this->permissionGatewayAllowing([
|
|
|
|
|
9 => [PermissionService::USERS_VIEW],
|
|
|
|
|
]);
|
2026-02-24 08:49:40 +01:00
|
|
|
|
|
|
|
|
$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
|
|
|
|
|
{
|
2026-03-04 15:56:58 +01:00
|
|
|
$permissionGateway = $this->permissionGatewayAllowing([
|
|
|
|
|
9 => [PermissionService::USERS_VIEW, PermissionService::USERS_VIEW_AUDIT],
|
|
|
|
|
]);
|
2026-02-24 08:49:40 +01:00
|
|
|
|
|
|
|
|
$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
|
|
|
|
|
{
|
2026-03-04 15:56:58 +01:00
|
|
|
$permissionGateway = $this->permissionGatewayAllowing([
|
|
|
|
|
9 => [PermissionService::USERS_VIEW],
|
|
|
|
|
]);
|
2026-02-24 08:49:40 +01:00
|
|
|
|
|
|
|
|
$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,
|
|
|
|
|
]);
|
|
|
|
|
|
2026-03-04 15:56:58 +01:00
|
|
|
$this->assertForbiddenDecision($decision);
|
2026-02-24 08:49:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testAvatarUploadAllowsOwnAccountWithSelfUpdate(): void
|
|
|
|
|
{
|
2026-03-04 15:56:58 +01:00
|
|
|
$permissionGateway = $this->permissionGatewayAllowing([
|
|
|
|
|
19 => [PermissionService::USERS_SELF_UPDATE],
|
|
|
|
|
]);
|
2026-02-24 08:49:40 +01:00
|
|
|
|
|
|
|
|
$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,
|
|
|
|
|
]);
|
|
|
|
|
|
2026-03-04 15:56:58 +01:00
|
|
|
$this->assertDeniedDecision($decision, 403, 'permission_denied');
|
2026-02-24 08:49:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testAvatarViewAllowsInScopeAddressBookViewer(): void
|
|
|
|
|
{
|
2026-03-04 15:56:58 +01:00
|
|
|
$permissionGateway = $this->permissionGatewayAllowing([
|
|
|
|
|
5 => [PermissionService::ADDRESS_BOOK_VIEW],
|
|
|
|
|
]);
|
2026-02-24 08:49:40 +01:00
|
|
|
|
|
|
|
|
$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,
|
|
|
|
|
]);
|
|
|
|
|
|
2026-03-04 15:56:58 +01:00
|
|
|
$this->assertDeniedDecision($decision, 403, 'forbidden');
|
2026-02-24 08:49:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testApiShowReturnsNotFoundWhenTenantScopeFails(): void
|
|
|
|
|
{
|
2026-03-04 15:56:58 +01:00
|
|
|
$permissionGateway = $this->permissionGatewayAllowing([
|
|
|
|
|
4 => [PermissionService::USERS_VIEW],
|
|
|
|
|
]);
|
2026-02-24 08:49:40 +01:00
|
|
|
|
|
|
|
|
$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,
|
|
|
|
|
]);
|
|
|
|
|
|
2026-03-04 15:56:58 +01:00
|
|
|
$this->assertDeniedDecision($decision, 404, 'not_found');
|
2026-02-24 08:49:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testApiShowReturnsNotFoundForScopedTokenCrossTenantResource(): void
|
|
|
|
|
{
|
2026-03-04 15:56:58 +01:00
|
|
|
$permissionGateway = $this->permissionGatewayAllowing([
|
|
|
|
|
4 => [PermissionService::USERS_VIEW],
|
|
|
|
|
]);
|
2026-02-24 08:49:40 +01:00
|
|
|
|
|
|
|
|
$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,
|
|
|
|
|
]);
|
|
|
|
|
|
2026-03-04 15:56:58 +01:00
|
|
|
$this->assertDeniedDecision($decision, 404, 'not_found');
|
2026-02-24 08:49:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testApiCreateForScopedTokenRejectsOutOfScopeTenantIds(): void
|
|
|
|
|
{
|
2026-03-04 15:56:58 +01:00
|
|
|
$permissionGateway = $this->permissionGatewayAllowing([
|
|
|
|
|
5 => [PermissionService::USERS_CREATE],
|
|
|
|
|
]);
|
2026-02-24 08:49:40 +01:00
|
|
|
|
|
|
|
|
$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]],
|
|
|
|
|
]);
|
|
|
|
|
|
2026-03-04 15:56:58 +01:00
|
|
|
$this->assertDeniedDecision($decision, 403, 'tenant_scoped_token_forbidden');
|
2026-02-24 08:49:40 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public function testApiCreateForScopedTokenEnforcesScopedTenantInInput(): void
|
|
|
|
|
{
|
2026-03-04 15:56:58 +01:00
|
|
|
$permissionGateway = $this->permissionGatewayAllowing([
|
|
|
|
|
5 => [PermissionService::USERS_CREATE],
|
|
|
|
|
]);
|
2026-02-24 08:49:40 +01:00
|
|
|
|
|
|
|
|
$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
|
|
|
|
|
{
|
2026-03-04 15:56:58 +01:00
|
|
|
$permissionGateway = $this->permissionGatewayAllowing([
|
|
|
|
|
6 => [PermissionService::USERS_UPDATE],
|
|
|
|
|
]);
|
2026-02-24 08:49:40 +01:00
|
|
|
|
|
|
|
|
$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],
|
|
|
|
|
]);
|
|
|
|
|
|
2026-03-04 15:56:58 +01:00
|
|
|
$this->assertDeniedDecision($decision, 403, 'tenant_scoped_token_forbidden');
|
2026-02-24 08:49:40 +01:00
|
|
|
}
|
|
|
|
|
}
|