Skip to content

Commit c723941

Browse files
committed
Add details for admin permission
1 parent 8b98b98 commit c723941

File tree

7 files changed

+135
-33
lines changed

7 files changed

+135
-33
lines changed

api/permissions/permissions_calculator.py

Lines changed: 106 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313
UserPermissionGroupOrganisationPermission,
1414
)
1515
from projects.models import (
16+
Project,
1617
UserPermissionGroupProjectPermission,
1718
UserProjectPermission,
1819
)
1920

20-
from .permission_service import is_user_project_admin
2121
from .rbac_wrapper import ( # type: ignore[attr-defined]
2222
get_roles_permission_data_for_environment,
2323
get_roles_permission_data_for_organisation,
@@ -99,6 +99,8 @@ class DetailedPermissionsData:
9999
@dataclass
100100
class UserDetailedPermissionsData:
101101
admin: bool
102+
derived_from: PermissionDerivedFromData
103+
is_directly_granted: bool
102104
permissions: typing.List[DetailedPermissionsData]
103105

104106

@@ -113,15 +115,21 @@ class PermissionData:
113115
roles: typing.List[RolePermissionData]
114116
is_organisation_admin: bool = False
115117
admin_override: bool = False
118+
inherited_admin_groups: typing.List[GroupPermissionData] = field(
119+
default_factory=list
120+
)
121+
inherited_admin_roles: typing.List[RolePermissionData] = field(default_factory=list)
116122

117123
@property
118124
def admin(self) -> bool:
119-
return (
125+
return bool(
120126
self.is_organisation_admin
121127
or self.user.admin
122128
or any(group.admin for group in self.groups)
123129
or any(role.admin for role in self.roles)
124130
or self.admin_override
131+
or self.inherited_admin_groups
132+
or self.inherited_admin_roles
125133
)
126134

127135
@property
@@ -157,8 +165,10 @@ def tag_based_permissions(self) -> list[dict]: # type: ignore[type-arg]
157165
if role_permission.role.tags
158166
]
159167

160-
def to_detailed_permissions_data(self) -> UserDetailedPermissionsData:
168+
def to_detailed_permissions_data(self) -> UserDetailedPermissionsData: # noqa: C901
161169
permission_map = {}
170+
is_admin_permission_directly_granted = False
171+
admin_permission_derived_from = PermissionDerivedFromData()
162172

163173
def add_permission(
164174
permission_key: str,
@@ -178,32 +188,38 @@ def add_permission(
178188

179189
# Add user's direct permissions
180190
for permission_key in self.user.permissions:
191+
if self.user.admin:
192+
is_admin_permission_directly_granted = True
193+
181194
add_permission(permission_key, None, None)
182195

183196
# Add group permissions
184197
for group_permission in self.groups:
198+
if group_permission.admin:
199+
admin_permission_derived_from.groups.append(group_permission.group)
200+
185201
for permission_key in group_permission.permissions:
186202
add_permission(permission_key, group_permission.group, None)
187203

188204
# Add role permissions
189205
for role_permission in self.roles:
206+
if role_permission.admin:
207+
admin_permission_derived_from.roles.append(role_permission.role)
208+
190209
for permission_key in role_permission.permissions:
191210
add_permission(permission_key, None, role_permission.role)
192211

212+
if self.is_organisation_admin or self.user.admin or self.admin_override:
213+
is_admin_permission_directly_granted = True
214+
193215
return UserDetailedPermissionsData(
194-
admin=self.admin, permissions=list(permission_map.values())
216+
admin=self.admin,
217+
is_directly_granted=is_admin_permission_directly_granted,
218+
derived_from=admin_permission_derived_from,
219+
permissions=list(permission_map.values()),
195220
)
196221

197222

198-
def get_project_permission_data(project_id: int, user_id: int) -> PermissionData:
199-
project_permission_svc = _ProjectPermissionService(project_id, user_id)
200-
return PermissionData(
201-
groups=get_groups_permission_data(project_permission_svc.group_qs),
202-
user=get_user_permission_data(project_permission_svc.user_permission), # type: ignore[arg-type]
203-
roles=get_roles_permission_data_for_project(project_id, user_id),
204-
)
205-
206-
207223
def get_organisation_permission_data(
208224
organisation_id: int, user: "FFAdminUser"
209225
) -> PermissionData:
@@ -212,19 +228,42 @@ def get_organisation_permission_data(
212228
is_organisation_admin=user.is_organisation_admin(organisation_id),
213229
groups=get_groups_permission_data(org_permission_svc.group_qs),
214230
user=get_user_permission_data(org_permission_svc.user_permission), # type: ignore[arg-type]
215-
roles=get_roles_permission_data_for_organisation(organisation_id, user.id),
231+
roles=org_permission_svc.roles,
232+
)
233+
234+
235+
def get_project_permission_data(
236+
project: Project, user: "FFAdminUser"
237+
) -> PermissionData:
238+
project_permission_svc = _ProjectPermissionService(project.id, user.id)
239+
return PermissionData(
240+
is_organisation_admin=user.is_organisation_admin(project.organisation_id),
241+
groups=get_groups_permission_data(project_permission_svc.group_qs),
242+
user=get_user_permission_data(project_permission_svc.user_permission), # type: ignore[arg-type]
243+
roles=project_permission_svc.roles,
216244
)
217245

218246

219247
def get_environment_permission_data(
220248
environment: "Environment", user: "FFAdminUser"
221249
) -> PermissionData:
222-
environment_permission_svc = _EnvironmentPermissionService(environment.id, user.id)
250+
project_permission_svc = _ProjectPermissionService(environment.project_id, user.id)
251+
environment_permission_svc = _EnvironmentPermissionService(
252+
environment.id, user.id, project_permission_svc=project_permission_svc
253+
)
254+
223255
return PermissionData(
256+
is_organisation_admin=user.is_organisation_admin(
257+
environment.project.organisation_id
258+
),
224259
groups=get_groups_permission_data(environment_permission_svc.group_qs),
225260
user=get_user_permission_data(environment_permission_svc.user_permission), # type: ignore[arg-type]
226-
roles=get_roles_permission_data_for_environment(environment.id, user.id),
227-
admin_override=is_user_project_admin(user, project=environment.project),
261+
roles=environment_permission_svc.roles_data,
262+
inherited_admin_groups=get_groups_permission_data(
263+
environment_permission_svc.inherited_admin_group_qs
264+
),
265+
inherited_admin_roles=environment_permission_svc.inherited_admin_roles,
266+
admin_override=environment_permission_svc.inherited_user_admin,
228267
)
229268

230269

@@ -289,11 +328,46 @@ def group_qs(self) -> GroupPermissionQs:
289328
group__users=self.user_id, organisation=self.organisation_id
290329
)
291330

331+
@property
332+
def roles(self) -> typing.List[RolePermissionData]:
333+
roles_permission_data: typing.List[RolePermissionData] = (
334+
get_roles_permission_data_for_organisation(
335+
self.organisation_id, self.user_id
336+
)
337+
)
338+
return roles_permission_data
339+
340+
341+
@dataclass
342+
class _ProjectPermissionService:
343+
project_id: int
344+
user_id: int
345+
346+
@property
347+
def user_permission(self) -> typing.Optional[UserProjectPermission]:
348+
return UserProjectPermission.objects.filter(
349+
project_id=self.project_id, user_id=self.user_id
350+
).first()
351+
352+
@property
353+
def group_qs(self) -> GroupPermissionQs:
354+
return UserPermissionGroupProjectPermission.objects.filter(
355+
group__users=self.user_id, project=self.project_id
356+
)
357+
358+
@property
359+
def roles(self) -> typing.List[RolePermissionData]:
360+
roles_permission_data: typing.List[RolePermissionData] = (
361+
get_roles_permission_data_for_project(self.project_id, self.user_id)
362+
)
363+
return roles_permission_data
364+
292365

293366
@dataclass
294367
class _EnvironmentPermissionService:
295368
environment_id: int
296369
user_id: int
370+
project_permission_svc: _ProjectPermissionService
297371

298372
@property
299373
def user_permission(self) -> typing.Optional[UserEnvironmentPermission]:
@@ -307,20 +381,23 @@ def group_qs(self) -> GroupPermissionQs:
307381
group__users=self.user_id, environment=self.environment_id
308382
)
309383

384+
@property
385+
def roles_data(self) -> typing.List[RolePermissionData]:
386+
roles_permission_data: typing.List[RolePermissionData] = (
387+
get_roles_permission_data_for_environment(self.environment_id, self.user_id)
388+
)
389+
return roles_permission_data
310390

311-
@dataclass
312-
class _ProjectPermissionService:
313-
project_id: int
314-
user_id: int
391+
@property
392+
def inherited_user_admin(self) -> bool:
393+
if user_permission := self.project_permission_svc.user_permission:
394+
return user_permission.admin
395+
return False
315396

316397
@property
317-
def user_permission(self) -> typing.Optional[UserProjectPermission]:
318-
return UserProjectPermission.objects.filter(
319-
project_id=self.project_id, user_id=self.user_id
320-
).first()
398+
def inherited_admin_group_qs(self) -> GroupPermissionQs:
399+
return self.project_permission_svc.group_qs.filter(admin=True)
321400

322401
@property
323-
def group_qs(self) -> GroupPermissionQs:
324-
return UserPermissionGroupProjectPermission.objects.filter(
325-
group__users=self.user_id, project=self.project_id
326-
)
402+
def inherited_admin_roles(self) -> typing.List[RolePermissionData]:
403+
return [role for role in self.project_permission_svc.roles if role.admin]

api/permissions/serializers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,3 +78,5 @@ class DetailedPermissionsSerializer(serializers.Serializer): # type: ignore[typ
7878
class UserDetailedPermissionsSerializer(serializers.Serializer): # type: ignore[type-arg]
7979
admin = serializers.BooleanField()
8080
permissions = DetailedPermissionsSerializer(many=True)
81+
is_directly_granted = serializers.BooleanField()
82+
derived_from = DerivedFromSerializer()

api/projects/views.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,9 @@ def user_permissions(self, request: Request, pk: int = None): # type: ignore[no
185185
"detail": "This endpoint can only be used with a user and not Master API Key"
186186
},
187187
)
188-
permission_data = get_project_permission_data(pk, user_id=request.user.id)
188+
189+
project = self.get_object()
190+
permission_data = get_project_permission_data(project, user=request.user) # type: ignore[arg-type]
189191
serializer = UserObjectPermissionsSerializer(instance=permission_data)
190192
return Response(serializer.data)
191193

