Skip to content

Commit e81b36f

Browse files
committed
[change] Hide sensitive fields from API response
1 parent e7d6eaa commit e81b36f

File tree

6 files changed

+114
-3
lines changed

6 files changed

+114
-3
lines changed

openwisp_users/api/mixins.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,13 +182,45 @@ def filter_fields(self):
182182
except AttributeError:
183183
pass
184184

185+
def get_sensitive_fields(self):
186+
"""
187+
Returns a list of sensitive fields that should be hidden
188+
when the organization is None and the user is not a superuser.
189+
"""
190+
ModelClass = self.Meta.model
191+
return getattr(ModelClass, "sensitive_fields", [])
192+
185193
def __init__(self, *args, **kwargs):
186194
super().__init__(*args, **kwargs)
187195
# only filter related fields if the serializer
188196
# is being initiated during an HTTP request
189197
if "request" in self.context:
190198
self.filter_fields()
191199

200+
def to_representation(self, data):
201+
rep = super().to_representation(data)
202+
if not isinstance(rep, dict):
203+
# Handle list serializers
204+
for obj in rep:
205+
self.hide_sensitive_fields(obj)
206+
else:
207+
# Handle single object serializers
208+
self.hide_sensitive_fields(rep)
209+
return rep
210+
211+
def hide_sensitive_fields(self, obj):
212+
request = self.context.get("request")
213+
if (
214+
request
215+
and not request.user.is_superuser
216+
and "organization" in obj
217+
and obj["organization"] is None
218+
):
219+
for field in self.get_sensitive_fields():
220+
if field in obj:
221+
del obj[field]
222+
return obj
223+
192224

193225
class FilterSerializerByOrgMembership(FilterSerializerByOrganization):
194226
"""

openwisp_users/multitenancy.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,9 @@ class MultitenantAdminMixin(object):
2020

2121
multitenant_shared_relations = None
2222
multitenant_parent = None
23-
sensitive_fields = []
2423

2524
def get_sensitive_fields(self, request, obj=None):
26-
return self.sensitive_fields
25+
return getattr(self.model, "sensitive_fields", [])
2726

2827
def __init__(self, *args, **kwargs):
2928
super().__init__(*args, **kwargs)

openwisp_users/tests/test_api/__init__.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,67 @@ def _test_superuser_access_shared_object(
165165
expected_status_codes=expected_status_codes,
166166
)
167167

168+
def _test_sensitive_fields_visibility_on_shared_and_org_objects(
169+
self,
170+
sensitive_fields,
171+
shared_obj,
172+
org_obj,
173+
detailview_name,
174+
listview_name,
175+
organization,
176+
org_admin=None,
177+
super_user=None,
178+
):
179+
def assert_sensitive_fields_visibility(obj, user, should_be_visible=False):
180+
token = self._obtain_auth_token(user.username, "tester")
181+
auth = {"HTTP_AUTHORIZATION": f"Bearer {token}"}
182+
# List view
183+
listview_path = reverse(listview_name)
184+
response = self.client.get(listview_path, **auth)
185+
self.assertEqual(response.status_code, 200)
186+
results = (
187+
response.data
188+
if "results" not in response.data
189+
else response.data["results"]
190+
)
191+
for item in results:
192+
if str(item["id"]) == str(obj.pk):
193+
break
194+
for field in sensitive_fields:
195+
self.assertEqual(
196+
field in item,
197+
should_be_visible,
198+
)
199+
# Detail view
200+
detailview_path = reverse(detailview_name, args=[obj.pk])
201+
response = self.client.get(detailview_path, **auth)
202+
self.assertEqual(response.status_code, 200)
203+
for field in sensitive_fields:
204+
if should_be_visible:
205+
self.assertIn(field, response.data)
206+
else:
207+
self.assertNotIn(field, response.data)
208+
209+
org_admin = org_admin or self._create_administrator(
210+
organizations=[organization]
211+
)
212+
super_user = super_user or self._get_admin()
213+
214+
with self.subTest("Org admin should not see sensitive fields in shared object"):
215+
assert_sensitive_fields_visibility(
216+
shared_obj, org_admin, should_be_visible=False
217+
)
218+
219+
with self.subTest("Org admin should see sensitive fields in org object"):
220+
assert_sensitive_fields_visibility(
221+
org_obj, org_admin, should_be_visible=True
222+
)
223+
224+
with self.subTest("Superuser should see sensitive fields in shared object"):
225+
assert_sensitive_fields_visibility(
226+
shared_obj, super_user, should_be_visible=True
227+
)
228+
168229

169230
class APITestCase(TestMultitenantApiMixin, AuthenticationMixin, TestCase):
170231
pass

tests/testapp/admin.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def change_view(self, request, object_id, form_url="", extra_context=None):
6161

6262

6363
class TemplateAdmin(BaseAdmin):
64-
sensitive_fields = ["secrets"]
64+
pass
6565

6666

6767
class TagAdmin(BaseAdmin):

tests/testapp/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88

99
class Template(ShareableOrgMixin):
10+
sensitive_fields = ["secrets"]
1011
name = models.CharField(max_length=16)
1112
secrets = models.TextField(
1213
blank=True,

tests/testapp/tests/test_permission_classes.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,3 +291,21 @@ def test_org_user_access_shared_object(self):
291291
"option": 200,
292292
},
293293
)
294+
295+
def test_template_sensitive_fields_visibility(self):
296+
"""
297+
Test that sensitive fields are hidden for shared objects for non-superusers.
298+
"""
299+
org = self._get_org()
300+
shared_template = self._create_template(
301+
organization=None, secrets="shared-secret"
302+
)
303+
org_template = self._create_template(organization=org, secrets="org-secret")
304+
self._test_sensitive_fields_visibility_on_shared_and_org_objects(
305+
sensitive_fields=["secrets"],
306+
shared_obj=shared_template,
307+
org_obj=org_template,
308+
detailview_name="test_template_detail",
309+
listview_name="test_template_list",
310+
organization=org,
311+
)

0 commit comments

Comments
 (0)