Encryptors 2.54__tar.gz → 2.57__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 (99) hide show
  1. {encryptors-2.54 → encryptors-2.57}/PKG-INFO +1 -1
  2. {encryptors-2.54 → encryptors-2.57}/setup.py +1 -1
  3. {encryptors-2.54 → encryptors-2.57}/src/Encryptors.egg-info/PKG-INFO +1 -1
  4. {encryptors-2.54 → encryptors-2.57}/src/Encryptors.egg-info/SOURCES.txt +1 -2
  5. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Constants/Message.py +0 -2
  6. encryptors-2.57/src/Osdental/Decorators/SecureResolver.py +250 -0
  7. encryptors-2.57/src/Osdental/Enums/ErrorSource.py +5 -0
  8. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Exception/ControlledException.py +26 -3
  9. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Graphql/_Helpers/_AuditHelper.py +12 -12
  10. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Helpers/AuditDispatcher.py +14 -20
  11. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Helpers/_AuthTokenProcessor.py +1 -2
  12. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Http/APIClient.py +3 -2
  13. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Services/JwtAuthTokenService.py +0 -3
  14. encryptors-2.54/src/Osdental/Decorators/SecureResolver.py +0 -139
  15. encryptors-2.54/src/Osdental/Models/AuditConfig.py +0 -9
  16. encryptors-2.54/src/Osdental/Models/_Audit.py +0 -14
  17. {encryptors-2.54 → encryptors-2.57}/README.md +0 -0
  18. {encryptors-2.54 → encryptors-2.57}/setup.cfg +0 -0
  19. {encryptors-2.54 → encryptors-2.57}/src/Encryptors.egg-info/dependency_links.txt +0 -0
  20. {encryptors-2.54 → encryptors-2.57}/src/Encryptors.egg-info/entry_points.txt +0 -0
  21. {encryptors-2.54 → encryptors-2.57}/src/Encryptors.egg-info/requires.txt +0 -0
  22. {encryptors-2.54 → encryptors-2.57}/src/Encryptors.egg-info/top_level.txt +0 -0
  23. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Cache/Redis.py +0 -0
  24. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Cache/__init__.py +0 -0
  25. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Cli/__init__.py +0 -0
  26. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Constants/Constant.py +0 -0
  27. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Constants/__init__.py +0 -0
  28. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Database/BaseRepository.py +0 -0
  29. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Database/Connection.py +0 -0
  30. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Database/__init__.py +0 -0
  31. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Decorators/Grpc.py +0 -0
  32. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Decorators/Retry.py +0 -0
  33. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Decorators/__init__.py +0 -0
  34. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Encryptor/Aes.py +0 -0
  35. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Encryptor/Argon2.py +0 -0
  36. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Encryptor/Bcrypt.py +0 -0
  37. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Encryptor/Jwt.py +0 -0
  38. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Encryptor/Rsa.py +0 -0
  39. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Encryptor/Sha512.py +0 -0
  40. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Encryptor/__init__.py +0 -0
  41. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Enums/FileType.py +0 -0
  42. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Enums/GrahpqlOperation.py +0 -0
  43. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Enums/Profile.py +0 -0
  44. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Enums/StatusCode.py +0 -0
  45. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Enums/__init__.py +0 -0
  46. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Exception/__init__.py +0 -0
  47. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Graphql/Extensions/AuditExtension.py +0 -0
  48. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Graphql/Extensions/__init__.py +0 -0
  49. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Graphql/Models/__init__.py +0 -0
  50. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Graphql/_Exceptions/__init__.py +0 -0
  51. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Graphql/_Helpers/_ExtractAuthToken.py +0 -0
  52. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Graphql/_Helpers/_TenantPolicy.py +0 -0
  53. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Graphql/_Helpers/_TokenService.py +0 -0
  54. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Graphql/_Helpers/__init__.py +0 -0
  55. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Graphql/__init__.py +0 -0
  56. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Helpers/AzureClassifier.py +0 -0
  57. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Helpers/GrpcConnection.py +0 -0
  58. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Helpers/JwtTokenHelper.py +0 -0
  59. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Helpers/Resilience.py +0 -0
  60. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Helpers/ResponseDecryptor.py +0 -0
  61. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Helpers/__init__.py +0 -0
  62. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Http/_Helpers.py +0 -0
  63. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Http/__init__.py +0 -0
  64. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Messaging/AzureServiceBus.py +0 -0
  65. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Messaging/Kafka.py +0 -0
  66. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Messaging/RabbitMQ.py +0 -0
  67. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Messaging/__init__.py +0 -0
  68. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Models/ApiResponse.py +0 -0
  69. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Models/Notification.py +0 -0
  70. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Models/Response.py +0 -0
  71. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Models/Token.py +0 -0
  72. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Models/TokenClaims.py +0 -0
  73. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Models/__init__.py +0 -0
  74. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Rest/Context/RequestContext.py +0 -0
  75. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Rest/Context/__init__.py +0 -0
  76. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Rest/Middlewares/RequestContextMiddleware.py +0 -0
  77. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Rest/Middlewares/__init__.py +0 -0
  78. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Rest/__init__.py +0 -0
  79. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Secrets/AzureKeyVaultProvider.py +0 -0
  80. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Secrets/__init__.py +0 -0
  81. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Services/WebsocketClient.py +0 -0
  82. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Services/__init__.py +0 -0
  83. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Storage/AzureBlobStorage.py +0 -0
  84. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Storage/S3Storage.py +0 -0
  85. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Storage/__init__.py +0 -0
  86. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Utils/CaseConverter.py +0 -0
  87. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Utils/CodeGenerator.py +0 -0
  88. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Utils/DataNormalizer.py +0 -0
  89. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Utils/DataUtils.py +0 -0
  90. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Utils/DateUtils.py +0 -0
  91. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Utils/FileMetaData.py +0 -0
  92. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Utils/HashValidator.py +0 -0
  93. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Utils/Mapper.py +0 -0
  94. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Utils/PasswordGenerator.py +0 -0
  95. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Utils/QueryGenerator.py +0 -0
  96. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Utils/RsaUtils.py +0 -0
  97. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Utils/TextProcessor.py +0 -0
  98. {encryptors-2.54 → encryptors-2.57}/src/Osdental/Utils/__init__.py +0 -0
  99. {encryptors-2.54 → encryptors-2.57}/src/Osdental/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Encryptors
