nautobot 3.0.0a2__py3-none-any.whl → 3.0.0a3__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 (420) hide show
  1. nautobot/apps/choices.py +0 -2
  2. nautobot/apps/filters.py +7 -9
  3. nautobot/apps/models.py +2 -2
  4. nautobot/apps/ui.py +9 -1
  5. nautobot/circuits/filters.py +3 -2
  6. nautobot/circuits/navigation.py +3 -2
  7. nautobot/circuits/templates/circuits/circuit.html +1 -1
  8. nautobot/circuits/templates/circuits/circuit_create.html +3 -3
  9. nautobot/circuits/templates/circuits/circuittermination.html +1 -1
  10. nautobot/circuits/templates/circuits/circuittermination_create.html +9 -24
  11. nautobot/circuits/templates/circuits/circuittype.html +1 -1
  12. nautobot/circuits/templates/circuits/inc/circuit_termination_cable_fragment.html +6 -6
  13. nautobot/circuits/templates/circuits/inc/speed_widget.html +12 -12
  14. nautobot/circuits/templates/circuits/providernetwork.html +1 -1
  15. nautobot/circuits/tests/integration/test_circuit.py +10 -13
  16. nautobot/cloud/filters.py +1 -1
  17. nautobot/cloud/navigation.py +3 -2
  18. nautobot/core/api/schema.py +1 -1
  19. nautobot/core/api/serializers.py +6 -1
  20. nautobot/core/api/urls.py +1 -0
  21. nautobot/core/api/views.py +8 -0
  22. nautobot/core/apps/__init__.py +11 -10
  23. nautobot/core/celery/__init__.py +3 -5
  24. nautobot/core/checks.py +46 -0
  25. nautobot/core/cli/bootstrap_v3_to_v5.py +70 -1
  26. nautobot/core/cli/migrate_deprecated_templates.py +200 -0
  27. nautobot/core/constants.py +3 -0
  28. nautobot/core/context_processors.py +9 -1
  29. nautobot/core/forms/forms.py +1 -1
  30. nautobot/core/jobs/__init__.py +6 -3
  31. nautobot/core/jobs/groups.py +31 -1
  32. nautobot/core/management/commands/generate_test_data.py +28 -9
  33. nautobot/core/models/generics.py +9 -1
  34. nautobot/core/models/tree_queries.py +10 -5
  35. nautobot/core/settings.py +18 -12
  36. nautobot/core/settings.yaml +13 -7
  37. nautobot/core/signals.py +12 -1
  38. nautobot/core/tables.py +13 -6
  39. nautobot/core/templates/40x.html +1 -1
  40. nautobot/core/templates/500.html +2 -2
  41. nautobot/core/templates/admin/config/config.html +12 -12
  42. nautobot/core/templates/admin/index.html +3 -3
  43. nautobot/core/templates/buttons/export.html +1 -1
  44. nautobot/core/templates/components/button/dropdown.html +5 -3
  45. nautobot/core/templates/components/panel/body_wrapper_generic_table.html +1 -1
  46. nautobot/core/templates/components/panel/panel.html +3 -3
  47. nautobot/core/templates/components/tab/content_wrapper.html +2 -3
  48. nautobot/core/templates/components/tab/label_wrapper_distinct_view.html +1 -1
  49. nautobot/core/templates/echarts/echarts.html +1 -1
  50. nautobot/core/templates/generic/object_bulk_add_component.html +2 -1
  51. nautobot/core/templates/generic/object_bulk_create.html +4 -3
  52. nautobot/core/templates/generic/object_bulk_destroy.html +3 -3
  53. nautobot/core/templates/generic/object_bulk_remove.html +2 -2
  54. nautobot/core/templates/generic/object_bulk_update.html +5 -4
  55. nautobot/core/templates/generic/object_create.html +5 -4
  56. nautobot/core/templates/generic/object_import.html +2 -1
  57. nautobot/core/templates/generic/object_list.html +12 -4
  58. nautobot/core/templates/generic/object_notes.html +5 -3
  59. nautobot/core/templates/generic/object_retrieve.html +2 -3
  60. nautobot/core/templates/graphene/graphiql.html +7 -7
  61. nautobot/core/templates/home.html +1 -1
  62. nautobot/core/templates/import_success.html +2 -1
  63. nautobot/core/templates/inc/computed_fields/panel_data.html +1 -1
  64. nautobot/core/templates/inc/created_updated.html +7 -3
  65. nautobot/core/templates/inc/custom_fields/panel_data.html +1 -1
  66. nautobot/core/templates/inc/form_static_field.html +6 -0
  67. nautobot/core/templates/inc/header.html +1 -1
  68. nautobot/core/templates/inc/image_attachments.html +2 -1
  69. nautobot/core/templates/inc/nav_menu.html +2 -1
  70. nautobot/core/templates/inc/search_panel.html +4 -4
  71. nautobot/core/templates/login.html +4 -2
  72. nautobot/core/templates/nautobot_config.py.j2 +6 -5
  73. nautobot/core/templates/redoc_ui.html +7 -0
  74. nautobot/core/templates/search.html +1 -1
  75. nautobot/core/templates/swagger_ui.html +17 -3
  76. nautobot/core/templates/system_jobs/import_objects.html +1 -2
  77. nautobot/core/templates/utilities/confirmation_form.html +2 -2
  78. nautobot/core/templates/utilities/obj_table.html +10 -2
  79. nautobot/core/templates/utilities/render_field.html +7 -7
  80. nautobot/core/templates/utilities/render_jinja2.html +2 -2
  81. nautobot/core/templates/utilities/templatetags/filter_form_drawer.html +4 -4
  82. nautobot/core/templates/utilities/theme_preview.html +16 -3
  83. nautobot/core/templates/widgets/selectwithdisabled_option.html +3 -1
  84. nautobot/core/templatetags/helpers.py +52 -6
  85. nautobot/core/testing/api.py +68 -9
  86. nautobot/core/testing/filters.py +0 -23
  87. nautobot/core/testing/integration.py +23 -10
  88. nautobot/core/testing/mixins.py +2 -0
  89. nautobot/core/testing/views.py +4 -0
  90. nautobot/core/tests/integration/test_app_home.py +34 -30
  91. nautobot/core/tests/integration/test_app_navbar.py +3 -0
  92. nautobot/core/tests/nautobot_config_without_example_apps.py +4 -0
  93. nautobot/core/tests/runner.py +9 -1
  94. nautobot/core/tests/test_api.py +5 -3
  95. nautobot/core/tests/test_breadcrumbs.py +6 -7
  96. nautobot/core/tests/test_checks.py +28 -0
  97. nautobot/core/tests/test_cli.py +40 -0
  98. nautobot/core/tests/test_config.py +2 -1
  99. nautobot/core/tests/test_forms.py +55 -13
  100. nautobot/core/tests/test_jobs.py +75 -1
  101. nautobot/core/tests/test_nautobot_server.py +2 -0
  102. nautobot/core/tests/test_navigations.py +76 -1
  103. nautobot/core/tests/test_patch_social_django.py +42 -0
  104. nautobot/core/tests/test_tables.py +3 -1
  105. nautobot/core/tests/test_templatetags_helpers.py +53 -13
  106. nautobot/core/tests/test_templatetags_ui_framework.py +4 -4
  107. nautobot/core/tests/test_tree_queries.py +14 -1
  108. nautobot/core/tests/test_ui.py +1 -1
  109. nautobot/core/tests/test_utils.py +31 -4
  110. nautobot/core/tests/test_views.py +159 -31
  111. nautobot/core/ui/breadcrumbs.py +2 -12
  112. nautobot/core/ui/choices.py +142 -10
  113. nautobot/core/ui/constants.py +76 -12
  114. nautobot/core/ui/object_detail.py +92 -12
  115. nautobot/core/urls.py +12 -1
  116. nautobot/core/utils/cache.py +2 -1
  117. nautobot/core/utils/filtering.py +17 -17
  118. nautobot/core/utils/lookup.py +3 -8
  119. nautobot/core/utils/module_loading.py +21 -0
  120. nautobot/core/utils/patch_social_django.py +128 -0
  121. nautobot/core/views/__init__.py +38 -1
  122. nautobot/core/views/generic.py +3 -3
  123. nautobot/core/views/mixins.py +15 -3
  124. nautobot/core/views/renderers.py +2 -0
  125. nautobot/core/views/viewsets.py +2 -1
  126. nautobot/data_validation/apps.py +1 -5
  127. nautobot/data_validation/custom_validators.py +4 -4
  128. nautobot/data_validation/filters.py +1 -1
  129. nautobot/data_validation/forms.py +40 -0
  130. nautobot/data_validation/migrations/0001_initial.py +0 -7
  131. nautobot/data_validation/migrations/0002_data_migration_from_app.py +0 -12
  132. nautobot/data_validation/models.py +16 -7
  133. nautobot/data_validation/navigation.py +8 -1
  134. nautobot/data_validation/tables.py +12 -5
  135. nautobot/data_validation/templates/data_validation/datacompliance_tab.html +1 -0
  136. nautobot/data_validation/templates/data_validation/device_constraints.html +61 -0
  137. nautobot/data_validation/tests/__init__.py +2 -2
  138. nautobot/data_validation/tests/migrations/test_migrations.py +83 -3
  139. nautobot/data_validation/tests/test_data_compliance_rules.py +12 -7
  140. nautobot/data_validation/tests/test_filters.py +8 -6
  141. nautobot/data_validation/tests/test_models.py +15 -0
  142. nautobot/data_validation/tests/test_views.py +190 -32
  143. nautobot/data_validation/urls.py +2 -5
  144. nautobot/data_validation/views.py +73 -40
  145. nautobot/dcim/api/serializers.py +0 -13
  146. nautobot/dcim/apps.py +4 -0
  147. nautobot/dcim/choices.py +16 -0
  148. nautobot/dcim/custom_validators.py +84 -0
  149. nautobot/dcim/filter_mixins.py +353 -4
  150. nautobot/dcim/{filters/__init__.py → filters.py} +2 -35
  151. nautobot/dcim/forms.py +1 -1
  152. nautobot/dcim/migrations/0078_remove_device_location_tenant_name_uniqueness.py +16 -0
  153. nautobot/dcim/migrations/0079_device_name_data_migration.py +59 -0
  154. nautobot/dcim/models/device_components.py +81 -68
  155. nautobot/dcim/models/devices.py +13 -16
  156. nautobot/dcim/navigation.py +7 -6
  157. nautobot/dcim/tables/devices.py +3 -0
  158. nautobot/dcim/tables/template_code.py +14 -14
  159. nautobot/dcim/templates/dcim/cable.html +2 -61
  160. nautobot/dcim/templates/dcim/cable_connect.html +28 -112
  161. nautobot/dcim/templates/dcim/cable_edit.html +2 -5
  162. nautobot/dcim/templates/dcim/cable_retrieve.html +61 -0
  163. nautobot/dcim/templates/dcim/cable_trace.html +1 -3
  164. nautobot/dcim/templates/dcim/cable_update.html +5 -0
  165. nautobot/dcim/templates/dcim/consoleport.html +6 -5
  166. nautobot/dcim/templates/dcim/consoleserverport.html +6 -5
  167. nautobot/dcim/templates/dcim/device/config.html +2 -2
  168. nautobot/dcim/templates/dcim/device/consoleports.html +1 -1
  169. nautobot/dcim/templates/dcim/device/consoleserverports.html +1 -1
  170. nautobot/dcim/templates/dcim/device/devicebays.html +1 -1
  171. nautobot/dcim/templates/dcim/device/frontports.html +1 -1
  172. nautobot/dcim/templates/dcim/device/interfaces.html +1 -1
  173. nautobot/dcim/templates/dcim/device/inventory.html +1 -1
  174. nautobot/dcim/templates/dcim/device/lldp_neighbors.html +1 -1
  175. nautobot/dcim/templates/dcim/device/modulebays.html +1 -1
  176. nautobot/dcim/templates/dcim/device/poweroutlets.html +1 -1
  177. nautobot/dcim/templates/dcim/device/powerports.html +1 -1
  178. nautobot/dcim/templates/dcim/device/rearports.html +1 -1
  179. nautobot/dcim/templates/dcim/device/status.html +8 -8
  180. nautobot/dcim/templates/dcim/device/wireless.html +1 -1
  181. nautobot/dcim/templates/dcim/device.html +1 -1
  182. nautobot/dcim/templates/dcim/device_component_add.html +2 -2
  183. nautobot/dcim/templates/dcim/device_create.html +5 -3
  184. nautobot/dcim/templates/dcim/device_interface_delete.html +1 -1
  185. nautobot/dcim/templates/dcim/device_list.html +73 -10
  186. nautobot/dcim/templates/dcim/devicebay_populate.html +2 -2
  187. nautobot/dcim/templates/dcim/devicetype.html +1 -1
  188. nautobot/dcim/templates/dcim/devicetype_component_add.html +2 -2
  189. nautobot/dcim/templates/dcim/footer_convert_to_contact_or_team_record.html +14 -0
  190. nautobot/dcim/templates/dcim/frontport.html +9 -8
  191. nautobot/dcim/templates/dcim/inc/edit_form_softwareversion_js.html +2 -2
  192. nautobot/dcim/templates/dcim/interface.html +26 -6
  193. nautobot/dcim/templates/dcim/interface_bulk_delete.html +1 -1
  194. nautobot/dcim/templates/dcim/inventoryitem_add.html +3 -1
  195. nautobot/dcim/templates/dcim/inventoryitem_bulk_delete.html +1 -1
  196. nautobot/dcim/templates/dcim/inventoryitem_edit.html +3 -1
  197. nautobot/dcim/templates/dcim/location_retrieve.html +1 -242
  198. nautobot/dcim/templates/dcim/module/base.html +49 -9
  199. nautobot/dcim/templates/dcim/module_list.html +57 -8
  200. nautobot/dcim/templates/dcim/modulefamily_retrieve.html +1 -1
  201. nautobot/dcim/templates/dcim/moduletype_retrieve.html +49 -9
  202. nautobot/dcim/templates/dcim/platform_create.html +1 -1
  203. nautobot/dcim/templates/dcim/powerfeed.html +1 -1
  204. nautobot/dcim/templates/dcim/powerpanel.html +1 -1
  205. nautobot/dcim/templates/dcim/powerport.html +5 -4
  206. nautobot/dcim/templates/dcim/rack_elevation_list.html +16 -4
  207. nautobot/dcim/templates/dcim/rack_retrieve.html +33 -15
  208. nautobot/dcim/templates/dcim/rearport.html +7 -6
  209. nautobot/dcim/templates/dcim/virtualchassis.html +1 -1
  210. nautobot/dcim/templates/dcim/virtualchassis_add_member.html +16 -14
  211. nautobot/dcim/templates/dcim/virtualchassis_update.html +14 -6
  212. nautobot/dcim/tests/integration/test_controller.py +1 -0
  213. nautobot/dcim/tests/test_api.py +8 -0
  214. nautobot/dcim/tests/test_custom_validators.py +229 -0
  215. nautobot/dcim/tests/test_filters.py +12 -6
  216. nautobot/dcim/tests/test_models.py +63 -4
  217. nautobot/dcim/tests/test_views.py +63 -22
  218. nautobot/dcim/urls.py +64 -21
  219. nautobot/dcim/utils.py +3 -3
  220. nautobot/dcim/views.py +547 -273
  221. nautobot/extras/api/views.py +9 -1
  222. nautobot/extras/choices.py +2 -13
  223. nautobot/extras/{filters/mixins.py → filter_mixins.py} +1 -1
  224. nautobot/extras/{filters/customfields.py → filter_mixins_customfields.py} +42 -6
  225. nautobot/extras/{filters/__init__.py → filters.py} +14 -46
  226. nautobot/extras/forms/forms.py +5 -13
  227. nautobot/extras/forms/mixins.py +0 -41
  228. nautobot/extras/management/__init__.py +9 -0
  229. nautobot/extras/migrations/0127_approval_workflow_models.py +6 -6
  230. nautobot/extras/migrations/0129_jobresult_debug_log_count_jobresult_error_log_count_and_more.py +37 -0
  231. nautobot/extras/migrations/0130_jobresult_generate_log_entry_counts.py +42 -0
  232. nautobot/extras/models/__init__.py +1 -2
  233. nautobot/extras/models/approvals.py +22 -13
  234. nautobot/extras/models/contacts.py +2 -0
  235. nautobot/extras/models/groups.py +44 -5
  236. nautobot/extras/models/jobs.py +59 -1
  237. nautobot/extras/models/mixins.py +28 -0
  238. nautobot/extras/models/models.py +13 -0
  239. nautobot/extras/models/secrets.py +1 -0
  240. nautobot/extras/models/statuses.py +0 -15
  241. nautobot/extras/navigation.py +13 -9
  242. nautobot/extras/plugins/__init__.py +33 -55
  243. nautobot/extras/plugins/tables.py +3 -3
  244. nautobot/extras/plugins/urls.py +2 -21
  245. nautobot/extras/plugins/utils.py +1 -33
  246. nautobot/extras/plugins/views.py +0 -4
  247. nautobot/extras/signals.py +20 -19
  248. nautobot/extras/tables.py +52 -68
  249. nautobot/extras/templates/extras/approval_dashboard.html +7 -5
  250. nautobot/extras/templates/extras/approvalworkflowdefinition_update.html +4 -2
  251. nautobot/extras/templates/extras/approvalworkflowstage_retrieve.html +20 -12
  252. nautobot/extras/templates/extras/computedfield.html +1 -1
  253. nautobot/extras/templates/extras/configcontext.html +1 -1
  254. nautobot/extras/templates/extras/configcontextschema_validation.html +2 -2
  255. nautobot/extras/templates/extras/customfield.html +1 -1
  256. nautobot/extras/templates/extras/dynamicgroup_retrieve.html +11 -5
  257. nautobot/extras/templates/extras/dynamicgroup_update.html +1 -1
  258. nautobot/extras/templates/extras/gitrepository_result.html +0 -2
  259. nautobot/extras/templates/extras/graphqlquery_retrieve.html +1 -96
  260. nautobot/extras/templates/extras/inc/approval_buttons_column.html +20 -6
  261. nautobot/extras/templates/extras/inc/bulk_edit_overridable_field.html +8 -7
  262. nautobot/extras/templates/extras/inc/configcontext_format.html +10 -3
  263. nautobot/extras/templates/extras/inc/graphqlquery_execute.html +71 -0
  264. nautobot/extras/templates/extras/inc/job_tiles.html +15 -3
  265. nautobot/extras/templates/extras/inc/json_format.html +10 -3
  266. nautobot/extras/templates/extras/inc/overridable_field.html +13 -12
  267. nautobot/extras/templates/extras/job.html +29 -12
  268. nautobot/extras/templates/extras/job_bulk_edit.html +18 -0
  269. nautobot/extras/templates/extras/job_edit.html +52 -46
  270. nautobot/extras/templates/extras/job_list.html +29 -25
  271. nautobot/extras/templates/extras/marketplace.html +5 -9
  272. nautobot/extras/templates/extras/object_configcontext.html +1 -1
  273. nautobot/extras/templates/extras/object_dynamicgroups.html +2 -2
  274. nautobot/extras/templates/extras/objectchange_retrieve.html +19 -37
  275. nautobot/extras/templates/extras/plugin_detail.html +26 -21
  276. nautobot/extras/templates/extras/plugins_list.html +16 -26
  277. nautobot/extras/templates/extras/role_retrieve.html +64 -0
  278. nautobot/extras/templates/extras/scheduledjob.html +4 -2
  279. nautobot/extras/templates/extras/secretsgroup.html +1 -1
  280. nautobot/extras/templates/extras/tag.html +1 -1
  281. nautobot/extras/templatetags/custom_links.py +12 -12
  282. nautobot/extras/templatetags/job_buttons.py +14 -12
  283. nautobot/extras/test_jobs/invalid_import.py +9 -0
  284. nautobot/extras/test_jobs/log_counts_by_level.py +23 -0
  285. nautobot/extras/test_jobs/missing_import.py +11 -0
  286. nautobot/extras/tests/integration/test_configcontextschema.py +27 -26
  287. nautobot/extras/tests/integration/test_customfields.py +8 -7
  288. nautobot/extras/tests/integration/test_dynamicgroups.py +5 -1
  289. nautobot/extras/tests/integration/test_plugin_banner.py +3 -0
  290. nautobot/extras/tests/integration/test_plugins.py +18 -6
  291. nautobot/extras/tests/test_api.py +27 -18
  292. nautobot/extras/tests/test_approvals.py +38 -38
  293. nautobot/extras/tests/test_changelog.py +35 -3
  294. nautobot/extras/tests/test_customfields.py +22 -13
  295. nautobot/extras/tests/test_customfields_filters.py +479 -0
  296. nautobot/extras/tests/test_dynamicgroups.py +39 -1
  297. nautobot/extras/tests/test_filters.py +21 -19
  298. nautobot/extras/tests/test_forms.py +18 -21
  299. nautobot/extras/tests/test_jobs.py +25 -4
  300. nautobot/extras/tests/test_migrations.py +1 -0
  301. nautobot/extras/tests/test_models.py +13 -31
  302. nautobot/extras/tests/test_plugins.py +36 -10
  303. nautobot/extras/tests/test_views.py +31 -30
  304. nautobot/extras/views.py +81 -19
  305. nautobot/ipam/factory.py +7 -0
  306. nautobot/ipam/filter_mixins.py +38 -0
  307. nautobot/ipam/filters.py +27 -38
  308. nautobot/ipam/formfields.py +1 -1
  309. nautobot/ipam/forms.py +6 -3
  310. nautobot/ipam/migrations/0030_ipam__namespaces.py +13 -0
  311. nautobot/ipam/migrations/0031_ipam___data_migrations.py +4 -1
  312. nautobot/ipam/migrations/0054_namespace_tenant.py +25 -0
  313. nautobot/ipam/models.py +29 -2
  314. nautobot/ipam/navigation.py +3 -2
  315. nautobot/ipam/signals.py +71 -0
  316. nautobot/ipam/tables.py +13 -6
  317. nautobot/ipam/templates/ipam/inc/toggle_available.html +10 -10
  318. nautobot/ipam/templates/ipam/inc/vlangroup_header.html +1 -0
  319. nautobot/ipam/templates/ipam/ipaddress.html +14 -0
  320. nautobot/ipam/templates/ipam/ipaddress_merge.html +3 -3
  321. nautobot/ipam/templates/ipam/ipaddresstointerface_retrieve.html +1 -0
  322. nautobot/ipam/templates/ipam/namespace_update.html +15 -0
  323. nautobot/ipam/templates/ipam/prefix_delete.html +1 -1
  324. nautobot/ipam/templates/ipam/prefix_list.html +14 -13
  325. nautobot/ipam/templates/ipam/service.html +1 -1
  326. nautobot/ipam/templates/ipam/vlan.html +1 -1
  327. nautobot/ipam/templates/ipam/vlan_interfaces.html +1 -1
  328. nautobot/ipam/templates/ipam/vlan_vminterfaces.html +1 -1
  329. nautobot/ipam/tests/migration/test_migrations.py +89 -0
  330. nautobot/ipam/tests/test_api.py +13 -6
  331. nautobot/ipam/tests/test_filters.py +10 -0
  332. nautobot/ipam/tests/test_forms.py +1 -1
  333. nautobot/ipam/tests/test_models.py +43 -1
  334. nautobot/ipam/tests/test_tables.py +1 -2
  335. nautobot/ipam/tests/test_utils.py +1 -1
  336. nautobot/ipam/tests/test_views.py +13 -14
  337. nautobot/ipam/ui.py +0 -17
  338. nautobot/ipam/utils/migrations.py +16 -2
  339. nautobot/ipam/utils/testing.py +9 -3
  340. nautobot/ipam/views.py +46 -6
  341. nautobot/project-static/dist/css/nautobot.css +1 -1
  342. nautobot/project-static/dist/css/nautobot.css.map +1 -1
  343. nautobot/project-static/dist/js/nautobot.js +1 -1
  344. nautobot/project-static/dist/js/nautobot.js.map +1 -1
  345. nautobot/project-static/js/cabletrace.js +1 -1
  346. nautobot/project-static/js/interface_filtering.js +20 -16
  347. nautobot/project-static/nautobot-icons/battery-3.svg +3 -0
  348. nautobot/project-static/nautobot-icons/cloud.svg +1 -1
  349. nautobot/project-static/nautobot-icons/control-panel.svg +1 -1
  350. nautobot/project-static/nautobot-icons/device-lifecycle.svg +1 -1
  351. nautobot/project-static/nautobot-icons/elements.svg +1 -1
  352. nautobot/project-static/nautobot-icons/extensibility.svg +3 -0
  353. nautobot/project-static/nautobot-icons/hammer.svg +1 -1
  354. nautobot/project-static/nautobot-icons/organization.svg +3 -0
  355. nautobot/project-static/nautobot-icons/secrets.svg +1 -1
  356. nautobot/project-static/nautobot-icons/security.svg +3 -0
  357. nautobot/project-static/nautobot-icons/server.svg +1 -1
  358. nautobot/project-static/nautobot-icons/star-filled.svg +1 -1
  359. nautobot/project-static/nautobot-icons/star.svg +1 -1
  360. nautobot/tenancy/api/serializers.py +1 -0
  361. nautobot/tenancy/api/views.py +2 -1
  362. nautobot/tenancy/{filters/__init__.py → filters.py} +2 -10
  363. nautobot/tenancy/navigation.py +3 -1
  364. nautobot/tenancy/tests/test_filters.py +0 -2
  365. nautobot/tenancy/views.py +2 -1
  366. nautobot/ui/src/js/collapse.js +3 -3
  367. nautobot/ui/src/js/nautobot.js +16 -0
  368. nautobot/ui/src/scss/colors.scss +1 -1
  369. nautobot/ui/src/scss/nautobot.scss +61 -28
  370. nautobot/users/templates/users/profile.html +45 -12
  371. nautobot/users/templates/users/sessionkey_delete.html +1 -1
  372. nautobot/users/tests/test_api.py +4 -0
  373. nautobot/users/views.py +4 -2
  374. nautobot/virtualization/models.py +1 -68
  375. nautobot/virtualization/navigation.py +3 -2
  376. nautobot/virtualization/templates/virtualization/virtual_machine_vminterface_delete.html +1 -1
  377. nautobot/virtualization/templates/virtualization/virtualmachine.html +1 -1
  378. nautobot/virtualization/templates/virtualization/virtualmachine_list.html +2 -2
  379. nautobot/virtualization/templates/virtualization/virtualmachine_update.html +3 -1
  380. nautobot/virtualization/tests/test_api.py +3 -0
  381. nautobot/virtualization/tests/test_models.py +44 -4
  382. nautobot/vpn/__init__.py +0 -0
  383. nautobot/vpn/api/serializers.py +113 -0
  384. nautobot/vpn/api/urls.py +19 -0
  385. nautobot/vpn/api/views.py +70 -0
  386. nautobot/vpn/apps.py +8 -0
  387. nautobot/vpn/choices.py +171 -0
  388. nautobot/vpn/factory.py +209 -0
  389. nautobot/vpn/filters.py +233 -0
  390. nautobot/vpn/forms.py +486 -0
  391. nautobot/vpn/homepage.py +19 -0
  392. nautobot/vpn/migrations/0001_initial.py +541 -0
  393. nautobot/vpn/migrations/0002_populate_defaults.py +199 -0
  394. nautobot/vpn/migrations/__init__.py +0 -0
  395. nautobot/vpn/models.py +527 -0
  396. nautobot/vpn/navigation.py +98 -0
  397. nautobot/vpn/tables.py +380 -0
  398. nautobot/vpn/templates/vpn/vpnprofile.html +2 -0
  399. nautobot/vpn/templates/vpn/vpnprofile_create.html +150 -0
  400. nautobot/vpn/tests/__init__.py +0 -0
  401. nautobot/vpn/tests/test_api.py +341 -0
  402. nautobot/vpn/tests/test_filters.py +139 -0
  403. nautobot/vpn/tests/test_forms.py +294 -0
  404. nautobot/vpn/tests/test_models.py +97 -0
  405. nautobot/vpn/tests/test_views.py +281 -0
  406. nautobot/vpn/urls.py +16 -0
  407. nautobot/vpn/views.py +437 -0
  408. nautobot/wireless/navigation.py +3 -2
  409. nautobot/wireless/tests/integration/test_radio_profile.py +1 -5
  410. nautobot/wireless/tests/test_api.py +1 -1
  411. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/METADATA +14 -14
  412. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/RECORD +417 -366
  413. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/entry_points.txt +1 -0
  414. nautobot/data_validation/template_content.py +0 -42
  415. nautobot/dcim/filters/mixins.py +0 -354
  416. nautobot/ipam/templates/ipam/inc/prefix_header_extra_content_table.html +0 -4
  417. /nautobot/tenancy/{filters/mixins.py → filter_mixins.py} +0 -0
  418. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/LICENSE.txt +0 -0
  419. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/NOTICE +0 -0
  420. {nautobot-3.0.0a2.dist-info → nautobot-3.0.0a3.dist-info}/WHEEL +0 -0
