gooddata-pipelines 1.47.1.dev1__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 gooddata-pipelines might be problematic. Click here for more details.
- gooddata_pipelines/__init__.py +59 -0
- gooddata_pipelines/_version.py +7 -0
- gooddata_pipelines/api/__init__.py +5 -0
- gooddata_pipelines/api/exceptions.py +41 -0
- gooddata_pipelines/api/gooddata_api.py +309 -0
- gooddata_pipelines/api/gooddata_api_wrapper.py +36 -0
- gooddata_pipelines/api/gooddata_sdk.py +374 -0
- gooddata_pipelines/api/utils.py +43 -0
- gooddata_pipelines/backup_and_restore/__init__.py +1 -0
- gooddata_pipelines/backup_and_restore/backup_input_processor.py +195 -0
- gooddata_pipelines/backup_and_restore/backup_manager.py +430 -0
- gooddata_pipelines/backup_and_restore/constants.py +42 -0
- gooddata_pipelines/backup_and_restore/csv_reader.py +41 -0
- gooddata_pipelines/backup_and_restore/models/__init__.py +1 -0
- gooddata_pipelines/backup_and_restore/models/input_type.py +11 -0
- gooddata_pipelines/backup_and_restore/models/storage.py +58 -0
- gooddata_pipelines/backup_and_restore/models/workspace_response.py +51 -0
- gooddata_pipelines/backup_and_restore/storage/__init__.py +1 -0
- gooddata_pipelines/backup_and_restore/storage/base_storage.py +18 -0
- gooddata_pipelines/backup_and_restore/storage/local_storage.py +37 -0
- gooddata_pipelines/backup_and_restore/storage/s3_storage.py +71 -0
- gooddata_pipelines/logger/__init__.py +8 -0
- gooddata_pipelines/logger/logger.py +115 -0
- gooddata_pipelines/provisioning/__init__.py +31 -0
- gooddata_pipelines/provisioning/assets/wdf_setting.json +14 -0
- gooddata_pipelines/provisioning/entities/__init__.py +1 -0
- gooddata_pipelines/provisioning/entities/user_data_filters/__init__.py +1 -0
- gooddata_pipelines/provisioning/entities/user_data_filters/models/__init__.py +1 -0
- gooddata_pipelines/provisioning/entities/user_data_filters/models/udf_models.py +32 -0
- gooddata_pipelines/provisioning/entities/user_data_filters/user_data_filters.py +221 -0
- gooddata_pipelines/provisioning/entities/users/__init__.py +1 -0
- gooddata_pipelines/provisioning/entities/users/models/__init__.py +1 -0
- gooddata_pipelines/provisioning/entities/users/models/permissions.py +242 -0
- gooddata_pipelines/provisioning/entities/users/models/user_groups.py +64 -0
- gooddata_pipelines/provisioning/entities/users/models/users.py +114 -0
- gooddata_pipelines/provisioning/entities/users/permissions.py +153 -0
- gooddata_pipelines/provisioning/entities/users/user_groups.py +212 -0
- gooddata_pipelines/provisioning/entities/users/users.py +179 -0
- gooddata_pipelines/provisioning/entities/workspaces/__init__.py +1 -0
- gooddata_pipelines/provisioning/entities/workspaces/models.py +78 -0
- gooddata_pipelines/provisioning/entities/workspaces/workspace.py +263 -0
- gooddata_pipelines/provisioning/entities/workspaces/workspace_data_filters.py +286 -0
- gooddata_pipelines/provisioning/entities/workspaces/workspace_data_parser.py +123 -0
- gooddata_pipelines/provisioning/entities/workspaces/workspace_data_validator.py +188 -0
- gooddata_pipelines/provisioning/provisioning.py +132 -0
- gooddata_pipelines/provisioning/utils/__init__.py +1 -0
- gooddata_pipelines/provisioning/utils/context_objects.py +32 -0
- gooddata_pipelines/provisioning/utils/exceptions.py +95 -0
- gooddata_pipelines/provisioning/utils/utils.py +80 -0
- gooddata_pipelines/py.typed +0 -0
- gooddata_pipelines-1.47.1.dev1.dist-info/METADATA +85 -0
- gooddata_pipelines-1.47.1.dev1.dist-info/RECORD +54 -0
- gooddata_pipelines-1.47.1.dev1.dist-info/WHEEL +4 -0
- gooddata_pipelines-1.47.1.dev1.dist-info/licenses/LICENSE.txt +1 -277
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# (C) 2025 GoodData Corporation
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Any, Iterator, TypeAlias
|
|
5
|
+
|
|
6
|
+
from gooddata_sdk.catalog.identifier import CatalogAssigneeIdentifier
|
|
7
|
+
from gooddata_sdk.catalog.permission.declarative_model.permission import (
|
|
8
|
+
CatalogDeclarativeSingleWorkspacePermission,
|
|
9
|
+
CatalogDeclarativeWorkspacePermissions,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from gooddata_pipelines.provisioning.utils.exceptions import BaseUserException
|
|
13
|
+
|
|
14
|
+
# TODO: refactor the full load and incremental load models to reuse as much as possible
|
|
15
|
+
# TODO: use pydantic models instead of dataclasses?
|
|
16
|
+
# TODO: make the validation logic more readable (as in PermissionIncrementalLoad)
|
|
17
|
+
|
|
18
|
+
TargetsPermissionDict: TypeAlias = dict[str, dict[str, bool]]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PermissionType(Enum):
|
|
22
|
+
user = "user"
|
|
23
|
+
user_group = "userGroup"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class PermissionIncrementalLoad:
|
|
28
|
+
permission: str
|
|
29
|
+
workspace_id: str
|
|
30
|
+
id: str
|
|
31
|
+
type: PermissionType
|
|
32
|
+
is_active: bool
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def from_list_of_dicts(
|
|
36
|
+
cls, data: list[dict[str, Any]]
|
|
37
|
+
) -> list["PermissionIncrementalLoad"]:
|
|
38
|
+
"""Creates a list of User objects from list of dicts."""
|
|
39
|
+
id: str
|
|
40
|
+
permissions = []
|
|
41
|
+
for permission in data:
|
|
42
|
+
user_id: str | None = permission.get("user_id")
|
|
43
|
+
user_group_id: str | None = permission.get("ug_id")
|
|
44
|
+
|
|
45
|
+
if user_id is not None:
|
|
46
|
+
target_type = PermissionType.user
|
|
47
|
+
id = user_id
|
|
48
|
+
elif user_group_id is not None:
|
|
49
|
+
target_type = PermissionType.user_group
|
|
50
|
+
id = user_group_id
|
|
51
|
+
|
|
52
|
+
permissions.append(
|
|
53
|
+
PermissionIncrementalLoad(
|
|
54
|
+
permission=permission["ws_permissions"],
|
|
55
|
+
workspace_id=permission["ws_id"],
|
|
56
|
+
id=id,
|
|
57
|
+
type=target_type,
|
|
58
|
+
is_active=str(permission["is_active"]).lower() == "true",
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
return permissions
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class PermissionFullLoad:
|
|
66
|
+
permission: str
|
|
67
|
+
workspace_id: str
|
|
68
|
+
id: str
|
|
69
|
+
type: PermissionType
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def from_list_of_dicts(
|
|
73
|
+
cls, data: list[dict[str, Any]]
|
|
74
|
+
) -> list["PermissionFullLoad"]:
|
|
75
|
+
"""Creates a list of User objects from list of dicts."""
|
|
76
|
+
permissions = []
|
|
77
|
+
for permission in data:
|
|
78
|
+
id = (
|
|
79
|
+
permission["user_id"]
|
|
80
|
+
if permission["user_id"]
|
|
81
|
+
else permission["ug_id"]
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if permission["user_id"]:
|
|
85
|
+
target_type = PermissionType.user
|
|
86
|
+
else:
|
|
87
|
+
target_type = PermissionType.user_group
|
|
88
|
+
|
|
89
|
+
permissions.append(
|
|
90
|
+
PermissionFullLoad(
|
|
91
|
+
permission=permission["ws_permissions"],
|
|
92
|
+
workspace_id=permission["ws_id"],
|
|
93
|
+
id=id,
|
|
94
|
+
type=target_type,
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
return permissions
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass
|
|
101
|
+
class PermissionDeclaration:
|
|
102
|
+
users: TargetsPermissionDict
|
|
103
|
+
user_groups: TargetsPermissionDict
|
|
104
|
+
|
|
105
|
+
@classmethod
|
|
106
|
+
def from_sdk_api(
|
|
107
|
+
cls, declaration: CatalogDeclarativeWorkspacePermissions
|
|
108
|
+
) -> "PermissionDeclaration":
|
|
109
|
+
"""
|
|
110
|
+
Constructs an WSPermissionDeclaration instance
|
|
111
|
+
from GoodData SDK CatalogDeclarativeWorkspacePermissions.
|
|
112
|
+
"""
|
|
113
|
+
users: TargetsPermissionDict = {}
|
|
114
|
+
user_groups: TargetsPermissionDict = {}
|
|
115
|
+
|
|
116
|
+
for permission in declaration.permissions:
|
|
117
|
+
permission_type, id = (
|
|
118
|
+
permission.assignee.type,
|
|
119
|
+
permission.assignee.id,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if permission_type == PermissionType.user.value:
|
|
123
|
+
target_dict = users
|
|
124
|
+
else:
|
|
125
|
+
target_dict = user_groups
|
|
126
|
+
|
|
127
|
+
id_permissions = target_dict.get(id)
|
|
128
|
+
if not id_permissions:
|
|
129
|
+
target_dict[id] = dict()
|
|
130
|
+
|
|
131
|
+
target_dict[id][permission.name] = True
|
|
132
|
+
|
|
133
|
+
return PermissionDeclaration(users, user_groups)
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def _construct_upstream_permission(
|
|
137
|
+
permission: str, assignee: CatalogAssigneeIdentifier
|
|
138
|
+
) -> CatalogDeclarativeSingleWorkspacePermission | None:
|
|
139
|
+
"""Constructs single permission declaration for the SDK API."""
|
|
140
|
+
try:
|
|
141
|
+
return CatalogDeclarativeSingleWorkspacePermission(
|
|
142
|
+
name=permission, assignee=assignee
|
|
143
|
+
)
|
|
144
|
+
except Exception as e:
|
|
145
|
+
raise BaseUserException(
|
|
146
|
+
f"Failed to construct SDK declaration for type={assignee.type} ",
|
|
147
|
+
f"id={assignee.id}. Error: {e}",
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
def _permissions_for_target(
|
|
151
|
+
self, permissions: dict[str, bool], assignee: CatalogAssigneeIdentifier
|
|
152
|
+
) -> Iterator[CatalogDeclarativeSingleWorkspacePermission]:
|
|
153
|
+
"""Constructs permission declarations for a single target."""
|
|
154
|
+
for permission, is_active in permissions.items():
|
|
155
|
+
if not is_active:
|
|
156
|
+
continue
|
|
157
|
+
declaration = self._construct_upstream_permission(
|
|
158
|
+
permission, assignee
|
|
159
|
+
)
|
|
160
|
+
if not declaration:
|
|
161
|
+
continue
|
|
162
|
+
yield declaration
|
|
163
|
+
|
|
164
|
+
def to_sdk_api(self) -> CatalogDeclarativeWorkspacePermissions:
|
|
165
|
+
"""
|
|
166
|
+
Constructs the GoodData SDK CatalogDeclarativeWorkspacePermissions
|
|
167
|
+
object from the WSPermissionDeclaration instance.
|
|
168
|
+
"""
|
|
169
|
+
permission_declarations: list[
|
|
170
|
+
CatalogDeclarativeSingleWorkspacePermission
|
|
171
|
+
] = []
|
|
172
|
+
|
|
173
|
+
for user_id, permissions in self.users.items():
|
|
174
|
+
assignee = CatalogAssigneeIdentifier(
|
|
175
|
+
id=user_id, type=PermissionType.user.value
|
|
176
|
+
)
|
|
177
|
+
for declaration in self._permissions_for_target(
|
|
178
|
+
permissions, assignee
|
|
179
|
+
):
|
|
180
|
+
permission_declarations.append(declaration)
|
|
181
|
+
|
|
182
|
+
for ug_id, permissions in self.user_groups.items():
|
|
183
|
+
assignee = CatalogAssigneeIdentifier(
|
|
184
|
+
id=ug_id, type=PermissionType.user_group.value
|
|
185
|
+
)
|
|
186
|
+
for declaration in self._permissions_for_target(
|
|
187
|
+
permissions, assignee
|
|
188
|
+
):
|
|
189
|
+
permission_declarations.append(declaration)
|
|
190
|
+
|
|
191
|
+
return CatalogDeclarativeWorkspacePermissions(
|
|
192
|
+
permissions=permission_declarations
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def add_permission(self, permission: PermissionIncrementalLoad) -> None:
|
|
196
|
+
"""
|
|
197
|
+
Adds WSPermission object into respective field within the instance.
|
|
198
|
+
Handles duplicate permissions and different combinations of input
|
|
199
|
+
and upstream is_active permission states.
|
|
200
|
+
"""
|
|
201
|
+
target_dict = (
|
|
202
|
+
self.users
|
|
203
|
+
if permission.type == PermissionType.user
|
|
204
|
+
else self.user_groups
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if permission.id not in target_dict:
|
|
208
|
+
target_dict[permission.id] = {}
|
|
209
|
+
|
|
210
|
+
is_active = permission.is_active
|
|
211
|
+
target_permissions = target_dict[permission.id]
|
|
212
|
+
permission_value = permission.permission
|
|
213
|
+
|
|
214
|
+
if permission_value not in target_permissions:
|
|
215
|
+
target_permissions[permission_value] = is_active
|
|
216
|
+
elif not is_active and target_permissions[permission_value] is True:
|
|
217
|
+
print(
|
|
218
|
+
"isActive=False provided after True has been specificed for the "
|
|
219
|
+
+ f"same input. Skipping '{permission}'"
|
|
220
|
+
)
|
|
221
|
+
elif is_active and target_permissions[permission_value] is False:
|
|
222
|
+
print(
|
|
223
|
+
"isActive=True provided after False has been specified for the "
|
|
224
|
+
+ f"same input. Overwriting '{permission}'"
|
|
225
|
+
)
|
|
226
|
+
target_permissions[permission_value] = is_active
|
|
227
|
+
|
|
228
|
+
def upsert(self, other: "PermissionDeclaration") -> None:
|
|
229
|
+
"""
|
|
230
|
+
Modifies the owner object by merging with the other.
|
|
231
|
+
Keeps the unmodified users/userGroups untouched.
|
|
232
|
+
If some user/userGroup is modified, it gets overwritten with permissions
|
|
233
|
+
defined in the input.
|
|
234
|
+
"""
|
|
235
|
+
for user_id, permissions in other.users.items():
|
|
236
|
+
self.users[user_id] = permissions
|
|
237
|
+
|
|
238
|
+
for ug_id, permissions in other.user_groups.items():
|
|
239
|
+
self.user_groups[ug_id] = permissions
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
WSPermissionsDeclarations: TypeAlias = dict[str, PermissionDeclaration]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# (C) 2025 GoodData Corporation
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from gooddata_pipelines.provisioning.utils.utils import SplitMixin
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BaseUserGroup(BaseModel, SplitMixin):
|
|
11
|
+
user_group_id: str
|
|
12
|
+
user_group_name: str
|
|
13
|
+
parent_user_groups: list[str]
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def _create_from_dict_data(
|
|
17
|
+
cls, user_group_data: dict[str, Any], delimiter: str = ","
|
|
18
|
+
) -> dict[str, Any]:
|
|
19
|
+
"""Helper method to extract common data from dict."""
|
|
20
|
+
parent_user_groups = cls.split(
|
|
21
|
+
user_group_data["parent_user_groups"], delimiter=delimiter
|
|
22
|
+
)
|
|
23
|
+
user_group_name = user_group_data["user_group_name"]
|
|
24
|
+
if not user_group_name:
|
|
25
|
+
user_group_name = user_group_data["user_group_id"]
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
"user_group_id": user_group_data["user_group_id"],
|
|
29
|
+
"user_group_name": user_group_name,
|
|
30
|
+
"parent_user_groups": parent_user_groups,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class UserGroupIncrementalLoad(BaseUserGroup):
|
|
35
|
+
is_active: bool
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def from_list_of_dicts(
|
|
39
|
+
cls, data: list[dict[str, Any]], delimiter: str = ","
|
|
40
|
+
) -> list["UserGroupIncrementalLoad"]:
|
|
41
|
+
"""Creates a list of User objects from list of dicts."""
|
|
42
|
+
user_groups = []
|
|
43
|
+
for user_group in data:
|
|
44
|
+
base_data = cls._create_from_dict_data(user_group, delimiter)
|
|
45
|
+
base_data["is_active"] = user_group["is_active"]
|
|
46
|
+
|
|
47
|
+
user_groups.append(UserGroupIncrementalLoad(**base_data))
|
|
48
|
+
|
|
49
|
+
return user_groups
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class UserGroupFullLoad(BaseUserGroup):
|
|
53
|
+
@classmethod
|
|
54
|
+
def from_list_of_dicts(
|
|
55
|
+
cls, data: list[dict[str, Any]], delimiter: str = ","
|
|
56
|
+
) -> list["UserGroupFullLoad"]:
|
|
57
|
+
"""Creates a list of User objects from list of dicts."""
|
|
58
|
+
user_groups = []
|
|
59
|
+
for user_group in data:
|
|
60
|
+
base_data = cls._create_from_dict_data(user_group, delimiter)
|
|
61
|
+
|
|
62
|
+
user_groups.append(UserGroupFullLoad(**base_data))
|
|
63
|
+
|
|
64
|
+
return user_groups
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# (C) 2025 GoodData Corporation
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from gooddata_sdk.catalog.user.entity_model.user import CatalogUser
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from gooddata_pipelines.provisioning.utils.utils import SplitMixin
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BaseUser(BaseModel, SplitMixin):
|
|
12
|
+
"""Base class containing shared user fields and functionality."""
|
|
13
|
+
|
|
14
|
+
user_id: str
|
|
15
|
+
firstname: str | None
|
|
16
|
+
lastname: str | None
|
|
17
|
+
email: str | None
|
|
18
|
+
auth_id: str | None
|
|
19
|
+
user_groups: list[str]
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def _create_from_dict_data(
|
|
23
|
+
cls, user_data: dict[str, Any], delimiter: str = ","
|
|
24
|
+
) -> dict[str, Any]:
|
|
25
|
+
"""Helper method to extract common data from dict."""
|
|
26
|
+
user_groups = cls.split(user_data["user_groups"], delimiter=delimiter)
|
|
27
|
+
return {
|
|
28
|
+
"user_id": user_data["user_id"],
|
|
29
|
+
"firstname": user_data["firstname"],
|
|
30
|
+
"lastname": user_data["lastname"],
|
|
31
|
+
"email": user_data["email"],
|
|
32
|
+
"auth_id": user_data["auth_id"],
|
|
33
|
+
"user_groups": user_groups,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@classmethod
|
|
37
|
+
def _create_from_sdk_data(cls, obj: CatalogUser) -> dict[str, Any]:
|
|
38
|
+
"""Helper method to extract common data from SDK object."""
|
|
39
|
+
if obj.attributes:
|
|
40
|
+
firstname = obj.attributes.firstname
|
|
41
|
+
lastname = obj.attributes.lastname
|
|
42
|
+
email = obj.attributes.email
|
|
43
|
+
auth_id = obj.attributes.authentication_id
|
|
44
|
+
else:
|
|
45
|
+
firstname = None
|
|
46
|
+
lastname = None
|
|
47
|
+
email = None
|
|
48
|
+
auth_id = None
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
"user_id": obj.id,
|
|
52
|
+
"firstname": firstname,
|
|
53
|
+
"lastname": lastname,
|
|
54
|
+
"email": email,
|
|
55
|
+
"auth_id": auth_id,
|
|
56
|
+
"user_groups": [ug.id for ug in obj.user_groups],
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
def to_sdk_obj(self) -> CatalogUser:
|
|
60
|
+
"""Converts to CatalogUser SDK object."""
|
|
61
|
+
return CatalogUser.init(
|
|
62
|
+
user_id=self.user_id,
|
|
63
|
+
firstname=self.firstname,
|
|
64
|
+
lastname=self.lastname,
|
|
65
|
+
email=self.email,
|
|
66
|
+
authentication_id=self.auth_id,
|
|
67
|
+
user_group_ids=self.user_groups,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class UserIncrementalLoad(BaseUser):
|
|
72
|
+
"""User model for incremental load operations with active status tracking."""
|
|
73
|
+
|
|
74
|
+
is_active: bool
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def from_list_of_dicts(
|
|
78
|
+
cls, data: list[dict[str, Any]], delimiter: str = ","
|
|
79
|
+
) -> list["UserIncrementalLoad"]:
|
|
80
|
+
"""Creates a list of User objects from list of dicts."""
|
|
81
|
+
converted_users = []
|
|
82
|
+
for user in data:
|
|
83
|
+
base_data = cls._create_from_dict_data(user, delimiter)
|
|
84
|
+
base_data["is_active"] = user["is_active"]
|
|
85
|
+
converted_users.append(cls(**base_data))
|
|
86
|
+
return converted_users
|
|
87
|
+
|
|
88
|
+
@classmethod
|
|
89
|
+
def from_sdk_obj(cls, obj: CatalogUser) -> "UserIncrementalLoad":
|
|
90
|
+
"""Creates GDUserTarget from CatalogUser SDK object."""
|
|
91
|
+
base_data = cls._create_from_sdk_data(obj)
|
|
92
|
+
base_data["is_active"] = True
|
|
93
|
+
return cls(**base_data)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class UserFullLoad(BaseUser):
|
|
97
|
+
"""User model for full load operations."""
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def from_list_of_dicts(
|
|
101
|
+
cls, data: list[dict[str, Any]], delimiter: str = ","
|
|
102
|
+
) -> list["UserFullLoad"]:
|
|
103
|
+
"""Creates a list of User objects from list of dicts."""
|
|
104
|
+
converted_users = []
|
|
105
|
+
for user in data:
|
|
106
|
+
base_data = cls._create_from_dict_data(user, delimiter)
|
|
107
|
+
converted_users.append(cls(**base_data))
|
|
108
|
+
return converted_users
|
|
109
|
+
|
|
110
|
+
@classmethod
|
|
111
|
+
def from_sdk_obj(cls, obj: CatalogUser) -> "UserFullLoad":
|
|
112
|
+
"""Creates GDUserTarget from CatalogUser SDK object."""
|
|
113
|
+
base_data = cls._create_from_sdk_data(obj)
|
|
114
|
+
return cls(**base_data)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# (C) 2025 GoodData Corporation
|
|
2
|
+
|
|
3
|
+
"""Module for provisioning user permissions in GoodData workspaces."""
|
|
4
|
+
|
|
5
|
+
from gooddata_pipelines.api.exceptions import GoodDataApiException
|
|
6
|
+
from gooddata_pipelines.provisioning.entities.users.models.permissions import (
|
|
7
|
+
PermissionDeclaration,
|
|
8
|
+
PermissionFullLoad,
|
|
9
|
+
PermissionIncrementalLoad,
|
|
10
|
+
PermissionType,
|
|
11
|
+
TargetsPermissionDict,
|
|
12
|
+
WSPermissionsDeclarations,
|
|
13
|
+
)
|
|
14
|
+
from gooddata_pipelines.provisioning.provisioning import Provisioning
|
|
15
|
+
from gooddata_pipelines.provisioning.utils.exceptions import BaseUserException
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PermissionProvisioner(
|
|
19
|
+
Provisioning[PermissionFullLoad, PermissionIncrementalLoad]
|
|
20
|
+
):
|
|
21
|
+
"""Provisioning class for user permissions in GoodData workspaces.
|
|
22
|
+
|
|
23
|
+
This class handles the provisioning of user permissions based on the provided
|
|
24
|
+
source data.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
source_group_incremental: list[PermissionIncrementalLoad]
|
|
28
|
+
source_group_full: list[PermissionFullLoad]
|
|
29
|
+
|
|
30
|
+
def _get_ws_declaration(self, ws_id: str) -> PermissionDeclaration:
|
|
31
|
+
users: TargetsPermissionDict = {}
|
|
32
|
+
user_groups: TargetsPermissionDict = {}
|
|
33
|
+
|
|
34
|
+
upstream_declaration = self._api.get_declarative_permissions(ws_id)
|
|
35
|
+
|
|
36
|
+
for permission in upstream_declaration.permissions:
|
|
37
|
+
permission_type, id = (
|
|
38
|
+
permission.assignee.type,
|
|
39
|
+
permission.assignee.id,
|
|
40
|
+
)
|
|
41
|
+
target_dict = (
|
|
42
|
+
users
|
|
43
|
+
if permission_type == PermissionType.user.value
|
|
44
|
+
else user_groups
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
id_permissions = target_dict.get(id)
|
|
48
|
+
if not id_permissions:
|
|
49
|
+
target_dict[id] = dict()
|
|
50
|
+
|
|
51
|
+
target_dict[id][permission.name] = True
|
|
52
|
+
|
|
53
|
+
return PermissionDeclaration(users, user_groups)
|
|
54
|
+
|
|
55
|
+
def _get_upstream_declaration(
|
|
56
|
+
self, ws_id: str
|
|
57
|
+
) -> PermissionDeclaration | None:
|
|
58
|
+
"""Retrieves upstream permission declaration for a workspace."""
|
|
59
|
+
declaration = self._api.get_declarative_permissions(ws_id)
|
|
60
|
+
return PermissionDeclaration.from_sdk_api(declaration)
|
|
61
|
+
|
|
62
|
+
def _get_upstream_declarations(
|
|
63
|
+
self, input_ws_ids: list[str]
|
|
64
|
+
) -> WSPermissionsDeclarations:
|
|
65
|
+
"""Retrieves upstream permission declarations for a list of workspaces."""
|
|
66
|
+
ws_dict: WSPermissionsDeclarations = {}
|
|
67
|
+
for ws_id in input_ws_ids:
|
|
68
|
+
declaration = self._get_upstream_declaration(ws_id)
|
|
69
|
+
if declaration:
|
|
70
|
+
ws_dict[ws_id] = declaration
|
|
71
|
+
return ws_dict
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def _construct_declarations(
|
|
75
|
+
permissions: list[PermissionIncrementalLoad],
|
|
76
|
+
) -> WSPermissionsDeclarations:
|
|
77
|
+
"""Constructs workspace permission declarations from the input permissions."""
|
|
78
|
+
ws_dict: WSPermissionsDeclarations = {}
|
|
79
|
+
for permission in permissions:
|
|
80
|
+
ws_id = permission.workspace_id
|
|
81
|
+
|
|
82
|
+
if ws_id not in ws_dict:
|
|
83
|
+
ws_dict[ws_id] = PermissionDeclaration({}, {})
|
|
84
|
+
|
|
85
|
+
ws_dict[ws_id].add_permission(permission)
|
|
86
|
+
return ws_dict
|
|
87
|
+
|
|
88
|
+
def _check_user_group_exists(self, ug_id: str) -> None:
|
|
89
|
+
"""Checks if user group with provided ID exists."""
|
|
90
|
+
self._api._sdk.catalog_user.get_user_group(ug_id)
|
|
91
|
+
|
|
92
|
+
def _validate_permission(
|
|
93
|
+
self, permission: PermissionIncrementalLoad
|
|
94
|
+
) -> None:
|
|
95
|
+
"""Validates if the permission is correctly defined."""
|
|
96
|
+
if permission.type == PermissionType.user:
|
|
97
|
+
self._api.get_user(permission.id, error_message="User not found")
|
|
98
|
+
else:
|
|
99
|
+
self._api.get_user_group(
|
|
100
|
+
permission.id, error_message="User group not found"
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
self._api.get_workspace(
|
|
104
|
+
permission.workspace_id, error_message="Workspace not found"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def _filter_invalid_permissions(
|
|
108
|
+
self, permissions: list[PermissionIncrementalLoad]
|
|
109
|
+
) -> list[PermissionIncrementalLoad]:
|
|
110
|
+
"""Filters out invalid permissions from the input list."""
|
|
111
|
+
valid_permissions: list[PermissionIncrementalLoad] = []
|
|
112
|
+
for permission in permissions:
|
|
113
|
+
try:
|
|
114
|
+
self._validate_permission(permission)
|
|
115
|
+
except (BaseUserException, GoodDataApiException) as e:
|
|
116
|
+
self.logger.error(
|
|
117
|
+
f"Skipping {permission}. Error: {e.error_message} "
|
|
118
|
+
+ f"Context: {permission.__dict__}"
|
|
119
|
+
)
|
|
120
|
+
continue
|
|
121
|
+
valid_permissions.append(permission)
|
|
122
|
+
return valid_permissions
|
|
123
|
+
|
|
124
|
+
def _manage_permissions(
|
|
125
|
+
self, permissions: list[PermissionIncrementalLoad]
|
|
126
|
+
) -> None:
|
|
127
|
+
"""Manages permissions for a list of workspaces.
|
|
128
|
+
Modify upstream workspace declarations for each input workspace and skip non-existent ws_ids
|
|
129
|
+
"""
|
|
130
|
+
valid_permissions = self._filter_invalid_permissions(permissions)
|
|
131
|
+
|
|
132
|
+
input_declarations = self._construct_declarations(valid_permissions)
|
|
133
|
+
|
|
134
|
+
input_ws_ids = list(input_declarations.keys())
|
|
135
|
+
upstream_declarations = self._get_upstream_declarations(input_ws_ids)
|
|
136
|
+
|
|
137
|
+
for ws_id, declaration in input_declarations.items():
|
|
138
|
+
if ws_id not in upstream_declarations:
|
|
139
|
+
continue
|
|
140
|
+
|
|
141
|
+
upstream_declarations[ws_id].upsert(declaration)
|
|
142
|
+
|
|
143
|
+
ws_permissions = upstream_declarations[ws_id].to_sdk_api()
|
|
144
|
+
|
|
145
|
+
self._api.put_declarative_permissions(ws_id, ws_permissions)
|
|
146
|
+
self.logger.info(f"Updated permissions for workspace {ws_id}")
|
|
147
|
+
|
|
148
|
+
def _provision_incremental_load(self) -> None:
|
|
149
|
+
"""Provision permissions based on the source group."""
|
|
150
|
+
self._manage_permissions(self.source_group_incremental)
|
|
151
|
+
|
|
152
|
+
def _provision_full_load(self) -> None:
|
|
153
|
+
raise NotImplementedError("Not implemented yet.")
|