mage-ai 0.9.33__py3-none-any.whl → 0.9.34__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.

Potentially problematic release.


This version of mage-ai might be problematic. Click here for more details.

Files changed (153) hide show
  1. mage_ai/api/mixins/__init__.py +0 -0
  2. mage_ai/api/mixins/result_set.py +65 -0
  3. mage_ai/api/operations/base.py +8 -3
  4. mage_ai/api/policies/BasePolicy.py +127 -2
  5. mage_ai/api/policies/PermissionPolicy.py +5 -0
  6. mage_ai/api/policies/UserPolicy.py +25 -2
  7. mage_ai/api/policies/mixins/user_permissions.py +0 -9
  8. mage_ai/api/presenters/PermissionPresenter.py +2 -0
  9. mage_ai/api/presenters/RolePresenter.py +15 -0
  10. mage_ai/api/presenters/UserPresenter.py +9 -15
  11. mage_ai/api/resources/PermissionResource.py +40 -3
  12. mage_ai/api/resources/RoleResource.py +1 -0
  13. mage_ai/api/resources/UserResource.py +35 -4
  14. mage_ai/orchestration/db/models/oauth.py +126 -0
  15. mage_ai/server/constants.py +1 -1
  16. mage_ai/server/frontend_dist/404.html +2 -2
  17. mage_ai/server/frontend_dist/_next/static/PBVuphyo_muEAj347ZP_b/_buildManifest.js +1 -0
  18. mage_ai/server/frontend_dist/_next/static/chunks/{3859-3501cdba0a33f9f2.js → 3859-ba594d21a1260cd2.js} +1 -1
  19. mage_ai/server/{frontend_dist_base_path_template/_next/static/chunks/7022-a0fb5af2ebd7bb45.js → frontend_dist/_next/static/chunks/7022-80d082a1d7fd1234.js} +1 -1
  20. mage_ai/server/frontend_dist/_next/static/chunks/7361-25f211ef377e5958.js +1 -0
  21. mage_ai/server/frontend_dist/_next/static/chunks/8146-941c5155c3bfcc35.js +1 -0
  22. mage_ai/server/frontend_dist/_next/static/chunks/9264-5730e4e059db40a8.js +1 -0
  23. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/edit-d7fd4857579e2b00.js +1 -0
  24. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/permissions/[...slug]-1a95628ea8d0d846.js +1 -0
  25. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/permissions-cb1cdf5f8e5bf9c5.js +1 -0
  26. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/users/[...slug]-5061c073e1c0de07.js +1 -0
  27. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/users-f551c5665bfd3494.js +1 -0
  28. mage_ai/server/frontend_dist/block-layout.html +2 -2
  29. mage_ai/server/frontend_dist/files.html +5 -5
  30. mage_ai/server/frontend_dist/global-data-products/[...slug].html +5 -5
  31. mage_ai/server/frontend_dist/global-data-products.html +5 -5
  32. mage_ai/server/frontend_dist/index.html +2 -2
  33. mage_ai/server/frontend_dist/manage/settings.html +5 -5
  34. mage_ai/server/frontend_dist/manage/users/[user].html +5 -5
  35. mage_ai/server/frontend_dist/manage/users/new.html +5 -5
  36. mage_ai/server/frontend_dist/manage/users.html +5 -5
  37. mage_ai/server/frontend_dist/manage.html +5 -5
  38. mage_ai/server/frontend_dist/overview.html +5 -5
  39. mage_ai/server/frontend_dist/pipeline-runs.html +5 -5
  40. mage_ai/server/frontend_dist/pipelines/[pipeline]/backfills/[...slug].html +5 -5
  41. mage_ai/server/frontend_dist/pipelines/[pipeline]/backfills.html +5 -5
  42. mage_ai/server/frontend_dist/pipelines/[pipeline]/dashboard.html +5 -5
  43. mage_ai/server/frontend_dist/pipelines/[pipeline]/edit.html +2 -2
  44. mage_ai/server/frontend_dist/pipelines/[pipeline]/logs.html +5 -5
  45. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors/block-runs.html +5 -5
  46. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors/block-runtime.html +5 -5
  47. mage_ai/server/frontend_dist/pipelines/[pipeline]/monitors.html +5 -5
  48. mage_ai/server/frontend_dist/pipelines/[pipeline]/runs/[run].html +5 -5
  49. mage_ai/server/frontend_dist/pipelines/[pipeline]/runs.html +5 -5
  50. mage_ai/server/frontend_dist/pipelines/[pipeline]/settings.html +5 -5
  51. mage_ai/server/frontend_dist/pipelines/[pipeline]/syncs.html +5 -5
  52. mage_ai/server/frontend_dist/pipelines/[pipeline]/triggers/[...slug].html +5 -5
  53. mage_ai/server/frontend_dist/pipelines/[pipeline]/triggers.html +5 -5
  54. mage_ai/server/frontend_dist/pipelines/[pipeline].html +2 -2
  55. mage_ai/server/frontend_dist/pipelines.html +5 -5
  56. mage_ai/server/frontend_dist/settings/account/profile.html +5 -5
  57. mage_ai/server/frontend_dist/settings/workspace/permissions/[...slug].html +5 -5
  58. mage_ai/server/frontend_dist/settings/workspace/permissions.html +5 -5
  59. mage_ai/server/frontend_dist/settings/workspace/preferences.html +5 -5
  60. mage_ai/server/frontend_dist/settings/workspace/roles/[...slug].html +5 -5
  61. mage_ai/server/frontend_dist/settings/workspace/roles.html +5 -5
  62. mage_ai/server/frontend_dist/settings/workspace/sync-data.html +5 -5
  63. mage_ai/server/frontend_dist/settings/workspace/users/[...slug].html +5 -5
  64. mage_ai/server/frontend_dist/settings/workspace/users.html +5 -5
  65. mage_ai/server/frontend_dist/settings.html +2 -2
  66. mage_ai/server/frontend_dist/sign-in.html +10 -10
  67. mage_ai/server/frontend_dist/templates/[...slug].html +5 -5
  68. mage_ai/server/frontend_dist/templates.html +5 -5
  69. mage_ai/server/frontend_dist/terminal.html +5 -5
  70. mage_ai/server/frontend_dist/test.html +4 -4
  71. mage_ai/server/frontend_dist/triggers.html +5 -5
  72. mage_ai/server/frontend_dist/version-control.html +5 -5
  73. mage_ai/server/frontend_dist_base_path_template/404.html +2 -2
  74. mage_ai/server/frontend_dist_base_path_template/_next/static/L-IKw5_bRZUs-wyjnpN_j/_buildManifest.js +1 -0
  75. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/{3859-3501cdba0a33f9f2.js → 3859-ba594d21a1260cd2.js} +1 -1
  76. mage_ai/server/{frontend_dist/_next/static/chunks/7022-a0fb5af2ebd7bb45.js → frontend_dist_base_path_template/_next/static/chunks/7022-80d082a1d7fd1234.js} +1 -1
  77. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/7361-25f211ef377e5958.js +1 -0
  78. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/8146-941c5155c3bfcc35.js +1 -0
  79. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/9264-5730e4e059db40a8.js +1 -0
  80. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/edit-d7fd4857579e2b00.js +1 -0
  81. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/permissions/[...slug]-1a95628ea8d0d846.js +1 -0
  82. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/permissions-cb1cdf5f8e5bf9c5.js +1 -0
  83. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/users/[...slug]-5061c073e1c0de07.js +1 -0
  84. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/users-f551c5665bfd3494.js +1 -0
  85. mage_ai/server/frontend_dist_base_path_template/block-layout.html +2 -2
  86. mage_ai/server/frontend_dist_base_path_template/files.html +5 -5
  87. mage_ai/server/frontend_dist_base_path_template/global-data-products/[...slug].html +5 -5
  88. mage_ai/server/frontend_dist_base_path_template/global-data-products.html +5 -5
  89. mage_ai/server/frontend_dist_base_path_template/index.html +2 -2
  90. mage_ai/server/frontend_dist_base_path_template/manage/settings.html +5 -5
  91. mage_ai/server/frontend_dist_base_path_template/manage/users/[user].html +5 -5
  92. mage_ai/server/frontend_dist_base_path_template/manage/users/new.html +5 -5
  93. mage_ai/server/frontend_dist_base_path_template/manage/users.html +5 -5
  94. mage_ai/server/frontend_dist_base_path_template/manage.html +5 -5
  95. mage_ai/server/frontend_dist_base_path_template/overview.html +5 -5
  96. mage_ai/server/frontend_dist_base_path_template/pipeline-runs.html +5 -5
  97. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/backfills/[...slug].html +5 -5
  98. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/backfills.html +5 -5
  99. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/dashboard.html +5 -5
  100. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/edit.html +2 -2
  101. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/logs.html +5 -5
  102. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors/block-runs.html +5 -5
  103. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors/block-runtime.html +5 -5
  104. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/monitors.html +5 -5
  105. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/runs/[run].html +5 -5
  106. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/runs.html +5 -5
  107. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/settings.html +5 -5
  108. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/syncs.html +5 -5
  109. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/triggers/[...slug].html +5 -5
  110. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline]/triggers.html +5 -5
  111. mage_ai/server/frontend_dist_base_path_template/pipelines/[pipeline].html +2 -2
  112. mage_ai/server/frontend_dist_base_path_template/pipelines.html +5 -5
  113. mage_ai/server/frontend_dist_base_path_template/settings/account/profile.html +5 -5
  114. mage_ai/server/frontend_dist_base_path_template/settings/workspace/permissions/[...slug].html +5 -5
  115. mage_ai/server/frontend_dist_base_path_template/settings/workspace/permissions.html +5 -5
  116. mage_ai/server/frontend_dist_base_path_template/settings/workspace/preferences.html +5 -5
  117. mage_ai/server/frontend_dist_base_path_template/settings/workspace/roles/[...slug].html +5 -5
  118. mage_ai/server/frontend_dist_base_path_template/settings/workspace/roles.html +5 -5
  119. mage_ai/server/frontend_dist_base_path_template/settings/workspace/sync-data.html +5 -5
  120. mage_ai/server/frontend_dist_base_path_template/settings/workspace/users/[...slug].html +5 -5
  121. mage_ai/server/frontend_dist_base_path_template/settings/workspace/users.html +5 -5
  122. mage_ai/server/frontend_dist_base_path_template/settings.html +2 -2
  123. mage_ai/server/frontend_dist_base_path_template/sign-in.html +10 -10
  124. mage_ai/server/frontend_dist_base_path_template/templates/[...slug].html +5 -5
  125. mage_ai/server/frontend_dist_base_path_template/templates.html +5 -5
  126. mage_ai/server/frontend_dist_base_path_template/terminal.html +5 -5
  127. mage_ai/server/frontend_dist_base_path_template/test.html +4 -4
  128. mage_ai/server/frontend_dist_base_path_template/triggers.html +5 -5
  129. mage_ai/server/frontend_dist_base_path_template/version-control.html +5 -5
  130. {mage_ai-0.9.33.dist-info → mage_ai-0.9.34.dist-info}/METADATA +1 -1
  131. {mage_ai-0.9.33.dist-info → mage_ai-0.9.34.dist-info}/RECORD +137 -133
  132. mage_ai/server/frontend_dist/_next/static/chunks/8146-6f46ffd7cbe2fd07.js +0 -1
  133. mage_ai/server/frontend_dist/_next/static/chunks/9264-11c4f9e79f8a9fd5.js +0 -1
  134. mage_ai/server/frontend_dist/_next/static/chunks/pages/pipelines/[pipeline]/edit-68f1591d3f4262f1.js +0 -1
  135. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/permissions/[...slug]-672599b18c4eb2a6.js +0 -1
  136. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/permissions-b7bc84646325062c.js +0 -1
  137. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/users/[...slug]-b7b99c3d2109fdea.js +0 -1
  138. mage_ai/server/frontend_dist/_next/static/chunks/pages/settings/workspace/users-fe480da25b026bb5.js +0 -1
  139. mage_ai/server/frontend_dist/_next/static/sGWtg0x8VBmlqQeSOAw9E/_buildManifest.js +0 -1
  140. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/8146-6f46ffd7cbe2fd07.js +0 -1
  141. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/9264-11c4f9e79f8a9fd5.js +0 -1
  142. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/pipelines/[pipeline]/edit-68f1591d3f4262f1.js +0 -1
  143. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/permissions/[...slug]-672599b18c4eb2a6.js +0 -1
  144. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/permissions-b7bc84646325062c.js +0 -1
  145. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/users/[...slug]-b7b99c3d2109fdea.js +0 -1
  146. mage_ai/server/frontend_dist_base_path_template/_next/static/chunks/pages/settings/workspace/users-fe480da25b026bb5.js +0 -1
  147. mage_ai/server/frontend_dist_base_path_template/_next/static/dFD_SGZQKSOB83hTxog2E/_buildManifest.js +0 -1
  148. /mage_ai/server/frontend_dist/_next/static/{sGWtg0x8VBmlqQeSOAw9E → PBVuphyo_muEAj347ZP_b}/_ssgManifest.js +0 -0
  149. /mage_ai/server/frontend_dist_base_path_template/_next/static/{dFD_SGZQKSOB83hTxog2E → L-IKw5_bRZUs-wyjnpN_j}/_ssgManifest.js +0 -0
  150. {mage_ai-0.9.33.dist-info → mage_ai-0.9.34.dist-info}/LICENSE +0 -0
  151. {mage_ai-0.9.33.dist-info → mage_ai-0.9.34.dist-info}/WHEEL +0 -0
  152. {mage_ai-0.9.33.dist-info → mage_ai-0.9.34.dist-info}/entry_points.txt +0 -0
  153. {mage_ai-0.9.33.dist-info → mage_ai-0.9.34.dist-info}/top_level.txt +0 -0
