Repository: nautobot/nautobot (Python)

Issue:
Unable to Edit/Delete/Add Objects in v1.5.6
<!--
    NOTE: IF YOUR ISSUE DOES NOT FOLLOW THIS TEMPLATE, IT WILL BE CLOSED.

    This form is only for reporting reproducible bugs. If you need assistance
    with Nautobot installation, or if you have a general question, please start a
    discussion instead: https://github.com/nautobot/nautobot/discussions

    Please describe the environment in which you are running Nautobot. Be sure
    that you are running an unmodified instance of the latest stable release
    before submitting a bug report, and that any plugins have been disabled.
-->
### Environment
* Nautobot version (Docker tag too if applicable): 1.5.6
* Python version: 3.8.10
* Database platform, version: postgresql, Ubuntu 12.12-0ubuntu0.20.04.1
* Middleware(s): -

<!--
    Describe in detail the exact steps that someone else can take to reproduce
    this bug using the current stable release of Nautobot. Begin with the
    creation of any necessary database objects and call out every operation
    being performed explicitly. If reporting a bug in the REST API, be sure to
    reconstruct the raw HTTP request(s) being made: Don't rely on a client
    library such as pynautobot.
-->
### Steps to Reproduce
1. Create/Edit/Delete an object (e.g. Interface/IP-Address) in Nautobot either via Web GUI, API-Page or pynautobot

<!-- What did you expect to happen? -->
### Expected Behavior
The creation / edit / deletion of the object will be made without an error. Worked on 1.4.10 Release until upgrade to 1.5.6

<!-- What happened instead? -->
### Observed Behavior
**Web GUI:** An HTTP 400 error (Bad Request (400)) will be shown and no change will be made. The following log will be created in the syslog
```
[...] nautobot-server[2528]: ERROR:django.security.DisallowedHost:Invalid HTTP_HOST header: 'testserver'. You may need to add 'testserver' to ALLOWED_HOSTS.
[...] nautobot-server[2528]: WARNING:django.request:Bad Request: /dcim/interfaces/add/
[...] nautobot-server[2528]: [pid: 2528|app: 0|req: 154/154] x.x.x.x () {76 vars in 1973 bytes}  POST /dcim/interfaces/add/?device=397a12bb-db12-4b87-9283-a3c7cf231c91&return_url=/dcim/devices/397a12bb-db12-4b87-9283-a3c7cf231c91/interfaces/ => generated 143 bytes in 69 msecs (HTTP/1.1 400) 6 headers in 182 bytes (1 switches on core 0)
```
**API-Page:**
_Request_
```
curl -X 'DELETE' \
  'https://***********/api/dcim/interfaces/da7c8120-5da4-42aa-b724-79760c900587/' \
  -H 'accept: */*' \
  -H 'X-CSRFTOKEN: SsqqVYngkfktBhknV2cChj9vDSU91nOsjgp70RSpd0MwU0BbGFakLA5pTNH9oM3u'
```

_Response body_
```
Download
{
  "error": "Invalid HTTP_HOST header: 'testserver'. You may need to add 'testserver' to ALLOWED_HOSTS.",
  "exception": "DisallowedHost",
  "nautobot_version": "1.5.6",
  "python_version": "3.8.10"
}
```

_Response headers_
```
connection: keep-alive 
 content-length: 191 
 content-type: application/json 
 date: Tue,03 Jan 2023 09:09:15 GMT 
 referrer-policy: same-origin 
 server: nginx/1.18.0 (Ubuntu) 
 strict-transport-security: max-age=157680000 
 vary: Cookie,Origin 
 x-content-type-options: nosniff 
 x-frame-options: DENY 
```

**pynautobot**
```
 File "C:\Users\***********\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\LocalCache\local-packages\Python310\site-packages\pynautobot\core\response.py", line 436, in delete
    return True if req.delete() else False
  File "C:\Users\***********\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\LocalCache\local-packages\Python310\site-packages\pynautobot\core\query.py", line 368, in delete
    return self._make_call(verb="delete")
  File "C:\Users\***********\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.10_qbz5n2kfra8p0\LocalCache\local-packages\Python310\site-packages\pynautobot\core\query.py", line 250, in _make_call
    raise RequestError(req)
pynautobot.core.query.RequestError: The request failed with code 500 Internal Server Error: {'error': "Invalid HTTP_HOST header: 'testserver'. You may need to add 'testserver' to ALLOWED_HOSTS.", 'exception': 'DisallowedHost', 'nautobot_version': '1.5.6', 'python_version': '3.8.10'}

```


