nautobot 2.3.14__py3-none-any.whl → 2.3.15b1__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.

@@ -144,13 +144,18 @@ class NautobotTestCaseMixin:
144
144
  # Permissions management
145
145
  #
146
146
 
147
- def add_permissions(self, *names):
147
+ def add_permissions(self, *names, **kwargs):
148
148
  """
149
149
  Assign a set of permissions to the test user. Accepts permission names in the form <app>.<action>_<model>.
150
+ Additional keyword arguments will be passed to the ObjectPermission constructor to allow creating more detailed permissions.
151
+
152
+ Examples:
153
+ >>> add_permissions("ipam.add_vlangroup", "ipam.view_vlangroup")
154
+ >>> add_permissions("ipam.add_vlangroup", "ipam.view_vlangroup", constraints={"pk": "uuid-1234"})
150
155
  """
151
156
  for name in names:
152
157
  ct, action = permissions.resolve_permission_ct(name)
153
- obj_perm = users_models.ObjectPermission(name=name, actions=[action])
158
+ obj_perm = users_models.ObjectPermission(name=name, actions=[action], **kwargs)
154
159
  obj_perm.save()
155
160
  obj_perm.users.add(self.user)
156
161
  obj_perm.object_types.add(ct)
@@ -227,7 +227,8 @@ class PrefixViewSet(NautobotModelViewSet):
227
227
 
228
228
  # Create the new Prefix(es)
229
229
  serializer.is_valid(raise_exception=True)
230
- serializer.save()
230
+ self.perform_create(serializer)
231
+
231
232
  return Response(serializer.data, status=status.HTTP_201_CREATED)
232
233
 
233
234
  else:
@@ -318,7 +319,8 @@ class PrefixViewSet(NautobotModelViewSet):
318
319
 
319
320
  # Create the new IP address(es)
320
321
  serializer.is_valid(raise_exception=True)
321
- serializer.save()
322
+ self.perform_create(serializer)
323
+
322
324
  return Response(serializer.data, status=status.HTTP_201_CREATED)
323
325
 
324
326
  # Determine the maximum number of IPs to return
@@ -396,21 +398,20 @@ class VLANGroupViewSet(NautobotModelViewSet):
396
398
  serializer_class = serializers.VLANGroupSerializer
397
399
  filterset_class = filters.VLANGroupFilterSet
398
400
 
399
- def restrict_queryset(self, request, *args, **kwargs):
400
- """
401
- Apply "view" permissions on the POST /available-vlans/ endpoint, otherwise as ModelViewSetMixin.
402
- """
403
- if request.user.is_authenticated and self.action == "available_vlans":
404
- self.queryset = self.queryset.restrict(request.user, "view")
405
- else:
406
- super().restrict_queryset(request, *args, **kwargs)
401
+ @staticmethod
402
+ def vlan_group_queryset():
403
+ return (
404
+ VLANGroup.objects.select_related("location")
405
+ .prefetch_related("tags")
406
+ .annotate(vlan_count=count_related(VLAN, "vlan_group"))
407
+ )
407
408
 
408
409
  class AvailableVLANPermissions(TokenPermissions):
409
- """As nautobot.core.api.authentication.TokenPermissions, but enforcing add_vlan permission."""
410
+ """As nautobot.core.api.authentication.TokenPermissions, but enforcing `add_vlan` and `view_vlan` permission."""
410
411
 
411
412
  perms_map = {
412
- "GET": ["ipam.view_vlangroup"],
413
- "POST": ["ipam.view_vlangroup", "ipam.add_vlan"],
413
+ "GET": ["ipam.view_vlangroup", "ipam.view_vlan"],
414
+ "POST": ["ipam.view_vlangroup", "ipam.view_vlan", "ipam.add_vlan"],
414
415
  }
415
416
 
416
417
  @extend_schema(methods=["get"], responses={200: ListSerializer(child=IntegerField())})
@@ -426,6 +427,7 @@ class VLANGroupViewSet(NautobotModelViewSet):
426
427
  methods=["get", "post"],
427
428
  permission_classes=[AvailableVLANPermissions],
428
429
  filterset_class=None,
430
+ queryset=VLAN.objects.all(),
429
431
  )
430
432
  def available_vlans(self, request, pk=None):
431
433
  """
@@ -433,7 +435,7 @@ class VLANGroupViewSet(NautobotModelViewSet):
433
435
  By default, the number of VIDs returned will be equivalent to PAGINATE_COUNT.
434
436
  An arbitrary limit (up to MAX_PAGE_SIZE, if set) may be passed, however results will not be paginated.
435
437
  """
