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.
Files changed (70) hide show
  1. blackant/__init__.py +31 -0
  2. blackant/auth/__init__.py +10 -0
  3. blackant/auth/blackant_auth.py +518 -0
  4. blackant/auth/keycloak_manager.py +363 -0
  5. blackant/auth/request_id.py +52 -0
  6. blackant/auth/role_assignment.py +443 -0
  7. blackant/auth/tokens.py +57 -0
  8. blackant/client.py +400 -0
  9. blackant/config/__init__.py +0 -0
  10. blackant/config/docker_config.py +457 -0
  11. blackant/config/keycloak_admin_config.py +107 -0
  12. blackant/docker/__init__.py +12 -0
  13. blackant/docker/builder.py +616 -0
  14. blackant/docker/client.py +983 -0
  15. blackant/docker/dao.py +462 -0
  16. blackant/docker/registry.py +172 -0
  17. blackant/exceptions.py +111 -0
  18. blackant/http/__init__.py +8 -0
  19. blackant/http/client.py +125 -0
  20. blackant/patterns/__init__.py +1 -0
  21. blackant/patterns/singleton.py +20 -0
  22. blackant/services/__init__.py +10 -0
  23. blackant/services/dao.py +414 -0
  24. blackant/services/registry.py +635 -0
  25. blackant/utils/__init__.py +8 -0
  26. blackant/utils/initialization.py +32 -0
  27. blackant/utils/logging.py +337 -0
  28. blackant/utils/request_id.py +13 -0
  29. blackant/utils/store.py +50 -0
  30. blackant_sdk-1.0.2.dist-info/METADATA +117 -0
  31. blackant_sdk-1.0.2.dist-info/RECORD +70 -0
  32. blackant_sdk-1.0.2.dist-info/WHEEL +5 -0
  33. blackant_sdk-1.0.2.dist-info/top_level.txt +5 -0
  34. calculation/__init__.py +0 -0
  35. calculation/base.py +26 -0
  36. calculation/errors.py +2 -0
  37. calculation/impl/__init__.py +0 -0
  38. calculation/impl/my_calculation.py +144 -0
  39. calculation/impl/simple_calc.py +53 -0
  40. calculation/impl/test.py +1 -0
  41. calculation/impl/test_calc.py +36 -0
  42. calculation/loader.py +227 -0
  43. notifinations/__init__.py +8 -0
  44. notifinations/mail_sender.py +212 -0
  45. storage/__init__.py +0 -0
  46. storage/errors.py +10 -0
  47. storage/factory.py +26 -0
  48. storage/interface.py +19 -0
  49. storage/minio.py +106 -0
  50. task/__init__.py +0 -0
  51. task/dao.py +38 -0
  52. task/errors.py +10 -0
  53. task/log_adapter.py +11 -0
  54. task/parsers/__init__.py +0 -0
  55. task/parsers/base.py +13 -0
  56. task/parsers/callback.py +40 -0
  57. task/parsers/cmd_args.py +52 -0
  58. task/parsers/freetext.py +19 -0
  59. task/parsers/objects.py +50 -0
  60. task/parsers/request.py +56 -0
  61. task/resource.py +84 -0
  62. task/states/__init__.py +0 -0
  63. task/states/base.py +14 -0
  64. task/states/error.py +47 -0
  65. task/states/idle.py +12 -0
  66. task/states/ready.py +51 -0
  67. task/states/running.py +21 -0
  68. task/states/set_up.py +40 -0
  69. task/states/tear_down.py +29 -0
  70. task/task.py +358 -0
