Encryptors 2.56__tar.gz → 2.58__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 (102) hide show
  1. {encryptors-2.56 → encryptors-2.58}/PKG-INFO +1 -1
  2. {encryptors-2.56 → encryptors-2.58}/setup.py +1 -1
  3. {encryptors-2.56 → encryptors-2.58}/src/Encryptors.egg-info/PKG-INFO +1 -1
  4. {encryptors-2.56 → encryptors-2.58}/src/Encryptors.egg-info/SOURCES.txt +5 -2
  5. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Constants/Message.py +0 -2
  6. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Decorators/SecureResolver.py +30 -55
  7. encryptors-2.58/src/Osdental/Enums/AuditType.py +5 -0
  8. encryptors-2.58/src/Osdental/Enums/ErrorSource.py +5 -0
  9. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Exception/ControlledException.py +26 -3
  10. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Graphql/_Helpers/_AuditHelper.py +2 -2
  11. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Helpers/AuditDispatcher.py +0 -2
  12. encryptors-2.58/src/Osdental/Helpers/AuditHelper.py +118 -0
  13. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Helpers/_AuthTokenProcessor.py +1 -2
  14. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Http/APIClient.py +3 -2
  15. encryptors-2.58/src/Osdental/Models/AuditContext.py +61 -0
  16. encryptors-2.58/src/Osdental/Services/ServiceBusAuditEmitter.py +17 -0
  17. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Services/__init__.py +7 -1
  18. encryptors-2.56/src/Osdental/Models/AuditConfig.py +0 -9
  19. encryptors-2.56/src/Osdental/Models/_Audit.py +0 -14
  20. {encryptors-2.56 → encryptors-2.58}/README.md +0 -0
  21. {encryptors-2.56 → encryptors-2.58}/setup.cfg +0 -0
  22. {encryptors-2.56 → encryptors-2.58}/src/Encryptors.egg-info/dependency_links.txt +0 -0
  23. {encryptors-2.56 → encryptors-2.58}/src/Encryptors.egg-info/entry_points.txt +0 -0
  24. {encryptors-2.56 → encryptors-2.58}/src/Encryptors.egg-info/requires.txt +0 -0
  25. {encryptors-2.56 → encryptors-2.58}/src/Encryptors.egg-info/top_level.txt +0 -0
  26. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Cache/Redis.py +0 -0
  27. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Cache/__init__.py +0 -0
  28. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Cli/__init__.py +0 -0
  29. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Constants/Constant.py +0 -0
  30. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Constants/__init__.py +0 -0
  31. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Database/BaseRepository.py +0 -0
  32. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Database/Connection.py +0 -0
  33. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Database/__init__.py +0 -0
  34. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Decorators/Grpc.py +0 -0
  35. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Decorators/Retry.py +0 -0
  36. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Decorators/__init__.py +0 -0
  37. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Encryptor/Aes.py +0 -0
  38. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Encryptor/Argon2.py +0 -0
  39. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Encryptor/Bcrypt.py +0 -0
  40. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Encryptor/Jwt.py +0 -0
  41. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Encryptor/Rsa.py +0 -0
  42. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Encryptor/Sha512.py +0 -0
  43. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Encryptor/__init__.py +0 -0
  44. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Enums/FileType.py +0 -0
  45. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Enums/GrahpqlOperation.py +0 -0
  46. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Enums/Profile.py +0 -0
  47. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Enums/StatusCode.py +0 -0
  48. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Enums/__init__.py +0 -0
  49. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Exception/__init__.py +0 -0
  50. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Graphql/Extensions/AuditExtension.py +0 -0
  51. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Graphql/Extensions/__init__.py +0 -0
  52. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Graphql/Models/__init__.py +0 -0
  53. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Graphql/_Exceptions/__init__.py +0 -0
  54. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Graphql/_Helpers/_ExtractAuthToken.py +0 -0
  55. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Graphql/_Helpers/_TenantPolicy.py +0 -0
  56. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Graphql/_Helpers/_TokenService.py +0 -0
  57. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Graphql/_Helpers/__init__.py +0 -0
  58. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Graphql/__init__.py +0 -0
  59. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Helpers/AzureClassifier.py +0 -0
  60. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Helpers/GrpcConnection.py +0 -0
  61. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Helpers/JwtTokenHelper.py +0 -0
  62. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Helpers/Resilience.py +0 -0
  63. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Helpers/ResponseDecryptor.py +0 -0
  64. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Helpers/__init__.py +0 -0
  65. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Http/_Helpers.py +0 -0
  66. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Http/__init__.py +0 -0
  67. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Messaging/AzureServiceBus.py +0 -0
  68. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Messaging/Kafka.py +0 -0
  69. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Messaging/RabbitMQ.py +0 -0
  70. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Messaging/__init__.py +0 -0
  71. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Models/ApiResponse.py +0 -0
  72. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Models/Notification.py +0 -0
  73. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Models/Response.py +0 -0
  74. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Models/Token.py +0 -0
  75. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Models/TokenClaims.py +0 -0
  76. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Models/__init__.py +0 -0
  77. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Rest/Context/RequestContext.py +0 -0
  78. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Rest/Context/__init__.py +0 -0
  79. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Rest/Middlewares/RequestContextMiddleware.py +0 -0
  80. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Rest/Middlewares/__init__.py +0 -0
  81. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Rest/__init__.py +0 -0
  82. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Secrets/AzureKeyVaultProvider.py +0 -0
  83. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Secrets/__init__.py +0 -0
  84. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Services/JwtAuthTokenService.py +0 -0
  85. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Services/WebsocketClient.py +0 -0
  86. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Storage/AzureBlobStorage.py +0 -0
  87. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Storage/S3Storage.py +0 -0
  88. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Storage/__init__.py +0 -0
  89. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Utils/CaseConverter.py +0 -0
  90. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Utils/CodeGenerator.py +0 -0
  91. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Utils/DataNormalizer.py +0 -0
  92. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Utils/DataUtils.py +0 -0
  93. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Utils/DateUtils.py +0 -0
  94. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Utils/FileMetaData.py +0 -0
  95. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Utils/HashValidator.py +0 -0
  96. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Utils/Mapper.py +0 -0
  97. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Utils/PasswordGenerator.py +0 -0
  98. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Utils/QueryGenerator.py +0 -0
  99. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Utils/RsaUtils.py +0 -0
  100. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Utils/TextProcessor.py +0 -0
  101. {encryptors-2.56 → encryptors-2.58}/src/Osdental/Utils/__init__.py +0 -0
  102. {encryptors-2.56 → encryptors-2.58}/src/Osdental/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Encryptors