436
- vlan_group = get_object_or_404(self.queryset, pk=pk)
438
+ vlan_group = get_object_or_404(self.vlan_group_queryset().restrict(user=request.user), pk=pk)
437
439
 
438
440
  if request.method == "POST":
439
441
  with cache.lock(
@@ -509,7 +511,7 @@ class VLANGroupViewSet(NautobotModelViewSet):
509
511
 
510
512
  # Create the new VLANs
511
513
  serializer.is_valid(raise_exception=True)
512
- serializer.save()
514
+ self.perform_create(serializer)
513
515
 
514
516
  data = serializer.data
515
517
 
@@ -571,6 +571,126 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
571
571
  self.assertIn("prefixcf", response.data[0]["custom_fields"])
572
572
  self.assertEqual("value 1", response.data[0]["custom_fields"]["prefixcf"])
573
573
 
574
+ def test_create_available_prefixes_with_permissions_constraint(self):
575
+ # Prepare prefix and permissions
576
+ prefix = Prefix.objects.create(
577
+ prefix="10.2.3.0/24",
578
+ type=choices.PrefixTypeChoices.TYPE_POOL,
579
+ namespace=self.namespace,
580
+ status=self.status,
581
+ description="This is the Prefix created for whole network.",
582
+ )
583
+ url = reverse("ipam-api:prefix-available-prefixes", kwargs={"pk": prefix.pk})
584
+ self.add_permissions("ipam.view_prefix")
585
+ self.add_permissions(
586
+ "ipam.add_prefix", constraints={"description__startswith": "This is the Prefix created for"}
587
+ )
588
+
589
+ # Test invalid request
590
+ data = {
591
+ "prefix_length": 26,
592
+ "status": self.status.pk,
593
+ }
594
+ invalid_data_list = [
595
+ data,
596
+ {**data, "description": ""},
597
+ {**data, "description": "Some description"},
598
+ {**data, "description": "Some description. This is the IP created for"},
599
+ ]
600
+
601
+ for invalid_data in invalid_data_list:
602
+ with self.subTest(case=invalid_data):
603
+ response = self.client.post(url, invalid_data, format="json", **self.header)
604
+ self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
605
+ self.assertIn("detail", response.data)
606
+ self.assertEqual(response.data["detail"], "You do not have permission to perform this action.")
607
+
608
+ # Verify that no prefixes were created (the entire prefix is still available)
609
+ response = self.client.get(url, **self.header)
610
+ self.assertHttpStatus(response, status.HTTP_200_OK)
611
+ self.assertEqual(len(response.data), 1)
612
+ self.assertEqual(response.data[0]["prefix"], prefix.cidr_str)
613
+
614
+ # Test valid request
615
+ valid_data = {
616
+ "prefix_length": 26,
617
+ "status": self.status.pk,
618
+ "description": "This is the Prefix created for my local network",
619
+ }
620
+ response = self.client.post(url, valid_data, format="json", **self.header)
621
+ self.assertHttpStatus(response, status.HTTP_201_CREATED)
622
+ self.assertEqual(response.data["prefix"], "10.2.3.0/26")
623
+
624
+ # Verify that prefix is created
625
+ response = self.client.get(url, **self.header)
626
+ self.assertHttpStatus(response, status.HTTP_200_OK)
627
+ self.assertEqual(len(response.data), 2)
628
+ self.assertEqual(response.data[0]["prefix"], "10.2.3.64/26")
629
+ self.assertEqual(response.data[1]["prefix"], "10.2.3.128/25")
630
+
631
+ def test_create_multiple_available_prefixes_with_permissions_constraint(self):
632
+ # Prepare prefix and permissions
633
+ prefix = Prefix.objects.create(
634
+ prefix="10.2.3.0/24",
635
+ type=choices.PrefixTypeChoices.TYPE_POOL,
636
+ namespace=self.namespace,
637
+ status=self.status,
638
+ description="This is the Prefix created for whole network.",
639
+ )
640
+ url = reverse("ipam-api:prefix-available-prefixes", kwargs={"pk": prefix.pk})
641
+ self.add_permissions("ipam.view_prefix")
642
+ self.add_permissions(
643
+ "ipam.add_prefix", constraints={"description__startswith": "This is the Prefix created for"}
644
+ )
645
+
646
+ # Test invalid request
647
+ data = [
648
+ {
649
+ "prefix_length": 26,
650
+ "status": self.status.pk,
651
+ "description": "This is the Prefix created for my local network",
652
+ },
653
+ {
654
+ "prefix_length": 26,
655
+ "status": self.status.pk,
656
+ },
657
+ ]
658
+ response = self.client.post(url, data, format="json", **self.header)
659
+ self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
660
+ self.assertIn("detail", response.data)
661
+ self.assertEqual(response.data["detail"], "You do not have permission to perform this action.")
662
+
663
+ # Verify that no prefixes were created (the entire prefix is still available)
664
+ response = self.client.get(url, **self.header)
665
+ self.assertHttpStatus(response, status.HTTP_200_OK)
666
+ self.assertEqual(len(response.data), 1)
667
+ self.assertEqual(response.data[0]["prefix"], prefix.cidr_str)
668
+
669
+ # Test valid request
670
+ data = [
671
+ {
672
+ "prefix_length": 26,
673
+ "status": self.status.pk,
674
+ "description": "This is the Prefix created for my local network",
675
+ },
676
+ {
677
+ "prefix_length": 26,
678
+ "status": self.status.pk,
679
+ "description": "This is the Prefix created for my guest house network",
680
+ },
681
+ ]
682
+ response = self.client.post(url, data, format="json", **self.header)
683
+ self.assertHttpStatus(response, status.HTTP_201_CREATED)
684
+ self.assertEqual(len(response.data), 2)
685
+ self.assertEqual(response.data[0]["prefix"], "10.2.3.0/26")
686
+ self.assertEqual(response.data[1]["prefix"], "10.2.3.64/26")
687
+
688
+ # Verify that prefixes were created
689
+ response = self.client.get(url, **self.header)
690
+ self.assertHttpStatus(response, status.HTTP_200_OK)
691
+ self.assertEqual(len(response.data), 1)
692
+ self.assertEqual(response.data[0]["prefix"], "10.2.3.128/25")
693
+
574
694
  def test_list_available_ips(self):
575
695
  """
576
696
  Test retrieval of all available IP addresses within a parent prefix.
@@ -731,6 +851,116 @@ class PrefixTest(APIViewTestCases.APIViewTestCase):
731
851
  self.assertIn("ipcf", response.data[0]["custom_fields"])
732
852
  self.assertEqual("1", response.data[0]["custom_fields"]["ipcf"])
733
853
 
854
+ def test_create_available_ips_with_permissions_constraint(self):
855
+ # Prepare prefix and permissions
856
+ prefix = Prefix.objects.create(
857
+ prefix="192.168.0.0/30",
858
+ type=choices.PrefixTypeChoices.TYPE_NETWORK,
859
+ namespace=self.namespace,
860
+ status=self.status,
861
+ description="This is the Prefix created for whole network.",
862
+ )
863
+ url = reverse("ipam-api:prefix-available-ips", kwargs={"pk": prefix.pk})
864
+ self.add_permissions("ipam.view_prefix", "ipam.view_ipaddress")
865
+ self.add_permissions(
866
+ "ipam.add_ipaddress", constraints={"description__startswith": "This is the IP created for"}
867
+ )
868
+
869
+ # Test invalid request
870
+ data = {
871
+ "status": self.status.pk,
872
+ }
873
+ invalid_data_list = [
874
+ data,
875
+ {**data, "description": ""},
876
+ {**data, "description": "Some description"},
877
+ {**data, "description": "Some description. This is the IP created for"},
878
+ ]
879
+
880
+ for invalid_data in invalid_data_list:
881
+ with self.subTest(case=invalid_data):
882
+ response = self.client.post(url, invalid_data, format="json", **self.header)
883
+ self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
884
+ self.assertIn("detail", response.data)
885
+ self.assertEqual(response.data["detail"], "You do not have permission to perform this action.")
886
+
887
+ # Verify that no IPs were created (the entire prefix pool is still available)
888
+ response = self.client.get(url, **self.header)
889
+ self.assertHttpStatus(response, status.HTTP_200_OK)
890
+ self.assertEqual(len(response.data), 2)
891
+
892
+ # Test valid request
893
+ valid_data = {
894
+ "status": self.status.pk,
895
+ "description": "This is the IP created for my private laptop",
896
+ }
897
+ response = self.client.post(url, valid_data, format="json", **self.header)
898
+ self.assertHttpStatus(response, status.HTTP_201_CREATED)
899
+ self.assertEqual(response.data["address"], "192.168.0.1/30")
900
+
901
+ # Verify that IP is created
902
+ response = self.client.get(url, **self.header)
903
+ self.assertHttpStatus(response, status.HTTP_200_OK)
904
+ self.assertEqual(len(response.data), 1)
905
+ self.assertEqual(response.data[0]["address"], "192.168.0.2/30")
906
+
907
+ def test_create_multiple_available_ips_with_permissions_constraint(self):
908
+ # Prepare prefix and permissions
909
+ prefix = Prefix.objects.create(
910
+ prefix="192.168.0.0/30",
911
+ type=choices.PrefixTypeChoices.TYPE_NETWORK,
912
+ namespace=self.namespace,
913
+ status=self.status,
914
+ description="This is a Prefix created for whole network.",
915
+ )
916
+ url = reverse("ipam-api:prefix-available-ips", kwargs={"pk": prefix.pk})
917
+ self.add_permissions("ipam.view_prefix", "ipam.view_ipaddress")
918
+ self.add_permissions(
919
+ "ipam.add_ipaddress", constraints={"description__startswith": "This is the IP created for"}
920
+ )
921
+
922
+ # Test invalid request
923
+ data = [
924
+ {
925
+ "status": self.status.pk,
926
+ },
927
+ {
928
+ "status": self.status.pk,
929
+ "description": "This is an IP created for my private laptop",
930
+ },
931
+ ]
932
+ response = self.client.post(url, data, format="json", **self.header)
933
+ self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
934
+ self.assertIn("detail", response.data)
935
+ self.assertEqual(response.data["detail"], "You do not have permission to perform this action.")
936
+
937
+ # Verify that no IPs were created (the entire prefix pool is still available)
938
+ response = self.client.get(url, **self.header)
939
+ self.assertHttpStatus(response, status.HTTP_200_OK)
940
+ self.assertEqual(len(response.data), 2)
941
+
942
+ # Test valid request
943
+ valid_data = [
944
+ {
945
+ "status": self.status.pk,
946
+ "description": "This is the IP created for my private laptop",
947
+ },
948
+ {
949
+ "status": self.status.pk,
950
+ "description": "This is the IP created for my gaming laptop",
951
+ },
952
+ ]
953
+ response = self.client.post(url, valid_data, format="json", **self.header)
954
+ self.assertHttpStatus(response, status.HTTP_201_CREATED)
955
+ self.assertEqual(len(response.data), 2)
956
+ self.assertEqual(response.data[0]["address"], "192.168.0.1/30")
957
+ self.assertEqual(response.data[1]["address"], "192.168.0.2/30")
958
+
959
+ # Verify that IPs are created
960
+ response = self.client.get(url, **self.header)
961
+ self.assertHttpStatus(response, status.HTTP_200_OK)
962
+ self.assertEqual(len(response.data), 0)
963
+
734
964
 
735
965
  class PrefixLocationAssignmentTest(APIViewTestCases.APIViewTestCase):
736
966
  model = PrefixLocationAssignment
@@ -1089,7 +1319,7 @@ class VLANGroupTest(APIViewTestCases.APIViewTestCase):
1089
1319
  Test retrieval of all available VLAN IDs within a VLANGroup.
1090
1320
  """
1091
1321
  url = reverse("ipam-api:vlangroup-available-vlans", kwargs={"pk": self.vlan_group.pk})
1092
- self.add_permissions("ipam.view_vlangroup")
1322
+ self.add_permissions("ipam.view_vlangroup", "ipam.view_vlan")
1093
1323
 
1094
1324
  # Retrieve all available VLAN IDs
1095
1325
  response = self.client.get(url, **self.header)
@@ -1106,6 +1336,7 @@ class VLANGroupTest(APIViewTestCases.APIViewTestCase):
1106
1336
  url = reverse("ipam-api:vlangroup-available-vlans", kwargs={"pk": self.vlan_group.pk})
1107
1337
  self.add_permissions(
1108
1338
  "ipam.view_vlangroup",
1339
+ "ipam.view_vlan",
1109
1340
  "ipam.add_vlan",
1110
1341
  )
1111
1342
 
@@ -1147,6 +1378,7 @@ class VLANGroupTest(APIViewTestCases.APIViewTestCase):
1147
1378
  url = reverse("ipam-api:vlangroup-available-vlans", kwargs={"pk": self.vlan_group.pk})
1148
1379
  self.add_permissions(
1149
1380
  "ipam.view_vlangroup",
1381
+ "ipam.view_vlan",
1150
1382
  "ipam.add_vlan",
1151
1383
  )
1152
1384
 
@@ -1193,6 +1425,7 @@ class VLANGroupTest(APIViewTestCases.APIViewTestCase):
1193
1425
  url = reverse("ipam-api:vlangroup-available-vlans", kwargs={"pk": self.vlan_group.pk})
1194
1426
  self.add_permissions(
1195
1427
  "ipam.view_vlangroup",
1428
+ "ipam.view_vlan",
1196
1429
  "ipam.add_vlan",
1197
1430
  )
1198
1431
 
@@ -1222,6 +1455,7 @@ class VLANGroupTest(APIViewTestCases.APIViewTestCase):
1222
1455
  url = reverse("ipam-api:vlangroup-available-vlans", kwargs={"pk": self.vlan_group.pk})
1223
1456
  self.add_permissions(
1224
1457
  "ipam.view_vlangroup",
1458
+ "ipam.view_vlan",
1225
1459
  "ipam.add_vlan",
1226
1460
  )
1227
1461
 
@@ -1249,6 +1483,105 @@ class VLANGroupTest(APIViewTestCases.APIViewTestCase):
1249
1483
  response.data["detail"],
1250
1484
  )
