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

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

This commit is contained in:
jeremystretch
2022-07-01 13:34:10 -04:00
parent c6dfdf10e5
commit 12c138b341
8 changed files with 48 additions and 8 deletions

View File

@ -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 |
| ----------- | ----------- | | ----------- | ----------- |

View File

@ -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.

View File

@ -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

View File

@ -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,

View File

@ -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}'

View File

@ -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'

View File

@ -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()

View File

@ -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)