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!}
### Example Constraint Definitions
#### Example Constraint Definitions
| 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.
### 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))
#### 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

View File

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

View File

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

View File

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

View File

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

View File

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