3
- Version: 2.54
3
+ Version: 2.57
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.54",
5
+ version="2.57",
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.54
3
+ Version: 2.57
4
4
  Summary: End-to-end algorithm library
5
5
  Author: OSDental LLC
6
6
  Author-email: support@osdental.ai
@@ -27,6 +27,7 @@ src/Osdental/Encryptor/Jwt.py
27
27
  src/Osdental/Encryptor/Rsa.py
28
28
  src/Osdental/Encryptor/Sha512.py
29
29
  src/Osdental/Encryptor/__init__.py
30
+ src/Osdental/Enums/ErrorSource.py
30
31
  src/Osdental/Enums/FileType.py
31
32
  src/Osdental/Enums/GrahpqlOperation.py
32
33
  src/Osdental/Enums/Profile.py
@@ -60,12 +61,10 @@ src/Osdental/Messaging/Kafka.py
60
61
  src/Osdental/Messaging/RabbitMQ.py
61
62
  src/Osdental/Messaging/__init__.py
62
63
  src/Osdental/Models/ApiResponse.py
63
- src/Osdental/Models/AuditConfig.py
64
64
  src/Osdental/Models/Notification.py
65
65
  src/Osdental/Models/Response.py
66
66
  src/Osdental/Models/Token.py
67
67
  src/Osdental/Models/TokenClaims.py
68
- src/Osdental/Models/_Audit.py
69
68
  src/Osdental/Models/__init__.py
70
69
  src/Osdental/Rest/__init__.py
71
70
  src/Osdental/Rest/Context/RequestContext.py
@@ -31,8 +31,6 @@ class Message:
31
31
  UNEXPECTED_DECRYPTED_DATA_FORMAT_MSG = "Unexpected format in decrypted data."
32
32
 
33
33
 
34
- DATABASE_EXECUTION_ERROR_MSG = "An unexpected error occurred while executing a database operation."
35
- DATABASE_INTEGRITY_ERROR_MSG = "A database integrity constraint was violated during execution."
36
34
  QUERY_NOT_PROVIDED_MSG = "Query not provided. Please include a valid query in your request."
