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