mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-14 01:41:22 -06:00
Extend ObjectPermission constraints to OR multiple JSON objects
This commit is contained in:
parent
b1ec332a56
commit
4a516103a6
@ -6,13 +6,15 @@ NetBox v2.9 introduced a new object-based permissions framework, which replace's
|
||||
|
||||
### Example Constraint Definitions
|
||||
|
||||
| Query Filter | Permission Constraints |
|
||||
| ------------ | --------------------- |
|
||||
| `filter(status='active')` | `{"status": "active"}` |
|
||||
| `filter(status='active', role='testing')` | `{"status": "active", "role": "testing"}` |
|
||||
| `filter(status__in=['planned', 'reserved'])` | `{"status__in": ["planned", "reserved"]}` |
|
||||
| `filter(name__startswith('Foo')` | `{"name__startswith": "Foo"}` |
|
||||
| `filter(vid__gte=100, vid__lt=200)` | `{"vid__gte": 100, "vid__lt": 200}` |
|
||||
| Constraints | Description |
|
||||
| ----------- | ----------- |
|
||||
| `{"status": "active"}` | Status is active |
|
||||
| `{"status__in": ["planned", "reserved"]}` | Status is active **OR** reserved |
|
||||
| `{"status": "active", "role": "testing"}` | Status is active **OR** role is testing |
|
||||
| `{"name__startswith": "Foo"}` | Name starts with "Foo" (case-sensitive) |
|
||||
| `{"name__iendswith": "bar"}` | Name ends with "bar" (case-insensitive) |
|
||||
| `{"vid__gte": 100, "vid__lt": 200}` | VLAN ID is greater than or equal to 100 **AND** less than 200 |
|
||||
| `[{"vid__lt": 200}, {"status": "reserved"}]` | VLAN ID is less than 200 **OR** status is reserved |
|
||||
|
||||
## Permissions Enforcement
|
||||
|
||||
|
@ -25,9 +25,9 @@ In addition to these, permissions can also grant custom actions that may be requ
|
||||
|
||||
## Constraints
|
||||
|
||||
Constraints are expressed as a JSON object representing a [Django query filter](https://docs.djangoproject.com/en/stable/ref/models/querysets/#field-lookups). This is the same syntax that you would pass to the QuerySet `filter()` method when performing a query using the Django ORM. As with query filters, double underscores can be used to traverse related objects or invoke lookup expressions. Some example queries and their corresponding definitions are shown below.
|
||||
Constraints are expressed as a JSON object or list representing a [Django query filter](https://docs.djangoproject.com/en/stable/ref/models/querysets/#field-lookups). This is the same syntax that you would pass to the QuerySet `filter()` method when performing a query using the Django ORM. As with query filters, double underscores can be used to traverse related objects or invoke lookup expressions. Some example queries and their corresponding definitions are shown below.
|
||||
|
||||
All constraints defined on a permission are applied with a logical AND. For example, suppose you assign a permission for the site model with the following constraints.
|
||||
All attributes defined within a single JSON object are applied with a logical AND. For example, suppose you assign a permission for the site model with the following constraints.
|
||||
|
||||
```json
|
||||
{
|
||||
@ -36,4 +36,20 @@ All constraints defined on a permission are applied with a logical AND. For exam
|
||||
}
|
||||
```
|
||||
|
||||
The permission will grant access only to sites which have a status of "active" **and** which are assigned to the "Americas" region. To achieve a logical OR with a different set of constraints, simply create another permission assignment for the same model and user/group.
|
||||
The permission will grant access only to sites which have a status of "active" **and** which are assigned to the "Americas" region.
|
||||
|
||||
To achieve a logical OR with a different set of constraints, define multiple objects within a list. For example, if you want to constrain the permission to VLANs with an ID between 100 and 199 _or_ a status of "reserved," do the following:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"vid__gte": 100,
|
||||
"vid__lt": 200
|
||||
},
|
||||
{
|
||||
"status": "reserved"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Additionally, where multiple permissions have been assigned for an object type, their collective constraints will be merged using a logical "OR" operation.
|
||||
|
@ -75,7 +75,10 @@ class ObjectPermissionBackend(ModelBackend):
|
||||
obj_perm_constraints = self.get_all_permissions(user_obj)[perm]
|
||||
constraints = Q()
|
||||
for perm_constraints in obj_perm_constraints:
|
||||
if perm_constraints:
|
||||
if type(perm_constraints) is list:
|
||||
for c in obj_perm_constraints:
|
||||
constraints |= Q(**c)
|
||||
elif perm_constraints:
|
||||
constraints |= Q(**perm_constraints)
|
||||
else:
|
||||
# Found ObjectPermission with null constraints; allow model-level access
|
||||
|
@ -4,6 +4,7 @@ from django.contrib.auth.admin import UserAdmin as UserAdmin_
|
||||
from django.contrib.auth.models import Group, User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import FieldError, ValidationError
|
||||
from django.db.models import Q
|
||||
|
||||
from extras.admin import order_content_types
|
||||
from .models import AdminGroup, AdminUser, ObjectPermission, Token, UserConfig
|
||||
@ -136,7 +137,8 @@ class ObjectPermissionForm(forms.ModelForm):
|
||||
help_texts = {
|
||||
'actions': 'Actions granted in addition to those listed above',
|
||||
'constraints': 'JSON expression of a queryset filter that will return only permitted objects. Leave null '
|
||||
'to match all objects of this type.'
|
||||
'to match all objects of this type. A list of multiple objects will result in a logical OR '
|
||||
'operation.'
|
||||
}
|
||||
labels = {
|
||||
'actions': 'Additional actions'
|
||||
@ -167,8 +169,8 @@ class ObjectPermissionForm(forms.ModelForm):
|
||||
self.instance.actions.remove(action)
|
||||
|
||||
def clean(self):
|
||||
object_types = self.cleaned_data['object_types']
|
||||
constraints = self.cleaned_data['constraints']
|
||||
object_types = self.cleaned_data.get('object_types')
|
||||
constraints = self.cleaned_data.get('constraints')
|
||||
|
||||
# Append any of the selected CRUD checkboxes to the actions list
|
||||
if not self.cleaned_data.get('actions'):
|
||||
@ -184,10 +186,13 @@ class ObjectPermissionForm(forms.ModelForm):
|
||||
# Validate the specified model constraints by attempting to execute a query. We don't care whether the query
|
||||
# returns anything; we just want to make sure the specified constraints are valid.
|
||||
if constraints:
|
||||
# Normalize the constraints to a list of dicts
|
||||
if type(constraints) is not list:
|
||||
constraints = [constraints]
|
||||
for ct in object_types:
|
||||
model = ct.model_class()
|
||||
try:
|
||||
model.objects.filter(**constraints).exists()
|
||||
model.objects.filter(*[Q(**c) for c in constraints]).exists()
|
||||
except FieldError as e:
|
||||
raise ValidationError({
|
||||
'constraints': f'Invalid filter for {model}: {e}'
|
||||
|
@ -44,8 +44,15 @@ class RestrictedQuerySet(QuerySet):
|
||||
else:
|
||||
attrs = Q()
|
||||
for perm_attrs in user._object_perm_cache[permission_required]:
|
||||
if perm_attrs:
|
||||
if type(perm_attrs) is list:
|
||||
for p in perm_attrs:
|
||||
attrs |= Q(**p)
|
||||
elif perm_attrs:
|
||||
attrs |= Q(**perm_attrs)
|
||||
else:
|
||||
# Any permission with null constraints grants access to _all_ instances
|
||||
attrs = Q()
|
||||
break
|
||||
qs = self.filter(attrs)
|
||||
|
||||
return qs
|
||||
|
Loading…
Reference in New Issue
Block a user