Discussion: Update:

I added "testserver" to NAUTOBOT_ALLOWED_HOSTS and the problem seems to be gone. Did I miss something during the upgrade process?

_http://testserver_ was defined in the following unit tests:
https://github.com/nautobot/nautobot/blob/3aa950a2a84cdcc1a4655882489f900c20d64d3f/nautobot/extras/tests/test_api.py
https://github.com/nautobot/nautobot/blob/4ff1ef04bc15073ddbfdf30c182a89e5351f04f2/nautobot/extras/tests/test_relationships.py

To my understanding, those classes won't be called during normal operation 

Hi, we hit the same issue after upgrade from 1.5.2. We tested `Relationship`, `RelationshipAssociation`, `IPaddress` and `Device`, neither can be created at all with v1.5.6.
IMHO the reason is in PR https://github.com/nautobot/nautobot/pull/3016 as those changes are recent and the ones in `test_api.py` are over 5 moths old according to `git blame`. It is now creating fake request during `serialize_object_v2()` via `APIRequestFactory` so I suspect this is the reason, but I miss the deeper understanding of your tests to understand exactly *why* :) 
~Just contributing a test case here. I have no relationships and basic set up in my environment (devices, sites, circuits). I am able to create and delete devices without issue. Perhaps something when relationships are defined?~
I encountered the same problem after upgrading from 1.5.5 to 1.5.6.
Here's a stack trace

```
Traceback (most recent call last):
  File "/usr/local/lib/python3.7/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/usr/local/lib/python3.7/site-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/usr/local/lib/python3.7/site-packages/django/views/generic/base.py", line 70, in view
    return self.dispatch(request, *args, **kwargs)
  File "/source/nautobot/core/views/generic.py", line 412, in dispatch
    return super().dispatch(request, *args, **kwargs)
  File "/source/nautobot/utilities/views.py", line 116, in dispatch
    return super().dispatch(request, *args, **kwargs)
  File "/usr/local/lib/python3.7/site-packages/django/views/generic/base.py", line 98, in dispatch
    return handler(request, *args, **kwargs)
  File "/source/nautobot/core/views/generic.py", line 446, in post
    obj = form.save()
  File "/source/nautobot/extras/forms/mixins.py", line 640, in save
    obj = super().save(commit)
  File "/usr/local/lib/python3.7/site-packages/django/forms/models.py", line 468, in save
    self.instance.save()
  File "/usr/local/lib/python3.7/site-packages/django/db/models/base.py", line 740, in save
    force_update=force_update, update_fields=update_fields)
  File "/usr/local/lib/python3.7/site-packages/django/db/models/base.py", line 789, in save_base
    update_fields=update_fields, raw=raw, using=using,
  File "/usr/local/lib/python3.7/site-packages/django/dispatch/dispatcher.py", line 182, in send
    for receiver in self._live_receivers(sender)
  File "/usr/local/lib/python3.7/site-packages/django/dispatch/dispatcher.py", line 182, in <listcomp>
    for receiver in self._live_receivers(sender)
  File "/source/nautobot/utilities/utils.py", line 453, in _curried
    return _curried_func(*args, *moreargs, **{**kwargs, **morekwargs})
  File "/source/nautobot/extras/signals.py", line 87, in _handle_changed_object
    objectchange = instance.to_objectchange(action)
  File "/source/nautobot/extras/models/change_logging.py", line 44, in to_objectchange
    object_data_v2=serialize_object_v2(self),
  File "/source/nautobot/utilities/utils.py", line 237, in serialize_object_v2
    data = serializer_class(obj, context={"request": request}).data
  File "/usr/local/lib/python3.7/site-packages/rest_framework/serializers.py", line 555, in data
    ret = super().data
  File "/usr/local/lib/python3.7/site-packages/rest_framework/serializers.py", line 253, in data
    self._data = self.to_representation(self.instance)
  File "/usr/local/lib/python3.7/site-packages/rest_framework/serializers.py", line 522, in to_representation
    ret[field.field_name] = field.to_representation(attribute)
  File "/usr/local/lib/python3.7/site-packages/rest_framework/relations.py", line 401, in to_representation
    url = self.get_url(value, self.view_name, request, format)
  File "/usr/local/lib/python3.7/site-packages/rest_framework/relations.py", line 339, in get_url
    return self.reverse(view_name, kwargs=kwargs, request=request, format=format)
  File "/usr/local/lib/python3.7/site-packages/rest_framework/reverse.py", line 47, in reverse
    url = _reverse(viewname, args, kwargs, request, format, **extra)
  File "/usr/local/lib/python3.7/site-packages/rest_framework/reverse.py", line 62, in _reverse
    return request.build_absolute_uri(url)
  File "/usr/local/lib/python3.7/site-packages/django/http/request.py", line 236, in build_absolute_uri
    location = self._current_scheme_host + location
  File "/usr/local/lib/python3.7/site-packages/django/utils/functional.py", line 48, in __get__
    res = instance.__dict__[self.name] = self.func(instance)
  File "/usr/local/lib/python3.7/site-packages/django/http/request.py", line 246, in _current_scheme_host
    return '{}://{}'.format(self.scheme, self.get_host())
  File "/usr/local/lib/python3.7/site-packages/django/http/request.py", line 149, in get_host
    raise DisallowedHost(msg)

Exception Type: DisallowedHost at /dcim/sites/add/
Exception Value: Invalid HTTP_HOST header: 'testserver'. You may need to add 'testserver' to ALLOWED_HOSTS.
```



