Skip to content

Commit 18f3f09

Browse files
fix: prevent unauthorised access from group permissions (#5893)
1 parent 736e652 commit 18f3f09

21 files changed

Lines changed: 426 additions & 122 deletions

api/conftest.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -265,8 +265,10 @@ def staff_client(staff_user): # type: ignore[no-untyped-def]
265265

266266

267267
@pytest.fixture()
268-
def organisation(db, admin_user, staff_user): # type: ignore[no-untyped-def]
269-
org = Organisation.objects.create(name="Test Org")
268+
def organisation(
269+
db: None, admin_user: FFAdminUser, staff_user: FFAdminUser
270+
) -> Organisation:
271+
org: Organisation = Organisation.objects.create(name="Test Org")
270272
admin_user.add_organisation(org, role=OrganisationRole.ADMIN)
271273
staff_user.add_organisation(org, role=OrganisationRole.USER)
272274
return org

api/e2etests/e2e_seed_data.py

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def seed_data() -> None:
6868
password=PASSWORD,
6969
username=settings.E2E_USER,
7070
)
71-
org_admin.add_organisation(organisation, OrganisationRole.ADMIN) # type: ignore[no-untyped-call]
71+
org_admin.add_organisation(organisation, OrganisationRole.ADMIN)
7272
non_admin_user_with_org_permissions: FFAdminUser = FFAdminUser.objects.create_user( # type: ignore[no-untyped-call] # noqa: E501
7373
email=settings.E2E_NON_ADMIN_USER_WITH_ORG_PERMISSIONS,
7474
password=PASSWORD,
@@ -87,18 +87,10 @@ def seed_data() -> None:
8787
email=settings.E2E_NON_ADMIN_USER_WITH_A_ROLE,
8888
password=PASSWORD,
8989
)
90-
non_admin_user_with_org_permissions.add_organisation( # type: ignore[no-untyped-call]
91-
organisation,
92-
)
93-
non_admin_user_with_project_permissions.add_organisation( # type: ignore[no-untyped-call]
94-
organisation,
95-
)
96-
non_admin_user_with_env_permissions.add_organisation( # type: ignore[no-untyped-call]
97-
organisation,
98-
)
99-
non_admin_user_with_a_role.add_organisation( # type: ignore[no-untyped-call]
100-
organisation,
101-
)
90+
non_admin_user_with_org_permissions.add_organisation(organisation)
91+
non_admin_user_with_project_permissions.add_organisation(organisation)
92+
non_admin_user_with_env_permissions.add_organisation(organisation)
93+
non_admin_user_with_a_role.add_organisation(organisation)
10294