@@ -47,6 +47,7 @@ from nautobot.core.utils.lookup import get_filterset_for_model, get_route_for_mo
47
47
  from nautobot.core.utils.permissions import get_permission_for_model
48
48
  from nautobot.core.views.paginator import EnhancedPaginator, get_paginate_count
49
49
  from nautobot.core.views.utils import get_obj_from_context
50
+ from nautobot.data_validation.tables import DataComplianceTable
50
51
  from nautobot.dcim.models import Rack
51
52
  from nautobot.extras.choices import CustomFieldTypeChoices
52
53
  from nautobot.extras.tables import AssociatedContactsTable, DynamicGroupTable, ObjectMetadataTable
@@ -101,6 +102,7 @@ class ObjectDetailContent:
101
102
  _ObjectDetailContactsTab(),
102
103
  _ObjectDetailGroupsTab(),
103
104
  _ObjectDetailMetadataTab(),
105
+ _ObjectDetailDataComplianceTab(),
104
106
  ]
105
107
  if extra_tabs is not None:
106
108
  tabs.extend(extra_tabs)
@@ -262,8 +264,11 @@ class Button(Component):
262
264
  }
263
265
 
264
266
  def should_render(self, context: Context):
267
+ # Only show if the user has the permission, which is enforce in super.
265
268
  if not super().should_render(context):