37
35
  FILE_PATH_NOT_PROVIDED_MSG = "File path not provided."
38
36
 
@@ -0,0 +1,250 @@
1
+ import logging
2
+ from functools import wraps
3
+ from typing import Callable, Dict, Any
4
+ from graphql import GraphQLResolveInfo
5
+ from Osdental.Models.Response import Response
6
+ from Osdental.Graphql.Models 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.Storage import IStorageService
19
+ from Osdental.Messaging import IMessageQueue
20
+ from Osdental.Graphql._Helpers._AuditHelper import AuditHelper
21
+ from Osdental.Constants.Constant import Constant
22
+ from Osdental.Helpers.ResponseDecryptor import decryptor_data, VALID_TYPES
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ async def __test(
28
+ messaging: IMessageQueue,
29
+ storage: IStorageService,
30
+ request_payload: Dict[str, Any],
31
+ request,
32
+ result: Response,
33
+ decrypted_key: str,
34
+ audit_type: str
35
+ ) -> None:
36
+
37
+ import json
38
+
39
+ if not request_payload.get("is_auditable"):
40
+ logger.info("Auditing is disabled. Skipping audit processing.")
41
+ return
42
+
43
+ operation_name = request_payload.get("operation_name")
44
+ if operation_name == 'UnknownOperation':
45
+ logger.info("Skipping audit: no data in GraphQL response")
46
+ return
47
+
48
+ request_audit_payload = AuditHelper.build_request_payload(
49
+ request=request,
50
+ payload=request_payload,
51
+ audit_type=audit_type
52
+ )
53
+
54
+ status_code = result.status
55
+ message = result.message
56
+ data = result.data
57
+ error = result.error if result.error else message
58
+
59
+ if audit_type == Constant.MESSAGE_LOG_INTERNAL:
60
+ ERROR_PREFIXES = ("ERROR", "WARNING")
61
+ if isinstance(status_code, int):
62
+ is_error = not (200 <= status_code < 300)
63
+ elif isinstance(status_code, str):
64
+ is_error = status_code.upper().startswith(ERROR_PREFIXES)
65
+ else:
66
+ try:
67
+ is_error = not (200 <= status_code < 300)
68
+ except ValueError:
69
+ is_error = True
70
+
71
+ if is_error:
72
+ err_payload = AuditHelper.build_final_payload(
73
+ _type="ERROR",
74
+ status_code=status_code,
75
+ error=error
76
+ )
77
+ audit_message = request_audit_payload | err_payload
78
+ else:
79
+ if isinstance(result, Response):
80
+
81
+ encryption_type = result.encryption_type
82
+
83
+
84
+ if encryption_type in VALID_TYPES and decrypted_key and data:
85
+ data = decryptor_data(encryption_type, decrypted_key, data)
86
+
87
+ res_payload = AuditHelper.build_final_payload(
88
+ _type="RESPONSE",
89
+ status_code=status_code,
90
+ result=data
91
+ )
92
+ audit_message = request_audit_payload | res_payload
93
+
94
+ message_log_id = request.headers.get("Idmessagelog")
95
+ if not message_log_id:
96
+ logger.warning("No Idmessagelog header found. Skipping audit storage.")
97
+ return
98
+
99
+ data = json.dumps(audit_message)
100
+ # print(f"data: {data}")
101
+ # blob_name = f"audits/{message_log_id}.text"
102
+ # success = await storage.upload(blob_name, data)
103
+
104
+ # if not success:
105
+ # logger.error("Failed to upload audit log to storage. Skipping message dispatch.")
106
+ # return
107
+
108
+ # await messaging.send_message(blob_name)
109
+
110
+ # dispatcher.dispatch(
111
+ # request=request,
112
+ # request_payload=request_payload,
113
+ # result=result,
114
+ # metadata={
115
+ # "decrypted_key": decrypted_key
116
+ # },
117
+ # audit_type="MESSAGE_LOG_INTERNAL"
118
+ # )
119
+
120
+ def resolver(public: bool = False, action=None):
121
+
122
+ def decorator(func: Callable):
123
+ func._is_public = public
124
+
125
+ @wraps(func)
126
+ async def wrapper(obj, info: GraphQLResolveInfo, **kwargs):
127
+ try:
128
+ context: BaseGraphQLContext = info.context
129
+ container = context.container
130
+ request = context.request
131
+ headers = request.headers
132
+ settings = container.settings
133
+
134
+ token: type[AuthToken] | None = None
135
+
136
+ # ── 1. AUTENTICACIÓN ──────────────────────────────
137
+ if not public:
138
+
139
+ token_service: IAuthTokenService = container.auth_token_service
140
+ user_token = extract_bearer_token(headers)
141
+
142
+ claims: UserTokenClaims = await token_service.validate_internal_access_token(
143
+ user_token
144
+ )
145
+
146
+ # ── 2. DECRYPT DEL PAYLOAD ────────────────────────
147
+ body = await request.json()
148
+ variables = body.get("variables") or {}
149
+ encrypted_payload = kwargs.get("data") or variables.get("data")
150
+ decrypted_payload = None
151
+
152
+ if not public and encrypted_payload is not None:
153
+ decrypted_payload = decrypt_and_parse_payload(
154
+ aes_key_auth=claims.aes_key_auth,
155
+ encrypted_payload=encrypted_payload
156
+ )
157
+
158
+ kwargs["data"] = decrypted_payload
159
+
160
+ # ── 3. TENANT POLICY ──────────────────────────────
161
+ if not public:
162
+
163
+ token = build_auth_token(
164
+ claims=claims,
165
+ headers=request.headers,
166
+ decrypted_payload=decrypted_payload,
167
+ operation_type=info.operation.operation.value
168
+ )
169
+
170
+ # ── 4. CONTROL DE ACCESO POR ROL ─────────────────
171
+ if not public and action:
172
+ if Profile(token.abbreviation) not in action.allowed_roles:
173
+ raise AccessDeniedException(
174
+ status_code=StatusCode.BUSINESS_RULE_WARNING,
175
+ error="User not allowed to perform this action"
176
+ )
177
+
178
+ # ── 5. EJECUTAR RESOLVER ──────────────────────────
179
+ result = await func(obj, info, **kwargs)
180
+
181
+ if not isinstance(result, Response):
182
+ raise TypeError("Resolver must return a Response instance")
183
+
184
+
185
+
186
+ request_payload = {
187
+ "env": settings.environment,
188
+ "ms_name": settings.microservice_name,
189
+ "ms_version": settings.microservice_version,
190
+ "is_auditable": settings.is_auditable,
191
+ "operation_type": info.operation.operation.value,
192
+ "operation_name": body.get("operationName", "UnknownOperation"),
193
+ "query": body.get("query"),
194
+ "variables": decrypted_payload,
195
+ "user": (
196
+ token.user_full_name
197
+ if token
198
+ else "Public"
199
+ )
200
+ }
201
+
202
+ if isinstance(result, Response):
203
+ decrypted_key = result.key if not token else token.aes_key_auth
204
+
205
+ await __test(
206
+ messaging=container.az_sb_audit,
207
+ storage=container.az_blob_storage,
208
+ request_payload=request_payload,
209
+ request=request,
210
+ decrypted_key=decrypted_key,
211
+ result=result,
212
+ audit_type="MESSAGE_LOG_INTERNAL"
213
+ )
214
+
215
+ return result
216
+
217
+ except OSDException as e:
218
+ # logger.warning(f"Business error: {str(e)}")
219
+ if e.is_external:
220
+ err_payload = AuditHelper.build_final_payload(
221
+ _type="ERROR",
222
+ status_code=e.status_code,
223
+ error=result.error if result.error else e.message
224
+ )
225
+ else:
226
+ err_payload = AuditHelper.build_final_payload(
227
+ _type="ERROR",
228
+ status_code=e.audit_status_code,
229
+ error=result.error if result.error else e.message
230
+ )
231
+ return Response(
232
+ status=e.status_code,
233
+ message=e.message,
234
+ error=e.error
235
+ )
236
+ except Exception as e:
237
+ # logger.exception(f"Unexpected error: {str(e)}")
238
+ err_payload = AuditHelper.build_final_payload(
239
+ _type="ERROR",
240
+ status_code=StatusCode.INTERNAL_SERVER_ERROR,
241
+ error=str(e)
242
+ )
243
+ return Response(
244
+ status=StatusCode.INTERNAL_SERVER_ERROR,
245
+ message="Could not process request.",
246
+ error=str(e)
247
+ )
248
+
249
+ return wrapper
250
+ return decorator
@@ -0,0 +1,5 @@
1
+ from enum import Enum
2
+
3
+ class ErrorSource(str, Enum):
4
+ INTERNAL = "internal"
5
+ EXTERNAL = "external"
@@ -1,6 +1,7 @@
1
1
  from typing import Any
