diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index c30ab2bb6..f429942be 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -26,7 +26,7 @@ body: attributes: label: NetBox Version description: What version of NetBox are you currently running? - placeholder: v4.0.10 + placeholder: v4.0.11 validations: required: true - type: dropdown diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml index 01cf14820..e5102f89c 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yaml +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -14,7 +14,7 @@ body: attributes: label: NetBox version description: What version of NetBox are you currently running? - placeholder: v4.0.10 + placeholder: v4.0.11 validations: required: true - type: dropdown diff --git a/docs/release-notes/version-4.0.md b/docs/release-notes/version-4.0.md index 5afb17871..864029eba 100644 --- a/docs/release-notes/version-4.0.md +++ b/docs/release-notes/version-4.0.md @@ -1,5 +1,15 @@ # NetBox v4.0 +## v4.0.11 (2024-09-03) + +### Bug Fixes + +* [#17310](https://github.com/netbox-community/netbox/issues/17310) - Enforce restricted queryset for related objects in GraphQL API requests +* [#17321](https://github.com/netbox-community/netbox/issues/17321) - Ensure the job is attributed to the specified user when using the `runscript` management command +* [#17323](https://github.com/netbox-community/netbox/issues/17323) - Associate job with script object when executed using the `runscript` management command +* [#17337](https://github.com/netbox-community/netbox/issues/17337) - Fix ordering of virtual device contexts by device name +* [#17341](https://github.com/netbox-community/netbox/issues/17341) - Avoid `NoReverseMatch` exceptions with specific dashboard widget configurations + ## v4.0.10 (2024-08-29) ### Enhancements diff --git a/netbox/circuits/tests/test_api.py b/netbox/circuits/tests/test_api.py index 3510ac147..1edcd531b 100644 --- a/netbox/circuits/tests/test_api.py +++ b/netbox/circuits/tests/test_api.py @@ -96,6 +96,7 @@ class CircuitTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'status': 'planned', } + user_permissions = ('circuits.view_provider', 'circuits.view_circuittype') @classmethod def setUpTestData(cls): @@ -150,6 +151,7 @@ class CircuitTest(APIViewTestCases.APIViewTestCase): class CircuitTerminationTest(APIViewTestCases.APIViewTestCase): model = CircuitTermination brief_fields = ['_occupied', 'cable', 'circuit', 'description', 'display', 'id', 'term_side', 'url'] + user_permissions = ('circuits.view_circuit', ) @classmethod def setUpTestData(cls): @@ -241,6 +243,7 @@ class CircuitGroupTest(APIViewTestCases.APIViewTestCase): class ProviderAccountTest(APIViewTestCases.APIViewTestCase): model = ProviderAccount brief_fields = ['account', 'description', 'display', 'id', 'name', 'url'] + user_permissions = ('circuits.view_provider',) @classmethod def setUpTestData(cls): @@ -287,6 +290,7 @@ class CircuitGroupAssignmentTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'priority': CircuitPriorityChoices.PRIORITY_INACTIVE, } + user_permissions = ('circuits.view_circuit', 'circuits.view_circuitgroup') @classmethod def setUpTestData(cls): @@ -355,6 +359,7 @@ class CircuitGroupAssignmentTest(APIViewTestCases.APIViewTestCase): class ProviderNetworkTest(APIViewTestCases.APIViewTestCase): model = ProviderNetwork brief_fields = ['description', 'display', 'id', 'name', 'url'] + user_permissions = ('circuits.view_provider', ) @classmethod def setUpTestData(cls): diff --git a/netbox/core/tests/test_api.py b/netbox/core/tests/test_api.py index 44db21bff..eeb3bd9c4 100644 --- a/netbox/core/tests/test_api.py +++ b/netbox/core/tests/test_api.py @@ -57,6 +57,7 @@ class DataFileTest( ): model = DataFile brief_fields = ['display', 'id', 'path', 'url'] + user_permissions = ('core.view_datasource', ) @classmethod def setUpTestData(cls): diff --git a/netbox/dcim/tables/devices.py b/netbox/dcim/tables/devices.py index 81684aba1..222d756a1 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -1051,7 +1051,7 @@ class VirtualDeviceContextTable(TenancyColumnsMixin, NetBoxTable): ) device = tables.TemplateColumn( verbose_name=_('Device'), - order_by=('_name',), + order_by=('device___name',), template_code=DEVICE_LINK, linkify=True ) diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index f8f97be6f..573fdbb96 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -192,6 +192,7 @@ class LocationTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + user_permissions = ('dcim.view_site',) @classmethod def setUpTestData(cls): @@ -277,6 +278,7 @@ class RackTypeTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'new description', } + user_permissions = ('dcim.view_manufacturer',) @classmethod def setUpTestData(cls): @@ -321,6 +323,7 @@ class RackTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'status': 'planned', } + user_permissions = ('dcim.view_site', ) @classmethod def setUpTestData(cls): @@ -409,6 +412,7 @@ class RackReservationTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + user_permissions = ('dcim.view_rack', 'users.view_user') @classmethod def setUpTestData(cls): @@ -488,6 +492,7 @@ class DeviceTypeTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'part_number': 'ABC123', } + user_permissions = ('dcim.view_manufacturer', ) @classmethod def setUpTestData(cls): @@ -533,6 +538,7 @@ class ModuleTypeTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'part_number': 'ABC123', } + user_permissions = ('dcim.view_manufacturer', ) @classmethod def setUpTestData(cls): @@ -704,6 +710,7 @@ class PowerOutletTemplateTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + user_permissions = ('dcim.view_devicetype', ) @classmethod def setUpTestData(cls): @@ -809,6 +816,7 @@ class FrontPortTemplateTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + user_permissions = ('dcim.view_rearporttemplate', ) @classmethod def setUpTestData(cls): @@ -946,6 +954,7 @@ class ModuleBayTemplateTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + user_permissions = ('dcim.view_devicetype', ) @classmethod def setUpTestData(cls): @@ -986,6 +995,7 @@ class DeviceBayTemplateTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + user_permissions = ('dcim.view_devicetype', ) @classmethod def setUpTestData(cls): @@ -1026,6 +1036,7 @@ class InventoryItemTemplateTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + user_permissions = ('dcim.view_devicetype', 'dcim.view_manufacturer',) @classmethod def setUpTestData(cls): @@ -1144,6 +1155,10 @@ class DeviceTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'status': 'failed', } + user_permissions = ( + 'dcim.view_site', 'dcim.view_rack', 'dcim.view_location', 'dcim.view_devicerole', 'dcim.view_devicetype', + 'extras.view_configtemplate', + ) @classmethod def setUpTestData(cls): @@ -1334,6 +1349,7 @@ class ModuleTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'serial': '1234ABCD', } + user_permissions = ('dcim.view_modulebay', 'dcim.view_moduletype', 'dcim.view_device') @classmethod def setUpTestData(cls): @@ -1400,6 +1416,7 @@ class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa 'description': 'New description', } peer_termination_type = ConsoleServerPort + user_permissions = ('dcim.view_device', ) @classmethod def setUpTestData(cls): @@ -1442,6 +1459,7 @@ class ConsoleServerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIView 'description': 'New description', } peer_termination_type = ConsolePort + user_permissions = ('dcim.view_device', ) @classmethod def setUpTestData(cls): @@ -1484,6 +1502,7 @@ class PowerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase 'description': 'New description', } peer_termination_type = PowerOutlet + user_permissions = ('dcim.view_device', ) @classmethod def setUpTestData(cls): @@ -1523,6 +1542,7 @@ class PowerOutletTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa 'description': 'New description', } peer_termination_type = PowerPort + user_permissions = ('dcim.view_device', ) @classmethod def setUpTestData(cls): @@ -1571,6 +1591,7 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase 'description': 'New description', } peer_termination_type = Interface + user_permissions = ('dcim.view_device', ) @classmethod def setUpTestData(cls): @@ -1705,6 +1726,7 @@ class FrontPortTest(APIViewTestCases.APIViewTestCase): 'description': 'New description', } peer_termination_type = Interface + user_permissions = ('dcim.view_device', 'dcim.view_rearport') @classmethod def setUpTestData(cls): @@ -1763,6 +1785,7 @@ class RearPortTest(APIViewTestCases.APIViewTestCase): 'description': 'New description', } peer_termination_type = Interface + user_permissions = ('dcim.view_device', ) @classmethod def setUpTestData(cls): @@ -1804,6 +1827,7 @@ class ModuleBayTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + user_permissions = ('dcim.view_device', ) @classmethod def setUpTestData(cls): @@ -1844,6 +1868,7 @@ class DeviceBayTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + user_permissions = ('dcim.view_device', ) @classmethod def setUpTestData(cls): @@ -1907,6 +1932,7 @@ class InventoryItemTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + user_permissions = ('dcim.view_device', 'dcim.view_manufacturer') @classmethod def setUpTestData(cls): @@ -2203,6 +2229,7 @@ class VirtualChassisTest(APIViewTestCases.APIViewTestCase): class PowerPanelTest(APIViewTestCases.APIViewTestCase): model = PowerPanel brief_fields = ['description', 'display', 'id', 'name', 'powerfeed_count', 'url'] + user_permissions = ('dcim.view_site', ) @classmethod def setUpTestData(cls): @@ -2255,6 +2282,7 @@ class PowerFeedTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'status': 'planned', } + user_permissions = ('dcim.view_powerpanel', ) @classmethod def setUpTestData(cls): diff --git a/netbox/extras/dashboard/widgets.py b/netbox/extras/dashboard/widgets.py index 1003b39f2..091819bd1 100644 --- a/netbox/extras/dashboard/widgets.py +++ b/netbox/extras/dashboard/widgets.py @@ -183,10 +183,13 @@ class ObjectCountsWidget(DashboardWidget): for model in get_models_from_content_types(self.config['models']): permission = get_permission_for_model(model, 'view') if request.user.has_perm(permission): - url = reverse(get_viewname(model, 'list')) + try: + url = reverse(get_viewname(model, 'list')) + except NoReverseMatch: + url = None qs = model.objects.restrict(request.user, 'view') # Apply any specified filters - if filters := self.config.get('filters'): + if url and (filters := self.config.get('filters')): params = dict_to_querydict(filters) filterset = getattr(resolve(url).func.view_class, 'filterset', None) qs = filterset(params, qs).qs diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py index 00c240769..59335cbbe 100644 --- a/netbox/ipam/tests/test_api.py +++ b/netbox/ipam/tests/test_api.py @@ -768,6 +768,7 @@ class FHRPGroupAssignmentTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'priority': 100, } + user_permissions = ('ipam.view_fhrpgroup', ) @classmethod def setUpTestData(cls): diff --git a/netbox/netbox/graphql/schema.py b/netbox/netbox/graphql/schema.py index d3f8c5dd7..a7609c9d2 100644 --- a/netbox/netbox/graphql/schema.py +++ b/netbox/netbox/graphql/schema.py @@ -38,7 +38,7 @@ schema = strawberry.Schema( query=Query, config=StrawberryConfig(auto_camel_case=False), extensions=[ - DjangoOptimizerExtension, + DjangoOptimizerExtension(prefetch_custom_queryset=True), MaxAliasesLimiter(max_alias_count=settings.GRAPHQL_MAX_ALIASES), ] ) diff --git a/netbox/netbox/tests/test_graphql.py b/netbox/netbox/tests/test_graphql.py index ab80c79c7..34ea3ad6a 100644 --- a/netbox/netbox/tests/test_graphql.py +++ b/netbox/netbox/tests/test_graphql.py @@ -6,6 +6,7 @@ from rest_framework import status from core.models import ObjectType from dcim.models import Site, Location +from ipam.models import ASN, RIR from users.models import ObjectPermission from utilities.testing import disable_warnings, APITestCase, TestCase @@ -45,7 +46,6 @@ class GraphQLTestCase(TestCase): class GraphQLAPITestCase(APITestCase): @override_settings(LOGIN_REQUIRED=True) - @override_settings(EXEMPT_VIEW_PERMISSIONS=['*', 'auth.user']) def test_graphql_filter_objects(self): """ Test the operation of filters for GraphQL API requests. @@ -66,6 +66,7 @@ class GraphQLAPITestCase(APITestCase): obj_perm.save() obj_perm.users.add(self.user) obj_perm.object_types.add(ObjectType.objects.get_for_model(Location)) + obj_perm.object_types.add(ObjectType.objects.get_for_model(Site)) # A valid request should return the filtered list url = reverse('graphql') @@ -75,6 +76,7 @@ class GraphQLAPITestCase(APITestCase): data = json.loads(response.content) self.assertNotIn('errors', data) self.assertEqual(len(data['data']['location_list']), 1) + self.assertIsNotNone(data['data']['location_list'][0]['site']) # An invalid request should return an empty list query = '{location_list(filters: {site_id: "99999"}) {id site {id}}}' # Invalid site ID @@ -82,3 +84,12 @@ class GraphQLAPITestCase(APITestCase): self.assertHttpStatus(response, status.HTTP_200_OK) data = json.loads(response.content) self.assertEqual(len(data['data']['location_list']), 0) + + # Removing the permissions from location should result in an empty locations list + obj_perm.object_types.remove(ObjectType.objects.get_for_model(Location)) + query = '{site(id: ' + str(sites[0].pk) + ') {id locations {id}}}' + response = self.client.post(url, data={'query': query}, format="json", **self.header) + self.assertHttpStatus(response, status.HTTP_200_OK) + data = json.loads(response.content) + self.assertNotIn('errors', data) + self.assertEqual(len(data['data']['site']['locations']), 0) diff --git a/netbox/project-static/yarn.lock b/netbox/project-static/yarn.lock index bd27e1064..0bdd1ab77 100644 --- a/netbox/project-static/yarn.lock +++ b/netbox/project-static/yarn.lock @@ -992,14 +992,7 @@ brace-expansion@^2.0.1: dependencies: balanced-match "^1.0.0" -braces@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -braces@~3.0.2: +braces@^3.0.3, braces@~3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== @@ -1575,7 +1568,7 @@ file-entry-cache@^6.0.1: dependencies: flat-cache "^3.0.4" -fill-range@^7.0.1, fill-range@^7.1.1: +fill-range@^7.1.1: version "7.1.1" resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== @@ -2190,11 +2183,11 @@ meros@^1.1.4: integrity sha512-2BNGOimxEz5hmjUG2FwoxCt5HN7BXdaWyFqEwxPTrJzVdABtrL4TiHTcsWSFAxPQ/tOnEaQEJh3qWq71QRMY+w== micromatch@^4.0.4: - version "4.0.5" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" - integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== dependencies: - braces "^3.0.2" + braces "^3.0.3" picomatch "^2.3.1" minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: diff --git a/netbox/templates/extras/dashboard/widgets/objectcounts.html b/netbox/templates/extras/dashboard/widgets/objectcounts.html index 6110aa4c0..70b71059f 100644 --- a/netbox/templates/extras/dashboard/widgets/objectcounts.html +++ b/netbox/templates/extras/dashboard/widgets/objectcounts.html @@ -3,7 +3,7 @@ {% if counts %}