Kaynağa Gözat

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

jeremystretch 3 yıl önce
ebeveyn
işleme
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!}
 {!models/users/objectpermission.md!}
 
 
-### Example Constraint Definitions
+#### Example Constraint Definitions
 
 
 | Constraints | Description |
 | 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.
 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))
 #### 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
 ### Enhancements
 
 
 * [#1202](https://github.com/netbox-community/netbox/issues/1202) - Support overlapping assignment of NAT IP addresses
 * [#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.core.exceptions import ImproperlyConfigured
 from django.db.models import Q
 from django.db.models import Q
 
 
+from users.constants import CONSTRAINT_TOKEN_USER
 from users.models import ObjectPermission
 from users.models import ObjectPermission
 from utilities.permissions import (
 from utilities.permissions import (
     permission_is_exempt, qs_filter_from_constraints, resolve_permission, resolve_permission_ct,
     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}")
             raise ValueError(f"Invalid permission {perm} for model {model}")
 
 
         # Compile a QuerySet filter that matches all instances of the specified 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
         # 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,
         # 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.admin.widgets import FilteredSelectMultiple
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import FieldError, ValidationError
 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 users.models import ObjectPermission, Token
 from utilities.forms.fields import ContentTypeMultipleChoiceField
 from utilities.forms.fields import ContentTypeMultipleChoiceField
+from utilities.permissions import qs_filter_from_constraints
 
 
 __all__ = (
 __all__ = (
     'GroupAdminForm',
     'GroupAdminForm',
@@ -125,7 +125,10 @@ class ObjectPermissionForm(forms.ModelForm):
             for ct in object_types:
             for ct in object_types:
                 model = ct.model_class()
                 model = ct.model_class()
                 try:
                 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:
                 except FieldError as e:
                     raise ValidationError({
                     raise ValidationError({
                         'constraints': f'Invalid filter for {model}: {e}'
                         '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='auth', model__in=['group', 'user']) |
     Q(app_label='users', model__in=['objectpermission', 'token'])
     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
     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.
     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()
     params = Q()
     for constraint in constraints:
     for constraint in constraints:
         if constraint:
         if constraint:
-            params |= Q(**constraint)
+            params |= Q(**{k: _replace_tokens(v, tokens) for k, v in constraint.items()})
         else:
         else:
             # Found null constraint; permit model-level access
             # Found null constraint; permit model-level access
             return Q()
             return Q()

+ 5 - 1
netbox/utilities/querysets.py

@@ -1,5 +1,6 @@
 from django.db.models import QuerySet
 from django.db.models import QuerySet
 
 
+from users.constants import CONSTRAINT_TOKEN_USER
 from utilities.permissions import permission_is_exempt, qs_filter_from_constraints
 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
         # Filter the queryset to include only objects with allowed attributes
         else:
         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.
             # #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.
             # DISTINCT acts globally on the entire request, which may not be desirable.
             allowed_objects = self.model.objects.filter(attrs)
             allowed_objects = self.model.objects.filter(attrs)