1251
1485
 
1486
+ def test_create_available_vlans_with_permissions_constraint(self):
1487
+ url = reverse("ipam-api:vlangroup-available-vlans", kwargs={"pk": self.vlan_group.pk})
1488
+ self.add_permissions(
1489
+ "ipam.view_vlangroup",
1490
+ "ipam.view_vlan",
1491
+ )
1492
+ self.add_permissions("ipam.add_vlan", constraints={"description__startswith": "This is the VLAN created for"})
1493
+
1494
+ data = {"name": "VLAN_6", "status": self.default_status.pk, "vid": 6}
1495
+ invalid_data_list = [
1496
+ data,
1497
+ {**data, "description": ""},
1498
+ {**data, "description": "Some description"},
1499
+ {**data, "description": "Some description. This is the VLAN created for"},
1500
+ ]
1501
+
1502
+ # Test invalid request
1503
+ for invalid_data in invalid_data_list:
1504
+ with self.subTest(case=invalid_data):
1505
+ response = self.client.post(url, data, format="json", **self.header)
1506
+ self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
1507
+ self.assertIn("detail", response.data)
1508
+ self.assertEqual(response.data["detail"], "You do not have permission to perform this action.")
1509
+
1510
+ # Verify that no VLANs were created (number of VLANs is the same as on the beginning of the test)
1511
+ response = self.client.get(url, **self.header)
1512
+ self.assertHttpStatus(response, status.HTTP_200_OK)
1513
+ self.assertEqual(len(response.data["results"]), len(self.unused_vids))
1514
+
1515
+ # Test valid request
1516
+ valid_data = {
1517
+ "name": "VLAN_6",
1518
+ "status": self.default_status.pk,
1519
+ "vid": 6,
1520
+ "description": "This is the VLAN created for home automation.",
1521
+ }
1522
+ response = self.client.post(url, valid_data, format="json", **self.header)
1523
+ self.assertHttpStatus(response, status.HTTP_201_CREATED)
1524
+ self.assertEqual(response.data["results"]["name"], valid_data["name"])
1525
+
1526
+ # Verify that VLAN is created
1527
+ response = self.client.get(url, **self.header)
1528
+ self.assertHttpStatus(response, status.HTTP_200_OK)
1529
+ self.assertEqual(
1530
+ len(response.data["results"]), len(self.unused_vids) - 1
1531
+ ) # initial unsued vids minus one created
1532
+
1533
+ def test_create_multiple_available_vlans_with_permissions_constraint(self):
1534
+ url = reverse("ipam-api:vlangroup-available-vlans", kwargs={"pk": self.vlan_group.pk})
1535
+ self.add_permissions(
1536
+ "ipam.view_vlangroup",
1537
+ "ipam.view_vlan",
1538
+ )
1539
+ self.add_permissions("ipam.add_vlan", constraints={"description__startswith": "This is the VLAN created for"})
1540
+
1541
+ # Test invalid request
1542
+ data = [
1543
+ {"name": "VLAN_6", "status": self.default_status.pk},
1544
+ {"name": "VLAN_7", "status": self.default_status.pk},
1545
+ {"name": "VLAN_8", "status": self.default_status.pk},
1546
+ ]
1547
+ response = self.client.post(url, data, format="json", **self.header)
1548
+ self.assertHttpStatus(response, status.HTTP_403_FORBIDDEN)
1549
+ self.assertIn("detail", response.data)
1550
+ self.assertEqual(response.data["detail"], "You do not have permission to perform this action.")
1551
+
1552
+ # Verify that no VLANs were created (number of VLANs is the same as on the beginning of the test)
1553
+ response = self.client.get(url, **self.header)
1554
+ self.assertHttpStatus(response, status.HTTP_200_OK)
1555
+ self.assertEqual(len(response.data["results"]), len(self.unused_vids))
1556
+
1557
+ # Test valid request
1558
+ valid_data = [
1559
+ {
1560
+ "name": "VLAN_6",
1561
+ "status": self.default_status.pk,
1562
+ "description": "This is the VLAN created for home automation.",
1563
+ },
1564
+ {
1565
+ "name": "VLAN_7",
1566
+ "status": self.default_status.pk,
1567
+ "description": "This is the VLAN created for IP cameras.",
1568
+ },
1569
+ {"name": "VLAN_8", "status": self.default_status.pk, "description": "This is the VLAN created for guests."},
1570
+ ]
1571
+ response = self.client.post(url, valid_data, format="json", **self.header)
1572
+ self.assertHttpStatus(response, status.HTTP_201_CREATED)
1573
+ self.assertHttpStatus(response, status.HTTP_201_CREATED)
1574
+ self.assertEqual(len(response.data["results"]), 3)
1575
+ for i, vlan_data in enumerate(data):
1576
+ self.assertEqual(response.data["results"][i]["name"], vlan_data["name"])
1577
+
1578
+ # Verify that VLANs are created
1579
+ response = self.client.get(url, **self.header)
1580
+ self.assertHttpStatus(response, status.HTTP_200_OK)
1581
+ self.assertEqual(
1582
+ len(response.data["results"]), len(self.unused_vids) - 3
1583
+ ) # initial unsued vids minus three created
1584
+
1252
1585
 
