mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-25 00:36:11 -06:00
Enforce object permissions when returning search results
This commit is contained in:
parent
e23b4b5357
commit
a7cde92113
@ -2,6 +2,8 @@ from django.contrib.contenttypes.fields import GenericForeignKey
|
|||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
from utilities.fields import RestrictedGenericForeignKey
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'CachedValue',
|
'CachedValue',
|
||||||
)
|
)
|
||||||
@ -18,7 +20,7 @@ class CachedValue(models.Model):
|
|||||||
related_name='+'
|
related_name='+'
|
||||||
)
|
)
|
||||||
object_id = models.PositiveBigIntegerField()
|
object_id = models.PositiveBigIntegerField()
|
||||||
object = GenericForeignKey(
|
object = RestrictedGenericForeignKey(
|
||||||
ct_field='object_type',
|
ct_field='object_type',
|
||||||
fk_field='object_id'
|
fk_field='object_id'
|
||||||
)
|
)
|
||||||
|
@ -11,6 +11,7 @@ from django.db.models.signals import post_delete, post_save
|
|||||||
from extras.models import CachedValue
|
from extras.models import CachedValue
|
||||||
from extras.registry import registry
|
from extras.registry import registry
|
||||||
from netbox.constants import SEARCH_MAX_RESULTS
|
from netbox.constants import SEARCH_MAX_RESULTS
|
||||||
|
from utilities.querysets import RestrictedPrefetch
|
||||||
from . import FieldTypes, LookupTypes, SearchResult, get_registry
|
from . import FieldTypes, LookupTypes, SearchResult, get_registry
|
||||||
|
|
||||||
# The cache for the initialized backend.
|
# The cache for the initialized backend.
|
||||||
@ -151,6 +152,7 @@ class CachedValueSearchBackend(SearchBackend):
|
|||||||
def search(self, request, value, lookup=DEFAULT_LOOKUP_TYPE):
|
def search(self, request, value, lookup=DEFAULT_LOOKUP_TYPE):
|
||||||
|
|
||||||
# Define the search parameters
|
# Define the search parameters
|
||||||
|
# TODO: Filter object types to only those which the use has permission to view
|
||||||
params = {
|
params = {
|
||||||
f'value__{lookup}': value
|
f'value__{lookup}': value
|
||||||
}
|
}
|
||||||
@ -168,14 +170,24 @@ class CachedValueSearchBackend(SearchBackend):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Construct a Prefetch to pre-fetch only those related objects for which the
|
||||||
|
# user has permission to view.
|
||||||
|
prefetch = RestrictedPrefetch('object', request.user, 'view')
|
||||||
|
|
||||||
# Wrap the base query to return only the lowest-weight result for each object
|
# Wrap the base query to return only the lowest-weight result for each object
|
||||||
# Hat-tip to https://blog.oyam.dev/django-filter-by-window-function/ for the solution
|
# Hat-tip to https://blog.oyam.dev/django-filter-by-window-function/ for the solution
|
||||||
sql, params = queryset.query.sql_with_params()
|
sql, params = queryset.query.sql_with_params()
|
||||||
return CachedValue.objects.prefetch_related('object').raw(
|
results = CachedValue.objects.prefetch_related(prefetch).raw(
|
||||||
f"SELECT * FROM ({sql}) t WHERE row_number = 1",
|
f"SELECT * FROM ({sql}) t WHERE row_number = 1",
|
||||||
params
|
params
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Omit any results pertaining to an object the user does not have permission to view
|
||||||
|
# TODO: We'll have to figure out how to handle pagination
|
||||||
|
return [
|
||||||
|
r for r in results if r.object is not None
|
||||||
|
]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def cache(cls, instance, data):
|
def cache(cls, instance, data):
|
||||||
ct = ContentType.objects.get_for_model(instance)
|
ct = ContentType.objects.get_for_model(instance)
|
||||||
|
@ -1,3 +1,6 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||||
from django.core.validators import RegexValidator
|
from django.core.validators import RegexValidator
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
@ -71,3 +74,62 @@ class NaturalOrderingField(models.CharField):
|
|||||||
[self.target_field],
|
[self.target_field],
|
||||||
kwargs,
|
kwargs,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RestrictedGenericForeignKey(GenericForeignKey):
|
||||||
|
|
||||||
|
# Replicated from GenericForeignKey
|
||||||
|
def get_prefetch_queryset(self, instances, queryset=None):
|
||||||
|
# Compensate for the hack in RestrictedPrefetch
|
||||||
|
restrict_params = queryset if type(queryset) is dict else {}
|
||||||
|
|
||||||
|
# For efficiency, group the instances by content type and then do one
|
||||||
|
# query per model
|
||||||
|
fk_dict = defaultdict(set)
|
||||||
|
# We need one instance for each group in order to get the right db:
|
||||||
|
instance_dict = {}
|
||||||
|
ct_attname = self.model._meta.get_field(self.ct_field).get_attname()
|
||||||
|
for instance in instances:
|
||||||
|
# We avoid looking for values if either ct_id or fkey value is None
|
||||||
|
ct_id = getattr(instance, ct_attname)
|
||||||
|
if ct_id is not None:
|
||||||
|
fk_val = getattr(instance, self.fk_field)
|
||||||
|
if fk_val is not None:
|
||||||
|
fk_dict[ct_id].add(fk_val)
|
||||||
|
instance_dict[ct_id] = instance
|
||||||
|
|
||||||
|
ret_val = []
|
||||||
|
for ct_id, fkeys in fk_dict.items():
|
||||||
|
instance = instance_dict[ct_id]
|
||||||
|
ct = self.get_content_type(id=ct_id, using=instance._state.db)
|
||||||
|
if restrict_params:
|
||||||
|
# Override the default behavior to call restrict() on each model's queryset
|
||||||
|
qs = ct.model_class().objects.filter(pk__in=fkeys).restrict(**restrict_params)
|
||||||
|
ret_val.extend(qs)
|
||||||
|
else:
|
||||||
|
# Default behavior
|
||||||
|
ret_val.extend(ct.get_all_objects_for_this_type(pk__in=fkeys))
|
||||||
|
|
||||||
|
# For doing the join in Python, we have to match both the FK val and the
|
||||||
|
# content type, so we use a callable that returns a (fk, class) pair.
|
||||||
|
def gfk_key(obj):
|
||||||
|
ct_id = getattr(obj, ct_attname)
|
||||||
|
if ct_id is None:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
model = self.get_content_type(
|
||||||
|
id=ct_id, using=obj._state.db
|
||||||
|
).model_class()
|
||||||
|
return (
|
||||||
|
model._meta.pk.get_prep_value(getattr(obj, self.fk_field)),
|
||||||
|
model,
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
ret_val,
|
||||||
|
lambda obj: (obj.pk, obj.__class__),
|
||||||
|
gfk_key,
|
||||||
|
True,
|
||||||
|
self.name,
|
||||||
|
False,
|
||||||
|
)
|
||||||
|
@ -1,9 +1,36 @@
|
|||||||
from django.db.models import QuerySet
|
from django.db.models import Prefetch, QuerySet
|
||||||
|
|
||||||
from users.constants import CONSTRAINT_TOKEN_USER
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class RestrictedPrefetch(Prefetch):
|
||||||
|
"""
|
||||||
|
Extend Django's Prefetch to accept a user and action to be passed to the
|
||||||
|
`restrict()` method of the related object's queryset.
|
||||||
|
"""
|
||||||
|
def __init__(self, lookup, user, action='view', queryset=None, to_attr=None):
|
||||||
|
self.restrict_user = user
|
||||||
|
self.restrict_action = action
|
||||||
|
|
||||||
|
super().__init__(lookup, queryset=queryset, to_attr=to_attr)
|
||||||
|
|
||||||
|
def get_current_queryset(self, level):
|
||||||
|
params = {
|
||||||
|
'user': self.restrict_user,
|
||||||
|
'action': self.restrict_action,
|
||||||
|
}
|
||||||
|
|
||||||
|
qs = super().get_current_queryset(level)
|
||||||
|
if qs:
|
||||||
|
return qs.filter(**params)
|
||||||
|
|
||||||
|
# Bit of a hack. If no queryset is defined, pass through the dict of restrict()
|
||||||
|
# kwargs to be called later. This is necessary e.g. for GenericForeignKey fields,
|
||||||
|
# which do not permit setting a queryset on a Prefetch object.
|
||||||
|
return params
|
||||||
|
|
||||||
|
|
||||||
class RestrictedQuerySet(QuerySet):
|
class RestrictedQuerySet(QuerySet):
|
||||||
|
|
||||||
def restrict(self, user, action='view'):
|
def restrict(self, user, action='view'):
|
||||||
|
Loading…
Reference in New Issue
Block a user