nautobot 1.6.31__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.

Files changed (235) hide show
  1. nautobot/circuits/forms.py +6 -0
  2. nautobot/circuits/views.py +1 -0
  3. nautobot/core/graphql/schema.py +3 -1
  4. nautobot/core/graphql/types.py +10 -0
  5. nautobot/core/models/__init__.py +2 -0
  6. nautobot/core/templates/generic/object_list.html +1 -1
  7. nautobot/core/tests/test_views.py +73 -0
  8. nautobot/core/urls.py +3 -3
  9. nautobot/core/views/__init__.py +21 -0
  10. nautobot/dcim/forms.py +29 -0
  11. nautobot/dcim/models/device_component_templates.py +4 -0
  12. nautobot/dcim/models/device_components.py +4 -0
  13. nautobot/dcim/models/devices.py +2 -0
  14. nautobot/dcim/views.py +4 -0
  15. nautobot/extras/forms/forms.py +12 -0
  16. nautobot/extras/models/customfields.py +2 -0
  17. nautobot/extras/models/datasources.py +2 -0
  18. nautobot/extras/models/groups.py +8 -0
  19. nautobot/extras/models/jobs.py +10 -0
  20. nautobot/extras/models/models.py +2 -0
  21. nautobot/extras/models/secrets.py +8 -2
  22. nautobot/extras/secrets/__init__.py +14 -0
  23. nautobot/extras/tests/test_models.py +26 -0
  24. nautobot/extras/views.py +1 -0
  25. nautobot/ipam/filters.py +1 -1
  26. nautobot/ipam/forms.py +6 -0
  27. nautobot/ipam/models.py +8 -0
  28. nautobot/ipam/views.py +1 -0
  29. nautobot/project-static/docs/404.html +62 -0
  30. nautobot/project-static/docs/additional-features/caching.html +62 -0
  31. nautobot/project-static/docs/additional-features/change-logging.html +62 -0
  32. nautobot/project-static/docs/additional-features/config-contexts.html +62 -0
  33. nautobot/project-static/docs/additional-features/graphql.html +62 -0
  34. nautobot/project-static/docs/additional-features/healthcheck.html +62 -0
  35. nautobot/project-static/docs/additional-features/job-scheduling-and-approvals.html +63 -1
  36. nautobot/project-static/docs/additional-features/jobs.html +62 -0
  37. nautobot/project-static/docs/additional-features/napalm.html +62 -0
  38. nautobot/project-static/docs/additional-features/prometheus-metrics.html +62 -0
  39. nautobot/project-static/docs/additional-features/template-filters.html +62 -0
  40. nautobot/project-static/docs/administration/celery-queues.html +63 -1
  41. nautobot/project-static/docs/administration/nautobot-server.html +62 -0
  42. nautobot/project-static/docs/administration/nautobot-shell.html +62 -0
  43. nautobot/project-static/docs/administration/permissions.html +62 -0
  44. nautobot/project-static/docs/administration/replicating-nautobot.html +62 -0
  45. nautobot/project-static/docs/administration/request-profiling.html +62 -0
  46. nautobot/project-static/docs/administration/security/index.html +4407 -0
  47. nautobot/project-static/docs/administration/security/notices.html +4797 -0
  48. nautobot/project-static/docs/apps/index.html +62 -0
  49. nautobot/project-static/docs/apps/nautobot-apps.html +62 -0
  50. nautobot/project-static/docs/code-reference/nautobot/apps/__init__.html +62 -0
  51. nautobot/project-static/docs/code-reference/nautobot/apps/admin.html +62 -0
  52. nautobot/project-static/docs/code-reference/nautobot/apps/api.html +62 -0
  53. nautobot/project-static/docs/code-reference/nautobot/apps/choices.html +62 -0
  54. nautobot/project-static/docs/code-reference/nautobot/apps/datasources.html +62 -0
  55. nautobot/project-static/docs/code-reference/nautobot/apps/filters.html +62 -0
  56. nautobot/project-static/docs/code-reference/nautobot/apps/forms.html +83 -21
  57. nautobot/project-static/docs/code-reference/nautobot/apps/models.html +67 -1
  58. nautobot/project-static/docs/code-reference/nautobot/apps/secrets.html +140 -52
  59. nautobot/project-static/docs/code-reference/nautobot/apps/tables.html +62 -0
  60. nautobot/project-static/docs/code-reference/nautobot/apps/testing.html +3203 -3101
  61. nautobot/project-static/docs/code-reference/nautobot/apps/ui.html +62 -0
  62. nautobot/project-static/docs/code-reference/nautobot/apps/urls.html +62 -0
  63. nautobot/project-static/docs/code-reference/nautobot/apps/utils.html +210 -148
  64. nautobot/project-static/docs/code-reference/nautobot/apps/views.html +62 -0
  65. nautobot/project-static/docs/configuration/authentication/ldap.html +62 -0
  66. nautobot/project-static/docs/configuration/authentication/remote.html +62 -0
  67. nautobot/project-static/docs/configuration/authentication/sso.html +62 -0
  68. nautobot/project-static/docs/configuration/index.html +62 -0
  69. nautobot/project-static/docs/configuration/optional-settings.html +67 -5
  70. nautobot/project-static/docs/configuration/required-settings.html +62 -0
  71. nautobot/project-static/docs/core-functionality/circuits.html +62 -0
  72. nautobot/project-static/docs/core-functionality/device-types.html +62 -0
  73. nautobot/project-static/docs/core-functionality/devices.html +62 -0
  74. nautobot/project-static/docs/core-functionality/ipam.html +62 -0
  75. nautobot/project-static/docs/core-functionality/power.html +62 -0
  76. nautobot/project-static/docs/core-functionality/secrets.html +62 -0
  77. nautobot/project-static/docs/core-functionality/services.html +62 -0
  78. nautobot/project-static/docs/core-functionality/sites-and-racks.html +62 -0
  79. nautobot/project-static/docs/core-functionality/tenancy.html +62 -0
  80. nautobot/project-static/docs/core-functionality/virtualization.html +62 -0
  81. nautobot/project-static/docs/core-functionality/vlans.html +62 -0
  82. nautobot/project-static/docs/development/application-registry.html +62 -0
  83. nautobot/project-static/docs/development/best-practices.html +62 -0
  84. nautobot/project-static/docs/development/docker-compose-advanced-use-cases.html +62 -0
  85. nautobot/project-static/docs/development/extending-models.html +62 -0
  86. nautobot/project-static/docs/development/generic-views.html +62 -0
  87. nautobot/project-static/docs/development/getting-started.html +63 -1
  88. nautobot/project-static/docs/development/homepage.html +62 -0
  89. nautobot/project-static/docs/development/index.html +62 -0
  90. nautobot/project-static/docs/development/navigation-menu.html +62 -0
  91. nautobot/project-static/docs/development/release-checklist.html +62 -0
  92. nautobot/project-static/docs/development/style-guide.html +62 -0
  93. nautobot/project-static/docs/development/templates.html +62 -0
  94. nautobot/project-static/docs/development/testing.html +62 -0
  95. nautobot/project-static/docs/development/user-preferences.html +62 -0
  96. nautobot/project-static/docs/docker/index.html +62 -0
  97. nautobot/project-static/docs/index.html +62 -0
  98. nautobot/project-static/docs/installation/centos.html +62 -0
  99. nautobot/project-static/docs/installation/external-authentication.html +62 -0
  100. nautobot/project-static/docs/installation/http-server.html +62 -0
  101. nautobot/project-static/docs/installation/index.html +62 -0
  102. nautobot/project-static/docs/installation/migrating-from-netbox.html +62 -0
  103. nautobot/project-static/docs/installation/migrating-from-postgresql.html +62 -0
  104. nautobot/project-static/docs/installation/nautobot.html +62 -0
  105. nautobot/project-static/docs/installation/selinux-troubleshooting.html +62 -0
  106. nautobot/project-static/docs/installation/services.html +62 -0
  107. nautobot/project-static/docs/installation/ubuntu.html +62 -0
  108. nautobot/project-static/docs/installation/upgrading.html +62 -0
  109. nautobot/project-static/docs/models/circuits/circuit.html +62 -0
  110. nautobot/project-static/docs/models/circuits/circuittermination.html +62 -0
  111. nautobot/project-static/docs/models/circuits/circuittype.html +62 -0
  112. nautobot/project-static/docs/models/circuits/provider.html +62 -0
  113. nautobot/project-static/docs/models/circuits/providernetwork.html +62 -0
  114. nautobot/project-static/docs/models/dcim/cable.html +62 -0
  115. nautobot/project-static/docs/models/dcim/consoleport.html +62 -0
  116. nautobot/project-static/docs/models/dcim/consoleporttemplate.html +62 -0
  117. nautobot/project-static/docs/models/dcim/consoleserverport.html +62 -0
  118. nautobot/project-static/docs/models/dcim/consoleserverporttemplate.html +62 -0
  119. nautobot/project-static/docs/models/dcim/device.html +62 -0
  120. nautobot/project-static/docs/models/dcim/devicebay.html +62 -0
  121. nautobot/project-static/docs/models/dcim/devicebaytemplate.html +62 -0
  122. nautobot/project-static/docs/models/dcim/deviceredundancygroup.html +62 -0
  123. nautobot/project-static/docs/models/dcim/devicerole.html +62 -0
  124. nautobot/project-static/docs/models/dcim/devicetype.html +62 -0
  125. nautobot/project-static/docs/models/dcim/frontport.html +62 -0
  126. nautobot/project-static/docs/models/dcim/frontporttemplate.html +62 -0
  127. nautobot/project-static/docs/models/dcim/interface.html +62 -0
  128. nautobot/project-static/docs/models/dcim/interfaceredundancygroup.html +62 -0
  129. nautobot/project-static/docs/models/dcim/interfacetemplate.html +62 -0
  130. nautobot/project-static/docs/models/dcim/inventoryitem.html +62 -0
  131. nautobot/project-static/docs/models/dcim/location.html +62 -0
  132. nautobot/project-static/docs/models/dcim/locationtype.html +62 -0
  133. nautobot/project-static/docs/models/dcim/manufacturer.html +62 -0
  134. nautobot/project-static/docs/models/dcim/platform.html +62 -0
  135. nautobot/project-static/docs/models/dcim/powerfeed.html +62 -0
  136. nautobot/project-static/docs/models/dcim/poweroutlet.html +62 -0
  137. nautobot/project-static/docs/models/dcim/poweroutlettemplate.html +62 -0
  138. nautobot/project-static/docs/models/dcim/powerpanel.html +62 -0
  139. nautobot/project-static/docs/models/dcim/powerport.html +62 -0
  140. nautobot/project-static/docs/models/dcim/powerporttemplate.html +62 -0
  141. nautobot/project-static/docs/models/dcim/rack.html +62 -0
  142. nautobot/project-static/docs/models/dcim/rackgroup.html +62 -0
  143. nautobot/project-static/docs/models/dcim/rackreservation.html +62 -0
  144. nautobot/project-static/docs/models/dcim/rackrole.html +62 -0
  145. nautobot/project-static/docs/models/dcim/rearport.html +62 -0
  146. nautobot/project-static/docs/models/dcim/rearporttemplate.html +62 -0
  147. nautobot/project-static/docs/models/dcim/region.html +62 -0
  148. nautobot/project-static/docs/models/dcim/site.html +62 -0
  149. nautobot/project-static/docs/models/dcim/virtualchassis.html +62 -0
  150. nautobot/project-static/docs/models/extras/computedfield.html +63 -1
  151. nautobot/project-static/docs/models/extras/configcontext.html +62 -0
  152. nautobot/project-static/docs/models/extras/configcontextschema.html +62 -0
  153. nautobot/project-static/docs/models/extras/customfield.html +62 -0
  154. nautobot/project-static/docs/models/extras/customlink.html +63 -1
  155. nautobot/project-static/docs/models/extras/dynamicgroup.html +62 -0
  156. nautobot/project-static/docs/models/extras/exporttemplate.html +62 -0
  157. nautobot/project-static/docs/models/extras/gitrepository.html +62 -0
  158. nautobot/project-static/docs/models/extras/graphqlquery.html +62 -0
  159. nautobot/project-static/docs/models/extras/imageattachment.html +62 -0
  160. nautobot/project-static/docs/models/extras/job.html +62 -0
  161. nautobot/project-static/docs/models/extras/jobbutton.html +63 -1
  162. nautobot/project-static/docs/models/extras/jobhook.html +62 -0
  163. nautobot/project-static/docs/models/extras/joblogentry.html +62 -0
  164. nautobot/project-static/docs/models/extras/jobresult.html +62 -0
  165. nautobot/project-static/docs/models/extras/note.html +62 -0
  166. nautobot/project-static/docs/models/extras/relationship.html +62 -0
  167. nautobot/project-static/docs/models/extras/secret.html +62 -0
  168. nautobot/project-static/docs/models/extras/secretsgroup.html +62 -0
  169. nautobot/project-static/docs/models/extras/status.html +62 -0
  170. nautobot/project-static/docs/models/extras/tag.html +62 -0
  171. nautobot/project-static/docs/models/extras/webhook.html +62 -0
  172. nautobot/project-static/docs/models/ipam/aggregate.html +62 -0
  173. nautobot/project-static/docs/models/ipam/ipaddress.html +62 -0
  174. nautobot/project-static/docs/models/ipam/prefix.html +62 -0
  175. nautobot/project-static/docs/models/ipam/rir.html +62 -0
  176. nautobot/project-static/docs/models/ipam/role.html +62 -0
  177. nautobot/project-static/docs/models/ipam/routetarget.html +62 -0
  178. nautobot/project-static/docs/models/ipam/service.html +62 -0
  179. nautobot/project-static/docs/models/ipam/vlan.html +62 -0
  180. nautobot/project-static/docs/models/ipam/vlangroup.html +62 -0
  181. nautobot/project-static/docs/models/ipam/vrf.html +62 -0
  182. nautobot/project-static/docs/models/tenancy/tenant.html +62 -0
  183. nautobot/project-static/docs/models/tenancy/tenantgroup.html +62 -0
  184. nautobot/project-static/docs/models/users/objectpermission.html +62 -0
  185. nautobot/project-static/docs/models/users/token.html +62 -0
  186. nautobot/project-static/docs/models/virtualization/cluster.html +62 -0
  187. nautobot/project-static/docs/models/virtualization/clustergroup.html +62 -0
  188. nautobot/project-static/docs/models/virtualization/clustertype.html +62 -0
  189. nautobot/project-static/docs/models/virtualization/virtualmachine.html +62 -0
  190. nautobot/project-static/docs/models/virtualization/vminterface.html +62 -0
  191. nautobot/project-static/docs/plugins/development.html +63 -1
  192. nautobot/project-static/docs/plugins/index.html +62 -0
  193. nautobot/project-static/docs/plugins/porting-from-netbox.html +62 -0
  194. nautobot/project-static/docs/release-notes/index.html +62 -0
  195. nautobot/project-static/docs/release-notes/version-1.0.html +62 -0
  196. nautobot/project-static/docs/release-notes/version-1.1.html +64 -2
  197. nautobot/project-static/docs/release-notes/version-1.2.html +63 -1
  198. nautobot/project-static/docs/release-notes/version-1.3.html +62 -0
  199. nautobot/project-static/docs/release-notes/version-1.4.html +62 -0
  200. nautobot/project-static/docs/release-notes/version-1.5.html +62 -0
  201. nautobot/project-static/docs/release-notes/version-1.6.html +354 -188
  202. nautobot/project-static/docs/rest-api/authentication.html +62 -0
  203. nautobot/project-static/docs/rest-api/filtering.html +62 -0
  204. nautobot/project-static/docs/rest-api/overview.html +62 -0
  205. nautobot/project-static/docs/search/search_index.json +1 -1
  206. nautobot/project-static/docs/sitemap.xml +197 -187
  207. nautobot/project-static/docs/sitemap.xml.gz +0 -0
  208. nautobot/project-static/docs/user-guides/custom-fields.html +62 -0
  209. nautobot/project-static/docs/user-guides/getting-started/creating-devices.html +63 -1
  210. nautobot/project-static/docs/user-guides/getting-started/index.html +62 -0
  211. nautobot/project-static/docs/user-guides/getting-started/interfaces.html +62 -0
  212. nautobot/project-static/docs/user-guides/getting-started/ipam.html +62 -0
  213. nautobot/project-static/docs/user-guides/getting-started/platforms.html +62 -0
  214. nautobot/project-static/docs/user-guides/getting-started/regions.html +62 -0
  215. nautobot/project-static/docs/user-guides/getting-started/search-bar.html +62 -0
  216. nautobot/project-static/docs/user-guides/getting-started/tenants.html +62 -0
  217. nautobot/project-static/docs/user-guides/getting-started/vlans-and-vlan-groups.html +63 -1
  218. nautobot/project-static/docs/user-guides/git-data-source.html +62 -0
  219. nautobot/project-static/docs/user-guides/graphql.html +62 -0
  220. nautobot/project-static/docs/user-guides/relationships.html +62 -0
  221. nautobot/project-static/docs/user-guides/s3-django-storage.html +62 -0
  222. nautobot/tenancy/forms.py +10 -0
  223. nautobot/tenancy/views.py +1 -0
  224. nautobot/users/models.py +4 -0
  225. nautobot/utilities/testing/views.py +17 -1
  226. nautobot/utilities/tests/test_jinja_filters.py +26 -2
  227. nautobot/utilities/utils.py +14 -0
  228. nautobot/virtualization/forms.py +12 -0
  229. nautobot/virtualization/views.py +2 -0
  230. {nautobot-1.6.31.dist-info → nautobot-1.6.32.dist-info}/METADATA +1 -1
  231. {nautobot-1.6.31.dist-info → nautobot-1.6.32.dist-info}/RECORD +235 -233
  232. {nautobot-1.6.31.dist-info → nautobot-1.6.32.dist-info}/LICENSE.txt +0 -0
  233. {nautobot-1.6.31.dist-info → nautobot-1.6.32.dist-info}/NOTICE +0 -0
  234. {nautobot-1.6.31.dist-info → nautobot-1.6.32.dist-info}/WHEEL +0 -0
  235. {nautobot-1.6.31.dist-info → nautobot-1.6.32.dist-info}/entry_points.txt +0 -0