2
2
  from Osdental.Enums.StatusCode import StatusCode
3
3
  from Osdental.Constants.Message import Message
4
+ from Osdental.Enums.ErrorSource import ErrorSource
4
5
 
5
6
  class OSDException(Exception):
6
7
  """ Base class for all custom exceptions. """
@@ -9,16 +10,28 @@ class OSDException(Exception):
9
10
  message: str = Message.UNEXPECTED_ERROR_MSG,
10
11
  error: str = None,
11
12
  status_code: Any = StatusCode.INTERNAL_SERVER_ERROR,
13
+ source: ErrorSource = ErrorSource.INTERNAL,
14
+ raw_status_code: Any = None,
12
15
  ):
13
16
  super().__init__(error)
14
17
  self.message = message
15
18
  self.error = error
16
19
  self.status_code = status_code
17
-
20
+ self.source = source
21
+ self.raw_status_code = raw_status_code
18
22
 
19
23
  def __str__(self):
20
24
  error_detail = self.error if self.error else "None"
21
- return f"[{self.status_code}] {self.message} | {error_detail}"
25
+ return f"[{self.source} | {self.status_code}] {self.message} | {error_detail}]"
26
+
27
+ @property
28
+ def is_external(self) -> bool:
29
+ return self.source == ErrorSource.EXTERNAL
30
+
31
+ @property
32
+ def audit_status_code(self) -> Any:
33
+ """El que va a la auditoría: real si existe, propio si no."""
34
+ return self.raw_status_code if self.raw_status_code is not None else self.status_code
22
35
 