@@ -0,0 +1,443 @@
1
+ """Role Assignment Manager
2
+
3
+ Orchestrates automatic role assignment after service publication.
4
+ Extracts user information from JWT token and assigns science_module role.
5
+
6
+ Supports two modes:
7
+ 1. Direct Keycloak access (requires admin credentials in SDK)
8
+ 2. SDK Services (credentials stay server-side, more secure)
9
+
10
+ Author: Balázs Milán (milan.balazs@uni-obuda.hu)
11
+ """
12
+
13
+ from typing import Optional, Dict, Any
14
+ import logging
15
+ import jwt
16
+ import requests
17
+ from datetime import datetime, timezone
18
+
19
+ from .keycloak_manager import KeycloakManager, get_keycloak_manager
20
+ from ..config.keycloak_admin_config import KeycloakAdminConfig
21
+ from ..config.docker_config import get_docker_config
22
+ from ..exceptions import (
23
+ RoleAssignmentError,
24
+ TokenValidationError,
25
+ BlackAntException
26
+ )
27
+
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ class RoleAssignmentManager:
33
+ """Manager for automatic role assignment after service publication.
34
+
35
+ Handles the workflow of:
36
+ 1. Extracting user ID from JWT token
37
+ 2. Checking if science_module role exists
38
+ 3. Assigning role if missing
39
+ 4. Audit logging
40
+
41
+ Supports two modes:
42
+ - Direct Keycloak: Uses KeycloakManager with admin credentials
43
+ - SDK Services: Delegates to backend, credentials stay server-side
44
+
45
+ Args:
46
+ keycloak_manager: Optional KeycloakManager instance
47
+ role_name: Role to assign (default: "science_module")
48
+
49
+ Example:
50
+ >>> manager = RoleAssignmentManager()
51
+ >>> success = manager.assign_role_after_publish(
52
+ ... user_token="eyJhbGciOiJSUzI1NiIs...",
53
+ ... service_name="my-calculation"
54
+ ... )
55
+ """
56
+
57
+ DEFAULT_ROLE = "science_module"
58
+
59
+ def __init__(
60
+ self,
61
+ keycloak_manager: Optional[KeycloakManager] = None,
62
+ role_name: str = DEFAULT_ROLE
63
+ ):
64
+ """Initialize role assignment manager.
65
+
66
+ Args:
67
+ keycloak_manager: Optional KeycloakManager (creates new if None)
68
+ role_name: Role to assign (default: "science_module")
69
+ """
70
+ self.keycloak_manager = keycloak_manager or get_keycloak_manager()
71
+ self.role_name = role_name
72
+ self.logger = logger
73
+
74
+ # Check SDK Services configuration
75
+ try:
76
+ self._docker_config = get_docker_config()
77
+ self._use_sdk_services = (
78
+ self._docker_config.sdk_services.use_for_roles and
79
+ self._docker_config.sdk_services.url
80
+ )
81
+ except Exception:
82
+ self._docker_config = None
83
+ self._use_sdk_services = False
84
+
85
+ def extract_user_id_from_token(self, token: str) -> str:
86
+ """Extract Keycloak user UUID from JWT token.
87
+
88
+ Args:
89
+ token: JWT token (Bearer token from authentication)
90
+
91
+ Returns:
92
+ User UUID string
93
+
94
+ Raises:
95
+ TokenValidationError: If token is invalid or user ID not found
96
+
97
+ Example:
98
+ >>> user_id = manager.extract_user_id_from_token(token)
99
+ >>> print(user_id)
100
+ 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'
101
+ """
102
+ try:
103
+ # Decode JWT without verification (we trust our own tokens)
104
+ # In production, you should verify signature with public key
105
+ decoded = jwt.decode(
106
+ token,
107
+ options={"verify_signature": False} # Trust our own tokens
108
+ )
109
+
110
+ # Extract user ID from 'sub' claim (standard JWT claim)
111
+ user_id = decoded.get('sub')
112
+ if not user_id:
113
+ raise TokenValidationError(
114
+ "Token does not contain 'sub' claim (user ID)"
115
+ )
116
+
117
+ # Additional info for logging
118
+ username = decoded.get('preferred_username', 'unknown')
119
+ email = decoded.get('email', 'unknown')
120
+
121
+ self.logger.debug(
122
+ f"Extracted user ID from token: {user_id} "
123
+ f"(username: {username}, email: {email})"
124
+ )
125
+
126
+ return user_id
127
+
128
+ except jwt.InvalidTokenError as e:
129
+ self.logger.error(f"Invalid JWT token: {e}")
130
+ raise TokenValidationError(f"Invalid JWT token: {e}")
131
+ except Exception as e:
132
+ self.logger.error(f"Failed to extract user ID from token: {e}")
133
+ raise TokenValidationError(f"Token parsing failed: {e}")
134
+
135
+ def assign_role_after_publish(
136
+ self,
137
+ user_token: str,
138
+ service_name: str,
139
+ metadata: Optional[Dict[str, Any]] = None
140
+ ) -> bool:
141
+ """Assign science_module role to user after service publication.
142
+
143
+ This is the main entry point called by ServiceRegistry after
144
+ successful Docker image push to registry.
145
+
146
+ If SDK Services is configured, delegates to backend.
147
+ Otherwise, uses direct Keycloak access with admin credentials.
148
+
149
+ Args:
150
+ user_token: JWT token of the user who published the service
151
+ service_name: Name of the published service
152
+ metadata: Optional metadata for audit logging
153
+
154
+ Returns:
155
+ True if role was assigned or already exists, False on failure
156
+
157
+ Raises:
158
+ RoleAssignmentError: If assignment fails critically
159
+
160
+ Example:
161
+ >>> success = manager.assign_role_after_publish(
162
+ ... user_token=jwt_token,
163
+ ... service_name="sentiment-analysis",
164
+ ... metadata={"image": "env.blackant.app/..."}
165
+ ... )
166
+ """
167
+ # Check if SDK Services should handle role assignment
168
+ if self._use_sdk_services:
169
+ return self._assign_role_via_sdk_services(
170
+ user_token, service_name, metadata
171
+ )
172
+
173
+ # Use direct Keycloak access
174
+ return self._assign_role_direct(user_token, service_name, metadata)
175
+
176
+ def _assign_role_via_sdk_services(
177
+ self,
178
+ user_token: str,
179
+ service_name: str,
180
+ metadata: Optional[Dict[str, Any]] = None
181
+ ) -> bool:
182
+ """Assign role via SDK Services endpoint.
183
+
184
+ Delegates role assignment to sdk-services backend,
185
+ keeping admin credentials server-side.
186
+
187
+ Args:
188
+ user_token: JWT token of the user
189
+ service_name: Name of the published service
190
+ metadata: Optional metadata
191
+
192
+ Returns:
193
+ True if successful, False otherwise
194
+ """
195
+ try:
196
+ # Extract user ID from token
197
+ user_id = self.extract_user_id_from_token(user_token)
198
+
199
+ assign_url = self._docker_config.sdk_services.roles_assign_url
200
+ self.logger.info(f"Assigning role via SDK Services: {assign_url}")
201
+
202
+ # Call SDK Services endpoint
203
+ response = requests.post(
204
+ assign_url,
205
+ json={
206
+ "user_id": user_id,
207
+ "role_name": self.role_name
208
+ },
209
+ headers={
210
+ "Content-Type": "application/json",
211
+ "Authorization": f"Bearer {user_token}"
212
+ },
213
+ timeout=self._docker_config.sdk_services.timeout,
214
+ verify=False # For dev environments
215
+ )
216
+
217
+ if response.status_code == 200:
218
+ result = response.json()
219
+ if result.get("success"):
220
+ self.logger.info(
221
+ f"Role '{self.role_name}' assigned to user {user_id} "
222
+ f"via SDK Services"
223
+ )
224
+ self._audit_log(
225
+ user_id=user_id,
226
+ service_name=service_name,
227
+ action="role_assigned_via_sdk_services",
228
+ result="success",
229
+ metadata=metadata
230
+ )
231
+ return True
232
+ else:
233
+ error_msg = result.get("message", "Unknown error")
234
+ self.logger.error(f"SDK Services role assign failed: {error_msg}")
235
+ return False
236
+ else:
237
+ self.logger.error(
238
+ f"SDK Services role assign failed with status {response.status_code}"
239
+ )
240
+ return False
241
+
242
+ except TokenValidationError as e:
243
+ self.logger.error(f"Token validation failed: {e}")
244
+ return False
245
+ except Exception as e:
246
+ self.logger.error(f"SDK Services role assign error: {e}")
247
+ return False
248
+
249
+ def _assign_role_direct(
250
+ self,
251
+ user_token: str,
252
+ service_name: str,
253
+ metadata: Optional[Dict[str, Any]] = None
254
+ ) -> bool:
255
+ """Assign role directly via Keycloak Manager.
256
+
257
+ Uses local admin credentials from configuration.
258
+
259
+ Args:
260
+ user_token: JWT token of the user
261
+ service_name: Name of the published service
262
+ metadata: Optional metadata
263
+
264
+ Returns:
265
+ True if successful, False otherwise
266
+ """
267
+ try:
268
+ # Step 1: Extract user ID from JWT token
269
+ self.logger.info(
270
+ f"Starting role assignment for service '{service_name}'"
271
+ )
272
+
273
+ user_id = self.extract_user_id_from_token(user_token)
274
+
275
+ default = True
276
+ if not default:
277
+ self.role_name=service_name;
278
+
279
+ #check if role exists
280
+ if not self.keycloak_manager.role_exists(self.role_name):
281
+ self.keycloak_manager.create_realm_role(self.role_name)
282
+
283
+ # Step 2: Check if user already has the role (idempotent)
284
+ has_role = self.keycloak_manager.has_role(user_id, self.role_name)
285
+
286
+ if has_role:
287
+ self.logger.info(
288
+ f"User {user_id} already has role '{self.role_name}', "
289
+ f"no assignment needed"
290
+ )
291
+ self._audit_log(
292
+ user_id=user_id,
293
+ service_name=service_name,
294
+ action="role_check",
295
+ result="already_exists",
296
+ metadata=metadata
297
+ )
298
+ return True
299
+
300
+ # Step 3: Assign the role
301
+ self.logger.info(
302
+ f"Assigning role '{self.role_name}' to user {user_id} "
303
+ f"for service '{service_name}'"
304
+ )
305
+
306
+ success = self.keycloak_manager.assign_role_to_user(
307
+ user_id=user_id,
308
+ role_name=self.role_name,
309
+ idempotent=True
310
+ )
311
+
312
+ if success:
313
+ self.logger.info(
314
+ f"Successfully assigned role '{self.role_name}' to user {user_id}"
315
+ )
316
+ self._audit_log(
317
+ user_id=user_id,
318
+ service_name=service_name,
319
+ action="role_assigned",
320
+ result="success",
321
+ metadata=metadata
322
+ )
323
+ return True
324
+ else:
325
+ self.logger.warning(
326
+ f"Role assignment returned False for user {user_id}"
327
+ )
328
+ self._audit_log(
329
+ user_id=user_id,
330
+ service_name=service_name,
331
+ action="role_assignment",
332
+ result="failed",
333
+ metadata=metadata
334
+ )
335
+ return False
336
+
337
+ except TokenValidationError as e:
338
+ # Token parsing error - log and return False (don't raise)
339
+ self.logger.error(
340
+ f"Token validation failed for service '{service_name}': {e}"
341
+ )
342
+ self._audit_log(
343
+ user_id="unknown",
344
+ service_name=service_name,
345
+ action="role_assignment",
346
+ result="token_error",
347
+ error=str(e),
348
+ metadata=metadata
349
+ )
350
+ return False
351
+
352
+ except RoleAssignmentError as e:
353
+ # Keycloak communication error - log and return False
354
+ self.logger.error(
355
+ f"Role assignment failed for service '{service_name}': {e}"
356
+ )
357
+ self._audit_log(
358
+ user_id=user_id if 'user_id' in locals() else "unknown",
359
+ service_name=service_name,
360
+ action="role_assignment",
361
+ result="keycloak_error",
362
+ error=str(e),
363
+ metadata=metadata
364
+ )
365
+ return False
366
+
367
+ except Exception as e:
368
+ # Unexpected error - log and return False
369
+ self.logger.exception(
370
+ f"Unexpected error during role assignment for service '{service_name}': {e}"
371
+ )
372
+ self._audit_log(
373
+ user_id=user_id if 'user_id' in locals() else "unknown",
374
+ service_name=service_name,
375
+ action="role_assignment",
376
+ result="unexpected_error",
377
+ error=str(e),
378
+ metadata=metadata
379
+ )
380
+ return False
381
+
382
+ def _audit_log(
383
+ self,
384
+ user_id: str,
385
+ service_name: str,
386
+ action: str,
387
+ result: str,
388
+ error: Optional[str] = None,
389
+ metadata: Optional[Dict[str, Any]] = None
390
+ ) -> None:
391
+ """Create audit log entry for role assignment.
392
+
393
+ Logs all role assignment operations for security auditing.
394
+
395
+ Args:
396
+ user_id: Keycloak user UUID
397
+ service_name: Service name
398
+ action: Action performed (e.g., "role_assigned")
399
+ result: Result of action (e.g., "success", "failed")
400
+ error: Optional error message
401
+ metadata: Optional additional metadata
402
+ """
403
+ audit_entry = {
404
+ "timestamp": datetime.now(timezone.utc).isoformat(),
405
+ "user_id": user_id,
406
+ "service_name": service_name,
407
+ "action": action,
408
+ "role": self.role_name,
409
+ "result": result,
410
+ }
411
+
412
+ if error:
413
+ audit_entry["error"] = error
414
+
415
+ if metadata:
416
+ audit_entry["metadata"] = metadata
417
+
418
+ # Log as structured JSON for audit trail
419
+ self.logger.info(
420
+ f"AUDIT: {audit_entry}",
421
+ extra={"audit": audit_entry}
422
+ )
423
+
424
+ # TODO: Send to centralized audit logging system
425
+ # (e.g., Elasticsearch, CloudWatch, etc.)
426
+
427
+
428
+ # Singleton instance (optional)
429
+ _role_assignment_manager: Optional[RoleAssignmentManager] = None
430
+
431
+
432
+ def get_role_assignment_manager() -> RoleAssignmentManager:
433
+ """Get or create RoleAssignmentManager singleton.
434
+
435
+ Returns:
436
+ RoleAssignmentManager instance
437
+ """
438
+ global _role_assignment_manager
439
+
440
+ if _role_assignment_manager is None:
441
+ _role_assignment_manager = RoleAssignmentManager()
442
+
443
+ return _role_assignment_manager
@@ -0,0 +1,57 @@
1
+ """Authentication token storage module.
2
+
3
+ Provides storage for user and admin authentication tokens
4
+ using the Store base class with thread-safe access.
5
+ """
6
+
7
+ from ..utils.store import Store
8
+
9
+
10
+ class AuthTokenStore(Store):
11
+ """Authentication token storage class.
12
+
13
+ Stores user and admin authentication tokens for later use
14
+ in API requests. Uses thread-local storage for thread safety.
15
+ """
16
+
17
+ @property
18
+ def user_token(self):
19
+ """str or None: User-level Bearer token for API authentication."""
20
+ return self._get("user")
21
+
22
+ @user_token.setter
23
+ def user_token(self, value):
24
+ """Set the user-level Bearer token.
25
+
26
+ Args:
27
+ value (str or None): The Bearer token string or None to clear.
28
+ """
29
+ self._set("user", value)
30
+
31
+ @property
32
+ def admin_token(self):
33
+ """str or None: Admin-level Bearer token for privileged API access."""
34
+ return self._get("admin")
35
+
36
+ @admin_token.setter
37
+ def admin_token(self, value):
38
+ """Set the admin-level Bearer token.
39
+
40
+ Args:
41
+ value (str or None): The admin Bearer token string or None to clear.
42
+ """
43
+ self._set("admin", value)
44
+
45
+ def clean_up(self):
46
+ """Clean up stored tokens.
47
+
48
+ Removes both user and admin tokens from storage.
49
+ """
50
+ try:
51
+ self._del("user")
52
+ except AttributeError:
53
+ pass
54
+ try:
55
+ self._del("admin")
56
+ except AttributeError:
57
+ pass