Encryptors 2.57__tar.gz → 2.59__tar.gz

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 (103) hide show
  1. {encryptors-2.57 → encryptors-2.59}/PKG-INFO +1 -1
  2. {encryptors-2.57 → encryptors-2.59}/setup.py +1 -1
  3. {encryptors-2.57 → encryptors-2.59}/src/Encryptors.egg-info/PKG-INFO +1 -1
  4. {encryptors-2.57 → encryptors-2.59}/src/Encryptors.egg-info/SOURCES.txt +7 -17
  5. encryptors-2.59/src/Osdental/Context/__init__.py +5 -0
  6. encryptors-2.59/src/Osdental/Decorators/SecureResolver.py +134 -0
  7. encryptors-2.59/src/Osdental/Enums/AuditType.py +5 -0
  8. encryptors-2.59/src/Osdental/Enums/ResultType.py +5 -0
  9. encryptors-2.59/src/Osdental/Helpers/AuditHelper.py +166 -0
  10. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Http/APIClient.py +39 -12
  11. encryptors-2.59/src/Osdental/Models/AuditContext.py +61 -0
  12. encryptors-2.59/src/Osdental/Services/ServiceBusAuditEmitter.py +17 -0
  13. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Services/__init__.py +7 -1
  14. encryptors-2.57/src/Osdental/Decorators/SecureResolver.py +0 -250
  15. encryptors-2.57/src/Osdental/Graphql/Extensions/AuditExtension.py +0 -196
  16. encryptors-2.57/src/Osdental/Graphql/_Exceptions/__init__.py +0 -8
  17. encryptors-2.57/src/Osdental/Graphql/_Helpers/_AuditHelper.py +0 -67
  18. encryptors-2.57/src/Osdental/Graphql/_Helpers/_ExtractAuthToken.py +0 -11
  19. encryptors-2.57/src/Osdental/Graphql/_Helpers/_TenantPolicy.py +0 -46
  20. encryptors-2.57/src/Osdental/Graphql/_Helpers/_TokenService.py +0 -44
  21. encryptors-2.57/src/Osdental/Helpers/AuditDispatcher.py +0 -169
  22. encryptors-2.57/src/Osdental/Http/_Helpers.py +0 -125
  23. encryptors-2.57/src/Osdental/Models/__init__.py +0 -0
  24. encryptors-2.57/src/Osdental/Rest/Context/RequestContext.py +0 -19
  25. encryptors-2.57/src/Osdental/Rest/Context/__init__.py +0 -0
  26. encryptors-2.57/src/Osdental/Rest/Middlewares/RequestContextMiddleware.py +0 -22
  27. encryptors-2.57/src/Osdental/Rest/Middlewares/__init__.py +0 -0
  28. encryptors-2.57/src/Osdental/Rest/__init__.py +0 -0
  29. encryptors-2.57/src/Osdental/Utils/__init__.py +0 -0
  30. encryptors-2.57/src/Osdental/__init__.py +0 -0
  31. {encryptors-2.57 → encryptors-2.59}/README.md +0 -0
  32. {encryptors-2.57 → encryptors-2.59}/setup.cfg +0 -0
  33. {encryptors-2.57 → encryptors-2.59}/src/Encryptors.egg-info/dependency_links.txt +0 -0
  34. {encryptors-2.57 → encryptors-2.59}/src/Encryptors.egg-info/entry_points.txt +0 -0
  35. {encryptors-2.57 → encryptors-2.59}/src/Encryptors.egg-info/requires.txt +0 -0
  36. {encryptors-2.57 → encryptors-2.59}/src/Encryptors.egg-info/top_level.txt +0 -0
  37. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Cache/Redis.py +0 -0
  38. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Cache/__init__.py +0 -0
  39. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Cli/__init__.py +0 -0
  40. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Constants/Constant.py +0 -0
  41. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Constants/Message.py +0 -0
  42. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Constants/__init__.py +0 -0
  43. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Database/BaseRepository.py +0 -0
  44. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Database/Connection.py +0 -0
  45. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Database/__init__.py +0 -0
  46. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Decorators/Grpc.py +0 -0
  47. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Decorators/Retry.py +0 -0
  48. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Decorators/__init__.py +0 -0
  49. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Encryptor/Aes.py +0 -0
  50. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Encryptor/Argon2.py +0 -0
  51. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Encryptor/Bcrypt.py +0 -0
  52. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Encryptor/Jwt.py +0 -0
  53. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Encryptor/Rsa.py +0 -0
  54. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Encryptor/Sha512.py +0 -0
  55. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Encryptor/__init__.py +0 -0
  56. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Enums/ErrorSource.py +0 -0
  57. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Enums/FileType.py +0 -0
  58. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Enums/GrahpqlOperation.py +0 -0
  59. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Enums/Profile.py +0 -0
  60. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Enums/StatusCode.py +0 -0
  61. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Enums/__init__.py +0 -0
  62. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Exception/ControlledException.py +0 -0
  63. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Exception/__init__.py +0 -0
  64. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Helpers/AzureClassifier.py +0 -0
  65. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Helpers/GrpcConnection.py +0 -0
  66. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Helpers/JwtTokenHelper.py +0 -0
  67. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Helpers/Resilience.py +0 -0
  68. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Helpers/ResponseDecryptor.py +0 -0
  69. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Helpers/_AuthTokenProcessor.py +0 -0
  70. {encryptors-2.57/src/Osdental/Graphql/Extensions → encryptors-2.59/src/Osdental/Helpers}/__init__.py +0 -0
  71. {encryptors-2.57/src/Osdental/Graphql/_Helpers → encryptors-2.59/src/Osdental/Http}/__init__.py +0 -0
  72. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Messaging/AzureServiceBus.py +0 -0
  73. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Messaging/Kafka.py +0 -0
  74. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Messaging/RabbitMQ.py +0 -0
  75. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Messaging/__init__.py +0 -0
  76. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Models/ApiResponse.py +0 -0
  77. /encryptors-2.57/src/Osdental/Graphql/Models/__init__.py → /encryptors-2.59/src/Osdental/Models/Graphql.py +0 -0
  78. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Models/Notification.py +0 -0
  79. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Models/Response.py +0 -0
  80. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Models/Token.py +0 -0
  81. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Models/TokenClaims.py +0 -0
  82. {encryptors-2.57/src/Osdental/Graphql → encryptors-2.59/src/Osdental/Models}/__init__.py +0 -0
  83. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Secrets/AzureKeyVaultProvider.py +0 -0
  84. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Secrets/__init__.py +0 -0
  85. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Services/JwtAuthTokenService.py +0 -0
  86. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Services/WebsocketClient.py +0 -0
  87. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Storage/AzureBlobStorage.py +0 -0
  88. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Storage/S3Storage.py +0 -0
  89. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Storage/__init__.py +0 -0
  90. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Utils/CaseConverter.py +0 -0
  91. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Utils/CodeGenerator.py +0 -0
  92. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Utils/DataNormalizer.py +0 -0
  93. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Utils/DataUtils.py +0 -0
  94. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Utils/DateUtils.py +0 -0
  95. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Utils/FileMetaData.py +0 -0
  96. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Utils/HashValidator.py +0 -0
  97. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Utils/Mapper.py +0 -0
  98. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Utils/PasswordGenerator.py +0 -0
  99. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Utils/QueryGenerator.py +0 -0
  100. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Utils/RsaUtils.py +0 -0
  101. {encryptors-2.57 → encryptors-2.59}/src/Osdental/Utils/TextProcessor.py +0 -0
  102. {encryptors-2.57/src/Osdental/Helpers → encryptors-2.59/src/Osdental/Utils}/__init__.py +0 -0
  103. {encryptors-2.57/src/Osdental/Http → encryptors-2.59/src/Osdental}/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Encryptors