File without changes
@@ -0,0 +1,65 @@
1
+ from typing import List
2
+
3
+ from mage_ai.orchestration.db import safe_db_query
4
+ from mage_ai.orchestration.db.models.oauth import (
5
+ Permission,
6
+ Role,
7
+ RolePermission,
8
+ UserRole,
9
+ )
10
+
11
+ CONTEXT_DATA_KEY_USER_PERMISSIONS = '__user_permissions'
12
+
13
+
14
+ class ResultSetMixIn:
15
+ @safe_db_query
16
+ async def load_and_cache_user_permissions(self) -> List[Permission]:
17
+ # This will fetch the user permissions and store it on the context data
18
+ # so that repeat policy user permission validations won’t keep querying the
19
+ # database for permissions.
20
+ if not self.current_user:
21
+ return
22
+
23
+ permissions = self.result_set().context.data.get(CONTEXT_DATA_KEY_USER_PERMISSIONS)
24
+ if permissions:
25
+ return permissions
26
+
27
+ query = (
28
+ Permission.
29
+ select(
30
+ Permission.access,
31
+ Permission.entity,
32
+ Permission.entity_id,
33
+ Permission.entity_name,
34
+ Permission.entity_type,
35
+ Permission.id,
36
+ Permission.options,
37
+ ).
38
+ join(
39
+ RolePermission,
40
+ RolePermission.permission_id == Permission.id).
41
+ join(
42
+ Role,
43
+ Role.id == RolePermission.role_id).
44
+ join(
45
+ UserRole,
46
+ UserRole.role_id == Role.id).
47
+ filter(UserRole.user_id == self.current_user.id)
48
+ )
49
+
50
+ permissions = []
51
+
52
+ for row in query.all():
53
+ permission = Permission()
54
+ permission.access = row.access
55
+ permission.entity = row.entity
56
+ permission.entity_id = row.entity_id
57
+ permission.entity_name = row.entity_name
58
+ permission.entity_type = row.entity_type
59
+ permission.id = row.id
60
+ permission.options = row.options
61
+ permissions.append(permission)
62
+
63
+ self.result_set().context.data[CONTEXT_DATA_KEY_USER_PERMISSIONS] = permissions
64
+
65
+ return permissions
@@ -27,6 +27,7 @@ from mage_ai.api.parsers.BaseParser import BaseParser
27
27
  from mage_ai.api.presenters.BasePresenter import CustomDict, CustomList
