openshell-shared 0.1.4__tar.gz → 0.1.5__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 (70) hide show
  1. {openshell_shared-0.1.4 → openshell_shared-0.1.5}/PKG-INFO +1 -1
  2. openshell_shared-0.1.5/openshell_shared/__init__.py +7 -0
  3. openshell_shared-0.1.5/openshell_shared/api/__init__.py +1 -0
  4. openshell_shared-0.1.5/openshell_shared/api/manager/__init__.py +1 -0
  5. openshell_shared-0.1.5/openshell_shared/api/manager/v1/__init__.py +78 -0
  6. openshell_shared-0.1.5/openshell_shared/api/manager/v1/authentication.py +278 -0
  7. openshell_shared-0.1.5/openshell_shared/api/manager/v1/client.py +111 -0
  8. openshell_shared-0.1.5/openshell_shared/api/manager/v1/domains.py +64 -0
  9. openshell_shared-0.1.5/openshell_shared/api/manager/v1/entities.py +97 -0
  10. openshell_shared-0.1.5/openshell_shared/api/manager/v1/exceptions.py +98 -0
  11. openshell_shared-0.1.5/openshell_shared/api/manager/v1/identity.py +44 -0
  12. openshell_shared-0.1.5/openshell_shared/api/manager/v1/models.py +342 -0
  13. openshell_shared-0.1.5/openshell_shared/api/manager/v1/passports.py +131 -0
  14. openshell_shared-0.1.5/openshell_shared/api/manager/v1/sessions.py +83 -0
  15. openshell_shared-0.1.5/openshell_shared/api/manager/v1/transport.py +253 -0
  16. openshell_shared-0.1.5/openshell_shared/api/manager/v1/tunnels.py +120 -0
  17. openshell_shared-0.1.5/openshell_shared/cryptography/__init__.py +0 -0
  18. openshell_shared-0.1.5/openshell_shared/cryptography/certificate.py +390 -0
  19. openshell_shared-0.1.5/openshell_shared/cryptography/encoding.py +0 -0
  20. openshell_shared-0.1.5/openshell_shared/cryptography/identity.py +124 -0
  21. openshell_shared-0.1.5/openshell_shared/cryptography/keys.py +463 -0
  22. openshell_shared-0.1.5/openshell_shared/cryptography/signatures.py +63 -0
  23. openshell_shared-0.1.5/openshell_shared/cryptography/utils.py +0 -0
  24. openshell_shared-0.1.5/openshell_shared/domain/__init__.py +0 -0
  25. openshell_shared-0.1.5/openshell_shared/domain/domain.py +80 -0
  26. openshell_shared-0.1.5/openshell_shared/domain/membership.py +21 -0
  27. openshell_shared-0.1.5/openshell_shared/domain/permissions.py +14 -0
  28. openshell_shared-0.1.5/openshell_shared/domain/policies.py +2 -0
  29. openshell_shared-0.1.5/openshell_shared/identity/__init__.py +0 -0
  30. openshell_shared-0.1.5/openshell_shared/identity/identification.py +64 -0
  31. openshell_shared-0.1.5/openshell_shared/identity/store.py +150 -0
  32. openshell_shared-0.1.5/openshell_shared/modules/__init__.py +0 -0
  33. openshell_shared-0.1.5/openshell_shared/modules/shell/__init__.py +0 -0
  34. openshell_shared-0.1.5/openshell_shared/modules/shell/client.py +361 -0
  35. openshell_shared-0.1.5/openshell_shared/modules/shell/models.py +61 -0
  36. openshell_shared-0.1.5/openshell_shared/modules/shell/protocol.py +249 -0
  37. openshell_shared-0.1.5/openshell_shared/modules/shell/server.py +511 -0
  38. openshell_shared-0.1.5/openshell_shared/modules/shell/session.py +339 -0
  39. openshell_shared-0.1.5/openshell_shared/modules/utils.py +212 -0
  40. openshell_shared-0.1.5/openshell_shared/protocols/__init__.py +0 -0
  41. openshell_shared-0.1.5/openshell_shared/protocols/negotiation/challenge.py +127 -0
  42. openshell_shared-0.1.5/openshell_shared/protocols/negotiation/models.py +28 -0
  43. openshell_shared-0.1.5/openshell_shared/standards/__init__.py +0 -0
  44. openshell_shared-0.1.5/openshell_shared/standards/certificates/__init__.py +0 -0
  45. openshell_shared-0.1.5/openshell_shared/standards/certificates/status.py +12 -0
  46. openshell_shared-0.1.5/openshell_shared/standards/certificates/types.py +11 -0
  47. openshell_shared-0.1.5/openshell_shared/standards/entities/__init__.py +0 -0
  48. openshell_shared-0.1.5/openshell_shared/standards/entities/types.py +14 -0
  49. openshell_shared-0.1.5/openshell_shared/standards/events/__init__.py +0 -0
  50. openshell_shared-0.1.5/openshell_shared/standards/events/schemas/__init__.py +0 -0
  51. openshell_shared-0.1.5/openshell_shared/standards/events/schemas/entity_registered.py +13 -0
  52. openshell_shared-0.1.5/openshell_shared/standards/events/types.py +18 -0
  53. openshell_shared-0.1.5/openshell_shared/standards/passports/__init__.py +0 -0
  54. openshell_shared-0.1.5/openshell_shared/standards/passports/types.py +5 -0
  55. openshell_shared-0.1.5/openshell_shared/standards/permissions/__init__.py +0 -0
  56. openshell_shared-0.1.5/openshell_shared/standards/permissions/types.py +14 -0
  57. openshell_shared-0.1.5/openshell_shared/standards/roles/__init__.py +0 -0
  58. openshell_shared-0.1.5/openshell_shared/standards/roles/types.py +8 -0
  59. openshell_shared-0.1.5/openshell_shared/standards/transports/__init__.py +0 -0
  60. openshell_shared-0.1.5/openshell_shared/standards/transports/types.py +24 -0
  61. {openshell_shared-0.1.4 → openshell_shared-0.1.5}/openshell_shared.egg-info/PKG-INFO +1 -1
  62. openshell_shared-0.1.5/openshell_shared.egg-info/SOURCES.txt +66 -0
  63. openshell_shared-0.1.5/openshell_shared.egg-info/top_level.txt +1 -0
  64. {openshell_shared-0.1.4 → openshell_shared-0.1.5}/pyproject.toml +2 -2
  65. openshell_shared-0.1.4/openshell_shared.egg-info/SOURCES.txt +0 -7
  66. openshell_shared-0.1.4/openshell_shared.egg-info/top_level.txt +0 -1
  67. {openshell_shared-0.1.4 → openshell_shared-0.1.5}/README.md +0 -0
  68. {openshell_shared-0.1.4 → openshell_shared-0.1.5}/openshell_shared.egg-info/dependency_links.txt +0 -0
  69. {openshell_shared-0.1.4 → openshell_shared-0.1.5}/openshell_shared.egg-info/requires.txt +0 -0
  70. {openshell_shared-0.1.4 → openshell_shared-0.1.5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openshell-shared
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: Shared library for OpenShell (OSAM/OSA/OSAC)
5
5
  Author: Specter
6
6
  License: Proprietary
@@ -0,0 +1,7 @@
1
+ from . import api
2
+ from . import cryptography
3
+ from . import domain
4
+ from . import identity
5
+ from . import modules
6
+ from . import protocols
7
+ from . import standards
@@ -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
@@ -0,0 +1,97 @@
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