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.
@@ -11,6 +11,6 @@ class Migration(migrations.Migration):
11
11
  migrations.AddField(
12
12
  model_name="policy",
13
13
  name="policy_name",
14
- field=models.CharField(blank=True, max_length=100, null=True),
14
+ field=models.CharField(blank=True, max_length=255, null=True),
15
15
  ),
16
16
  ]
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
- 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
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
- policy_name_override = action_name
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
- 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)
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
- 'action': f"{basename}:{action_name}",
117
- 'resource_type': basename,
118
- 'policy_name': policy_name_override,
119
- 'description': description
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
- 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
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."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: drf-iam
3
- Version: 0.2.5
3
+ Version: 0.2.7
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
@@ -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=3K0f0SKWiiMFKbiLQ_SKrwV-B5jzzm-HH2gO8nOGs54,1296
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=WxQczamrefP6lUxtOU425CK1FwLzcm4lapL-2aYSE6c,353
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=z_0cx3UlcDs-n22wHPF5shseacf7m6Ieip9_HpZoPfc,15047
13
13
  drf_iam/utils/logging_utils.py,sha256=9I9hhSVgOVW5xdKXSKofbjgl6rMYbmAI40euiB5WNlM,2074
14
- drf_iam-0.2.5.dist-info/METADATA,sha256=HbyQy2MXh3L64jkKQk5W2EEdkqLPpna1JOtgQNsjDc8,2763
15
- drf_iam-0.2.5.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
16
- drf_iam-0.2.5.dist-info/top_level.txt,sha256=daz6AaQ9e_cfCjLk2aRoLb_PCOoFofYUX4DU85VwHSM,8
17
- drf_iam-0.2.5.dist-info/RECORD,,
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,,