28
28
  from mage_ai.api.result_set import ResultSet
29
29
  from mage_ai.orchestration.db.errors import DoesNotExistError
30
+ from mage_ai.settings import REQUIRE_USER_PERMISSIONS
30
31
  from mage_ai.shared.array import flatten
31
32
  from mage_ai.shared.hash import ignore_keys, merge_dict
32
33
  from mage_ai.shared.strings import classify
@@ -170,9 +171,13 @@ class BaseOperation():
170
171
  }
171
172
  except ApiError as err:
172
173
  if err.code == 403 and \
173
- self.user and self.user.project_access == 0 and not self.user.roles:
174
- err.message = 'You do not have access to this project. ' + \
175
- 'Please ask an admin or owner for permissions.'
174
+ self.user and \
175
+ self.user.project_access == 0 and \
176
+ not self.user.roles:
177
+
178
+ if not REQUIRE_USER_PERMISSIONS:
179
+ err.message = 'You don’t have access to this project. ' + \
180
+ 'Please ask an admin or owner for permissions.'
176
181
  if settings.DEBUG:
177
182
  raise err
178
183
  else:
@@ -1,7 +1,7 @@
1
1
  import importlib
2
2
  import inspect
3
3
  from collections.abc import Iterable
4
- from typing import List, Tuple, Union
4
+ from typing import Callable, List, Tuple, Union
5
5
 