3
- Version: 2.57
3
+ Version: 2.59
4
4
  Summary: End-to-end algorithm library
5
5
  Author: OSDental LLC
6
6
  Author-email: support@osdental.ai
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
  # ANDERSON REVISAR EL CACHE LOCAL DEL KEYVAULT PARA VALIDAR SI FUNCIONA
3
3
  setup(
4
4
  name="Encryptors",
5
- version="2.57",
5
+ version="2.59",
6
6
  author="OSDental LLC",
7
7
  author_email="support@osdental.ai",
8
8
  description="End-to-end algorithm library",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Encryptors
3
- Version: 2.57
3
+ Version: 2.59
4
4
  Summary: End-to-end algorithm library
5
5
  Author: OSDental LLC
6
6
  Author-email: support@osdental.ai
@@ -13,6 +13,7 @@ src/Osdental/Cli/__init__.py
13
13
  src/Osdental/Constants/Constant.py
14
14
  src/Osdental/Constants/Message.py
15
15
  src/Osdental/Constants/__init__.py
16
+ src/Osdental/Context/__init__.py
16
17
  src/Osdental/Database/BaseRepository.py
17
18
  src/Osdental/Database/Connection.py
18
19
  src/Osdental/Database/__init__.py
@@ -27,25 +28,17 @@ src/Osdental/Encryptor/Jwt.py
27
28
  src/Osdental/Encryptor/Rsa.py
28
29
  src/Osdental/Encryptor/Sha512.py
29
30
  src/Osdental/Encryptor/__init__.py
31
+ src/Osdental/Enums/AuditType.py
30
32
  src/Osdental/Enums/ErrorSource.py
31
33
  src/Osdental/Enums/FileType.py
32
34
  src/Osdental/Enums/GrahpqlOperation.py
33
35
  src/Osdental/Enums/Profile.py
36
+ src/Osdental/Enums/ResultType.py
34
37
  src/Osdental/Enums/StatusCode.py
35
38
  src/Osdental/Enums/__init__.py
36
39
  src/Osdental/Exception/ControlledException.py
37
40
  src/Osdental/Exception/__init__.py
38
- src/Osdental/Graphql/__init__.py
39
- src/Osdental/Graphql/Extensions/AuditExtension.py
40
- src/Osdental/Graphql/Extensions/__init__.py
41
- src/Osdental/Graphql/Models/__init__.py
42
- src/Osdental/Graphql/_Exceptions/__init__.py
43
- src/Osdental/Graphql/_Helpers/_AuditHelper.py
44
- src/Osdental/Graphql/_Helpers/_ExtractAuthToken.py
45
- src/Osdental/Graphql/_Helpers/_TenantPolicy.py
46
- src/Osdental/Graphql/_Helpers/_TokenService.py
47
- src/Osdental/Graphql/_Helpers/__init__.py
48
- src/Osdental/Helpers/AuditDispatcher.py
41
+ src/Osdental/Helpers/AuditHelper.py
49
42
  src/Osdental/Helpers/AzureClassifier.py
50
43
  src/Osdental/Helpers/GrpcConnection.py
51
44
  src/Osdental/Helpers/JwtTokenHelper.py
@@ -54,26 +47,23 @@ src/Osdental/Helpers/ResponseDecryptor.py
54
47
  src/Osdental/Helpers/_AuthTokenProcessor.py
55
48
  src/Osdental/Helpers/__init__.py
56
49
  src/Osdental/Http/APIClient.py
57
- src/Osdental/Http/_Helpers.py
58
50
  src/Osdental/Http/__init__.py
59
51
  src/Osdental/Messaging/AzureServiceBus.py
60
52
  src/Osdental/Messaging/Kafka.py
61
53
  src/Osdental/Messaging/RabbitMQ.py
62
54
  src/Osdental/Messaging/__init__.py
63
55
  src/Osdental/Models/ApiResponse.py
56
+ src/Osdental/Models/AuditContext.py
57
+ src/Osdental/Models/Graphql.py
64
58
  src/Osdental/Models/Notification.py
65
59
  src/Osdental/Models/Response.py
66
60
  src/Osdental/Models/Token.py
67
61
  src/Osdental/Models/TokenClaims.py
68
62
  src/Osdental/Models/__init__.py
69
- src/Osdental/Rest/__init__.py
70
- src/Osdental/Rest/Context/RequestContext.py
71
- src/Osdental/Rest/Context/__init__.py
72
- src/Osdental/Rest/Middlewares/RequestContextMiddleware.py
73
- src/Osdental/Rest/Middlewares/__init__.py
74
63
  src/Osdental/Secrets/AzureKeyVaultProvider.py
75
64
  src/Osdental/Secrets/__init__.py
76
65
  src/Osdental/Services/JwtAuthTokenService.py
66
+ src/Osdental/Services/ServiceBusAuditEmitter.py
77
67
  src/Osdental/Services/WebsocketClient.py
78
68
  src/Osdental/Services/__init__.py
79
69
  src/Osdental/Storage/AzureBlobStorage.py
@@ -0,0 +1,5 @@
1
+ from contextvars import ContextVar
2
+ from typing import Optional
3
+ from Osdental.Models.AuditContext import AuditContext
4
+
5
+ _ctx: ContextVar[Optional[AuditContext]] = ContextVar('ctx', default=None)
@@ -0,0 +1,134 @@
1
+ import logging
2
+ from functools import wraps
3
+ from typing import Callable
4
+ from graphql import GraphQLResolveInfo
5
+ from Osdental.Models.Response import Response
6
+ from Osdental.Models.Graphql import BaseGraphQLContext
7
+ from Osdental.Models.TokenClaims import UserTokenClaims
8
+ from Osdental.Models.Token import AuthToken
9
+ from Osdental.Enums.Profile import Profile
10
+ from Osdental.Exception.ControlledException import (
11
+ OSDException, AccessDeniedException
12
+ )
13
+ from Osdental.Services import IAuthTokenService
14
+ from Osdental.Helpers._AuthTokenProcessor import (
15
+ extract_bearer_token, build_auth_token, decrypt_and_parse_payload
16
+ )
17
+ from Osdental.Enums.StatusCode import StatusCode
18
+ from Osdental.Models.AuditContext import AuditContext
19
+ from Osdental.Helpers.AuditHelper import (
20
+ _build_success_payload, _build_error_payload, _build_unexpected_payload
21
+ )
22
+ from Osdental.Context import _ctx
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ def resolver(public: bool = False, action=None):
28
+
29
+ def decorator(func: Callable):
30
+ func._is_public = public
31
+
32
+ @wraps(func)
33
+ async def wrapper(obj, info: GraphQLResolveInfo, **kwargs):
34
+ try:
35
+ context: BaseGraphQLContext = info.context
36
+ container = context.container
37
+ request = context.request
38
+ headers = request.headers
39
+ settings = container.settings
40
+
41
+ token: type[AuthToken] | None = None
42
+
43
+ # ── 1. AUTENTICACIÓN ──────────────────────────────
44
+ if not public:
45
+
46
+ token_service: IAuthTokenService = container.auth_token_service
47
+ user_token = extract_bearer_token(headers)
48
+
49
+ claims: UserTokenClaims = await token_service.validate_internal_access_token(
50
+ user_token
51
+ )
52
+
53
+ # ── 2. DECRYPT DEL PAYLOAD ────────────────────────
54
+ body = await request.json()
55
+ variables = body.get("variables") or {}
56
+ encrypted_payload = kwargs.get("data") or variables.get("data")
57
+ decrypted_payload = None
58
+
59
+ if not public and encrypted_payload is not None:
60
+ decrypted_payload = decrypt_and_parse_payload(
61
+ aes_key_auth=claims.aes_key_auth,
62
+ encrypted_payload=encrypted_payload
63
+ )
64
+
65
+ kwargs["data"] = decrypted_payload
66
+
67
+ # ── 3. TENANT POLICY ──────────────────────────────
68
+ if not public:
69
+
70
+ token = build_auth_token(
71
+ claims=claims,
72
+ headers=request.headers,
73
+ decrypted_payload=decrypted_payload,
74
+ operation_type=info.operation.operation.value
75
+ )
76
+
77
+ # ── 4. CONTROL DE ACCESO POR ROL ─────────────────
78
+ if not public and action:
79
+ if Profile(token.abbreviation) not in action.allowed_roles:
80
+ raise AccessDeniedException(
81
+ status_code=StatusCode.BUSINESS_RULE_WARNING,
82
+ error="User not allowed to perform this action"
83
+ )
84
+
85
+ # ── 5. CONTEXTO DE AUDITORÍA ──────────────
86
+ # Lo construimos antes del resolver para que siempre exista
87
+
88
+ audit_ctx = AuditContext.from_request(
89
+ request=request,
90
+ settings=settings,
91
+ body=body,
92
+ operation_type=info.operation.operation.value,
93
+ token=token,
94
+ decrypted_payload=decrypted_payload,
95
+ )
96
+
97
+ _ctx.set(audit_ctx)
98
+
99
+ # ── 6. EJECUTAR RESOLVER ──────────────────
100
+ result = await func(obj, info, **kwargs)
101
+
102
+ if not isinstance(result, Response):
103
+ raise TypeError("Resolver must return a Response instance")
104
+
105
+ # ── 7. AUDITAR ÉXITO ──────────────────────
106
+ await container.audit_emitter.emit(
107
+ _build_success_payload(audit_ctx, result)
108
+ )
109
+
110
+ return result
111
+
112
+ except OSDException as e:
113
+ # logger.warning(f"Business error: {str(e)}")
114
+ await container.audit_emitter.emit(
115
+ _build_error_payload(audit_ctx, e)
116
+ )
117
+ return Response(
118
+ status=e.status_code,
119
+ message=e.message,
120
+ error=e.error
121
+ )
122
+ except Exception as e:
123
+ # logger.exception(f"Unexpected error: {str(e)}")
124
+ await container.audit_emitter.emit(
125
+ _build_unexpected_payload(audit_ctx, e)
126
+ )
127
+ return Response(
128
+ status=StatusCode.INTERNAL_SERVER_ERROR,
129
+ message="Could not process request.",
130
+ error=str(e)
131
+ )
132
+
133
+ return wrapper
134
+ return decorator
@@ -0,0 +1,5 @@
1
+ from enum import StrEnum
2
+
3
+ class AuditType(StrEnum):
4
+ INTERNAL = "MESSAGE_LOG_INTERNAL"
5
+ EXTERNAL = "MESSAGE_LOG_EXTERNAL"
@@ -0,0 +1,5 @@
1
+ from enum import StrEnum
2
+
3
+ class ResultType(StrEnum):
4
+ RESPONSE = "Response"
5
+ ERROR = "Error"
@@ -0,0 +1,166 @@
1
+ import json
2
+ from typing import Optional, List, Dict, Any
3
+ from fastapi import Request
4
+ from httpx import HTTPStatusError, RequestError
5
+ from Osdental.Models.AuditContext import AuditContext
6
+ from Osdental.Models.Response import Response
7
+ from Osdental.Models.ApiResponse import ApiResponse
8
+ from Osdental.Exception.ControlledException import OSDException
9
+ from Osdental.Enums.StatusCode import StatusCode
10
+ from Osdental.Enums.AuditType import AuditType
11
+ from Osdental.Enums.ResultType import ResultType
12
+
13
+ def build_request_payload(
14
+ request: Request,
15
+ payload: Dict[str, Any],
16
+ audit_type: str
17
+ ) -> Dict[str, Any]:
18
+
19
+ default_value = "*"
20
+
21
+ user_ip = request.headers.get("X-Forwarded-For")
22
+ if user_ip:
23
+ user_ip = user_ip.split(",")[0]
24
+ else:
25
+ user_ip = getattr(request.client, "host", "*")
26
+
27
+ SAFE_HEADERS = {
28
+ "user-agent",
29
+ "host",
30
+ "origin",
31
+ "referer",
32
+ "content-type"
33
+ }
34
+
35
+ headers = {
36
+ k: v
37
+ for k, v in request.headers.items()
38
+ if k.lower() in SAFE_HEADERS
39
+ }
40
+
41
+ return {
42
+ "idMessageLog": request.headers.get("Idmessagelog"),
43
+ "environment": payload.get("env"),
44
+ "header": json.dumps(headers),
45
+ "microServiceUrl": str(request.url),
46
+ "microServiceName": payload.get("ms_name"),
47
+ "microServiceVersion": payload.get("ms_version"),
48
+ "serviceName": payload.get("operation_name"),
49
+ "machineNameUser": request.headers.get("Machinenameuser", default_value),
50
+ "ipUser": user_ip or default_value,
51
+ "userName": payload.get("user"),
52
+ "localitation": default_value,
53
+ "httpMethod": request.method,
54
+ "messageIn": json.dumps(payload) if payload else default_value,
55
+ "auditLog": audit_type,
56
+ }
57
+
58
+ def _build_final_payload(
59
+ status_code: int | str,
60
+ type_: ResultType = ResultType.ERROR,
61
+ result: Optional[Any] = None,
62
+ error: Optional[Any] = None
63
+ ) -> Dict[str, Any]:
64
+
65
+ result = "*" if result is None else result
66
+ error = "*" if error is None else error
67
+
68
+ if isinstance(result, (dict, list)):
69
+ result = json.dumps(result)
70
+
71
+ return {
72
+ "type": type_,
73
+ "httpResponseCode": status_code,
74
+ "messageOut": result,
75
+ "errorProducer": error
76
+ }
77
+
78
+ def _build_success_payload(ctx: Optional[AuditContext], result: Response) -> Dict[str, Any]:
79
+ base = _base_payload(ctx)
80
+ return base | _build_final_payload(
81
+ type_=ResultType.RESPONSE,
82
+ status_code=result.status,
83
+ result=result.data
84
+ )
85
+
86
+ def _build_error_payload(ctx: Optional[AuditContext], exc: OSDException) -> Dict[str, Any]:
87
+ base = _base_payload(ctx)
88
+ return base | _build_final_payload(
89
+ status_code=exc.audit_status_code,
90
+ error=exc.error or exc.message
91
+ )
92
+
93
+ def _build_unexpected_payload(ctx: Optional[AuditContext], exc: Exception) -> Dict[str, Any]:
94
+ base = _base_payload(ctx)
95
+ return base | _build_final_payload(
96
+ status_code=StatusCode.INTERNAL_SERVER_ERROR,
97
+ error=str(exc)
98
+ )
99
+
100
+ def _base_payload(ctx: Optional[AuditContext], audit_type: AuditType = AuditType.INTERNAL) -> Dict[str, Any]:
101
+ if not ctx:
102
+ return {"user": "unknown", "operation_name": "unknown"}
103
+
104
+ return {
105
+ "idMessageLog": ctx.message_log_id,
106
+ "environment": ctx.env,
107
+ "header": json.dumps(ctx.headers or {}),
108
+ "microServiceUrl": ctx.url,
109
+ "microServiceName": ctx.ms_name,
110
+ "microServiceVersion": ctx.ms_version,
111
+ "serviceName": ctx.operation_name,
112
+ "machineNameUser": "*",
113
+ "ipUser": ctx.user_ip or "*",
114
+ "userName": ctx.user,
115
+ "localitation": "*",
116
+ "httpMethod": ctx.http_method,
117
+ "messageIn": json.dumps(ctx.variables) if ctx.variables else "*",
118
+ "auditLog": audit_type,
119
+ }
120
+
121
+
122
+ # ------------ FOR EXTERNAL APIS --------------------
123
+ def _build_success_http_payload(ctx: Optional[AuditContext], result: ApiResponse) -> Dict[str, Any]:
124
+ base = _base_payload(ctx, AuditType.EXTERNAL)
125
+ return base | _build_final_payload(
126
+ type_=ResultType.RESPONSE,
127
+ status_code=result.status,
128
+ result=result.data
129
+ )
130
+
131
+ def _build_error_http_payload(ctx: Optional[AuditContext], exc: HTTPStatusError) -> Dict[str, Any]:
132
+ base = _base_payload(ctx, AuditType.EXTERNAL)
133
+ return base | _build_final_payload(
134
+ status_code=exc.response.status_code,
135
+ error=exc.response.text
136
+ )
137
+
138
+ def _build_error_http_request_payload(ctx: Optional[AuditContext], exc: RequestError) -> Dict[str, Any]:
139
+ base = _base_payload(ctx, AuditType.EXTERNAL)
140
+ return base | _build_final_payload(
141
+ status_code=503,
142
+ error=str(exc)
143
+ )
144
+
145
+ def _build_unexpected_http_payload(ctx: Optional[AuditContext], exc: Exception) -> Dict[str, Any]:
146
+ base = _base_payload(ctx, AuditType.EXTERNAL)
147
+ return base | _build_final_payload(
148
+ status_code=StatusCode.INTERNAL_SERVER_ERROR,
149
+ error=str(exc)
150
+ )
151
+
152
+ def _build_success_graphql_payload(ctx: Optional[AuditContext], result: Dict[str, Any]) -> Dict[str, Any]:
153
+ base = _base_payload(ctx, AuditType.EXTERNAL)
154
+ return base | _build_final_payload(
155
+ type_=ResultType.RESPONSE,
156
+ status_code=200,
157
+ result=result
158
+ )
159
+
160
+
161
+ def _build_error_graphql_payload(ctx: Optional[AuditContext], errors: List[Dict[str, Any]]) -> Dict[str, Any]:
162
+ base = _base_payload(ctx, AuditType.EXTERNAL)
163
+ return base | _build_final_payload(
164
+ status_code=StatusCode.INTERNAL_SERVER_ERROR,
165
+ error=json.dumps(errors)
166
+ )
@@ -3,12 +3,16 @@ from typing import Optional, Dict, Any
3
3
  import httpx
4
4
  from http import HTTPMethod
5
5
  from Osdental.Decorators.Retry import rest_retry
6
- from Osdental.Http._Helpers import (
7
- audit_success, audit_http_error, audit_exception_error,
8
- audit_unknown_error, audit_graphql_error
9
- )
6
+ from Osdental.Models.ApiResponse import ApiResponse
7
+ from Osdental.Services import IAuditEmitter
10
8
  from Osdental.Exception.ControlledException import HttpClientException
11
9
  from Osdental.Enums.StatusCode import StatusCode
10
+ from Osdental.Context import _ctx
11
+ from Osdental.Helpers.AuditHelper import (
12
+ _build_success_http_payload, _build_error_http_payload,
13
+ _build_error_http_request_payload, _build_unexpected_http_payload,
14
+ _build_success_graphql_payload, _build_error_graphql_payload
15
+ )
12
16
 
13
17
  logger = logging.getLogger(__name__)
14
18
 
@@ -16,9 +20,12 @@ class APIClient:
16
20
 
17
21
  def __init__(
18
22
  self,
23
+ audit: Optional[IAuditEmitter] = None,
19
24
  timeout: Optional[httpx.Timeout] = None,
20
25
  limits: Optional[httpx.Limits] = None
21
26
  ):
27
+ self._audit = audit
28
+
22
29
  self._client = httpx.AsyncClient(
23
30
  follow_redirects=True,
24
31
  timeout=timeout or httpx.Timeout(10.0, read=20.0),
@@ -42,24 +49,37 @@ class APIClient:
42
49
  **kwargs
43
50
  )
44
51
 
52
+ audit_ctx = _ctx.get()
53
+
54
+ audit_ctx.headers = kwargs["headers"] or {}
55
+
45
56
  response.raise_for_status()
46
57
 
47
- audit_success(response, method, url, kwargs)
58
+ api_res = ApiResponse(
59
+ status=response.status_code,
60
+ data=response.text
61
+ )
62
+
63
+ await self._audit_emit.emit(
64
+ _build_success_http_payload(audit_ctx, api_res)
65
+ )
48
66
 
49
67
  return response
50
68
 
51
69
  except httpx.HTTPStatusError as exc:
52
70
 
53
- audit_http_error(exc.response, method, url, kwargs)
71
+ await self._audit_emit.emit(
72
+ _build_error_http_payload(audit_ctx, exc)
73
+ )
74
+
54
75
  raise HttpClientException(
55
76
  status_code=StatusCode.BAD_GATEWAY,
56
77
  error=exc.response.text,
57
- raw_status_code=exc.response.status_code,
58
78
  ) from exc
59
79
 
60
80
  except httpx.RequestError as exc:
61
81
 
62
- audit_exception_error(exc, method, url, kwargs)
82
+ _build_error_http_request_payload(audit_ctx, exc)
63
83
  raise HttpClientException(
64
84
  status_code=StatusCode.GATEWAY_TIMEOUT,
65
85
  error=str(exc),
@@ -67,7 +87,7 @@ class APIClient:
67
87
 
68
88
  except Exception as exc:
69
89
 
70
- audit_unknown_error(exc, method, url, kwargs)
90
+ _build_unexpected_http_payload(audit_ctx, exc)
71
91
 
72
92
  raise
73
93
 
@@ -119,13 +139,20 @@ class APIClient:
119
139
 
120
140
  data = response.json()
121
141
 
122
- if "errors" in data:
142
+ audit_ctx = _ctx.get()
143
+
144
+ audit_ctx.headers = headers or {}
123
145
 
146
+ _build_success_graphql_payload(audit_ctx, data)
147
+
148
+ if "errors" in data:
149
+
150
+ errors = data['errors']
124
151
  logger.error(
125
- f"GraphQL error: {data['errors']}"
152
+ f"GraphQL error: {errors}"
126
153
  )
127
154
 
128
- audit_graphql_error(data["errors"], url, payload)
155
+ _build_error_graphql_payload(audit_ctx, errors)
129
156
  raise HttpClientException(
130
157
  message="GraphQL execution failed",
131
158
  error=str(data["errors"])
@@ -0,0 +1,61 @@
1
+ from dataclasses import dataclass
2
+ from typing import Optional, Dict, Any
3
+ from fastapi import Request
4
+
5
+
6
+ @dataclass
7
+ class AuditContext:
8
+ env: str
9
+ ms_name: str
10
+ ms_version: str
11
+ is_auditable: bool
12
+ operation_type: str
13
+ operation_name: str
14
+ query: Optional[str]
15
+ variables: Optional[Dict[str, Any]]
16
+ user: str
17
+ message_log_id: Optional[str]
18
+ decrypted_key: Optional[str] = None
19
+ user_ip: Optional[str] = None
20
+ headers: Optional[Dict[str, str]] = None
21
+ url: Optional[str] = None
22
+ http_method: Optional[str] = None
23
+
24
+ SAFE_HEADERS = {"user-agent", "host", "origin", "referer", "content-type"}
25
+
26
+ @classmethod
27
+ def from_request(
28
+ cls,
29
+ request: Request,
30
+ settings,
31
+ body: Dict,
32
+ operation_type: str,
33
+ token=None,
34
+ decrypted_payload=None,
35
+ ) -> "AuditContext":
36
+
37
+ user_ip = request.headers.get("X-Forwarded-For")
38
+ user_ip = user_ip.split(",")[0] if user_ip else getattr(request.client, "host", "*")
39
+
40
+ safe_headers = {
41
+ k: v for k, v in request.headers.items()
42
+ if k.lower() in cls.SAFE_HEADERS
43
+ }
44
+
45
+ return cls(
46
+ env=settings.environment,
47
+ ms_name=settings.microservice_name,
48
+ ms_version=settings.microservice_version,
49
+ is_auditable=settings.is_auditable,
50
+ operation_type=operation_type,
51
+ operation_name=body.get("operationName", "UnknownOperation"),
52
+ query=body.get("query"),
53
+ variables=decrypted_payload,
54
+ user=token.user_full_name if token else "Public",
55
+ message_log_id=request.headers.get("Idmessagelog"),
56
+ decrypted_key=token.aes_key_auth if token else None,
57
+ user_ip=user_ip,
58
+ headers=safe_headers,
59
+ url=str(request.url),
60
+ http_method=request.method,
61
+ )
@@ -0,0 +1,17 @@
1
+ import json
2
+ from typing import Dict, Any
3
+ from Osdental.Messaging import IMessageQueue
4
+ from Osdental.Storage import IStorageService
5
+
6
+ class ServiceBusAuditEmitter:
7
+
8
+ def __init__(self, messaging: IMessageQueue, storage: IStorageService):
9
+ self._messaging = messaging
10
+ self._storage = storage
11
+
12
+ async def emit(self, payload: Dict[str, Any]) -> None:
13
+ message_log_id = payload.get("idMessageLog")
14
+ data = json.dumps(payload)
15
+ blob_name = f"audits/{message_log_id}.text"
16
+ await self._storage.upload(blob_name, data)
17
+ await self._messaging.send_message(blob_name)
@@ -1,4 +1,5 @@
1
1
  from abc import ABC, abstractmethod
2
+ from typing import Protocol, Any, Dict
2
3
  from Osdental.Models.Notification import Notification
3
4
 
4
5
  class INotificationPublisher(ABC):
@@ -44,4 +45,9 @@ class IAuthTokenService(ABC):
44
45
  self,
45
46
  token: str,
46
47
  ) -> ActionTokenClaims:
47
- pass
48
+ pass
49
+
50
+
51
+ class IAuditEmitter(Protocol):
52
+ async def emit(self, payload: Dict[str, Any]) -> None:
53
+ ...