Encryptors 2.28__tar.gz → 2.30__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. {encryptors-2.28 → encryptors-2.30}/PKG-INFO +1 -1
  2. {encryptors-2.28 → encryptors-2.30}/setup.py +1 -1
  3. {encryptors-2.28 → encryptors-2.30}/src/Encryptors.egg-info/PKG-INFO +1 -1
  4. {encryptors-2.28 → encryptors-2.30}/src/Encryptors.egg-info/SOURCES.txt +10 -0
  5. encryptors-2.30/src/Osdental/Decorators/AuditLog.py +87 -0
  6. encryptors-2.30/src/Osdental/Decorators/SecureResolver.py +70 -0
  7. encryptors-2.30/src/Osdental/Graphql/Extensions/AuditExtension.py +96 -0
  8. encryptors-2.30/src/Osdental/Graphql/_Helpers/_AuditHelper.py +64 -0
  9. encryptors-2.30/src/Osdental/Graphql/_Helpers/_TenantPolicy.py +33 -0
  10. encryptors-2.30/src/Osdental/Graphql/_Helpers/_TokenService.py +34 -0
  11. encryptors-2.30/src/Osdental/Helpers/AuditDispatcher.py +104 -0
  12. encryptors-2.30/src/Osdental/Models/AuditConfig.py +7 -0
  13. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Models/Response.py +0 -8
  14. encryptors-2.30/src/Osdental/Models/Token.py +25 -0
  15. encryptors-2.30/src/Osdental/Shared/Utils/__init__.py +0 -0
  16. encryptors-2.30/src/Osdental/Shared/__init__.py +0 -0
  17. encryptors-2.30/src/Osdental/__init__.py +0 -0
  18. encryptors-2.28/src/Osdental/Decorators/AuditLog.py +0 -158
  19. encryptors-2.28/src/Osdental/Models/Token.py +0 -43
  20. {encryptors-2.28 → encryptors-2.30}/README.md +0 -0
  21. {encryptors-2.28 → encryptors-2.30}/setup.cfg +0 -0
  22. {encryptors-2.28 → encryptors-2.30}/src/Encryptors.egg-info/dependency_links.txt +0 -0
  23. {encryptors-2.28 → encryptors-2.30}/src/Encryptors.egg-info/entry_points.txt +0 -0
  24. {encryptors-2.28 → encryptors-2.30}/src/Encryptors.egg-info/requires.txt +0 -0
  25. {encryptors-2.28 → encryptors-2.30}/src/Encryptors.egg-info/top_level.txt +0 -0
  26. {encryptors-2.28 → encryptors-2.30}/src/Osdental/BlobStorage/Storage.py +0 -0
  27. {encryptors-2.28 → encryptors-2.30}/src/Osdental/BlobStorage/__init__.py +0 -0
  28. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Cli/__init__.py +0 -0
  29. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Database/BaseRepository.py +0 -0
  30. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Database/Connection.py +0 -0
  31. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Database/UnitOfWork.py +0 -0
  32. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Database/UowFactory.py +0 -0
  33. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Database/__init__.py +0 -0
  34. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Decorators/DecryptedData.py +0 -0
  35. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Decorators/Grpc.py +0 -0
  36. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Decorators/Retry.py +0 -0
  37. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Decorators/SqlDataNormalizer.py +0 -0
  38. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Decorators/__init__.py +0 -0
  39. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Encryptor/Aes.py +0 -0
  40. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Encryptor/Argon2.py +0 -0
  41. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Encryptor/Bcrypt.py +0 -0
  42. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Encryptor/Jwt.py +0 -0
  43. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Encryptor/Rsa.py +0 -0
  44. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Encryptor/Sha512.py +0 -0
  45. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Encryptor/__init__.py +0 -0
  46. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Exception/ControlledException.py +0 -0
  47. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Exception/__init__.py +0 -0
  48. {encryptors-2.28 → encryptors-2.30}/src/Osdental/ExternalHttp/Client.py +0 -0
  49. {encryptors-2.28 → encryptors-2.30}/src/Osdental/ExternalHttp/__init__.py +0 -0
  50. {encryptors-2.28/src/Osdental/Grpc/Base → encryptors-2.30/src/Osdental/Graphql/Extensions}/__init__.py +0 -0
  51. {encryptors-2.28/src/Osdental/Grpc/Client → encryptors-2.30/src/Osdental/Graphql/_Helpers}/__init__.py +0 -0
  52. {encryptors-2.28/src/Osdental/Grpc/Generated → encryptors-2.30/src/Osdental/Graphql}/__init__.py +0 -0
  53. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Grpc/Base/GrpcClientBase.py +0 -0
  54. {encryptors-2.28/src/Osdental/Grpc → encryptors-2.30/src/Osdental/Grpc/Base}/__init__.py +0 -0
  55. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Grpc/Client/PortalClient.py +0 -0
  56. {encryptors-2.28/src/Osdental/Helpers → encryptors-2.30/src/Osdental/Grpc/Client}/__init__.py +0 -0
  57. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Grpc/Generated/Common_pb2.py +0 -0
  58. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Grpc/Generated/Common_pb2_grpc.py +0 -0
  59. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Grpc/Generated/Portal_pb2.py +0 -0
  60. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Grpc/Generated/Portal_pb2_grpc.py +0 -0
  61. {encryptors-2.28/src/Osdental/InternalHttp → encryptors-2.30/src/Osdental/Grpc/Generated}/__init__.py +0 -0
  62. {encryptors-2.28/src/Osdental/Models → encryptors-2.30/src/Osdental/Grpc}/__init__.py +0 -0
  63. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Helpers/KeyVaultService.py +0 -0
  64. {encryptors-2.28/src/Osdental/RedisCache → encryptors-2.30/src/Osdental/Helpers}/__init__.py +0 -0
  65. {encryptors-2.28 → encryptors-2.30}/src/Osdental/InternalHttp/Request.py +0 -0
  66. {encryptors-2.28 → encryptors-2.30}/src/Osdental/InternalHttp/Response.py +0 -0
  67. {encryptors-2.28/src/Osdental/ServicesBus → encryptors-2.30/src/Osdental/InternalHttp}/__init__.py +0 -0
  68. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Messaging/AzureServiceBus.py +0 -0
  69. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Messaging/Kafka.py +0 -0
  70. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Messaging/RabbitMQ.py +0 -0
  71. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Messaging/__init__.py +0 -0
  72. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Models/CDataIntegration.py +0 -0
  73. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Models/Catalog.py +0 -0
  74. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Models/Legacy.py +0 -0
  75. {encryptors-2.28/src/Osdental/Shared/Enums → encryptors-2.30/src/Osdental/Models}/__init__.py +0 -0
  76. {encryptors-2.28 → encryptors-2.30}/src/Osdental/RedisCache/Redis.py +0 -0
  77. {encryptors-2.28/src/Osdental/Shared/Utils → encryptors-2.30/src/Osdental/RedisCache}/__init__.py +0 -0
  78. {encryptors-2.28 → encryptors-2.30}/src/Osdental/ServicesBus/ServicesBus.py +0 -0
  79. {encryptors-2.28 → encryptors-2.30}/src/Osdental/ServicesBus/TaskQueue.py +0 -0
  80. {encryptors-2.28/src/Osdental/Shared → encryptors-2.30/src/Osdental/ServicesBus}/__init__.py +0 -0
  81. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Shared/Config/__init__.py +0 -0
  82. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Shared/Enums/Code.py +0 -0
  83. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Shared/Enums/Constant.py +0 -0
  84. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Shared/Enums/FileType.py +0 -0
  85. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Shared/Enums/GrahpqlOperation.py +0 -0
  86. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Shared/Enums/Message.py +0 -0
  87. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Shared/Enums/Profile.py +0 -0
  88. {encryptors-2.28/src/Osdental → encryptors-2.30/src/Osdental/Shared/Enums}/__init__.py +0 -0
  89. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Shared/Logger.py +0 -0
  90. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Shared/Utils/CaseConverter.py +0 -0
  91. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Shared/Utils/CodeGenerator.py +0 -0
  92. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Shared/Utils/DataNormalizer.py +0 -0
  93. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Shared/Utils/DataUtils.py +0 -0
  94. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Shared/Utils/DateUtils.py +0 -0
  95. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Shared/Utils/FileMetaData.py +0 -0
  96. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Shared/Utils/HashValidator.py +0 -0
  97. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Shared/Utils/Mapper.py +0 -0
  98. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Shared/Utils/PasswordGenerator.py +0 -0
  99. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Shared/Utils/QueryGenerator.py +0 -0
  100. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Shared/Utils/TextProcessor.py +0 -0
  101. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Storage/AzureBlobStorage.py +0 -0
  102. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Storage/S3Storage.py +0 -0
  103. {encryptors-2.28 → encryptors-2.30}/src/Osdental/Storage/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Encryptors
