openshell-shared 0.1.2__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 (67) hide show
  1. openshell_shared-0.1.2/PKG-INFO +59 -0
  2. openshell_shared-0.1.2/README.md +46 -0
  3. openshell_shared-0.1.2/api/__init__.py +1 -0
  4. openshell_shared-0.1.2/api/manager/__init__.py +1 -0
  5. openshell_shared-0.1.2/api/manager/v1/__init__.py +78 -0
  6. openshell_shared-0.1.2/api/manager/v1/authentication.py +278 -0
  7. openshell_shared-0.1.2/api/manager/v1/client.py +111 -0
  8. openshell_shared-0.1.2/api/manager/v1/domains.py +64 -0
  9. openshell_shared-0.1.2/api/manager/v1/entities.py +97 -0
  10. openshell_shared-0.1.2/api/manager/v1/exceptions.py +98 -0
  11. openshell_shared-0.1.2/api/manager/v1/identity.py +44 -0
  12. openshell_shared-0.1.2/api/manager/v1/models.py +342 -0
  13. openshell_shared-0.1.2/api/manager/v1/passports.py +131 -0
  14. openshell_shared-0.1.2/api/manager/v1/sessions.py +83 -0
  15. openshell_shared-0.1.2/api/manager/v1/transport.py +253 -0
  16. openshell_shared-0.1.2/api/manager/v1/tunnels.py +120 -0
  17. openshell_shared-0.1.2/cryptography/__init__.py +0 -0
  18. openshell_shared-0.1.2/cryptography/certificate.py +390 -0
  19. openshell_shared-0.1.2/cryptography/encoding.py +0 -0
  20. openshell_shared-0.1.2/cryptography/identity.py +124 -0
  21. openshell_shared-0.1.2/cryptography/keys.py +463 -0
  22. openshell_shared-0.1.2/cryptography/signatures.py +63 -0
  23. openshell_shared-0.1.2/cryptography/utils.py +0 -0
  24. openshell_shared-0.1.2/domain/__init__.py +0 -0
  25. openshell_shared-0.1.2/domain/domain.py +80 -0
  26. openshell_shared-0.1.2/domain/membership.py +21 -0
  27. openshell_shared-0.1.2/domain/permissions.py +14 -0
  28. openshell_shared-0.1.2/domain/policies.py +2 -0
  29. openshell_shared-0.1.2/identity/__init__.py +0 -0
  30. openshell_shared-0.1.2/identity/identification.py +64 -0
  31. openshell_shared-0.1.2/identity/store.py +150 -0
  32. openshell_shared-0.1.2/modules/__init__.py +0 -0
  33. openshell_shared-0.1.2/modules/shell/__init__.py +0 -0
  34. openshell_shared-0.1.2/modules/shell/client.py +361 -0
  35. openshell_shared-0.1.2/modules/shell/models.py +61 -0
  36. openshell_shared-0.1.2/modules/shell/protocol.py +249 -0
  37. openshell_shared-0.1.2/modules/shell/server.py +511 -0
  38. openshell_shared-0.1.2/modules/shell/session.py +339 -0
  39. openshell_shared-0.1.2/modules/utils.py +212 -0
  40. openshell_shared-0.1.2/openshell_shared.egg-info/PKG-INFO +59 -0
  41. openshell_shared-0.1.2/openshell_shared.egg-info/SOURCES.txt +65 -0
  42. openshell_shared-0.1.2/openshell_shared.egg-info/dependency_links.txt +1 -0
  43. openshell_shared-0.1.2/openshell_shared.egg-info/requires.txt +4 -0
  44. openshell_shared-0.1.2/openshell_shared.egg-info/top_level.txt +9 -0
  45. openshell_shared-0.1.2/protocols/__init__.py +0 -0
  46. openshell_shared-0.1.2/protocols/negotiation/challenge.py +127 -0
  47. openshell_shared-0.1.2/protocols/negotiation/models.py +28 -0
  48. openshell_shared-0.1.2/pyproject.toml +31 -0
  49. openshell_shared-0.1.2/setup.cfg +4 -0
  50. openshell_shared-0.1.2/standards/__init__.py +0 -0
  51. openshell_shared-0.1.2/standards/certificates/__init__.py +0 -0
  52. openshell_shared-0.1.2/standards/certificates/status.py +12 -0
  53. openshell_shared-0.1.2/standards/certificates/types.py +11 -0
  54. openshell_shared-0.1.2/standards/entities/__init__.py +0 -0
  55. openshell_shared-0.1.2/standards/entities/types.py +14 -0
  56. openshell_shared-0.1.2/standards/events/__init__.py +0 -0
  57. openshell_shared-0.1.2/standards/events/schemas/__init__.py +0 -0
  58. openshell_shared-0.1.2/standards/events/schemas/entity_registered.py +13 -0
  59. openshell_shared-0.1.2/standards/events/types.py +18 -0
  60. openshell_shared-0.1.2/standards/passports/__init__.py +0 -0
  61. openshell_shared-0.1.2/standards/passports/types.py +5 -0
  62. openshell_shared-0.1.2/standards/permissions/__init__.py +0 -0
  63. openshell_shared-0.1.2/standards/permissions/types.py +14 -0
  64. openshell_shared-0.1.2/standards/roles/__init__.py +0 -0
  65. openshell_shared-0.1.2/standards/roles/types.py +8 -0
  66. openshell_shared-0.1.2/standards/transports/__init__.py +0 -0
  67. openshell_shared-0.1.2/standards/transports/types.py +24 -0
