Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions docs/user/basic-concepts.rst
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ different roles in each.

Here's a summary of the default organization roles.

.. _users_organization_manager:

Organization Manager
~~~~~~~~~~~~~~~~~~~~

Expand Down Expand Up @@ -241,6 +243,18 @@ objects are defined and managed by super administrators and can include
configurations, policies, or other data that need to be consistent across
all organizations.

Shared objects can only be created by superusers. Non-superusers (e.g.
:ref:`users_organization_manager`) have view-only access to shared
objects, both through the admin interface and the REST API. However, they
can use these shared objects when creating related organization-specific
resources. For example, an organization manager can use a shared VPN
server to create a configuration template for their organization.

In some cases, non-superusers may be restricted from viewing sensitive
details of shared objects—particularly if such information could allow
them to gain unauthorized access to infrastructure or data used by other
organizations.

By sharing common resources, global uniformity and consistency can be
enforced across the entire system.

Expand Down
27 changes: 27 additions & 0 deletions openwisp_users/api/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,13 +182,40 @@ def filter_fields(self):
except AttributeError:
pass

def get_sensitive_fields(self):
"""
Returns a list of sensitive fields that should be hidden
when the organization is None and the user is not a superuser.
"""
ModelClass = self.Meta.model
return getattr(ModelClass, "sensitive_fields", [])

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# only filter related fields if the serializer
# is being initiated during an HTTP request
if "request" in self.context:
self.filter_fields()

def to_representation(self, data):
rep = super().to_representation(data)
# Handle single object serializers
self.hide_sensitive_fields(rep)
return rep

def hide_sensitive_fields(self, obj):
request = self.context.get("request")
if (
request
and not request.user.is_superuser
and "organization" in obj
and obj["organization"] is None
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WIP: The organization field will be made configurable.

):
for field in self.get_sensitive_fields():
if field in obj:
del obj[field]
return obj


class FilterSerializerByOrgMembership(FilterSerializerByOrganization):
"""
Expand Down
63 changes: 59 additions & 4 deletions openwisp_users/multitenancy.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ class MultitenantAdminMixin(object):
multitenant_shared_relations = None
multitenant_parent = None

def get_sensitive_fields(self, request, obj=None):
return getattr(self.model, "sensitive_fields", [])

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
parent = self.multitenant_parent
Expand All @@ -37,6 +40,21 @@ def get_repr(self, obj):

get_repr.short_description = _("name")

def get_fields(self, request, obj=None):
"""
Return the list of fields to be displayed in the admin.

If the user is not a superuser, it will remove sensitive fields.
"""
fields = super().get_fields(request, obj)
if obj and not request.user.is_superuser:
if self.multitenant_parent:
obj = getattr(obj, self.multitenant_parent)
if getattr(obj, "organization_id", None) is None:
sensitive_fields = self.get_sensitive_fields(request, obj)
return [f for f in fields if f not in sensitive_fields]
return fields

def get_queryset(self, request):
"""
If current user is not superuser, show only the
Expand All @@ -49,14 +67,49 @@ def get_queryset(self, request):
if user.is_superuser:
return qs
if hasattr(self.model, "organization"):
return qs.filter(organization__in=user.organizations_managed)
return qs.filter(
Q(organization__in=user.organizations_managed) | Q(organization=None)
)
if self.model.__name__ == "Organization":
return qs.filter(pk__in=user.organizations_managed)
elif not self.multitenant_parent:
return qs
else:
qsarg = "{0}__organization__in".format(self.multitenant_parent)
return qs.filter(**{qsarg: user.organizations_managed})
qsarg = f"{self.multitenant_parent}__organization"
return qs.filter(
Q(**{f"{qsarg}__in": user.organizations_managed}) | Q(**{qsarg: None})
)

def _has_org_permission(self, request, obj, perm_func):
"""
Helper method to check object-level permissions for users
associated with specific organizations.
"""
perm = perm_func(request, obj)
if obj and self.multitenant_parent:
# In case of a multitenant parent, we need to check if the
# user has permission on the parent object.
obj = getattr(obj, self.multitenant_parent)
if not request.user.is_superuser and obj and hasattr(obj, "organization_id"):
perm = perm and (
obj.organization_id
and str(obj.organization_id) in request.user.organizations_managed
)
return perm

def has_change_permission(self, request, obj=None):
"""
Returns True if the user has permission to change the object.
Non-superusers cannot change shared objects.
"""
return self._has_org_permission(request, obj, super().has_change_permission)

def has_delete_permission(self, request, obj=None):
"""
Returns True if the user has permission to delete the object.
Non-superusers cannot change shared objects.
"""
return self._has_org_permission(request, obj, super().has_delete_permission)

def _edit_form(self, request, form):
"""
Expand Down Expand Up @@ -149,7 +202,9 @@ class MultitenantOrgFilter(AutocompleteFilter):
org_lookup = "id__in"
title = _("organization")
widget_attrs = AutocompleteFilter.widget_attrs.copy()
widget_attrs.update({"data-empty-label": SHARED_SYSTEMWIDE_LABEL})
widget_attrs.update(
{"data-empty-label": SHARED_SYSTEMWIDE_LABEL, "data-is-filter": "true"}
)


class MultitenantRelatedOrgFilter(MultitenantOrgFilter):
Expand Down
39 changes: 39 additions & 0 deletions openwisp_users/static/admin/js/autocomplete.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Custom override of Django's autocomplete.js to add the "is_filter" parameter
// to AJAX requests. This parameter enables the backend autocomplete view to
// determine if the request is for filtering, allowing it to include the shared
// option when appropriate (e.g., for filtering shared objects in the admin).

"use strict";
{
const $ = django.jQuery;

$.fn.djangoAdminSelect2 = function () {
$.each(this, function (i, element) {
$(element).select2({
ajax: {
data: (params) => {
return {
term: params.term,
page: params.page,
app_label: element.dataset.appLabel,
model_name: element.dataset.modelName,
field_name: element.dataset.fieldName,
is_filter: element.dataset.isFilter,
};
},
},
});
});
return this;
};

$(function () {
// Initialize all autocomplete widgets except the one in the template
// form used when a new formset is added.
$(".admin-autocomplete").not("[name*=__prefix__]").djangoAdminSelect2();
});

document.addEventListener("formset:added", (event) => {
$(event.target).find(".admin-autocomplete").djangoAdminSelect2();
});
}
Loading
Loading