Fix (patch):
diff --git a/changes/2951.added b/changes/2951.added
deleted file mode 100644
index de048fe58aa..00000000000
--- a/changes/2951.added
+++ /dev/null
@@ -1,1 +0,0 @@
-Added change logging when relationships are changed.
diff --git a/changes/3065.fixed b/changes/3065.fixed
new file mode 100644
index 00000000000..964f078ff8f
--- /dev/null
+++ b/changes/3065.fixed
@@ -0,0 +1,1 @@
+Rolled back the changes made in 1.5.6 by #3016 to fix a breaking issue with `ALLOWED_HOSTS` and change-logging.
diff --git a/development/dev.env b/development/dev.env
index 3a5d87eb6e9..5bf03b100d1 100644
--- a/development/dev.env
+++ b/development/dev.env
@@ -1,4 +1,4 @@
-NAUTOBOT_ALLOWED_HOSTS=*
+NAUTOBOT_ALLOWED_HOSTS=".localhost 127.0.0.1 [::1]"
 NAUTOBOT_CHANGELOG_RETENTION=0
 NAUTOBOT_CONFIG=/opt/nautobot/nautobot_config.py
 NAUTOBOT_DB_HOST=db
diff --git a/nautobot/extras/context_managers.py b/nautobot/extras/context_managers.py
index 523d29d816b..f96cf8710dd 100644
--- a/nautobot/extras/context_managers.py
+++ b/nautobot/extras/context_managers.py
@@ -2,16 +2,11 @@
 from contextlib import contextmanager
 
 from django.contrib.auth import get_user_model
-from django.db.models.signals import m2m_changed, post_delete, post_save, pre_delete
+from django.db.models.signals import m2m_changed, pre_delete, post_save
 from django.test.client import RequestFactory
 
 from nautobot.extras.choices import ObjectChangeEventContextChoices
-from nautobot.extras.models import RelationshipAssociation
-from nautobot.extras.signals import (
-    _handle_changed_object,
-    _handle_deleted_object,
-    _handle_relationship_association_change,
-)
+from nautobot.extras.signals import _handle_changed_object, _handle_deleted_object
 from nautobot.utilities.utils import curry
 
 
@@ -92,22 +87,11 @@ def change_logging(change_context):
     # Curry signals receivers to pass the current request
     handle_changed_object = curry(_handle_changed_object, change_context)
     handle_deleted_object = curry(_handle_deleted_object, change_context)
-    handle_relationship_association_change = curry(_handle_relationship_association_change, change_context)
 
     # Connect our receivers to the post_save and post_delete signals.
     post_save.connect(handle_changed_object, dispatch_uid="handle_changed_object")
     m2m_changed.connect(handle_changed_object, dispatch_uid="handle_changed_object")
     pre_delete.connect(handle_deleted_object, dispatch_uid="handle_deleted_object")
-    post_save.connect(
-        handle_relationship_association_change,
-        dispatch_uid="handle_relationship_association_change",
-        sender=RelationshipAssociation,
-    )
-    post_delete.connect(
-        handle_relationship_association_change,
-        dispatch_uid="handle_relationship_association_change",
-        sender=RelationshipAssociation,
-    )
 
     yield
 
@@ -116,16 +100,6 @@ def change_logging(change_context):
     post_save.disconnect(handle_changed_object, dispatch_uid="handle_changed_object")
     m2m_changed.disconnect(handle_changed_object, dispatch_uid="handle_changed_object")
     pre_delete.disconnect(handle_deleted_object, dispatch_uid="handle_deleted_object")