266
269
  return False
270
+ if self.render_on_tab_id == "__all__":
271
+ return True
267
272
  return context.get("active_tab", "main") == self.render_on_tab_id
268
273
 
269
274
  def render(self, context: Context):
@@ -377,8 +382,9 @@ class Tab(Component):
377
382
  WEIGHT_CONTACTS_TAB = 300
378
383
  WEIGHT_GROUPS_TAB = 400
379
384
  WEIGHT_METADATA_TAB = 500
380
- WEIGHT_NOTES_TAB = 600 # reserved, not yet using this framework
381
- WEIGHT_CHANGELOG_TAB = 700 # reserved, not yet using this framework
385
+ WEIGHT_DATACOMPLIANCE_TAB = 600
386
+ WEIGHT_NOTES_TAB = 700 # reserved, not yet using this framework
387
+ WEIGHT_CHANGELOG_TAB = 800 # reserved, not yet using this framework
382
388
 
383
389
  def panels_for_section(self, section):
384
390
  """
@@ -540,6 +546,7 @@ class Panel(Component):
540
546
  self,
541
547
  *,
542
548
  label="",
549
+ css_class="default",
543
550
  section=SectionChoices.FULL_WIDTH,
544
551
  body_id=None,
545
552
  body_content_template_path=None,
@@ -554,6 +561,7 @@ class Panel(Component):
554
561
 
555
562
  Args:
556
563
  label (str): Label to display for this panel. Optional; if an empty string, the panel will have no label.
564
+ css_class (str): Panel variant to render as, e.g. "default", "warning", "info".
557
565
  section (str): One of the [`SectionChoices`](./ui.md#nautobot.apps.ui.SectionChoices) values, indicating the layout section this Panel belongs to.
558
566
  body_id (str): HTML element `id` to attach to the rendered body wrapper of the panel.
559
567
  body_content_template_path (str): Template path to render the content contained *within* the panel body.
@@ -565,6 +573,7 @@ class Panel(Component):
565
573
  (a `div` or `table`) as well as its contents. Generally you won't override this as a user.
566
574
  """
