drf-iam 0.2.6__py3-none-any.whl → 0.2.8__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.
@@ -5,6 +5,7 @@ from typing import List, Dict, Any, Set, Tuple, Iterator, Type, Optional
5
5
 
6
6
  from django.conf import settings
7
7
  from django.urls import get_resolver, URLPattern, URLResolver
8
+ from rest_framework.views import APIView
8
9
  from rest_framework.viewsets import ViewSetMixin
9
10
 
10
11
  from drf_iam.models import Policy
@@ -33,7 +34,12 @@ class PolicyDetail:
33
34
 
34
35
  def is_viewset(cls: Any) -> bool:
35
36
  """Checks if the given class is a DRF ViewSet."""
36
- return isinstance(cls, type) and issubclass(cls, ViewSetMixin)
37
+ return isinstance(cls, type) and (issubclass(cls, ViewSetMixin))
38
+
39
+
40
+ def is_api_view(cls: Any) -> bool:
41
+ """Checks if the given class is a DRF APIView."""
42
+ return isinstance(cls, type) and issubclass(cls, APIView)
37
43
 
38
44
 
39
45
  def get_viewset_actions(viewset_cls: Type[ViewSetMixin]) -> Set[str]:
@@ -42,7 +48,7 @@ def get_viewset_actions(viewset_cls: Type[ViewSetMixin]) -> Set[str]:
42
48
  Checks Django settings for 'DRF_IAM_DEFAULT_VIEWSET_ACTIONS'.