3
- Version: 2.28
3
+ Version: 2.30
4
4
  Summary: End-to-end algorithm library
5
5
  Author: OSDental LLC
6
6
  Author-email: support@osdental.ai
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="Encryptors",
5
- version="2.28",
5
+ version="2.30",
6
6
  author="OSDental LLC",
7
7
  author_email="support@osdental.ai",
8
8
  description="End-to-end algorithm library",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Encryptors
3
- Version: 2.28
3
+ Version: 2.30
4
4
  Summary: End-to-end algorithm library
5
5
  Author: OSDental LLC
6
6
  Author-email: support@osdental.ai
@@ -19,6 +19,7 @@ src/Osdental/Decorators/AuditLog.py
19
19
  src/Osdental/Decorators/DecryptedData.py
20
20
  src/Osdental/Decorators/Grpc.py
21
21
  src/Osdental/Decorators/Retry.py
22
+ src/Osdental/Decorators/SecureResolver.py
22
23
  src/Osdental/Decorators/SqlDataNormalizer.py
23
24
  src/Osdental/Decorators/__init__.py
24
25
  src/Osdental/Encryptor/Aes.py
@@ -32,6 +33,13 @@ src/Osdental/Exception/ControlledException.py
32
33
  src/Osdental/Exception/__init__.py
33
34
  src/Osdental/ExternalHttp/Client.py
34
35
  src/Osdental/ExternalHttp/__init__.py
