From 84ea68927f3e509f5c6d1c76807081050c7205b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adri=C3=A1n=20Pe=C3=B1a?= Date: Thu, 9 Jul 2026 10:36:49 +0200 Subject: [PATCH] fix(api): scope user role updates to tenant (#11903) --- .../user-role-tenant-scope.security.md | 1 + api/src/backend/api/tests/test_views.py | 64 ++++++++++++++++++- api/src/backend/api/v1/serializers.py | 6 +- 3 files changed, 66 insertions(+), 5 deletions(-) create mode 100644 api/changelog.d/user-role-tenant-scope.security.md diff --git a/api/changelog.d/user-role-tenant-scope.security.md b/api/changelog.d/user-role-tenant-scope.security.md new file mode 100644 index 0000000000..62cc2d14fe --- /dev/null +++ b/api/changelog.d/user-role-tenant-scope.security.md @@ -0,0 +1 @@ +User role relationship updates are limited to the active tenant to preserve role assignments in other tenants diff --git a/api/src/backend/api/tests/test_views.py b/api/src/backend/api/tests/test_views.py index 6b51261688..4fdb023955 100644 --- a/api/src/backend/api/tests/test_views.py +++ b/api/src/backend/api/tests/test_views.py @@ -9478,9 +9478,15 @@ class TestUserRoleRelationshipViewSet: content_type="application/vnd.api+json", ) assert response.status_code == status.HTTP_204_NO_CONTENT - relationships = UserRoleRelationship.objects.filter(user=create_test_user.id) + tenant = roles_fixture[2].tenant + relationships = UserRoleRelationship.objects.filter( + user=create_test_user.id, tenant=tenant + ) assert relationships.count() == 1 assert {rel.role.id for rel in relationships} == {roles_fixture[2].id} + assert ( + UserRoleRelationship.objects.filter(user=create_test_user.id).count() == 2 + ) data = { "data": [ @@ -9494,12 +9500,66 @@ class TestUserRoleRelationshipViewSet: content_type="application/vnd.api+json", ) assert response.status_code == status.HTTP_204_NO_CONTENT - relationships = UserRoleRelationship.objects.filter(user=create_test_user.id) + relationships = UserRoleRelationship.objects.filter( + user=create_test_user.id, tenant=tenant + ) assert relationships.count() == 2 assert {rel.role.id for rel in relationships} == { roles_fixture[1].id, roles_fixture[2].id, } + assert ( + UserRoleRelationship.objects.filter(user=create_test_user.id).count() == 3 + ) + + def test_partial_update_relationship_preserves_foreign_tenant_roles( + self, authenticated_client, roles_fixture, tenants_fixture + ): + tenant_a, tenant_b, _ = tenants_fixture + tenant_a_role = roles_fixture[1] + replacement_role = roles_fixture[2] + foreign_role = Role.objects.create( + name=f"foreign-role-{uuid4()}", + tenant=tenant_b, + manage_users=False, + manage_account=False, + manage_billing=False, + manage_providers=False, + manage_integrations=False, + manage_scans=False, + unlimited_visibility=False, + ) + shared_user = User.objects.create_user( + name="shared_user", + email=f"shared-user-{uuid4()}@prowler.com", + password="TmpPass123@", + ) + Membership.objects.create(user=shared_user, tenant=tenant_a) + Membership.objects.create(user=shared_user, tenant=tenant_b) + UserRoleRelationship.objects.create( + user=shared_user, role=tenant_a_role, tenant=tenant_a + ) + UserRoleRelationship.objects.create( + user=shared_user, role=foreign_role, tenant=tenant_b + ) + + data = {"data": [{"type": "roles", "id": str(replacement_role.id)}]} + response = authenticated_client.patch( + reverse("user-roles-relationship", kwargs={"pk": shared_user.id}), + data=data, + content_type="application/vnd.api+json", + ) + + assert response.status_code == status.HTTP_204_NO_CONTENT + tenant_a_relationships = UserRoleRelationship.objects.filter( + user=shared_user, tenant=tenant_a + ) + assert tenant_a_relationships.count() == 1 + assert {rel.role_id for rel in tenant_a_relationships} == {replacement_role.id} + assert UserRoleRelationship.objects.filter( + user=shared_user, tenant=tenant_b, role=foreign_role + ).exists() + assert UserRoleRelationship.objects.filter(user=shared_user).count() == 2 def test_destroy_relationship_other_user( self, authenticated_client, roles_fixture, create_test_user, tenants_fixture diff --git a/api/src/backend/api/v1/serializers.py b/api/src/backend/api/v1/serializers.py index 9cb9591b35..57aa32f0a0 100644 --- a/api/src/backend/api/v1/serializers.py +++ b/api/src/backend/api/v1/serializers.py @@ -444,8 +444,8 @@ class UserRoleRelationshipSerializer(RLSSerializer, BaseWriteSerializer): def create(self, validated_data): role_ids = [item["id"] for item in validated_data["roles"]] - roles = Role.objects.filter(id__in=role_ids) tenant_id = self.context.get("tenant_id") + roles = Role.objects.filter(id__in=role_ids, tenant_id=tenant_id) new_relationships = [ UserRoleRelationship( @@ -459,8 +459,8 @@ class UserRoleRelationshipSerializer(RLSSerializer, BaseWriteSerializer): def update(self, instance, validated_data): role_ids = [item["id"] for item in validated_data["roles"]] - roles = Role.objects.filter(id__in=role_ids) tenant_id = self.context.get("tenant_id") + roles = Role.objects.filter(id__in=role_ids, tenant_id=tenant_id) # Safeguard: A tenant must always have at least one user with MANAGE_ACCOUNT. # If the target roles do NOT include MANAGE_ACCOUNT, and the current user is @@ -490,7 +490,7 @@ class UserRoleRelationshipSerializer(RLSSerializer, BaseWriteSerializer): } ) - instance.roles.clear() + UserRoleRelationship.objects.filter(user=instance, tenant_id=tenant_id).delete() new_relationships = [ UserRoleRelationship(user=instance, role=r, tenant_id=tenant_id) for r in roles