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

1"""Helper functions for user permission checks.""" 

2 

3from typing import Optional 

4 

5from django.contrib.auth.models import User 

6from django.db import models 

7from django.db.models.query import Prefetch, QuerySet 

8 

9import InvenTree.cache 

10from users.ruleset import RULESET_CHANGE_INHERIT, get_ruleset_ignore, get_ruleset_models 

11 

12 

13def split_model(model_label: str) -> tuple[str, str]: 

14 """Split a model string into its component parts. 

15 

16 Arguments: 

17 model_label: The model class to check (e.g. 'part_partcategory') 

18 

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 

25 

26 

27def get_model_permission_string(model: models.Model, permission: str) -> str: 

28 """Generate a permission string for a given model and permission type. 

29 

30 Arguments: 

31 model: The model class to check 

32 permission: The permission to check (e.g. 'view' / 'delete') 

33 

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}' 

39 

40 

41def split_permission(app: str, perm: str) -> tuple[str, str]: 

42 """Split the permission string into its component parts. 

43 

44 Arguments: 

45 app: The application name (e.g. 'part') 

46 perm: The permission string (e.g. 'view_part' / 'delete_partcategory') 

47 

48 Returns: 

49 A tuple of the permission and model names 

50 """ 

51 permission_name, *model = perm.split('_') 

52 

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 

59 

60 

61def prefetch_rule_sets(user) -> QuerySet: 

62 """Return a queryset of groups with prefetched rule sets for the given user. 

63 

64 Arguments: 

65 user: The user object 

66 

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 ) 

73 

74 

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. 

83 

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) 

90 

91 Returns: 

92 bool: True if the user has the specified role:permission combination 

93 

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 

98 

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 

101 

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 

104 

105 # First, check the session cache 

106 cache_key = f'role_{user.pk}_{role}_{permission}' 

107 result = InvenTree.cache.get_session_cache(cache_key) 

108 

109 if result is not None: 

110 return result 

111 

112 # Default for no match 

113 result = False 

114 

115 groups = groups or prefetch_rule_sets(user) 

116 

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 

125 

126 # Save result to session-cache 

127 InvenTree.cache.set_session_cache(cache_key, result) 

128 

129 return result 

130 

131 

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. 

140 

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) 

147 

148 Returns: 

149 bool: True if the user has the specified permission 

150 

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 

155 

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 

158 

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 

161 

162 table_name = f'{model._meta.app_label}_{model._meta.model_name}' 

163 

164 # Particular table does not require specific permissions 

165 if table_name in get_ruleset_ignore(): 

166 return True 

167 

168 groups = groups or prefetch_rule_sets(user) 

169 

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 

174 

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}' 

179 

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 

184 

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}' 

188 

189 # First, check the session cache 

190 cache_key = f'permission_{user.pk}_{permission_name}' 

191 result = InvenTree.cache.get_session_cache(cache_key) 

192 

193 if result is not None: 

194 return result 

195 

196 result = user.has_perm(permission_name) 

197 

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) 

202 

203 # Save result to session-cache 

204 InvenTree.cache.set_session_cache(cache_key, result) 

205 

206 return result