1253
1586
  class VLANTest(APIViewTestCases.APIViewTestCase):
1254
1587
  model = VLAN
@@ -13624,14 +13624,21 @@ This expects a field named <code>devices</code> on the model and a filter named
13624
13624
 
13625
13625
 
13626
13626
  <h3 id="nautobot.apps.testing.NautobotTestCaseMixin.add_permissions" class="doc doc-heading">
13627
- <code class="highlight language-python"><span class="n">add_permissions</span><span class="p">(</span><span class="o">*</span><span class="n">names</span><span class="p">)</span></code>
13627
+ <code class="highlight language-python"><span class="n">add_permissions</span><span class="p">(</span><span class="o">*</span><span class="n">names</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">)</span></code>
13628
13628
 
13629
13629
  <a href="#nautobot.apps.testing.NautobotTestCaseMixin.add_permissions" class="headerlink" title="Permanent link">&para;</a></h3>
13630
13630
 
13631
13631
 
13632
13632
  <div class="doc doc-contents ">
13633
13633
 
13634
- <p>Assign a set of permissions to the test user. Accepts permission names in the form <app>.<action>_<model>.</p>
13634
+ <p>Assign a set of permissions to the test user. Accepts permission names in the form <app>.<action>_<model>.
13635
+ Additional keyword arguments will be passed to the ObjectPermission constructor to allow creating more detailed permissions.</p>
13636
+
13637
+
13638
+ <p><span class="doc-section-title">Examples:</span></p>
13639
+ <div class="highlight"><pre><span></span><code><a id="__codelineno-0-1" name="__codelineno-0-1" href="#__codelineno-0-1"></a><span class="gp">&gt;&gt;&gt; </span><span class="n">add_permissions</span><span class="p">(</span><span class="s2">&quot;ipam.add_vlangroup&quot;</span><span class="p">,</span> <span class="s2">&quot;ipam.view_vlangroup&quot;</span><span class="p">)</span>
13640
+ <a id="__codelineno-0-2" name="__codelineno-0-2" href="#__codelineno-0-2"></a><span class="gp">&gt;&gt;&gt; </span><span class="n">add_permissions</span><span class="p">(</span><span class="s2">&quot;ipam.add_vlangroup&quot;</span><span class="p">,</span> <span class="s2">&quot;ipam.view_vlangroup&quot;</span><span class="p">,</span> <span class="n">constraints</span><span class="o">=</span><span class="p">{</span><span class="s2">&quot;pk&quot;</span><span class="p">:</span> <span class="s2">&quot;uuid-1234&quot;</span><span class="p">})</span>
13641
+ </code></pre></div>
13635
13642
 
13636
13643
  </div>
13637
13644