43
49
  """
44
50
  actions: Set[str] = set()
45
-
51
+
46
52
  fallback_default_actions = {
47
53
  'list', 'retrieve', 'create', 'update', 'partial_update', 'destroy'
48
54
  }
@@ -79,61 +85,146 @@ class PermissionLoader:
79
85
  elif isinstance(pattern, URLPattern):
80
86
  callback = pattern.callback
81
87
  viewset_class = getattr(callback, 'cls', None)
82
- if is_viewset(viewset_class):
88
+ if is_viewset(viewset_class) or is_api_view(viewset_class):
83
89
  yield {
84
90
  'prefix': prefix,
85
91
  'pattern': pattern.pattern,
86
92
  'viewset': viewset_class,
87
93
  'callback': callback,
94
+ 'name': pattern.name
88
95
  }
89
96
 
97
+ def _validate_conflicting_definitions(
98
+ self,
99
+ action_method: Any,
100
+ action_name: str,
101
+ iam_perms_for_action: Dict[str, Any]
102
+ ) -> None:
103
+ """
104
+ Validates that IAM-related attributes are not defined in conflicting ways
105
+ (e.g., both in drf_iam_permissions dict and as a direct method attribute).
106
+
107
+ Raises:
108
+ Exception: If any attribute is defined in both locations.
109
+ """
110
+ conflicts: List[str] = []
111
+ error_message_template = (
112
+ "Attribute '{}' for action '{}' on ViewSet '{}' cannot be defined in both "
113
+ "'drf_iam_permissions' dictionary and as a direct method attribute (e.g., via decorator). "
114
+ "Please choose a single source of truth."
115
+ )
116
+
117
+ viewset_name = action_method.__self__.__class__.__name__ if hasattr(action_method,
118
+ '__self__') else 'UnknownViewSet'
119
+
120
+ # Check for 'policy_name'
121
+ if iam_perms_for_action.get("policy_name") and hasattr(action_method, 'policy_name'):
122
+ conflicts.append(error_message_template.format('policy_name', action_name, viewset_name))
123
+
124
+ # Check for 'policy_description' (key 'description' in dict, attribute 'policy_description' on method)
125
+ if iam_perms_for_action.get("description") and hasattr(action_method, 'policy_description'):
126
+ conflicts.append(
127
+ error_message_template.format('policy_description (dict key "description")', action_name, viewset_name))
128
+
129
+ # Check for 'exclude_from_iam'
130
+ if iam_perms_for_action.get("exclude_from_iam") is not None and \
131
+ hasattr(action_method, 'exclude_from_iam'):
132
+ conflicts.append(error_message_template.format('exclude_from_iam', action_name, viewset_name))
133
+
134
+ if conflicts:
135
+ raise Exception("\n".join(conflicts))
136
+
90
137
  def _generate_policy_details_from_viewsets(self) -> None:
91
138
  """Generates a list of desired PolicyDetail objects from URL patterns."""
92
139
  raw_policy_details: List[Dict[str, Any]] = []
93
- for entry in self._extract_viewsets_from_urlpatterns(self.urlpatterns):
94
- viewset_cls: Type[ViewSetMixin] = entry['viewset']
95
- basename = getattr(viewset_cls, "iam_policy_name", None) or \
96
- viewset_cls.__name__.lower().replace('viewset', '')
97
-
98
- exclude_from_permissions = getattr(viewset_cls, "drf_iam_exclude_from_permissions", False)
99
- if exclude_from_permissions:
100
- continue
140
+ resource_type_name_cache: Dict[Type[ViewSetMixin], str] = {}
101
141
 
142
+ for viewset_info in self._extract_viewsets_from_urlpatterns(self.urlpatterns):
143
+ viewset_cls = viewset_info['callback'].cls
102
144
  actions = get_viewset_actions(viewset_cls)
103
145
 
146
+ # Cache resource type name
147
+ if viewset_cls not in resource_type_name_cache:
148
+ resource_type_name_cache[viewset_cls] = getattr(
149
+ viewset_cls,
150
+ "iam_policy_name",
151
+ None
152
+ ) or viewset_cls.__name__.lower().replace('viewset',
153
+ '')
154
+
155
+ resource_type_name = resource_type_name_cache[viewset_cls]
156
+
157
+ drf_iam_permissions = getattr(viewset_cls, "drf_iam_permissions", {})
158
+
104
159
  for action_name in actions:
105
160
  action_method = getattr(viewset_cls, action_name, None)
106
- policy_name_override = action_name
107
- description = f"Permission for {action_name} on {basename}"
161
+ iam_perms_for_action = drf_iam_permissions.get(action_name, {})
108
162
 
109
- if action_method:
110
- if getattr(action_method, 'exclude_from_iam', False):
111
- continue
112
- policy_name_override = getattr(action_method, 'policy_name', policy_name_override)
113
- description = getattr(action_method, 'policy_description', description)
163
+ if action_method: # Ensure the method actually exists
164
+ self._validate_conflicting_definitions(
165
+ action_method,
166
+ action_name,
167
+ iam_perms_for_action
168
+ )
169
+
170
+ # Determine if the action should be excluded
171
+ # Precedence: method attribute > dict setting > default (False)
172
+ excluded = False
173
+ if hasattr(action_method, 'exclude_from_iam'):
174
+ excluded = getattr(action_method, 'exclude_from_iam')
175
+ elif "exclude_from_iam" in iam_perms_for_action:
176
+ excluded = iam_perms_for_action.get("exclude_from_iam", False)
177
+
178
+ if excluded:
179
+ logger.debug(
180
+ f"Action '{action_name}' on ViewSet "
181
+ f"'{viewset_cls.__name__}' is excluded from IAM policies."
182
+ )
183
+ continue
184
+
185
+ # Determine policy_name
186
+ # Precedence: method attribute > dict setting > default generated name
187
+ policy_name_str: str
188
+ if hasattr(action_method, 'policy_name'):
189
+ policy_name_str = getattr(action_method, 'policy_name')
190
+ elif "policy_name" in iam_perms_for_action:
191
+ policy_name_str = iam_perms_for_action["policy_name"]
192
+ else:
193
+ policy_name_str = f"{action_name.replace('_', ' ').title()} {resource_type_name.replace('_', ' ').title()}"
194
+
195
+ # Determine description
196
+ # Precedence: method attribute 'policy_description' > dict setting 'description' > default generated description
197
+ description_str: str
198
+ if hasattr(action_method, 'policy_description'):
199
+ description_str = getattr(action_method, 'policy_description')
200
+ elif "description" in iam_perms_for_action:
201
+ description_str = iam_perms_for_action["description"]
202
+ else:
203
+ description_str = f"Allows to {action_name.replace('_', ' ')} for {resource_type_name.replace('_', ' ')}"
114
204
 
115
205
  raw_policy_details.append({
116
- 'action': f"{basename}:{action_name}",
117
- 'resource_type': basename,
118
- 'policy_name': policy_name_override,
119
- 'description': description
206
+ "action": action_name, # Just the action name
207
+ "resource_type": resource_type_name,
208
+ "policy_name": policy_name_str,
209
+ "description": description_str,
120
210
  })
121
211
 
122
- seen_policies: Set[Tuple[str, str]] = set()
123
- unique_policy_details: List[PolicyDetail] = []
124
- for detail_dict in raw_policy_details:
125
- key = (detail_dict['action'], detail_dict['resource_type'])
126
- if key not in seen_policies:
127
- seen_policies.add(key)
128
- unique_policy_details.append(
129
- PolicyDetail(
130
- action=detail_dict['action'],
131
- resource_type=detail_dict['resource_type'],
132
- policy_name=detail_dict['policy_name'],
133
- description=detail_dict['description'],
134
- )
135
- )
136
- self.desired_policies = unique_policy_details
212
+ # Remove duplicates and create PolicyDetail objects
213
+ # Ensures that if multiple URL patterns point to the same viewset action,
214
+ # we only consider one policy detail for it based on action and resource_type.
215
+ unique_policy_details_set = {
216
+ (p["action"], p["resource_type"]): p for p in raw_policy_details
217
+ }.values()
218
+
219
+ self.desired_policies = [
220
+ PolicyDetail(
221
+ action=p["action"],
222
+ resource_type=p["resource_type"],
223
+ policy_name=p["policy_name"],
224
+ description=p["description"],
225
+ )
226
+ for p in unique_policy_details_set
227
+ ]
137
228
 
138
229
  def _get_current_policies_from_db(self) -> None:
139
230
  """Fetches all current policies from the database."""
@@ -158,7 +249,8 @@ class PermissionLoader:
158
249
  to_create = list(desired_set - current_set)
159
250
 
160
251
  policies_to_delete_with_id: List[PolicyDetail] = []
161
- current_map_for_delete = {(p.action, p.resource_type): p.id for p in self.current_db_policies if p.id is not None}
252
+ current_map_for_delete = {(p.action, p.resource_type): p.id for p in self.current_db_policies if
253
+ p.id is not None}
162
254
  policies_to_delete_stubs = current_set - desired_set
163
255
  for policy_stub in policies_to_delete_stubs:
164
256
  policy_id = current_map_for_delete.get((policy_stub.action, policy_stub.resource_type))
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drf-iam
3
- Version: 0.2.6
3
+ Version: 0.2.8
4
4
  Summary: IAM-style roles and permissions for Django Rest Framework
5
5
  Home-page: https://github.com/tushar1328/drf-iam.git
6
6
  Author: Tushar Patel
@@ -9,9 +9,9 @@ drf_iam/migrations/0001_initial.py,sha256=y_4jXnr7gjU4UXxVrgVrStTSFu3h1ZrmjEZDL4
9
9
  drf_iam/migrations/0002_add_policy_name.py,sha256=EUZ2OCmobOlmnlpYv0jsMBb3QR0HknW4qAjgn-zOzA0,353
10
10
  drf_iam/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
11
  drf_iam/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- drf_iam/utils/load_viewset_permissions.py,sha256=NRjTekNgtuCiDbBH1hyttcW6nbchaBCD0fQnHALgYtI,10769
12
+ drf_iam/utils/load_viewset_permissions.py,sha256=HtEQqZ48NOtYtpyDNLg0eMJFkdImkEaq8YEMADD0I_c,15099
13
13
  drf_iam/utils/logging_utils.py,sha256=9I9hhSVgOVW5xdKXSKofbjgl6rMYbmAI40euiB5WNlM,2074
14
- drf_iam-0.2.6.dist-info/METADATA,sha256=F8YLkxqBELfDfnjaDMS6gO8_bRMpcLsgtfmSz-7MKS8,2763
15
- drf_iam-0.2.6.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
16
- drf_iam-0.2.6.dist-info/top_level.txt,sha256=daz6AaQ9e_cfCjLk2aRoLb_PCOoFofYUX4DU85VwHSM,8
17
- drf_iam-0.2.6.dist-info/RECORD,,
14
+ drf_iam-0.2.8.dist-info/METADATA,sha256=EUbV0JxfcIP6MI_CXjhbes7tR8euXBmzV8rapZgIWBA,2763
15
+ drf_iam-0.2.8.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
16
+ drf_iam-0.2.8.dist-info/top_level.txt,sha256=daz6AaQ9e_cfCjLk2aRoLb_PCOoFofYUX4DU85VwHSM,8
17
+ drf_iam-0.2.8.dist-info/RECORD,,