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:
@ -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 |
|
||||
| ----------- | ----------- |
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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}'
|
||||
|
@ -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'
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
Reference in New Issue
Block a user