36
+ src/Osdental/Graphql/__init__.py
37
+ src/Osdental/Graphql/Extensions/AuditExtension.py
38
+ src/Osdental/Graphql/Extensions/__init__.py
39
+ src/Osdental/Graphql/_Helpers/_AuditHelper.py
40
+ src/Osdental/Graphql/_Helpers/_TenantPolicy.py
41
+ src/Osdental/Graphql/_Helpers/_TokenService.py
42
+ src/Osdental/Graphql/_Helpers/__init__.py
35
43
  src/Osdental/Grpc/__init__.py
36
44
  src/Osdental/Grpc/Base/GrpcClientBase.py
37
45
  src/Osdental/Grpc/Base/__init__.py
@@ -42,6 +50,7 @@ src/Osdental/Grpc/Generated/Common_pb2_grpc.py
42
50
  src/Osdental/Grpc/Generated/Portal_pb2.py
43
51
  src/Osdental/Grpc/Generated/Portal_pb2_grpc.py
44
52
  src/Osdental/Grpc/Generated/__init__.py
53
+ src/Osdental/Helpers/AuditDispatcher.py
45
54
  src/Osdental/Helpers/KeyVaultService.py
46
55
  src/Osdental/Helpers/__init__.py
47
56
  src/Osdental/InternalHttp/Request.py
@@ -51,6 +60,7 @@ src/Osdental/Messaging/AzureServiceBus.py
51
60
  src/Osdental/Messaging/Kafka.py
52
61
  src/Osdental/Messaging/RabbitMQ.py
53
62
  src/Osdental/Messaging/__init__.py
63
+ src/Osdental/Models/AuditConfig.py
54
64
  src/Osdental/Models/CDataIntegration.py
55
65
  src/Osdental/Models/Catalog.py
56
66
  src/Osdental/Models/Legacy.py