6
6
  import inflection
7
7
 
@@ -37,6 +37,10 @@ class BasePolicy(UserPermissionMixIn, ResultSetMixIn):
37
37
  query_rules = {}
38
38
  read_rules = {}
39
39
  write_rules = {}
40
+ override_permission_action_rules = {}
41
+ override_permission_query_rules = {}
42
+ override_permission_read_rules = {}
43
+ override_permission_write_rules = {}
40
44
 
41
45
  def __init__(self, resource, current_user, **kwargs):
42
46
  self.current_user = current_user
@@ -67,6 +71,12 @@ class BasePolicy(UserPermissionMixIn, ResultSetMixIn):
67
71
  self.action_rules[self.__name__] = {}
68
72
  return self.action_rules[self.__name__].get(action)
69
73
 
74
+ @classmethod
75
+ def override_permission_action_rule(self, action):
76
+ if not self.override_permission_action_rules.get(self.__name__):
77
+ self.override_permission_action_rules[self.__name__] = {}
78
+ return self.override_permission_action_rules[self.__name__].get(action)
79
+
70
80
  @classmethod
71
81
  def query_rule(self, query):
72
82
  if REQUIRE_USER_PERMISSIONS:
@@ -76,6 +86,12 @@ class BasePolicy(UserPermissionMixIn, ResultSetMixIn):
76
86
  self.query_rules[self.__name__] = {}
77
87
  return self.query_rules[self.__name__].get(query)
78
88
 
89
+ @classmethod
90
+ def override_permission_query_rule(self, query):
91
+ if not self.override_permission_query_rules.get(self.__name__):
92
+ self.override_permission_query_rules[self.__name__] = {}
93
+ return self.override_permission_query_rules[self.__name__].get(query)
94
+
79
95
  @classmethod
80
96
  def read_rule(self, read):
81
97
  if REQUIRE_USER_PERMISSIONS:
@@ -85,6 +101,12 @@ class BasePolicy(UserPermissionMixIn, ResultSetMixIn):
85
101
  self.read_rules[self.__name__] = {}
86
102
  return self.read_rules[self.__name__].get(read)
87
103
 
104
+ @classmethod
105
+ def override_permission_read_rule(self, read):
106
+ if not self.override_permission_read_rules.get(self.__name__):
107
+ self.override_permission_read_rules[self.__name__] = {}
108
+ return self.override_permission_read_rules[self.__name__].get(read)
109
+
88
110
  @classmethod
89
111
  def write_rule(self, write):
90
112
  if REQUIRE_USER_PERMISSIONS:
@@ -94,10 +116,18 @@ class BasePolicy(UserPermissionMixIn, ResultSetMixIn):
94
116
  self.write_rules[self.__name__] = {}
95
117
  return self.write_rules[self.__name__].get(write)
96
118
 
119
+ @classmethod
120
+ def override_permission_write_rule(self, write):
121
+ if not self.override_permission_write_rules.get(self.__name__):
122
+ self.override_permission_write_rules[self.__name__] = {}
123
+ return self.override_permission_write_rules[self.__name__].get(write)
124
+
97
125
  @classmethod
