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 d3745f2b1..0fc530447 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): @@ -209,6 +211,7 @@ class CircuitTerminationTest(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): @@ -252,6 +255,7 @@ class ProviderAccountTest(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 4156a5a0a..e63a0c44c 100644 --- a/netbox/dcim/tables/devices.py +++ b/netbox/dcim/tables/devices.py @@ -1031,7 +1031,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 52b850b24..90b097e0a 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -195,6 +195,7 @@ class LocationTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + user_permissions = ('dcim.view_site', ) @classmethod def setUpTestData(cls): @@ -280,6 +281,7 @@ class RackTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'status': 'planned', } + user_permissions = ('dcim.view_site', ) @classmethod def setUpTestData(cls): @@ -368,6 +370,7 @@ class RackReservationTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + user_permissions = ('dcim.view_rack', 'users.view_user') @classmethod def setUpTestData(cls): @@ -447,6 +450,7 @@ class DeviceTypeTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'part_number': 'ABC123', } + user_permissions = ('dcim.view_manufacturer', ) @classmethod def setUpTestData(cls): @@ -492,6 +496,7 @@ class ModuleTypeTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'part_number': 'ABC123', } + user_permissions = ('dcim.view_manufacturer', ) @classmethod def setUpTestData(cls): @@ -663,6 +668,7 @@ class PowerOutletTemplateTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + user_permissions = ('dcim.view_devicetype', ) @classmethod def setUpTestData(cls): @@ -768,6 +774,7 @@ class FrontPortTemplateTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + user_permissions = ('dcim.view_rearporttemplate', ) @classmethod def setUpTestData(cls): @@ -905,6 +912,7 @@ class ModuleBayTemplateTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + user_permissions = ('dcim.view_devicetype', ) @classmethod def setUpTestData(cls): @@ -945,6 +953,7 @@ class DeviceBayTemplateTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + user_permissions = ('dcim.view_devicetype', ) @classmethod def setUpTestData(cls): @@ -985,6 +994,7 @@ class InventoryItemTemplateTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + user_permissions = ('dcim.view_devicetype', 'dcim.view_manufacturer',) @classmethod def setUpTestData(cls): @@ -1103,6 +1113,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): @@ -1293,6 +1307,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): @@ -1358,6 +1373,7 @@ class ConsolePortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa 'description': 'New description', } peer_termination_type = ConsoleServerPort + user_permissions = ('dcim.view_device', ) @classmethod def setUpTestData(cls): @@ -1400,6 +1416,7 @@ class ConsoleServerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIView 'description': 'New description', } peer_termination_type = ConsolePort + user_permissions = ('dcim.view_device', ) @classmethod def setUpTestData(cls): @@ -1442,6 +1459,7 @@ class PowerPortTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase 'description': 'New description', } peer_termination_type = PowerOutlet + user_permissions = ('dcim.view_device', ) @classmethod def setUpTestData(cls): @@ -1481,6 +1499,7 @@ class PowerOutletTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCa 'description': 'New description', } peer_termination_type = PowerPort + user_permissions = ('dcim.view_device', ) @classmethod def setUpTestData(cls): @@ -1529,6 +1548,7 @@ class InterfaceTest(Mixins.ComponentTraceMixin, APIViewTestCases.APIViewTestCase 'description': 'New description', } peer_termination_type = Interface + user_permissions = ('dcim.view_device', ) @classmethod def setUpTestData(cls): @@ -1663,6 +1683,7 @@ class FrontPortTest(APIViewTestCases.APIViewTestCase): 'description': 'New description', } peer_termination_type = Interface + user_permissions = ('dcim.view_device', 'dcim.view_rearport') @classmethod def setUpTestData(cls): @@ -1721,6 +1742,7 @@ class RearPortTest(APIViewTestCases.APIViewTestCase): 'description': 'New description', } peer_termination_type = Interface + user_permissions = ('dcim.view_device', ) @classmethod def setUpTestData(cls): @@ -1762,6 +1784,7 @@ class ModuleBayTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + user_permissions = ('dcim.view_device', ) @classmethod def setUpTestData(cls): @@ -1801,6 +1824,7 @@ class DeviceBayTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + user_permissions = ('dcim.view_device', ) @classmethod def setUpTestData(cls): @@ -1864,6 +1888,7 @@ class InventoryItemTest(APIViewTestCases.APIViewTestCase): bulk_update_data = { 'description': 'New description', } + user_permissions = ('dcim.view_device', 'dcim.view_manufacturer') @classmethod def setUpTestData(cls): @@ -2160,6 +2185,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): @@ -2212,6 +2238,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 df41cd34b..d23919d9e 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/extras/management/commands/runscript.py b/netbox/extras/management/commands/runscript.py index ef1bd5141..4afbd733e 100644 --- a/netbox/extras/management/commands/runscript.py +++ b/netbox/extras/management/commands/runscript.py @@ -34,7 +34,7 @@ class Command(BaseCommand): def handle(self, *args, **options): - def _run_script(): + def _run_script(script): """ Core script execution task. We capture this within a subfunction to allow for conditionally wrapping it with the event_tracking context manager (which is bypassed if commit == False). @@ -85,7 +85,6 @@ class Command(BaseCommand): module_name, script_name = script.split('.', 1) module, script = get_module_and_script(module_name, script_name) - script = script.python_class # Take user from command line if provided and exists, other if options['user']: @@ -102,7 +101,7 @@ class Command(BaseCommand): stdouthandler.setLevel(logging.DEBUG) stdouthandler.setFormatter(formatter) - logger = logging.getLogger(f"netbox.scripts.{script.full_name}") + logger = logging.getLogger(f"netbox.scripts.{script.python_class.full_name}") logger.addHandler(stdouthandler) try: @@ -118,14 +117,14 @@ class Command(BaseCommand): raise CommandError(f"Invalid log level: {loglevel}") # Initialize the script form - script = script() - form = script.as_form(data, None) + script_instance = script.python_class() + form = script_instance.as_form(data, None) # Create the job job = Job.objects.create( - object=module, - name=script.class_name, - user=User.objects.filter(is_superuser=True).order_by('pk')[0], + object=script, + name=script_instance.class_name, + user=user, job_id=uuid.uuid4() ) @@ -149,7 +148,7 @@ class Command(BaseCommand): # Execute the script. If commit is True, wrap it with the event_tracking context manager to ensure we process # change logging, webhooks, etc. with event_tracking(request): - _run_script() + _run_script(script_instance) else: logger.error('Data is not valid:') for field, errors in form.errors.get_json_data().items(): diff --git a/netbox/ipam/tests/test_api.py b/netbox/ipam/tests/test_api.py index 2cf7a2f1c..aed66e979 100644 --- a/netbox/ipam/tests/test_api.py +++ b/netbox/ipam/tests/test_api.py @@ -767,6 +767,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 2b4c83405..78b2e54e7 100644 --- a/netbox/netbox/graphql/schema.py +++ b/netbox/netbox/graphql/schema.py @@ -36,6 +36,6 @@ schema = strawberry.Schema( query=Query, config=StrawberryConfig(auto_camel_case=False), extensions=[ - DjangoOptimizerExtension, + DjangoOptimizerExtension(prefetch_custom_queryset=True), ] ) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 6201409aa..76167025e 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -25,7 +25,7 @@ from utilities.string import trailing_slash # Environment setup # -VERSION = '4.0.10' +VERSION = '4.0.11' HOSTNAME = platform.node() # Set the base directory two levels up BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 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 3fdf8ba32..ce7aa20c0 100644 --- a/netbox/project-static/yarn.lock +++ b/netbox/project-static/yarn.lock @@ -986,14 +986,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== @@ -1564,7 +1557,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== @@ -2174,11 +2167,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 %}