23
36
  class UnauthorizedException(OSDException):
24
37
  def __init__(
@@ -29,6 +42,8 @@ class UnauthorizedException(OSDException):
29
42
  ):
30
43
  super().__init__(message=message, error=error, status_code=status_code)
31
44
 
45
+
46
+
32
47
  class AccessDeniedException(OSDException):
33
48
  def __init__(
34
49
  self,
@@ -119,5 +134,13 @@ class HttpClientException(OSDException):
119
134
  message: str = Message.UNEXPECTED_ERROR_MSG,
120
135
  error: str = None,
121
136
  status_code: str = StatusCode.INTERNAL_SERVER_ERROR,
137
+ source: ErrorSource = ErrorSource.EXTERNAL,
138
+ raw_status_code: Any = None,
122
139
  ):
123
- super().__init__(message=message, error=error, status_code=status_code)
140
+ super().__init__(
141
+ message=message,
142
+ error=error,
143
+ status_code=status_code,
144
+ source=source,
145
+ raw_status_code=raw_status_code
146
+ )
@@ -1,14 +1,14 @@
1
1
  import json
2
2
  from typing import Dict, Literal, Any, Optional
3
- from Osdental.Models._Audit import Audit
4
3
 
5
4
  class AuditHelper:
6
5
 
7
6
  @staticmethod
8
- async def build_request_payload(audit: Audit) -> Dict[str, Any]:
9
- request = audit.request
10
- audit_config = audit.audit_config
11
- payload = audit.payload
7
+ def build_request_payload(
8
+ request,
9
+ payload: Dict[str, Any],
10
+ audit_type: str
11
+ ) -> Dict[str, Any]:
12
12
 
13
13
  default_value = "*"
14
14
 
@@ -30,19 +30,19 @@ class AuditHelper:
30
30
 