@@ -0,0 +1,87 @@
1
+ import json
2
+ import asyncio
3
+ from typing import List, Dict, Any
4
+ from functools import wraps
5
+ from Osdental.InternalHttp.Request import CustomRequest
6
+ from Osdental.InternalHttp.Response import CustomResponse
7
+ from Osdental.Encryptor.Rsa import RSAEncryptor
8
+ from Osdental.Exception.ControlledException import OSDException, RSAEncryptException, AESEncryptException
9
+ from Osdental.Encryptor.Aes import AES
10
+ from Osdental.Shared.Utils.TextProcessor import TextProcessor
11
+ from Osdental.Shared.Logger import logger as custom_logger
12
+ from Osdental.Models.Legacy import Legacy
13
+ from Osdental.Grpc.Client.PortalClient import PortalClient
14
+ from Osdental.Shared.Enums.Code import Code
15
+
16
+ portal_client = PortalClient()
17
+ aes = AES()
18
+
19
+
20
+
21
+ def try_decrypt_or_return_raw(data: str, private_key_rsa: str, aes_key: str) -> str:
22
+ try:
23
+ return RSAEncryptor.decrypt(data, private_key_rsa, silent=True)
24
+ except RSAEncryptException:
25
+ try:
26
+ return aes.decrypt(aes_key, data, silent=True)
27
+ except AESEncryptException:
28
+ return data
29
+
30
+ def enqueue_response(data: Any, headers: Dict[str,str], msg_info: str = None):
31
+ content = json.dumps(data) if isinstance(data, dict) else msg_info
32
+ custom_response = CustomResponse(content=content, headers=headers)
33
+ _ = asyncio.create_task(custom_response.send_to_service_bus())
34
+
35
+
36
+ def handle_audit_and_exception():
37
+ def decorator(func):
38
+ @wraps(func)
39
+ async def wrapper(*args, **kwargs):
40
+ headers = {}
41
+
42
+
43
+ # Extract request and operation name before opening the span
44
+ try:
45
+ _, info = args[:2]
46
+ request = info.context.get('request')
47
+ headers = info.context.get('headers') or {}
48
+
49
+ if request:
50
+ # Send audit of the request to Service Bus
51
+ custom_request = CustomRequest(request)
52
+ _ = asyncio.create_task(custom_request.send_to_service_bus())
53
+
54
+ except Exception as ex:
55
+ logger.warning(f"Failed to extract operationName: {ex}")
56
+
57
+ try:
58
+ # Get legacy
59
+ res = await portal_client.get_legacy()
60
+ data = json.loads(res.data)
61
+ legacy = Legacy.from_db(data) if res.status == Code.PROCESS_SUCCESS_CODE else None
62
+ # Run the resolver
63
+ response = await func(*args, **kwargs)
64
+ # Prepare data and message
65
+ msg_info = TextProcessor.concatenate(response.get('status'), '-', response.get('message'))
66
+ raw_data = response.get('data')
67
+ data_to_enqueue = None
68
+ if raw_data:
69
+ data_to_enqueue = try_decrypt_or_return_raw(raw_data, legacy.private_key2, legacy.aes_key_auth)
70
+ # Enqueue response
71
+ enqueue_response(data_to_enqueue, headers, msg_info)
72
+ return response
73
+ except OSDException as ex:
74
+ # Controlled log
75
+ custom_logger.error(f'Controlled error: {str(ex)}')
76
+ ex.headers = headers
77
+ _ = asyncio.create_task(ex.send_to_service_bus())
78
+ return ex.get_response()
79
+ except Exception as e:
80
+ # Unhandled exception log and span
81
+ custom_logger.error(f'Unexpected error: {str(e)}')
82
+ ex = OSDException(error=str(e), headers=headers)
83
+ _ = asyncio.create_task(ex.send_to_service_bus())
84
+ return ex.get_response()
85
+
86
+ return wrapper
87
+ return decorator
@@ -0,0 +1,70 @@
1
+ import inspect
2
+ import asyncio
3
+ from functools import wraps
4
+ from typing import Callable, Dict, Any
5
+ from graphql import GraphQLResolveInfo
6
+ from Osdental.Models.Token import AuthToken
7
+ from Osdental.Models.Response import Response
8
+ from Osdental.Shared.Enums.Profile import Profile
9
+ from Osdental.Exception.ControlledException import OSDException
10
+ from Osdental.Encryptor.Aes import AES
11
+
12
+
13
+ aes = AES()
14
+ def secure_resolver(action = None):
15
+ def decorator(func: Callable):
16
+
17
+ signature = inspect.signature(func)
18
+ accepts_data = "data" in signature.parameters
19
+
20
+ @wraps(func)
21
+ async def wrapper(obj: Any, info: GraphQLResolveInfo, **kwargs: Dict):
22
+ try:
23
+ token: AuthToken = info.context.token
24
+ payload = getattr(info.context, "decrypted_payload", None)
25
+ aes_key = getattr(info.context, "aes_key", None)
26
+
27
+ if action:
28
+ if Profile(token.abbreviation) not in action.allowed_roles:
29
+ raise OSDException("DB_ERROR_AUTH", "You are not authorized to perform this action.")
30
+
31
+ if accepts_data and payload:
32
+ kwargs["data"] = payload
33
+
34
+ result: Response = await func(obj, info, **kwargs)
35
+
36
+ if result.data is not None:
37
+ result.data = aes.encrypt(aes_key, result.data)
38
+
39
+ return result
40
+
41
+ # await info.context.audit_dispatcher.dispatch(
42
+ # security=security,
43
+ # result=result
44
+ # )
45
+
46
+ if result.data is not None:
47
+ result.data = aes.encrypt(security.encryptor.aes_auth, result.data)
48
+
49
+
50
+ return result
51
+
52
+ except (Exception, OSDException) as e:
53
+ result = {
54
+ "status": getattr(e, "status_code", "DB_ERROR_UNEXPECTED"),
55
+ "message": getattr(e, "message", "Could not process request."),
56
+ "error": getattr(e, "error", type(e).__name__),
57
+ "data": None
58
+ }
59
+
60
+ # _ = asyncio.create_task(
61
+ # info.context.audit_dispatcher.dispatch(
62
+ # security=security,
63
+ # result=result
64
+ # )
65
+ # )
66
+
67
+ return result
68
+
69
+ return wrapper
70
+ return decorator
@@ -0,0 +1,96 @@
1
+ import time
2
+ import json
3
+ from graphql.pyutils import is_awaitable
4
+ from ariadne.types import Extension
5
+ from Osdental.Encryptor.Aes import AES
6
+ from Osdental.Graphql._Helpers._TenantPolicy import TenantPolicy
7
+ from Osdental.Models.Token import AuthToken
8
+
9
+ class AuditExtension(Extension):
10
+
11
+ def request_started(self, context):
12
+ self.start_time = time.perf_counter()
13
+ self.errors = None
14
+ self.request_payload = None
15
+ self.result = None
16
+ self.aes = AES()
17
+
18
+ async def resolve(self, next_, root, info, **kwargs):
19
+
20
+ if not self.request_payload:
21
+ context = info.context
22
+
23
+ request = info.context.request
24
+ self.request = request
25
+
26
+ body = request.state.graphql_body
27
+
28
+ container = context.container
29
+
30
+ token_service = container.token_service
31
+
32
+ aes_key = request.app.state.aes_auth
33
+ if not aes_key:
34
+ raise ValueError("Could not find authorization key Aes.")
35
+
36
+ original_token = await token_service.authenticate(request)
37
+
38
+ variables = body.get("variables") or {}
39
+ encrypted_payload = variables.get("data")
40
+
41
+ decrypted_payload = None
42
+
43
+ if encrypted_payload:
44
+ try:
45
+ decrypted_payload = self.aes.decrypt(aes_key, encrypted_payload)
46
+
47
+ try:
48
+ decrypted_payload = json.loads(decrypted_payload)
49
+ except Exception:
50
+ pass
51
+
52
+ except Exception:
53
+ decrypted_payload = None
54
+
55
+ token: AuthToken = TenantPolicy.resolve(
56
+ token=original_token,
57
+ headers=request.headers,
58
+ decrypted_payload=decrypted_payload,
59
+ operation_type=info.operation.operation.value
60
+ )
61
+
62
+ self.request_payload = {
63
+ "operation_type": info.operation.operation.value,
64
+ "operation_name": body.get("operationName", "UnknownOperation"),
65
+ "query": body.get("query"),
66
+ "variables": decrypted_payload,
67
+ "user": token.user_full_name
68
+ }
69
+
70
+ context.decrypted_payload = decrypted_payload
71
+ context.token = token
72
+ context.aes_key = aes_key
73
+
74
+ result = next_(root, info, **kwargs)
75
+
76
+ if is_awaitable(result):
77
+ result = await result
78
+
79
+ if root is None:
80
+ self.result = result
81
+
82
+ return result
83
+
84
+ def has_errors(self, errors, context):
85
+ self.errors = errors
86
+
87
+ def request_finished(self, context):
88
+
89
+ dispatcher = context.request.app.state.audit_dispatcher
90
+
91
+ dispatcher.dispatch(
92
+ request=self.request,
93
+ request_payload=self.request_payload,
94
+ result=self.result,
95
+ errors=self.errors
96
+ )
@@ -0,0 +1,64 @@
1
+ import json
2
+ from typing import Dict, Literal, Any, Optional
3
+ from fastapi import Request
4
+ from Osdental.Models.AuditConfig import AuditConfig
5
+
6
+ class AuditHelper:
7
+
8
+ @staticmethod
9
+ async def build_request_payload(
10
+ request: Request,
11
+ audit_config: AuditConfig,
12
+ payload: Any = {},
13
+ operation_name: str = "Unknown",
14
+ full_name: str = "Joe Doe"
15
+ ) -> Dict:
16
+
17
+ default_value = "*"
18
+
19
+ user_ip = request.headers.get("X-Forwarded-For")
20
+ if user_ip:
21
+ user_ip = user_ip.split(',')[0]
22
+ else:
23
+ user_ip = getattr(request.client, "host", "*")
24
+
25
+ SAFE_HEADERS = {
26
+ "user-agent",
27
+ "host",
28
+ "origin",
29
+ "referer",
30
+ "content-type"
31
+ }
32
+
33
+ headers = {k: v for k, v in request.headers.items() if k.lower() in SAFE_HEADERS}
34
+
35
+ return {
36
+ "idMessageLog": request.headers.get("Idmessagelog"),
37
+ "environment": audit_config.environment,
38
+ "header": json.dumps(headers),
39
+ "microServiceUrl": str(request.url),
40
+ "microServiceName": audit_config.microservice_name,
41
+ "microServiceVersion": audit_config.microservice_version,
42
+ "serviceName": operation_name,
43
+ "machineNameUser": request.headers.get("Machinenameuser", default_value),
44
+ "ipUser": user_ip or default_value,
45
+ "userName": full_name,
46
+ "localitation": default_value,
47
+ "httpMethod": request.method,
48
+ "messageIn": json.dumps(payload) if payload else default_value,
49
+ "auditLog": "MESSAGE_LOG_INTERNAL"
50
+ }
51
+
52
+ @staticmethod
53
+ def build_final_payload(
54
+ _type: Literal["RESPONSE", "ERROR"],
55
+ status_code: Any,
56
+ result: Optional[Any] = "*",
57
+ error: Optional[Any] = "*"
58
+ ):
59
+ return {
60
+ "type": _type,
61
+ "httpResponseCode": status_code,
62
+ "messageOut": json.dumps(result) if isinstance(result, dict) else result,
63
+ "errorProducer": error
64
+ }
@@ -0,0 +1,33 @@
1
+ from Osdental.Encryptor.Aes import AES
2
+ from uuid import UUID
3
+ from graphql import OperationType
4
+
5
+ class TenantPolicy:
6
+
7
+ @staticmethod
8
+ def resolve(token, headers, decrypted_payload, operation_type: str):
9
+ aes = AES()
10
+ # Solo aplicamos cambios si es QUERY
11
+ if operation_type != OperationType.QUERY.value:
12
+ return token # devolver token tal cual
13
+
14
+ # SUPER ADMIN / OSDEL ADMIN -> UUID 0
15
+ if token.abbreviation.startswith(("SPAU", "OSDA")):
16
+ token.id_external_enterprise = str(UUID(int=0))
17
+ return token
18
+
19
+ # Marketing -> tomar dynamicClientId del header
20
+ if token.abbreviation.startswith("OSDMK"):
21
+ dynamic_client_id = headers.get("dynamicClientId")
22
+ if dynamic_client_id:
23
+ decrypted_mk_id = aes.decrypt(token.aes_key_auth, dynamic_client_id)
24
+ token.id_external_enterprise = decrypted_mk_id
25
+ token.mk_id_external_enterprise = decrypted_mk_id
26
+ return token
27
+
28
+ # If it comes by request, it is taken as priority
29
+ external_enterprise_req = decrypted_payload.get('idExternalEnterprise')
30
+ if external_enterprise_req and token:
31
+ token.id_external_enterprise = external_enterprise_req
32
+
33
+ return token
@@ -0,0 +1,34 @@
1
+ from Osdental.Encryptor.Aes import AES
2
+ from Osdental.Encryptor.Jwt import JWT
3
+ from Osdental.Models.Token import AuthToken
4
+
5
+ class TokenService:
6
+
7
+ def __init__(self, jwt_user_key: str, auth_validator):
8
+ self.jwt_user_key = jwt_user_key
9
+ self.auth_validator = auth_validator
10
+ self.aes = AES()
11
+
12
+ async def authenticate(self, request):
13
+ authorization = request.headers.get("authorization")
14
+ if not authorization or not authorization.startswith("Bearer "):
15
+ raise ValueError("Missing Bearer token")
16
+
17
+ encrypted_token = authorization.split(" ")[1]
18
+
19
+ aes_user = request.app.state.aes_user
20
+
21
+ user_token = self.aes.decrypt(aes_user, encrypted_token)
22
+ payload = JWT.extract_payload(user_token, self.jwt_user_key)
23
+ token = AuthToken(**payload)
24
+
25
+ # Validate via RPC
26
+ request = {
27
+ 'idToken': token.id_token,
28
+ 'idUser': token.id_user
29
+ }
30
+ is_valid = await self.auth_validator.validate_auth_token(request)
31
+ if not is_valid:
32
+ raise ValueError("You are not authorized to access this portal.")
33
+
34
+ return token
@@ -0,0 +1,104 @@
1
+ import asyncio
2
+ import logging
3
+ from Osdental.Models.AuditConfig import AuditConfig
4
+ from Osdental.Messaging import IMessageQueue
5
+ from Osdental.Graphql._Helpers._AuditHelper import AuditHelper
6
+
7
+ class AuditDispatcher:
8
+
9
+ def __init__(self, messaging: IMessageQueue, audit_config: AuditConfig, max_queue_size: int = 5000):
10
+ self._messaging = messaging
11
+ self._audit_config = audit_config
12
+ self._queue = asyncio.Queue(maxsize=max_queue_size)
13
+ self._worker_task = None
14
+
15
+ # Se llama en startup
16
+ def start(self):
17
+ self._worker_task = asyncio.create_task(self._worker())
18
+
19
+ # Se llama en shutdown
20
+ async def stop(self):
21
+ # Esperar a que la cola se vacíe
22
+ await self._queue.join()
23
+
24
+ # Enviar sentinel para cerrar worker
25
+ await self._queue.put(None)
26
+
27
+ # Esperar a que el worker termine
28
+ if self._worker_task:
29
+ await self._worker_task
30
+
31
+ # Ultra rápido, no bloquea
32
+ def dispatch(self, request, request_payload, result, errors = None):
33
+ try:
34
+ payload = {
35
+ "request": request,
36
+ "request_payload": request_payload,
37
+ "result": result,
38
+ "errors": errors
39
+ }
40
+
41
+ self._queue.put_nowait(payload)
42
+
43
+ except asyncio.QueueFull:
44
+ logging.error("Audit queue full. Dropping message.")
45
+
46
+ # Worker real
47
+ async def _worker(self):
48
+ while True:
49
+ payload = await self._queue.get()
50
+ # Sentinel detectado → salir limpio
51
+ if payload is None:
52
+ self._queue.task_done()
53
+ break
54
+
55
+ try:
56
+ await self._process(payload)
57
+ except Exception:
58
+ logging.exception("Audit processing failed")
59
+ finally:
60
+ self._queue.task_done()
61
+
62
+ # Logica
63
+ async def _process(self, payload):
64
+ request = payload["request"]
65
+ request_payload = payload["request_payload"]
66
+ result = payload["result"]
67
+
68
+ operation_name = request_payload.get("operation_name")
69
+ if operation_name == 'UnknownOperation':
70
+ logging.info("Skipping audit: no data in GraphQL response")
71
+ return
72
+
73
+ request_audit_payload = await AuditHelper.build_request_payload(
74
+ request=request,
75
+ audit_config=self._audit_config,
76
+ operation_name=operation_name,
77
+ full_name=request_payload.get("user"),
78
+ payload=request_payload.get("variables")
79
+ )
80
+
81
+ status = result.get("status")
82
+ message = result.get("message")
83
+ data = result.get("data")
84
+
85
+ ERROR_PREFIXES = ("DB_ERROR", "DB_WARNING")
86
+ if status.upper().startswith(ERROR_PREFIXES):
87
+ payload = AuditHelper.build_final_payload(
88
+ type="ERROR",
89
+ status_code=status,
90
+ error=message
91
+ )
92
+
93
+ audit_message = request_audit_payload | payload
94
+
95
+ else:
96
+ payload = AuditHelper.build_final_payload(
97
+ type="RESPONSE",
98
+ status_code="200",
99
+ result=data
100
+ )
101
+
102
+ audit_message = request_audit_payload | payload
103
+
104
+ await self._messaging.enqueue(audit_message)
@@ -0,0 +1,7 @@
1
+ from dataclasses import dataclass
2
+
3
+ @dataclass(frozen=True)
4
+ class AuditConfig:
5
+ environment: str
6
+ microservice_name: str
7
+ microservice_version: str
@@ -1,5 +1,4 @@
1
1
  from dataclasses import dataclass, field, asdict
