createMock(PermissionService::class); $permissionService->expects($this->once()) ->method('userHas') ->with(13, PermissionService::USERS_VIEW) ->willReturn(false); $scopeGateway = $this->createMock(TenantScopeService::class); $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway); $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_VIEW, [ 'actor_user_id' => 13, ]); $this->assertForbiddenDecision($decision); } public function testAdminCreateRequiresUsersCreatePermission(): void { $permissionService = $this->createMock(PermissionService::class); $permissionService->expects($this->once()) ->method('userHas') ->with(13, PermissionService::USERS_CREATE) ->willReturn(false); $scopeGateway = $this->createMock(TenantScopeService::class); $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway); $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_CREATE, [ 'actor_user_id' => 13, ]); $this->assertDeniedDecision($decision, 403, 'forbidden'); } public function testAdminActivateRequiresUsersUpdatePermission(): void { $permissionService = $this->createMock(PermissionService::class); $permissionService->expects($this->once()) ->method('userHas') ->with(11, PermissionService::USERS_UPDATE) ->willReturn(false); $scopeGateway = $this->createMock(TenantScopeService::class); $scopeGateway->expects($this->never())->method('canAccess'); $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway); $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_ACTIVATE, [ 'actor_user_id' => 11, 'target_user_id' => 22, ]); $this->assertForbiddenDecision($decision); } public function testAdminActivateReturnsPermissionDeniedWhenTenantScopeFails(): void { $permissionService = $this->permissionGatewayAllowing([ 11 => [PermissionService::USERS_UPDATE], ]); $scopeGateway = $this->createMock(TenantScopeService::class); $scopeGateway->expects($this->once()) ->method('canAccess') ->with('users', 22, 11) ->willReturn(false); $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway); $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_ACTIVATE, [ 'actor_user_id' => 11, 'target_user_id' => 22, ]); $this->assertDeniedDecision($decision, 403, 'permission_denied'); } public function testAdminDeactivateAllowsSelfWithoutScopeLookup(): void { $permissionService = $this->permissionGatewayAllowing([ 9 => [PermissionService::USERS_UPDATE], ]); $scopeGateway = $this->createMock(TenantScopeService::class); $scopeGateway->expects($this->never())->method('canAccess'); $policy = new UserAuthorizationPolicy($permissionService, $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 { $permissionService = $this->createMock(PermissionService::class); $permissionService->expects($this->once()) ->method('userHas') ->with(7, PermissionService::USERS_DELETE) ->willReturn(false); $scopeGateway = $this->createMock(TenantScopeService::class); $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway); $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_BULK, [ 'actor_user_id' => 7, 'bulk_action' => 'delete', ]); $this->assertDeniedDecision($decision, 403, 'forbidden'); } public function testAdminDeleteReturnsPermissionDeniedWhenTenantScopeFails(): void { $permissionService = $this->permissionGatewayAllowing([ 7 => [PermissionService::USERS_DELETE], ]); $scopeGateway = $this->createMock(TenantScopeService::class); $scopeGateway->expects($this->once()) ->method('canAccess') ->with('users', 17, 7) ->willReturn(false); $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway); $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_DELETE, [ 'actor_user_id' => 7, 'target_user_id' => 17, ]); $this->assertDeniedDecision($decision, 403, 'permission_denied'); } public function testAdminAccessPdfBulkRequiresPermission(): void { $permissionService = $this->createMock(PermissionService::class); $permissionService->expects($this->once()) ->method('userHas') ->with(21, PermissionService::USERS_ACCESS_PDF) ->willReturn(false); $scopeGateway = $this->createMock(TenantScopeService::class); $scopeGateway->expects($this->never())->method('canAccess'); $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway); $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_ACCESS_PDF_BULK, [ 'actor_user_id' => 21, ]); $this->assertDeniedDecision($decision, 403, 'forbidden'); } public function testAdminApiTokensManageRequiresScopeForOtherUsers(): void { $permissionService = $this->permissionGatewayAllowing([ 8 => [PermissionService::API_TOKENS_MANAGE], ]); $scopeGateway = $this->createMock(TenantScopeService::class); $scopeGateway->expects($this->once()) ->method('canAccess') ->with('users', 18, 8) ->willReturn(false); $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway); $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_API_TOKENS_MANAGE, [ 'actor_user_id' => 8, 'target_user_id' => 18, ]); $this->assertDeniedDecision($decision, 403, 'permission_denied'); } public function testAdminSendAccessAllowsSelfWithSelfUpdatePermission(): void { $permissionService = $this->permissionGatewayAllowing([ 30 => [PermissionService::USERS_SELF_UPDATE], ]); $scopeGateway = $this->createMock(TenantScopeService::class); $scopeGateway->expects($this->never())->method('canAccess'); $policy = new UserAuthorizationPolicy($permissionService, $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 { $permissionService = $this->permissionGatewayAllowing([ 30 => [PermissionService::USERS_SELF_UPDATE], ]); $scopeGateway = $this->createMock(TenantScopeService::class); $scopeGateway->expects($this->never())->method('canAccess'); $scopeGateway->expects($this->once()) ->method('getUserTenantIds') ->with(30) ->willReturn([2, 3]); $policy = new UserAuthorizationPolicy($permissionService, $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 { $permissionService = $this->createMock(PermissionService::class); $scopeGateway = $this->createMock(TenantScopeService::class); $scopeGateway->expects($this->once()) ->method('canAccess') ->with('users', 66, 33) ->willReturn(false); $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway); $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_EDIT_CONTEXT, [ 'actor_user_id' => 33, 'target_user_id' => 66, ]); $this->assertDeniedDecision($decision, 403, 'permission_denied'); } public function testEditContextHidesSecurityArtifactsWithoutAuditPermission(): void { $permissionService = $this->permissionGatewayAllowing([ 9 => [PermissionService::USERS_VIEW], ]); $scopeGateway = $this->createMock(TenantScopeService::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($permissionService, $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 { $permissionService = $this->permissionGatewayAllowing([ 9 => [PermissionService::USERS_VIEW, PermissionService::USERS_VIEW_AUDIT], ]); $scopeGateway = $this->createMock(TenantScopeService::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($permissionService, $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 { $permissionService = $this->permissionGatewayAllowing([ 9 => [PermissionService::USERS_VIEW], ]); $scopeGateway = $this->createMock(TenantScopeService::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($permissionService, $scopeGateway); $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_EDIT_SUBMIT, [ 'actor_user_id' => 9, 'target_user_id' => 55, ]); $this->assertForbiddenDecision($decision); } public function testAvatarUploadAllowsOwnAccountWithSelfUpdate(): void { $permissionService = $this->permissionGatewayAllowing([ 19 => [PermissionService::USERS_SELF_UPDATE], ]); $scopeGateway = $this->createMock(TenantScopeService::class); $scopeGateway->expects($this->never())->method('canAccess'); $policy = new UserAuthorizationPolicy($permissionService, $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 { $permissionService = $this->createMock(PermissionService::class); $scopeGateway = $this->createMock(TenantScopeService::class); $scopeGateway->expects($this->once()) ->method('canAccess') ->with('users', 42, 7) ->willReturn(false); $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway); $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_AVATAR_DELETE, [ 'actor_user_id' => 7, 'target_user_id' => 42, ]); $this->assertDeniedDecision($decision, 403, 'permission_denied'); } public function testAvatarViewAllowsInScopeAddressBookViewer(): void { $permissionService = $this->permissionGatewayAllowing([ 5 => [PermissionService::ADDRESS_BOOK_VIEW], ]); $scopeGateway = $this->createMock(TenantScopeService::class); $scopeGateway->expects($this->once()) ->method('canAccess') ->with('users', 70, 5) ->willReturn(true); $policy = new UserAuthorizationPolicy($permissionService, $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 { $permissionService = $this->createMock(PermissionService::class); $permissionService->method('userHas')->willReturn(false); $scopeGateway = $this->createMock(TenantScopeService::class); $scopeGateway->expects($this->once()) ->method('canAccess') ->with('users', 70, 5) ->willReturn(true); $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway); $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_ADMIN_USERS_AVATAR_VIEW, [ 'actor_user_id' => 5, 'target_user_id' => 70, ]); $this->assertDeniedDecision($decision, 403, 'forbidden'); } public function testApiShowReturnsNotFoundWhenTenantScopeFails(): void { $permissionService = $this->permissionGatewayAllowing([ 4 => [PermissionService::USERS_VIEW], ]); $scopeGateway = $this->createMock(TenantScopeService::class); $scopeGateway->expects($this->once()) ->method('canAccess') ->with('users', 44, 4) ->willReturn(false); $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway); $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_API_USERS_SHOW_GET, [ 'actor_user_id' => 4, 'target_user_id' => 44, 'scoped_tenant_id' => null, ]); $this->assertDeniedDecision($decision, 404, 'not_found'); } public function testApiShowReturnsNotFoundForScopedTokenCrossTenantResource(): void { $permissionService = $this->permissionGatewayAllowing([ 4 => [PermissionService::USERS_VIEW], ]); $scopeGateway = $this->createMock(TenantScopeService::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($permissionService, $scopeGateway); $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_API_USERS_SHOW_GET, [ 'actor_user_id' => 4, 'target_user_id' => 44, 'scoped_tenant_id' => 2, ]); $this->assertDeniedDecision($decision, 404, 'not_found'); } public function testApiCreateForScopedTokenRejectsOutOfScopeTenantIds(): void { $permissionService = $this->permissionGatewayAllowing([ 5 => [PermissionService::USERS_CREATE], ]); $scopeGateway = $this->createMock(TenantScopeService::class); $policy = new UserAuthorizationPolicy($permissionService, $scopeGateway); $decision = $policy->authorize(UserAuthorizationPolicy::ABILITY_API_USERS_INDEX_POST, [ 'actor_user_id' => 5, 'scoped_tenant_id' => 9, 'input' => ['tenant_ids' => [9, 10]], ]); $this->assertDeniedDecision($decision, 403, 'tenant_scoped_token_forbidden'); } public function testApiCreateForScopedTokenEnforcesScopedTenantInInput(): void { $permissionService = $this->permissionGatewayAllowing([ 5 => [PermissionService::USERS_CREATE], ]); $scopeGateway = $this->createMock(TenantScopeService::class); $policy = new UserAuthorizationPolicy($permissionService, $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 { $permissionService = $this->permissionGatewayAllowing([ 6 => [PermissionService::USERS_UPDATE], ]); $scopeGateway = $this->createMock(TenantScopeService::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($permissionService, $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->assertDeniedDecision($decision, 403, 'tenant_scoped_token_forbidden'); } }