@@ -265,8 +267,10 @@ def get_serializer_class(self): # type: ignore[no-untyped-def]
265267
@permission_classes([IsAuthenticated, IsProjectAdmin])
266268
def get_user_project_permissions(request, **kwargs): # type: ignore[no-untyped-def]
267269
user_id = kwargs["user_pk"]
270+
project = get_object_or_404(Project, pk=kwargs["project_pk"])
271+
user = get_object_or_404(FFAdminUser, pk=user_id)
268272

269-
permission_data = get_project_permission_data(kwargs["project_pk"], user_id=user_id)
273+
permission_data = get_project_permission_data(project, user=user)
270274
# TODO: expose `user` and `groups` attributes from user_permissions_data
271275
serializer = UserObjectPermissionsSerializer(instance=permission_data)
272276
return Response(serializer.data)

api/tests/unit/environments/test_unit_environments_views.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,8 @@ def test_environment_user_can_get_their_detailed_permissions(
315315
# Then
316316
assert response.status_code == status.HTTP_200_OK
317317
assert response.json()["admin"] is False
318+
assert response.json()["is_directly_granted"] is False
319+
assert response.json()["derived_from"] == {"groups": [], "roles": []}
318320
assert response.json()["permissions"] == [
319321
{
320322
"permission_key": "VIEW_ENVIRONMENT",
@@ -364,6 +366,8 @@ def test_environment_admin_can_get_detailed_permissions_of_other_user(
364366
# Then
365367
assert response.status_code == status.HTTP_200_OK
366368
assert response.json()["admin"] is False
369+
assert response.json()["is_directly_granted"] is False
370+
assert response.json()["derived_from"] == {"groups": [], "roles": []}
367371
assert response.json()["permissions"] == [
368372
{
369373
"permission_key": "VIEW_ENVIRONMENT",

api/tests/unit/organisations/test_unit_organisations_views.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2083,6 +2083,8 @@ def test_organisation_user_can_get_their_detailed_permissions(
20832083
# Then
20842084
assert response.status_code == status.HTTP_200_OK
20852085
assert response.json()["admin"] is False
2086+
assert response.json()["is_directly_granted"] is False
2087+
assert response.json()["derived_from"] == {"groups": [], "roles": []}
20862088
assert response.json()["permissions"] == [
20872089
{
20882090
"permission_key": "CREATE_PROJECT",
@@ -2132,6 +2134,8 @@ def test_organisation_admin_can_get_detailed_permissions_of_other_user(
21322134
# Then
21332135
assert response.status_code == status.HTTP_200_OK
21342136
assert response.json()["admin"] is False
2137+
assert response.json()["is_directly_granted"] is False
2138+
assert response.json()["derived_from"] == {"groups": [], "roles": []}
21352139
assert response.json()["permissions"] == [
21362140
{
21372141
"permission_key": "CREATE_PROJECT",

api/tests/unit/permissions/test_unit_permissions_calculator.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ def test_project_permissions_calculator_get_permission_data( # type: ignore[no-
105105
group_project_permission.permissions.add(project_permissions[permission_key])
106106

107107
# When
108-
user_permission_data = get_project_permission_data(project.id, user_id=user.id)
108+
user_permission_data = get_project_permission_data(project, user=user)
109109

110110
# Then
111111
assert user_permission_data.admin == expected_admin
@@ -378,7 +378,6 @@ def test_permission_data_to_detailed_permissions_data() -> None:
378378
role_three_permission_data,
379379
],
380380
).to_detailed_permissions_data()
381-
382381
# Then
383382
for permission in detailed_permission_data.permissions:
384383
assert permission.permission_key in expected_permissions
@@ -389,3 +388,11 @@ def test_permission_data_to_detailed_permissions_data() -> None:
389388

390389
assert detailed_permission_data.admin is True
391390
assert len(detailed_permission_data.permissions) == 4
391+
assert detailed_permission_data.is_directly_granted is True
392+
assert detailed_permission_data.derived_from.groups == [
393+
group_one_permission_data.group,
394+
group_two_permission_data.group,
395+
]
396+
assert detailed_permission_data.derived_from.roles == [
397+
role_one_permission_data.role
398+
]

api/tests/unit/projects/test_unit_projects_views.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -964,6 +964,8 @@ def test_project_user_can_get_their_detailed_permissions(
964964
# Then
965965
assert response.status_code == status.HTTP_200_OK
966966
assert response.json()["admin"] is False
967+
assert response.json()["is_directly_granted"] is False
968+
assert response.json()["derived_from"] == {"groups": [], "roles": []}
967969
assert response.json()["permissions"] == [
968970
{
969971
"permission_key": "VIEW_PROJECT",
@@ -1013,6 +1015,8 @@ def test_project_admin_can_get_detailed_permissions_of_other_user(
10131015
# Then
10141016
assert response.status_code == status.HTTP_200_OK
10151017
assert response.json()["admin"] is False
1018+
assert response.json()["is_directly_granted"] is False
1019+
assert response.json()["derived_from"] == {"groups": [], "roles": []}
10161020
assert response.json()["permissions"] == [
10171021
{
10181022
"permission_key": "VIEW_PROJECT",

0 commit comments

Comments
 (0)