drf-iam 0.2.5__py3-none-any.whl → 0.2.7__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/migrations/0002_add_policy_name.py +1 -1
- drf_iam/models.py +1 -0
- drf_iam/utils/load_viewset_permissions.py +124 -37
- {drf_iam-0.2.5.dist-info → drf_iam-0.2.7.dist-info}/METADATA +1 -1
- {drf_iam-0.2.5.dist-info → drf_iam-0.2.7.dist-info}/RECORD +7 -7
- {drf_iam-0.2.5.dist-info → drf_iam-0.2.7.dist-info}/WHEEL +0 -0
- {drf_iam-0.2.5.dist-info → drf_iam-0.2.7.dist-info}/top_level.txt +0 -0
drf_iam/models.py
CHANGED
@@ -12,6 +12,7 @@ class Role(models.Model):
|
|
12
12
|
|
13
13
|
|
14
14
|
class Policy(models.Model):
|
15
|
+
policy_name = models.CharField(max_length=255, blank=True, null=True)
|
15
16
|
action = models.CharField(max_length=100)
|
16
17
|
resource_type = models.CharField(max_length=100)
|
17
18
|
description = models.TextField(blank=True)
|
@@ -6,6 +6,7 @@ from typing import List, Dict, Any, Set, Tuple, Iterator, Type, Optional
|
|
6
6
|
from django.conf import settings
|
7
7
|
from django.urls import get_resolver, URLPattern, URLResolver
|
8
8
|
from rest_framework.viewsets import ViewSetMixin
|
9
|
+
from rest_framework.views import APIView
|
9
10
|
|
10
11
|
from drf_iam.models import Policy
|
11
12
|
from .logging_utils import setup_colorful_logger
|
@@ -33,7 +34,11 @@ 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
|
+
def is_api_view(cls: Any) -> bool:
|
40
|
+
"""Checks if the given class is a DRF APIView."""
|
41
|
+
return isinstance(cls, type) and issubclass(cls, APIView)
|
37
42
|
|
38
43
|
|
39
44
|
def get_viewset_actions(viewset_cls: Type[ViewSetMixin]) -> Set[str]:
|
@@ -79,61 +84,143 @@ class PermissionLoader:
|
|
79
84
|
elif isinstance(pattern, URLPattern):
|
80
85
|
callback = pattern.callback
|
81
86
|
viewset_class = getattr(callback, 'cls', None)
|
82
|
-
if is_viewset(viewset_class):
|
87
|
+
if is_viewset(viewset_class) or is_api_view(viewset_class):
|
83
88
|
yield {
|
84
89
|
'prefix': prefix,
|
85
90
|
'pattern': pattern.pattern,
|
86
91
|
'viewset': viewset_class,
|
87
92
|
'callback': callback,
|
93
|
+
'name': pattern.name
|
88
94
|
}
|
89
95
|
|
96
|
+
def _validate_conflicting_definitions(
|
97
|
+
self,
|
98
|
+
action_method: Any,
|
99
|
+
action_name: str,
|
100
|
+
iam_perms_for_action: Dict[str, Any]
|
101
|
+
) -> None:
|
102
|
+
"""
|
103
|
+
Validates that IAM-related attributes are not defined in conflicting ways
|
104
|
+
(e.g., both in drf_iam_permissions dict and as a direct method attribute).
|
105
|
+
|
106
|
+
Raises:
|
107
|
+
Exception: If any attribute is defined in both locations.
|
108
|
+
"""
|
109
|
+
conflicts: List[str] = []
|
110
|
+
error_message_template = (
|
111
|
+
"Attribute '{}' for action '{}' on ViewSet '{}' cannot be defined in both "
|
112
|
+
"'drf_iam_permissions' dictionary and as a direct method attribute (e.g., via decorator). "
|
113
|
+
"Please choose a single source of truth."
|
114
|
+
)
|
115
|
+
|
116
|
+
viewset_name = action_method.__self__.__class__.__name__ if hasattr(action_method, '__self__') else 'UnknownViewSet'
|
117
|
+
|
118
|
+
# Check for 'policy_name'
|
119
|
+
if iam_perms_for_action.get("policy_name") and hasattr(action_method, 'policy_name'):
|
120
|
+
conflicts.append(error_message_template.format('policy_name', action_name, viewset_name))
|
121
|
+
|
122
|
+
# Check for 'policy_description' (key 'description' in dict, attribute 'policy_description' on method)
|
123
|
+
if iam_perms_for_action.get("description") and hasattr(action_method, 'policy_description'):
|
124
|
+
conflicts.append(error_message_template.format('policy_description (dict key "description")', action_name, viewset_name))
|
125
|
+
|
126
|
+
# Check for 'exclude_from_iam'
|
127
|
+
if iam_perms_for_action.get("exclude_from_iam") is not None and \
|
128
|
+
hasattr(action_method, 'exclude_from_iam'):
|
129
|
+
conflicts.append(error_message_template.format('exclude_from_iam', action_name, viewset_name))
|
130
|
+
|
131
|
+
if conflicts:
|
132
|
+
raise Exception("\n".join(conflicts))
|
133
|
+
|
90
134
|
def _generate_policy_details_from_viewsets(self) -> None:
|
91
135
|
"""Generates a list of desired PolicyDetail objects from URL patterns."""
|
92
136
|
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
|
137
|
+
resource_type_name_cache: Dict[Type[ViewSetMixin], str] = {}
|
101
138
|
|
139
|
+
for viewset_info in self._extract_viewsets_from_urlpatterns(self.urlpatterns):
|
140
|
+
viewset_cls = viewset_info['callback'].cls
|
102
141
|
actions = get_viewset_actions(viewset_cls)
|
142
|
+
resource_name_from_path = viewset_info['name'] # Used if iam_resource_type_name not set
|
143
|
+
|
144
|
+
# Cache resource type name
|
145
|
+
if viewset_cls not in resource_type_name_cache:
|
146
|
+
resource_type_name_cache[viewset_cls] = getattr(
|
147
|
+
viewset_cls,
|
148
|
+
"iam_resource_type_name",
|
149
|
+
resource_name_from_path.replace('-', '_') # Default from URL name
|
150
|
+
)
|
151
|
+
resource_type_name = resource_type_name_cache[viewset_cls]
|
152
|
+
|
153
|
+
drf_iam_permissions = getattr(viewset_cls, "drf_iam_permissions", {})
|
103
154
|
|
104
155
|
for action_name in actions:
|
105
156
|
action_method = getattr(viewset_cls, action_name, None)
|
106
|
-
|
107
|
-
description = f"Permission for {action_name} on {basename}"
|
157
|
+
iam_perms_for_action = drf_iam_permissions.get(action_name, {})
|
108
158
|
|
109
|
-
if action_method:
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
159
|
+
if action_method: # Ensure the method actually exists
|
160
|
+
self._validate_conflicting_definitions(
|
161
|
+
action_method,
|
162
|
+
action_name,
|
163
|
+
iam_perms_for_action
|
164
|
+
)
|
114
165
|
|
166
|
+
# Determine if the action should be excluded
|
167
|
+
# Precedence: method attribute > dict setting > default (False)
|
168
|
+
excluded = False
|
169
|
+
if hasattr(action_method, 'exclude_from_iam'):
|
170
|
+
excluded = getattr(action_method, 'exclude_from_iam')
|
171
|
+
elif "exclude_from_iam" in iam_perms_for_action:
|
172
|
+
excluded = iam_perms_for_action.get("exclude_from_iam", False)
|
173
|
+
|
174
|
+
if excluded:
|
175
|
+
logger.debug(
|
176
|
+
f"Action '{action_name}' on ViewSet "
|
177
|
+
f"'{viewset_cls.__name__}' is excluded from IAM policies."
|
178
|
+
)
|
179
|
+
continue
|
180
|
+
|
181
|
+
# Determine policy_name
|
182
|
+
# Precedence: method attribute > dict setting > default generated name
|
183
|
+
policy_name_str: str
|
184
|
+
if hasattr(action_method, 'policy_name'):
|
185
|
+
policy_name_str = getattr(action_method, 'policy_name')
|
186
|
+
elif "policy_name" in iam_perms_for_action:
|
187
|
+
policy_name_str = iam_perms_for_action["policy_name"]
|
188
|
+
else:
|
189
|
+
policy_name_str = f"{action_name.replace('_', ' ').title()} {resource_type_name.replace('_', ' ').title()}"
|
190
|
+
|
191
|
+
# Determine description
|
192
|
+
# Precedence: method attribute 'policy_description' > dict setting 'description' > default generated description
|
193
|
+
description_str: str
|
194
|
+
if hasattr(action_method, 'policy_description'):
|
195
|
+
description_str = getattr(action_method, 'policy_description')
|
196
|
+
elif "description" in iam_perms_for_action:
|
197
|
+
description_str = iam_perms_for_action["description"]
|
198
|
+
else:
|
199
|
+
description_str = f"Allows to {action_name.replace('_', ' ')} for {resource_type_name.replace('_', ' ')}"
|
200
|
+
|
115
201
|
raw_policy_details.append({
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
202
|
+
"action": action_name, # Just the action name
|
203
|
+
"resource_type": resource_type_name,
|
204
|
+
"policy_name": policy_name_str,
|
205
|
+
"description": description_str,
|
120
206
|
})
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
for
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
207
|
+
|
208
|
+
# Remove duplicates and create PolicyDetail objects
|
209
|
+
# Ensures that if multiple URL patterns point to the same viewset action,
|
210
|
+
# we only consider one policy detail for it based on action and resource_type.
|
211
|
+
unique_policy_details_set = {
|
212
|
+
(p["action"], p["resource_type"]): p for p in raw_policy_details
|
213
|
+
}.values()
|
214
|
+
|
215
|
+
self.desired_policies = [
|
216
|
+
PolicyDetail(
|
217
|
+
action=p["action"],
|
218
|
+
resource_type=p["resource_type"],
|
219
|
+
policy_name=p["policy_name"],
|
220
|
+
description=p["description"],
|
221
|
+
)
|
222
|
+
for p in unique_policy_details_set
|
223
|
+
]
|
137
224
|
|
138
225
|
def _get_current_policies_from_db(self) -> None:
|
139
226
|
"""Fetches all current policies from the database."""
|
@@ -2,16 +2,16 @@ drf_iam/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
drf_iam/admin.py,sha256=xH4T6xGKuylRCTwqLPJAx3Eb07Mb0Qb43iYAWX_cSN8,788
|
3
3
|
drf_iam/apps.py,sha256=FX7qTVZz5ptxYC2RHH1c5YSgAP94IR_vjy0I56Utjgc,3184
|
4
4
|
drf_iam/decorators.py,sha256=11NEWGQ4cOZiH7gj-clLKi9M929gGtl1ZnaqEbVvhOc,1040
|
5
|
-
drf_iam/models.py,sha256=
|
5
|
+
drf_iam/models.py,sha256=mCjaIgAKBbkM7l195XVQ3V_yztZqLo5-8LPXffMi_tw,1370
|
6
6
|
drf_iam/permissions.py,sha256=kUhB7V48sb4v3uZI2lWv40hvI85nE61rL-amh6OY8mg,958
|
7
7
|
drf_iam/tests.py,sha256=mrbGGRNg5jwbTJtWWa7zSKdDyeB4vmgZCRc2nk6VY-g,60
|
8
8
|
drf_iam/migrations/0001_initial.py,sha256=y_4jXnr7gjU4UXxVrgVrStTSFu3h1ZrmjEZDL4FtZa4,3086
|
9
|
-
drf_iam/migrations/0002_add_policy_name.py,sha256=
|
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=z_0cx3UlcDs-n22wHPF5shseacf7m6Ieip9_HpZoPfc,15047
|
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.7.dist-info/METADATA,sha256=CS4IBDCUkFiuLuTf9rV1cHjDl2pEAP5seACWLgHVLR8,2763
|
15
|
+
drf_iam-0.2.7.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
|
16
|
+
drf_iam-0.2.7.dist-info/top_level.txt,sha256=daz6AaQ9e_cfCjLk2aRoLb_PCOoFofYUX4DU85VwHSM,8
|
17
|
+
drf_iam-0.2.7.dist-info/RECORD,,
|
File without changes
|
File without changes
|