567
575
  self.label = label
576
+ self.css_class = css_class
568
577
  self.section = section
569
578
  self.body_id = body_id
570
579
  self.body_content_template_path = body_content_template_path
@@ -593,6 +602,7 @@ class Panel(Component):
593
602
  self.template_path,
594
603
  context,
595
604
  label=self.render_label(context),
605
+ css_class=self.css_class,
596
606
  header_extra_content=self.render_header_extra_content(context),
597
607
  body=self.render_body(context),
598
608
  footer_content=self.render_footer_content(context),
@@ -1299,8 +1309,11 @@ class EChartsPanel(Panel, EChartsBase):
1299
1309
  self.width = width
1300
1310
  self.height = height
1301
1311
  self.chart_container_id = chart_container_id
1312
+ self.body_id = (
1313
+ self.chart_container_id or f"{slugify('echart-' + chart_kwargs.get('header', ''))}-{uuid.uuid4().hex[:8]}"
1314
+ )
1302
1315
 
1303
- super().__init__(body_wrapper_template_path=body_wrapper_template_path, **kwargs)
1316
+ super().__init__(body_wrapper_template_path=body_wrapper_template_path, body_id=self.body_id, **kwargs)
1304
1317
  EChartsBase.__init__(self, **chart_kwargs)
1305
1318
 