@@ -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
@@ -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
@@ -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
 
@@ -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
@@ -78,3 +78,5 @@ class BaseModel(models.Model):
78
78
  """
79
79
  self.full_clean()
80
80
  self.save(*args, **kwargs)
81
+
82
+ validated_save.alters_data = True
@@ -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").firstElementChild));
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>", serve, {"document_root": settings.MEDIA_ROOT}),
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")),
@@ -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",
@@ -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
 
@@ -764,6 +765,7 @@ class ManufacturerListView(generic.ObjectListView):
764
765
  platform_count=count_related(Platform, "manufacturer"),
765
766
  )
766
767
  filterset = filters.ManufacturerFilterSet
768
+ filterset_form = forms.ManufacturerFilterForm
767
769
  table = tables.ManufacturerTable
768
770
 
769
771
 
@@ -1241,6 +1243,7 @@ class DeviceRoleListView(generic.ObjectListView):
1241
1243
  vm_count=count_related(VirtualMachine, "role"),
1242
1244
  )
1243
1245
  filterset = filters.DeviceRoleFilterSet
1246
+ filterset_form = forms.DeviceRoleFilterForm
1244
1247
  table = tables.DeviceRoleTable
1245
1248
 
1246
1249
 
@@ -1300,6 +1303,7 @@ class PlatformListView(generic.ObjectListView):
1300
1303
  vm_count=count_related(VirtualMachine, "platform"),
1301
1304
  )
1302
1305
  filterset = filters.PlatformFilterSet
1306
+ filterset_form = forms.PlatformFilterForm
1303
1307
  table = tables.PlatformTable
1304
1308
 
1305
1309
 
@@ -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.
@@ -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.
@@ -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"""
@@ -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.exceptions import UndefinedError, TemplateSyntaxError
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