diff --git a/docs/administration/permissions.md b/docs/administration/permissions.md index f859266af..60717c28a 100644 --- a/docs/administration/permissions.md +++ b/docs/administration/permissions.md @@ -4,7 +4,7 @@ NetBox v2.9 introduced a new object-based permissions framework, which replaces {!models/users/objectpermission.md!} -### Example Constraint Definitions +#### Example Constraint Definitions | Constraints | Description | | ----------- | ----------- | diff --git a/docs/models/users/objectpermission.md b/docs/models/users/objectpermission.md index 48970dd05..075a2cae5 100644 --- a/docs/models/users/objectpermission.md +++ b/docs/models/users/objectpermission.md @@ -53,3 +53,17 @@ To achieve a logical OR with a different set of constraints, define multiple obj ``` Additionally, where multiple permissions have been assigned for an object type, their collective constraints will be merged using a logical "OR" operation. + +### Tokens + +!!! info "This feature was introduced in NetBox v3.3" + +When defining a permission constraint, administrators may use the special token `$user` to reference the current user at the time of evaluation. This can be helpful to restrict users to editing only their own journal entries, for example. Such a constraint might be defined as: + +```json +{ + "created_by": "$user" +} +``` + +The `$user` token can be used only as a constraint value, or as an item within a list of values. It cannot be modified or extended to reference specific user attributes. diff --git a/docs/release-notes/version-3.3.md b/docs/release-notes/version-3.3.md index a0abb81c4..ea9e67a38 100644 --- a/docs/release-notes/version-3.3.md +++ b/docs/release-notes/version-3.3.md @@ -15,6 +15,8 @@ #### Restrict API Tokens by Client IP ([#8233](https://github.com/netbox-community/netbox/issues/8233)) +#### Reference User in Permission Constraints ([#9074](https://github.com/netbox-community/netbox/issues/9074)) + ### Enhancements * [#1202](https://github.com/netbox-community/netbox/issues/1202) - Support overlapping assignment of NAT IP addresses diff --git a/netbox/netbox/authentication.py b/netbox/netbox/authentication.py index c16095fdc..62512943e 100644 --- a/netbox/netbox/authentication.py +++ b/netbox/netbox/authentication.py @@ -8,6 +8,7 @@ from django.contrib.auth.models import Group, AnonymousUser from django.core.exceptions import ImproperlyConfigured from django.db.models import Q +from users.constants import CONSTRAINT_TOKEN_USER from users.models import ObjectPermission from utilities.permissions import ( permission_is_exempt, qs_filter_from_constraints, resolve_permission, resolve_permission_ct, @@ -118,7 +119,10 @@ class ObjectPermissionMixin: raise ValueError(f"Invalid permission {perm} for model {model}") # Compile a QuerySet filter that matches all instances of the specified model - qs_filter = qs_filter_from_constraints(object_permissions[perm]) + tokens = { + CONSTRAINT_TOKEN_USER: user_obj, + } + qs_filter = qs_filter_from_constraints(object_permissions[perm], tokens) # Permission to perform the requested action on the object depends on whether the specified object matches # the specified constraints. Note that this check is made against the *database* record representing the object, diff --git a/netbox/users/admin/forms.py b/netbox/users/admin/forms.py index bc3d44862..540735ecc 100644 --- a/netbox/users/admin/forms.py +++ b/netbox/users/admin/forms.py @@ -3,11 +3,11 @@ from django.contrib.auth.models import Group, User from django.contrib.admin.widgets import FilteredSelectMultiple from django.contrib.contenttypes.models import ContentType from django.core.exceptions import FieldError, ValidationError -from django.db.models import Q -from users.constants import OBJECTPERMISSION_OBJECT_TYPES +from users.constants import CONSTRAINT_TOKEN_USER, OBJECTPERMISSION_OBJECT_TYPES from users.models import ObjectPermission, Token from utilities.forms.fields import ContentTypeMultipleChoiceField +from utilities.permissions import qs_filter_from_constraints __all__ = ( 'GroupAdminForm', @@ -125,7 +125,10 @@ class ObjectPermissionForm(forms.ModelForm): for ct in object_types: model = ct.model_class() try: - model.objects.filter(*[Q(**c) for c in constraints]).exists() + tokens = { + CONSTRAINT_TOKEN_USER: 0, # Replace token with a null user ID + } + model.objects.filter(qs_filter_from_constraints(constraints, tokens)).exists() except FieldError as e: raise ValidationError({ 'constraints': f'Invalid filter for {model}: {e}' diff --git a/netbox/users/constants.py b/netbox/users/constants.py index e6917c482..1e6e7c71c 100644 --- a/netbox/users/constants.py +++ b/netbox/users/constants.py @@ -6,3 +6,5 @@ OBJECTPERMISSION_OBJECT_TYPES = Q( Q(app_label='auth', model__in=['group', 'user']) | Q(app_label='users', model__in=['objectpermission', 'token']) ) + +CONSTRAINT_TOKEN_USER = '$user' diff --git a/netbox/utilities/permissions.py b/netbox/utilities/permissions.py index 123df9e45..b20aafce0 100644 --- a/netbox/utilities/permissions.py +++ b/netbox/utilities/permissions.py @@ -80,14 +80,25 @@ def permission_is_exempt(name): return False -def qs_filter_from_constraints(constraints): +def qs_filter_from_constraints(constraints, tokens=None): """ Construct a Q filter object from an iterable of ObjectPermission constraints. + + Args: + tokens: A dictionary mapping string tokens to be replaced with a value. """ + if tokens is None: + tokens = {} + + def _replace_tokens(value, tokens): + if type(value) is list: + return list(map(lambda v: tokens.get(v, v), value)) + return tokens.get(value, value) + params = Q() for constraint in constraints: if constraint: - params |= Q(**constraint) + params |= Q(**{k: _replace_tokens(v, tokens) for k, v in constraint.items()}) else: # Found null constraint; permit model-level access return Q() diff --git a/netbox/utilities/querysets.py b/netbox/utilities/querysets.py index 8ec6012bf..955a10d64 100644 --- a/netbox/utilities/querysets.py +++ b/netbox/utilities/querysets.py @@ -1,5 +1,6 @@ from django.db.models import QuerySet +from users.constants import CONSTRAINT_TOKEN_USER from utilities.permissions import permission_is_exempt, qs_filter_from_constraints @@ -28,7 +29,10 @@ class RestrictedQuerySet(QuerySet): # Filter the queryset to include only objects with allowed attributes else: - attrs = qs_filter_from_constraints(user._object_perm_cache[permission_required]) + tokens = { + CONSTRAINT_TOKEN_USER: user, + } + attrs = qs_filter_from_constraints(user._object_perm_cache[permission_required], tokens) # #8715: Avoid duplicates when JOIN on many-to-many fields without using DISTINCT. # DISTINCT acts globally on the entire request, which may not be desirable. allowed_objects = self.model.objects.filter(attrs)