Encryptors 2.57__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 (101) hide show
  1. {encryptors-2.57 → encryptors-2.58}/PKG-INFO +1 -1
  2. {encryptors-2.57 → encryptors-2.58}/setup.py +1 -1
  3. {encryptors-2.57 → encryptors-2.58}/src/Encryptors.egg-info/PKG-INFO +1 -1
  4. {encryptors-2.57 → encryptors-2.58}/src/Encryptors.egg-info/SOURCES.txt +4 -0
  5. encryptors-2.58/src/Osdental/Decorators/SecureResolver.py +133 -0
  6. encryptors-2.58/src/Osdental/Enums/AuditType.py +5 -0
  7. encryptors-2.58/src/Osdental/Helpers/AuditHelper.py +118 -0
  8. encryptors-2.58/src/Osdental/Models/AuditContext.py +61 -0
  9. encryptors-2.58/src/Osdental/Services/ServiceBusAuditEmitter.py +17 -0
  10. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Services/__init__.py +7 -1
  11. encryptors-2.57/src/Osdental/Decorators/SecureResolver.py +0 -250
  12. {encryptors-2.57 → encryptors-2.58}/README.md +0 -0
  13. {encryptors-2.57 → encryptors-2.58}/setup.cfg +0 -0
  14. {encryptors-2.57 → encryptors-2.58}/src/Encryptors.egg-info/dependency_links.txt +0 -0
  15. {encryptors-2.57 → encryptors-2.58}/src/Encryptors.egg-info/entry_points.txt +0 -0
  16. {encryptors-2.57 → encryptors-2.58}/src/Encryptors.egg-info/requires.txt +0 -0
  17. {encryptors-2.57 → encryptors-2.58}/src/Encryptors.egg-info/top_level.txt +0 -0
  18. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Cache/Redis.py +0 -0
  19. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Cache/__init__.py +0 -0
  20. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Cli/__init__.py +0 -0
  21. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Constants/Constant.py +0 -0
  22. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Constants/Message.py +0 -0
  23. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Constants/__init__.py +0 -0
  24. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Database/BaseRepository.py +0 -0
  25. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Database/Connection.py +0 -0
  26. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Database/__init__.py +0 -0
  27. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Decorators/Grpc.py +0 -0
  28. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Decorators/Retry.py +0 -0
  29. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Decorators/__init__.py +0 -0
  30. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Encryptor/Aes.py +0 -0
  31. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Encryptor/Argon2.py +0 -0
  32. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Encryptor/Bcrypt.py +0 -0
  33. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Encryptor/Jwt.py +0 -0
  34. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Encryptor/Rsa.py +0 -0
  35. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Encryptor/Sha512.py +0 -0
  36. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Encryptor/__init__.py +0 -0
  37. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Enums/ErrorSource.py +0 -0
  38. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Enums/FileType.py +0 -0
  39. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Enums/GrahpqlOperation.py +0 -0
  40. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Enums/Profile.py +0 -0
  41. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Enums/StatusCode.py +0 -0
  42. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Enums/__init__.py +0 -0
  43. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Exception/ControlledException.py +0 -0
  44. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Exception/__init__.py +0 -0
  45. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Graphql/Extensions/AuditExtension.py +0 -0
  46. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Graphql/Extensions/__init__.py +0 -0
  47. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Graphql/Models/__init__.py +0 -0
  48. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Graphql/_Exceptions/__init__.py +0 -0
  49. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Graphql/_Helpers/_AuditHelper.py +0 -0
  50. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Graphql/_Helpers/_ExtractAuthToken.py +0 -0
  51. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Graphql/_Helpers/_TenantPolicy.py +0 -0
  52. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Graphql/_Helpers/_TokenService.py +0 -0
  53. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Graphql/_Helpers/__init__.py +0 -0
  54. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Graphql/__init__.py +0 -0
  55. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Helpers/AuditDispatcher.py +0 -0
  56. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Helpers/AzureClassifier.py +0 -0
  57. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Helpers/GrpcConnection.py +0 -0
  58. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Helpers/JwtTokenHelper.py +0 -0
  59. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Helpers/Resilience.py +0 -0
  60. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Helpers/ResponseDecryptor.py +0 -0
  61. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Helpers/_AuthTokenProcessor.py +0 -0
  62. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Helpers/__init__.py +0 -0
  63. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Http/APIClient.py +0 -0
  64. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Http/_Helpers.py +0 -0
  65. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Http/__init__.py +0 -0
  66. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Messaging/AzureServiceBus.py +0 -0
  67. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Messaging/Kafka.py +0 -0
  68. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Messaging/RabbitMQ.py +0 -0
  69. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Messaging/__init__.py +0 -0
  70. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Models/ApiResponse.py +0 -0
  71. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Models/Notification.py +0 -0
  72. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Models/Response.py +0 -0
  73. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Models/Token.py +0 -0
  74. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Models/TokenClaims.py +0 -0
  75. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Models/__init__.py +0 -0
  76. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Rest/Context/RequestContext.py +0 -0
  77. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Rest/Context/__init__.py +0 -0
  78. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Rest/Middlewares/RequestContextMiddleware.py +0 -0
  79. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Rest/Middlewares/__init__.py +0 -0
  80. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Rest/__init__.py +0 -0
  81. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Secrets/AzureKeyVaultProvider.py +0 -0
  82. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Secrets/__init__.py +0 -0
  83. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Services/JwtAuthTokenService.py +0 -0
  84. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Services/WebsocketClient.py +0 -0
  85. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Storage/AzureBlobStorage.py +0 -0
  86. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Storage/S3Storage.py +0 -0
  87. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Storage/__init__.py +0 -0
  88. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Utils/CaseConverter.py +0 -0
  89. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Utils/CodeGenerator.py +0 -0
  90. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Utils/DataNormalizer.py +0 -0
  91. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Utils/DataUtils.py +0 -0
  92. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Utils/DateUtils.py +0 -0
  93. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Utils/FileMetaData.py +0 -0
  94. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Utils/HashValidator.py +0 -0
  95. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Utils/Mapper.py +0 -0
  96. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Utils/PasswordGenerator.py +0 -0
  97. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Utils/QueryGenerator.py +0 -0
  98. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Utils/RsaUtils.py +0 -0
  99. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Utils/TextProcessor.py +0 -0
  100. {encryptors-2.57 → encryptors-2.58}/src/Osdental/Utils/__init__.py +0 -0
  101. {encryptors-2.57 → 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.57
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.57",
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.57
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,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/AuditType.py
30
31
  src/Osdental/Enums/ErrorSource.py
31
32
  src/Osdental/Enums/FileType.py
32
33
  src/Osdental/Enums/GrahpqlOperation.py
@@ -46,6 +47,7 @@ src/Osdental/Graphql/_Helpers/_TenantPolicy.py
46
47
  src/Osdental/Graphql/_Helpers/_TokenService.py
47
48
  src/Osdental/Graphql/_Helpers/__init__.py
48
49
  src/Osdental/Helpers/AuditDispatcher.py
50
+ src/Osdental/Helpers/AuditHelper.py
49
51
  src/Osdental/Helpers/AzureClassifier.py
50
52
  src/Osdental/Helpers/GrpcConnection.py
51
53
  src/Osdental/Helpers/JwtTokenHelper.py
@@ -61,6 +63,7 @@ src/Osdental/Messaging/Kafka.py
61
63
  src/Osdental/Messaging/RabbitMQ.py
62
64
  src/Osdental/Messaging/__init__.py
63
65
  src/Osdental/Models/ApiResponse.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
@@ -74,6 +77,7 @@ src/Osdental/Rest/Middlewares/__init__.py
74
77
  src/Osdental/Secrets/AzureKeyVaultProvider.py
75
78
  src/Osdental/Secrets/__init__.py
76
79
  src/Osdental/Services/JwtAuthTokenService.py
80
+ src/Osdental/Services/ServiceBusAuditEmitter.py
77
81
  src/Osdental/Services/WebsocketClient.py
78
82
  src/Osdental/Services/__init__.py
79
83
  src/Osdental/Storage/AzureBlobStorage.py
@@ -0,0 +1,133 @@
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
+ from Osdental.Models.AuditContext import AuditContext
19
+ from Osdental.Helpers.AuditHelper import (
20
+ _build_success_payload, _build_error_payload, _build_unexpected_payload
21
+ )
22
+
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+
28
+ def resolver(public: bool = False, action=None):
29
+
30
+ def decorator(func: Callable):
31
+ func._is_public = public
32
+
33
+ @wraps(func)
34
+ async def wrapper(obj, info: GraphQLResolveInfo, **kwargs):
35
+ try:
36
+ context: BaseGraphQLContext = info.context
37
+ container = context.container
38
+ request = context.request
39
+ headers = request.headers
40
+ settings = container.settings
41
+
42
+ token: type[AuthToken] | None = None
43
+
44
+ # ── 1. AUTENTICACIÓN ──────────────────────────────
45
+ if not public:
46
+
47
+ token_service: IAuthTokenService = container.auth_token_service
48
+ user_token = extract_bearer_token(headers)
49
+
50
+ claims: UserTokenClaims = await token_service.validate_internal_access_token(
51
+ user_token
52
+ )
53
+
54
+ # ── 2. DECRYPT DEL PAYLOAD ────────────────────────
55
+ body = await request.json()
56
+ variables = body.get("variables") or {}
57
+ encrypted_payload = kwargs.get("data") or variables.get("data")
58
+ decrypted_payload = None
59
+
60
+ if not public and encrypted_payload is not None:
61
+ decrypted_payload = decrypt_and_parse_payload(
62
+ aes_key_auth=claims.aes_key_auth,
63
+ encrypted_payload=encrypted_payload
64
+ )
65
+
66
+ kwargs["data"] = decrypted_payload
67
+
68
+ # ── 3. TENANT POLICY ──────────────────────────────
69
+ if not public:
70
+
71
+ token = build_auth_token(
72
+ claims=claims,
73
+ headers=request.headers,
74
+ decrypted_payload=decrypted_payload,
75
+ operation_type=info.operation.operation.value
76
+ )
77
+
78
+ # ── 4. CONTROL DE ACCESO POR ROL ─────────────────
79
+ if not public and action:
80
+ if Profile(token.abbreviation) not in action.allowed_roles:
81
+ raise AccessDeniedException(
82
+ status_code=StatusCode.BUSINESS_RULE_WARNING,
83
+ error="User not allowed to perform this action"
84
+ )
85
+
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 ──────────────────
99
+ result = await func(obj, info, **kwargs)
100
+
101
+ if not isinstance(result, Response):
102
+ raise TypeError("Resolver must return a Response instance")
103
+
104
+ # ── 7. AUDITAR ÉXITO ──────────────────────
105
+ await container.audit_emitter.emit(
106
+ _build_success_payload(audit_ctx, result)
107
+ )
108
+
109
+ return result
110
+
111
+ except OSDException as e:
112
+ # logger.warning(f"Business error: {str(e)}")
113
+ await container.audit_emitter.emit(
114
+ _build_error_payload(audit_ctx, e)
115
+ )
116
+ return Response(
117
+ status=e.status_code,
118
+ message=e.message,
119
+ error=e.error
120
+ )
121
+ except Exception as e:
122
+ # logger.exception(f"Unexpected error: {str(e)}")
123
+ await container.audit_emitter.emit(
124
+ _build_unexpected_payload(audit_ctx, e)
125
+ )
126
+ return Response(
127
+ status=StatusCode.INTERNAL_SERVER_ERROR,
128
+ message="Could not process request.",
129
+ error=str(e)
130
+ )
131
+
132
+ return wrapper
133
+ 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,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
+ }
@@ -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,250 +0,0 @@
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
File without changes
File without changes