3
- Version: 2.56
3
+ Version: 2.58
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.56",
5
+ version="2.58",
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.56
3
+ Version: 2.58
4
4
  Summary: End-to-end algorithm library
5
5
  Author: OSDental LLC
6
6
  Author-email: support@osdental.ai
@@ -27,6 +27,8 @@ 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/AuditType.py
31
+ src/Osdental/Enums/ErrorSource.py
30
32
  src/Osdental/Enums/FileType.py
31
33
  src/Osdental/Enums/GrahpqlOperation.py
32
34
  src/Osdental/Enums/Profile.py
@@ -45,6 +47,7 @@ src/Osdental/Graphql/_Helpers/_TenantPolicy.py
45
47
  src/Osdental/Graphql/_Helpers/_TokenService.py
46
48
  src/Osdental/Graphql/_Helpers/__init__.py
47
49
  src/Osdental/Helpers/AuditDispatcher.py
50
+ src/Osdental/Helpers/AuditHelper.py
48
51
  src/Osdental/Helpers/AzureClassifier.py
49
52
  src/Osdental/Helpers/GrpcConnection.py
50
53
  src/Osdental/Helpers/JwtTokenHelper.py
@@ -60,12 +63,11 @@ src/Osdental/Messaging/Kafka.py
60
63
  src/Osdental/Messaging/RabbitMQ.py
