Explorar o código

Closes #9074: Enable referencing the current user when evaluating permission constraints

jeremystretch %!s(int64=3) %!d(string=hai) anos
pai
achega
12c138b341

+ 1 - 1
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 |
 | ----------- | ----------- |

+ 14 - 0
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.

+ 2 - 0
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

+ 5 - 1
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,

+ 6 - 3
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}'

+ 2 - 0
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'

+ 13 - 2
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()

+ 5 - 1
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)