openshell-shared 0.1.2__tar.gz → 0.1.4__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.
- {openshell_shared-0.1.2 → openshell_shared-0.1.4}/PKG-INFO +2 -2
- {openshell_shared-0.1.2 → openshell_shared-0.1.4}/openshell_shared.egg-info/PKG-INFO +2 -2
- openshell_shared-0.1.4/openshell_shared.egg-info/SOURCES.txt +7 -0
- openshell_shared-0.1.4/openshell_shared.egg-info/top_level.txt +1 -0
- {openshell_shared-0.1.2 → openshell_shared-0.1.4}/pyproject.toml +3 -3
- openshell_shared-0.1.2/api/__init__.py +0 -1
- openshell_shared-0.1.2/api/manager/__init__.py +0 -1
- openshell_shared-0.1.2/api/manager/v1/__init__.py +0 -78
- openshell_shared-0.1.2/api/manager/v1/authentication.py +0 -278
- openshell_shared-0.1.2/api/manager/v1/client.py +0 -111
- openshell_shared-0.1.2/api/manager/v1/domains.py +0 -64
- openshell_shared-0.1.2/api/manager/v1/entities.py +0 -97
- openshell_shared-0.1.2/api/manager/v1/exceptions.py +0 -98
- openshell_shared-0.1.2/api/manager/v1/identity.py +0 -44
- openshell_shared-0.1.2/api/manager/v1/models.py +0 -342
- openshell_shared-0.1.2/api/manager/v1/passports.py +0 -131
- openshell_shared-0.1.2/api/manager/v1/sessions.py +0 -83
- openshell_shared-0.1.2/api/manager/v1/transport.py +0 -253
- openshell_shared-0.1.2/api/manager/v1/tunnels.py +0 -120
- openshell_shared-0.1.2/cryptography/__init__.py +0 -0
- openshell_shared-0.1.2/cryptography/certificate.py +0 -390
- openshell_shared-0.1.2/cryptography/encoding.py +0 -0
- openshell_shared-0.1.2/cryptography/identity.py +0 -124
- openshell_shared-0.1.2/cryptography/keys.py +0 -463
- openshell_shared-0.1.2/cryptography/signatures.py +0 -63
- openshell_shared-0.1.2/cryptography/utils.py +0 -0
- openshell_shared-0.1.2/domain/__init__.py +0 -0
- openshell_shared-0.1.2/domain/domain.py +0 -80
- openshell_shared-0.1.2/domain/membership.py +0 -21
- openshell_shared-0.1.2/domain/permissions.py +0 -14
- openshell_shared-0.1.2/domain/policies.py +0 -2
- openshell_shared-0.1.2/identity/__init__.py +0 -0
- openshell_shared-0.1.2/identity/identification.py +0 -64
- openshell_shared-0.1.2/identity/store.py +0 -150
- openshell_shared-0.1.2/modules/__init__.py +0 -0
- openshell_shared-0.1.2/modules/shell/__init__.py +0 -0
- openshell_shared-0.1.2/modules/shell/client.py +0 -361
- openshell_shared-0.1.2/modules/shell/models.py +0 -61
- openshell_shared-0.1.2/modules/shell/protocol.py +0 -249
- openshell_shared-0.1.2/modules/shell/server.py +0 -511
- openshell_shared-0.1.2/modules/shell/session.py +0 -339
- openshell_shared-0.1.2/modules/utils.py +0 -212
- openshell_shared-0.1.2/openshell_shared.egg-info/SOURCES.txt +0 -65
- openshell_shared-0.1.2/openshell_shared.egg-info/top_level.txt +0 -9
- openshell_shared-0.1.2/protocols/__init__.py +0 -0
- openshell_shared-0.1.2/protocols/negotiation/challenge.py +0 -127
- openshell_shared-0.1.2/protocols/negotiation/models.py +0 -28
- openshell_shared-0.1.2/standards/__init__.py +0 -0
- openshell_shared-0.1.2/standards/certificates/__init__.py +0 -0
- openshell_shared-0.1.2/standards/certificates/status.py +0 -12
- openshell_shared-0.1.2/standards/certificates/types.py +0 -11
- openshell_shared-0.1.2/standards/entities/__init__.py +0 -0
- openshell_shared-0.1.2/standards/entities/types.py +0 -14
- openshell_shared-0.1.2/standards/events/__init__.py +0 -0
- openshell_shared-0.1.2/standards/events/schemas/__init__.py +0 -0
- openshell_shared-0.1.2/standards/events/schemas/entity_registered.py +0 -13
- openshell_shared-0.1.2/standards/events/types.py +0 -18
- openshell_shared-0.1.2/standards/passports/__init__.py +0 -0
- openshell_shared-0.1.2/standards/passports/types.py +0 -5
- openshell_shared-0.1.2/standards/permissions/__init__.py +0 -0
- openshell_shared-0.1.2/standards/permissions/types.py +0 -14
- openshell_shared-0.1.2/standards/roles/__init__.py +0 -0
- openshell_shared-0.1.2/standards/roles/types.py +0 -8
- openshell_shared-0.1.2/standards/transports/__init__.py +0 -0
- openshell_shared-0.1.2/standards/transports/types.py +0 -24
- {openshell_shared-0.1.2 → openshell_shared-0.1.4}/README.md +0 -0
- {openshell_shared-0.1.2 → openshell_shared-0.1.4}/openshell_shared.egg-info/dependency_links.txt +0 -0
- {openshell_shared-0.1.2 → openshell_shared-0.1.4}/openshell_shared.egg-info/requires.txt +0 -0
- {openshell_shared-0.1.2 → openshell_shared-0.1.4}/setup.cfg +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openshell-shared
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Shared library for OpenShell (OSAM/OSA/OSAC)
|
|
5
|
-
Author:
|
|
5
|
+
Author: Specter
|
|
6
6
|
License: Proprietary
|
|
7
7
|
Requires-Python: >=3.12
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openshell-shared
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Shared library for OpenShell (OSAM/OSA/OSAC)
|
|
5
|
-
Author:
|
|
5
|
+
Author: Specter
|
|
6
6
|
License: Proprietary
|
|
7
7
|
Requires-Python: >=3.12
|
|
8
8
|
Description-Content-Type: text/markdown
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -4,13 +4,13 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "openshell-shared"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.4"
|
|
8
8
|
description = "Shared library for OpenShell (OSAM/OSA/OSAC)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
11
11
|
|
|
12
12
|
authors = [
|
|
13
|
-
{ name = "
|
|
13
|
+
{ name = "Specter" }
|
|
14
14
|
]
|
|
15
15
|
|
|
16
16
|
license = { text = "Proprietary" }
|
|
@@ -27,5 +27,5 @@ include-package-data = true
|
|
|
27
27
|
|
|
28
28
|
[tool.setuptools.packages.find]
|
|
29
29
|
where = ["."]
|
|
30
|
-
include = ["
|
|
30
|
+
include = ["openshell_shared/*"]
|
|
31
31
|
exclude = ["*__pycache__*", "*.egg-info*"]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
from . import manager
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
from . import v1
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
# shared/api/manager/v1/__init__.py
|
|
2
|
-
"""
|
|
3
|
-
SDK oficial de Python para consumir la API HTTP de OpenShell Access
|
|
4
|
-
Manager (OSAM).
|
|
5
|
-
|
|
6
|
-
Uso típico::
|
|
7
|
-
|
|
8
|
-
from shared.api.manager.v1 import OSAMClient
|
|
9
|
-
|
|
10
|
-
async with OSAMClient(host="...", port=8000, protocol="https") as client:
|
|
11
|
-
...
|
|
12
|
-
|
|
13
|
-
Todo el sistema (OpenShell Console, OpenShell Agent, GUIs, herramientas
|
|
14
|
-
automatizadas) debe consumir OSAM exclusivamente a través de este paquete;
|
|
15
|
-
ningún otro componente debe importar ``httpx``/``requests`` directamente
|
|
16
|
-
para hablar con OSAM.
|
|
17
|
-
"""
|
|
18
|
-
|
|
19
|
-
from .client import OSAMClient
|
|
20
|
-
from .exceptions import (
|
|
21
|
-
APIError,
|
|
22
|
-
AuthenticationError,
|
|
23
|
-
AuthorizationError,
|
|
24
|
-
EntityNotFoundError,
|
|
25
|
-
InvalidResponseError,
|
|
26
|
-
NetworkError,
|
|
27
|
-
OSAMError,
|
|
28
|
-
ServerError,
|
|
29
|
-
ValidationError,
|
|
30
|
-
)
|
|
31
|
-
from .models import (
|
|
32
|
-
ClientChallenge,
|
|
33
|
-
ClientChallengeVerification,
|
|
34
|
-
CryptographicIdentity,
|
|
35
|
-
Domain,
|
|
36
|
-
Entity,
|
|
37
|
-
EntityTypeInfo,
|
|
38
|
-
IntegrationResult,
|
|
39
|
-
LogicalIdentity,
|
|
40
|
-
Passport,
|
|
41
|
-
ServerChallengeResponse,
|
|
42
|
-
SessionDeletionResult,
|
|
43
|
-
SessionInfo,
|
|
44
|
-
TunnelInfo,
|
|
45
|
-
TunnelOperationResult,
|
|
46
|
-
)
|
|
47
|
-
|
|
48
|
-
__all__ = [
|
|
49
|
-
# Cliente principal
|
|
50
|
-
"OSAMClient",
|
|
51
|
-
# Excepciones
|
|
52
|
-
"OSAMError",
|
|
53
|
-
"NetworkError",
|
|
54
|
-
"InvalidResponseError",
|
|
55
|
-
"APIError",
|
|
56
|
-
"ValidationError",
|
|
57
|
-
"AuthenticationError",
|
|
58
|
-
"AuthorizationError",
|
|
59
|
-
"EntityNotFoundError",
|
|
60
|
-
"ServerError",
|
|
61
|
-
# Modelos
|
|
62
|
-
"LogicalIdentity",
|
|
63
|
-
"CryptographicIdentity",
|
|
64
|
-
"EntityTypeInfo",
|
|
65
|
-
"ClientChallenge",
|
|
66
|
-
"ClientChallengeVerification",
|
|
67
|
-
"ServerChallengeResponse",
|
|
68
|
-
"Domain",
|
|
69
|
-
"Entity",
|
|
70
|
-
"Passport",
|
|
71
|
-
"IntegrationResult",
|
|
72
|
-
"SessionInfo",
|
|
73
|
-
"SessionDeletionResult",
|
|
74
|
-
"TunnelInfo",
|
|
75
|
-
"TunnelOperationResult",
|
|
76
|
-
]
|
|
77
|
-
|
|
78
|
-
__version__ = "0.1.0"
|
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
# shared/api/manager/v1/authentication.py
|
|
2
|
-
"""
|
|
3
|
-
Dominio: Authentication.
|
|
4
|
-
|
|
5
|
-
Implementa el protocolo de autenticación por challenge-response (Ed25519)
|
|
6
|
-
de OSAM, en sus dos direcciones:
|
|
7
|
-
|
|
8
|
-
* Autenticación del **cliente** ante el servidor (``client/challenge`` +
|
|
9
|
-
``verify``): la entidad que llama solicita un reto, lo firma con su clave
|
|
10
|
-
privada y envía la respuesta para que el servidor la valide.
|
|
11
|
-
* Autenticación del **servidor** ante el cliente (``server/challenge``):
|
|
12
|
-
el cliente ya posee un ``challenge_id`` (generado en un paso anterior del
|
|
13
|
-
protocolo, fuera del alcance de este módulo) y le pide al servidor que
|
|
14
|
-
produzca su respuesta firmada para poder verificarla localmente.
|
|
15
|
-
|
|
16
|
-
Endpoints cubiertos (todos bajo ``/api/v/1/auth``):
|
|
17
|
-
POST /client/challenge
|
|
18
|
-
POST /verify
|
|
19
|
-
POST /server/challenge
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
from __future__ import annotations
|
|
23
|
-
|
|
24
|
-
from .models import (
|
|
25
|
-
ClientChallenge,
|
|
26
|
-
ClientChallengeVerification,
|
|
27
|
-
ServerChallengeResponse,
|
|
28
|
-
)
|
|
29
|
-
from .transport import HttpTransport
|
|
30
|
-
from ....protocols.negotiation.challenge import ChallengeProtocol, CHALLENGE_TYPE_SERVER_AUTHENTICATION
|
|
31
|
-
|
|
32
|
-
_PREFIX = "/api/v/1/auth"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class AuthenticationAPI:
|
|
36
|
-
"""API especializada para el protocolo de autenticación de OSAM."""
|
|
37
|
-
|
|
38
|
-
def __init__(self, transport: HttpTransport) -> None:
|
|
39
|
-
self._transport = transport
|
|
40
|
-
|
|
41
|
-
async def create_client_challenge(
|
|
42
|
-
self, entity_uid: str, public_key: str
|
|
43
|
-
) -> dict:
|
|
44
|
-
"""
|
|
45
|
-
POST /api/v/1/auth/client/challenge
|
|
46
|
-
|
|
47
|
-
Solicita al servidor un reto de autenticación para la entidad
|
|
48
|
-
identificada por ``entity_uid`` / ``public_key``.
|
|
49
|
-
"""
|
|
50
|
-
body = await self._transport.post(
|
|
51
|
-
f"{_PREFIX}/client/challenge",
|
|
52
|
-
json={"entity_uid": entity_uid, "public_key": public_key},
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
return ClientChallenge.from_dict(body)
|
|
56
|
-
|
|
57
|
-
async def verify_client_challenge(
|
|
58
|
-
self,
|
|
59
|
-
challenge_id: str,
|
|
60
|
-
response: str,
|
|
61
|
-
entity_uid: str,
|
|
62
|
-
public_key: str,
|
|
63
|
-
) -> ClientChallengeVerification:
|
|
64
|
-
"""
|
|
65
|
-
POST /api/v/1/auth/verify
|
|
66
|
-
|
|
67
|
-
Envía la respuesta firmada a un reto previamente emitido por
|
|
68
|
-
``create_client_challenge`` para completar la autenticación.
|
|
69
|
-
"""
|
|
70
|
-
body = await self._transport.post(
|
|
71
|
-
f"{_PREFIX}/verify",
|
|
72
|
-
json={
|
|
73
|
-
"challenge_id": challenge_id,
|
|
74
|
-
"response": response,
|
|
75
|
-
"entity_uid": entity_uid,
|
|
76
|
-
"public_key": public_key,
|
|
77
|
-
},
|
|
78
|
-
)
|
|
79
|
-
return body
|
|
80
|
-
|
|
81
|
-
async def authenticate_client(self,
|
|
82
|
-
entity_uid: str,
|
|
83
|
-
entity_pik: str,
|
|
84
|
-
entity_ppik: str
|
|
85
|
-
) -> str:
|
|
86
|
-
# Create challenge
|
|
87
|
-
client_challenge = await self.create_client_challenge(
|
|
88
|
-
entity_uid=entity_uid,
|
|
89
|
-
public_key=entity_pik
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
client_challenge_object = ChallengeProtocol.challenge_from_dict(
|
|
93
|
-
client_challenge.challenge
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
client_auth_signed = ChallengeProtocol.sign(
|
|
97
|
-
private_key=entity_ppik,
|
|
98
|
-
challenge=client_challenge_object
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
response = await self.verify_client_challenge(
|
|
102
|
-
challenge_id=client_challenge.challenge.get("challenge_id"),
|
|
103
|
-
response=ChallengeProtocol.response_to_dict(client_auth_signed),
|
|
104
|
-
entity_uid=entity_uid,
|
|
105
|
-
public_key=entity_pik
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
return response
|
|
109
|
-
|
|
110
|
-
# =====================================================
|
|
111
|
-
# SERVER AUTHENTICATION
|
|
112
|
-
# =====================================================
|
|
113
|
-
|
|
114
|
-
async def register_server_challenge(
|
|
115
|
-
self,
|
|
116
|
-
challenge: dict
|
|
117
|
-
) -> dict:
|
|
118
|
-
"""
|
|
119
|
-
Register challenge in remote server.
|
|
120
|
-
|
|
121
|
-
Client -> Server
|
|
122
|
-
|
|
123
|
-
POST:
|
|
124
|
-
/api/v/1/auth/server/challenge/register
|
|
125
|
-
"""
|
|
126
|
-
|
|
127
|
-
body = await self._transport.post(
|
|
128
|
-
f"{_PREFIX}/server/challenge/register",
|
|
129
|
-
json={
|
|
130
|
-
"challenge": challenge
|
|
131
|
-
}
|
|
132
|
-
)
|
|
133
|
-
|
|
134
|
-
return body
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
async def request_server_response(
|
|
139
|
-
self,
|
|
140
|
-
challenge_id: str
|
|
141
|
-
) -> dict:
|
|
142
|
-
"""
|
|
143
|
-
Request signed challenge response.
|
|
144
|
-
|
|
145
|
-
Client -> Server
|
|
146
|
-
|
|
147
|
-
POST:
|
|
148
|
-
/api/v/1/auth/server/challenge/response
|
|
149
|
-
"""
|
|
150
|
-
|
|
151
|
-
body = await self._transport.post(
|
|
152
|
-
f"{_PREFIX}/server/challenge/response",
|
|
153
|
-
json={
|
|
154
|
-
"challenge_id": challenge_id
|
|
155
|
-
}
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
return body
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def verify_server_response(
|
|
163
|
-
self,
|
|
164
|
-
challenge: dict,
|
|
165
|
-
response: dict,
|
|
166
|
-
server_public_key: str
|
|
167
|
-
) -> bool:
|
|
168
|
-
"""
|
|
169
|
-
Verify server signature locally.
|
|
170
|
-
"""
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
challenge_object = (
|
|
174
|
-
ChallengeProtocol.challenge_from_dict(
|
|
175
|
-
challenge
|
|
176
|
-
)
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
if not ChallengeProtocol.validate_type(
|
|
181
|
-
challenge_object,
|
|
182
|
-
CHALLENGE_TYPE_SERVER_AUTHENTICATION
|
|
183
|
-
):
|
|
184
|
-
|
|
185
|
-
raise ValueError(
|
|
186
|
-
"Invalid server challenge type"
|
|
187
|
-
)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
response_object = (
|
|
191
|
-
ChallengeProtocol.response_from_dict(
|
|
192
|
-
response["response"]
|
|
193
|
-
)
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
return ChallengeProtocol.verify(
|
|
198
|
-
public_key=server_public_key,
|
|
199
|
-
challenge=challenge_object,
|
|
200
|
-
response=response_object
|
|
201
|
-
)
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
async def authenticate_server(
|
|
206
|
-
self,
|
|
207
|
-
client_uid: str,
|
|
208
|
-
server_uid: str,
|
|
209
|
-
server_public_key: str
|
|
210
|
-
) -> bool:
|
|
211
|
-
"""
|
|
212
|
-
Authenticate remote server.
|
|
213
|
-
"""
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
# -----------------------------------------
|
|
217
|
-
# 1. Create challenge
|
|
218
|
-
# -----------------------------------------
|
|
219
|
-
|
|
220
|
-
challenge = ChallengeProtocol.create(
|
|
221
|
-
challenge_type=(
|
|
222
|
-
CHALLENGE_TYPE_SERVER_AUTHENTICATION
|
|
223
|
-
),
|
|
224
|
-
|
|
225
|
-
issuer_uid=client_uid,
|
|
226
|
-
|
|
227
|
-
target_uid=server_uid
|
|
228
|
-
)
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
challenge_dict = (
|
|
232
|
-
ChallengeProtocol.challenge_to_dict(
|
|
233
|
-
challenge
|
|
234
|
-
)
|
|
235
|
-
)
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
# -----------------------------------------
|
|
239
|
-
# 2. Register challenge remotely
|
|
240
|
-
# -----------------------------------------
|
|
241
|
-
|
|
242
|
-
await self.register_server_challenge(
|
|
243
|
-
challenge_dict
|
|
244
|
-
)
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
# -----------------------------------------
|
|
248
|
-
# 3. Ask server signature
|
|
249
|
-
# -----------------------------------------
|
|
250
|
-
|
|
251
|
-
response = await self.request_server_response(
|
|
252
|
-
challenge_dict["challenge_id"]
|
|
253
|
-
)
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
# -----------------------------------------
|
|
257
|
-
# 4. Verify server proof
|
|
258
|
-
# -----------------------------------------
|
|
259
|
-
|
|
260
|
-
verified = (
|
|
261
|
-
self.verify_server_response(
|
|
262
|
-
challenge=challenge_dict,
|
|
263
|
-
|
|
264
|
-
response=response,
|
|
265
|
-
|
|
266
|
-
server_public_key=server_public_key
|
|
267
|
-
)
|
|
268
|
-
)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if not verified:
|
|
272
|
-
|
|
273
|
-
raise ValueError(
|
|
274
|
-
"Server authentication failed"
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
return True
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
# shared/api/manager/v1/client.py
|
|
2
|
-
"""
|
|
3
|
-
Cliente principal de la SDK de OSAM.
|
|
4
|
-
|
|
5
|
-
``OSAMClient`` es el único punto de entrada que el resto del sistema
|
|
6
|
-
(OpenShell Console, OpenShell Agent, futuras GUIs, herramientas
|
|
7
|
-
automatizadas) debe usar para hablar con la API HTTP de OSAM. Internamente
|
|
8
|
-
compone una única instancia de ``HttpTransport`` (la capa HTTP real) y la
|
|
9
|
-
inyecta en cada API de dominio especializada.
|
|
10
|
-
|
|
11
|
-
No hay estado global ni singletons: cada instancia de ``OSAMClient``
|
|
12
|
-
representa una conexión independiente a un host:puerto concreto, y puede
|
|
13
|
-
crearse tantas veces como se necesite (por ejemplo, una Console que habla
|
|
14
|
-
simultáneamente con el Manager y, en el futuro, con un Agent expuesto en
|
|
15
|
-
otro host).
|
|
16
|
-
|
|
17
|
-
Ejemplo de uso::
|
|
18
|
-
|
|
19
|
-
import asyncio
|
|
20
|
-
from shared.api.manager.v1 import OSAMClient
|
|
21
|
-
|
|
22
|
-
async def main():
|
|
23
|
-
async with OSAMClient(host="fortaprest.org", port=8000, protocol="https") as client:
|
|
24
|
-
identity = await client.identity.get_logical_identity()
|
|
25
|
-
print(identity.uid)
|
|
26
|
-
|
|
27
|
-
asyncio.run(main())
|
|
28
|
-
"""
|
|
29
|
-
|
|
30
|
-
from __future__ import annotations
|
|
31
|
-
|
|
32
|
-
import logging
|
|
33
|
-
from typing import Union
|
|
34
|
-
|
|
35
|
-
from .authentication import AuthenticationAPI
|
|
36
|
-
from .domains import DomainsAPI
|
|
37
|
-
from .entities import EntitiesAPI
|
|
38
|
-
from .identity import IdentityAPI
|
|
39
|
-
from .passports import PassportsAPI
|
|
40
|
-
from .sessions import SessionsAPI
|
|
41
|
-
from .transport import HttpTransport
|
|
42
|
-
from .tunnels import TunnelsAPI
|
|
43
|
-
|
|
44
|
-
logger = logging.getLogger("osam.client")
|
|
45
|
-
|
|
46
|
-
_VALID_PROTOCOLS = ("http", "https")
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
class OSAMClient:
|
|
50
|
-
"""
|
|
51
|
-
Cliente de alto nivel para la API HTTP de OSAM.
|
|
52
|
-
|
|
53
|
-
Args:
|
|
54
|
-
host: Host o IP del servidor OSAM (Manager, normalmente).
|
|
55
|
-
port: Puerto HTTP/HTTPS de la API.
|
|
56
|
-
protocol: ``"http"`` o ``"https"``. Use ``"https"`` en producción;
|
|
57
|
-
el endurecimiento TLS del proyecto vive en el lado del servidor
|
|
58
|
-
(ver variables ``OSAM_MANAGER_HOST`` / ``OSAM_CA_BUNDLE``), esta
|
|
59
|
-
SDK solo necesita saber qué esquema usar y, opcionalmente, qué
|
|
60
|
-
bundle de CA validar contra él.
|
|
61
|
-
timeout: Timeout en segundos aplicado a cada petición HTTP.
|
|
62
|
-
verify_ssl: ``True``/``False`` para activar/desactivar la
|
|
63
|
-
verificación TLS, o una ruta a un bundle de CA personalizado
|
|
64
|
-
(equivalente al parámetro ``verify`` de ``httpx``). Útil para
|
|
65
|
-
apuntar a ``OSAM_CA_BUNDLE`` durante el endurecimiento HTTPS.
|
|
66
|
-
|
|
67
|
-
Atributos expuestos (una instancia por dominio funcional):
|
|
68
|
-
identity, authentication, domains, tunnels, sessions, passports,
|
|
69
|
-
entities.
|
|
70
|
-
"""
|
|
71
|
-
|
|
72
|
-
def __init__(
|
|
73
|
-
self,
|
|
74
|
-
host: str,
|
|
75
|
-
port: int,
|
|
76
|
-
protocol: str = "http",
|
|
77
|
-
*,
|
|
78
|
-
timeout: float = 10.0,
|
|
79
|
-
verify_ssl: Union[bool, str] = True,
|
|
80
|
-
) -> None:
|
|
81
|
-
if protocol not in _VALID_PROTOCOLS:
|
|
82
|
-
raise ValueError(
|
|
83
|
-
f"protocol debe ser uno de {_VALID_PROTOCOLS!r}, recibido {protocol!r}"
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
base_url = f"{protocol}://{host}:{port}"
|
|
87
|
-
logger.debug("Inicializando OSAMClient hacia %s", base_url)
|
|
88
|
-
|
|
89
|
-
self._transport = HttpTransport(
|
|
90
|
-
base_url=base_url,
|
|
91
|
-
timeout=timeout,
|
|
92
|
-
verify_ssl=verify_ssl,
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
self.identity = IdentityAPI(self._transport)
|
|
96
|
-
self.authentication = AuthenticationAPI(self._transport)
|
|
97
|
-
self.domains = DomainsAPI(self._transport)
|
|
98
|
-
self.tunnels = TunnelsAPI(self._transport)
|
|
99
|
-
self.sessions = SessionsAPI(self._transport)
|
|
100
|
-
self.passports = PassportsAPI(self._transport)
|
|
101
|
-
self.entities = EntitiesAPI(self._transport)
|
|
102
|
-
|
|
103
|
-
async def close(self) -> None:
|
|
104
|
-
"""Cierra las conexiones HTTP subyacentes. Idempotente."""
|
|
105
|
-
await self._transport.aclose()
|
|
106
|
-
|
|
107
|
-
async def __aenter__(self) -> "OSAMClient":
|
|
108
|
-
return self
|
|
109
|
-
|
|
110
|
-
async def __aexit__(self, *exc_info: object) -> None:
|
|
111
|
-
await self.close()
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
# shared/api/manager/v1/domains.py
|
|
2
|
-
"""
|
|
3
|
-
Dominio: Domains.
|
|
4
|
-
|
|
5
|
-
Encapsula la consulta de los dominios a los que pertenece la entidad
|
|
6
|
-
autenticada. El servidor solo expone, por ahora, un endpoint de listado
|
|
7
|
-
(``query``); este módulo añade ``get_domain`` como utilidad de "detalle"
|
|
8
|
-
construida en el lado del cliente (filtrando la lista), ya que el servidor
|
|
9
|
-
no expone todavía un endpoint dedicado para un único dominio.
|
|
10
|
-
|
|
11
|
-
Endpoints cubiertos:
|
|
12
|
-
POST /api/v/1/domains/query
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
from __future__ import annotations
|
|
16
|
-
|
|
17
|
-
from typing import List, Optional
|
|
18
|
-
|
|
19
|
-
from .models import Domain
|
|
20
|
-
from .transport import HttpTransport
|
|
21
|
-
|
|
22
|
-
_PREFIX = "/api/v/1/domains"
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class DomainsAPI:
|
|
26
|
-
"""API especializada para consultar dominios de OSAM."""
|
|
27
|
-
|
|
28
|
-
def __init__(self, transport: HttpTransport) -> None:
|
|
29
|
-
self._transport = transport
|
|
30
|
-
|
|
31
|
-
async def query(self, auth_token: str) -> List[Domain]:
|
|
32
|
-
"""
|
|
33
|
-
POST /api/v/1/domains/query
|
|
34
|
-
|
|
35
|
-
Devuelve la lista de dominios a los que pertenece la entidad
|
|
36
|
-
identificada por ``auth_token``.
|
|
37
|
-
"""
|
|
38
|
-
body = await self._transport.post(
|
|
39
|
-
f"{_PREFIX}/query",
|
|
40
|
-
json={"auth_token": auth_token},
|
|
41
|
-
)
|
|
42
|
-
# El servidor devuelve la lista de dominios como cuerpo JSON raíz;
|
|
43
|
-
# HttpTransport la normaliza como {"items": [...]} para mantener
|
|
44
|
-
# una interfaz homogénea (ver HttpTransport._parse_body).
|
|
45
|
-
items = body.get("items", [])
|
|
46
|
-
if not isinstance(items, list):
|
|
47
|
-
items = []
|
|
48
|
-
|
|
49
|
-
return items
|
|
50
|
-
|
|
51
|
-
async def get_domain(
|
|
52
|
-
self, auth_token: str, domain_uid: str
|
|
53
|
-
) -> Optional[Domain]:
|
|
54
|
-
"""
|
|
55
|
-
Utilidad de conveniencia: obtiene un dominio concreto filtrando el
|
|
56
|
-
resultado de :meth:`query`. No existe (todavía) un endpoint de
|
|
57
|
-
servidor dedicado a la consulta de un único dominio.
|
|
58
|
-
"""
|
|
59
|
-
domains = await self.query(auth_token)
|
|
60
|
-
print(domains)
|
|
61
|
-
for domain in domains:
|
|
62
|
-
if domain.uid == domain_uid:
|
|
63
|
-
return domain
|
|
64
|
-
return None
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
# shared/api/manager/v1/entities.py
|
|
2
|
-
"""
|
|
3
|
-
Dominio: Entities.
|
|
4
|
-
|
|
5
|
-
Encapsula la consulta de entidades visibles para el solicitante. La API
|
|
6
|
-
actual del servidor solo expone consulta de entidades de tipo ``AGENT``
|
|
7
|
-
(``/entities/agent/query``); no existe todavía un endpoint genérico por
|
|
8
|
-
tipo arbitrario ni un filtro por dominio. Este módulo expone:
|
|
9
|
-
|
|
10
|
-
* ``query_agents``: llamada directa al único endpoint real disponible.
|
|
11
|
-
* ``query``: punto de extensión genérico preparado para cuando el servidor
|
|
12
|
-
exponga más tipos de entidad; hoy delega en ``query_agents`` para
|
|
13
|
-
``entity_type in (None, "AGENT")`` y lanza ``NotImplementedError`` para
|
|
14
|
-
cualquier otro valor, dejando explícito qué falta en el servidor.
|
|
15
|
-
* ``query_by_domain``: punto de extensión, no soportado aún por el servidor.
|
|
16
|
-
* ``get_agent``: utilidad de conveniencia que filtra del lado del cliente.
|
|
17
|
-
|
|
18
|
-
Endpoints cubiertos:
|
|
19
|
-
POST /api/v/1/entities/agent/query
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
from __future__ import annotations
|
|
23
|
-
|
|
24
|
-
from typing import List, Optional
|
|
25
|
-
|
|
26
|
-
from .models import Entity
|
|
27
|
-
from .transport import HttpTransport
|
|
28
|
-
|
|
29
|
-
_PREFIX = "/api/v/1/entities"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class EntitiesAPI:
|
|
33
|
-
"""API especializada para consultar entidades visibles en OSAM."""
|
|
34
|
-
|
|
35
|
-
def __init__(self, transport: HttpTransport) -> None:
|
|
36
|
-
self._transport = transport
|
|
37
|
-
|
|
38
|
-
async def query_agents(self, auth_token: str) -> list:
|
|
39
|
-
"""
|
|
40
|
-
POST /api/v/1/entities/agent/query
|
|
41
|
-
|
|
42
|
-
Devuelve las entidades de tipo AGENT visibles para la entidad
|
|
43
|
-
identificada por ``auth_token``, cada una con su ``status`` de
|
|
44
|
-
túnel actual.
|
|
45
|
-
"""
|
|
46
|
-
body = await self._transport.post(
|
|
47
|
-
f"{_PREFIX}/agent/query",
|
|
48
|
-
json={"auth_token": auth_token},
|
|
49
|
-
)
|
|
50
|
-
entities = body.get("entities", [])
|
|
51
|
-
if not isinstance(entities, list):
|
|
52
|
-
entities = []
|
|
53
|
-
return entities
|
|
54
|
-
|
|
55
|
-
async def query(
|
|
56
|
-
self, auth_token: str, entity_type: Optional[str] = None
|
|
57
|
-
) -> list:
|
|
58
|
-
"""
|
|
59
|
-
Punto de extensión genérico por tipo de entidad.
|
|
60
|
-
|
|
61
|
-
Actualmente el servidor solo soporta ``entity_type in (None, "AGENT")``;
|
|
62
|
-
cualquier otro valor lanza ``NotImplementedError`` de forma explícita
|
|
63
|
-
en vez de devolver un resultado vacío silencioso.
|
|
64
|
-
"""
|
|
65
|
-
if entity_type not in (None, "AGENT"):
|
|
66
|
-
raise NotImplementedError(
|
|
67
|
-
"El servidor de OSAM solo expone consulta de entidades "
|
|
68
|
-
"AGENT en esta versión de la API "
|
|
69
|
-
"(/api/v/1/entities/agent/query). "
|
|
70
|
-
f"entity_type={entity_type!r} no está soportado todavía."
|
|
71
|
-
)
|
|
72
|
-
return await self.query_agents(auth_token)
|
|
73
|
-
|
|
74
|
-
async def query_by_domain(self, auth_token: str, domain_uid: str) -> List[Entity]:
|
|
75
|
-
"""
|
|
76
|
-
Punto de extensión: el servidor no expone (todavía) filtrado de
|
|
77
|
-
entidades por dominio. Queda declarado aquí para que, cuando se
|
|
78
|
-
añada el endpoint correspondiente, solo haya que actualizar la
|
|
79
|
-
implementación de este método sin tocar el resto de la SDK.
|
|
80
|
-
"""
|
|
81
|
-
raise NotImplementedError(
|
|
82
|
-
"El servidor de OSAM no expone todavía un endpoint de consulta "
|
|
83
|
-
"de entidades filtrado por dominio."
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
async def get_agent(
|
|
87
|
-
self, auth_token: str, entity_uid: str
|
|
88
|
-
) -> Optional[Entity]:
|
|
89
|
-
"""
|
|
90
|
-
Utilidad de conveniencia: obtiene un AGENT concreto filtrando el
|
|
91
|
-
resultado de :meth:`query_agents` del lado del cliente.
|
|
92
|
-
"""
|
|
93
|
-
agents = await self.query_agents(auth_token)
|
|
94
|
-
for agent in agents:
|
|
95
|
-
if agent.entity_uid == entity_uid:
|
|
96
|
-
return agent
|
|
97
|
-
return None
|