Encryptors 2.35__tar.gz → 2.37__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.35 → encryptors-2.37}/PKG-INFO +1 -1
- {encryptors-2.35 → encryptors-2.37}/setup.py +1 -1
- {encryptors-2.35 → encryptors-2.37}/src/Encryptors.egg-info/PKG-INFO +1 -1
- {encryptors-2.35 → encryptors-2.37}/src/Encryptors.egg-info/SOURCES.txt +1 -0
- encryptors-2.37/src/Osdental/Cli/__init__.py +118 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Database/BaseRepository.py +34 -20
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Decorators/SecureResolver.py +11 -13
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Encryptor/Aes.py +10 -10
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Exception/ControlledException.py +9 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Graphql/Extensions/AuditExtension.py +16 -11
- encryptors-2.37/src/Osdental/Graphql/Models/__init__.py +14 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Graphql/_Exceptions/__init__.py +1 -1
- encryptors-2.37/src/Osdental/Graphql/_Helpers/_ExtractAuthToken.py +11 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Graphql/_Helpers/_TenantPolicy.py +10 -7
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Graphql/_Helpers/_TokenService.py +6 -9
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Models/Token.py +0 -2
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Shared/Enums/Code.py +1 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Shared/Enums/Message.py +1 -1
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Shared/Utils/FileMetaData.py +1 -1
- encryptors-2.35/src/Osdental/Cli/__init__.py +0 -505
- encryptors-2.35/src/Osdental/Graphql/_Helpers/_ExtractAuthToken.py +0 -11
- {encryptors-2.35 → encryptors-2.37}/README.md +0 -0
- {encryptors-2.35 → encryptors-2.37}/setup.cfg +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Encryptors.egg-info/dependency_links.txt +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Encryptors.egg-info/entry_points.txt +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Encryptors.egg-info/requires.txt +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Encryptors.egg-info/top_level.txt +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Database/Connection.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Database/__init__.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Decorators/Grpc.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Decorators/Retry.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Decorators/SqlDataNormalizer.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Decorators/__init__.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Encryptor/Argon2.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Encryptor/Bcrypt.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Encryptor/Jwt.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Encryptor/Rsa.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Encryptor/Sha512.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Encryptor/__init__.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Exception/__init__.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Graphql/Extensions/__init__.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Graphql/_Helpers/_AuditHelper.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Graphql/_Helpers/__init__.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Graphql/__init__.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Helpers/AuditDispatcher.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Helpers/KeyVaultService.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Helpers/__init__.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Http/APIClient.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Http/_Exceptions.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Http/__init__.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Messaging/AzureServiceBus.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Messaging/Kafka.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Messaging/RabbitMQ.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Messaging/__init__.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Models/AuditConfig.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Models/Catalog.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Models/Legacy.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Models/Response.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Models/_Audit.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Models/__init__.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/RedisCache/Redis.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/RedisCache/__init__.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Rest/Context/RequestContext.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Rest/Context/__init__.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Rest/Middlewares/RequestContextMiddleware.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Rest/Middlewares/__init__.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Rest/__init__.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Shared/Enums/Constant.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Shared/Enums/FileType.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Shared/Enums/GrahpqlOperation.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Shared/Enums/Profile.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Shared/Enums/__init__.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Shared/Logger.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Shared/Utils/CaseConverter.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Shared/Utils/CodeGenerator.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Shared/Utils/DataNormalizer.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Shared/Utils/DataUtils.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Shared/Utils/DateUtils.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Shared/Utils/HashValidator.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Shared/Utils/Mapper.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Shared/Utils/PasswordGenerator.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Shared/Utils/QueryGenerator.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Shared/Utils/TextProcessor.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Shared/Utils/__init__.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Shared/__init__.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Storage/AzureBlobStorage.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Storage/S3Storage.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/Storage/__init__.py +0 -0
- {encryptors-2.35 → encryptors-2.37}/src/Osdental/__init__.py +0 -0
|
@@ -28,6 +28,7 @@ src/Osdental/Exception/__init__.py
|
|
|
28
28
|
src/Osdental/Graphql/__init__.py
|
|
29
29
|
src/Osdental/Graphql/Extensions/AuditExtension.py
|
|
30
30
|
src/Osdental/Graphql/Extensions/__init__.py
|
|
31
|
+
src/Osdental/Graphql/Models/__init__.py
|
|
31
32
|
src/Osdental/Graphql/_Exceptions/__init__.py
|
|
32
33
|
src/Osdental/Graphql/_Helpers/_AuditHelper.py
|
|
33
34
|
src/Osdental/Graphql/_Helpers/_ExtractAuthToken.py
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import subprocess
|
|
4
|
+
import platform
|
|
5
|
+
import click
|
|
6
|
+
from Osdental.Shared.Logger import logger
|
|
7
|
+
from Osdental.Shared.Enums.Message import Message
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@click.group()
|
|
11
|
+
def cli():
|
|
12
|
+
"""Custom commands to manage the project."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
@cli.command()
|
|
16
|
+
def clean():
|
|
17
|
+
"""Borrar todos los __pycache__."""
|
|
18
|
+
if platform.system() == 'Windows':
|
|
19
|
+
subprocess.run('for /d /r . %d in (__pycache__) do @if exist "%d" rd /s/q "%d"', shell=True)
|
|
20
|
+
else:
|
|
21
|
+
subprocess.run("find . -name '__pycache__' -type d -exec rm -rf {} +", shell=True)
|
|
22
|
+
|
|
23
|
+
logger.info(Message.PYCACHE_CLEANUP_SUCCESS_MSG)
|
|
24
|
+
|
|
25
|
+
@cli.command()
|
|
26
|
+
@click.argument('port')
|
|
27
|
+
def start(port: int):
|
|
28
|
+
"""Start the FastAPI server.."""
|
|
29
|
+
try:
|
|
30
|
+
subprocess.run(['uvicorn', 'app:app', '--port', str(port), '--reload'], check=True)
|
|
31
|
+
except subprocess.CalledProcessError as e:
|
|
32
|
+
logger.error(f'{Message.SERVER_NETWORK_ACCESS_ERROR_MSG}: {e}')
|
|
33
|
+
|
|
34
|
+
@cli.command()
|
|
35
|
+
@click.argument('port')
|
|
36
|
+
def serve(port: int):
|
|
37
|
+
"""Set up the FastAPI server accessible from any machine."""
|
|
38
|
+
try:
|
|
39
|
+
subprocess.run(['uvicorn', 'app:app', '--host', '0.0.0.0', '--port', str(port), '--reload'], check=True)
|
|
40
|
+
except subprocess.CalledProcessError as e:
|
|
41
|
+
logger.error(f'{Message.SERVER_NETWORK_ACCESS_ERROR_MSG}: {e}')
|
|
42
|
+
|
|
43
|
+
@cli.command("clean-redis")
|
|
44
|
+
@click.argument('redis_env')
|
|
45
|
+
async def clean_redis(redis_env: str):
|
|
46
|
+
try:
|
|
47
|
+
from Osdental.RedisCache.Redis import RedisCacheAsync
|
|
48
|
+
redis_url = os.getenv(redis_env)
|
|
49
|
+
if not redis_url:
|
|
50
|
+
logger.warning(f'Environment variable not found: {redis_env}')
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
redis = RedisCacheAsync(redis_url=redis_url)
|
|
54
|
+
await redis.flush()
|
|
55
|
+
logger.info(Message.REDIS_CLEANUP_SUCCESS_MSG)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
logger.error(f'{Message.REDIS_CLEANUP_ERROR_MSG}: {e}')
|
|
58
|
+
|
|
59
|
+
@cli.command(name='proto-files')
|
|
60
|
+
@click.argument('name')
|
|
61
|
+
def proto_files(name: str):
|
|
62
|
+
proto_dir = os.path.join('src', 'Infrastructure', 'Grpc', 'Proto').replace('\\', '/')
|
|
63
|
+
gen_dir = os.path.join('src', 'Infrastructure', 'Grpc', 'Generated').replace('\\', '/')
|
|
64
|
+
|
|
65
|
+
proto_path = os.path.join(proto_dir, f'{name}.proto').replace('\\', '/')
|
|
66
|
+
common_path = os.path.join(proto_dir, 'Common.proto').replace('\\', '/')
|
|
67
|
+
|
|
68
|
+
common_py = os.path.join(gen_dir, 'Common_pb2.py').replace('\\', '/')
|
|
69
|
+
common_grpc_py = os.path.join(gen_dir, 'Common_pb2_grpc.py').replace('\\', '/')
|
|
70
|
+
|
|
71
|
+
proto_files = [proto_path]
|
|
72
|
+
if not (os.path.exists(common_py) and os.path.exists(common_grpc_py)):
|
|
73
|
+
proto_files.append(common_path)
|
|
74
|
+
|
|
75
|
+
cmd = [
|
|
76
|
+
sys.executable, "-m", "grpc_tools.protoc",
|
|
77
|
+
f"-I={proto_dir}",
|
|
78
|
+
f"--python_out={gen_dir}",
|
|
79
|
+
f"--grpc_python_out={gen_dir}",
|
|
80
|
+
*proto_files
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
subprocess.run(cmd, shell=False, check=True)
|
|
84
|
+
logger.info(Message.PROTO_FILES_GENERATED_MSG)
|
|
85
|
+
|
|
86
|
+
@cli.command('run-server')
|
|
87
|
+
@click.option('--host', default="0.0.0.0", help='Host where to set up the server')
|
|
88
|
+
@click.option('--port', default=5000, help='Server port')
|
|
89
|
+
@click.option('--workers', default=4, help='Number of workers')
|
|
90
|
+
def run_server(host, port, workers):
|
|
91
|
+
"""
|
|
92
|
+
Launch the application with uvicorn on Windows or gunicorn on other systems
|
|
93
|
+
:host
|
|
94
|
+
:port
|
|
95
|
+
:workers
|
|
96
|
+
"""
|
|
97
|
+
if sys.platform.startswith('win'):
|
|
98
|
+
cmd = [
|
|
99
|
+
sys.executable, '-m', 'uvicorn',
|
|
100
|
+
'app:app',
|
|
101
|
+
'--host', host,
|
|
102
|
+
'--port', str(port),
|
|
103
|
+
'--workers', str(workers)
|
|
104
|
+
]
|
|
105
|
+
else:
|
|
106
|
+
cmd = [
|
|
107
|
+
sys.executable, '-m', 'gunicorn',
|
|
108
|
+
'app:app',
|
|
109
|
+
'-k', 'uvicorn.workers.UvicornWorker',
|
|
110
|
+
'--bind', f'{host}:{port}',
|
|
111
|
+
'--workers', str(workers)
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
logger.info(f'Running: {' '.join(cmd)}')
|
|
115
|
+
subprocess.run(cmd)
|
|
116
|
+
|
|
117
|
+
if __name__ == "__main__":
|
|
118
|
+
cli()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
from sqlalchemy import text
|
|
3
3
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
4
|
-
from Osdental.Exception.ControlledException import DatabaseException
|
|
4
|
+
from Osdental.Exception.ControlledException import DatabaseException, OSDException
|
|
5
5
|
from Osdental.Shared.Utils.DataNormalizer import normalize
|
|
6
6
|
|
|
7
7
|
class BaseRepository:
|
|
@@ -23,6 +23,7 @@ class BaseRepository:
|
|
|
23
23
|
many: bool = False,
|
|
24
24
|
as_dict: bool = False
|
|
25
25
|
) -> Any:
|
|
26
|
+
|
|
26
27
|
result = await self.async_session.execute(
|
|
27
28
|
text(query),
|
|
28
29
|
params or {}
|
|
@@ -34,13 +35,11 @@ class BaseRepository:
|
|
|
34
35
|
else result.mappings().first()
|
|
35
36
|
)
|
|
36
37
|
|
|
37
|
-
if
|
|
38
|
-
return data
|
|
39
|
-
|
|
40
|
-
if many:
|
|
38
|
+
if as_dict and data is not None:
|
|
41
39
|
return normalize(data)
|
|
42
40
|
|
|
43
|
-
return
|
|
41
|
+
return data
|
|
42
|
+
|
|
44
43
|
|
|
45
44
|
async def _execute_command(
|
|
46
45
|
self,
|
|
@@ -48,8 +47,10 @@ class BaseRepository:
|
|
|
48
47
|
params: dict | None = None,
|
|
49
48
|
*,
|
|
50
49
|
validate: bool = True,
|
|
51
|
-
success_codes: str | int | tuple[str | int, ...] | None = None
|
|
52
|
-
|
|
50
|
+
success_codes: str | int | tuple[str | int, ...] | None = None,
|
|
51
|
+
as_dict: bool = False
|
|
52
|
+
) -> Any:
|
|
53
|
+
|
|
53
54
|
result = await self.async_session.execute(
|
|
54
55
|
text(query),
|
|
55
56
|
params or {}
|
|
@@ -58,7 +59,8 @@ class BaseRepository:
|
|
|
58
59
|
row = result.mappings().first()
|
|
59
60
|
|
|
60
61
|
if not validate or not row:
|
|
61
|
-
return row
|
|
62
|
+
return normalize(row) if as_dict else row
|
|
63
|
+
|
|
62
64
|
|
|
63
65
|
status_code = self._get_status_value(
|
|
64
66
|
row,
|
|
@@ -82,17 +84,29 @@ class BaseRepository:
|
|
|
82
84
|
error="Status code missing",
|
|
83
85
|
status_code=500
|
|
84
86
|
)
|
|
85
|
-
return row
|
|
86
87
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
else:
|
|
89
|
+
|
|
90
|
+
if not isinstance(success_codes, tuple):
|
|
91
|
+
success_codes = (success_codes,)
|
|
92
|
+
|
|
93
|
+
if status_code not in success_codes:
|
|
94
|
+
raise DatabaseException(
|
|
95
|
+
message=status_message,
|
|
96
|
+
error=status_message,
|
|
97
|
+
status_code=status_code
|
|
98
|
+
)
|
|
90
99
|
|
|
91
|
-
if
|
|
92
|
-
|
|
93
|
-
message=status_message,
|
|
94
|
-
error=status_message,
|
|
95
|
-
status_code=status_code
|
|
96
|
-
)
|
|
100
|
+
return normalize(row) if as_dict else row
|
|
101
|
+
|
|
97
102
|
|
|
98
|
-
|
|
103
|
+
async def _execute_scalar(
|
|
104
|
+
self,
|
|
105
|
+
query: str,
|
|
106
|
+
params: dict | None = None
|
|
107
|
+
) -> Any | None:
|
|
108
|
+
result = await self.async_session.execute(
|
|
109
|
+
text(query),
|
|
110
|
+
params or {}
|
|
111
|
+
)
|
|
112
|
+
return result.scalar_one_or_none()
|
|
@@ -2,15 +2,13 @@ import inspect
|
|
|
2
2
|
import copy
|
|
3
3
|
from functools import wraps
|
|
4
4
|
from typing import Callable, Dict, Any
|
|
5
|
-
from graphql import GraphQLResolveInfo
|
|
6
5
|
from Osdental.Models.Token import AuthToken
|
|
7
6
|
from Osdental.Models.Response import Response
|
|
8
7
|
from Osdental.Shared.Enums.Profile import Profile
|
|
9
|
-
from Osdental.Exception.ControlledException import OSDException
|
|
8
|
+
from Osdental.Exception.ControlledException import OSDException, AccessDeniedException
|
|
10
9
|
from Osdental.Encryptor.Aes import AES
|
|
10
|
+
from Osdental.Graphql.Models import BaseGraphQLContext
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
aes = AES()
|
|
14
12
|
def secure_resolver(action = None):
|
|
15
13
|
def decorator(func: Callable):
|
|
16
14
|
|
|
@@ -18,25 +16,25 @@ def secure_resolver(action = None):
|
|
|
18
16
|
accepts_data = "data" in signature.parameters
|
|
19
17
|
|
|
20
18
|
@wraps(func)
|
|
21
|
-
async def wrapper(obj: Any,
|
|
19
|
+
async def wrapper(obj: Any, context: BaseGraphQLContext, **kwargs: Dict[str, Any]):
|
|
22
20
|
try:
|
|
23
|
-
token: AuthToken =
|
|
24
|
-
payload = getattr(
|
|
25
|
-
|
|
21
|
+
token: AuthToken = context.token
|
|
22
|
+
payload = getattr(context, "decrypted_payload", None)
|
|
23
|
+
aes_auth = getattr(context, "aes_auth", None)
|
|
26
24
|
|
|
27
25
|
if action:
|
|
28
26
|
if Profile(token.abbreviation) not in action.allowed_roles:
|
|
29
|
-
raise
|
|
27
|
+
raise AccessDeniedException(error="The user's profile is not allowed to perform this action.")
|
|
30
28
|
|
|
31
29
|
if accepts_data and payload:
|
|
32
30
|
kwargs["data"] = payload
|
|
33
31
|
|
|
34
|
-
result: Response = await func(obj,
|
|
32
|
+
result: Response = await func(obj, context, **kwargs)
|
|
35
33
|
|
|
36
|
-
|
|
34
|
+
context.audit_plain_response = copy.deepcopy(result)
|
|
37
35
|
|
|
38
36
|
if result.data is not None:
|
|
39
|
-
result.data =
|
|
37
|
+
result.data = AES.encrypt(aes_auth, result.data)
|
|
40
38
|
|
|
41
39
|
return result.send()
|
|
42
40
|
|
|
@@ -46,7 +44,7 @@ def secure_resolver(action = None):
|
|
|
46
44
|
message=getattr(e, "message", "Could not process request."),
|
|
47
45
|
error=f"{type(e).__name__}: {str(e)}"
|
|
48
46
|
)
|
|
49
|
-
|
|
47
|
+
context.audit_plain_response = result
|
|
50
48
|
return result.send()
|
|
51
49
|
|
|
52
50
|
return wrapper
|
|
@@ -10,9 +10,8 @@ from Osdental.Shared.Enums.Constant import Constant
|
|
|
10
10
|
|
|
11
11
|
class AES:
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
self.TAG_LENGTH = 16
|
|
13
|
+
IV_LENGTH = 32
|
|
14
|
+
TAG_LENGTH = 16
|
|
16
15
|
|
|
17
16
|
@staticmethod
|
|
18
17
|
def generate_key() -> str:
|
|
@@ -20,7 +19,8 @@ class AES:
|
|
|
20
19
|
key = os.urandom(32)
|
|
21
20
|
return base64.b64encode(key).decode(Constant.DEFAULT_ENCODING)
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
@classmethod
|
|
23
|
+
def encrypt(cls, aes_key:str, data:Dict[str,str] | str | List[Dict[str,str]]) -> str:
|
|
24
24
|
"""
|
|
25
25
|
Encrypts data using AES-GCM.
|
|
26
26
|
Supports dictionary, string, or list inputs.
|
|
@@ -34,7 +34,7 @@ class AES:
|
|
|
34
34
|
|
|
35
35
|
try:
|
|
36
36
|
key = base64.b64decode(aes_key)
|
|
37
|
-
iv = os.urandom(
|
|
37
|
+
iv = os.urandom(cls.IV_LENGTH)
|
|
38
38
|
if isinstance(data, (dict, list)):
|
|
39
39
|
json_data = json.dumps(data)
|
|
40
40
|
else:
|
|
@@ -52,8 +52,8 @@ class AES:
|
|
|
52
52
|
logger.error(f'Unexpected AES encryption error: {str(e)}')
|
|
53
53
|
raise AESEncryptException(error=str(e))
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
def decrypt(
|
|
55
|
+
@classmethod
|
|
56
|
+
def decrypt(cls, aes_key:str, encrypted_data:str, silent:bool = False):
|
|
57
57
|
"""
|
|
58
58
|
Decrypts data using AES-GCM.
|
|
59
59
|
Expects encrypted data to represent either a JSON object (dict) or a plain string.
|
|
@@ -65,9 +65,9 @@ class AES:
|
|
|
65
65
|
try:
|
|
66
66
|
key = base64.b64decode(aes_key)
|
|
67
67
|
encrypted_data = base64.b64decode(encrypted_data)
|
|
68
|
-
iv = encrypted_data[:
|
|
69
|
-
tag = encrypted_data[-
|
|
70
|
-
ciphertext = encrypted_data[
|
|
68
|
+
iv = encrypted_data[:cls.IV_LENGTH]
|
|
69
|
+
tag = encrypted_data[-cls.TAG_LENGTH:]
|
|
70
|
+
ciphertext = encrypted_data[cls.IV_LENGTH:-cls.TAG_LENGTH]
|
|
71
71
|
cipher = Cipher(algorithms.AES(key), modes.GCM(iv, tag))
|
|
72
72
|
decryptor = cipher.decryptor()
|
|
73
73
|
plaintext = decryptor.update(ciphertext) + decryptor.finalize()
|
|
@@ -24,6 +24,15 @@ class UnauthorizedException(OSDException):
|
|
|
24
24
|
):
|
|
25
25
|
super().__init__(message=message, error=error, status_code=status_code)
|
|
26
26
|
|
|
27
|
+
class AccessDeniedException(OSDException):
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
message: str = Message.ACCESS_DENIED_MSG,
|
|
31
|
+
error: str = None,
|
|
32
|
+
status_code: str = Code.INVALID_REQUEST_PARAMS_CODE
|
|
33
|
+
):
|
|
34
|
+
super().__init__(message=message, error=error, status_code=status_code)
|
|
35
|
+
|
|
27
36
|
class RequestDataException(OSDException):
|
|
28
37
|
def __init__(
|
|
29
38
|
self,
|
|
@@ -7,6 +7,7 @@ from Osdental.Rest.Context.RequestContext import (
|
|
|
7
7
|
current_context,
|
|
8
8
|
RequestContext
|
|
9
9
|
)
|
|
10
|
+
from Osdental.Graphql.Models import BaseGraphQLContext
|
|
10
11
|
from Osdental.Graphql._Exceptions import AESKeyNotFound
|
|
11
12
|
from Osdental.Shared.Enums.Constant import Constant
|
|
12
13
|
|
|
@@ -25,27 +26,31 @@ class AuditExtension(Extension):
|
|
|
25
26
|
self.errors = None
|
|
26
27
|
self.request_payload = None
|
|
27
28
|
self.result = None
|
|
28
|
-
self.aes = AES()
|
|
29
29
|
|
|
30
30
|
async def resolve(self, next_, root, info, **kwargs):
|
|
31
31
|
|
|
32
32
|
if not self.request_payload:
|
|
33
|
-
context = info.context
|
|
33
|
+
context: BaseGraphQLContext = info.context
|
|
34
34
|
|
|
35
|
-
request =
|
|
35
|
+
request = context.request
|
|
36
36
|
self.request = request
|
|
37
37
|
|
|
38
|
-
body = request.
|
|
38
|
+
body = await request.json()
|
|
39
|
+
headers = request.headers
|
|
39
40
|
|
|
40
41
|
container = context.container
|
|
41
|
-
|
|
42
42
|
token_service = container.token_service
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
44
|
+
aes_auth = request.app.state.aes_auth
|
|
45
|
+
aes_user = request.app.state.aes_user
|
|
46
|
+
|
|
47
|
+
if not aes_auth:
|
|
48
|
+
raise AESKeyNotFound("AES user key is missing.", "aes_auth")
|
|
49
|
+
|
|
50
|
+
if not aes_user:
|
|
51
|
+
raise AESKeyNotFound("AES authorization key is missing.", "aes_user")
|
|
47
52
|
|
|
48
|
-
original_token = await token_service.authenticate(
|
|
53
|
+
original_token = await token_service.authenticate(headers, aes_user)
|
|
49
54
|
|
|
50
55
|
variables = body.get("variables") or {}
|
|
51
56
|
encrypted_payload = variables.get("data")
|
|
@@ -54,7 +59,7 @@ class AuditExtension(Extension):
|
|
|
54
59
|
|
|
55
60
|
if encrypted_payload:
|
|
56
61
|
try:
|
|
57
|
-
decrypted_payload =
|
|
62
|
+
decrypted_payload = AES.decrypt(aes_auth, encrypted_payload)
|
|
58
63
|
|
|
59
64
|
try:
|
|
60
65
|
decrypted_payload = json.loads(decrypted_payload)
|
|
@@ -81,7 +86,7 @@ class AuditExtension(Extension):
|
|
|
81
86
|
|
|
82
87
|
context.decrypted_payload = decrypted_payload
|
|
83
88
|
context.token = token
|
|
84
|
-
context.
|
|
89
|
+
context.aes_auth = aes_auth
|
|
85
90
|
|
|
86
91
|
result = next_(root, info, **kwargs)
|
|
87
92
|
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional, Any
|
|
3
|
+
from fastapi import Request
|
|
4
|
+
from Osdental.Models.Token import AuthToken
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class BaseGraphQLContext:
|
|
8
|
+
request: Request
|
|
9
|
+
container: Any
|
|
10
|
+
# valores agregados durante ejecución (AuditExtention)
|
|
11
|
+
token: Optional[AuthToken] = None
|
|
12
|
+
decrypted_payload: Optional[dict] = None
|
|
13
|
+
aes_auth: Optional[str] = None
|
|
14
|
+
audit_plain_response: Optional[Any] = None
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from starlette.datastructures import Headers
|
|
2
|
+
|
|
3
|
+
class ExtractAuthToken:
|
|
4
|
+
|
|
5
|
+
@staticmethod
|
|
6
|
+
def get_auth_token(headers: Headers) -> str:
|
|
7
|
+
authorization = headers.get("authorization")
|
|
8
|
+
if not authorization or not authorization.startswith("Bearer "):
|
|
9
|
+
raise ValueError("Missing Bearer token")
|
|
10
|
+
|
|
11
|
+
return authorization.split(" ")[1]
|
|
@@ -14,13 +14,16 @@ class TenantPolicy:
|
|
|
14
14
|
decrypted_payload: Dict[str, Any],
|
|
15
15
|
operation_type: str
|
|
16
16
|
) -> AuthToken:
|
|
17
|
-
aes = AES()
|
|
18
|
-
# Solo aplicamos cambios si es QUERY
|
|
19
|
-
if operation_type != OperationType.QUERY.value:
|
|
20
|
-
return token # devolver token tal cual
|
|
21
17
|
|
|
18
|
+
# Set original idExternalEnterprise
|
|
19
|
+
token.base_id_external_enterprise = token.id_external_enterprise
|
|
22
20
|
# SUPER ADMIN / OSDEL ADMIN -> UUID 0
|
|
23
|
-
|
|
21
|
+
should_use_zero_uuid = (
|
|
22
|
+
token.abbreviation.startswith(("SPAU", "OSDA"))
|
|
23
|
+
and operation_type == OperationType.QUERY
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
if should_use_zero_uuid:
|
|
24
27
|
token.id_external_enterprise = str(UUID(int=0))
|
|
25
28
|
return token
|
|
26
29
|
|
|
@@ -28,13 +31,13 @@ class TenantPolicy:
|
|
|
28
31
|
if token.abbreviation.startswith("OSDMK"):
|
|
29
32
|
dynamic_client_id = headers.get("dynamicClientId")
|
|
30
33
|
if dynamic_client_id:
|
|
31
|
-
decrypted_mk_id =
|
|
34
|
+
decrypted_mk_id = AES.decrypt(token.aes_key_auth, dynamic_client_id)
|
|
32
35
|
token.id_external_enterprise = decrypted_mk_id
|
|
33
36
|
token.mk_id_external_enterprise = decrypted_mk_id
|
|
34
37
|
return token
|
|
35
38
|
|
|
36
39
|
# If it comes by request, it is taken as priority
|
|
37
|
-
external_enterprise_req = decrypted_payload.get(
|
|
40
|
+
external_enterprise_req = decrypted_payload.get("idExternalEnterprise")
|
|
38
41
|
if external_enterprise_req and token:
|
|
39
42
|
token.id_external_enterprise = external_enterprise_req
|
|
40
43
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from
|
|
1
|
+
from starlette.datastructures import Headers
|
|
2
2
|
from Osdental.Encryptor.Aes import AES
|
|
3
3
|
from Osdental.Encryptor.Jwt import JWT
|
|
4
4
|
from Osdental.Models.Token import AuthToken
|
|
@@ -9,23 +9,20 @@ class TokenService:
|
|
|
9
9
|
def __init__(self, jwt_user_key: str, auth_validator):
|
|
10
10
|
self.jwt_user_key = jwt_user_key
|
|
11
11
|
self.auth_validator = auth_validator
|
|
12
|
-
self.aes = AES()
|
|
13
12
|
|
|
14
|
-
async def authenticate(self,
|
|
15
|
-
encrypted_token = ExtractAuthToken.get_auth_token(
|
|
13
|
+
async def authenticate(self, headers: Headers, aes_user: str) -> AuthToken:
|
|
14
|
+
encrypted_token = ExtractAuthToken.get_auth_token(headers)
|
|
16
15
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
user_token = self.aes.decrypt(aes_user, encrypted_token)
|
|
16
|
+
user_token = AES.decrypt(aes_user, encrypted_token)
|
|
20
17
|
payload = JWT.extract_payload(user_token, self.jwt_user_key)
|
|
21
18
|
token = AuthToken(**payload)
|
|
22
19
|
|
|
23
20
|
# Validate via RPC
|
|
24
|
-
|
|
21
|
+
paylod = {
|
|
25
22
|
'idToken': token.id_token,
|
|
26
23
|
'idUser': token.id_user
|
|
27
24
|
}
|
|
28
|
-
is_valid = await self.auth_validator.validate_auth_token(
|
|
25
|
+
is_valid = await self.auth_validator.validate_auth_token(paylod)
|
|
29
26
|
if not is_valid:
|
|
30
27
|
raise ValueError("You are not authorized to access this portal.")
|
|
31
28
|
|
|
@@ -18,8 +18,6 @@ class AuthToken(BaseModel):
|
|
|
18
18
|
access_token: Optional[str] = Field(default=None, alias="accessToken")
|
|
19
19
|
base_id_external_enterprise: Optional[str] = Field(default=None, alias="baseIdExternalEnterprise")
|
|
20
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
21
|
|
|
24
22
|
class ConfigDict:
|
|
25
23
|
populate_by_name = True
|
|
@@ -8,6 +8,7 @@ class Code(StrEnum):
|
|
|
8
8
|
APP_ERROR_CODE = 'DB_ERROR_CONTROLLED'
|
|
9
9
|
UNEXPECTED_ERROR_CODE = 'DB_ERROR_UNEXPECTED'
|
|
10
10
|
INVALID_REQUEST_PARAMS_CODE = 'DB_WARNING_INVALID_REQUEST_PARAMS'
|
|
11
|
+
ACCESS_DENIED_CODE = "DB_WARNING_ACCESS_DENIED_CODE"
|
|
11
12
|
DATABASE_ERROR_CODE = 'DB_WARNING_DATABASE_ERROR'
|
|
12
13
|
DATABASE_CONNECTION_ERROR_CODE = 'DB_ERROR_DATABASE_CONNECTION'
|
|
13
14
|
RSA_ERROR_CODE = 'DB_ERROR_RSA'
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from enum import StrEnum
|
|
2
2
|
|
|
3
3
|
class Message(StrEnum):
|
|
4
|
+
ACCESS_DENIED_MSG = "You do not have the necessary privileges for this operation."
|
|
4
5
|
UNEXPECTED_ERROR_MSG = 'Something went wrong while processing your request. Please try again later.'
|
|
5
6
|
PORTAL_ACCESS_RESTRICTED_MSG = 'You are not authorized to access this portal.'
|
|
6
7
|
PROCESS_SUCCESS_MSG = 'Process executed successfully.'
|
|
@@ -20,7 +21,6 @@ class Message(StrEnum):
|
|
|
20
21
|
AES_KEY_USER_REQUIRED_MSG = 'AES key user cannot be empty.'
|
|
21
22
|
AES_KEY_AUTH_REQUIRED_MSG = 'AES key auth cannot be empty.'
|
|
22
23
|
MISSING_FIELD_ERROR_MSG = 'A required field is missing. Please review the data.'
|
|
23
|
-
ID_CDATA_INTEGRATION_REQUIRED = 'Cdata integration cannot be empty or null'
|
|
24
24
|
EXP_TIME_REQUIRED = 'Expiration time (cdata integration) cannot be empty or null'
|
|
25
25
|
KEY_PRIVATE_REQUIRED = 'Key private cannot be empty or null'
|
|
26
26
|
SUB_ACCOUNT_REQUIRED = 'Sub account parameter is required'
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import mimetypes
|
|
3
3
|
from Osdental.Exception.ControlledException import ValidationDataException
|
|
4
|
-
from Osdental.Shared.Message import Message
|
|
4
|
+
from Osdental.Shared.Enums.Message import Message
|
|
5
5
|
from Osdental.Shared.Enums.FileType import FileType
|
|
6
6
|
|
|
7
7
|
class FileMetaData:
|