1306
1319
  def get_data(self, context: Context) -> dict[str, Any] | None:
@@ -1344,8 +1357,7 @@ class EChartsPanel(Panel, EChartsBase):
1344
1357
  "chart_config": chart_config,
1345
1358
  "chart_width": self.width,
1346
1359
  "chart_height": self.height,
1347
- "chart_container_id": self.chart_container_id
1348
- or f"{slugify(f'echart-{self.header}')}-{uuid.uuid4().hex[:8]}",
1360
+ "chart_container_id": self.body_id,
1349
1361
  }
1350
1362
 
1351
1363
 
@@ -1542,7 +1554,7 @@ class GroupedKeyValueTablePanel(KeyValueTablePanel):
1542
1554
  super().__init__(body_id=body_id, **kwargs)
1543
1555
 
1544
1556
  def render_header_extra_content(self, context: Context):
1545
- """Add a "Collapse All" button to the header."""
1557
+ """Add a "Collapse All Groups" button to the header."""
1546
1558
  return format_html(
1547
1559
  """
1548
1560
  <button
@@ -1552,7 +1564,7 @@ class GroupedKeyValueTablePanel(KeyValueTablePanel):
1552
1564
  data-nb-toggle="collapse-all"
1553
1565
  type="button"
1554
1566
  >
1555
- Collapse All
1567
+ Collapse All Groups
1556
1568
  </button>
1557
1569
  """,
1558
1570
  body_id=self.body_id,
@@ -1727,7 +1739,7 @@ class StatsPanel(Panel):
1727
1739
  instance = get_obj_from_context(context)
1728
1740
  request = context["request"]
1729
1741
  if isinstance(instance, TreeModel):
1730
- self.filter_pks = (
1742
+ self.filter_pks = list(
1731
1743
  instance.descendants(include_self=True).restrict(request.user, "view").values_list("pk", flat=True)
1732
1744
  )
1733
1745
  else:
@@ -1743,9 +1755,10 @@ class StatsPanel(Panel):
1743
1755
  else:
1744
1756
  related_object_model_class, query = related_field, f"{self.filter_name}__in"
1745
1757
  filter_dict = {query: self.filter_pks}
1746
- related_object_count = (
1747
- related_object_model_class.objects.restrict(request.user, "view").filter(**filter_dict).count()
1748
- )
1758
+ qs = related_object_model_class.objects.restrict(request.user, "view").filter(**filter_dict)
1759
+ if len(self.filter_pks) > 1:
1760
+ qs = qs.distinct()
1761
+ related_object_count = qs.count()
1749
1762
  related_object_model_class_meta = related_object_model_class._meta
1750
1763
  related_object_list_url = validated_viewname(related_object_model_class, "list")
1751
1764
  related_object_title = bettertitle(related_object_model_class_meta.verbose_name_plural)
@@ -2153,6 +2166,72 @@ class _ObjectDetailContactsTab(Tab):
2153
2166
  )
2154
2167
 
2155
2168
 
2169
+ class _ObjectDetailDataComplianceTab(DistinctViewTab):
2170
+ """Built-in class for a Tab displaying information about data compliance."""
2171
+
2172
+ def __init__(
2173
+ self,
2174
+ *,
2175
+ tab_id="data_compliance",
2176
+ label="Data Compliance",
2177
+ weight=Tab.WEIGHT_DATACOMPLIANCE_TAB,
2178
+ panels=None,
2179
+ **kwargs,
2180
+ ):
2181
+ if panels is None:
2182
+ panels = (
2183
+ ObjectsTablePanel(
2184
+ weight=100,
2185
+ table_class=DataComplianceTable,
2186
+ table_attribute="associated_data_compliance",
2187
+ related_field_name="object_id",
2188
+ label="Data Compliance",
2189
+ add_button_route=None,
2190
+ header_extra_content_template_path=None,
2191
+ include_paginator=True,
2192
+ ),
2193
+ )
2194
+ super().__init__(url_name="", tab_id=tab_id, label=label, weight=weight, panels=panels, **kwargs)
2195
+
2196
+ def get_extra_context(self, context: Context):
2197
+ return {"url": get_obj_from_context(context).get_data_compliance_url()}
2198
+
2199
+ def should_render(self, context: Context):
2200
+ if not super().should_render(context):
2201
+ return False
2202
+ return getattr(get_obj_from_context(context), "is_data_compliance_model", False)
2203
+
2204
+
2205
+ class DynamicGroupsTextPanel(BaseTextPanel):
2206
+ """Panel displaying a note about caching of dynamic groups."""
2207
+
2208
+ def __init__(
2209
+ self,
2210
+ *,
2211
+ weight,
2212
+ render_as=BaseTextPanel.RenderOptions.MARKDOWN,
2213
+ label="Dynamic Group caching",
2214
+ css_class="warning",
2215
+ **kwargs,
2216
+ ):
2217
+ super().__init__(weight=weight, render_as=render_as, label=label, css_class=css_class, **kwargs)
2218
+
2219
+ def get_value(self, context):
2220
+ dg_list_url = reverse("extras:dynamicgroup_list")
2221
+ job_run_url = reverse(
2222
+ "extras:job_run_by_class_path",
2223
+ kwargs={"class_path": "nautobot.core.jobs.groups.RefreshDynamicGroupCaches"},
2224
+ )
2225
+ return (
2226
+ "Dynamic group membership is cached for performance reasons, "
2227
+ "therefore this page may not always be up-to-date.\n\n"
2228
+ "You can refresh the membership of any specific group by accessing it from the list below or from the "
2229
+ f'[Dynamic Groups list view]({dg_list_url}) and clicking the "Refresh Members" button.\n\n'
2230
+ "You can also refresh the membership of **all** groups by running the "
2231
+ f"[Refresh Dynamic Group Caches job]({job_run_url})."
2232
+ )
2233
+
2234
+
2156
2235
  @dataclass
2157
2236
  class _ObjectDetailGroupsTab(Tab):
2158
2237
  """Built-in class for a Tab displaying information about associated dynamic groups."""
@@ -2169,8 +2248,9 @@ class _ObjectDetailGroupsTab(Tab):
2169
2248
  ):
