Skip to content

Commit e6000ca

Browse files
committed
[change] Improved logic for hiding sensitive fields
1 parent 42c9734 commit e6000ca

3 files changed

Lines changed: 67 additions & 37 deletions

File tree

openwisp_users/api/mixins.py

Lines changed: 55 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,52 @@ class FilterByParentOwned(SharedObjectsLookup, FilterByParent):
154154
_user_attr = "organizations_owned"
155155

156156

157+
class HideSensitiveFieldsMixin:
158+
"""
159+
Mixin to hide sensitive fields in the serializer representation
160+
based on the organization of the user.
161+
"""
162+
163+
def get_sensitive_fields(self):
164+
"""
165+
Returns a list of sensitive fields that should be hidden.
166+
"""
167+
ModelClass = self.Meta.model
168+
return getattr(ModelClass, "sensitive_fields", [])
169+
170+
def _is_object_shared(self, instance):
171+
"""
172+
Returns the organization of the instance if it exists.
173+
"""
174+
view = self.context.get("view")
175+
organization_field = getattr(view, "organization_field", "organization_id")
176+
related_field = instance
177+
for field in organization_field.split("__"):
178+
if hasattr(related_field, field):
179+
related_field = getattr(related_field, field)
180+
else:
181+
return False
182+
return related_field is None
183+
184+
def hide_sensitive_fields(self, obj):
185+
request = self.context.get("request")
186+
if (
187+
request
188+
and not request.user.is_superuser
189+
and "organization" in obj
190+
and obj["organization"] is None
191+
):
192+
for field in self.get_sensitive_fields():
193+
if field in obj:
194+
del obj[field]
195+
return obj
196+
197+
def to_representation(self, data):
198+
rep = super().to_representation(data)
199+
self.hide_sensitive_fields(rep)
200+
return rep
201+
202+
157203
class FilterSerializerByOrganization(OrgLookup):
158204
"""
159205
Filter the options in browsable API for serializers
@@ -190,58 +236,37 @@ def filter_fields(self):
190236
except AttributeError:
191237
pass
192238

193-
def get_sensitive_fields(self):
194-
"""
195-
Returns a list of sensitive fields that should be hidden
196-
when the organization is None and the user is not a superuser.
197-
"""
198-
ModelClass = self.Meta.model
199-
return getattr(ModelClass, "sensitive_fields", [])
200-
201239
def __init__(self, *args, **kwargs):
202240
super().__init__(*args, **kwargs)
203241
# only filter related fields if the serializer
204242
# is being initiated during an HTTP request
205243
if "request" in self.context:
206244
self.filter_fields()
207245

208-
def to_representation(self, data):
209-
rep = super().to_representation(data)
210-
# Handle single object serializers
211-
self.hide_sensitive_fields(rep)
212-
return rep
213246

214-
def hide_sensitive_fields(self, obj):
215-
request = self.context.get("request")
216-
if (
217-
request
218-
and not request.user.is_superuser
219-
and "organization" in obj
220-
and obj["organization"] is None
221-
):
222-
for field in self.get_sensitive_fields():
223-
if field in obj:
224-
del obj[field]
225-
return obj
226-
227-
228-
class FilterSerializerByOrgMembership(FilterSerializerByOrganization):
247+
class FilterSerializerByOrgMembership(
248+
HideSensitiveFieldsMixin, FilterSerializerByOrganization
249+
):
229250
"""
230251
Filter serializer by organizations the user is member of
231252
"""
232253

233254
_user_attr = "organizations_dict"
234255

235256

236-
class FilterSerializerByOrgManaged(FilterSerializerByOrganization):
257+
class FilterSerializerByOrgManaged(
258+
HideSensitiveFieldsMixin, FilterSerializerByOrganization
259+
):
237260
"""
238261
Filter serializer by organizations managed by user
239262
"""
240263

241264
_user_attr = "organizations_managed"
242265

243266

244-
class FilterSerializerByOrgOwned(FilterSerializerByOrganization):
267+
class FilterSerializerByOrgOwned(
268+
HideSensitiveFieldsMixin, FilterSerializerByOrganization
269+
):
245270
"""
246271
Filter serializer by organizations owned by user
247272
"""

openwisp_users/multitenancy.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,23 +80,25 @@ def get_queryset(self, request):
8080
# if there is no organization field, return the queryset as is
8181
return qs
8282
return qs.filter(
83-
Q(**{f"{self.org_field}__in": user.organizations_managed}) | Q(**{self.org_field: None})
83+
Q(**{f"{self.org_field}__in": user.organizations_managed})
84+
| Q(**{self.org_field: None})
8485
)
8586

8687
def get_search_results(self, request, queryset, search_term):
8788
"""
8889
Override to ensure that the search results are filtered by the
8990
organization of the current user.
9091
"""
91-
if request.GET.get('field_name') and not request.user.is_superuser and not self.multitenant_shared_relations:
92+
if (
93+
request.GET.get("field_name")
94+
and not request.user.is_superuser
95+
and not self.multitenant_shared_relations
96+
):
9297
queryset = queryset.filter(
93-
**{
94-
f"{self.org_field}__in": request.user.organizations_managed
95-
}
98+
**{f"{self.org_field}__in": request.user.organizations_managed}
9699
)
97100
return super().get_search_results(request, queryset, search_term)
98101

99-
100102
def _has_org_permission(self, request, obj, perm_func):
101103
"""
102104
Helper method to check object-level permissions for users

openwisp_users/tests/test_api/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,10 @@ def _test_access_shared_object(
4747
**auth,
4848
)
4949
self.assertEqual(response.status_code, expected_status_codes["create"])
50-
if expected_status_codes["create"] == 400 and 'organization' in create_payload:
50+
if (
51+
expected_status_codes["create"] == 400
52+
and "organization" in create_payload
53+
):
5154
self.assertEqual(
5255
str(response.data["organization"][0]),
5356
"This field may not be null.",

0 commit comments

Comments
 (0)