61
64
  src/Osdental/Messaging/__init__.py
62
65
  src/Osdental/Models/ApiResponse.py
63
- src/Osdental/Models/AuditConfig.py
66
+ src/Osdental/Models/AuditContext.py
64
67
  src/Osdental/Models/Notification.py
65
68
  src/Osdental/Models/Response.py
66
69
  src/Osdental/Models/Token.py
67
70
  src/Osdental/Models/TokenClaims.py
68
- src/Osdental/Models/_Audit.py
69
71
  src/Osdental/Models/__init__.py
70
72
  src/Osdental/Rest/__init__.py
71
73
  src/Osdental/Rest/Context/RequestContext.py
@@ -75,6 +77,7 @@ src/Osdental/Rest/Middlewares/__init__.py
75
77
  src/Osdental/Secrets/AzureKeyVaultProvider.py
76
78
  src/Osdental/Secrets/__init__.py
77
79
  src/Osdental/Services/JwtAuthTokenService.py
80
+ src/Osdental/Services/ServiceBusAuditEmitter.py
78
81
  src/Osdental/Services/WebsocketClient.py
79
82
  src/Osdental/Services/__init__.py
80
83
  src/Osdental/Storage/AzureBlobStorage.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
 
@@ -1,6 +1,6 @@
1
1
  import logging
2
2
  from functools import wraps
3
- from typing import Callable, Dict, Any
3
+ from typing import Callable
4
4
  from graphql import GraphQLResolveInfo
5
5
  from Osdental.Models.Response import Response
6
6
  from Osdental.Graphql.Models import BaseGraphQLContext
@@ -15,29 +15,15 @@ from Osdental.Helpers._AuthTokenProcessor import (
15
15
  extract_bearer_token, build_auth_token, decrypt_and_parse_payload
16
16
  )
17
17
  from Osdental.Enums.StatusCode import StatusCode
18
- from Osdental.Helpers.AuditDispatcher import AuditDispatcher
18
+ from Osdental.Models.AuditContext import AuditContext
19
+ from Osdental.Helpers.AuditHelper import (
20
+ _build_success_payload, _build_error_payload, _build_unexpected_payload
21
+ )
19
22
 
20
23
 
21
24
  logger = logging.getLogger(__name__)
22
25
 
23
26
 
24
- def __test(
25
- dispatcher: AuditDispatcher,
26
- request,
27
- request_payload: Dict[str, Any],
28
- result: Response,
29
- decrypted_key: str
30
- ) -> None:
31
-
32
- dispatcher.dispatch(
33
- request=request,
34
- request_payload=request_payload,
35
- result=result,
36
- metadata={
37
- "decrypted_key": decrypted_key
38
- },
39
- audit_type="MESSAGE_LOG_INTERNAL"
40
- )
41
27
 
42
28
  def resolver(public: bool = False, action=None):
43
29
 
@@ -61,7 +47,7 @@ def resolver(public: bool = False, action=None):
61
47
  token_service: IAuthTokenService = container.auth_token_service
62
48
  user_token = extract_bearer_token(headers)
63
49
 
64
- claims: UserTokenClaims = token_service.validate_internal_access_token(
50
+ claims: UserTokenClaims = await token_service.validate_internal_access_token(
65
51
  user_token
66
52
  )
67
53
 
@@ -97,57 +83,46 @@ def resolver(public: bool = False, action=None):
97
83
  error="User not allowed to perform this action"
98
84
  )
99
85
 
100
- # ── 5. EJECUTAR RESOLVER ──────────────────────────
86
+ # ── 5. CONTEXTO DE AUDITORÍA ──────────────
87
+ # Lo construimos antes del resolver para que siempre exista
88
+
89
+ audit_ctx = AuditContext.from_request(
90
+ request=request,
91
+ settings=settings,
92
+ body=body,
93
+ operation_type=info.operation.operation.value,
94
+ token=token,
95
+ decrypted_payload=decrypted_payload,
96
+ )
97
+
98
+ # ── 6. EJECUTAR RESOLVER ──────────────────
101
99
  result = await func(obj, info, **kwargs)