10395
# Add permissions to the non-admin user with org permissions
10496
user_org_permission = UserOrganisationPermission.objects.create(

api/permissions/permission_service.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,16 +86,25 @@ def get_permitted_projects_for_user(
8686
tag_ids=tag_ids,
8787
)
8888

89-
organisation_filter = Q(
89+
# The user has access to any projects belonging to organisations
90+
# they are an admin of
91+
admin_organisations_filter = Q(
9092
organisation__userorganisation__user=user,
9193
organisation__userorganisation__role=OrganisationRole.ADMIN.name,
9294
)
93-
project_ids_from_organisation = Project.objects.filter(
94-
organisation_filter
95+
project_ids_from_admin_organisations = Project.objects.filter(
96+
admin_organisations_filter
9597
).values_list("id", flat=True)
9698

97-
project_ids = project_ids_from_base_filter | set(project_ids_from_organisation)
98-
return Project.objects.filter(id__in=project_ids)
99+
project_ids = project_ids_from_base_filter | set(
100+
project_ids_from_admin_organisations
101+
)
102+
queryset = Project.objects.filter(id__in=project_ids)
103+
104+
# Final check to ensure that the user is a member of the organisation
105+
queryset = queryset.filter(organisation__users=user)
106+
107+
return queryset
99108

100109

101110
def get_permitted_projects_for_master_api_key(
@@ -154,6 +163,9 @@ def get_permitted_environments_for_user(
154163
if prefetch_metadata:
155164
queryset = queryset.prefetch_related("metadata")
156165

166+
# Final check to ensure the user is a member of the organisation
167+
queryset = queryset.filter(project__organisation__users=user)
168+
157169
# Description is defered due to Oracle support where a
158170
# query can't have a where clause if description is in
159171
# the select parameters. This leads to an N+1 query for
@@ -199,7 +211,12 @@ def user_has_organisation_permission(
199211
)
200212
filter_ = base_filter & Q(id=organisation.id)
201213

202-
return Organisation.objects.filter(filter_).exists() # type: ignore[no-any-return]
214+
queryset = Organisation.objects.filter(filter_)
215+
216+
# Final check to verify that user belongs to organisation
217+
queryset = queryset.filter(users=user)
218+
219+
return queryset.exists() # type: ignore[no-any-return]
203220

204221

205222
def master_api_key_has_organisation_permission(
@@ -219,6 +236,14 @@ def _is_user_object_admin(
219236
ModelClass = type(object_)
220237
base_filter = get_base_permission_filter(user, ModelClass) # type: ignore[arg-type]
221238
filter_ = base_filter & Q(id=object_.id)
239+
240+
if ModelClass is Project:
241+
filter_ = filter_ & Q(organisation__users=user)
242+
elif ModelClass is Environment:
243+
filter_ = filter_ & Q(project__organisation__users=user)
244+
else: # pragma: no cover
245+
raise ValueError(f"Unexpected object type {type(object_)}")
246+
222247
return ModelClass.objects.filter(filter_).exists()
223248

224249

api/tests/unit/environments/identities/traits/test_traits_views.py

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@
33

44
from common.environments.permissions import (
55
MANAGE_IDENTITIES,
6-
VIEW_ENVIRONMENT,
76
VIEW_IDENTITIES,
87
)
9-
from common.projects.permissions import VIEW_PROJECT
108
from django.test import override_settings
119
from django.urls import reverse
1210
from rest_framework import status
@@ -26,6 +24,7 @@
2624
from organisations.models import Organisation
2725
from permissions.models import PermissionModel
2826
from projects.models import Project, UserProjectPermission
27+
from users.models import FFAdminUser
2928

3029

3130
def test_can_set_trait_for_an_identity(
@@ -970,27 +969,21 @@ def test_set_trait_for_an_identity_is_not_throttled_by_user_throttle( # type: i
970969
assert res.status_code == status.HTTP_200_OK
971970

972971

973-
def test_user_with_manage_identities_permission_can_add_trait_for_identity( # type: ignore[no-untyped-def]
974-
environment, identity, django_user_model, api_client
975-
):
972+
def test_user_with_manage_identities_permission_can_add_trait_for_identity(
973+
environment: Environment,
974+
identity: Identity,
975+
staff_user: FFAdminUser,
976+
staff_client: APIClient,
977+
user_environment_permission: UserEnvironmentPermission,
978+
user_project_permission: UserProjectPermission,
979+
view_environment_permission: PermissionModel,
980+
manage_identities_permission: PermissionModel,
981+
view_project_permission: PermissionModel,
982+
) -> None:
976983
# Given
977-
user = django_user_model.objects.create(email="user@example.com")
978-
api_client.force_authenticate(user)
979-
980-
view_environment_permission = PermissionModel.objects.get(key=VIEW_ENVIRONMENT)
981-
manage_identities_permission = PermissionModel.objects.get(key=MANAGE_IDENTITIES)
982-
view_project_permission = PermissionModel.objects.get(key=VIEW_PROJECT)
983-
984-
user_environment_permission = UserEnvironmentPermission.objects.create(
985-
user=user, environment=environment
986-
)
987984
user_environment_permission.permissions.add(
988985
view_environment_permission, manage_identities_permission
989986
)
990-
991-
user_project_permission = UserProjectPermission.objects.create(
992-
user=user, project=environment.project
993-
)
994987
user_project_permission.permissions.add(view_project_permission)
995988

996989
url = reverse(
@@ -999,7 +992,7 @@ def test_user_with_manage_identities_permission_can_add_trait_for_identity( # t
999992
)
1000993

1001994
# When
1002-
response = api_client.post(
995+
response = staff_client.post(
1003996
url, data={"trait_key": "foo", "value_type": "unicode", "string_value": "foo"}
1004997
)
1005998

api/tests/unit/environments/permissions/test_unit_environments_permissions.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from common.projects.permissions import (
44
CREATE_ENVIRONMENT,
55
)
6+
from pytest_mock import MockerFixture
67

78
from environments.identities.models import Identity
89
from environments.models import Environment
@@ -45,24 +46,28 @@ def test_environment_admin_permissions_has_permissions_returns_false_for_non_adm
4546
assert has_permission is False
4647

4748

48-
def test_environment_admin_permissions_has_permissions_returns_true_for_admin_user( # type: ignore[no-untyped-def]
49-
environment, django_user_model, mocker
49+
def test_environment_admin_permissions_has_permissions_returns_true_for_admin_user(
50+
environment: Environment,
51+
staff_user: FFAdminUser,
52+
user_environment_permission: UserEnvironmentPermission,
53+
mocker: MockerFixture,
5054
) -> None:
5155
# Given
52-
user = django_user_model.objects.create(username="test_user")
53-
UserEnvironmentPermission.objects.create(
54-
user=user, environment=environment, admin=True
55-
)
5656
mocked_request = mocker.MagicMock()
57-
mocked_request.user = user
57+
mocked_request.user = staff_user
5858

5959
mocked_view = mocker.MagicMock()
6060
mocked_view.kwargs = {"environment_api_key": environment.api_key}
6161

62+
user_environment_permission.admin = True
63+
user_environment_permission.save()
64+
6265
# When
6366
has_permission = environment_admin_permissions.has_permission( # type: ignore[no-untyped-call]
6467
mocked_request, mocked_view
6568
)
69+
70+
# Then
6671
assert has_permission is True
6772

6873

api/tests/unit/environments/test_unit_environments_permissions.py

Lines changed: 43 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1+
from django.test import RequestFactory
2+
from pytest_mock import MockerFixture
3+
4+
from environments.models import Environment
15
from environments.permissions.models import UserEnvironmentPermission
26
from environments.permissions.permissions import NestedEnvironmentPermissions
37
from permissions.models import ENVIRONMENT_PERMISSION_TYPE, PermissionModel
8+
from users.models import FFAdminUser
49

510

611
def test_nested_environment_permissions_has_permission_false_if_no_env_key( # type: ignore[no-untyped-def]
@@ -19,28 +24,27 @@ def test_nested_environment_permissions_has_permission_false_if_no_env_key( # t
1924
assert result is False
2025

2126

22-
def test_nested_environment_permissions_has_permission_true_if_action_in_map( # type: ignore[no-untyped-def]
23-
rf, mocker, db, environment, django_user_model
24-
):
27+
def test_nested_environment_permissions_has_permission_true_if_action_in_map(
28+
rf: RequestFactory,
29+
mocker: MockerFixture,
30+
environment: Environment,
31+
staff_user: FFAdminUser,
32+
user_environment_permission: UserEnvironmentPermission,
33+
) -> None:
2534
# Given
2635
permission_key = "SOME_PERMISSION"
2736
permission = PermissionModel.objects.create(
2837
key=permission_key, type=ENVIRONMENT_PERMISSION_TYPE, description="foobar"
2938
)
39+
user_environment_permission.permissions.add(permission)
3040

3141
action = "retrieve"
3242
permissions = NestedEnvironmentPermissions(
3343
action_permission_map={action: permission.key}
3444
)
3545

36-
user = django_user_model.objects.create(email="test@example.com")
37-
user_env_permission = UserEnvironmentPermission.objects.create(
38-
user=user, environment=environment
39-
)
40-
user_env_permission.permissions.add(permission)
41-
4246
request = rf.get("/")
43-
request.user = user
47+
request.user = staff_user
4448
view = mocker.MagicMock(
4549
action=action, kwargs={"environment_api_key": environment.api_key}
4650
)
@@ -52,19 +56,21 @@ def test_nested_environment_permissions_has_permission_true_if_action_in_map( #
5256
assert has_permission is True
5357

5458

55-
def test_nested_environment_permissions_has_permission_if_create_and_user_is_admin( # type: ignore[no-untyped-def]
56-
rf, mocker, db, environment, django_user_model
57-
):
59+
def test_nested_environment_permissions_has_permission_if_create_and_user_is_admin(
60+
rf: RequestFactory,
61+
mocker: MockerFixture,
62+
environment: Environment,
63+
staff_user: FFAdminUser,
64+
user_environment_permission: UserEnvironmentPermission,
65+
) -> None:
5866
# Given
5967
permissions = NestedEnvironmentPermissions()
6068

61-
user = django_user_model.objects.create(email="test@example.com")
62-
UserEnvironmentPermission.objects.create(
63-
user=user, environment=environment, admin=True
64-
)
69+
user_environment_permission.admin = True
70+
user_environment_permission.save()
6571

6672
request = rf.get("/")
67-
request.user = user
73+
request.user = staff_user
6874
view = mocker.MagicMock(
6975
action="create", kwargs={"environment_api_key": environment.api_key}
7076
)
@@ -76,9 +82,13 @@ def test_nested_environment_permissions_has_permission_if_create_and_user_is_adm
7682
assert has_permission is True
7783

7884

79-
def test_nested_environment_permissions_has_object_permission_true_if_action_in_map( # type: ignore[no-untyped-def]
80-
rf, mocker, django_user_model, environment
81-
):
85+
def test_nested_environment_permissions_has_object_permission_true_if_action_in_map(
86+
rf: RequestFactory,
87+
mocker: MockerFixture,
88+
environment: Environment,
89+
staff_user: FFAdminUser,
90+
user_environment_permission: UserEnvironmentPermission,
91+
) -> None:
8292
# Given
8393
permission_key = "SOME_PERMISSION"
8494
permission = PermissionModel.objects.create(
@@ -90,14 +100,10 @@ def test_nested_environment_permissions_has_object_permission_true_if_action_in_
90100
action_permission_map={action: permission.key}
91101
)
92102

93-
user = django_user_model.objects.create(email="test@example.com")
94-
user_env_permission = UserEnvironmentPermission.objects.create(
95-
user=user, environment=environment
96-
)
97-
user_env_permission.permissions.add(permission)
103+
user_environment_permission.permissions.add(permission)
98104

99105
request = rf.get("/")
100-
request.user = user
106+
request.user = staff_user
101107
view = mocker.MagicMock(
102108
action=action, kwargs={"environment_api_key": environment.api_key}
103109
)
@@ -111,19 +117,21 @@ def test_nested_environment_permissions_has_object_permission_true_if_action_in_
111117
assert has_object_permission is True
112118

113119

114-
def test_nested_environment_permissions_has_object_permission_true_if_user_is_admin( # type: ignore[no-untyped-def]
115-
rf, mocker, django_user_model, environment
116-
):
120+
def test_nested_environment_permissions_has_object_permission_true_if_user_is_admin(
121+
rf: RequestFactory,
122+
mocker: MockerFixture,
123+
environment: Environment,
124+
staff_user: FFAdminUser,
125+
user_environment_permission: UserEnvironmentPermission,
126+
) -> None:
117127
# Given
118128
permissions = NestedEnvironmentPermissions()
119129

120-
user = django_user_model.objects.create(email="test@example.com")
121-
UserEnvironmentPermission.objects.create(
122-
user=user, environment=environment, admin=True
123-
)
130+
user_environment_permission.admin = True
131+
user_environment_permission.save()
124132

125133
request = rf.get("/")
126-
request.user = user
134+
request.user = staff_user
127135
view = mocker.MagicMock(
128136
action="action", kwargs={"environment_api_key": environment.api_key}
129137
)

api/tests/unit/features/test_unit_features_views.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1051,6 +1051,7 @@ def test_create_segment_override_staff(
10511051
staff_user: FFAdminUser,
10521052
staff_client: APIClient,
10531053
manage_segment_overrides_permission: PermissionModel,
1054+
user_environment_permission: UserEnvironmentPermission,
10541055
) -> None:
10551056
# Given
10561057
url = reverse(
@@ -1066,9 +1067,6 @@ def test_create_segment_override_staff(
10661067
"enabled": enabled,
10671068
"feature_segment": {"segment": segment.id},
10681069
}
1069-
user_environment_permission = UserEnvironmentPermission.objects.create(
1070-
user=staff_user, admin=False, environment=environment
1071-
)
10721070
user_environment_permission.permissions.add(manage_segment_overrides_permission)
10731071

10741072
response = staff_client.post(
@@ -1160,7 +1158,7 @@ def test_list_feature_states_from_simple_view_set(
11601158
# add another organisation with a project, environment and feature (which should be
11611159
# excluded)
11621160
another_organisation = Organisation.objects.create(name="another_organisation")
1163-
admin_user.add_organisation(another_organisation) # type: ignore[no-untyped-call]
1161+
admin_user.add_organisation(another_organisation)
11641162
another_project = Project.objects.create(
11651163
name="another_project", organisation=another_organisation
11661164
)

0 commit comments

Comments
 (0)