netbox-community / netbox

The premier source of truth powering network automation. Open source under Apache 2. Try NetBox Cloud free: https://netboxlabs.com/free-netbox-cloud/
http://netboxlabs.com/oss/netbox/
Apache License 2.0
15.98k stars 2.56k forks source link

Devices > Edit: (get() returned more than one Device) when using multi custom permissions by tags #8715

Closed seros1521 closed 2 years ago

seros1521 commented 2 years ago

NetBox version

v3.1.8 (netbox-docker)

Python version

3.9

Steps to Reproduce

  1. create 2 tags: "tag1" and "tag2"
  2. add both tags to any device
  3. add to test user permission to read devices
  4. create permission: name - can_edit_device_with_tag1, action - 'can_change', object types - 'dcim > devices', constraints: '{"tags__name":"tag1"}'
  5. create permission: name - can_edit_device_with_tag2, action - 'can_change', object types - 'dcim > devices', constraints: '{"tags__name":"tag2"}'
  6. assign both permissions to the test user directly or to the groups they belong to
  7. try to edit device

Expected Behavior

The form for editing the device will open.

Observed Behavior

Exception Type: MultipleObjectsReturned at /dcim/devices/1/edit/ Exception Value: get() returned more than one Device -- it returned 2! Detail:

Environment:

Request Method: GET
Request URL: http://127.0.0.1:8000/dcim/devices/1/edit/

Django Version: 3.2.12
Python Version: 3.9.5
Installed Applications:
['django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'django.contrib.humanize',
 'corsheaders',
 'debug_toolbar',
 'graphiql_debug_toolbar',
 'django_filters',
 'django_tables2',
 'django_prometheus',
 'graphene_django',
 'mptt',
 'rest_framework',
 'social_django',
 'taggit',
 'timezone_field',
 'circuits',
 'dcim',
 'ipam',
 'extras',
 'tenancy',
 'users',
 'utilities',
 'virtualization',
 'wireless',
 'django_rq',
 'drf_yasg']
Installed Middleware:
['graphiql_debug_toolbar.middleware.DebugToolbarMiddleware',
 'django_prometheus.middleware.PrometheusBeforeMiddleware',
 'corsheaders.middleware.CorsMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware',
 'django.middleware.security.SecurityMiddleware',
 'netbox.middleware.ExceptionHandlingMiddleware',
 'netbox.middleware.RemoteUserMiddleware',
 'netbox.middleware.LoginRequiredMiddleware',
 'netbox.middleware.DynamicConfigMiddleware',
 'netbox.middleware.APIVersionMiddleware',
 'netbox.middleware.ObjectChangeMiddleware',
 'django_prometheus.middleware.PrometheusAfterMiddleware']

Traceback (most recent call last):
  File "/opt/netbox/venv/lib/python3.9/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/opt/netbox/venv/lib/python3.9/site-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/opt/netbox/venv/lib/python3.9/site-packages/django/views/generic/base.py", line 70, in view
    return self.dispatch(request, *args, **kwargs)
  File "/opt/netbox/netbox/netbox/views/generic.py", line 322, in dispatch
    return super().dispatch(request, *args, **kwargs)
  File "/opt/netbox/netbox/utilities/views.py", line 93, in dispatch
    return super().dispatch(request, *args, **kwargs)
  File "/opt/netbox/venv/lib/python3.9/site-packages/django/views/generic/base.py", line 98, in dispatch
    return handler(request, *args, **kwargs)
  File "/opt/netbox/netbox/netbox/views/generic.py", line 325, in get
    obj = self.alter_obj(self.get_object(kwargs), request, args, kwargs)
  File "/opt/netbox/netbox/netbox/views/generic.py", line 302, in get_object
    obj = get_object_or_404(self.queryset, pk=kwargs['pk'])
  File "/opt/netbox/venv/lib/python3.9/site-packages/django/shortcuts.py", line 76, in get_object_or_404
    return queryset.get(*args, **kwargs)
  File "/opt/netbox/venv/lib/python3.9/site-packages/django/db/models/query.py", line 439, in get
    raise self.model.MultipleObjectsReturned(

Exception Type: MultipleObjectsReturned at /dcim/devices/1/edit/
Exception Value: get() returned more than one Device -- it returned 2!
seros1521 commented 2 years ago

See also #8351

seros1521 commented 2 years ago

if added to the call https://github.com/netbox-community/netbox/blob/90ee689d5ae61e829042e35824b4ff4201476e44/netbox/utilities/querysets.py#L42 .distinct() the error disappears, but I'm not sure if this is the right way to solve it and it won't break anything.

jeremystretch commented 2 years ago

Seems like a bug with the tags filter:

>>> Site.objects.filter(tags__name='Golf').filter(tags__name='Lima').get(pk=24)
<Site: Butler Communications>
>>> Site.objects.filter(tags__name__in=['Golf', 'Lima']).get(pk=24)
Traceback (most recent call last):
  File "<console>", line 1, in <module>
  File "/opt/netbox/venv/lib/python3.8/site-packages/django/db/models/query.py", line 439, in get
    raise self.model.MultipleObjectsReturned(
dcim.models.sites.Site.MultipleObjectsReturned: get() returned more than one Site -- it returned 2!
seros1521 commented 2 years ago

From: https://django-taggit.readthedocs.io/en/latest/api.html#filtering:

If you’re filtering on multiple tags, it’s very common to get duplicate results, because of the way relational databases work. Often you’ll want to make use of the distinct() method on QuerySets:

>>> Food.objects.filter(tags__name__in=["delicious", "red"])
[<Food: apple>, <Food: apple>]
>>> Food.objects.filter(tags__name__in=["delicious", "red"]).distinct()
[<Food: apple>]