2170
2249
  if panels is None:
2171
2250
  panels = (
2251
+ DynamicGroupsTextPanel(weight=100),
2172
2252
  ObjectsTablePanel(
2173
- weight=100,
2253
+ weight=200,
2174
2254
  table_class=DynamicGroupTable,
2175
2255
  table_attribute="dynamic_groups",
2176
2256
  exclude_columns=["content_type"],
nautobot/core/urls.py CHANGED
@@ -1,10 +1,11 @@
1
1
  from django.conf import settings
2
2
  from django.http import HttpResponse, HttpResponseNotFound
3
3
  from django.urls import include, path
4
- from django.views.generic import TemplateView
4
+ from django.views.generic import RedirectView, TemplateView
5
5
 
6
6
  from nautobot.core.views import (
7
7
  AboutView,
8
+ AppDocsView,
8
9
  CustomGraphQLView,
9
10
  get_file_with_authorization,
10
11
  HomeView,
@@ -46,6 +47,7 @@ urlpatterns = [
46
47
  path("user/", include("nautobot.users.urls")),
47
48
  path("users/", include("nautobot.users.urls", "users")),
48
49
  path("virtualization/", include("nautobot.virtualization.urls")),
50
+ path("vpn/", include("nautobot.vpn.urls")),
49
51
  path("wireless/", include("nautobot.wireless.urls")),
50
52
  # API
51
53
  path("api/", include("nautobot.core.api.urls")),
@@ -59,6 +61,15 @@ urlpatterns = [
59
61
  path("media-failure/", StaticMediaFailureView.as_view(), name="media_failure"),
60
62
  # Apps
61
63
  path("apps/", include((apps_patterns, "apps"))),
64
+ # Redirect /docs/<app_base_url>/ -> /docs/<app_base_url>/index.html
65
+ path(
66
+ "docs/<str:app_base_url>/",
67
+ RedirectView.as_view(pattern_name="docs_file", permanent=False),
68
+ kwargs={"path": "index.html"},
69
+ name="docs_index_redirect",
70
+ ),
71
+ # Apps docs - Serve docs file
72
+ path("docs/<str:app_base_url>/<path:path>", AppDocsView.as_view(), name="docs_file"),
62
73
  path("plugins/", include((plugin_patterns, "plugins"))),
63
74
  path("admin/plugins/", include(plugin_admin_patterns)),
64
75
  # Social auth/SSO
@@ -66,5 +66,6 @@ def construct_cache_key(obj, *, method_name=None, branch_aware=True, **params):
66
66
  if params_tokens:
67
67
  cache_key += f"({','.join(params_tokens)})"
68
68
 
69
- logger.debug("Constructed cache key is %s", cache_key)
69
+ # Disabled as it's very noisy in some cases
70
+ # logger.debug("Constructed cache key is %s", cache_key)
70
71
  return cache_key
@@ -17,6 +17,21 @@ from nautobot.core.utils.lookup import get_filterset_for_model
17
17
  # e.g `name__ic` has lookup expr `ic (icontains)` while `name` has no lookup expr
18
18
  CONTAINS_LOOKUP_EXPR_RE = re.compile(r"(?<=__)\w+")
19
19
 
20
+ MODEL_VERBOSE_NAME_PLURAL_TO_FEATURE_NAME_MAPPING = {
21
+ "approval_workflow_definitions": "approval_workflows",
22
+ "cables": "cable_terminations",
23
+ "data_compliance": "custom_validators",
24
+ "location_types": "locations",
25
+ "metadata_types": "metadata",
26
+ "min_max_validation_rules": "custom_validators",
27
+ "object_metadata": "metadata",
28
+ "regular_expression_validation_rules": "custom_validators",
29
+ "relationship_associations": "relationships",
30
+ "required_validation_rules": "custom_validators",
31
+ "static_group_associations": "dynamic_groups",
32
+ "unique_validation_rules": "custom_validators",
33
+ }
34
+
20
35
 
21
36
  def build_lookup_label(field_name, _verbose_name):
22
37
  """
@@ -131,26 +146,11 @@ def get_filterset_parameter_form_field(model, parameter, filterset=None):
131
146
  elif isinstance(
132
147
  field, ContentTypeMultipleChoiceFilter
133
148
  ): # While there are other objects using `ContentTypeMultipleChoiceFilter`, the case where
134
- # models that have such a filter and the `verbose_name_plural` has multiple words is ony one: "dynamic groups".
149
+ # models that have such a filter and the `verbose_name_plural` does not match, we can lookup the feature name.
135
150
  from nautobot.core.models.fields import slugify_dashes_to_underscores # Avoid circular import
136
151
 
137
152
  plural_name = slugify_dashes_to_underscores(model._meta.verbose_name_plural)
138
-
139
- # Cable-connectable models use "cable_terminations", not "cables", as the feature name
140
- if plural_name == "cables":
141
- plural_name = "cable_terminations"
142
- elif plural_name == "metadata_types":
143
- plural_name = "metadata"
144
- elif plural_name == "object_metadata":
145
- plural_name = "metadata"
146
- elif plural_name in [
147
- "data_compliance",
148
- "min_max_validation_rules",
149
- "regular_expression_validation_rules",
150
- "required_validation_rules",
151
- "unique_validation_rules",
152
- ]:
153
- plural_name = "custom_validators"
153
+ plural_name = MODEL_VERBOSE_NAME_PLURAL_TO_FEATURE_NAME_MAPPING.get(plural_name, plural_name)
154
154
  try:
155
155
  form_field = MultipleContentTypeField(choices_as_strings=True, feature=plural_name)
156
156
  except KeyError:
@@ -11,9 +11,10 @@ from django.contrib.contenttypes.models import ContentType
11
11
  from django.core.exceptions import ObjectDoesNotExist
12
12
  from django.db.models import ForeignKey, Model
13
13
  from django.urls import get_resolver, resolve, reverse, URLPattern, URLResolver
14
- from django.utils.module_loading import import_string
15
14
  from django.views.generic.base import RedirectView
16
15
 
16
+ from nautobot.core.utils.module_loading import import_string_optional
17
+
17
18
 
18
19
  def resolve_attr(obj, dotted_field):
19
20
  """
@@ -181,13 +182,7 @@ def get_related_class_for_model(model, module_name, object_suffix):
181
182
  object_name = f"{model.__name__}{object_suffix}"
182
183
  object_path = f"{app_config.name}.{module_name}.{object_name}"
183
184
 
184
- try:
185
- return import_string(object_path)
186
- # The name of the module is not correct or unable to find the desired object for this model
187
- except (AttributeError, ImportError, ModuleNotFoundError):
188
- pass
189
-
190
- return None
185
+ return import_string_optional(object_path)
191
186
 
192
187
 
193
188
  def get_filterset_for_model(model):
@@ -7,9 +7,30 @@ import os
7
7
  import pkgutil
8
8
  import sys
9
9
 
10
+ from django.utils.module_loading import import_string
11
+
10
12
  logger = logging.getLogger(__name__)
11
13
 
12
14
 
15
+ def import_string_optional(dotted_path):
16
+ """An extension/wrapper of Django's `import_string()` that returns `None` if no such dotted path exists."""
17
+ try:
18
+ return import_string(dotted_path)
19
+ except ModuleNotFoundError as err:
20
+ # No such module
21
+ module_name, _ = dotted_path.rsplit(".", 1)
22
+ if module_name.startswith(err.name): # tried to import foo.bar.baz but couldn't find foo.bar, etc.
23
+ return None
24
+ # Some import *from within* the given module couldn't find what it was looking for?
25
+ raise
26
+ except ImportError as err:
27
+ if "does not define" in str(err):
28
+ # Exception raised by Django if the module exists but has no such attribute
29
+ return None
30
+ # Maybe a legitimate problem with the import?
31
+ raise
32
+
33
+
13
34
  @contextmanager
14
35
  def _temporarily_add_to_sys_path(path):
15
36
  """
@@ -0,0 +1,128 @@
1
+ from django.core.exceptions import FieldDoesNotExist
2
+ from django.db import router, transaction
3
+ from django.db.utils import IntegrityError
4
+ from social_core.exceptions import AuthAlreadyAssociated
5
+
6
+ """
7
+ Social Auth Account Takeover Vulnerability Patch
8
+ =================================================
9
+
10
+ This module patches CVE-2025-61783, a medium security vulnerability in social_django that allows
11
+ account takeover when using OAuth providers that don't verify email addresses.
12
+
13
+ VULNERABILITY OVERVIEW
14
+ ----------------------
15
+ The vulnerability exists in social_django.storage.DjangoUserMixin.create_user(),
16
+ specifically in how it handles IntegrityError exceptions. When user creation fails due to
17
+ a duplicate email or username, the original code catches the IntegrityError and blindly
18
+ retrieves an existing user via manager.get(), returning that user without verifying that
19
+ a social auth association exists for the provider/UID combination.
20
+
21
+ PATCHING STRATEGY
22
+ -----------------
23
+ This implementation patches the `create_user` method on the `user` class property of
24
+ DjangoStorage, which is where the vulnerability manifests. The patch changes the behavior
25
+ to raise AuthAlreadyAssociated when an IntegrityError occurs, preventing the silent
26
+ return of an existing user.
27
+
28
+ By patching at this level, we:
29
+ - Maintain compatibility with custom pipelines
30
+ - Don't require changes to user's social auth configuration
31
+ - Apply the fix exactly where the vulnerability occurs
32
+ - Preserve all other social auth functionality
33
+
34
+ REMOVAL
35
+ -------
36
+ Remove this patch when upgrading to social-auth-app-django >= 5.6.0
37
+ (version that includes PR #803 merged into the main branch).
38
+
39
+ To verify if you still need the patch:
40
+ pip show social-auth-app-django
41
+ # Check version against PR #803 merge status
42
+
43
+ REFERENCES
44
+ ----------
45
+ - Vulnerability Report: https://github.com/python-social-auth/social-app-django/security/advisories/GHSA-wv4w-6qv2-qqfg
46
+ - Original Issue: https://github.com/python-social-auth/social-app-django/issues/220
47
+ - Official Fix PR: https://github.com/python-social-auth/social-app-django/pull/803
48
+
49
+ SECURITY NOTICE
50
+ ---------------
51
+ This patch addresses a MEDIUM security vulnerability
52
+
53
+ Disabling this patch without mitigation will expose your application to account
54
+ takeover attacks.
55
+
56
+ AUTHOR & MAINTENANCE
57
+ --------------------
58
+ Patch implemented as temporary security measure for Nautobot deployment.
59
+ As picking up the latest social_django would require a major version upgrade of Django,
60
+ which itself would require a breaking change to the Nautobot configuration, this patch
61
+ is intended to be a stopgap until such time as Nautobot can upgrade to a version of
62
+ social_django that includes the fix.
63
+ """
64
+
65
+
66
+ def patch_django_storage(original_django_storage):
67
+ """
68
+ Apply security patch to DjangoStorage.user.create_user method.
69
+
70
+ This patches the vulnerability in python-social-auth where create_user
71
+ catches IntegrityError and blindly returns an existing user, enabling
72
+ account takeover via unverified OAuth providers.
73
+
74
+ Args:
75
+ storage_class (DjangoStorage): The original DjangoStorage class to patch.
76
+
77
+ Returns:
78
+ None
79
+
80
+ Note:
81
+ The patch is a nearly verbatim copy of the original create_user method
82
+ from social_django.storage.DjangoUserMixin from 5.4.3, except that it
83
+ adopts the fail-closed change described in
84
+ https://github.com/python-social-auth/social-app-django/pull/803
85
+
86
+ The modified lines are called out with "Patched logic" comments below.
87
+ """
88
+
89
+ def patched_create_user(cls, *args, **kwargs):
90
+ username_field = cls.username_field()
91
+ if "username" in kwargs:
92
+ if username_field not in kwargs:
93
+ kwargs[username_field] = kwargs.pop("username")
94
+ else:
95
+ # If username_field is 'email' and there is no field named "username"
96
+ # then latest should be removed from kwargs.
97
+ try:
98
+ cls.user_model()._meta.get_field("username")
99
+ except FieldDoesNotExist:
100
+ kwargs.pop("username")
101
+ try:
102
+ if hasattr(transaction, "atomic"):
103
+ # In Django versions that have an "atomic" transaction decorator / context
104
+ # manager, there's a transaction wrapped around this call.
105
+ # If the create fails below due to an IntegrityError, ensure that the transaction
106
+ # stays undamaged by wrapping the create in an atomic.
107
+ using = router.db_for_write(cls.user_model())
108
+ with transaction.atomic(using=using):
109
+ user = cls.user_model()._default_manager.create_user(*args, **kwargs)
110
+ else:
111
+ user = cls.user_model()._default_manager.create_user(*args, **kwargs)
112
+ except IntegrityError as exc:
113
+ # ORIGINAL CODE BELOW:
114
+ # # If email comes in as None it won't get found in the get
115
+ # if kwargs.get("email", True) is None:
116
+ # kwargs["email"] = ""
117
+ # try:
118
+ # user = cls.user_model()._default_manager.get(*args, **kwargs)
119
+ # except cls.user_model().DoesNotExist:
120
+ # raise exc
121
+
122
+ # BEGIN Patched logic
123
+ raise AuthAlreadyAssociated(None) from exc
124
+ # END Patched logic
125
+ return user
126
+
127
+ # Apply the patch to the original DjangoStorage.user.create_user method
128
+ original_django_storage.user.create_user = classmethod(patched_create_user)
@@ -1,6 +1,8 @@
1
1
  import contextlib
2
2
  import datetime
3
+ from importlib import resources
3
4
  import logging
5
+ import mimetypes
4
6
  import os
5
7
  import platform
6
8
  import posixpath
@@ -16,7 +18,7 @@ from django.contrib.auth.decorators import permission_required
16
18
  from django.contrib.auth.mixins import AccessMixin, LoginRequiredMixin, UserPassesTestMixin
17
19
  from django.contrib.contenttypes.models import ContentType
18
20
  from django.core.cache import cache
19
- from django.http import HttpResponseForbidden, HttpResponseServerError, JsonResponse
21
+ from django.http import FileResponse, HttpResponseForbidden, HttpResponseServerError, JsonResponse
20
22
  from django.shortcuts import get_object_or_404, render
21
23
  from django.template import loader, RequestContext, Template
22
24
  from django.template.exceptions import TemplateDoesNotExist
@@ -61,6 +63,7 @@ from nautobot.core.views.utils import (
61
63
  )
62
64
  from nautobot.extras.forms import GraphQLQueryForm
63
65
  from nautobot.extras.models import FileProxy, GraphQLQuery, Status
66
+ from nautobot.extras.plugins.urls import BASE_URL_TO_APP_LABEL
64
67
  from nautobot.extras.registry import registry
65
68
  from nautobot.extras.tables import StatusTable
66
69
 
@@ -144,6 +147,40 @@ class HomeView(AccessMixin, TemplateView):
144
147
  return self.render_to_response(context)
145
148
 
146
149
 
150
+ class AppDocsView(LoginRequiredMixin, View):
151
+ """
152
+ Serve documentation files for any pip-installed app from inside the package,
153
+ only for authenticated users.
154
+ """
155
+
156
+ def get(self, request, app_base_url, path="index.html"):
157
+ app_label = BASE_URL_TO_APP_LABEL.get(app_base_url)
158
+ if not app_label:
159
+ return JsonResponse({"detail": f"Unknown base_url '{app_base_url}'."}, status=404)
160
+ try:
161
+ base_dir = resources.files(app_label)
162
+ except ModuleNotFoundError:
163
+ return JsonResponse({"detail": f"App {app_label} not found."}, status=404)
164
+
165
+ # Dir to documentation inside the package
166
+ docs_dir = base_dir / "docs"
167
+ # Normalize path to avoid (../) etc.
168
+ normalized_path = posixpath.normpath(path).lstrip("/")
169
+ file_path = docs_dir / normalized_path
170
+
171
+ # Additional check to ensure the resolved path is still within docs_dir
172
+ if not file_path.resolve().is_relative_to(docs_dir.resolve()):
173
+ return JsonResponse({"detail": "Access denied."}, status=403)
174
+
175
+ if not file_path.is_file():
176
+ return JsonResponse({"detail": f"File {file_path} not found."}, status=404)
177
+
178
+ # Determine the MIME type based on the file extension and return the file as an HTTP response.
179
+ # This ensures that browsers interpret the file correctly (e.g., HTML, CSS, JS, images).
180
+ content_type, _ = mimetypes.guess_type(str(file_path))
181
+ return FileResponse(open(file_path, "rb"), content_type=content_type)
182
+
183
+
147
184
  class MediaView(AccessMixin, View):
148
185
  """
149
186
  Serves media files while enforcing login restrictions.
@@ -587,7 +587,7 @@ class ObjectDeleteView(UIComponentsMixin, GetReturnURLMixin, ObjectPermissionReq
587
587
  """
588
588
 
589
589
  queryset: Optional[QuerySet] = None # TODO: required, declared Optional only to avoid a breaking change
590
- template_name = "generic/object_delete.html"
590
+ template_name = "generic/object_destroy.html"
591
591
 
592
592
  def get_required_permission(self):
593
593
  return get_permission_for_model(self.queryset.model, "delete")
@@ -1044,7 +1044,7 @@ class BulkEditView(
1044
1044
  filterset: Optional[type[FilterSet]] = None
1045
1045
  table: Optional[type[Table]] = None # TODO: required, declared Optional only to avoid a breaking change
1046
1046
  form: Optional[type[Form]] = None # TODO: required, declared Optional only to avoid a breaking change
1047
- template_name = "generic/object_bulk_edit.html"
1047
+ template_name = "generic/object_bulk_update.html"
1048
1048
 
1049
1049
  def get_required_permission(self):
1050
1050
  return get_permission_for_model(self.queryset.model, "change")
@@ -1246,7 +1246,7 @@ class BulkDeleteView(
1246
1246
  filterset: Optional[type[FilterSet]] = None
1247
1247
  table: Optional[type[Table]] = None # TODO: required, declared Optional only to avoid a breaking change
1248
1248
  form: Optional[type[Form]] = None
1249
- template_name = "generic/object_bulk_delete.html"
1249
+ template_name = "generic/object_bulk_destroy.html"
1250
1250
 
1251
1251
  def get_required_permission(self):
1252
1252
  return get_permission_for_model(self.queryset.model, "delete")
@@ -73,6 +73,7 @@ PERMISSIONS_ACTION_MAP = {
73
73
  "bulk_update": "change",
74
74
  "changelog": "view",
75
75
  "notes": "view",
76
+ "data_compliance": "view",
76
77
  "approve": "change",
77
78
  "deny": "change",
78
79
  }
@@ -532,7 +533,8 @@ class NautobotViewSetMixin(GenericViewSet, UIComponentsMixin, AccessMixin, GetRe
532
533
  form.add_error(None, msg)
533
534
  return form
534
535
 
535
- def _handle_not_implemented_error(self):
536
+ def _handle_not_implemented_error(self, error):
537
+ self.logger.debug(f"NotImplementedError raised on action {self.action} resulting in error: {error}")
536
538
  # Blanket handler for NotImplementedError raised by form helper functions
537
539
  msg = "Please provide the appropriate mixin before using this helper function"
538
540
  messages.error(self.request, msg)
@@ -567,8 +569,8 @@ class NautobotViewSetMixin(GenericViewSet, UIComponentsMixin, AccessMixin, GetRe
567
569
  self._handle_validation_error(e)
568
570
  except ObjectDoesNotExist:
569
571
  form = self._handle_object_does_not_exist(form)
570
- except NotImplementedError:
571
- self._handle_not_implemented_error()
572
+ except NotImplementedError as error:
573
+ self._handle_not_implemented_error(error)
572
574
 
573
575
  if not self.has_error:
574
576
  self.logger.debug("Form validation was successful")
@@ -1527,3 +1529,13 @@ class ObjectNotesViewMixin(NautobotViewSetMixin):
1527
1529
  "active_tab": "notes",
1528
1530
  }
1529
1531
  return Response(data)
1532
+
1533
+
1534
+ class ObjectDataComplianceViewMixin(NautobotViewSetMixin):
1535
+ """
1536
+ UI Mixin for a DataCompliance to show up for a given object.
1537
+ """
1538
+
1539
+ @drf_action(detail=True)
1540
+ def data_compliance(self, request, *args, **kwargs):
1541
+ return Response({})
@@ -365,6 +365,8 @@ class NautobotHTMLRenderer(renderers.BrowsableAPIRenderer):
365
365
  # See form_valid() for self.action == "bulk_create".
366
366
  self.template = data.get("template", view.get_template_name())
367
367
 
368
+ data["request"] = request
369
+
368
370
  return super().render(data, accepted_media_type=accepted_media_type, renderer_context=renderer_context)
369
371
 
370
372
  @staticmethod
@@ -11,9 +11,10 @@ class NautobotUIViewSet(
11
11
  mixins.ObjectBulkUpdateViewMixin,
12
12
  mixins.ObjectChangeLogViewMixin,
13
13
  mixins.ObjectNotesViewMixin,
14
+ mixins.ObjectDataComplianceViewMixin,
14
15
  ):
15
16
  """
16
17
  Nautobot BaseViewSet that is intended for UI use only. It provides default Nautobot functionalities such as
17
18
  `create()`, `update()`, `partial_update()`, `bulk_update()`, `destroy()`, `bulk_destroy()`, `retrieve()`
18
- `notes()`, `changelog()` and `list()` actions.
19
+ `notes()`, `changelog()`, `list()`, and `data_compliance()` actions.
19
20
  """