Coverage for src/backend/InvenTree/users/permissions.py: 28%
73 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-07 17:47 +0000
1"""Helper functions for user permission checks."""
3from typing import Optional
5from django.contrib.auth.models import User
6from django.db import models
7from django.db.models.query import Prefetch, QuerySet
9import InvenTree.cache
10from users.ruleset import RULESET_CHANGE_INHERIT, get_ruleset_ignore, get_ruleset_models
13def split_model(model_label: str) -> tuple[str, str]:
14 """Split a model string into its component parts.
16 Arguments:
17 model_label: The model class to check (e.g. 'part_partcategory')
19 Returns:
20 A tuple of the model and app names (e.g. ('partcategory', 'part'))
21 """
22 *app, model = model_label.split('_')
23 app = '_'.join(app) if len(app) > 1 else app[0]
24 return model, app
27def get_model_permission_string(model: models.Model, permission: str) -> str:
28 """Generate a permission string for a given model and permission type.
30 Arguments:
31 model: The model class to check
32 permission: The permission to check (e.g. 'view' / 'delete')
34 Returns:
35 str: The permission string (e.g. 'part.view_part')
36 """
37 _model, _app = split_model(model)
38 return f'{_app}.{permission}_{_model}'
41def split_permission(app: str, perm: str) -> tuple[str, str]:
42 """Split the permission string into its component parts.
44 Arguments:
45 app: The application name (e.g. 'part')
46 perm: The permission string (e.g. 'view_part' / 'delete_partcategory')
48 Returns:
49 A tuple of the permission and model names
50 """
51 permission_name, *model = perm.split('_')
53 # Handle models that have underscores
54 if len(model) > 1: # pragma: no cover
55 app += '_' + '_'.join(model[:-1])
56 perm = permission_name + '_' + model[-1:][0]
57 model = model[-1:][0]
58 return perm, model
61def prefetch_rule_sets(user) -> QuerySet:
62 """Return a queryset of groups with prefetched rule sets for the given user.
64 Arguments:
65 user: The user object
67 Returns:
68 QuerySet: The queryset of groups with prefetched rule sets
69 """
70 return user.groups.all().prefetch_related(
71 Prefetch('rule_sets', to_attr='prefetched_rule_sets')
72 )
75def check_user_role(
76 user: User,
77 role: str,
78 permission: str,
79 allow_inactive: bool = False,
80 groups: Optional[QuerySet] = None,
81) -> bool:
82 """Check if a user has a particular role:permission combination.
84 Arguments:
85 user: The user object to check
86 role: The role to check (e.g. 'part' / 'stock')
87 permission: The permission to check (e.g. 'view' / 'delete')
88 allow_inactive: If False, disallow inactive users from having permissions
89 groups: Optional cached queryset of groups to check (defaults to user's groups)
91 Returns:
92 bool: True if the user has the specified role:permission combination
94 Note: As this check may be called frequently, we cache the result in the session cache.
95 """
96 if not user: 96 ↛ 97line 96 didn't jump to line 97 because the condition on line 96 was never true
97 return False
99 if not user.is_active and not allow_inactive: 99 ↛ 100line 99 didn't jump to line 100 because the condition on line 99 was never true
100 return False
102 if user.is_superuser: 102 ↛ 106line 102 didn't jump to line 106 because the condition on line 102 was always true
103 return True
105 # First, check the session cache
106 cache_key = f'role_{user.pk}_{role}_{permission}'
107 result = InvenTree.cache.get_session_cache(cache_key)
109 if result is not None:
110 return result
112 # Default for no match
113 result = False
115 groups = groups or prefetch_rule_sets(user)
117 for group in groups:
118 for rule in group.prefetched_rule_sets:
119 if rule.name == role:
120 # Check if the rule has the specified permission
121 # e.g. "view" role maps to "can_view" attribute
122 if getattr(rule, f'can_{permission}', False):
123 result = True
124 break
126 # Save result to session-cache
127 InvenTree.cache.set_session_cache(cache_key, result)
129 return result
132def check_user_permission(
133 user: User,
134 model: models.Model,
135 permission: str,
136 allow_inactive: bool = False,
137 groups: Optional[QuerySet] = None,
138) -> bool:
139 """Check if the user has a particular permission against a given model type.
141 Arguments:
142 user: The user object to check
143 model: The model class to check (e.g. 'part')
144 permission: The permission to check (e.g. 'view' / 'delete')
145 allow_inactive: If False, disallow inactive users from having permissions
146 groups: Optional cached queryset of groups to check (defaults to user's groups)
148 Returns:
149 bool: True if the user has the specified permission
151 Note: As this check may be called frequently, we cache the result in the session cache.
152 """
153 if not user: 153 ↛ 154line 153 didn't jump to line 154 because the condition on line 153 was never true
154 return False
156 if not user.is_active and not allow_inactive: 156 ↛ 157line 156 didn't jump to line 157 because the condition on line 156 was never true
157 return False
159 if user.is_superuser: 159 ↛ 162line 159 didn't jump to line 162 because the condition on line 159 was always true
160 return True
162 table_name = f'{model._meta.app_label}_{model._meta.model_name}'
164 # Particular table does not require specific permissions
165 if table_name in get_ruleset_ignore():
166 return True
168 groups = groups or prefetch_rule_sets(user)
170 for role, table_names in get_ruleset_models().items():
171 if table_name in table_names:
172 if check_user_role(user, role, permission, groups=groups):
173 return True
175 # Check for children models which inherits from parent role
176 for parent, child in RULESET_CHANGE_INHERIT:
177 # Get child model name
178 parent_child_string = f'{parent}_{child}'
180 if parent_child_string == table_name:
181 # Check if parent role has change permission
182 if check_user_role(user, parent, 'change', groups=groups):
183 return True
185 # Generate the permission name based on the model and permission
186 # e.g. 'part.view_part'
187 permission_name = f'{model._meta.app_label}.{permission}_{model._meta.model_name}'
189 # First, check the session cache
190 cache_key = f'permission_{user.pk}_{permission_name}'
191 result = InvenTree.cache.get_session_cache(cache_key)
193 if result is not None:
194 return result
196 result = user.has_perm(permission_name)
198 # If the user does not have permissions (as determined above), check if the model class provides a custom permission check method
199 # This is required for non-standard models (i.e. defined via plugins), which do not have the required ruleset definitions
200 if not result and hasattr(model, 'check_user_permission'): # pragma: no cover
201 result = model.check_user_permission(user, permission)
203 # Save result to session-cache
204 InvenTree.cache.set_session_cache(cache_key, result)
206 return result