98
126
  def allow_actions(self, array, **kwargs):
99
127
  if not self.action_rules.get(self.__name__):
100
128
  self.action_rules[self.__name__] = {}
129
+ if not self.override_permission_action_rules.get(self.__name__):
130
+ self.override_permission_action_rules[self.__name__] = {}
101
131
 
102
132
  array_use = array or [OperationType.ALL]
103
133
  for key in array_use:
@@ -106,18 +136,33 @@ class BasePolicy(UserPermissionMixIn, ResultSetMixIn):
106
136
  for scope in kwargs.get('scopes', []):
107
137
  self.action_rules[self.__name__][key][scope] = extract(kwargs, [
108
138
  'condition'])
139
+ if not self.override_permission_action_rules[self.__name__].get(key):
140
+ self.override_permission_action_rules[self.__name__][key] = {}
141
+ for scope in kwargs.get('scopes', []):
142
+ self.override_permission_action_rules[self.__name__][key][scope] = extract(
143
+ kwargs,
144
+ [
145
+ 'override_permission_condition',
146
+ ],
147
+ )
109
148
 
110
149
  @classmethod
111
150
  def allow_query(self, array: List = None, **kwargs):
112
151
  if not self.query_rules.get(self.__name__):
113
152
  self.query_rules[self.__name__] = {}
153
+ if not self.override_permission_query_rules.get(self.__name__):
154
+ self.override_permission_query_rules[self.__name__] = {}
114
155
 
115
156
  array_use = array or [AttributeType.ALL]
116
157
  for key in array_use:
117
158
  if not self.query_rules[self.__name__].get(key):
118
159
  self.query_rules[self.__name__][key] = {}
160
+ if not self.override_permission_query_rules[self.__name__].get(key):
161
+ self.override_permission_query_rules[self.__name__][key] = {}
162
+
119
163
  actions = kwargs.get('on_action', [OperationType.ALL])
120
164
  actions = actions if isinstance(actions, list) else [actions]
165
+
121
166
  for scope in kwargs.get('scopes', []):
122
167
  if not self.query_rules[self.__name__][key].get(scope):
123
168
  self.query_rules[self.__name__][key][scope] = {}
@@ -129,15 +174,32 @@ class BasePolicy(UserPermissionMixIn, ResultSetMixIn):
129
174
  ],
130
175
  )
131
176
 
177
+ if not self.override_permission_query_rules[self.__name__][key].get(scope):
178
+ self.override_permission_query_rules[self.__name__][key][scope] = {}
179
+ for action in actions:
180
+ self.override_permission_query_rules[self.__name__][key][scope][action] = \
181
+ extract(
182
+ kwargs,
183
+ [
184
+ 'override_permission_condition',
185
+ ])
186
+
132
187
  @classmethod
133
188
  def allow_read(self, array, **kwargs):
134
189
  if not self.read_rules.get(self.__name__):
135
190
  self.read_rules[self.__name__] = {}
191
+ if not self.override_permission_read_rules.get(self.__name__):
192
+ self.override_permission_read_rules[self.__name__] = {}
193
+
136
194
  for key in array:
137
195
  if not self.read_rules[self.__name__].get(key):
138
196
  self.read_rules[self.__name__][key] = {}
197
+ if not self.override_permission_read_rules[self.__name__].get(key):
198
+ self.override_permission_read_rules[self.__name__][key] = {}
199
+
139
200
  actions = kwargs.get('on_action', [OperationType.ALL])
140
201
  actions = actions if isinstance(actions, list) else [actions]
202
+
141
203
  for scope in kwargs.get('scopes', []):
142
204
  if not self.read_rules[self.__name__][key].get(scope):
143
205
  self.read_rules[self.__name__][key][scope] = {}
@@ -145,15 +207,32 @@ class BasePolicy(UserPermissionMixIn, ResultSetMixIn):
145
207
  self.read_rules[self.__name__][key][scope][action] = extract(kwargs, [
146
208
  'condition'])
147
209
 
210
+ if not self.override_permission_read_rules[self.__name__][key].get(scope):
211
+ self.override_permission_read_rules[self.__name__][key][scope] = {}
212
+ for action in actions:
213
+ self.override_permission_read_rules[self.__name__][key][scope][action] = \
214
+ extract(
215
+ kwargs,
216
+ [
217
+ 'override_permission_condition',
218
+ ])
219
+
148
220
  @classmethod
149
221
  def allow_write(self, array, **kwargs):
150
222
  if not self.write_rules.get(self.__name__):
151
223
  self.write_rules[self.__name__] = {}
224
+ if not self.override_permission_write_rules.get(self.__name__):
225
+ self.override_permission_write_rules[self.__name__] = {}
226
+
152
227
  for key in array:
153
228
  if not self.write_rules[self.__name__].get(key):
154
229
  self.write_rules[self.__name__][key] = {}
230
+ if not self.override_permission_write_rules[self.__name__].get(key):
231
+ self.override_permission_write_rules[self.__name__][key] = {}
232
+
155
233
  actions = kwargs.get('on_action', [OperationType.ALL])
156
234
  actions = actions if isinstance(actions, list) else [actions]
235
+
157
236
  for scope in kwargs.get('scopes', []):
158
237
  if not self.write_rules[self.__name__][key].get(scope):
