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.
- drf_iam/utils/load_viewset_permissions.py +130 -38
- {drf_iam-0.2.6.dist-info → drf_iam-0.2.8.dist-info}/METADATA +1 -1
- {drf_iam-0.2.6.dist-info → drf_iam-0.2.8.dist-info}/RECORD +5 -5
- {drf_iam-0.2.6.dist-info → drf_iam-0.2.8.dist-info}/WHEEL +0 -0
- {drf_iam-0.2.6.dist-info → drf_iam-0.2.8.dist-info}/top_level.txt +0 -0
@@ -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
|
-
|
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
|
-
|
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
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
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
|
-
|
123
|
-
|
124
|
-
for
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
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
|
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))
|
@@ -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=
|
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.
|
15
|
-
drf_iam-0.2.
|
16
|
-
drf_iam-0.2.
|
17
|
-
drf_iam-0.2.
|
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,,
|
File without changes
|
File without changes
|