@@ -0,0 +1,59 @@
1
+ Metadata-Version: 2.4
2
+ Name: openshell-shared
3
+ Version: 0.1.2
4
+ Summary: Shared library for OpenShell (OSAM/OSA/OSAC)
5
+ Author: Daniel Pérez
6
+ License: Proprietary
7
+ Requires-Python: >=3.12
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: httpx
10
+ Requires-Dist: cryptography
11
+ Requires-Dist: websockets
12
+ Requires-Dist: uuid6
13
+
14
+ # OpenShell Shared
15
+
16
+ Librería compartida usada por los componentes de OpenShell (OSAM, OSA, OSAC).
17
+
18
+ Contiene:
19
+
20
+ - **identity/** — Identidad lógica de las entidades (`EntityIdentity`, `Identification`) y su
21
+ persistencia (`store.py`).
22
+ - **cryptography/** — Primitivas Ed25519 (`keys.py`, `signatures.py`), identidad criptográfica
23
+ (`identity.py`) y certificados (`certificate.py`). `encoding.py` y `utils.py` son módulos
24
+ reservados para trabajo futuro (actualmente sin implementación).
25
+ - **protocols/negotiation/** — Protocolo de challenge-response (`challenge.py`) y sus modelos.
26
+ - **domain/** — Modelo de dominio (`Domain`, `Membership`, `Permission`) y políticas de
27
+ autorización. `policies.py` es un stub pendiente de implementación real.
28
+ - **standards/** — Enumeraciones y contratos compartidos entre componentes: tipos de entidad,
29
+ certificados, eventos, roles, permisos, passports y transportes.
30
+ - **modules/shell/** — Implementación del subsistema de shell remoto (cliente, servidor, sesión
31
+ y protocolo de framing) usado por OSA y OSAC.
32
+ - **api/manager/v1/** — SDK oficial async para consumir la API HTTP de OSAM (`OSAMClient`).
33
+ Todo el sistema (OSAC, OSA, herramientas internas) debe consumir OSAM exclusivamente a través
34
+ de este paquete; ningún otro componente debe importar `httpx`/`requests` directamente para
35
+ hablar con OSAM.
36
+
37
+ ## Notas de la fusión (v2.1.0)
38
+
39
+ Esta versión unifica dos ramas que habían divergido:
40
+
41
+ - Se conserva la estructura de empaquetado (`pyproject.toml`, layout `src`) y los módulos
42
+ `domain/` y `standards/`, introducidos en la rama de refactor v2.
43
+ - Se restaura `modules/shell/` y se sustituye el cliente HTTP monolítico
44
+ (`api/manager/1/osam_client.py`) por el SDK modular (`api/manager/v1/`), que es la versión
45
+ vigente y más completa (excepciones tipadas, dataclasses, suite de tests con mock transport).
46
+
47
+ ### Deuda técnica pendiente identificada durante la fusión
48
+
49
+ 1. `domain/permissions.py` y `standards/permissions/types.py` definen dos enums `Permission`
50
+ distintos y no compatibles entre sí (`AGENT_READ/AGENT_EXECUTE/DOMAIN_ADMIN/PROXY_USE` vs.
51
+ `DOMAIN_READ/DOMAIN_WRITE/ENTITY_REGISTER/ENTITY_REVOKE/PROXY_USE`). No se han unificado
52
+ automáticamente porque implica una decisión de diseño (cuál es la fuente de verdad de
53
+ permisos). Requiere consolidación manual.
54
+ 2. `cryptography/encoding.py`, `cryptography/utils.py` y `domain/policies.py` son placeholders
55
+ vacíos o triviales (`Policy.evaluate` siempre retorna `True`). No implementan lógica real.
56
+ 3. El SDK (`api/manager/v1/`) no reexpone los helpers de alto nivel `full_connect` /
57
+ `open_and_link` que existían en el cliente monolítico anterior (composición de
58
+ autenticación + túnel + sesión en una sola llamada). Si se siguen usando, conviene
59
+ reimplementarlos como métodos de conveniencia sobre `OSAMClient`.
@@ -0,0 +1,46 @@
1
+ # OpenShell Shared
2
+
3
+ Librería compartida usada por los componentes de OpenShell (OSAM, OSA, OSAC).
4
+
5
+ Contiene:
6
+
7
+ - **identity/** — Identidad lógica de las entidades (`EntityIdentity`, `Identification`) y su
8
+ persistencia (`store.py`).
9
+ - **cryptography/** — Primitivas Ed25519 (`keys.py`, `signatures.py`), identidad criptográfica
10
+ (`identity.py`) y certificados (`certificate.py`). `encoding.py` y `utils.py` son módulos
11
+ reservados para trabajo futuro (actualmente sin implementación).
12
+ - **protocols/negotiation/** — Protocolo de challenge-response (`challenge.py`) y sus modelos.
13
+ - **domain/** — Modelo de dominio (`Domain`, `Membership`, `Permission`) y políticas de
14
+ autorización. `policies.py` es un stub pendiente de implementación real.
15
+ - **standards/** — Enumeraciones y contratos compartidos entre componentes: tipos de entidad,
16
+ certificados, eventos, roles, permisos, passports y transportes.
17
+ - **modules/shell/** — Implementación del subsistema de shell remoto (cliente, servidor, sesión
18
+ y protocolo de framing) usado por OSA y OSAC.
19
+ - **api/manager/v1/** — SDK oficial async para consumir la API HTTP de OSAM (`OSAMClient`).
20
+ Todo el sistema (OSAC, OSA, herramientas internas) debe consumir OSAM exclusivamente a través
21
+ de este paquete; ningún otro componente debe importar `httpx`/`requests` directamente para
22
+ hablar con OSAM.
23
+
24
+ ## Notas de la fusión (v2.1.0)
25
+
26
+ Esta versión unifica dos ramas que habían divergido:
27
+
28
+ - Se conserva la estructura de empaquetado (`pyproject.toml`, layout `src`) y los módulos
29
+ `domain/` y `standards/`, introducidos en la rama de refactor v2.
30
+ - Se restaura `modules/shell/` y se sustituye el cliente HTTP monolítico
31
+ (`api/manager/1/osam_client.py`) por el SDK modular (`api/manager/v1/`), que es la versión
32
+ vigente y más completa (excepciones tipadas, dataclasses, suite de tests con mock transport).
33
+
34
+ ### Deuda técnica pendiente identificada durante la fusión
35
+
36
+ 1. `domain/permissions.py` y `standards/permissions/types.py` definen dos enums `Permission`
37
+ distintos y no compatibles entre sí (`AGENT_READ/AGENT_EXECUTE/DOMAIN_ADMIN/PROXY_USE` vs.
38
+ `DOMAIN_READ/DOMAIN_WRITE/ENTITY_REGISTER/ENTITY_REVOKE/PROXY_USE`). No se han unificado
39
+ automáticamente porque implica una decisión de diseño (cuál es la fuente de verdad de
40
+ permisos). Requiere consolidación manual.
41
+ 2. `cryptography/encoding.py`, `cryptography/utils.py` y `domain/policies.py` son placeholders
42
+ vacíos o triviales (`Policy.evaluate` siempre retorna `True`). No implementan lógica real.
43
+ 3. El SDK (`api/manager/v1/`) no reexpone los helpers de alto nivel `full_connect` /
44
+ `open_and_link` que existían en el cliente monolítico anterior (composición de
45
+ autenticación + túnel + sesión en una sola llamada). Si se siguen usando, conviene
46
+ reimplementarlos como métodos de conveniencia sobre `OSAMClient`.
@@ -0,0 +1 @@
1
+ from . import manager
@@ -0,0 +1 @@
1
+ from . import v1
@@ -0,0 +1,78 @@
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"
@@ -0,0 +1,278 @@
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
@@ -0,0 +1,111 @@
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()
@@ -0,0 +1,64 @@
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