159
238
  self.write_rules[self.__name__][key][scope] = {}
@@ -161,6 +240,16 @@ class BasePolicy(UserPermissionMixIn, ResultSetMixIn):
161
240
  self.write_rules[self.__name__][key][scope][action] = extract(kwargs, [
162
241
  'condition'])
163
242
 
243
+ if not self.override_permission_write_rules[self.__name__][key].get(scope):
244
+ self.override_permission_write_rules[self.__name__][key][scope] = {}
245
+ for action in actions:
246
+ self.override_permission_write_rules[self.__name__][key][scope][action] = \
247
+ extract(
248
+ kwargs,
249
+ [
250
+ 'override_permission_condition',
251
+ ])
252
+
164
253
  @classmethod
165
254
  def resource_name(self):
166
255
  return inflection.pluralize(self.resource_name_singular())
@@ -230,6 +319,9 @@ class BasePolicy(UserPermissionMixIn, ResultSetMixIn):
230
319
  action,
231
320
  config[self.current_scope()]['condition'],
232
321
  operation=action,
322
+ override_permission_condition=(self.__class__.override_permission_action_rule(
323
+ action,
324
+ ) or {}).get(self.current_scope(), {}).get('override_permission_condition'),
233
325
  )
234
326
  else:
235
327
  error = ApiError.UNAUTHORIZED_ACCESS
@@ -250,9 +342,11 @@ class BasePolicy(UserPermissionMixIn, ResultSetMixIn):
250
342
  if 'read' == read_or_write:
251
343
  attribute_operation = AttributeOperationType.READ
252
344
  orig_config = self.__class__.read_rule(attrb)
345
+ orig_config_override = self.__class__.override_permission_read_rule(attrb)
253
346
  else:
254
- orig_config = self.__class__.write_rule(attrb)
255
347
  attribute_operation = AttributeOperationType.WRITE
348
+ orig_config = self.__class__.write_rule(attrb)
349
+ orig_config_override = self.__class__.override_permission_write_rule(attrb)
256
350
 
257
351
  config = None
258
352
  if orig_config:
@@ -263,6 +357,15 @@ class BasePolicy(UserPermissionMixIn, ResultSetMixIn):
263
357
  if config is None:
264
358
  config = config_scope.get(OperationType.ALL)
265
359
 
360
+ config_override = None
361
+ if orig_config_override:
362
+ await self.__validate_scopes(attrb, orig_config_override.keys())
363
+ config_override_scope = orig_config_override.get(self.current_scope(), {})
364
+ config_override = config_override_scope.get(api_operation_action)
365
+
366
+ if config_override is None:
367
+ config_override = config_override_scope.get(OperationType.ALL)
368
+
266
369
  if config is None:
267
370
  error = ApiError.UNAUTHORIZED_ACCESS
268
371
  error.update({
@@ -274,13 +377,16 @@ class BasePolicy(UserPermissionMixIn, ResultSetMixIn):
274
377
  ),
275
378
  })
276
379
  raise ApiError(error)
380
+
277
381
  cond = config.get('condition')
382
+
278
383
  if cond:
279
384
  await self.__validate_condition(
280
385
  attrb,
281
386
  cond,
282
387
  attribute_operation=attribute_operation,
283
388
  operation=api_operation_action,
389
+ override_permission_condition=config_override.get('override_permission_condition'),
284
390
  **kwargs,
285
391
  )
286
392
 
@@ -320,6 +426,18 @@ class BasePolicy(UserPermissionMixIn, ResultSetMixIn):
320
426
  if config is None:
321
427
  config = config_scope.get(OperationType.ALL)
322
428
 
429
+ orig_config_override = self.__class__.override_permission_query_rule(key) or \
430
+ self.__class__.override_permission_query_rule(AttributeType.ALL)
431
+
432
+ config_override = None
433
+ if orig_config_override:
434
+ await self.__validate_scopes(key, orig_config_override.keys())
435
+ config_override_scope = orig_config_override.get(self.current_scope(), {})
436
+ config_override = config_override_scope.get(api_operation_action)
437
+
438
+ if config_override is None:
439
+ config_override = config_override_scope.get(OperationType.ALL)
440
+
323
441
  if config is None:
324
442
  error = ApiError.UNAUTHORIZED_ACCESS
325
443
  error.update({
@@ -334,6 +452,7 @@ class BasePolicy(UserPermissionMixIn, ResultSetMixIn):
334
452
  cond,
335
453
  message=error_message,
336
454
  operation=api_operation_action,
455
+ override_permission_condition=config_override.get('override_permission_condition'),
337
456
  )
338
457
 
339
458
  def parent_model(self):
@@ -375,6 +494,7 @@ class BasePolicy(UserPermissionMixIn, ResultSetMixIn):
375
494
  cond,
376
495
  attribute_operation: AttributeOperationType = None,
377
496
  operation: OperationType = None,
497
+ override_permission_condition: Callable = None,
378
498
  **kwargs,
379
499
  ):
380
500
  if not cond:
@@ -384,6 +504,11 @@ class BasePolicy(UserPermissionMixIn, ResultSetMixIn):
384
504
  if validation and inspect.isawaitable(validation):
385
505
  validation = await validation
386
506
 
