nautobot 1.6.30__py3-none-any.whl → 1.6.32__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of nautobot might be problematic. Click here for more details.
- nautobot/circuits/forms.py +6 -0
- nautobot/circuits/views.py +1 -0
- nautobot/core/graphql/schema.py +3 -1
- nautobot/core/graphql/types.py +10 -0
- nautobot/core/models/__init__.py +2 -0
- nautobot/core/templates/generic/object_list.html +1 -1
- nautobot/core/tests/test_views.py +73 -0
- nautobot/core/urls.py +3 -3
- nautobot/core/views/__init__.py +21 -0
- nautobot/dcim/forms.py +29 -0
- nautobot/dcim/models/device_component_templates.py +4 -0
- nautobot/dcim/models/device_components.py +4 -0
- nautobot/dcim/models/devices.py +2 -0
- nautobot/dcim/views.py +5 -0
- nautobot/extras/forms/forms.py +12 -0
- nautobot/extras/models/customfields.py +2 -0
- nautobot/extras/models/datasources.py +2 -0
- nautobot/extras/models/groups.py +8 -0
- nautobot/extras/models/jobs.py +10 -0
- nautobot/extras/models/models.py +2 -0
- nautobot/extras/models/secrets.py +8 -2
- nautobot/extras/secrets/__init__.py +14 -0
- nautobot/extras/tests/test_models.py +26 -0
- nautobot/extras/views.py +1 -0
- nautobot/ipam/filters.py +1 -1
- nautobot/ipam/forms.py +6 -0
- nautobot/ipam/models.py +8 -0
- nautobot/ipam/views.py +1 -0
- nautobot/project-static/docs/404.html +62 -0
- nautobot/project-static/docs/additional-features/caching.html +62 -0
- nautobot/project-static/docs/additional-features/change-logging.html +62 -0
- nautobot/project-static/docs/additional-features/config-contexts.html +62 -0
- nautobot/project-static/docs/additional-features/graphql.html +62 -0
- nautobot/project-static/docs/additional-features/healthcheck.html +62 -0
- nautobot/project-static/docs/additional-features/job-scheduling-and-approvals.html +63 -1
- nautobot/project-static/docs/additional-features/jobs.html +62 -0
- nautobot/project-static/docs/additional-features/napalm.html +62 -0
- nautobot/project-static/docs/additional-features/prometheus-metrics.html +62 -0
- nautobot/project-static/docs/additional-features/template-filters.html +62 -0
- nautobot/project-static/docs/administration/celery-queues.html +63 -1
- nautobot/project-static/docs/administration/nautobot-server.html +62 -0
- nautobot/project-static/docs/administration/nautobot-shell.html +62 -0
- nautobot/project-static/docs/administration/permissions.html +62 -0
- nautobot/project-static/docs/administration/replicating-nautobot.html +62 -0
- nautobot/project-static/docs/administration/request-profiling.html +62 -0
- nautobot/project-static/docs/administration/security/index.html +4407 -0
- nautobot/project-static/docs/administration/security/notices.html +4797 -0
- nautobot/project-static/docs/apps/index.html +62 -0
- nautobot/project-static/docs/apps/nautobot-apps.html +62 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +62 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +62 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/api.html +62 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +62 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +62 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +62 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +83 -21
- nautobot/project-static/docs/code-reference/nautobot/apps/models.html +67 -1
- nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +140 -52
- nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +62 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +3203 -3101
- nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +62 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +62 -0
- nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +210 -148
- nautobot/project-static/docs/code-reference/nautobot/apps/views.html +62 -0
- nautobot/project-static/docs/configuration/authentication/ldap.html +62 -0
- nautobot/project-static/docs/configuration/authentication/remote.html +62 -0
- nautobot/project-static/docs/configuration/authentication/sso.html +62 -0
- nautobot/project-static/docs/configuration/index.html +62 -0
- nautobot/project-static/docs/configuration/optional-settings.html +67 -5
- nautobot/project-static/docs/configuration/required-settings.html +62 -0
- nautobot/project-static/docs/core-functionality/circuits.html +62 -0
- nautobot/project-static/docs/core-functionality/device-types.html +62 -0
- nautobot/project-static/docs/core-functionality/devices.html +62 -0
- nautobot/project-static/docs/core-functionality/ipam.html +62 -0
- nautobot/project-static/docs/core-functionality/power.html +62 -0
- nautobot/project-static/docs/core-functionality/secrets.html +62 -0
- nautobot/project-static/docs/core-functionality/services.html +62 -0
- nautobot/project-static/docs/core-functionality/sites-and-racks.html +62 -0
- nautobot/project-static/docs/core-functionality/tenancy.html +62 -0
- nautobot/project-static/docs/core-functionality/virtualization.html +62 -0
- nautobot/project-static/docs/core-functionality/vlans.html +62 -0
- nautobot/project-static/docs/development/application-registry.html +62 -0
- nautobot/project-static/docs/development/best-practices.html +62 -0
- nautobot/project-static/docs/development/docker-compose-advanced-use-cases.html +62 -0
- nautobot/project-static/docs/development/extending-models.html +62 -0
- nautobot/project-static/docs/development/generic-views.html +62 -0
- nautobot/project-static/docs/development/getting-started.html +63 -1
- nautobot/project-static/docs/development/homepage.html +62 -0
- nautobot/project-static/docs/development/index.html +62 -0
- nautobot/project-static/docs/development/navigation-menu.html +62 -0
- nautobot/project-static/docs/development/release-checklist.html +62 -0
- nautobot/project-static/docs/development/style-guide.html +62 -0
- nautobot/project-static/docs/development/templates.html +62 -0
- nautobot/project-static/docs/development/testing.html +62 -0
- nautobot/project-static/docs/development/user-preferences.html +62 -0
- nautobot/project-static/docs/docker/index.html +62 -0
- nautobot/project-static/docs/index.html +62 -0
- nautobot/project-static/docs/installation/centos.html +62 -0
- nautobot/project-static/docs/installation/external-authentication.html +62 -0
- nautobot/project-static/docs/installation/http-server.html +62 -0
- nautobot/project-static/docs/installation/index.html +62 -0
- nautobot/project-static/docs/installation/migrating-from-netbox.html +62 -0
- nautobot/project-static/docs/installation/migrating-from-postgresql.html +62 -0
- nautobot/project-static/docs/installation/nautobot.html +62 -0
- nautobot/project-static/docs/installation/selinux-troubleshooting.html +62 -0
- nautobot/project-static/docs/installation/services.html +62 -0
- nautobot/project-static/docs/installation/ubuntu.html +62 -0
- nautobot/project-static/docs/installation/upgrading.html +62 -0
- nautobot/project-static/docs/models/circuits/circuit.html +62 -0
- nautobot/project-static/docs/models/circuits/circuittermination.html +62 -0
- nautobot/project-static/docs/models/circuits/circuittype.html +62 -0
- nautobot/project-static/docs/models/circuits/provider.html +62 -0
- nautobot/project-static/docs/models/circuits/providernetwork.html +62 -0
- nautobot/project-static/docs/models/dcim/cable.html +62 -0
- nautobot/project-static/docs/models/dcim/consoleport.html +62 -0
- nautobot/project-static/docs/models/dcim/consoleporttemplate.html +62 -0
- nautobot/project-static/docs/models/dcim/consoleserverport.html +62 -0
- nautobot/project-static/docs/models/dcim/consoleserverporttemplate.html +62 -0
- nautobot/project-static/docs/models/dcim/device.html +62 -0
- nautobot/project-static/docs/models/dcim/devicebay.html +62 -0
- nautobot/project-static/docs/models/dcim/devicebaytemplate.html +62 -0
- nautobot/project-static/docs/models/dcim/deviceredundancygroup.html +62 -0
- nautobot/project-static/docs/models/dcim/devicerole.html +62 -0
- nautobot/project-static/docs/models/dcim/devicetype.html +62 -0
- nautobot/project-static/docs/models/dcim/frontport.html +62 -0
- nautobot/project-static/docs/models/dcim/frontporttemplate.html +62 -0
- nautobot/project-static/docs/models/dcim/interface.html +62 -0
- nautobot/project-static/docs/models/dcim/interfaceredundancygroup.html +62 -0
- nautobot/project-static/docs/models/dcim/interfacetemplate.html +62 -0
- nautobot/project-static/docs/models/dcim/inventoryitem.html +62 -0
- nautobot/project-static/docs/models/dcim/location.html +62 -0
- nautobot/project-static/docs/models/dcim/locationtype.html +62 -0
- nautobot/project-static/docs/models/dcim/manufacturer.html +62 -0
- nautobot/project-static/docs/models/dcim/platform.html +62 -0
- nautobot/project-static/docs/models/dcim/powerfeed.html +62 -0
- nautobot/project-static/docs/models/dcim/poweroutlet.html +62 -0
- nautobot/project-static/docs/models/dcim/poweroutlettemplate.html +62 -0
- nautobot/project-static/docs/models/dcim/powerpanel.html +62 -0
- nautobot/project-static/docs/models/dcim/powerport.html +62 -0
- nautobot/project-static/docs/models/dcim/powerporttemplate.html +62 -0
- nautobot/project-static/docs/models/dcim/rack.html +62 -0
- nautobot/project-static/docs/models/dcim/rackgroup.html +62 -0
- nautobot/project-static/docs/models/dcim/rackreservation.html +62 -0
- nautobot/project-static/docs/models/dcim/rackrole.html +62 -0
- nautobot/project-static/docs/models/dcim/rearport.html +62 -0
- nautobot/project-static/docs/models/dcim/rearporttemplate.html +62 -0
- nautobot/project-static/docs/models/dcim/region.html +62 -0
- nautobot/project-static/docs/models/dcim/site.html +62 -0
- nautobot/project-static/docs/models/dcim/virtualchassis.html +62 -0
- nautobot/project-static/docs/models/extras/computedfield.html +63 -1
- nautobot/project-static/docs/models/extras/configcontext.html +62 -0
- nautobot/project-static/docs/models/extras/configcontextschema.html +62 -0
- nautobot/project-static/docs/models/extras/customfield.html +62 -0
- nautobot/project-static/docs/models/extras/customlink.html +63 -1
- nautobot/project-static/docs/models/extras/dynamicgroup.html +62 -0
- nautobot/project-static/docs/models/extras/exporttemplate.html +62 -0
- nautobot/project-static/docs/models/extras/gitrepository.html +62 -0
- nautobot/project-static/docs/models/extras/graphqlquery.html +62 -0
- nautobot/project-static/docs/models/extras/imageattachment.html +62 -0
- nautobot/project-static/docs/models/extras/job.html +62 -0
- nautobot/project-static/docs/models/extras/jobbutton.html +63 -1
- nautobot/project-static/docs/models/extras/jobhook.html +62 -0
- nautobot/project-static/docs/models/extras/joblogentry.html +62 -0
- nautobot/project-static/docs/models/extras/jobresult.html +62 -0
- nautobot/project-static/docs/models/extras/note.html +62 -0
- nautobot/project-static/docs/models/extras/relationship.html +62 -0
- nautobot/project-static/docs/models/extras/secret.html +62 -0
- nautobot/project-static/docs/models/extras/secretsgroup.html +62 -0
- nautobot/project-static/docs/models/extras/status.html +62 -0
- nautobot/project-static/docs/models/extras/tag.html +62 -0
- nautobot/project-static/docs/models/extras/webhook.html +62 -0
- nautobot/project-static/docs/models/ipam/aggregate.html +62 -0
- nautobot/project-static/docs/models/ipam/ipaddress.html +62 -0
- nautobot/project-static/docs/models/ipam/prefix.html +62 -0
- nautobot/project-static/docs/models/ipam/rir.html +62 -0
- nautobot/project-static/docs/models/ipam/role.html +62 -0
- nautobot/project-static/docs/models/ipam/routetarget.html +62 -0
- nautobot/project-static/docs/models/ipam/service.html +62 -0
- nautobot/project-static/docs/models/ipam/vlan.html +62 -0
- nautobot/project-static/docs/models/ipam/vlangroup.html +62 -0
- nautobot/project-static/docs/models/ipam/vrf.html +62 -0
- nautobot/project-static/docs/models/tenancy/tenant.html +62 -0
- nautobot/project-static/docs/models/tenancy/tenantgroup.html +62 -0
- nautobot/project-static/docs/models/users/objectpermission.html +62 -0
- nautobot/project-static/docs/models/users/token.html +62 -0
- nautobot/project-static/docs/models/virtualization/cluster.html +62 -0
- nautobot/project-static/docs/models/virtualization/clustergroup.html +62 -0
- nautobot/project-static/docs/models/virtualization/clustertype.html +62 -0
- nautobot/project-static/docs/models/virtualization/virtualmachine.html +62 -0
- nautobot/project-static/docs/models/virtualization/vminterface.html +62 -0
- nautobot/project-static/docs/plugins/development.html +63 -1
- nautobot/project-static/docs/plugins/index.html +62 -0
- nautobot/project-static/docs/plugins/porting-from-netbox.html +62 -0
- nautobot/project-static/docs/release-notes/index.html +62 -0
- nautobot/project-static/docs/release-notes/version-1.0.html +62 -0
- nautobot/project-static/docs/release-notes/version-1.1.html +64 -2
- nautobot/project-static/docs/release-notes/version-1.2.html +63 -1
- nautobot/project-static/docs/release-notes/version-1.3.html +62 -0
- nautobot/project-static/docs/release-notes/version-1.4.html +62 -0
- nautobot/project-static/docs/release-notes/version-1.5.html +62 -0
- nautobot/project-static/docs/release-notes/version-1.6.html +466 -217
- nautobot/project-static/docs/requirements.txt +2 -1
- nautobot/project-static/docs/rest-api/authentication.html +62 -0
- nautobot/project-static/docs/rest-api/filtering.html +62 -0
- nautobot/project-static/docs/rest-api/overview.html +62 -0
- nautobot/project-static/docs/search/search_index.json +1 -1
- nautobot/project-static/docs/sitemap.xml +197 -187
- nautobot/project-static/docs/sitemap.xml.gz +0 -0
- nautobot/project-static/docs/user-guides/custom-fields.html +62 -0
- nautobot/project-static/docs/user-guides/getting-started/creating-devices.html +63 -1
- nautobot/project-static/docs/user-guides/getting-started/index.html +62 -0
- nautobot/project-static/docs/user-guides/getting-started/interfaces.html +62 -0
- nautobot/project-static/docs/user-guides/getting-started/ipam.html +62 -0
- nautobot/project-static/docs/user-guides/getting-started/platforms.html +62 -0
- nautobot/project-static/docs/user-guides/getting-started/regions.html +62 -0
- nautobot/project-static/docs/user-guides/getting-started/search-bar.html +62 -0
- nautobot/project-static/docs/user-guides/getting-started/tenants.html +62 -0
- nautobot/project-static/docs/user-guides/getting-started/vlans-and-vlan-groups.html +63 -1
- nautobot/project-static/docs/user-guides/git-data-source.html +62 -0
- nautobot/project-static/docs/user-guides/graphql.html +62 -0
- nautobot/project-static/docs/user-guides/relationships.html +62 -0
- nautobot/project-static/docs/user-guides/s3-django-storage.html +62 -0
- nautobot/tenancy/forms.py +10 -0
- nautobot/tenancy/views.py +1 -0
- nautobot/users/models.py +4 -0
- nautobot/utilities/testing/views.py +17 -1
- nautobot/utilities/tests/test_jinja_filters.py +26 -2
- nautobot/utilities/utils.py +14 -0
- nautobot/virtualization/forms.py +12 -0
- nautobot/virtualization/views.py +2 -0
- {nautobot-1.6.30.dist-info → nautobot-1.6.32.dist-info}/METADATA +2 -2
- {nautobot-1.6.30.dist-info → nautobot-1.6.32.dist-info}/RECORD +236 -234
- {nautobot-1.6.30.dist-info → nautobot-1.6.32.dist-info}/LICENSE.txt +0 -0
- {nautobot-1.6.30.dist-info → nautobot-1.6.32.dist-info}/NOTICE +0 -0
- {nautobot-1.6.30.dist-info → nautobot-1.6.32.dist-info}/WHEEL +0 -0
- {nautobot-1.6.30.dist-info → nautobot-1.6.32.dist-info}/entry_points.txt +0 -0
nautobot/circuits/forms.py
CHANGED
|
@@ -173,6 +173,12 @@ class CircuitTypeForm(NautobotModelForm):
|
|
|
173
173
|
]
|
|
174
174
|
|
|
175
175
|
|
|
176
|
+
class CircuitTypeFilterForm(NautobotFilterForm):
|
|
177
|
+
model = CircuitType
|
|
178
|
+
q = forms.CharField(required=False, label="Search")
|
|
179
|
+
name = forms.CharField(required=False)
|
|
180
|
+
|
|
181
|
+
|
|
176
182
|
class CircuitTypeCSVForm(CustomFieldModelCSVForm):
|
|
177
183
|
class Meta:
|
|
178
184
|
model = CircuitType
|
nautobot/circuits/views.py
CHANGED
|
@@ -30,6 +30,7 @@ class CircuitTypeUIViewSet(
|
|
|
30
30
|
):
|
|
31
31
|
bulk_create_form_class = forms.CircuitTypeCSVForm
|
|
32
32
|
filterset_class = filters.CircuitTypeFilterSet
|
|
33
|
+
filterset_form_class = forms.CircuitTypeFilterForm
|
|
33
34
|
form_class = forms.CircuitTypeForm
|
|
34
35
|
queryset = CircuitType.objects.annotate(circuit_count=count_related(Circuit, "type"))
|
|
35
36
|
serializer_class = serializers.CircuitTypeSerializer
|
nautobot/core/graphql/schema.py
CHANGED
|
@@ -23,7 +23,7 @@ from nautobot.core.graphql.generators import (
|
|
|
23
23
|
generate_schema_type,
|
|
24
24
|
generate_null_choices_resolver,
|
|
25
25
|
)
|
|
26
|
-
from nautobot.core.graphql.types import ContentTypeType, DateType
|
|
26
|
+
from nautobot.core.graphql.types import ContentTypeType, DateType, JSON
|
|
27
27
|
from nautobot.dcim.graphql.types import (
|
|
28
28
|
CableType,
|
|
29
29
|
CablePathType,
|
|
@@ -84,6 +84,8 @@ CUSTOM_FIELD_MAPPING = {
|
|
|
84
84
|
CustomFieldTypeChoices.TYPE_DATE: DateType(),
|
|
85
85
|
CustomFieldTypeChoices.TYPE_URL: graphene.String(),
|
|
86
86
|
CustomFieldTypeChoices.TYPE_SELECT: graphene.String(),
|
|
87
|
+
CustomFieldTypeChoices.TYPE_JSON: JSON(),
|
|
88
|
+
CustomFieldTypeChoices.TYPE_MULTISELECT: graphene.List(graphene.String),
|
|
87
89
|
}
|
|
88
90
|
|
|
89
91
|
|
nautobot/core/graphql/types.py
CHANGED
|
@@ -56,3 +56,13 @@ class DateType(graphene.Date):
|
|
|
56
56
|
return date
|
|
57
57
|
else:
|
|
58
58
|
raise GraphQLError(f'Received not compatible date "{date!r}"')
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class JSON(graphene.Scalar):
|
|
62
|
+
@staticmethod
|
|
63
|
+
def serialize_data(dt):
|
|
64
|
+
return dt
|
|
65
|
+
|
|
66
|
+
serialize = serialize_data
|
|
67
|
+
parse_value = serialize_data
|
|
68
|
+
parse_literal = serialize_data
|
nautobot/core/models/__init__.py
CHANGED
|
@@ -217,7 +217,7 @@
|
|
|
217
217
|
let search_query = new URLSearchParams()
|
|
218
218
|
let dynamic_query = new URLSearchParams(new FormData(document.getElementById("dynamic-filter-form")));
|
|
219
219
|
dynamic_query.forEach((value, key) => { if (value != "") { search_query.append(key, value); }});
|
|
220
|
-
let default_query = new URLSearchParams(new FormData(document.getElementById("default-filter")
|
|
220
|
+
let default_query = new URLSearchParams(new FormData(document.getElementById("default-filter")?.firstElementChild));
|
|
221
221
|
default_query.forEach((value, key) => {
|
|
222
222
|
if (value != "" && !search_query.has(key, value)) { search_query.append(key, value); }
|
|
223
223
|
});
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import re
|
|
3
|
+
import tempfile
|
|
2
4
|
from unittest import mock
|
|
3
5
|
import urllib.parse
|
|
4
6
|
|
|
@@ -133,6 +135,77 @@ class HomeViewTestCase(TestCase):
|
|
|
133
135
|
self.assertNotIn("Welcome to Nautobot!", response.content.decode(response.charset))
|
|
134
136
|
|
|
135
137
|
|
|
138
|
+
class MediaViewTestCase(TestCase):
|
|
139
|
+
def test_media_unauthenticated(self):
|
|
140
|
+
"""
|
|
141
|
+
Test that unauthenticated users are redirected to login when accessing media files whether they exist or not.
|
|
142
|
+
"""
|
|
143
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
144
|
+
with override_settings(
|
|
145
|
+
MEDIA_ROOT=temp_dir,
|
|
146
|
+
BRANDING_FILEPATHS={"logo": os.path.join("branding", "logo.txt")},
|
|
147
|
+
):
|
|
148
|
+
file_path = os.path.join(temp_dir, "foo.txt")
|
|
149
|
+
url = reverse("media", kwargs={"path": "foo.txt"})
|
|
150
|
+
self.client.logout()
|
|
151
|
+
|
|
152
|
+
# Unauthenticated request to nonexistent media file should redirect to login page
|
|
153
|
+
response = self.client.get(url)
|
|
154
|
+
self.assertRedirects(
|
|
155
|
+
response, expected_url=f"{reverse('login')}?next={url}", status_code=302, target_status_code=200
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Unauthenticated request to existent media file should redirect to login page as well
|
|
159
|
+
with open(file_path, "w") as f:
|
|
160
|
+
f.write("Hello, world!")
|
|
161
|
+
response = self.client.get(url)
|
|
162
|
+
self.assertRedirects(
|
|
163
|
+
response, expected_url=f"{reverse('login')}?next={url}", status_code=302, target_status_code=200
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def test_branding_media(self):
|
|
167
|
+
"""
|
|
168
|
+
Test that users can access branding files listed in `settings.BRANDING_FILEPATHS` regardless of authentication.
|
|
169
|
+
"""
|
|
170
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
171
|
+
with override_settings(
|
|
172
|
+
MEDIA_ROOT=temp_dir,
|
|
173
|
+
BRANDING_FILEPATHS={"logo": os.path.join("branding", "logo.txt")},
|
|
174
|
+
):
|
|
175
|
+
os.makedirs(os.path.join(temp_dir, "branding"))
|
|
176
|
+
file_path = os.path.join(temp_dir, "branding", "logo.txt")
|
|
177
|
+
with open(file_path, "w") as f:
|
|
178
|
+
f.write("Hello, world!")
|
|
179
|
+
|
|
180
|
+
url = reverse("media", kwargs={"path": "branding/logo.txt"})
|
|
181
|
+
|
|
182
|
+
# Authenticated request succeeds
|
|
183
|
+
response = self.client.get(url)
|
|
184
|
+
self.assertHttpStatus(response, 200)
|
|
185
|
+
self.assertIn("Hello, world!", b"".join(response).decode(response.charset))
|
|
186
|
+
|
|
187
|
+
# Unauthenticated request also succeeds
|
|
188
|
+
self.client.logout()
|
|
189
|
+
response = self.client.get(url)
|
|
190
|
+
self.assertHttpStatus(response, 200)
|
|
191
|
+
self.assertIn("Hello, world!", b"".join(response).decode(response.charset))
|
|
192
|
+
|
|
193
|
+
def test_media_authenticated(self):
|
|
194
|
+
"""
|
|
195
|
+
Test that authenticated users can access regular media files stored in the `MEDIA_ROOT`.
|
|
196
|
+
"""
|
|
197
|
+
with tempfile.TemporaryDirectory() as temp_dir:
|
|
198
|
+
with override_settings(MEDIA_ROOT=temp_dir):
|
|
199
|
+
file_path = os.path.join(temp_dir, "foo.txt")
|
|
200
|
+
with open(file_path, "w") as f:
|
|
201
|
+
f.write("Hello, world!")
|
|
202
|
+
|
|
203
|
+
url = reverse("media", kwargs={"path": "foo.txt"})
|
|
204
|
+
response = self.client.get(url)
|
|
205
|
+
self.assertHttpStatus(response, 200)
|
|
206
|
+
self.assertIn("Hello, world!", b"".join(response).decode(response.charset))
|
|
207
|
+
|
|
208
|
+
|
|
136
209
|
@override_settings(BRANDING_TITLE="Nautobot")
|
|
137
210
|
class SearchFieldsTestCase(TestCase):
|
|
138
211
|
def test_search_bar_redirect_to_login(self):
|
nautobot/core/urls.py
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
from django.conf import settings
|
|
2
2
|
from django.conf.urls import include, url
|
|
3
3
|
from django.urls import path
|
|
4
|
-
from django.views.static import serve
|
|
5
4
|
|
|
6
5
|
from nautobot.core.views import (
|
|
7
6
|
CustomGraphQLView,
|
|
8
7
|
HomeView,
|
|
8
|
+
MediaView,
|
|
9
9
|
StaticMediaFailureView,
|
|
10
10
|
SearchView,
|
|
11
11
|
nautobot_metrics_view,
|
|
@@ -38,8 +38,8 @@ urlpatterns = [
|
|
|
38
38
|
path("api/", include("nautobot.core.api.urls")),
|
|
39
39
|
# GraphQL
|
|
40
40
|
path("graphql/", CustomGraphQLView.as_view(graphiql=True), name="graphql"),
|
|
41
|
-
# Serving static media in Django
|
|
42
|
-
path("media/<path:path>",
|
|
41
|
+
# Serving static media in Django (TODO: should be DEBUG mode only - "This view is NOT hardened for production use")
|
|
42
|
+
path("media/<path:path>", MediaView.as_view(), name="media"),
|
|
43
43
|
# Admin
|
|
44
44
|
path("admin/", admin_site.urls),
|
|
45
45
|
path("admin/background-tasks/", include("django_rq.urls")),
|
nautobot/core/views/__init__.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import platform
|
|
3
|
+
import posixpath
|
|
3
4
|
import sys
|
|
4
5
|
import time
|
|
5
6
|
|
|
@@ -17,6 +18,7 @@ from django.views.decorators.csrf import requires_csrf_token
|
|
|
17
18
|
from django.views.defaults import ERROR_500_TEMPLATE_NAME, page_not_found
|
|
18
19
|
from django.views.csrf import csrf_failure as _csrf_failure
|
|
19
20
|
from django.views.generic import TemplateView, View
|
|
21
|
+
from django.views.static import serve
|
|
20
22
|
from packaging import version
|
|
21
23
|
from graphene_django.views import GraphQLView
|
|
22
24
|
from prometheus_client import multiprocess
|
|
@@ -110,6 +112,25 @@ class HomeView(AccessMixin, TemplateView):
|
|
|
110
112
|
return self.render_to_response(context)
|
|
111
113
|
|
|
112
114
|
|
|
115
|
+
class MediaView(AccessMixin, View):
|
|
116
|
+
"""
|
|
117
|
+
Serves media files while enforcing login restrictions.
|
|
118
|
+
|
|
119
|
+
This view wraps Django's `serve()` function to ensure that access to media files (with the exception of
|
|
120
|
+
branding files defined in `settings.BRANDING_FILEPATHS`) is restricted to authenticated users.
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def get(self, request, path):
|
|
124
|
+
if request.user.is_authenticated:
|
|
125
|
+
return serve(request, path, document_root=settings.MEDIA_ROOT)
|
|
126
|
+
|
|
127
|
+
# Unauthenticated users can access BRANDING_FILEPATHS only
|
|
128
|
+
if posixpath.normpath(path).lstrip("/") in settings.BRANDING_FILEPATHS.values():
|
|
129
|
+
return serve(request, path, document_root=settings.MEDIA_ROOT)
|
|
130
|
+
|
|
131
|
+
return self.handle_no_permission()
|
|
132
|
+
|
|
133
|
+
|
|
113
134
|
class SearchView(AccessMixin, View):
|
|
114
135
|
def get(self, request):
|
|
115
136
|
# if user is not authenticated, redirect to login page
|
nautobot/dcim/forms.py
CHANGED
|
@@ -62,6 +62,7 @@ from nautobot.utilities.forms import (
|
|
|
62
62
|
StaticSelect2,
|
|
63
63
|
StaticSelect2Multiple,
|
|
64
64
|
TagFilterField,
|
|
65
|
+
BOOLEAN_CHOICES,
|
|
65
66
|
)
|
|
66
67
|
from nautobot.utilities.forms.constants import BOOLEAN_WITH_BLANK_CHOICES
|
|
67
68
|
from nautobot.virtualization.models import Cluster, ClusterGroup
|
|
@@ -631,6 +632,12 @@ class RackRoleForm(NautobotModelForm):
|
|
|
631
632
|
]
|
|
632
633
|
|
|
633
634
|
|
|
635
|
+
class RackRoleFilterForm(NautobotFilterForm):
|
|
636
|
+
model = RackRole
|
|
637
|
+
q = forms.CharField(required=False, label="Search")
|
|
638
|
+
color = forms.CharField(max_length=6, required=False, widget=ColorSelect()) # RGB color code
|
|
639
|
+
|
|
640
|
+
|
|
634
641
|
class RackRoleCSVForm(CustomFieldModelCSVForm):
|
|
635
642
|
class Meta:
|
|
636
643
|
model = RackRole
|
|
@@ -1014,6 +1021,15 @@ class ManufacturerForm(NautobotModelForm):
|
|
|
1014
1021
|
]
|
|
1015
1022
|
|
|
1016
1023
|
|
|
1024
|
+
class ManufacturerFilterForm(NautobotFilterForm):
|
|
1025
|
+
model = Manufacturer
|
|
1026
|
+
q = forms.CharField(required=False, label="Search")
|
|
1027
|
+
device_types = DynamicModelMultipleChoiceField(
|
|
1028
|
+
queryset=DeviceType.objects.all(), to_field_name="model", required=False
|
|
1029
|
+
)
|
|
1030
|
+
platforms = DynamicModelMultipleChoiceField(queryset=Platform.objects.all(), to_field_name="name", required=False)
|
|
1031
|
+
|
|
1032
|
+
|
|
1017
1033
|
class ManufacturerCSVForm(CustomFieldModelCSVForm):
|
|
1018
1034
|
class Meta:
|
|
1019
1035
|
model = Manufacturer
|
|
@@ -1763,6 +1779,12 @@ class DeviceRoleForm(NautobotModelForm):
|
|
|
1763
1779
|
]
|
|
1764
1780
|
|
|
1765
1781
|
|
|
1782
|
+
class DeviceRoleFilterForm(NautobotFilterForm):
|
|
1783
|
+
model = DeviceRole
|
|
1784
|
+
q = forms.CharField(required=False, label="Search")
|
|
1785
|
+
vm_role = forms.ChoiceField(choices=BOOLEAN_CHOICES, required=False, label="VM Role")
|
|
1786
|
+
|
|
1787
|
+
|
|
1766
1788
|
class DeviceRoleCSVForm(CustomFieldModelCSVForm):
|
|
1767
1789
|
class Meta:
|
|
1768
1790
|
model = DeviceRole
|
|
@@ -1797,6 +1819,13 @@ class PlatformForm(NautobotModelForm):
|
|
|
1797
1819
|
}
|
|
1798
1820
|
|
|
1799
1821
|
|
|
1822
|
+
class PlatformFilterForm(NautobotFilterForm):
|
|
1823
|
+
model = Platform
|
|
1824
|
+
q = forms.CharField(required=False, label="Search")
|
|
1825
|
+
name = forms.CharField(required=False)
|
|
1826
|
+
network_driver = forms.CharField(required=False)
|
|
1827
|
+
|
|
1828
|
+
|
|
1800
1829
|
class PlatformCSVForm(CustomFieldModelCSVForm):
|
|
1801
1830
|
manufacturer = CSVModelChoiceField(
|
|
1802
1831
|
queryset=Manufacturer.objects.all(),
|
|
@@ -64,6 +64,8 @@ class ComponentTemplateModel(BaseModel, ChangeLoggedModel, CustomFieldModel, Rel
|
|
|
64
64
|
"""
|
|
65
65
|
raise NotImplementedError()
|
|
66
66
|
|
|
67
|
+
instantiate.alters_data = True
|
|
68
|
+
|
|
67
69
|
def to_objectchange(self, action, **kwargs):
|
|
68
70
|
"""
|
|
69
71
|
Return a new ObjectChange with the `related_object` pinned to the `device_type` by default.
|
|
@@ -96,6 +98,8 @@ class ComponentTemplateModel(BaseModel, ChangeLoggedModel, CustomFieldModel, Rel
|
|
|
96
98
|
**kwargs,
|
|
97
99
|
)
|
|
98
100
|
|
|
101
|
+
instantiate_model.alters_data = True
|
|
102
|
+
|
|
99
103
|
|
|
100
104
|
@extras_features(
|
|
101
105
|
"custom_fields",
|
|
@@ -879,6 +879,8 @@ class InterfaceRedundancyGroup(StatusModel, PrimaryModel): # pylint: disable=to
|
|
|
879
879
|
)
|
|
880
880
|
return instance.validated_save()
|
|
881
881
|
|
|
882
|
+
add_interface.alters_data = True
|
|
883
|
+
|
|
882
884
|
def remove_interface(self, interface):
|
|
883
885
|
"""
|
|
884
886
|
Remove an interface.
|
|
@@ -892,6 +894,8 @@ class InterfaceRedundancyGroup(StatusModel, PrimaryModel): # pylint: disable=to
|
|
|
892
894
|
)
|
|
893
895
|
return instance.delete()
|
|
894
896
|
|
|
897
|
+
remove_interface.alters_data = True
|
|
898
|
+
|
|
895
899
|
|
|
896
900
|
@extras_features(
|
|
897
901
|
"relationships",
|
nautobot/dcim/models/devices.py
CHANGED
|
@@ -873,6 +873,8 @@ class Device(PrimaryModel, ConfigContextModel, StatusModel):
|
|
|
873
873
|
model.objects.bulk_create([x.instantiate(self) for x in templates])
|
|
874
874
|
return instantiated_components
|
|
875
875
|
|
|
876
|
+
create_components.alters_data = True
|
|
877
|
+
|
|
876
878
|
def to_csv(self):
|
|
877
879
|
return (
|
|
878
880
|
self.name or "",
|
nautobot/dcim/views.py
CHANGED
|
@@ -511,6 +511,7 @@ class RackGroupBulkDeleteView(generic.BulkDeleteView):
|
|
|
511
511
|
class RackRoleListView(generic.ObjectListView):
|
|
512
512
|
queryset = RackRole.objects.annotate(rack_count=count_related(Rack, "role"))
|
|
513
513
|
filterset = filters.RackRoleFilterSet
|
|
514
|
+
filterset_form = forms.RackRoleFilterForm
|
|
514
515
|
table = tables.RackRoleTable
|
|
515
516
|
|
|
516
517
|
|
|
@@ -585,6 +586,7 @@ class RackElevationListView(generic.ObjectListView):
|
|
|
585
586
|
"reverse", # control of ordering
|
|
586
587
|
)
|
|
587
588
|
filterset = filters.RackFilterSet
|
|
589
|
+
filterset_form = forms.RackFilterForm
|
|
588
590
|
action_buttons = []
|
|
589
591
|
template_name = "dcim/rack_elevation_list.html"
|
|
590
592
|
|
|
@@ -763,6 +765,7 @@ class ManufacturerListView(generic.ObjectListView):
|
|
|
763
765
|
platform_count=count_related(Platform, "manufacturer"),
|
|
764
766
|
)
|
|
765
767
|
filterset = filters.ManufacturerFilterSet
|
|
768
|
+
filterset_form = forms.ManufacturerFilterForm
|
|
766
769
|
table = tables.ManufacturerTable
|
|
767
770
|
|
|
768
771
|
|
|
@@ -1240,6 +1243,7 @@ class DeviceRoleListView(generic.ObjectListView):
|
|
|
1240
1243
|
vm_count=count_related(VirtualMachine, "role"),
|
|
1241
1244
|
)
|
|
1242
1245
|
filterset = filters.DeviceRoleFilterSet
|
|
1246
|
+
filterset_form = forms.DeviceRoleFilterForm
|
|
1243
1247
|
table = tables.DeviceRoleTable
|
|
1244
1248
|
|
|
1245
1249
|
|
|
@@ -1299,6 +1303,7 @@ class PlatformListView(generic.ObjectListView):
|
|
|
1299
1303
|
vm_count=count_related(VirtualMachine, "platform"),
|
|
1300
1304
|
)
|
|
1301
1305
|
filterset = filters.PlatformFilterSet
|
|
1306
|
+
filterset_form = forms.PlatformFilterForm
|
|
1302
1307
|
table = tables.PlatformTable
|
|
1303
1308
|
|
|
1304
1309
|
|
nautobot/extras/forms/forms.py
CHANGED
|
@@ -103,6 +103,7 @@ __all__ = (
|
|
|
103
103
|
"CustomFieldModelCSVForm",
|
|
104
104
|
"CustomFieldBulkCreateForm", # 2.0 TODO remove this deprecated class
|
|
105
105
|
"CustomFieldChoiceFormSet",
|
|
106
|
+
"CustomFieldViewFilterForm",
|
|
106
107
|
"CustomLinkForm",
|
|
107
108
|
"CustomLinkFilterForm",
|
|
108
109
|
"DynamicGroupForm",
|
|
@@ -400,6 +401,17 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
|
|
|
400
401
|
)
|
|
401
402
|
|
|
402
403
|
|
|
404
|
+
class CustomFieldViewFilterForm(BootstrapMixin, forms.Form):
|
|
405
|
+
model = CustomField
|
|
406
|
+
q = forms.CharField(required=False, label="Search")
|
|
407
|
+
content_types = MultipleContentTypeField(
|
|
408
|
+
queryset=ContentType.objects.filter(FeatureQuery("custom_fields").get_query()),
|
|
409
|
+
choices_as_strings=True,
|
|
410
|
+
required=False,
|
|
411
|
+
label="Content Type(s)",
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
|
|
403
415
|
class CustomFieldModelCSVForm(CSVModelForm, CustomFieldModelFormMixin):
|
|
404
416
|
"""Base class for CSV export of models that support custom fields."""
|
|
405
417
|
|
|
@@ -241,6 +241,8 @@ class CustomFieldModel(models.Model):
|
|
|
241
241
|
elif cf.required:
|
|
242
242
|
raise ValidationError(f"Missing required custom field '{cf.name}'.")
|
|
243
243
|
|
|
244
|
+
clean.alters_data = True
|
|
245
|
+
|
|
244
246
|
# Computed Field Methods
|
|
245
247
|
def has_computed_fields(self, advanced_ui=None):
|
|
246
248
|
"""
|
|
@@ -130,6 +130,8 @@ class GitRepository(PrimaryModel):
|
|
|
130
130
|
"""
|
|
131
131
|
self._dryrun = True
|
|
132
132
|
|
|
133
|
+
set_dryrun.alters_data = True
|
|
134
|
+
|
|
133
135
|
def save(self, *args, trigger_resync=True, **kwargs):
|
|
134
136
|
if self.__initial_token and self._token == self.TOKEN_PLACEHOLDER:
|
|
135
137
|
# User edited the repo but did NOT specify a new token value. Make sure we keep the existing value.
|
nautobot/extras/models/groups.py
CHANGED
|
@@ -357,6 +357,8 @@ class DynamicGroup(OrganizationalModel):
|
|
|
357
357
|
|
|
358
358
|
return self.members_cached
|
|
359
359
|
|
|
360
|
+
update_cached_members.alters_data = True
|
|
361
|
+
|
|
360
362
|
def has_member(self, obj, use_cache=False):
|
|
361
363
|
"""
|
|
362
364
|
Return True if the given object is a member of this group.
|
|
@@ -460,6 +462,8 @@ class DynamicGroup(OrganizationalModel):
|
|
|
460
462
|
|
|
461
463
|
self.filter = new_filter
|
|
462
464
|
|
|
465
|
+
set_filter.alters_data = True
|
|
466
|
+
|
|
463
467
|
def get_initial(self):
|
|
464
468
|
"""
|
|
465
469
|
Return an form-friendly version of `self.filter` for initial form data.
|
|
@@ -693,6 +697,8 @@ class DynamicGroup(OrganizationalModel):
|
|
|
693
697
|
instance = self.children.through(parent_group=self, group=child, operator=operator, weight=weight)
|
|
694
698
|
return instance.validated_save()
|
|
695
699
|
|
|
700
|
+
add_child.alters_data = True
|
|
701
|
+
|
|
696
702
|
def remove_child(self, child):
|
|
697
703
|
"""
|
|
698
704
|
Remove a child group.
|
|
@@ -703,6 +709,8 @@ class DynamicGroup(OrganizationalModel):
|
|
|
703
709
|
instance = self.children.through.objects.get(parent_group=self, group=child)
|
|
704
710
|
return instance.delete()
|
|
705
711
|
|
|
712
|
+
remove_child.alters_data = True
|
|
713
|
+
|
|
706
714
|
def get_descendants(self, group=None):
|
|
707
715
|
"""
|
|
708
716
|
Recursively return a list of the children of all child groups.
|
nautobot/extras/models/jobs.py
CHANGED
|
@@ -738,6 +738,8 @@ class JobResult(BaseModel, CustomFieldModel):
|
|
|
738
738
|
duration.total_seconds()
|
|
739
739
|
)
|
|
740
740
|
|
|
741
|
+
set_status.alters_data = True
|
|
742
|
+
|
|
741
743
|
@classmethod
|
|
742
744
|
def enqueue_job(cls, func, name, obj_type, user, *args, celery_kwargs=None, schedule=None, **kwargs):
|
|
743
745
|
"""
|
|
@@ -805,6 +807,8 @@ class JobResult(BaseModel, CustomFieldModel):
|
|
|
805
807
|
|
|
806
808
|
return job_result
|
|
807
809
|
|
|
810
|
+
enqueue_job.__func__.alters_data = True
|
|
811
|
+
|
|
808
812
|
def log(
|
|
809
813
|
self,
|
|
810
814
|
message,
|
|
@@ -868,6 +872,8 @@ class JobResult(BaseModel, CustomFieldModel):
|
|
|
868
872
|
|
|
869
873
|
return log
|
|
870
874
|
|
|
875
|
+
log.alters_data = True
|
|
876
|
+
|
|
871
877
|
|
|
872
878
|
#
|
|
873
879
|
# Job Button
|
|
@@ -945,6 +951,8 @@ class ScheduledJobs(models.Model):
|
|
|
945
951
|
if not instance.no_changes:
|
|
946
952
|
cls.update_changed()
|
|
947
953
|
|
|
954
|
+
changed.__func__.alters_data = True
|
|
955
|
+
|
|
948
956
|
@classmethod
|
|
949
957
|
def update_changed(cls, raw=False, **kwargs):
|
|
950
958
|
"""This function acts as a signal handler to track changes to the scheduled job that is triggered after a change"""
|
|
@@ -952,6 +960,8 @@ class ScheduledJobs(models.Model):
|
|
|
952
960
|
return
|
|
953
961
|
cls.objects.update_or_create(ident=1, defaults={"last_update": timezone.now()})
|
|
954
962
|
|
|
963
|
+
update_changed.__func__.alters_data = True
|
|
964
|
+
|
|
955
965
|
@classmethod
|
|
956
966
|
def last_change(cls):
|
|
957
967
|
"""This function acts as a getter for the last update on scheduled jobs"""
|
nautobot/extras/models/models.py
CHANGED
|
@@ -466,6 +466,8 @@ class ExportTemplate(BaseModel, ChangeLoggedModel, RelationshipModel, NotesMixin
|
|
|
466
466
|
):
|
|
467
467
|
raise ValidationError({"name": "An ExportTemplate with this name and content type already exists."})
|
|
468
468
|
|
|
469
|
+
clean.alters_data = True
|
|
470
|
+
|
|
469
471
|
|
|
470
472
|
#
|
|
471
473
|
# File attachments
|
|
@@ -4,8 +4,8 @@ from django.core.exceptions import ValidationError
|
|
|
4
4
|
from django.core.serializers.json import DjangoJSONEncoder
|
|
5
5
|
from django.db import models
|
|
6
6
|
from django.urls import reverse
|
|
7
|
-
|
|
8
|
-
from jinja2.
|
|
7
|
+
from jinja2.exceptions import TemplateSyntaxError, UndefinedError
|
|
8
|
+
from jinja2.sandbox import unsafe
|
|
9
9
|
|
|
10
10
|
from nautobot.core.fields import AutoSlugField
|
|
11
11
|
from nautobot.core.models import BaseModel
|
|
@@ -78,6 +78,7 @@ class Secret(PrimaryModel):
|
|
|
78
78
|
except (TemplateSyntaxError, UndefinedError) as exc:
|
|
79
79
|
raise SecretParametersError(self, registry["secrets_providers"].get(self.provider), str(exc)) from exc
|
|
80
80
|
|
|
81
|
+
@unsafe
|
|
81
82
|
def get_value(self, obj=None):
|
|
82
83
|
"""Retrieve the secret value that this Secret is a representation of.
|
|
83
84
|
|
|
@@ -97,6 +98,8 @@ class Secret(PrimaryModel):
|
|
|
97
98
|
except Exception as exc:
|
|
98
99
|
raise SecretError(self, provider, str(exc)) from exc
|
|
99
100
|
|
|
101
|
+
get_value.do_not_call_in_templates = True
|
|
102
|
+
|
|
100
103
|
def clean(self):
|
|
101
104
|
provider = registry["secrets_providers"].get(self.provider)
|
|
102
105
|
if not provider:
|
|
@@ -137,6 +140,7 @@ class SecretsGroup(OrganizationalModel):
|
|
|
137
140
|
def to_csv(self):
|
|
138
141
|
return (self.name, self.slug, self.description)
|
|
139
142
|
|
|
143
|
+
@unsafe
|
|
140
144
|
def get_secret_value(self, access_type, secret_type, obj=None, **kwargs):
|
|
141
145
|
"""Helper method to retrieve a specific secret from this group.
|
|
142
146
|
|
|
@@ -145,6 +149,8 @@ class SecretsGroup(OrganizationalModel):
|
|
|
145
149
|
secret = self.secrets.through.objects.get(group=self, access_type=access_type, secret_type=secret_type).secret
|
|
146
150
|
return secret.get_value(obj=obj, **kwargs)
|
|
147
151
|
|
|
152
|
+
get_secret_value.do_not_call_in_templates = True
|
|
153
|
+
|
|
148
154
|
|
|
149
155
|
@extras_features(
|
|
150
156
|
"graphql",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
2
|
|
|
3
|
+
from jinja2.sandbox import unsafe
|
|
4
|
+
|
|
3
5
|
from nautobot.extras.registry import registry
|
|
4
6
|
|
|
5
7
|
from .exceptions import SecretError, SecretParametersError, SecretProviderError, SecretValueNotFoundError
|
|
@@ -31,6 +33,7 @@ class SecretsProvider(ABC):
|
|
|
31
33
|
|
|
32
34
|
@classmethod
|
|
33
35
|
@abstractmethod
|
|
36
|
+
@unsafe
|
|
34
37
|
def get_value_for_secret(cls, secret, obj=None, **kwargs):
|
|
35
38
|
"""Retrieve the stored value described by the given Secret record.
|
|
36
39
|
|
|
@@ -41,6 +44,17 @@ class SecretsProvider(ABC):
|
|
|
41
44
|
obj (object): Django model instance or similar providing additional context for retrieving the secret.
|
|
42
45
|
"""
|
|
43
46
|
|
|
47
|
+
get_value_for_secret.__func__.do_not_call_in_templates = True
|
|
48
|
+
|
|
49
|
+
def __init_subclass__(cls, **kwargs):
|
|
50
|
+
# Automatically apply protection against Django and Jinja2 template execution to child classes.
|
|
51
|
+
if not getattr(cls.get_value_for_secret, "do_not_call_in_templates", False): # Django
|
|
52
|
+
cls.get_value_for_secret.__func__.do_not_call_in_templates = True
|
|
53
|
+
if not getattr(cls.get_value_for_secret, "unsafe_callable", False): # Jinja @unsafe decorator
|
|
54
|
+
cls.get_value_for_secret.__func__.unsafe_callable = True
|
|
55
|
+
|
|
56
|
+
super().__init_subclass__(**kwargs)
|
|
57
|
+
|
|
44
58
|
|
|
45
59
|
def register_secrets_provider(provider):
|
|
46
60
|
"""
|
|
@@ -86,6 +86,13 @@ class ComputedFieldTest(TestCase):
|
|
|
86
86
|
fallback_value="An error occurred while rendering this template.",
|
|
87
87
|
weight=50,
|
|
88
88
|
)
|
|
89
|
+
self.evil_computed_field = ComputedField.objects.create(
|
|
90
|
+
content_type=ContentType.objects.get_for_model(Secret),
|
|
91
|
+
slug="evil_computed_field",
|
|
92
|
+
label="Evil Computed Field",
|
|
93
|
+
template="{{ obj.get_value() }}",
|
|
94
|
+
weight=666,
|
|
95
|
+
)
|
|
89
96
|
self.blank_fallback_value = ComputedField.objects.create(
|
|
90
97
|
content_type=ContentType.objects.get_for_model(Site),
|
|
91
98
|
slug="blank_fallback_value",
|
|
@@ -94,6 +101,18 @@ class ComputedFieldTest(TestCase):
|
|
|
94
101
|
weight=50,
|
|
95
102
|
)
|
|
96
103
|
self.site1 = Site.objects.first()
|
|
104
|
+
self.secret = Secret.objects.create(
|
|
105
|
+
name="Environment Variable Secret",
|
|
106
|
+
provider="environment-variable",
|
|
107
|
+
parameters={"variable": "NAUTOBOT_ROOT"},
|
|
108
|
+
)
|
|
109
|
+
self.secrets_group = SecretsGroup.objects.create(name="Group of Secrets")
|
|
110
|
+
SecretsGroupAssociation.objects.create(
|
|
111
|
+
group=self.secrets_group,
|
|
112
|
+
secret=self.secret,
|
|
113
|
+
access_type=SecretsGroupAccessTypeChoices.TYPE_GENERIC,
|
|
114
|
+
secret_type=SecretsGroupSecretTypeChoices.TYPE_SECRET,
|
|
115
|
+
)
|
|
97
116
|
|
|
98
117
|
def test_render_method(self):
|
|
99
118
|
rendered_value = self.good_computed_field.render(context={"obj": self.site1})
|
|
@@ -107,6 +126,13 @@ class ComputedFieldTest(TestCase):
|
|
|
107
126
|
rendered_value = self.bad_computed_field.render(context={"obj": self.site1})
|
|
108
127
|
self.assertEqual(rendered_value, self.bad_computed_field.fallback_value)
|
|
109
128
|
|
|
129
|
+
def test_render_method_evil_template(self):
|
|
130
|
+
rendered_value = self.evil_computed_field.render(context={"obj": self.secret})
|
|
131
|
+
self.assertEqual(rendered_value, "")
|
|
132
|
+
self.evil_computed_field.template = "{{ obj.secrets_groups.first().get_secret_value('Generic', 'secret') }}"
|
|
133
|
+
rendered_value = self.evil_computed_field.render(context={"obj": self.secret})
|
|
134
|
+
self.assertEqual(rendered_value, "")
|
|
135
|
+
|
|
110
136
|
|
|
111
137
|
class ConfigContextTest(TestCase):
|
|
112
138
|
"""
|
nautobot/extras/views.py
CHANGED
|
@@ -356,6 +356,7 @@ class CustomFieldListView(generic.ObjectListView):
|
|
|
356
356
|
queryset = CustomField.objects.all()
|
|
357
357
|
table = tables.CustomFieldTable
|
|
358
358
|
filterset = filters.CustomFieldFilterSet
|
|
359
|
+
filterset_form = forms.CustomFieldViewFilterForm
|
|
359
360
|
action_buttons = ("add",)
|
|
360
361
|
|
|
361
362
|
|
nautobot/ipam/filters.py
CHANGED
|
@@ -181,7 +181,7 @@ class AggregateFilterSet(NautobotFilterSet, IPAMFilterSetMixin, TenancyModelFilt
|
|
|
181
181
|
class RoleFilterSet(NautobotFilterSet, NameSlugSearchFilterSet):
|
|
182
182
|
class Meta:
|
|
183
183
|
model = Role
|
|
184
|
-
fields = ["id", "name", "slug"]
|
|
184
|
+
fields = ["id", "name", "slug", "weight"]
|
|
185
185
|
|
|
186
186
|
|
|
187
187
|
class PrefixFilterSet(
|
nautobot/ipam/forms.py
CHANGED
|
@@ -334,6 +334,12 @@ class RoleForm(NautobotModelForm):
|
|
|
334
334
|
]
|
|
335
335
|
|
|
336
336
|
|
|
337
|
+
class RoleFilterForm(NautobotFilterForm):
|
|
338
|
+
model = Role
|
|
339
|
+
q = forms.CharField(required=False, label="Search")
|
|
340
|
+
weight = forms.IntegerField(required=False, label="Weight")
|
|
341
|
+
|
|
342
|
+
|
|
337
343
|
class RoleCSVForm(CustomFieldModelCSVForm):
|
|
338
344
|
class Meta:
|
|
339
345
|
model = Role
|