diff --git a/netbox/utilities/permissions.py b/netbox/utilities/permissions.py index de024cf99..38064b689 100644 --- a/netbox/utilities/permissions.py +++ b/netbox/utilities/permissions.py @@ -19,12 +19,26 @@ def get_permission_for_model(model, action): ) +def get_permission_action(name): + """ + Return the action component (e.g. view or add) from a permission name. + + :param name: Permission name in the format ._ + """ + try: + return name.split('.')[1].split('_')[0] + except ValueError: + raise ValueError( + f"Invalid permission name: {name}. Must be in the format ._" + ) + + def resolve_permission(name): """ Given a permission name, return the relevant ContentType and action. For example, "dcim.view_site" returns (Site, "view"). - :param name: Permission name in the format ._ + :param name: Permission name in the format ._ """ app_label, codename = name.split('.') action, model_name = codename.split('_') @@ -40,7 +54,7 @@ def permission_is_exempt(name): """ Determine whether a specified permission is exempt from evaluation. - :param name: Permission name in the format ._ + :param name: Permission name in the format ._ """ app_label, codename = name.split('.') action, model_name = codename.split('_') diff --git a/netbox/utilities/querysets.py b/netbox/utilities/querysets.py index 6649e4d9c..1ac79e90a 100644 --- a/netbox/utilities/querysets.py +++ b/netbox/utilities/querysets.py @@ -1,5 +1,7 @@ from django.db.models import Q, QuerySet +from utilities.permissions import permission_is_exempt + class DummyQuerySet: """ @@ -19,7 +21,6 @@ class RestrictedQuerySet(QuerySet): Filter the QuerySet to return only objects on which the specified user has been granted the specified permission. - :param queryset: Base QuerySet to be restricted :param user: User instance :param action: The action which must be permitted (e.g. "view" for "dcim.view_site") """ @@ -28,17 +29,12 @@ class RestrictedQuerySet(QuerySet): model_name = self.model._meta.model_name permission_required = f'{app_label}.{action}_{model_name}' - # TODO: Handle anonymous users - if not user.is_authenticated: + # Bypass restriction for superusers and exempt views + if user.is_superuser or permission_is_exempt(permission_required): return self - # Determine what constraints (if any) have been placed on this user for this action and model - # TODO: Find a better way to ensure permissions are cached - if not hasattr(user, '_object_perm_cache'): - user.get_all_permissions() - - # User has not been granted any permission - if permission_required not in user._object_perm_cache: + # User is anonymous or has not been granted the requisite permission + if not user.is_authenticated or permission_required not in user.get_all_permissions(): return self.none() # Filter the queryset to include only objects with allowed attributes diff --git a/netbox/utilities/views.py b/netbox/utilities/views.py index f59492a0c..0304780f3 100644 --- a/netbox/utilities/views.py +++ b/netbox/utilities/views.py @@ -28,7 +28,7 @@ from extras.models import CustomField, CustomFieldValue, ExportTemplate from extras.querysets import CustomFieldQueryset from utilities.exceptions import AbortTransaction from utilities.forms import BootstrapMixin, CSVDataField, TableConfigForm -from utilities.permissions import get_permission_for_model +from utilities.permissions import get_permission_action, get_permission_for_model from utilities.utils import csv_format, prepare_cloned_fields from .error_handlers import handle_protectederror from .forms import ConfirmationForm, ImportForm @@ -60,16 +60,16 @@ class ObjectPermissionRequiredMixin(AccessMixin): user = self.request.user permission_required = self.get_required_permission() - # First, check that the user is granted the required permission(s) at either the model or object level. - if not user.has_perms((permission_required, *self.additional_permissions)): - return False + # Check that the user has been granted the required permission(s). + if user.has_perms((permission_required, *self.additional_permissions)): - # Update the view's QuerySet to filter only the permitted objects - if user.is_authenticated and not user.is_superuser: - action = permission_required.split('.')[1].split('_')[0] + # Update the view's QuerySet to filter only the permitted objects + action = get_permission_action(permission_required) self.queryset = self.queryset.restrict(user, action) - return True + return True + + return False def dispatch(self, request, *args, **kwargs):