createMock(PermissionGateway::class); $permissionGateway->expects($this->once()) ->method('userHas') ->with(11, PermissionService::TENANTS_VIEW) ->willReturn(false); $scopeGateway = $this->createMock(DirectoryScopeGateway::class); $scopeGateway->expects($this->never())->method('getUserTenantIds'); $scopeGateway->expects($this->never())->method('isStrict'); $scopeGateway->expects($this->never())->method('hasGlobalAccess'); $policy = new TenantAuthorizationPolicy($permissionGateway, $scopeGateway); $decision = $policy->authorize(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_VIEW, [ 'actor_user_id' => 11, ]); $this->assertForbiddenDecision($decision); } public function testViewReturnsCapabilitiesAndScopeData(): void { $permissionGateway = $this->permissionGatewayAllowing([ 11 => [PermissionService::TENANTS_VIEW, PermissionService::TENANTS_CREATE], ]); $scopeGateway = $this->createMock(DirectoryScopeGateway::class); $scopeGateway->expects($this->once()) ->method('getUserTenantIds') ->with(11) ->willReturn([2, 3]); $scopeGateway->expects($this->once()) ->method('isStrict') ->willReturn(true); $scopeGateway->expects($this->once()) ->method('hasGlobalAccess') ->with(11) ->willReturn(false); $policy = new TenantAuthorizationPolicy($permissionGateway, $scopeGateway); $decision = $policy->authorize(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_VIEW, [ 'actor_user_id' => 11, ]); $capabilities = $decision->attribute('capabilities', []); $this->assertTrue($decision->isAllowed()); $this->assertTrue((bool) ($capabilities['can_create_tenant'] ?? false)); $this->assertSame([2, 3], $capabilities['allowed_tenant_ids'] ?? []); $this->assertTrue((bool) ($capabilities['is_strict_scope'] ?? false)); $this->assertFalse((bool) ($capabilities['has_global_access'] ?? true)); } public function testCreateRequiresTenantsCreatePermission(): void { $permissionGateway = $this->createMock(PermissionGateway::class); $permissionGateway->expects($this->once()) ->method('userHas') ->with(11, PermissionService::TENANTS_CREATE) ->willReturn(false); $scopeGateway = $this->createMock(DirectoryScopeGateway::class); $policy = new TenantAuthorizationPolicy($permissionGateway, $scopeGateway); $decision = $policy->authorize(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_CREATE, [ 'actor_user_id' => 11, ]); $this->assertDeniedDecision($decision, 403, 'forbidden'); } public function testCreateReturnsSsoAndCustomFieldCapabilities(): void { $permissionGateway = $this->permissionGatewayAllowing([ 11 => [PermissionService::TENANTS_CREATE, PermissionService::TENANTS_SSO_MANAGE], ]); $scopeGateway = $this->createMock(DirectoryScopeGateway::class); $policy = new TenantAuthorizationPolicy($permissionGateway, $scopeGateway); $decision = $policy->authorize(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_CREATE, [ 'actor_user_id' => 11, ]); $capabilities = $decision->attribute('capabilities', []); $this->assertTrue($decision->isAllowed()); $this->assertTrue((bool) ($capabilities['can_manage_sso'] ?? false)); $this->assertFalse((bool) ($capabilities['can_manage_custom_fields'] ?? true)); } public function testEditContextAllowsInScopeViewer(): void { $permissionGateway = $this->permissionGatewayAllowing([ 11 => [PermissionService::TENANTS_VIEW], ]); $scopeGateway = $this->createMock(DirectoryScopeGateway::class); $scopeGateway->expects($this->once()) ->method('canAccess') ->with('tenants', 22, 11) ->willReturn(true); $policy = new TenantAuthorizationPolicy($permissionGateway, $scopeGateway); $decision = $policy->authorize(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_EDIT_CONTEXT, [ 'actor_user_id' => 11, 'target_tenant_id' => 22, ]); $this->assertTrue($decision->isAllowed()); $capabilities = $decision->attribute('capabilities', []); $this->assertTrue((bool) ($capabilities['can_view_page'] ?? false)); $this->assertFalse((bool) ($capabilities['can_update_tenant'] ?? true)); } public function testEditContextDeniesOutOfScopeTenant(): void { $permissionGateway = $this->createMock(PermissionGateway::class); $scopeGateway = $this->createMock(DirectoryScopeGateway::class); $scopeGateway->expects($this->once()) ->method('canAccess') ->with('tenants', 22, 11) ->willReturn(false); $policy = new TenantAuthorizationPolicy($permissionGateway, $scopeGateway); $decision = $policy->authorize(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_EDIT_CONTEXT, [ 'actor_user_id' => 11, 'target_tenant_id' => 22, ]); $this->assertDeniedDecision($decision, 403, 'permission_denied'); } public function testEditSubmitRequiresTenantsUpdatePermission(): void { $permissionGateway = $this->permissionGatewayAllowing([ 11 => [PermissionService::TENANTS_VIEW], ]); $scopeGateway = $this->createMock(DirectoryScopeGateway::class); $scopeGateway->expects($this->once()) ->method('canAccess') ->with('tenants', 22, 11) ->willReturn(true); $policy = new TenantAuthorizationPolicy($permissionGateway, $scopeGateway); $decision = $policy->authorize(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_EDIT_SUBMIT, [ 'actor_user_id' => 11, 'target_tenant_id' => 22, 'input' => ['description' => 'Tenant A'], ]); $this->assertForbiddenDecision($decision); } public function testEditSubmitDeniesSsoFieldsWithoutSsoPermission(): void { $permissionGateway = $this->permissionGatewayAllowing([ 11 => [PermissionService::TENANTS_VIEW, PermissionService::TENANTS_UPDATE], ]); $scopeGateway = $this->createMock(DirectoryScopeGateway::class); $scopeGateway->expects($this->once()) ->method('canAccess') ->with('tenants', 22, 11) ->willReturn(true); $policy = new TenantAuthorizationPolicy($permissionGateway, $scopeGateway); $decision = $policy->authorize(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_EDIT_SUBMIT, [ 'actor_user_id' => 11, 'target_tenant_id' => 22, 'input' => ['microsoft_enabled' => '1'], ]); $this->assertForbiddenDecision($decision); } public function testCustomFieldsManageRequiresPermissionAndScope(): void { $permissionGateway = $this->permissionGatewayAllowing([ 9 => [PermissionService::CUSTOM_FIELDS_MANAGE], ]); $scopeGateway = $this->createMock(DirectoryScopeGateway::class); $scopeGateway->expects($this->once()) ->method('canAccess') ->with('tenants', 5, 9) ->willReturn(true); $policy = new TenantAuthorizationPolicy($permissionGateway, $scopeGateway); $decision = $policy->authorize(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_CUSTOM_FIELDS_MANAGE, [ 'actor_user_id' => 9, 'target_tenant_id' => 5, ]); $this->assertTrue($decision->isAllowed()); } public function testCustomFieldsManageDeniedWhenOutOfScope(): void { $permissionGateway = $this->permissionGatewayAllowing([ 9 => [PermissionService::CUSTOM_FIELDS_MANAGE], ]); $scopeGateway = $this->createMock(DirectoryScopeGateway::class); $scopeGateway->expects($this->once()) ->method('canAccess') ->with('tenants', 5, 9) ->willReturn(false); $policy = new TenantAuthorizationPolicy($permissionGateway, $scopeGateway); $decision = $policy->authorize(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_CUSTOM_FIELDS_MANAGE, [ 'actor_user_id' => 9, 'target_tenant_id' => 5, ]); $this->assertDeniedDecision($decision, 403, 'permission_denied'); } public function testAvatarViewAllowsTenantViewerInScope(): void { $permissionGateway = $this->permissionGatewayAllowing([ 15 => [PermissionService::TENANTS_VIEW], ]); $scopeGateway = $this->createMock(DirectoryScopeGateway::class); $scopeGateway->expects($this->once()) ->method('canAccess') ->with('tenants', 9, 15) ->willReturn(true); $policy = new TenantAuthorizationPolicy($permissionGateway, $scopeGateway); $decision = $policy->authorize(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_AVATAR_VIEW, [ 'actor_user_id' => 15, 'target_tenant_id' => 9, ]); $this->assertTrue($decision->isAllowed()); } public function testMediaUpdateRequiresTenantsUpdatePermission(): void { $permissionGateway = $this->createMock(PermissionGateway::class); $scopeGateway = $this->createMock(DirectoryScopeGateway::class); $scopeGateway->expects($this->never())->method('canAccess'); $policy = new TenantAuthorizationPolicy($permissionGateway, $scopeGateway); $decision = $policy->authorize(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_MEDIA_UPDATE, [ 'actor_user_id' => 15, 'target_tenant_id' => 9, ]); $this->assertDeniedDecision($decision, 403, 'forbidden'); } public function testDeleteRequiresPermissionAndScope(): void { $permissionGateway = $this->permissionGatewayAllowing([ 5 => [PermissionService::TENANTS_DELETE], ]); $scopeGateway = $this->createMock(DirectoryScopeGateway::class); $scopeGateway->expects($this->once()) ->method('canAccess') ->with('tenants', 17, 5) ->willReturn(true); $policy = new TenantAuthorizationPolicy($permissionGateway, $scopeGateway); $decision = $policy->authorize(TenantAuthorizationPolicy::ABILITY_ADMIN_TENANTS_DELETE, [ 'actor_user_id' => 5, 'target_tenant_id' => 17, ]); $this->assertTrue($decision->isAllowed()); } }