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.
- {encryptors-2.57 → encryptors-2.58}/PKG-INFO +1 -1
- {encryptors-2.57 → encryptors-2.58}/setup.py +1 -1
- {encryptors-2.57 → encryptors-2.58}/src/Encryptors.egg-info/PKG-INFO +1 -1
- {encryptors-2.57 → encryptors-2.58}/src/Encryptors.egg-info/SOURCES.txt +4 -0
- encryptors-2.58/src/Osdental/Decorators/SecureResolver.py +133 -0
- encryptors-2.58/src/Osdental/Enums/AuditType.py +5 -0
- encryptors-2.58/src/Osdental/Helpers/AuditHelper.py +118 -0
- encryptors-2.58/src/Osdental/Models/AuditContext.py +61 -0
- encryptors-2.58/src/Osdental/Services/ServiceBusAuditEmitter.py +17 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Services/__init__.py +7 -1
- encryptors-2.57/src/Osdental/Decorators/SecureResolver.py +0 -250
- {encryptors-2.57 → encryptors-2.58}/README.md +0 -0
- {encryptors-2.57 → encryptors-2.58}/setup.cfg +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Encryptors.egg-info/dependency_links.txt +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Encryptors.egg-info/entry_points.txt +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Encryptors.egg-info/requires.txt +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Encryptors.egg-info/top_level.txt +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Cache/Redis.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Cache/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Cli/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Constants/Constant.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Constants/Message.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Constants/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Database/BaseRepository.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Database/Connection.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Database/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Decorators/Grpc.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Decorators/Retry.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Decorators/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Encryptor/Aes.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Encryptor/Argon2.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Encryptor/Bcrypt.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Encryptor/Jwt.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Encryptor/Rsa.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Encryptor/Sha512.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Encryptor/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Enums/ErrorSource.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Enums/FileType.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Enums/GrahpqlOperation.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Enums/Profile.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Enums/StatusCode.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Enums/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Exception/ControlledException.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Exception/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Graphql/Extensions/AuditExtension.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Graphql/Extensions/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Graphql/Models/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Graphql/_Exceptions/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Graphql/_Helpers/_AuditHelper.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Graphql/_Helpers/_ExtractAuthToken.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Graphql/_Helpers/_TenantPolicy.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Graphql/_Helpers/_TokenService.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Graphql/_Helpers/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Graphql/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Helpers/AuditDispatcher.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Helpers/AzureClassifier.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Helpers/GrpcConnection.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Helpers/JwtTokenHelper.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Helpers/Resilience.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Helpers/ResponseDecryptor.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Helpers/_AuthTokenProcessor.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Helpers/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Http/APIClient.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Http/_Helpers.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Http/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Messaging/AzureServiceBus.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Messaging/Kafka.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Messaging/RabbitMQ.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Messaging/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Models/ApiResponse.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Models/Notification.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Models/Response.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Models/Token.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Models/TokenClaims.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Models/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Rest/Context/RequestContext.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Rest/Context/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Rest/Middlewares/RequestContextMiddleware.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Rest/Middlewares/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Rest/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Secrets/AzureKeyVaultProvider.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Secrets/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Services/JwtAuthTokenService.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Services/WebsocketClient.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Storage/AzureBlobStorage.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Storage/S3Storage.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Storage/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Utils/CaseConverter.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Utils/CodeGenerator.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Utils/DataNormalizer.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Utils/DataUtils.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Utils/DateUtils.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Utils/FileMetaData.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Utils/HashValidator.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Utils/Mapper.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Utils/PasswordGenerator.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Utils/QueryGenerator.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Utils/RsaUtils.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Utils/TextProcessor.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/src/Osdental/Utils/__init__.py +0 -0
- {encryptors-2.57 → encryptors-2.58}/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.58",
|
|
6
6
|
author="OSDental LLC",
|
|
7
7
|
author_email="support@osdental.ai",
|
|
8
8
|
description="End-to-end algorithm library",
|
|
@@ -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,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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{encryptors-2.57 → encryptors-2.58}/src/Osdental/Rest/Middlewares/RequestContextMiddleware.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|