102
100
 
103
101
  if not isinstance(result, Response):
104
102
  raise TypeError("Resolver must return a Response instance")
105
103
 
106
-
107
-
108
- request_payload = {
109
- "env": settings.environment,
110
- "ms_name": settings.microservice_name,
111
- "ms_version": settings.microservice_version,
112
- "is_auditable": settings.is_auditable,
113
- "operation_type": info.operation.operation.value,
114
- "operation_name": body.get("operationName", "UnknownOperation"),
115
- "query": body.get("query"),
116
- "variables": decrypted_payload,
117
- "user": (
118
- token.user_full_name
119
- if token
120
- else "Public"
121
- )
122
- }
123
-
124
- if isinstance(result, Response):
125
- decrypted_key = result.key if not token else token.aes_key_auth
126
-
127
- dispatcher = AuditDispatcher(
128
- messaging=container.az_sb_audit,
129
- storage=container.az_blob_storage
130
- )
131
-
132
- __test(
133
- dispatcher=dispatcher,
134
- request_payload=request_payload,
135
- request=request,
136
- decrypted_key=decrypted_key,
137
- result=result
104
+ # ── 7. AUDITAR ÉXITO ──────────────────────
105
+ await container.audit_emitter.emit(
106
+ _build_success_payload(audit_ctx, result)
138
107
  )
139
108
 
140
109
  return result
141
110
 
142
111
  except OSDException as e:
143
- logger.warning(f"Business error: {str(e)}")
112
+ # logger.warning(f"Business error: {str(e)}")
113
+ await container.audit_emitter.emit(
114
+ _build_error_payload(audit_ctx, e)
115
+ )
144
116
  return Response(
145
117
  status=e.status_code,
146
118
  message=e.message,
147
119
  error=e.error
148
120
  )
149
121
  except Exception as e:
150
- logger.exception(f"Unexpected error: {str(e)}")
122
+ # logger.exception(f"Unexpected error: {str(e)}")
123
+ await container.audit_emitter.emit(
124
+ _build_unexpected_payload(audit_ctx, e)
125
+ )
151
126
  return Response(
152
127
  status=StatusCode.INTERNAL_SERVER_ERROR,
153
128
  message="Could not process request.",
@@ -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 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
+ )
@@ -4,10 +4,10 @@ from typing import Dict, Literal, Any, Optional
4
4
  class AuditHelper:
5
5
 
6
6
  @staticmethod
7
- async def build_request_payload(
7
+ def build_request_payload(
8
8
  request,
9
9
  payload: Dict[str, Any],
10
- audit_type
10
+ audit_type: str
11
11
  ) -> Dict[str, Any]:
12
12
 
13
13
  default_value = "*"
@@ -91,8 +91,6 @@ class AuditDispatcher:
91
91
  result: Response | ApiResponse = payload["result"]
92
92
  audit_type = payload["audit_type"]
93
93
 
94
- print(request_payload)
95
-
96
94
  if not request_payload.get("is_auditable"):
97
95
  logger.info("Auditing is disabled. Skipping audit processing.")
98
96
  return
@@ -0,0 +1,118 @@
1
+ import json
2
+ from fastapi import Request
3
+ from typing import Optional, Dict, Any, Literal
4
+ from Osdental.Models.AuditContext import AuditContext
5
+ from Osdental.Models.Response import Response
6
+ from Osdental.Exception.ControlledException import OSDException
7
+ from Osdental.Enums.StatusCode import StatusCode
8
+ from Osdental.Enums.AuditType import AuditType
9
+
10
+ def build_request_payload(
11
+ request: Request,
12
+ payload: Dict[str, Any],
13
+ audit_type: str
14
+ ) -> Dict[str, Any]:
15
+
16
+ default_value = "*"
17
+
18
+ user_ip = request.headers.get("X-Forwarded-For")
19
+ if user_ip:
20
+ user_ip = user_ip.split(",")[0]
21
+ else:
22
+ user_ip = getattr(request.client, "host", "*")
23
+
24
+ SAFE_HEADERS = {
25
+ "user-agent",
26
+ "host",
27
+ "origin",
28
+ "referer",
29
+ "content-type"
30
+ }
31
+
32
+ headers = {
33
+ k: v
34
+ for k, v in request.headers.items()
35
+ if k.lower() in SAFE_HEADERS
36
+ }
37
+
38
+ return {
39
+ "idMessageLog": request.headers.get("Idmessagelog"),
40
+ "environment": payload.get("env"),
41
+ "header": json.dumps(headers),
42
+ "microServiceUrl": str(request.url),
43
+ "microServiceName": payload.get("ms_name"),
44
+ "microServiceVersion": payload.get("ms_version"),
45
+ "serviceName": payload.get("operation_name"),
46
+ "machineNameUser": request.headers.get("Machinenameuser", default_value),
47
+ "ipUser": user_ip or default_value,
48
+ "userName": payload.get("user"),
49
+ "localitation": default_value,
50
+ "httpMethod": request.method,
51
+ "messageIn": json.dumps(payload) if payload else default_value,
52
+ "auditLog": audit_type,
53
+ }
54
+
55
+ def _build_final_payload(
56
+ type_: Literal["RESPONSE", "ERROR"],
57
+ status_code: int,
58
+ result: Optional[Any] = None,
59
+ error: Optional[Any] = None
60
+ ) -> Dict[str, Any]:
61
+
62
+ result = "*" if result is None else result
63
+ error = "*" if error is None else error
64
+
65
+ if isinstance(result, (dict, list)):
66
+ result = json.dumps(result)
67
+
68
+ return {
69
+ "type": type_,
70
+ "httpResponseCode": status_code,
71
+ "messageOut": result,
72
+ "errorProducer": error
73
+ }
74
+
75
+ def _build_success_payload(ctx: Optional[AuditContext], result: Response) -> Dict[str, Any]:
76
+ base = _base_payload(ctx, AuditType.INTERNAL)
77
+ return base | _build_final_payload(
78
+ type_="RESPONSE",
79
+ status_code=result.status,
80
+ result=result.data
81
+ )
82
+
83
+ def _build_error_payload(ctx: Optional[AuditContext], exc: OSDException) -> Dict[str, Any]:
84
+ base = _base_payload(ctx, AuditType.INTERNAL)
85
+ return base | _build_final_payload(
86
+ type_="ERROR",
87
+ status_code=exc.audit_status_code,
88
+ error=exc.error or exc.message
89
+ )
90
+
91
+ def _build_unexpected_payload(ctx: Optional[AuditContext], exc: Exception) -> Dict[str, Any]:
92
+ base = _base_payload(ctx, AuditType.INTERNAL)
93
+ return base | _build_final_payload(
94
+ type_="ERROR",
95
+ status_code=StatusCode.INTERNAL_SERVER_ERROR,
96
+ error=str(exc)
97
+ )
98
+
99
+ def _base_payload(ctx: Optional[AuditContext], audit_type: AuditType = AuditType.INTERNAL) -> Dict[str, Any]:
100
+ if not ctx:
101
+ return {"user": "unknown", "operation_name": "unknown"}
102
+
103
+ return {
104
+ "idMessageLog": ctx.message_log_id,
105
+ "environment": ctx.env,
106
+ "header": json.dumps(ctx.headers or {}),
107
+ "microServiceUrl": ctx.url,
108
+ "microServiceName": ctx.ms_name,
109
+ "microServiceVersion": ctx.ms_version,
110
+ "serviceName": ctx.operation_name,
111
+ "machineNameUser": "*",
112
+ "ipUser": ctx.user_ip or "*",
113
+ "userName": ctx.user,
114
+ "localitation": "*",
115
+ "httpMethod": ctx.http_method,
116
+ "messageIn": json.dumps(ctx.variables) if ctx.variables else "*",
117
+ "auditLog": audit_type,
118
+ }
@@ -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:
@@ -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
+ ...
@@ -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