-    post_save.disconnect(
-        handle_relationship_association_change,
-        dispatch_uid="handle_relationship_association_change",
-        sender=RelationshipAssociation,
-    )
-    post_delete.disconnect(
-        handle_relationship_association_change,
-        dispatch_uid="handle_relationship_association_change",
-        sender=RelationshipAssociation,
-    )
 
 
 @contextmanager
diff --git a/nautobot/extras/signals.py b/nautobot/extras/signals.py
index a18a325933d..84fd64ad0ac 100644
--- a/nautobot/extras/signals.py
+++ b/nautobot/extras/signals.py
@@ -19,15 +19,8 @@
 from nautobot.extras.utils import refresh_job_model_from_job_class
 from nautobot.utilities.config import get_settings_or_config
 from nautobot.extras.constants import CHANGELOG_MAX_CHANGE_CONTEXT_DETAIL
-from nautobot.extras.choices import JobResultStatusChoices, ObjectChangeActionChoices
-from nautobot.extras.models import (
-    CustomField,
-    DynamicGroup,
-    DynamicGroupMembership,
-    GitRepository,
-    JobResult,
-    ObjectChange,
-)
+from .choices import JobResultStatusChoices, ObjectChangeActionChoices
+from .models import CustomField, DynamicGroup, DynamicGroupMembership, GitRepository, JobResult, ObjectChange
 from .webhooks import enqueue_webhooks
 
 logger = logging.getLogger("nautobot.extras.signals")
@@ -305,43 +298,3 @@ def refresh_job_models(sender, *, apps, **kwargs):
             )
             job_model.installed = False
             job_model.save()
-
-
-def _handle_relationship_association_change(change_context, sender, instance, **kwargs):
-    """
-    Signal handler for RelationshipAssociation changes. When a RelationshipAssociation
-    is changed or deleted, also create ObjectChanges for related objects.
-    """
-    from .jobs import enqueue_job_hooks  # avoid circular import
-
-    for related_object in [instance.source, instance.destination]:
-        if related_object is None:
-            continue
-        if hasattr(related_object, "to_objectchange"):
-            updated_objectchange = related_object.to_objectchange(ObjectChangeActionChoices.ACTION_UPDATE)
-            related_changes = ObjectChange.objects.filter(
-                changed_object_type=ContentType.objects.get_for_model(related_object),
-                changed_object_id=related_object.pk,
-                request_id=change_context.change_id,
-            )
-            if related_changes.exists():
-                related_changes.update(
-                    object_data=updated_objectchange.object_data, object_data_v2=updated_objectchange.object_data_v2
-                )
-                enqueue_job_hooks(related_changes.first())
-            else:
-                updated_objectchange.user = _get_user_if_authenticated(change_context.get_user(), updated_objectchange)
-                updated_objectchange.request_id = change_context.change_id
-                updated_objectchange.change_context = change_context.context
-                updated_objectchange.change_context_detail = change_context.context_detail[
-                    :CHANGELOG_MAX_CHANGE_CONTEXT_DETAIL
-                ]
-                updated_objectchange.save()
-                enqueue_job_hooks(updated_objectchange)
-
-        enqueue_webhooks(
-            related_object,
-            change_context.get_user(),
-            change_context.change_id,
-            ObjectChangeActionChoices.ACTION_UPDATE,
-        )
diff --git a/nautobot/utilities/utils.py b/nautobot/utilities/utils.py
index 5f59200ebc8..b9484f13ad0 100644
--- a/nautobot/utilities/utils.py
+++ b/nautobot/utilities/utils.py
@@ -32,7 +32,6 @@
     NumberFilter,
 )
 from django_filters.utils import verbose_lookup_expr
-from rest_framework.test import APIRequestFactory
 from taggit.managers import _TaggableManager
 
 from nautobot.dcim.choices import CableLengthUnitChoices
@@ -231,10 +230,7 @@ def serialize_object_v2(obj):
     # Try serializing obj(model instance) using its API Serializer
     try:
         serializer_class = get_serializer_for_model(obj.__class__)
-        # create a fake request to include opt-in `relationships` field if available
-        request = APIRequestFactory().get("/", {"include": ["relationships"]})
-        request.version = settings.REST_FRAMEWORK_VERSION
-        data = serializer_class(obj, context={"request": request}).data
+        data = serializer_class(obj, context={"request": None}).data
     except SerializerNotFound:
         # Fall back to generic JSON representation of obj
         data = serialize_object(obj)