1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Refine queryset restriction logic

This commit is contained in:
Jeremy Stretch
2020-06-01 13:09:34 -04:00
parent 9679557747
commit 3a9512f086
3 changed files with 30 additions and 20 deletions

View File

@ -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 <app_label>.<action>_<model>
"""
try:
return name.split('.')[1].split('_')[0]
except ValueError:
raise ValueError(
f"Invalid permission name: {name}. Must be in the format <app_label>.<action>_<model>"
)
def resolve_permission(name): def resolve_permission(name):
""" """
Given a permission name, return the relevant ContentType and action. For example, "dcim.view_site" returns Given a permission name, return the relevant ContentType and action. For example, "dcim.view_site" returns
(Site, "view"). (Site, "view").
:param name: Permission name in the format <app>.<action>_<model> :param name: Permission name in the format <app_label>.<action>_<model>
""" """
app_label, codename = name.split('.') app_label, codename = name.split('.')
action, model_name = codename.split('_') action, model_name = codename.split('_')
@ -40,7 +54,7 @@ def permission_is_exempt(name):
""" """
Determine whether a specified permission is exempt from evaluation. Determine whether a specified permission is exempt from evaluation.
:param name: Permission name in the format <app>.<action>_<model> :param name: Permission name in the format <app_label>.<action>_<model>
""" """
app_label, codename = name.split('.') app_label, codename = name.split('.')
action, model_name = codename.split('_') action, model_name = codename.split('_')

View File

@ -1,5 +1,7 @@
from django.db.models import Q, QuerySet from django.db.models import Q, QuerySet
from utilities.permissions import permission_is_exempt
class DummyQuerySet: 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 Filter the QuerySet to return only objects on which the specified user has been granted the specified
permission. permission.
:param queryset: Base QuerySet to be restricted
:param user: User instance :param user: User instance
:param action: The action which must be permitted (e.g. "view" for "dcim.view_site") :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 model_name = self.model._meta.model_name
permission_required = f'{app_label}.{action}_{model_name}' permission_required = f'{app_label}.{action}_{model_name}'
# TODO: Handle anonymous users # Bypass restriction for superusers and exempt views
if not user.is_authenticated: if user.is_superuser or permission_is_exempt(permission_required):
return self return self
# Determine what constraints (if any) have been placed on this user for this action and model # User is anonymous or has not been granted the requisite permission
# TODO: Find a better way to ensure permissions are cached if not user.is_authenticated or permission_required not in user.get_all_permissions():
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:
return self.none() return self.none()
# Filter the queryset to include only objects with allowed attributes # Filter the queryset to include only objects with allowed attributes

View File

@ -28,7 +28,7 @@ from extras.models import CustomField, CustomFieldValue, ExportTemplate
from extras.querysets import CustomFieldQueryset from extras.querysets import CustomFieldQueryset
from utilities.exceptions import AbortTransaction from utilities.exceptions import AbortTransaction
from utilities.forms import BootstrapMixin, CSVDataField, TableConfigForm 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 utilities.utils import csv_format, prepare_cloned_fields
from .error_handlers import handle_protectederror from .error_handlers import handle_protectederror
from .forms import ConfirmationForm, ImportForm from .forms import ConfirmationForm, ImportForm
@ -60,16 +60,16 @@ class ObjectPermissionRequiredMixin(AccessMixin):
user = self.request.user user = self.request.user
permission_required = self.get_required_permission() permission_required = self.get_required_permission()
# First, check that the user is granted the required permission(s) at either the model or object level. # Check that the user has been granted the required permission(s).
if not user.has_perms((permission_required, *self.additional_permissions)): if user.has_perms((permission_required, *self.additional_permissions)):
return False
# Update the view's QuerySet to filter only the permitted objects # Update the view's QuerySet to filter only the permitted objects
if user.is_authenticated and not user.is_superuser: action = get_permission_action(permission_required)
action = permission_required.split('.')[1].split('_')[0]
self.queryset = self.queryset.restrict(user, action) self.queryset = self.queryset.restrict(user, action)
return True return True
return False
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):