openshell-shared 0.1.2__py3-none-any.whl
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.
- api/__init__.py +1 -0
- api/manager/__init__.py +1 -0
- api/manager/v1/__init__.py +78 -0
- api/manager/v1/authentication.py +278 -0
- api/manager/v1/client.py +111 -0
- api/manager/v1/domains.py +64 -0
- api/manager/v1/entities.py +97 -0
- api/manager/v1/exceptions.py +98 -0
- api/manager/v1/identity.py +44 -0
- api/manager/v1/models.py +342 -0
- api/manager/v1/passports.py +131 -0
- api/manager/v1/sessions.py +83 -0
- api/manager/v1/transport.py +253 -0
- api/manager/v1/tunnels.py +120 -0
- cryptography/__init__.py +0 -0
- cryptography/certificate.py +390 -0
- cryptography/encoding.py +0 -0
- cryptography/identity.py +124 -0
- cryptography/keys.py +463 -0
- cryptography/signatures.py +63 -0
- cryptography/utils.py +0 -0
- domain/__init__.py +0 -0
- domain/domain.py +80 -0
- domain/membership.py +21 -0
- domain/permissions.py +14 -0
- domain/policies.py +2 -0
- identity/__init__.py +0 -0
- identity/identification.py +64 -0
- identity/store.py +150 -0
- modules/__init__.py +0 -0
- modules/shell/__init__.py +0 -0
- modules/shell/client.py +361 -0
- modules/shell/models.py +61 -0
- modules/shell/protocol.py +249 -0
- modules/shell/server.py +511 -0
- modules/shell/session.py +339 -0
- modules/utils.py +212 -0
- openshell_shared-0.1.2.dist-info/METADATA +59 -0
- openshell_shared-0.1.2.dist-info/RECORD +62 -0
- openshell_shared-0.1.2.dist-info/WHEEL +5 -0
- openshell_shared-0.1.2.dist-info/top_level.txt +7 -0
- protocols/__init__.py +0 -0
- protocols/negotiation/challenge.py +127 -0
- protocols/negotiation/models.py +28 -0
- standards/__init__.py +0 -0
- standards/certificates/__init__.py +0 -0
- standards/certificates/status.py +12 -0
- standards/certificates/types.py +11 -0
- standards/entities/__init__.py +0 -0
- standards/entities/types.py +14 -0
- standards/events/__init__.py +0 -0
- standards/events/schemas/__init__.py +0 -0
- standards/events/schemas/entity_registered.py +13 -0
- standards/events/types.py +18 -0
- standards/passports/__init__.py +0 -0
- standards/passports/types.py +5 -0
- standards/permissions/__init__.py +0 -0
- standards/permissions/types.py +14 -0
- standards/roles/__init__.py +0 -0
- standards/roles/types.py +8 -0
- standards/transports/__init__.py +0 -0
- standards/transports/types.py +24 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# shared/api/manager/v1/exceptions.py
|
|
2
|
+
"""
|
|
3
|
+
Jerarquía de excepciones de la SDK de OSAM.
|
|
4
|
+
|
|
5
|
+
Diseño:
|
|
6
|
+
|
|
7
|
+
OSAMError -> raíz de toda excepción producida por la SDK
|
|
8
|
+
├── NetworkError -> fallos de transporte (timeout, DNS, conexión rechazada...)
|
|
9
|
+
├── InvalidResponseError -> el servidor respondió pero el cuerpo no es JSON
|
|
10
|
+
│ válido o no contiene los campos esperados
|
|
11
|
+
└── APIError -> el servidor respondió con un código de error HTTP
|
|
12
|
+
├── ValidationError (400 / 422 - payload inválido o incompleto)
|
|
13
|
+
├── AuthenticationError (401 - token/credencial inválida o ausente)
|
|
14
|
+
├── AuthorizationError (403 - la entidad no tiene permiso sobre el recurso)
|
|
15
|
+
├── EntityNotFoundError (404 - el recurso solicitado no existe)
|
|
16
|
+
└── ServerError (5xx - error interno del servidor OSAM)
|
|
17
|
+
|
|
18
|
+
Ningún método de la SDK debe propagar excepciones de ``httpx`` ni errores
|
|
19
|
+
ambiguos (``KeyError``, ``ValueError`` genéricos, etc.) hacia el código que
|
|
20
|
+
consume la librería. Toda esa traducción ocurre en ``transport.py``.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from typing import Any, Optional
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class OSAMError(Exception):
|
|
29
|
+
"""Excepción base de la SDK de OSAM. Todo error propio hereda de esta clase."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, message: str, *, detail: Optional[Any] = None) -> None:
|
|
32
|
+
super().__init__(message)
|
|
33
|
+
self.message = message
|
|
34
|
+
self.detail = detail
|
|
35
|
+
|
|
36
|
+
def __str__(self) -> str: # pragma: no cover - cosmético
|
|
37
|
+
if self.detail is not None and str(self.detail) != self.message:
|
|
38
|
+
return f"{self.message} (detail={self.detail!r})"
|
|
39
|
+
return self.message
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class NetworkError(OSAMError):
|
|
43
|
+
"""
|
|
44
|
+
La petición no pudo completarse a nivel de transporte: timeout, host
|
|
45
|
+
inalcanzable, conexión rechazada, error de DNS, error de TLS, etc.
|
|
46
|
+
|
|
47
|
+
No hubo respuesta HTTP que interpretar; el problema es la red o el
|
|
48
|
+
servidor remoto está caído/inaccesible.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class InvalidResponseError(OSAMError):
|
|
53
|
+
"""
|
|
54
|
+
El servidor respondió, pero el contenido no se pudo interpretar como se
|
|
55
|
+
esperaba: JSON malformado, o ausencia de campos que la SDK considera
|
|
56
|
+
obligatorios para construir el modelo de respuesta correspondiente.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class APIError(OSAMError):
|
|
61
|
+
"""
|
|
62
|
+
Clase base para errores reportados explícitamente por la API mediante un
|
|
63
|
+
código de estado HTTP de error. Contiene siempre el código de estado y,
|
|
64
|
+
si estuvo disponible, el cuerpo de error devuelto por el servidor (que en
|
|
65
|
+
OSAM sigue el formato estándar de FastAPI: ``{"detail": ...}``).
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
message: str,
|
|
71
|
+
*,
|
|
72
|
+
status_code: int,
|
|
73
|
+
detail: Optional[Any] = None,
|
|
74
|
+
response_body: Optional[Any] = None,
|
|
75
|
+
) -> None:
|
|
76
|
+
super().__init__(message, detail=detail)
|
|
77
|
+
self.status_code = status_code
|
|
78
|
+
self.response_body = response_body
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class ValidationError(APIError):
|
|
82
|
+
"""400 / 422 — la petición enviada por el cliente es inválida o incompleta."""
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class AuthenticationError(APIError):
|
|
86
|
+
"""401 — token de autenticación ausente, inválido o expirado."""
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class AuthorizationError(APIError):
|
|
90
|
+
"""403 — la entidad autenticada no tiene permisos sobre el recurso solicitado."""
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class EntityNotFoundError(APIError):
|
|
94
|
+
"""404 — el recurso solicitado (entidad, dominio, sesión...) no existe."""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class ServerError(APIError):
|
|
98
|
+
"""5xx — error interno no controlado dentro del servidor OSAM."""
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# shared/api/manager/v1/identity.py
|
|
2
|
+
"""
|
|
3
|
+
Dominio: Identity.
|
|
4
|
+
|
|
5
|
+
Expone la identidad lógica, criptográfica y de tipo de la entidad que
|
|
6
|
+
responde en el otro extremo de la conexión HTTP (típicamente el Manager,
|
|
7
|
+
pero el mismo contrato aplica a cualquier entidad OSAM que exponga esta
|
|
8
|
+
misma API). Ninguno de estos endpoints requiere autenticación: son el
|
|
9
|
+
primer paso antes de iniciar el protocolo de challenge-response.
|
|
10
|
+
|
|
11
|
+
Endpoints cubiertos (todos bajo ``/api/v/1/identity``):
|
|
12
|
+
GET /logical
|
|
13
|
+
GET /cryptographic
|
|
14
|
+
GET /type
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from .models import CryptographicIdentity, EntityTypeInfo, LogicalIdentity
|
|
20
|
+
from .transport import HttpTransport
|
|
21
|
+
|
|
22
|
+
_PREFIX = "/api/v/1/identity"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class IdentityAPI:
|
|
26
|
+
"""API especializada para consultar la identidad de una entidad OSAM."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, transport: HttpTransport) -> None:
|
|
29
|
+
self._transport = transport
|
|
30
|
+
|
|
31
|
+
async def get_logical_identity(self) -> LogicalIdentity:
|
|
32
|
+
"""GET /api/v/1/identity/logical -> identificador lógico (uid)."""
|
|
33
|
+
body = await self._transport.get(f"{_PREFIX}/logical")
|
|
34
|
+
return LogicalIdentity.from_dict(body)
|
|
35
|
+
|
|
36
|
+
async def get_cryptographic_identity(self) -> CryptographicIdentity:
|
|
37
|
+
"""GET /api/v/1/identity/cryptographic -> identidad criptográfica."""
|
|
38
|
+
body = await self._transport.get(f"{_PREFIX}/cryptographic")
|
|
39
|
+
return CryptographicIdentity.from_dict(body)
|
|
40
|
+
|
|
41
|
+
async def get_entity_type(self) -> EntityTypeInfo:
|
|
42
|
+
"""GET /api/v/1/identity/type -> tipo de entidad (AGENT/CONSOLE/...)."""
|
|
43
|
+
body = await self._transport.get(f"{_PREFIX}/type")
|
|
44
|
+
return EntityTypeInfo.from_dict(body)
|
api/manager/v1/models.py
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# shared/api/manager/v1/models.py
|
|
2
|
+
"""
|
|
3
|
+
Modelos tipados (dataclasses) en los que la SDK convierte las respuestas
|
|
4
|
+
JSON crudas de la API HTTP de OSAM.
|
|
5
|
+
|
|
6
|
+
Nota de transparencia sobre el origen de estos modelos
|
|
7
|
+
-------------------------------------------------------
|
|
8
|
+
Esta SDK se construyó a partir del código fuente de la capa
|
|
9
|
+
``api/http/routes/*.py`` del Manager (no se tuvo acceso al paquete
|
|
10
|
+
``core/`` donde viven las implementaciones reales de ``core.auth``,
|
|
11
|
+
``core.tunnel``, ``core.session``, ``core.passports``, etc.). Por lo tanto:
|
|
12
|
+
|
|
13
|
+
* Los campos marcados sin comentario están **confirmados**: se ve
|
|
14
|
+
explícitamente en las rutas que el servidor los lee o los escribe
|
|
15
|
+
(ej. ``entity["entity_uid"]``, ``payload.get("challenge_id")``).
|
|
16
|
+
* Los campos marcados con ``# inferido`` son una suposición razonable
|
|
17
|
+
basada en la convención de nombres del propio proyecto (ver
|
|
18
|
+
jerarquía auth_token / session_token / tunnel_token / connection_uid),
|
|
19
|
+
pero no se observaron directamente en el código de rutas.
|
|
20
|
+
|
|
21
|
+
En ambos casos, cualquier clave adicional que el servidor incluya en la
|
|
22
|
+
respuesta y que no esté declarada explícitamente en el dataclass **no se
|
|
23
|
+
pierde**: queda disponible en el atributo ``.extra`` (y, en conjunto con los
|
|
24
|
+
campos conocidos, a través de ``.raw``). Así, si el equipo de ``core``
|
|
25
|
+
cambia o añade un campo, el código que use atributos explícitos sigue
|
|
26
|
+
funcionando, y el dato sigue siendo accesible sin tener que actualizar la
|
|
27
|
+
SDK de inmediato.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import dataclasses
|
|
33
|
+
from dataclasses import dataclass, field
|
|
34
|
+
from typing import Any, ClassVar, Dict, Mapping, Optional, Type, TypeVar
|
|
35
|
+
|
|
36
|
+
T = TypeVar("T", bound="_BaseModel")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class _BaseModel:
|
|
41
|
+
"""
|
|
42
|
+
Clase base para todos los modelos de respuesta de la SDK.
|
|
43
|
+
|
|
44
|
+
Provee:
|
|
45
|
+
* ``from_dict``: construye una instancia a partir de un dict JSON,
|
|
46
|
+
asignando las claves conocidas a sus campos declarados y guardando
|
|
47
|
+
el resto en ``extra``.
|
|
48
|
+
* ``raw``: reconstruye el dict completo (campos conocidos + extra),
|
|
49
|
+
útil para depuración/logging sin exponer dicts crudos como tipo de
|
|
50
|
+
retorno habitual de la SDK.
|
|
51
|
+
* ``get``: acceso conveniente a un campo conocido o a ``extra`` sin
|
|
52
|
+
tener que recordar en cuál de los dos vive.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
extra: Dict[str, Any] = field(default_factory=dict, repr=False)
|
|
56
|
+
|
|
57
|
+
# Nombre de las claves "id" alternativas que algunas respuestas usan
|
|
58
|
+
# indistintamente (uid / <recurso>_uid). Se usa en from_dict para no
|
|
59
|
+
# duplicar lógica de aliasing en cada subclase.
|
|
60
|
+
_id_aliases: ClassVar[Dict[str, str]] = {}
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def from_dict(cls: Type[T], data: Mapping[str, Any]) -> T:
|
|
64
|
+
if not isinstance(data, Mapping):
|
|
65
|
+
data = {}
|
|
66
|
+
|
|
67
|
+
known_fields = {
|
|
68
|
+
f.name for f in dataclasses.fields(cls) if f.name != "extra"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
working = dict(data)
|
|
72
|
+
|
|
73
|
+
# Resuelve alias de identificadores (ej. "uid" -> "passport_uid")
|
|
74
|
+
# sin pisar un valor que ya venga con el nombre canónico.
|
|
75
|
+
for canonical, alias in cls._id_aliases.items():
|
|
76
|
+
if canonical in known_fields and canonical not in working:
|
|
77
|
+
if alias in working:
|
|
78
|
+
working[canonical] = working[alias]
|
|
79
|
+
|
|
80
|
+
kwargs = {k: v for k, v in working.items() if k in known_fields}
|
|
81
|
+
extra = {k: v for k, v in working.items() if k not in known_fields}
|
|
82
|
+
|
|
83
|
+
return cls(**kwargs, extra=extra)
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def raw(self) -> Dict[str, Any]:
|
|
87
|
+
"""Devuelve el payload completo (campos tipados + extra) como dict."""
|
|
88
|
+
result = {
|
|
89
|
+
f.name: getattr(self, f.name)
|
|
90
|
+
for f in dataclasses.fields(self)
|
|
91
|
+
if f.name != "extra"
|
|
92
|
+
}
|
|
93
|
+
result.update(self.extra)
|
|
94
|
+
return result
|
|
95
|
+
|
|
96
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
97
|
+
"""Acceso conveniente a un campo conocido o a un campo en `extra`."""
|
|
98
|
+
if hasattr(self, key) and key != "extra":
|
|
99
|
+
return getattr(self, key)
|
|
100
|
+
return self.extra.get(key, default)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# =============================================================================
|
|
104
|
+
# IDENTITY
|
|
105
|
+
# =============================================================================
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass
|
|
109
|
+
class LogicalIdentity(_BaseModel):
|
|
110
|
+
"""Respuesta de GET /api/v/1/identity/logical."""
|
|
111
|
+
|
|
112
|
+
uid: Optional[str] = None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class CryptographicIdentity(_BaseModel):
|
|
117
|
+
"""
|
|
118
|
+
Respuesta de GET /api/v/1/identity/cryptographic.
|
|
119
|
+
|
|
120
|
+
El endpoint devuelve ``core.get_public_identity()`` sin envoltorio, cuyo
|
|
121
|
+
esquema exacto vive en ``core`` (no incluido en este paquete). Se modelan
|
|
122
|
+
los campos que, de acuerdo a la convención del proyecto (identidad
|
|
123
|
+
criptográfica Ed25519 por entidad), son casi seguros.
|
|
124
|
+
"""
|
|
125
|
+
|
|
126
|
+
public_key: Optional[str] = None # inferido
|
|
127
|
+
uid: Optional[str] = None # inferido
|
|
128
|
+
algorithm: Optional[str] = None # inferido
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass
|
|
132
|
+
class EntityTypeInfo(_BaseModel):
|
|
133
|
+
"""Respuesta de GET /api/v/1/identity/type."""
|
|
134
|
+
|
|
135
|
+
type: Optional[str] = None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# =============================================================================
|
|
139
|
+
# AUTHENTICATION
|
|
140
|
+
# =============================================================================
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@dataclass
|
|
144
|
+
class ClientChallenge(_BaseModel):
|
|
145
|
+
"""
|
|
146
|
+
Respuesta de POST /api/v/1/auth/client/challenge.
|
|
147
|
+
|
|
148
|
+
Confirmado: la respuesta es ``{"ok": True, **result}`` donde ``result``
|
|
149
|
+
proviene de ``core.auth.create_client_authentication_challenge(...)``.
|
|
150
|
+
El campo ``challenge_id`` es el único que se puede inferir con certeza
|
|
151
|
+
razonable, ya que es el identificador que luego se debe reenviar a
|
|
152
|
+
``verify_client_challenge``. Cualquier otro dato del reto (nonce, datos a
|
|
153
|
+
firmar, expiración) quedará disponible en ``.extra``.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
ok: Optional[bool] = None
|
|
157
|
+
challenge: Optional[dict] = None
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@dataclass
|
|
161
|
+
class ClientChallengeVerification(_BaseModel):
|
|
162
|
+
"""
|
|
163
|
+
Respuesta de POST /api/v/1/auth/verify.
|
|
164
|
+
|
|
165
|
+
Confirmado: ``{"ok": True, **result}`` con ``result`` proveniente de
|
|
166
|
+
``core.auth.verify_client_authentication(...)``. Tras una verificación
|
|
167
|
+
exitosa, lo esperable es que el servidor emita un ``auth_token`` para la
|
|
168
|
+
entidad (ver jerarquía de tokens del proyecto); se modela como campo
|
|
169
|
+
inferido.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
ok: Optional[bool] = None
|
|
173
|
+
auth_token: Optional[str] = None # inferido
|
|
174
|
+
uid: Optional[str] = None # inferido
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass
|
|
178
|
+
class ServerChallengeResponse(_BaseModel):
|
|
179
|
+
"""
|
|
180
|
+
Respuesta de POST /api/v/1/auth/server/challenge.
|
|
181
|
+
|
|
182
|
+
Confirmado: ``{"ok": True, **result}`` con ``result`` proveniente de
|
|
183
|
+
``core.auth.create_server_authentication_response(challenge_id)``.
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
ok: Optional[bool] = None
|
|
187
|
+
response: Optional[Any] = None # inferido
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# =============================================================================
|
|
191
|
+
# DOMAINS
|
|
192
|
+
# =============================================================================
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@dataclass
|
|
196
|
+
class Domain(_BaseModel):
|
|
197
|
+
"""
|
|
198
|
+
Elemento de la lista devuelta por POST /api/v/1/domains/query.
|
|
199
|
+
|
|
200
|
+
El esquema exacto de cada dominio no se observa en las rutas (la lista se
|
|
201
|
+
retorna tal cual la entrega ``core.domain_manager.query_entity_domains``).
|
|
202
|
+
Se modelan los campos más probables según la convención del proyecto.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
_id_aliases: ClassVar[Dict[str, str]] = {"uid": "domain_uid"}
|
|
206
|
+
|
|
207
|
+
uid: Optional[str] = None # inferido (o domain_uid, ver alias)
|
|
208
|
+
name: Optional[str] = None # inferido
|
|
209
|
+
role: Optional[str] = None # inferido (rol de membresía del solicitante)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
# =============================================================================
|
|
213
|
+
# ENTITIES
|
|
214
|
+
# =============================================================================
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@dataclass
|
|
218
|
+
class Entity(_BaseModel):
|
|
219
|
+
"""
|
|
220
|
+
Elemento de la lista ``entities`` devuelta por
|
|
221
|
+
POST /api/v/1/entities/agent/query.
|
|
222
|
+
|
|
223
|
+
Confirmado en el código de la ruta: cada entidad expone ``entity_uid``,
|
|
224
|
+
y la ruta le añade explícitamente la clave ``status`` (resultado de
|
|
225
|
+
``core.services.tunnel_service.get_entity_status(...)``).
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
entity_uid: Optional[str] = None
|
|
229
|
+
entity_type: Optional[str] = None # inferido
|
|
230
|
+
status: Optional[Any] = None
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
# =============================================================================
|
|
234
|
+
# PASSPORTS
|
|
235
|
+
# =============================================================================
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@dataclass
|
|
239
|
+
class Passport(_BaseModel):
|
|
240
|
+
"""
|
|
241
|
+
Objeto ``passport`` devuelto dentro de
|
|
242
|
+
POST /api/v/1/passports/open/create -> {"created": True, "passport": ...}.
|
|
243
|
+
|
|
244
|
+
Confirmado en otras rutas que consumen un passport ya creado: expone al
|
|
245
|
+
menos ``entity_type`` (usado en ``passports.py`` para verificar que solo
|
|
246
|
+
una CONSOLE pueda emitir pasaportes OPEN). El resto de campos son los
|
|
247
|
+
parámetros de creación, que el servidor normalmente devuelve reflejados
|
|
248
|
+
en el objeto creado, más un identificador propio.
|
|
249
|
+
"""
|
|
250
|
+
|
|
251
|
+
_id_aliases: ClassVar[Dict[str, str]] = {"uid": "passport_uid"}
|
|
252
|
+
|
|
253
|
+
uid: Optional[str] = None # inferido (o passport_uid, ver alias)
|
|
254
|
+
domain_uid: Optional[str] = None # inferido (eco del parámetro de entrada)
|
|
255
|
+
passport_type: Optional[str] = None # inferido ("OPEN" / "CLOSED")
|
|
256
|
+
entity_type: Optional[str] = None
|
|
257
|
+
role: Optional[str] = None # inferido
|
|
258
|
+
expiration_hours: Optional[int] = None # inferido
|
|
259
|
+
usage_limit: Optional[int] = None # inferido
|
|
260
|
+
security_code: Optional[str] = None # inferido (código que luego se usa
|
|
261
|
+
# para "integrar" una entidad vía /integration/open o /integration/closed)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@dataclass
|
|
265
|
+
class IntegrationResult(_BaseModel):
|
|
266
|
+
"""
|
|
267
|
+
Resultado de POST /api/v/1/integration/open o
|
|
268
|
+
POST /api/v/1/integration/closed.
|
|
269
|
+
|
|
270
|
+
El cuerpo de respuesta es completamente opaco desde la capa de rutas
|
|
271
|
+
(se retorna tal cual lo entrega ``core.integration.integrate_open/closed``),
|
|
272
|
+
por lo que no se declaran campos propios más allá de los heredados de
|
|
273
|
+
``_BaseModel``. Todo el contenido de la respuesta queda en ``.extra`` /
|
|
274
|
+
accesible vía ``.raw``.
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# =============================================================================
|
|
279
|
+
# SESSIONS
|
|
280
|
+
# =============================================================================
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@dataclass
|
|
284
|
+
class SessionInfo(_BaseModel):
|
|
285
|
+
"""
|
|
286
|
+
Respuesta de POST /api/v/1/sessions/request, y forma esperada de cada
|
|
287
|
+
elemento dentro de la lista ``sessions`` de
|
|
288
|
+
POST /api/v/1/sessions/query.
|
|
289
|
+
|
|
290
|
+
Confirmado: el envoltorio ``{"ok": True, **result}`` para `/request`.
|
|
291
|
+
El campo ``session_token`` es el identificador central definido por el
|
|
292
|
+
propio proyecto para este recurso.
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
ok: Optional[bool] = None
|
|
296
|
+
session_token: Optional[str] = None # inferido
|
|
297
|
+
tunnel_token: Optional[str] = None # inferido (eco del parámetro de entrada)
|
|
298
|
+
destination_uid: Optional[str] = None # inferido (eco del parámetro de entrada)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
@dataclass
|
|
302
|
+
class SessionDeletionResult(_BaseModel):
|
|
303
|
+
"""Respuesta de POST /api/v/1/sessions/delete. Confirmado en su totalidad."""
|
|
304
|
+
|
|
305
|
+
ok: Optional[bool] = None
|
|
306
|
+
revoked: Optional[bool] = None
|
|
307
|
+
session_token: Optional[str] = None
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# =============================================================================
|
|
311
|
+
# TUNNELS
|
|
312
|
+
# =============================================================================
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@dataclass
|
|
316
|
+
class TunnelInfo(_BaseModel):
|
|
317
|
+
"""
|
|
318
|
+
Respuesta de POST /api/v/1/services/tunnels/request.
|
|
319
|
+
|
|
320
|
+
Confirmado: el envoltorio es
|
|
321
|
+
``{"ok": True, "tunnel_port": <int>, **result}`` donde ``tunnel_port`` se
|
|
322
|
+
lee literalmente de ``core.services.tunnel_service.ws._port`` en la ruta.
|
|
323
|
+
El resto de campos del ``result`` (lo que retorna
|
|
324
|
+
``core.tunnel.create_tunnel(auth_token)``) son inferidos según la
|
|
325
|
+
convención de tokens del proyecto.
|
|
326
|
+
"""
|
|
327
|
+
|
|
328
|
+
ok: Optional[bool] = None
|
|
329
|
+
tunnel_port: Optional[int] = None
|
|
330
|
+
tunnel_token: Optional[str] = None # inferido
|
|
331
|
+
tunnel_uid: Optional[str] = None # inferido
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
@dataclass
|
|
335
|
+
class TunnelOperationResult(_BaseModel):
|
|
336
|
+
"""
|
|
337
|
+
Resultado de las operaciones legacy/deprecated
|
|
338
|
+
POST /api/v/1/services/tunnels/open y .../link.
|
|
339
|
+
|
|
340
|
+
El cuerpo de respuesta es opaco desde la capa de rutas (se retorna tal
|
|
341
|
+
cual lo entrega ``tunnel_service.open_tunnel`` / ``.link_tunnels``).
|
|
342
|
+
"""
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# shared/api/manager/v1/passports.py
|
|
2
|
+
"""
|
|
3
|
+
Dominio: Passports.
|
|
4
|
+
|
|
5
|
+
Encapsula el ciclo de vida de los "pasaportes" OSAM (OPEN / CLOSED), que
|
|
6
|
+
son el mecanismo por el cual una entidad ya integrada (típicamente una
|
|
7
|
+
CONSOLE) autoriza el ingreso de nuevas entidades a un dominio.
|
|
8
|
+
|
|
9
|
+
Cobertura real de la API del servidor en esta versión:
|
|
10
|
+
|
|
11
|
+
* Creación de pasaportes **OPEN**: soportada
|
|
12
|
+
(``POST /api/v/1/passports/open/create``).
|
|
13
|
+
* Creación de pasaportes **CLOSED**: NO existe endpoint todavía. Se deja
|
|
14
|
+
``create_closed`` como punto de extensión explícito.
|
|
15
|
+
* "Validación" de un pasaporte: en la API actual, validar un pasaporte y
|
|
16
|
+
consumirlo ocurren en el mismo paso, a través de los endpoints de
|
|
17
|
+
integración (``integrate_open`` / ``integrate_closed``), que reciben el
|
|
18
|
+
``security_code`` emitido al crear el pasaporte. No existe un endpoint
|
|
19
|
+
de validación "de solo lectura" independiente.
|
|
20
|
+
* "Consulta" de pasaportes ya creados: NO existe endpoint todavía. Se deja
|
|
21
|
+
``get`` como punto de extensión explícito.
|
|
22
|
+
|
|
23
|
+
Nota importante sobre autenticación de estos endpoints: a diferencia del
|
|
24
|
+
resto de la API (que recibe ``auth_token`` en el cuerpo JSON), los
|
|
25
|
+
endpoints de integración lo esperan como cabecera ``Authorization: Bearer``.
|
|
26
|
+
La SDK respeta esa asimetría: ``HttpTransport`` soporta ambos esquemas y
|
|
27
|
+
cada método de este módulo usa el que corresponde según la ruta real.
|
|
28
|
+
|
|
29
|
+
Endpoints cubiertos:
|
|
30
|
+
POST /api/v/1/passports/open/create (auth_token en el body)
|
|
31
|
+
POST /api/v/1/integration/open (auth_token como Bearer token)
|
|
32
|
+
POST /api/v/1/integration/closed (auth_token como Bearer token)
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
from typing import Optional
|
|
38
|
+
|
|
39
|
+
from .models import IntegrationResult, Passport
|
|
40
|
+
from .transport import HttpTransport
|
|
41
|
+
|
|
42
|
+
_PASSPORTS_PREFIX = "/api/v/1/passports"
|
|
43
|
+
_INTEGRATION_PREFIX = "/api/v/1/integration"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class PassportsAPI:
|
|
47
|
+
"""API especializada para la gestión de pasaportes OSAM."""
|
|
48
|
+
|
|
49
|
+
def __init__(self, transport: HttpTransport) -> None:
|
|
50
|
+
self._transport = transport
|
|
51
|
+
|
|
52
|
+
async def create_open(
|
|
53
|
+
self,
|
|
54
|
+
auth_token: str,
|
|
55
|
+
domain_uid: str,
|
|
56
|
+
entity_role: str,
|
|
57
|
+
expiration_hours: int,
|
|
58
|
+
usage_limit: int,
|
|
59
|
+
) -> Passport:
|
|
60
|
+
"""
|
|
61
|
+
POST /api/v/1/passports/open/create
|
|
62
|
+
|
|
63
|
+
Crea un pasaporte OPEN para ``domain_uid``, válido para el rol
|
|
64
|
+
``entity_role``, con expiración y límite de usos indicados.
|
|
65
|
+
Solo entidades CONSOLE integradas pueden invocar esta operación
|
|
66
|
+
(el servidor lo valida; ver ``passports.py`` en las rutas).
|
|
67
|
+
"""
|
|
68
|
+
body = await self._transport.post(
|
|
69
|
+
f"{_PASSPORTS_PREFIX}/open/create",
|
|
70
|
+
json={
|
|
71
|
+
"auth_token": auth_token,
|
|
72
|
+
"domain_uid": domain_uid,
|
|
73
|
+
"entity_role": entity_role,
|
|
74
|
+
"expiration_hours": expiration_hours,
|
|
75
|
+
"usage_limit": usage_limit,
|
|
76
|
+
},
|
|
77
|
+
)
|
|
78
|
+
return Passport.from_dict(body.get("passport", {}))
|
|
79
|
+
|
|
80
|
+
async def create_closed(self, *args: object, **kwargs: object) -> Passport:
|
|
81
|
+
"""
|
|
82
|
+
Punto de extensión: el servidor de OSAM no expone todavía un
|
|
83
|
+
endpoint de creación de pasaportes CLOSED.
|
|
84
|
+
"""
|
|
85
|
+
raise NotImplementedError(
|
|
86
|
+
"El servidor de OSAM no expone todavía un endpoint de creación "
|
|
87
|
+
"de pasaportes CLOSED."
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
async def integrate_open(
|
|
91
|
+
self, auth_token: str, security_code: str, entity_type: str
|
|
92
|
+
) -> IntegrationResult:
|
|
93
|
+
"""
|
|
94
|
+
POST /api/v/1/integration/open
|
|
95
|
+
|
|
96
|
+
Redime un ``security_code`` de un pasaporte OPEN, declarando el
|
|
97
|
+
``entity_type`` con el que la entidad solicitante se integra.
|
|
98
|
+
``auth_token`` se envía como cabecera ``Authorization: Bearer``.
|
|
99
|
+
"""
|
|
100
|
+
body = await self._transport.post(
|
|
101
|
+
f"{_INTEGRATION_PREFIX}/open",
|
|
102
|
+
json={"security_code": security_code, "entity_type": entity_type},
|
|
103
|
+
bearer_token=auth_token,
|
|
104
|
+
)
|
|
105
|
+
return body
|
|
106
|
+
|
|
107
|
+
async def integrate_closed(
|
|
108
|
+
self, auth_token: str, security_code: str
|
|
109
|
+
) -> IntegrationResult:
|
|
110
|
+
"""
|
|
111
|
+
POST /api/v/1/integration/closed
|
|
112
|
+
|
|
113
|
+
Redime un ``security_code`` de un pasaporte CLOSED.
|
|
114
|
+
``auth_token`` se envía como cabecera ``Authorization: Bearer``.
|
|
115
|
+
"""
|
|
116
|
+
body = await self._transport.post(
|
|
117
|
+
f"{_INTEGRATION_PREFIX}/closed",
|
|
118
|
+
json={"security_code": security_code},
|
|
119
|
+
bearer_token=auth_token,
|
|
120
|
+
)
|
|
121
|
+
return body
|
|
122
|
+
|
|
123
|
+
async def get(self, auth_token: str, passport_uid: str) -> Optional[Passport]:
|
|
124
|
+
"""
|
|
125
|
+
Punto de extensión: el servidor de OSAM no expone todavía un
|
|
126
|
+
endpoint de consulta de un pasaporte por identificador.
|
|
127
|
+
"""
|
|
128
|
+
raise NotImplementedError(
|
|
129
|
+
"El servidor de OSAM no expone todavía un endpoint de consulta "
|
|
130
|
+
"de pasaportes existentes."
|
|
131
|
+
)
|