Encryptors 2.78__tar.gz → 2.79__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.78 → encryptors-2.79}/PKG-INFO +1 -1
- {encryptors-2.78 → encryptors-2.79}/setup.py +1 -1
- {encryptors-2.78 → encryptors-2.79}/src/Encryptors.egg-info/PKG-INFO +1 -1
- {encryptors-2.78 → encryptors-2.79}/src/Encryptors.egg-info/SOURCES.txt +1 -0
- encryptors-2.79/src/Osdental/Decorators/PublicResolver.py +139 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Decorators/SecureResolver.py +19 -36
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Helpers/AuthTokenProcessor.py +2 -2
- {encryptors-2.78 → encryptors-2.79}/README.md +0 -0
- {encryptors-2.78 → encryptors-2.79}/setup.cfg +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Encryptors.egg-info/dependency_links.txt +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Encryptors.egg-info/entry_points.txt +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Encryptors.egg-info/requires.txt +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Encryptors.egg-info/top_level.txt +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Cache/Redis.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Cache/__init__.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Cli/__init__.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Constants/Constant.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Constants/Message.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Constants/__init__.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Context/__init__.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Database/BaseRepository.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Database/Connection.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Database/__init__.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Decorators/Grpc.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Decorators/Retry.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Decorators/__init__.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Encryptor/Aes.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Encryptor/Argon2.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Encryptor/Bcrypt.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Encryptor/Jwt.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Encryptor/Rsa.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Encryptor/Sha512.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Encryptor/__init__.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Enums/AuditType.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Enums/CipherType.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Enums/FileType.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Enums/GrahpqlOperation.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Enums/GrpcStatusCode.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Enums/Profile.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Enums/ResultType.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Enums/Secret.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Enums/StatusCode.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Enums/__init__.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Exception/ControlledException.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Exception/__init__.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Helpers/AuditHelper.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Helpers/AuditQueue.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Helpers/AzureClassifier.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Helpers/CipherHelper.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Helpers/JwtTokenHelper.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Helpers/Resilience.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Helpers/__init__.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Http/APIClient.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Http/__init__.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Messaging/AzureServiceBus.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Messaging/Kafka.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Messaging/RabbitMQ.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Messaging/__init__.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Models/ApiResponse.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Models/AuditContext.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Models/Graphql.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Models/Notification.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Models/Response.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Models/Token.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Models/TokenClaims.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Models/__init__.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Secrets/AzureKeyVaultProvider.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Secrets/__init__.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Services/GrpcConnection.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Services/JwtAuthTokenService.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Services/ServiceBusAuditEmitter.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Services/WebsocketClient.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Services/__init__.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Storage/AzureBlobStorage.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Storage/S3Storage.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Storage/__init__.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Utils/CaseConverter.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Utils/CodeGenerator.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Utils/DataNormalizer.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Utils/DataUtils.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Utils/DateUtils.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Utils/FileMetaData.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Utils/HashValidator.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Utils/Mapper.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Utils/PasswordGenerator.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Utils/QueryGenerator.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Utils/RsaUtils.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Utils/TextProcessor.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/Utils/__init__.py +0 -0
- {encryptors-2.78 → encryptors-2.79}/src/Osdental/__init__.py +0 -0
|
@@ -18,6 +18,7 @@ src/Osdental/Database/BaseRepository.py
|
|
|
18
18
|
src/Osdental/Database/Connection.py
|
|
19
19
|
src/Osdental/Database/__init__.py
|
|
20
20
|
src/Osdental/Decorators/Grpc.py
|
|
21
|
+
src/Osdental/Decorators/PublicResolver.py
|
|
21
22
|
src/Osdental/Decorators/Retry.py
|
|
22
23
|
src/Osdental/Decorators/SecureResolver.py
|
|
23
24
|
src/Osdental/Decorators/__init__.py
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import asyncio
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from typing import Callable
|
|
5
|
+
from graphql import GraphQLResolveInfo
|
|
6
|
+
from Osdental.Models.Response import Response
|
|
7
|
+
from Osdental.Models.Graphql import BaseGraphQLContext
|
|
8
|
+
from Osdental.Models.TokenClaims import AccessTokenClaims
|
|
9
|
+
from Osdental.Exception.ControlledException import (
|
|
10
|
+
OSDException
|
|
11
|
+
)
|
|
12
|
+
from Osdental.Services import IAuthTokenService
|
|
13
|
+
from Osdental.Helpers.AuthTokenProcessor import (
|
|
14
|
+
decrypt_and_parse_payload, get_current_user,
|
|
15
|
+
get_current_aes_key
|
|
16
|
+
)
|
|
17
|
+
from Osdental.Enums.StatusCode import StatusCode
|
|
18
|
+
from Osdental.Models.AuditContext import AuditContext
|
|
19
|
+
from Osdental.Helpers.AuditHelper import (
|
|
20
|
+
_build_success_payload, _build_error_payload, _build_unexpected_payload
|
|
21
|
+
)
|
|
22
|
+
from Osdental.Context import _ctx
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def public_resolver():
|
|
28
|
+
|
|
29
|
+
def decorator(func: Callable):
|
|
30
|
+
|
|
31
|
+
@wraps(func)
|
|
32
|
+
async def wrapper(obj, info: GraphQLResolveInfo, **kwargs):
|
|
33
|
+
try:
|
|
34
|
+
context: BaseGraphQLContext = info.context
|
|
35
|
+
container = context.container
|
|
36
|
+
request = context.request
|
|
37
|
+
headers = request.headers
|
|
38
|
+
settings = container.settings
|
|
39
|
+
|
|
40
|
+
access_token = headers.get("Accesstoken")
|
|
41
|
+
claims: AccessTokenClaims | None = None
|
|
42
|
+
audit_ctx: AuditContext | None = None
|
|
43
|
+
operation_type = info.operation.operation.value
|
|
44
|
+
|
|
45
|
+
# ── 1. AUTENTICACIÓN ──────────────────────────────
|
|
46
|
+
token_service: IAuthTokenService = container.auth_token_service
|
|
47
|
+
|
|
48
|
+
claims = await token_service.validate_external_access_token(
|
|
49
|
+
access_token
|
|
50
|
+
)
|
|
51
|
+
context.access_token = access_token
|
|
52
|
+
context.access_token_claims = claims
|
|
53
|
+
|
|
54
|
+
# ── 2. DECRYPT DEL PAYLOAD ────────────────────────
|
|
55
|
+
body = await request.json()
|
|
56
|
+
encrypted_payload = (
|
|
57
|
+
kwargs.get("data")
|
|
58
|
+
or body.get("variables", {}).get("data")
|
|
59
|
+
)
|
|
60
|
+
decrypted_payload = None
|
|
61
|
+
|
|
62
|
+
if encrypted_payload is not None:
|
|
63
|
+
decrypted_payload = decrypt_and_parse_payload(
|
|
64
|
+
aes_key_auth=claims.aes_key,
|
|
65
|
+
encrypted_payload=encrypted_payload
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
kwargs["data"] = decrypted_payload
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ── 5. CONTEXTO DE AUDITORÍA ──────────────
|
|
72
|
+
# Lo construimos antes del resolver para que siempre exista
|
|
73
|
+
user = get_current_user(context)
|
|
74
|
+
key = get_current_aes_key(context)
|
|
75
|
+
audit_ctx = AuditContext.from_request(
|
|
76
|
+
request=request,
|
|
77
|
+
settings=settings,
|
|
78
|
+
body=body,
|
|
79
|
+
operation_type=operation_type,
|
|
80
|
+
id_user=user,
|
|
81
|
+
decrypted_key=key,
|
|
82
|
+
decrypted_payload=decrypted_payload,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
_ctx.set(audit_ctx)
|
|
86
|
+
|
|
87
|
+
# ── 6. EJECUTAR RESOLVER ──────────────────
|
|
88
|
+
result = await func(obj, info, **kwargs)
|
|
89
|
+
|
|
90
|
+
if not isinstance(result, Response):
|
|
91
|
+
raise TypeError("Resolver must return a Response instance")
|
|
92
|
+
|
|
93
|
+
# ── 7. AUDITAR ÉXITO ──────────────────────
|
|
94
|
+
_ = asyncio.create_task(
|
|
95
|
+
container.audit_emitter.emit(
|
|
96
|
+
{
|
|
97
|
+
"payload": _build_success_payload(audit_ctx, result),
|
|
98
|
+
"is_auditable": settings.is_auditable,
|
|
99
|
+
}
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return result
|
|
104
|
+
|
|
105
|
+
except OSDException as e:
|
|
106
|
+
logger.warning(f"Business error: {str(e)}")
|
|
107
|
+
if audit_ctx:
|
|
108
|
+
_ = asyncio.create_task(
|
|
109
|
+
container.audit_emitter.emit(
|
|
110
|
+
{
|
|
111
|
+
"payload": _build_error_payload(audit_ctx, e),
|
|
112
|
+
"is_auditable": settings.is_auditable,
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
return Response(
|
|
117
|
+
status=e.status_code,
|
|
118
|
+
message=e.message,
|
|
119
|
+
error=e.error
|
|
120
|
+
)
|
|
121
|
+
except Exception as e:
|
|
122
|
+
logger.exception(f"Unexpected error: {str(e)}")
|
|
123
|
+
if audit_ctx:
|
|
124
|
+
_ = asyncio.create_task(
|
|
125
|
+
container.audit_emitter.emit(
|
|
126
|
+
{
|
|
127
|
+
"payload": _build_unexpected_payload(audit_ctx, e),
|
|
128
|
+
"is_auditable": settings.is_auditable,
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
)
|
|
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
|
|
@@ -6,7 +6,7 @@ from graphql import GraphQLResolveInfo
|
|
|
6
6
|
from Osdental.Models.Response import Response
|
|
7
7
|
from Osdental.Models.Graphql import BaseGraphQLContext
|
|
8
8
|
from Osdental.Models.TokenClaims import (
|
|
9
|
-
UserTokenClaims
|
|
9
|
+
UserTokenClaims
|
|
10
10
|
)
|
|
11
11
|
from Osdental.Models.Token import AuthToken
|
|
12
12
|
from Osdental.Enums.Profile import Profile
|
|
@@ -28,14 +28,9 @@ from Osdental.Context import _ctx
|
|
|
28
28
|
logger = logging.getLogger(__name__)
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
def resolver(
|
|
32
|
-
public: bool = False,
|
|
33
|
-
skip_token_validation: bool = False,
|
|
34
|
-
action=None
|
|
35
|
-
):
|
|
31
|
+
def resolver(action=None):
|
|
36
32
|
|
|
37
33
|
def decorator(func: Callable):
|
|
38
|
-
func._is_public = public
|
|
39
34
|
|
|
40
35
|
@wraps(func)
|
|
41
36
|
async def wrapper(obj, info: GraphQLResolveInfo, **kwargs):
|
|
@@ -46,29 +41,19 @@ def resolver(
|
|
|
46
41
|
headers = request.headers
|
|
47
42
|
settings = container.settings
|
|
48
43
|
|
|
49
|
-
access_token = headers.get("Accesstoken")
|
|
50
44
|
token: type[AuthToken] | None = None
|
|
51
|
-
|
|
52
|
-
claims: UserTokenClaims | AccessTokenClaims | None = None
|
|
45
|
+
claims: UserTokenClaims | None = None
|
|
53
46
|
audit_ctx: AuditContext | None = None
|
|
54
47
|
operation_type = info.operation.operation.value
|
|
55
48
|
|
|
56
49
|
# ── 1. AUTENTICACIÓN ──────────────────────────────
|
|
57
50
|
token_service: IAuthTokenService = container.auth_token_service
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
elif public and access_token:
|
|
66
|
-
claims = await token_service.validate_external_access_token(
|
|
67
|
-
access_token
|
|
68
|
-
)
|
|
69
|
-
aes_key = claims.aes_key
|
|
70
|
-
context.access_token = access_token
|
|
71
|
-
context.access_token_claims = claims
|
|
51
|
+
user_token = extract_bearer_token(headers)
|
|
52
|
+
context.user_token = user_token
|
|
53
|
+
|
|
54
|
+
claims = await token_service.validate_internal_access_token(
|
|
55
|
+
user_token
|
|
56
|
+
)
|
|
72
57
|
|
|
73
58
|
# ── 2. DECRYPT DEL PAYLOAD ────────────────────────
|
|
74
59
|
body = await request.json()
|
|
@@ -80,26 +65,24 @@ def resolver(
|
|
|
80
65
|
|
|
81
66
|
if encrypted_payload is not None:
|
|
82
67
|
decrypted_payload = decrypt_and_parse_payload(
|
|
83
|
-
aes_key_auth=
|
|
68
|
+
aes_key_auth=claims.aes_key_auth,
|
|
84
69
|
encrypted_payload=encrypted_payload
|
|
85
70
|
)
|
|
86
71
|
|
|
87
72
|
kwargs["data"] = decrypted_payload
|
|
88
73
|
|
|
89
74
|
# ── 3. TENANT POLICY ──────────────────────────────
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
context.token = token
|
|
75
|
+
token = build_auth_token(
|
|
76
|
+
claims=claims,
|
|
77
|
+
headers=request.headers,
|
|
78
|
+
decrypted_payload=decrypted_payload,
|
|
79
|
+
operation_type=operation_type,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
context.token = token
|
|
100
83
|
|
|
101
84
|
# ── 4. CONTROL DE ACCESO POR ROL ─────────────────
|
|
102
|
-
if
|
|
85
|
+
if action:
|
|
103
86
|
if Profile(token.abbreviation) not in action.allowed_roles:
|
|
104
87
|
raise AccessDeniedException(
|
|
105
88
|
status_code=StatusCode.BUSINESS_RULE_WARNING,
|
|
@@ -10,11 +10,11 @@ from Osdental.Enums.Profile import Profile
|
|
|
10
10
|
from Osdental.Models.TokenClaims import UserTokenClaims
|
|
11
11
|
from Osdental.Models.Graphql import BaseGraphQLContext
|
|
12
12
|
|
|
13
|
-
def extract_bearer_token(headers: Headers) -> str:
|
|
13
|
+
def extract_bearer_token(headers: Headers) -> str | None:
|
|
14
14
|
|
|
15
15
|
authorization = headers.get("authorization")
|
|
16
16
|
if not authorization or not authorization.startswith("Bearer "):
|
|
17
|
-
|
|
17
|
+
return None
|
|
18
18
|
|
|
19
19
|
return authorization.removeprefix("Bearer ").strip()
|
|
20
20
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
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
|