Coverage for users/models/permissions.py: 48%
65 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1from django.apps import apps
2from django.contrib.postgres.fields import ArrayField
3from django.db import models
4from django.urls import reverse
5from django.utils.translation import gettext_lazy as _
7from netbox.models.features import CloningMixin
8from netbox.registry import registry
9from users.constants import RESERVED_ACTIONS
10from utilities.querysets import RestrictedQuerySet
12__all__ = (
13 'ObjectPermission',
14)
17class ObjectPermission(CloningMixin, models.Model):
18 """
19 A mapping of view, add, change, and/or delete permission for users and/or groups to an arbitrary set of objects
20 identified by ORM query parameters.
21 """
22 name = models.CharField(
23 verbose_name=_('name'),
24 max_length=100
25 )
26 description = models.CharField(
27 verbose_name=_('description'),
28 max_length=200,
29 blank=True
30 )
31 enabled = models.BooleanField(
32 verbose_name=_('enabled'),
33 default=True
34 )
35 object_types = models.ManyToManyField(
36 to='contenttypes.ContentType',
37 related_name='object_permissions'
38 )
39 actions = ArrayField(
40 base_field=models.CharField(max_length=30),
41 help_text=_("The list of actions granted by this permission")
42 )
43 constraints = models.JSONField(
44 blank=True,
45 null=True,
46 verbose_name=_('constraints'),
47 help_text=_("Queryset filter matching the applicable objects of the selected type(s)")
48 )
50 clone_fields = (
51 'description', 'enabled', 'object_types', 'actions', 'constraints',
52 )
54 objects = RestrictedQuerySet.as_manager()
56 class Meta:
57 ordering = ['name']
58 indexes = (
59 models.Index(fields=('name',)), # Default ordering
60 )
61 verbose_name = _('permission')
62 verbose_name_plural = _('permissions')
64 def __str__(self):
65 return self.name
67 @property
68 def can_view(self):
69 return 'view' in self.actions
71 @property
72 def can_add(self):
73 return 'add' in self.actions
75 @property
76 def can_change(self):
77 return 'change' in self.actions
79 @property
80 def can_delete(self):
81 return 'delete' in self.actions
83 def list_constraints(self):
84 """
85 Return all constraint sets as a list (even if only a single set is defined).
86 """
87 if type(self.constraints) is not list:
88 return [self.constraints]
89 return self.constraints
91 def get_registered_actions(self):
92 """
93 Return a list of dicts for all registered actions:
94 name: The action identifier
95 help_text: Human-friendly description (first registration wins)
96 enabled: Whether this action is enabled on this permission
97 models: Sorted list of human-friendly model verbose names
98 """
99 enabled_actions = set(self.actions) - set(RESERVED_ACTIONS)
101 action_info = {}
102 action_models = {}
103 for model_key, model_actions in registry['model_actions'].items():
104 app_label, model_name = model_key.split('.')
105 try:
106 verbose_name = str(apps.get_model(app_label, model_name)._meta.verbose_name)
107 except LookupError:
108 verbose_name = model_key
109 for action in model_actions:
110 # First registration's help_text wins for shared action names
111 if action.name not in action_info:
112 action_info[action.name] = action
113 action_models.setdefault(action.name, []).append(verbose_name)
115 return [
116 {
117 'name': name,
118 'help_text': action_info[name].help_text,
119 'enabled': name in enabled_actions,
120 'models': sorted(action_models[name]),
121 }
122 for name in sorted(action_models)
123 ]
125 def get_additional_actions(self):
126 """
127 Return a sorted list of actions that are neither CRUD nor registered.
128 These are manually-entered actions from the "Additional actions" field.
129 """
130 registered_names = set()
131 for model_actions in registry['model_actions'].values():
132 for action in model_actions:
133 registered_names.add(action.name)
135 return sorted(
136 a for a in self.actions
137 if a not in RESERVED_ACTIONS and a not in registered_names
138 )
140 def get_absolute_url(self):
141 return reverse('users:objectpermission', args=[self.pk])