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!}
|
{!models/users/objectpermission.md!}
|
||||||
|
|
||||||
### Example Constraint Definitions
|
#### Example Constraint Definitions
|
||||||
|
|
||||||
| Constraints | Description |
|
| 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.
|
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))
|
#### 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
|
||||||
|
@ -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,
|
||||||
|
@ -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}'
|
||||||
|
@ -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'
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
|
Reference in New Issue
Block a user