2
- from enum import Enum
3
2
  from Osdental.Shared.Enums.Code import Code
4
3
  from Osdental.Shared.Enums.Message import Message
5
4
 
@@ -9,12 +8,5 @@ class Response:
9
8
  message: str = field(default=Message.PROCESS_SUCCESS_MSG)
10
9
  data: str = field(default=None)
11
10
 
12
- def __post_init__(self):
13
- # Asegura que status y message sean str si son Enum
14
- if isinstance(self.status, Enum):
15
- self.status = str(self.status)
16
- if isinstance(self.message, Enum):
17
- self.message = str(self.message)
18
-
19
11
  def send(self):
20
12
  return asdict(self)
@@ -0,0 +1,25 @@
1
+ from typing import Optional
2
+ from pydantic import BaseModel, Field
3
+ from Osdental.Models.Legacy import Legacy
4
+
5
+ class AuthToken(BaseModel):
6
+
7
+ id_token: str = Field(alias="idToken")
8
+ id_user: str = Field(alias="idUser")
9
+ id_external_enterprise: str = Field(alias="idExternalEnterprise")
10
+ id_profile: str = Field(alias="idProfile")
11
+ id_legacy: str = Field(alias="idLegacy")
12
+ id_authorization: str = Field(default=None, alias="idAuthorization")
13
+ id_item_report: str = Field(alias="idItemReport")
14
+ id_enterprise: str = Field(alias="idEnterprise")
15
+ user_full_name: str = Field(alias="userFullName")
16
+ abbreviation: str = Field(alias="abbreviation")
17
+ aes_key_auth: str = Field(alias="aesKeyAuth")
18
+ access_token: Optional[str] = Field(default=None, alias="accessToken")
19
+ base_id_external_enterprise: Optional[str] = Field(default=None, alias="baseIdExternalEnterprise")
20
+ mk_id_external_enterprise: Optional[str] = Field(default=None, alias="mkIdExternalEnterprise")
21
+ jwt_user_key: Optional[str] = Field(default=None, alias="jwtUserKey")
22
+ legacy: Optional[Legacy] = Field(default=None, alias="legacy")
23
+
24
+ class ConfigDict:
25
+ populate_by_name = True
File without changes
File without changes
File without changes
@@ -1,158 +0,0 @@
1
- import json
2
- import asyncio
3
- import logging
4
- import time
5
- from typing import List, Dict, Any
6
- from functools import wraps
7
- from azure.monitor.opentelemetry import configure_azure_monitor
8
- from opentelemetry import trace
9
- from Osdental.InternalHttp.Request import CustomRequest
10
- from Osdental.InternalHttp.Response import CustomResponse
11
- from Osdental.Encryptor.Rsa import RSAEncryptor
12
- from Osdental.Exception.ControlledException import OSDException, RSAEncryptException, AESEncryptException
13
- from Osdental.Encryptor.Aes import AES
14
- from Osdental.Shared.Utils.TextProcessor import TextProcessor
15
- from Osdental.Shared.Logger import logger as custom_logger
16
- from Osdental.Models.Legacy import Legacy
17
- from Osdental.Grpc.Client.PortalClient import PortalClient
18
- from Osdental.Shared.Enums.Code import Code
19
- from Osdental.Shared.Config import Config
20
- from Osdental.Shared.Enums.Constant import Constant
21
-
22
- portal_client = PortalClient()
23
- aes = AES()
24
-
25
- # Configuration Azure Monitor
26
- configure_azure_monitor(
27
- connection_string=Config.APPLICATIONINSIGHTS_CONNECTION_STRING
28
- )
29
- # Logger
30
- logging.basicConfig(level=logging.INFO)
31
- logger = logging.getLogger('graphql')
32
- logger.setLevel(logging.INFO)
33
- # Tracer for spans
34
- tracer = trace.get_tracer(__name__)
35
-
36
- def split_into_batches(data: List[Any], batch:int = 250):
37
- for i in range(0, len(data), batch):
38
- yield data[i:i + batch]
39
-
40
- def try_decrypt_or_return_raw(data: str, private_key_rsa: str, aes_key: str) -> str:
41
- try:
42
- return RSAEncryptor.decrypt(data, private_key_rsa, silent=True)
43
- except RSAEncryptException:
44
- try:
45
- return aes.decrypt(aes_key, data, silent=True)
46
- except AESEncryptException:
47
- return data
48
-
49
- def enqueue_response(data: Any, batch: int, headers: Dict[str,str], msg_info: str = None):
50
- if data and isinstance(data, list):
51
- if batch > 0 and len(data) > batch:
52
- batches = split_into_batches(data, batch)
53
- for idx, data_batch in enumerate(batches, start=1):
54
- custom_response = CustomResponse(content=json.dumps(data_batch), headers=headers, batch=idx)
55
- _ = asyncio.create_task(custom_response.send_to_service_bus())
56
- else:
57
- custom_response = CustomResponse(content=json.dumps(data), headers=headers)
58
- _ = asyncio.create_task(custom_response.send_to_service_bus())
59
- else:
60
- content = json.dumps(data) if isinstance(data, dict) else msg_info
61
- custom_response = CustomResponse(content=content, headers=headers)
62
- _ = asyncio.create_task(custom_response.send_to_service_bus())
63
-
64
-
65
- def handle_audit_and_exception(batch: int = 0):
66
- def decorator(func):
67
- @wraps(func)
68
- async def wrapper(*args, **kwargs):
69
- headers = {}
70
- operation_name = 'UnknownOperation'
71
-
72
- # Extract request and operation name before opening the span
73
- try:
74
- _, info = args[:2]
75
- request = info.context.get('request')
76
- headers = info.context.get('headers') or {}
77
-
78
- if request:
79
- body = await request.body()
80
- try:
81
- body_data = json.loads(body.decode("utf-8"))
82
- operation_name = body_data.get('operationName', 'UnknownOperation')
83
- except Exception:
84
- pass
85
-
86
- # Send audit of the request to Service Bus
87
- custom_request = CustomRequest(request)
88
- _ = asyncio.create_task(custom_request.send_to_service_bus())
89
-
90
- except Exception as ex:
91
- logger.warning(f"Failed to extract operationName: {ex}")
92
-
93
- # Open the span with the correct operation name
94
- with tracer.start_as_current_span(f"GraphQL.{operation_name}") as span:
95
- start_time = time.time()
96
- try:
97
- # Get legacy
98
- res = await portal_client.get_legacy()
99
- data = json.loads(res.data)
100
- legacy = Legacy.from_db(data) if res.status == Code.PROCESS_SUCCESS_CODE else None
101
-
102
- # Run the resolver
103
- response = await func(*args, **kwargs)
104
-
105
- # Prepare data and message
106
- msg_info = TextProcessor.concatenate(response.get('status'), '-', response.get('message'))
107
- raw_data = response.get('data')
108
- data_to_enqueue = None
109
- if raw_data:
110
- data_to_enqueue = try_decrypt_or_return_raw(raw_data, legacy.private_key2, legacy.aes_key_auth)
111
-
112
- # Enqueue response
113
- enqueue_response(data_to_enqueue, batch, headers, msg_info)
114
-
115
- # Measure duration
116
- duration = (time.time() - start_time) * 1000
117
-
118
- # Span attributes
119
- span.set_attribute(Constant.GRAPHQL_OPERATION_NAME, operation_name)
120
- span.set_attribute(Constant.GRAPHQL_STATUS, response.get('status'))
121
- span.set_attribute(Constant.GRAPHQL_MESSAGE, response.get('message'))
122
- span.set_attribute(Constant.GRAPHQL_DURATION_MS, duration)
123
-
124
- return response
125
-
126
- except OSDException as ex:
127
- # Controlled log
128
- custom_logger.error(f'Controlled error: {str(ex)}')
129
- duration = (time.time() - start_time) * 1000
130
- msg = str(ex) if str(ex) else getattr(ex, 'message', 'OSDException')
131
- span.set_attribute(Constant.GRAPHQL_OPERATION_NAME, operation_name)
132
- span.set_attribute(Constant.GRAPHQL_STATUS, ex.status_code)
133
- span.set_attribute(Constant.GRAPHQL_MESSAGE, msg)
134
- span.set_attribute(Constant.GRAPHQL_DURATION_MS, duration)
135
- ex.headers = headers
136
- span.record_exception(ex)
137
-
138
- _ = asyncio.create_task(ex.send_to_service_bus())
139
- return ex.get_response()
140
-
141
- except Exception as e:
142
- # Unhandled exception log and span
143
- custom_logger.error(f'Unexpected error: {str(e)}')
144
- ex = OSDException(error=str(e), headers=headers)
145
- msg = str(e) if str(e) else getattr(ex, 'message', 'Unknown Exception')
146
- duration = (time.time() - start_time) * 1000
147
- span.set_attribute(Constant.GRAPHQL_OPERATION_NAME, operation_name)
148
- span.set_attribute(Constant.GRAPHQL_STATUS, ex.status_code)
149
- span.set_attribute(Constant.GRAPHQL_MESSAGE, msg)
150
- span.record_exception(e)
151
- span.set_status(trace.status.Status(trace.status.StatusCode.ERROR))
152
- span.set_attribute(Constant.GRAPHQL_DURATION_MS, duration)
153
-
154
- _ = asyncio.create_task(ex.send_to_service_bus())
155
- return ex.get_response()
156
-
157
- return wrapper
158
- return decorator
@@ -1,43 +0,0 @@
1
- from __future__ import annotations
2
- from dataclasses import dataclass
3
- from typing import Optional, Dict
4
- from Osdental.Models.Legacy import Legacy
5
- from Osdental.Shared.Utils.CaseConverter import CaseConverter
6
- from Osdental.Exception.ControlledException import MissingFieldException
7
-
8
- @dataclass
9
- class AuthToken:
10
- id_token: str
11
- id_user: str
12
- id_external_enterprise: str
13
- id_profile: str
14
- id_legacy: str
15
- id_item_report: str
16
- id_enterprise: str
17
- id_authorization: str
18
- user_full_name: str
19
- abbreviation: str
20
- aes_key_auth: str
21
- access_token: Optional[str] = None
22
- base_id_external_enterprise: Optional[str] = None
23
- mk_id_external_enterprise: Optional[str] = None
24
- jwt_user_key: Optional[str] = None
25
- legacy: Optional[Legacy] = None
26
-
27
- def __post_init__(self):
28
- required_fields = [
29
- 'id_token', 'id_user', 'id_external_enterprise', 'id_profile',
30
- 'id_legacy', 'id_item_report', 'id_enterprise', 'id_authorization',
31
- 'user_full_name', 'abbreviation', 'aes_key_auth'
32
- ]
33
- missing = [f for f in required_fields if not getattr(self, f)]
34
- if missing:
35
- raise MissingFieldException(error=f"Missing required fields: {', '.join(missing)}")
36
-
37
-
38
- @classmethod
39
- def from_jwt(cls, payload: Dict[str,str]) -> AuthToken:
40
- mapped = {CaseConverter.case_to_snake(key): value for key, value in payload.items()}
41
- valid_fields = cls.__dataclass_fields__.keys()
42
- clean = {k: v for k, v in mapped.items() if k in valid_fields}
43
- return cls(**clean)
File without changes
File without changes