507
+ if not validation and override_permission_condition:
508
+ validation = override_permission_condition(self)
509
+ if validation and inspect.isawaitable(validation):
510
+ validation = await validation
511
+
387
512
  if not validation:
388
513
  r_name = self.resource_name()
389
514
  error = ApiError.UNAUTHORIZED_ACCESS
@@ -45,10 +45,14 @@ PermissionPolicy.allow_read(PermissionPresenter.default_attributes + [
45
45
  'role',
46
46
  'user',
47
47
  'user_id',
48
+ 'users',
48
49
  ], scopes=[
49
50
  OauthScope.CLIENT_PRIVATE,
50
51
  ], on_action=[
52
+ constants.CREATE,
53
+ constants.DELETE,
51
54
  constants.DETAIL,
55
+ constants.UPDATE,
52
56
  ], condition=lambda policy: policy.has_at_least_viewer_role())
53
57
 
54
58
 
@@ -59,6 +63,7 @@ PermissionPolicy.allow_write([
59
63
  'entity_name',
60
64
  'entity_type',
61
65
  'role_id',
66
+ 'role_ids',
62
67
  ], scopes=[
63
68
  OauthScope.CLIENT_PRIVATE,
64
69
  ], on_action=[
@@ -16,6 +16,7 @@ UserPolicy.allow_actions([
16
16
  OauthScope.CLIENT_PRIVATE,
17
17
  ], condition=lambda policy: policy.is_owner())
18
18
 
19
+
19
20
  UserPolicy.allow_actions([
20
21
  constants.DETAIL,
21
22
  constants.UPDATE,
@@ -23,24 +24,40 @@ UserPolicy.allow_actions([
23
24
  OauthScope.CLIENT_PRIVATE,
24
25
  ], condition=lambda policy: policy.is_current_user() or policy.has_at_least_admin_role())
25
26
 
27
+
26
28
  UserPolicy.allow_actions([
27
29
  constants.LIST,
28
30
  ], scopes=[
29
31
  OauthScope.CLIENT_PRIVATE,
30
32
  ], condition=lambda policy: policy.has_at_least_admin_role())
31
33
 
34
+
32
35
  UserPolicy.allow_read(UserPresenter.default_attributes, scopes=[
33
36
  OauthScope.CLIENT_PRIVATE,
34
37
  ], condition=lambda policy: policy.is_current_user() or policy.has_at_least_admin_role())
35
38
 
39
+
36
40
  UserPolicy.allow_read(UserPresenter.default_attributes + [
41
+ 'permissions',
42
+ 'token',
43
+ ], scopes=[
44
+ OauthScope.CLIENT_PRIVATE,
45
+ ], on_action=[
46
+ constants.DETAIL,
47
+ constants.UPDATE,
48
+ ], condition=lambda policy: policy.is_current_user() or policy.has_at_least_admin_role())
49
+
50
+
51
+ UserPolicy.allow_read(UserPresenter.default_attributes + [
52
+ 'permissions',
37
53
  'token',
38
54
  ], scopes=[
39
55
  OauthScope.CLIENT_PRIVATE,
40
56
  ], on_action=[
41
57
  constants.CREATE,
42
58
  constants.DELETE,
43
- ], condition=lambda policy: policy.is_current_user() or policy.is_owner())
59
+ ], condition=lambda policy: policy.is_owner())
60
+
44
61
 
45
62
  UserPolicy.allow_write([
46
63
  'avatar',
@@ -57,15 +74,19 @@ UserPolicy.allow_write([
57
74
  constants.UPDATE,
58
75
  ], condition=lambda policy: policy.is_current_user() or policy.has_at_least_admin_role())
59
76
 
77
+
60
78
  UserPolicy.allow_write([
79
+ 'role_ids',
61
80
  'roles',
62
- 'roles_new'
81
+ 'roles_new',
63
82
  ], scopes=[
64
83
  OauthScope.CLIENT_PRIVATE,
65
84
  ], on_action=[
85
+ constants.DELETE,
66
86
  constants.UPDATE,
67
87
  ], condition=lambda policy: policy.has_at_least_admin_role())
68
88
 
89
+
69
90
  UserPolicy.allow_write([
70
91
  'owner',
71
92
  ], scopes=[
@@ -74,6 +95,7 @@ UserPolicy.allow_write([
74
95
  constants.UPDATE,
75
96
  ], condition=lambda policy: policy.is_owner())
76
97
 
98
+
77
99
  UserPolicy.allow_write([
78
100
  'avatar',
79
101
  'email',
@@ -81,6 +103,7 @@ UserPolicy.allow_write([
81
103
  'last_name',
82
104
  'password',
83
105
  'password_confirmation',
106
+ 'role_ids',
84
107
  'roles',
85
108
  'roles_new',
86
109
  'username',
@@ -135,15 +135,6 @@ async def validate_condition_with_permissions(
135
135
  has_access_for_all_attributes
136
136
  )
137
137
 
138
- print(
139
- 'WTFFFFFFFFFFFFFFFFFFFFFF',
140
- disable_access_for_attribute_operation,
141
- resource_attribute,
142
- permission.id,
143
- permission.access,
144
- permission.access & disable_access_for_attribute_operation,
145
- )
146
-
147
138
  # Don’t grant access if permission disables access to this entity’s attributes
148
139
  # for this attribute operation.
149
140
  if disable_access_for_attribute_operation is not None and \
@@ -42,6 +42,7 @@ PermissionPresenter.register_format(
42
42
 
43
43
  PermissionPresenter.register_formats([
44
44
  constants.CREATE,
45
+ constants.DELETE,
45
46
  constants.DETAIL,
46
47
  constants.UPDATE,
47
48
  ], PermissionPresenter.default_attributes + [
@@ -53,6 +54,7 @@ PermissionPresenter.register_formats([
53
54
  'roles',
54
55
  'user',
55
56
  'user_id',
57
+ 'users',
56
58
  'write_attributes',
57
59
  ])
58
60
 
@@ -47,3 +47,18 @@ RolePresenter.register_formats([
47
47
  'updated_at',
48
48
  'users',
49
49
  ])
50
+
51
+
52
+ RolePresenter.register_formats([
53
+ f'user/{constants.CREATE}',
54
+ f'user/{constants.DELETE}',
55
+ f'user/{constants.DETAIL}',
56
+ f'user/{constants.LIST}',
57
+ f'user/{constants.UPDATE}',
58
+ ], [
59
+ 'created_at',
60
+ 'id',
61
+ 'name',
62
+ 'permissions',
63
+ 'updated_at',
64
+ ])
@@ -1,6 +1,5 @@
1
1
  from mage_ai.api.operations import constants
2
2
  from mage_ai.api.presenters.BasePresenter import BasePresenter
3
- from mage_ai.shared.hash import extract
4
3
 
5
4
 
6
5
  class UserPresenter(BasePresenter):
@@ -20,21 +19,16 @@ class UserPresenter(BasePresenter):
20
19
  'username',
21
20
  ]
22
21
 
23
- async def prepare_present(self, **kwargs):
24
- data = self.model.to_dict(include_attributes=self.default_attributes)
25
- data = extract(data, self.default_attributes, include_blank_values=True)
26
22
 
27
- roles_new = self.model.roles_new
28
- data['roles_new'] = [
29
- role.to_dict(include_attributes=['permissions'])
30
- for role in roles_new
31
- ]
32
-
33
- return data
34
-
35
-
36
- UserPresenter.register_format(
37
- constants.CREATE, UserPresenter.default_attributes + ['token', ])
23
+ UserPresenter.register_formats([
24
+ constants.CREATE,
25
+ constants.DELETE,
26
+ constants.DETAIL,
27
+ constants.UPDATE,
28
+ ], UserPresenter.default_attributes + [
29
+ 'permissions',
30
+ 'token',
31
+ ])
38
32
 
39
33
 
40
34
  UserPresenter.register_formats([
@@ -1,7 +1,7 @@
1
1
  from mage_ai.api.resources.DatabaseResource import DatabaseResource
2
- from mage_ai.orchestration.db import safe_db_query
3
- from mage_ai.orchestration.db.models.oauth import Permission, Role
4
- from mage_ai.shared.hash import merge_dict
2
+ from mage_ai.orchestration.db import db_connection, safe_db_query
3
+ from mage_ai.orchestration.db.models.oauth import Permission, Role, RolePermission
4
+ from mage_ai.shared.hash import ignore_keys, index_by, merge_dict
5
5
 
6
6
 
7
7
  class PermissionResource(DatabaseResource):
@@ -42,5 +42,42 @@ class PermissionResource(DatabaseResource):
42
42
  user_id=user.id if user else None,
43
43
  )), user, **kwargs)
44
44
 
45
+ @safe_db_query
46
+ def update(self, payload, **kwargs):
47
+ role_ids = [int(i) for i in payload.get('role_ids') or []]
48
+ roles_mapping = index_by(lambda x: x.id, self.roles or [])
49
+
50
+ role_ids_create = []
51
+ role_ids_delete = []
52
+
53
+ for role_id in role_ids:
54
+ if role_id not in roles_mapping:
55
+ role_ids_create.append(role_id)
56
+
57
+ for role_id in roles_mapping.keys():
58
+ if role_id not in role_ids:
59
+ role_ids_delete.append(role_id)
60
+
61
+ if role_ids_create:
62
+ db_connection.session.bulk_save_objects(
63
+ [RolePermission(
64
+ permission_id=self.model.id,
65
+ role_id=role_id,
66
+ user_id=self.current_user.id,
67
+ ) for role_id in role_ids_create],
68
+ return_defaults=True,
69
+ )
70
+
71
+ if role_ids_delete:
72
+ delete_statement = RolePermission.__table__.delete().where(
73
+ RolePermission.permission_id == self.id,
74
+ RolePermission.role_id.in_(role_ids_delete),
75
+ )
76
+ db_connection.session.execute(delete_statement)
77
+
78
+ return super().update(ignore_keys(payload, [
79
+ 'role_ids',
80
+ ]), **kwargs)
81
+
45
82
 
46
83
  PermissionResource.register_parent_model('role_id', Role)
@@ -41,6 +41,7 @@ class RoleResource(DatabaseResource):
41
41
  roles = []
42
42
  for permission in permissions:
43
43
  roles.append(permission.role)
44
+ roles = list(filter(lambda x: x, roles))
44
45
  else:
45
46
  roles = Role.query.all()
46
47