31
31
  return {
32
32
  "idMessageLog": request.headers.get("Idmessagelog"),
33
- "environment": audit_config.environment,
33
+ "environment": payload.get("env"),
34
34
  "header": json.dumps(headers),
35
35
  "microServiceUrl": str(request.url),
36
- "microServiceName": audit_config.microservice_name,
37
- "microServiceVersion": audit_config.microservice_version,
38
- "serviceName": audit.operation_name,
36
+ "microServiceName": payload.get("ms_name"),
37
+ "microServiceVersion": payload.get("ms_version"),
38
+ "serviceName": payload.get("operation_name"),
39
39
  "machineNameUser": request.headers.get("Machinenameuser", default_value),
40
40
  "ipUser": user_ip or default_value,
41
- "userName": audit.full_name,
41
+ "userName": payload.get("user"),
42
42
  "localitation": default_value,
43
43
  "httpMethod": request.method,
44
44
  "messageIn": json.dumps(payload) if payload else default_value,
45
- "auditLog": audit.audit_type
45
+ "auditLog": audit_type,
46
46
  }
47
47
 
48
48
  @staticmethod
@@ -64,4 +64,4 @@ class AuditHelper:
64
64
  "httpResponseCode": status_code,
65
65
  "messageOut": result,
66
66
  "errorProducer": error
67
- }
67
+ }
@@ -2,12 +2,10 @@ import logging
2
2
  import asyncio
3
3
  from typing import Dict, Any, Optional
4
4
  from fastapi import Request
5
- from Osdental.Models.AuditConfig import AuditConfig
6
5
  from Osdental.Messaging import IMessageQueue
7
6
  from Osdental.Graphql._Helpers._AuditHelper import AuditHelper
8
7
  from Osdental.Storage import IStorageService
9
8
  from Osdental.Helpers.ResponseDecryptor import decryptor_data, VALID_TYPES
10
- from Osdental.Models._Audit import Audit
11
9
  from Osdental.Models.Response import Response
12
10
  from Osdental.Models.ApiResponse import ApiResponse
13
11
  from Osdental.Constants.Constant import Constant
@@ -21,19 +19,14 @@ class AuditDispatcher:
21
19
  self,
22
20
  messaging: IMessageQueue,
23
21
  storage: IStorageService,
24
- audit_config: AuditConfig,
25
22
  max_queue_size: int = 5000
26
23
  ):
27
24
  self._messaging = messaging
28
25
  self._storage = storage
29
- self._audit_config = audit_config
26
+
30
27
  self._queue = asyncio.Queue(maxsize=max_queue_size)
31
28
  self._worker_task = None
32
29
 
33
- # Se llama en startup
34
- def start(self):
35
- self._worker_task = asyncio.create_task(self._worker())
36
-
37
30
  # Se llama en shutdown
38
31
  async def stop(self):
39
32
  # Esperar a que la cola se vacíe
@@ -56,6 +49,9 @@ class AuditDispatcher:
56
49
  audit_type: str = None
57
50
  ):
58
51
  try:
