blackant-sdk 1.0.2__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.
- blackant/__init__.py +31 -0
- blackant/auth/__init__.py +10 -0
- blackant/auth/blackant_auth.py +518 -0
- blackant/auth/keycloak_manager.py +363 -0
- blackant/auth/request_id.py +52 -0
- blackant/auth/role_assignment.py +443 -0
- blackant/auth/tokens.py +57 -0
- blackant/client.py +400 -0
- blackant/config/__init__.py +0 -0
- blackant/config/docker_config.py +457 -0
- blackant/config/keycloak_admin_config.py +107 -0
- blackant/docker/__init__.py +12 -0
- blackant/docker/builder.py +616 -0
- blackant/docker/client.py +983 -0
- blackant/docker/dao.py +462 -0
- blackant/docker/registry.py +172 -0
- blackant/exceptions.py +111 -0
- blackant/http/__init__.py +8 -0
- blackant/http/client.py +125 -0
- blackant/patterns/__init__.py +1 -0
- blackant/patterns/singleton.py +20 -0
- blackant/services/__init__.py +10 -0
- blackant/services/dao.py +414 -0
- blackant/services/registry.py +635 -0
- blackant/utils/__init__.py +8 -0
- blackant/utils/initialization.py +32 -0
- blackant/utils/logging.py +337 -0
- blackant/utils/request_id.py +13 -0
- blackant/utils/store.py +50 -0
- blackant_sdk-1.0.2.dist-info/METADATA +117 -0
- blackant_sdk-1.0.2.dist-info/RECORD +70 -0
- blackant_sdk-1.0.2.dist-info/WHEEL +5 -0
- blackant_sdk-1.0.2.dist-info/top_level.txt +5 -0
- calculation/__init__.py +0 -0
- calculation/base.py +26 -0
- calculation/errors.py +2 -0
- calculation/impl/__init__.py +0 -0
- calculation/impl/my_calculation.py +144 -0
- calculation/impl/simple_calc.py +53 -0
- calculation/impl/test.py +1 -0
- calculation/impl/test_calc.py +36 -0
- calculation/loader.py +227 -0
- notifinations/__init__.py +8 -0
- notifinations/mail_sender.py +212 -0
- storage/__init__.py +0 -0
- storage/errors.py +10 -0
- storage/factory.py +26 -0
- storage/interface.py +19 -0
- storage/minio.py +106 -0
- task/__init__.py +0 -0
- task/dao.py +38 -0
- task/errors.py +10 -0
- task/log_adapter.py +11 -0
- task/parsers/__init__.py +0 -0
- task/parsers/base.py +13 -0
- task/parsers/callback.py +40 -0
- task/parsers/cmd_args.py +52 -0
- task/parsers/freetext.py +19 -0
- task/parsers/objects.py +50 -0
- task/parsers/request.py +56 -0
- task/resource.py +84 -0
- task/states/__init__.py +0 -0
- task/states/base.py +14 -0
- task/states/error.py +47 -0
- task/states/idle.py +12 -0
- task/states/ready.py +51 -0
- task/states/running.py +21 -0
- task/states/set_up.py +40 -0
- task/states/tear_down.py +29 -0
- task/task.py +358 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
"""Keycloak Admin API Manager
|
|
2
|
+
|
|
3
|
+
Wrapper around python-keycloak library for role management operations.
|
|
4
|
+
Handles service account authentication, token management, and role assignments.
|
|
5
|
+
|
|
6
|
+
Author: Balázs Milán (milan.balazs@uni-obuda.hu)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Optional, List, Dict, Any
|
|
10
|
+
import logging
|
|
11
|
+
from keycloak import KeycloakAdmin
|
|
12
|
+
from keycloak.exceptions import KeycloakError, KeycloakAuthenticationError
|
|
13
|
+
|
|
14
|
+
from ..exceptions import (
|
|
15
|
+
KeycloakConnectionError,
|
|
16
|
+
KeycloakAuthenticationError as BlackAntKeycloakAuthError,
|
|
17
|
+
RoleAssignmentError
|
|
18
|
+
)
|
|
19
|
+
from ..config.keycloak_admin_config import KeycloakAdminConfig
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class KeycloakManager:
|
|
26
|
+
"""Keycloak Admin API manager for role operations.
|
|
27
|
+
|
|
28
|
+
Provides high-level interface for Keycloak role management using
|
|
29
|
+
service account authentication. Handles token refresh automatically.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
config: KeycloakAdminConfig instance with service account credentials
|
|
33
|
+
|
|
34
|
+
Example:
|
|
35
|
+
>>> config = KeycloakAdminConfig.from_env()
|
|
36
|
+
>>> manager = KeycloakManager(config)
|
|
37
|
+
>>> manager.assign_role_to_user("user-uuid", "science_module")
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, config: KeycloakAdminConfig):
|
|
41
|
+
"""Initialize Keycloak manager with admin config.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
config: Keycloak admin configuration
|
|
45
|
+
|
|
46
|
+
Raises:
|
|
47
|
+
KeycloakConnectionError: If connection to Keycloak fails
|
|
48
|
+
KeycloakAuthenticationError: If service account auth fails
|
|
49
|
+
"""
|
|
50
|
+
self.config = config
|
|
51
|
+
self.logger = logger
|
|
52
|
+
self._admin_client: Optional[KeycloakAdmin] = None
|
|
53
|
+
self._initialize_admin_client()
|
|
54
|
+
|
|
55
|
+
def _initialize_admin_client(self) -> None:
|
|
56
|
+
"""Initialize Keycloak admin client with service account.
|
|
57
|
+
|
|
58
|
+
Uses client credentials grant flow for service account authentication.
|
|
59
|
+
The python-keycloak library handles token refresh automatically.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
KeycloakConnectionError: If connection fails
|
|
63
|
+
KeycloakAuthenticationError: If authentication fails
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
self._admin_client = KeycloakAdmin(
|
|
67
|
+
server_url=self.config.server_url,
|
|
68
|
+
realm_name=self.config.realm_name,
|
|
69
|
+
client_id=self.config.client_id,
|
|
70
|
+
client_secret_key=self.config.client_secret,
|
|
71
|
+
verify=self.config.verify_ssl,
|
|
72
|
+
timeout=self.config.timeout
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# Test connection with a simple API call
|
|
76
|
+
self._admin_client.get_realm_roles()
|
|
77
|
+
|
|
78
|
+
self.logger.info(
|
|
79
|
+
f"Keycloak admin client initialized successfully "
|
|
80
|
+
f"(realm: {self.config.realm_name})"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
except KeycloakAuthenticationError as e:
|
|
84
|
+
self.logger.error(f"Keycloak authentication failed: {e}")
|
|
85
|
+
raise BlackAntKeycloakAuthError(
|
|
86
|
+
f"Failed to authenticate with Keycloak service account: {e}"
|
|
87
|
+
)
|
|
88
|
+
except Exception as e:
|
|
89
|
+
self.logger.error(f"Failed to connect to Keycloak: {e}")
|
|
90
|
+
raise KeycloakConnectionError(
|
|
91
|
+
f"Could not establish connection to Keycloak server: {e}"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def get_user_roles(self, user_id: str) -> List[Dict[str, Any]]:
|
|
95
|
+
"""Get all roles assigned to a user.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
user_id: Keycloak user UUID
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
List of role dictionaries with keys: id, name, description
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
RoleAssignmentError: If role query fails
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
>>> roles = manager.get_user_roles("user-uuid")
|
|
108
|
+
>>> print([r['name'] for r in roles])
|
|
109
|
+
['default-roles', 'science_module']
|
|
110
|
+
"""
|
|
111
|
+
try:
|
|
112
|
+
# Get realm-level roles
|
|
113
|
+
realm_roles = self._admin_client.get_realm_roles_of_user(user_id)
|
|
114
|
+
|
|
115
|
+
self.logger.debug(
|
|
116
|
+
f"Retrieved {len(realm_roles)} roles for user {user_id}"
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return realm_roles
|
|
120
|
+
|
|
121
|
+
except KeycloakError as e:
|
|
122
|
+
self.logger.error(f"Failed to get user roles: {e}")
|
|
123
|
+
raise RoleAssignmentError(
|
|
124
|
+
f"Could not retrieve roles for user {user_id}: {e}"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def has_role(self, user_id: str, role_name: str) -> bool:
|
|
128
|
+
"""Check if user has a specific role.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
user_id: Keycloak user UUID
|
|
132
|
+
role_name: Role name to check (e.g., "science_module")
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
True if user has the role, False otherwise
|
|
136
|
+
|
|
137
|
+
Example:
|
|
138
|
+
>>> if manager.has_role("user-uuid", "science_module"):
|
|
139
|
+
... print("User has access to science modules")
|
|
140
|
+
"""
|
|
141
|
+
try:
|
|
142
|
+
user_roles = self.get_user_roles(user_id)
|
|
143
|
+
role_names = [role['name'] for role in user_roles]
|
|
144
|
+
has_role = role_name in role_names
|
|
145
|
+
|
|
146
|
+
self.logger.debug(
|
|
147
|
+
f"User {user_id} {'has' if has_role else 'does not have'} "
|
|
148
|
+
f"role '{role_name}'"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return has_role
|
|
152
|
+
|
|
153
|
+
except RoleAssignmentError:
|
|
154
|
+
# If we can't get roles, assume user doesn't have the role
|
|
155
|
+
self.logger.warning(
|
|
156
|
+
f"Could not check role for user {user_id}, assuming no role"
|
|
157
|
+
)
|
|
158
|
+
return False
|
|
159
|
+
|
|
160
|
+
def assign_role_to_user(
|
|
161
|
+
self,
|
|
162
|
+
user_id: str,
|
|
163
|
+
role_name: str,
|
|
164
|
+
idempotent: bool = True
|
|
165
|
+
) -> bool:
|
|
166
|
+
"""Assign a role to a user.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
user_id: Keycloak user UUID
|
|
170
|
+
role_name: Role name to assign (e.g., "science_module")
|
|
171
|
+
idempotent: If True, skip assignment if user already has role
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
True if role was assigned or already exists, False otherwise
|
|
175
|
+
|
|
176
|
+
Raises:
|
|
177
|
+
RoleAssignmentError: If role assignment fails
|
|
178
|
+
|
|
179
|
+
Example:
|
|
180
|
+
>>> success = manager.assign_role_to_user("user-uuid", "science_module")
|
|
181
|
+
>>> if success:
|
|
182
|
+
... print("Role assigned successfully")
|
|
183
|
+
"""
|
|
184
|
+
try:
|
|
185
|
+
# Check if role already exists (idempotent operation)
|
|
186
|
+
if idempotent and self.has_role(user_id, role_name):
|
|
187
|
+
self.logger.info(
|
|
188
|
+
f"User {user_id} already has role '{role_name}', skipping"
|
|
189
|
+
)
|
|
190
|
+
return True
|
|
191
|
+
|
|
192
|
+
# Get role object
|
|
193
|
+
role = self._admin_client.get_realm_role(role_name)
|
|
194
|
+
if not role:
|
|
195
|
+
raise RoleAssignmentError(
|
|
196
|
+
f"Role '{role_name}' does not exist in realm "
|
|
197
|
+
f"{self.config.realm_name}"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Assign role to user (realm-level role)
|
|
201
|
+
self._admin_client.assign_realm_roles(
|
|
202
|
+
user_id=user_id,
|
|
203
|
+
roles=[role]
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
self.logger.info(
|
|
207
|
+
f"Successfully assigned role '{role_name}' to user {user_id}"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
return True
|
|
211
|
+
|
|
212
|
+
except KeycloakError as e:
|
|
213
|
+
self.logger.error(
|
|
214
|
+
f"Failed to assign role '{role_name}' to user {user_id}: {e}"
|
|
215
|
+
)
|
|
216
|
+
raise RoleAssignmentError(
|
|
217
|
+
f"Could not assign role '{role_name}': {e}"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
def remove_role_from_user(self, user_id: str, role_name: str) -> bool:
|
|
221
|
+
"""Remove a role from a user.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
user_id: Keycloak user UUID
|
|
225
|
+
role_name: Role name to remove
|
|
226
|
+
|
|
227
|
+
Returns:
|
|
228
|
+
True if role was removed, False if user didn't have the role
|
|
229
|
+
|
|
230
|
+
Raises:
|
|
231
|
+
RoleAssignmentError: If role removal fails
|
|
232
|
+
"""
|
|
233
|
+
try:
|
|
234
|
+
# Check if user has the role
|
|
235
|
+
if not self.has_role(user_id, role_name):
|
|
236
|
+
self.logger.info(
|
|
237
|
+
f"User {user_id} doesn't have role '{role_name}', skipping"
|
|
238
|
+
)
|
|
239
|
+
return False
|
|
240
|
+
|
|
241
|
+
# Get role object
|
|
242
|
+
role = self._admin_client.get_realm_role(role_name)
|
|
243
|
+
|
|
244
|
+
# Remove role from user
|
|
245
|
+
self._admin_client.delete_realm_roles_of_user(
|
|
246
|
+
user_id=user_id,
|
|
247
|
+
roles=[role]
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
self.logger.info(
|
|
251
|
+
f"Successfully removed role '{role_name}' from user {user_id}"
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
return True
|
|
255
|
+
|
|
256
|
+
except KeycloakError as e:
|
|
257
|
+
self.logger.error(
|
|
258
|
+
f"Failed to remove role '{role_name}' from user {user_id}: {e}"
|
|
259
|
+
)
|
|
260
|
+
raise RoleAssignmentError(
|
|
261
|
+
f"Could not remove role '{role_name}': {e}"
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
def get_users_with_role(self, role_name: str) -> List[Dict[str, Any]]:
|
|
265
|
+
"""Get all users that have a specific role.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
role_name: Role name to query
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
List of user dictionaries
|
|
272
|
+
|
|
273
|
+
Raises:
|
|
274
|
+
RoleAssignmentError: If query fails
|
|
275
|
+
"""
|
|
276
|
+
try:
|
|
277
|
+
# Get role object
|
|
278
|
+
role = self._admin_client.get_realm_role(role_name)
|
|
279
|
+
|
|
280
|
+
# Get users with this role
|
|
281
|
+
users = self._admin_client.get_realm_role_members(role['id'])
|
|
282
|
+
|
|
283
|
+
self.logger.debug(
|
|
284
|
+
f"Found {len(users)} users with role '{role_name}'"
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
return users
|
|
288
|
+
|
|
289
|
+
except KeycloakError as e:
|
|
290
|
+
self.logger.error(f"Failed to get users with role '{role_name}': {e}")
|
|
291
|
+
raise RoleAssignmentError(
|
|
292
|
+
f"Could not retrieve users with role '{role_name}': {e}"
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
def health_check(self) -> bool:
|
|
296
|
+
"""Check if Keycloak admin connection is healthy.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
True if connection is healthy, False otherwise
|
|
300
|
+
"""
|
|
301
|
+
try:
|
|
302
|
+
# Simple API call to verify connection
|
|
303
|
+
self._admin_client.get_realm_roles()
|
|
304
|
+
return True
|
|
305
|
+
except Exception as e:
|
|
306
|
+
self.logger.error(f"Keycloak health check failed: {e}")
|
|
307
|
+
return False
|
|
308
|
+
|
|
309
|
+
def role_exists(self, role_name) -> bool:
|
|
310
|
+
"""Checks if given role exists in the realm
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
True if role exists, false otherwise
|
|
314
|
+
"""
|
|
315
|
+
try:
|
|
316
|
+
self._admin_client.get_role(role_name)
|
|
317
|
+
return True
|
|
318
|
+
except Exception:
|
|
319
|
+
return False
|
|
320
|
+
|
|
321
|
+
def create_realm_role(self, role_name, description="") -> bool:
|
|
322
|
+
role = {
|
|
323
|
+
"name": role_name,
|
|
324
|
+
"description": description,
|
|
325
|
+
}
|
|
326
|
+
try:
|
|
327
|
+
self._admin_client.create_realm_role(role)
|
|
328
|
+
print(f"Created realm role: {role_name}")
|
|
329
|
+
return True
|
|
330
|
+
except Exception as e:
|
|
331
|
+
self.logger.error(f"Keycloak realm role creation failed: {e}")
|
|
332
|
+
return False
|
|
333
|
+
|
|
334
|
+
def delete_realm_role(self, role_name) -> bool:
|
|
335
|
+
try:
|
|
336
|
+
self._admin_client.delete_realm_role(role_name)
|
|
337
|
+
print(f"Deleted realm role: {role_name}")
|
|
338
|
+
return True
|
|
339
|
+
except Exception as e:
|
|
340
|
+
self.logger.error(f"Keycloak realm role deletion failed: {e}")
|
|
341
|
+
return False
|
|
342
|
+
|
|
343
|
+
# Singleton instance (optional - can be instantiated per-use)
|
|
344
|
+
_keycloak_manager: Optional[KeycloakManager] = None
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def get_keycloak_manager(config: Optional[KeycloakAdminConfig] = None) -> KeycloakManager:
|
|
348
|
+
"""Get or create KeycloakManager singleton instance.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
config: Optional config override (uses env config if None)
|
|
352
|
+
|
|
353
|
+
Returns:
|
|
354
|
+
KeycloakManager instance
|
|
355
|
+
"""
|
|
356
|
+
global _keycloak_manager
|
|
357
|
+
|
|
358
|
+
if _keycloak_manager is None or config is not None:
|
|
359
|
+
if config is None:
|
|
360
|
+
config = KeycloakAdminConfig.from_env()
|
|
361
|
+
_keycloak_manager = KeycloakManager(config)
|
|
362
|
+
|
|
363
|
+
return _keycloak_manager
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Request ID storage and generation module.
|
|
2
|
+
|
|
3
|
+
Provides unique request ID generation and storage for
|
|
4
|
+
distributed tracing and request tracking.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
import uuid
|
|
9
|
+
|
|
10
|
+
from ..utils.store import Store
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RequestIdStore(Store):
|
|
14
|
+
"""Request ID storage and generator class.
|
|
15
|
+
|
|
16
|
+
Generates and stores unique request identifiers for
|
|
17
|
+
HTTP requests and distributed tracing.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, header_name="X-Request-ID"):
|
|
21
|
+
"""Initialize request ID store.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
header_name (str): HTTP header name for request ID.
|
|
25
|
+
"""
|
|
26
|
+
super().__init__()
|
|
27
|
+
# Use instance variable instead of class variable
|
|
28
|
+
if not hasattr(self, 'header_name'):
|
|
29
|
+
self.header_name = header_name
|
|
30
|
+
if not hasattr(self, '_counter'):
|
|
31
|
+
self._counter = 0
|
|
32
|
+
|
|
33
|
+
def generate_id(self):
|
|
34
|
+
"""Generate a new unique request ID.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
str: Unique request ID.
|
|
38
|
+
"""
|
|
39
|
+
self._counter += 1
|
|
40
|
+
timestamp = int(time.time() * 1000)
|
|
41
|
+
request_id = f"req_{timestamp}_{self._counter}_{uuid.uuid4().hex[:8]}"
|
|
42
|
+
self._set("current_request_id", request_id)
|
|
43
|
+
return request_id
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def rid(self):
|
|
47
|
+
"""Get current request ID.
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
str or None: Current request ID or None if not set.
|
|
51
|
+
"""
|
|
52
|
+
return self._get("current_request_id")
|