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.
- {encryptors-2.57 → encryptors-2.59}/PKG-INFO +1 -1
- {encryptors-2.57 → encryptors-2.59}/setup.py +1 -1
- {encryptors-2.57 → encryptors-2.59}/src/Encryptors.egg-info/PKG-INFO +1 -1
- {encryptors-2.57 → encryptors-2.59}/src/Encryptors.egg-info/SOURCES.txt +7 -17
- encryptors-2.59/src/Osdental/Context/__init__.py +5 -0
- encryptors-2.59/src/Osdental/Decorators/SecureResolver.py +134 -0
- encryptors-2.59/src/Osdental/Enums/AuditType.py +5 -0
- encryptors-2.59/src/Osdental/Enums/ResultType.py +5 -0
- encryptors-2.59/src/Osdental/Helpers/AuditHelper.py +166 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Http/APIClient.py +39 -12
- encryptors-2.59/src/Osdental/Models/AuditContext.py +61 -0
- encryptors-2.59/src/Osdental/Services/ServiceBusAuditEmitter.py +17 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Services/__init__.py +7 -1
- encryptors-2.57/src/Osdental/Decorators/SecureResolver.py +0 -250
- encryptors-2.57/src/Osdental/Graphql/Extensions/AuditExtension.py +0 -196
- encryptors-2.57/src/Osdental/Graphql/_Exceptions/__init__.py +0 -8
- encryptors-2.57/src/Osdental/Graphql/_Helpers/_AuditHelper.py +0 -67
- encryptors-2.57/src/Osdental/Graphql/_Helpers/_ExtractAuthToken.py +0 -11
- encryptors-2.57/src/Osdental/Graphql/_Helpers/_TenantPolicy.py +0 -46
- encryptors-2.57/src/Osdental/Graphql/_Helpers/_TokenService.py +0 -44
- encryptors-2.57/src/Osdental/Helpers/AuditDispatcher.py +0 -169
- encryptors-2.57/src/Osdental/Http/_Helpers.py +0 -125
- encryptors-2.57/src/Osdental/Models/__init__.py +0 -0
- encryptors-2.57/src/Osdental/Rest/Context/RequestContext.py +0 -19
- encryptors-2.57/src/Osdental/Rest/Context/__init__.py +0 -0
- encryptors-2.57/src/Osdental/Rest/Middlewares/RequestContextMiddleware.py +0 -22
- encryptors-2.57/src/Osdental/Rest/Middlewares/__init__.py +0 -0
- encryptors-2.57/src/Osdental/Rest/__init__.py +0 -0
- encryptors-2.57/src/Osdental/Utils/__init__.py +0 -0
- encryptors-2.57/src/Osdental/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/README.md +0 -0
- {encryptors-2.57 → encryptors-2.59}/setup.cfg +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Encryptors.egg-info/dependency_links.txt +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Encryptors.egg-info/entry_points.txt +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Encryptors.egg-info/requires.txt +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Encryptors.egg-info/top_level.txt +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Cache/Redis.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Cache/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Cli/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Constants/Constant.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Constants/Message.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Constants/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Database/BaseRepository.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Database/Connection.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Database/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Decorators/Grpc.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Decorators/Retry.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Decorators/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Encryptor/Aes.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Encryptor/Argon2.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Encryptor/Bcrypt.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Encryptor/Jwt.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Encryptor/Rsa.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Encryptor/Sha512.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Encryptor/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Enums/ErrorSource.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Enums/FileType.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Enums/GrahpqlOperation.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Enums/Profile.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Enums/StatusCode.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Enums/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Exception/ControlledException.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Exception/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Helpers/AzureClassifier.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Helpers/GrpcConnection.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Helpers/JwtTokenHelper.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Helpers/Resilience.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Helpers/ResponseDecryptor.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Helpers/_AuthTokenProcessor.py +0 -0
- {encryptors-2.57/src/Osdental/Graphql/Extensions → encryptors-2.59/src/Osdental/Helpers}/__init__.py +0 -0
- {encryptors-2.57/src/Osdental/Graphql/_Helpers → encryptors-2.59/src/Osdental/Http}/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Messaging/AzureServiceBus.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Messaging/Kafka.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Messaging/RabbitMQ.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Messaging/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Models/ApiResponse.py +0 -0
- /encryptors-2.57/src/Osdental/Graphql/Models/__init__.py → /encryptors-2.59/src/Osdental/Models/Graphql.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Models/Notification.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Models/Response.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Models/Token.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Models/TokenClaims.py +0 -0
- {encryptors-2.57/src/Osdental/Graphql → encryptors-2.59/src/Osdental/Models}/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Secrets/AzureKeyVaultProvider.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Secrets/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Services/JwtAuthTokenService.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Services/WebsocketClient.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Storage/AzureBlobStorage.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Storage/S3Storage.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Storage/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Utils/CaseConverter.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Utils/CodeGenerator.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Utils/DataNormalizer.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Utils/DataUtils.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Utils/DateUtils.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Utils/FileMetaData.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Utils/HashValidator.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Utils/Mapper.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Utils/PasswordGenerator.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Utils/QueryGenerator.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Utils/RsaUtils.py +0 -0
- {encryptors-2.57 → encryptors-2.59}/src/Osdental/Utils/TextProcessor.py +0 -0
- {encryptors-2.57/src/Osdental/Helpers → encryptors-2.59/src/Osdental/Utils}/__init__.py +0 -0
- {encryptors-2.57/src/Osdental/Http → encryptors-2.59/src/Osdental}/__init__.py +0 -0
|
@@ -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.
|
|
5
|
+
version="2.59",
|
|
6
6
|
author="OSDental LLC",
|
|
7
7
|
author_email="support@osdental.ai",
|
|
8
8
|
description="End-to-end algorithm library",
|
|
@@ -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/
|
|
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,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,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.
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
152
|
+
f"GraphQL error: {errors}"
|
|
126
153
|
)
|
|
127
154
|
|
|
128
|
-
|
|
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
|
+
...
|