52
+ if self._worker_task is None:
53
+ self._worker_task = asyncio.create_task(self._worker())
54
+
59
55
  payload = {
60
56
  "request": request,
61
57
  "request_payload": request_payload,
@@ -87,31 +83,28 @@ class AuditDispatcher:
87
83
 
88
84
  # Logica
89
85
  async def _process(self, payload: Dict[str, Any]) -> None:
90
- if not self._audit_config.is_auditable:
91
- logger.info("Auditing is disabled. Skipping audit processing.")
92
- return
93
-
86
+
94
87
  import json
95
-
88
+
96
89
  request = payload["request"]
97
90
  request_payload = payload["request_payload"]
98
91
  result: Response | ApiResponse = payload["result"]
99
92
  audit_type = payload["audit_type"]
93
+
94
+ if not request_payload.get("is_auditable"):
95
+ logger.info("Auditing is disabled. Skipping audit processing.")
96
+ return
100
97
 
101
98
  operation_name = request_payload.get("operation_name")
102
99
  if operation_name == 'UnknownOperation':
103
100
  logger.info("Skipping audit: no data in GraphQL response")
104
101
  return
105
102
 
106
- audit = Audit(
107
- audit_type=audit_type,
103
+ request_audit_payload = await AuditHelper.build_request_payload(
108
104
  request=request,
109
- audit_config=self._audit_config,
110
- operation_name=operation_name,
111
- full_name=request_payload.get("user", "Joe Doe"),
112
- payload=request_payload.get("variables")
105
+ payload=request_payload,
106
+ audit_type=audit_type
113
107
  )
114
- request_audit_payload = await AuditHelper.build_request_payload(audit=audit)
115
108
 
116
109
  status_code = result.status
117
110
  message = result.message
@@ -168,6 +161,7 @@ class AuditDispatcher:
168
161
  data = json.dumps(audit_message)
169
162
  blob_name = f"audits/{message_log_id}.text"
170
163
  success = await self._storage.upload(blob_name, data)
164
+
171
165
  if not success:
172
166
  logger.error("Failed to upload audit log to storage. Skipping message dispatch.")
173
167
  return
@@ -17,7 +17,6 @@ def extract_bearer_token(headers: Headers) -> str:
17
17
  return authorization.removeprefix("Bearer ").strip()
18
18
 
19
19
 
20
-
21
20
  def build_auth_token(
22
21
  claims: UserTokenClaims,
23
22
  headers: Headers,
@@ -31,6 +30,7 @@ def build_auth_token(
31
30
  decrypted_payload.get("idExternalEnterprise")
32
31
  if decrypted_payload else None
33
32
  )
33
+ mk_id_external_enterprise = None
34
34
 
35
35
  if external_enterprise_req:
36
36
  final_id = external_enterprise_req
@@ -50,7 +50,6 @@ def build_auth_token(
50
50
 
51
51
  final_id = str(UUID(int=0))
52
52
 
53
-
54
53
  token = AuthToken(
55
54
  id_token=claims.id_token,
56
55
  id_user=claims.id_user,
@@ -53,7 +53,8 @@ class APIClient:
53
53
  audit_http_error(exc.response, method, url, kwargs)
54
54
  raise HttpClientException(
55
55
  status_code=StatusCode.BAD_GATEWAY,
56
- error=exc.response.text
56
+ error=exc.response.text,
57
+ raw_status_code=exc.response.status_code,
57
58
  ) from exc
58
59
 
59
60
  except httpx.RequestError as exc:
@@ -61,7 +62,7 @@ class APIClient:
61
62
  audit_exception_error(exc, method, url, kwargs)
62
63
  raise HttpClientException(
63
64
  status_code=StatusCode.GATEWAY_TIMEOUT,
64
- error=str(exc)
65
+ error=str(exc),
65
66
  ) from exc
66
67
 
67
68
  except Exception as exc:
@@ -79,7 +79,6 @@ class JwtAuthTokenService(IAuthTokenService):
79
79
  claims = self._jwt.validate_token(
80
80
  token=token,
81
81
  options=TokenValidationOptions(
82
- validate_expiration=True,
83
82
  validate_issuer=False,
84
83
  validate_audience=False,
85
84
  )
@@ -103,7 +102,6 @@ class JwtAuthTokenService(IAuthTokenService):
103
102
  claims = self._jwt.validate_token(
104
103
  token=token,
105
104
  options=TokenValidationOptions(
106
- validate_expiration=True,
107
105
  validate_issuer=False,
108
106
  validate_audience=False,
109
107
  )
@@ -127,7 +125,6 @@ class JwtAuthTokenService(IAuthTokenService):
127
125
  options=TokenValidationOptions(
128
126
  issuer=self._issuer,
129
127
  audience=self._audience,
130
- validate_expiration=True,
131
128
  validate_issuer=True,
132
129
  validate_audience=True,
133
130
  )
@@ -1,139 +0,0 @@
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.Graphql.Models 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
-
19
-
20
- logger = logging.getLogger(__name__)
21
-
22
-
23
- def __test(dispatcher, request, request_payload, result, decrypted_key):
24
- dispatcher.dispatch(
25
- request=request,
26
- request_payload=request_payload,
27
- result=result,
28
- metadata={
29
- "decrypted_key": decrypted_key
30
- },
31
- audit_type="MESSAGE_LOG_INTERNAL"
32
- )
33
-
34
- def resolver(public: bool = False, action=None):
35
-
36
- def decorator(func: Callable):
37
- func._is_public = public
38
-
39
- @wraps(func)
40
- async def wrapper(obj, info: GraphQLResolveInfo, **kwargs):
41
- try:
42
- context: BaseGraphQLContext = info.context
43
- request = context.request
44
- token: type[AuthToken] | None = None
45
-
46
- # ── 1. AUTENTICACIÓN ──────────────────────────────
47
- if not public:
48
-
49
- headers = request.headers
50
- container = context.container
51
- token_service: IAuthTokenService = container.auth_token_service
52
- user_token = extract_bearer_token(headers)
53
-
54
- claims: UserTokenClaims = token_service.validate_internal_access_token(
55
- user_token
56
- )
57
-
58
- # ── 2. DECRYPT DEL PAYLOAD ────────────────────────
59
- body = await request.json()
60
- variables = body.get("variables") or {}
61
- encrypted_payload = kwargs.get("data") or variables.get("data")
62
- decrypted_payload = None
63
-
64
- if not public and encrypted_payload is not None:
65
- decrypted_payload = decrypt_and_parse_payload(
66
- aes_key_auth=claims.aes_key_auth,
67
- encrypted_payload=encrypted_payload
68
- )
69
-
70
- kwargs["data"] = decrypted_payload
71
-
72
- # ── 3. TENANT POLICY ──────────────────────────────
73
- if not public:
74
-
75
- token = build_auth_token(
76
- claims=claims,
77
- headers=request.headers,
78
- decrypted_payload=decrypted_payload,
79
- operation_type=info.operation.operation.value
80
- )
81
-
82
- # ── 4. CONTROL DE ACCESO POR ROL ─────────────────
83
- if not public and action:
84
- if Profile(token.abbreviation) not in action.allowed_roles:
85
- raise AccessDeniedException(
86
- status_code=StatusCode.BUSINESS_RULE_WARNING,
87
- error="User not allowed to perform this action"
88
- )
89
-
90
- # ── 5. EJECUTAR RESOLVER ──────────────────────────
91
- result = await func(obj, info, **kwargs)
92
-
93
- if not isinstance(result, Response):
94
- raise TypeError("Resolver must return a Response instance")
95
-
96
- dispatcher = request.app.state.audit_dispatcher
97
-
98
- request_payload = {
99
- "operation_type": info.operation.operation.value,
100
- "operation_name": body.get("operationName", "UnknownOperation"),
101
- "query": body.get("query"),
102
- "variables": decrypted_payload,
103
- "user": (
104
- token.user_full_name
105
- if token
106
- else "Public"
107
- )
108
- }
109
-
110
- if isinstance(result, Response):
111
- decrypted_key = result.key if not token else token.aes_key_auth
112
-
113
- __test(
114
- dispatcher=dispatcher,
115
- request_payload=request_payload,
116
- request=request,
117
- decrypted_key=decrypted_key,
118
- result=result
119
- )
120
-
121
- return result
122
-
123
- except OSDException as e:
124
- logger.warning(f"Business error: {str(e)}")
125
- return Response(
126
- status=e.status_code,
127
- message=e.message,
128
- error=e.error
129
- )
130
- except Exception as e:
131
- logger.exception(f"Unexpected error: {str(e)}")
132
- return Response(
133
- status=StatusCode.INTERNAL_SERVER_ERROR,
134
- message="Could not process request.",
135
- error=str(e)
136
- )
137
-
138
- return wrapper
139
- return decorator
@@ -1,9 +0,0 @@
1
- from typing import Optional
2
- from dataclasses import dataclass
3
-
4
- @dataclass(frozen=True)
5
- class AuditConfig:
6
- environment: str
7
- microservice_name: str
8
- microservice_version: str
9
- is_auditable: Optional[bool] = True
@@ -1,14 +0,0 @@
1
- from dataclasses import dataclass, field
2
- from typing import Any, Optional
3
- from fastapi import Request
4
- from Osdental.Models.AuditConfig import AuditConfig
5
-
6
-
7
- @dataclass
8
- class Audit:
9
- request: Request
10
- audit_config: AuditConfig
11
- payload: Any = field(default_factory=dict)
12
- audit_type: str = field(default="MESSAGE_LOG_INTERNAL")
13
- operation_name: Optional[str] = "Unknown"
14
- full_name: Optional[str] = "Joe Doe"
File without changes
File without changes