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.
Files changed (62) hide show
  1. api/__init__.py +1 -0
  2. api/manager/__init__.py +1 -0
  3. api/manager/v1/__init__.py +78 -0
  4. api/manager/v1/authentication.py +278 -0
  5. api/manager/v1/client.py +111 -0
  6. api/manager/v1/domains.py +64 -0
  7. api/manager/v1/entities.py +97 -0
  8. api/manager/v1/exceptions.py +98 -0
  9. api/manager/v1/identity.py +44 -0
  10. api/manager/v1/models.py +342 -0
  11. api/manager/v1/passports.py +131 -0
  12. api/manager/v1/sessions.py +83 -0
  13. api/manager/v1/transport.py +253 -0
  14. api/manager/v1/tunnels.py +120 -0
  15. cryptography/__init__.py +0 -0
  16. cryptography/certificate.py +390 -0
  17. cryptography/encoding.py +0 -0
  18. cryptography/identity.py +124 -0
  19. cryptography/keys.py +463 -0
  20. cryptography/signatures.py +63 -0
  21. cryptography/utils.py +0 -0
  22. domain/__init__.py +0 -0
  23. domain/domain.py +80 -0
  24. domain/membership.py +21 -0
  25. domain/permissions.py +14 -0
  26. domain/policies.py +2 -0
  27. identity/__init__.py +0 -0
  28. identity/identification.py +64 -0
  29. identity/store.py +150 -0
  30. modules/__init__.py +0 -0
  31. modules/shell/__init__.py +0 -0
  32. modules/shell/client.py +361 -0
  33. modules/shell/models.py +61 -0
  34. modules/shell/protocol.py +249 -0
  35. modules/shell/server.py +511 -0
  36. modules/shell/session.py +339 -0
  37. modules/utils.py +212 -0
  38. openshell_shared-0.1.2.dist-info/METADATA +59 -0
  39. openshell_shared-0.1.2.dist-info/RECORD +62 -0
  40. openshell_shared-0.1.2.dist-info/WHEEL +5 -0
  41. openshell_shared-0.1.2.dist-info/top_level.txt +7 -0
  42. protocols/__init__.py +0 -0
  43. protocols/negotiation/challenge.py +127 -0
  44. protocols/negotiation/models.py +28 -0
  45. standards/__init__.py +0 -0
  46. standards/certificates/__init__.py +0 -0
  47. standards/certificates/status.py +12 -0
  48. standards/certificates/types.py +11 -0
  49. standards/entities/__init__.py +0 -0
  50. standards/entities/types.py +14 -0
  51. standards/events/__init__.py +0 -0
  52. standards/events/schemas/__init__.py +0 -0
  53. standards/events/schemas/entity_registered.py +13 -0
  54. standards/events/types.py +18 -0
  55. standards/passports/__init__.py +0 -0
  56. standards/passports/types.py +5 -0
  57. standards/permissions/__init__.py +0 -0
  58. standards/permissions/types.py +14 -0
  59. standards/roles/__init__.py +0 -0
  60. standards/roles/types.py +8 -0
  61. standards/transports/__init__.py +0 -0
  62. standards/transports/types.py +24 -0
@@ -0,0 +1,83 @@
1
+ # shared/api/manager/v1/sessions.py
2
+ """
3
+ Dominio: Sessions.
4
+
5
+ Encapsula la creación, listado y cierre de sesiones de shell sobre un
6
+ túnel ya establecido.
7
+
8
+ Endpoints cubiertos (todos bajo ``/api/v/1/sessions``):
9
+ POST /request
10
+ POST /query
11
+ POST /delete
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from typing import List
17
+
18
+ from .models import SessionDeletionResult, SessionInfo
19
+ from .transport import HttpTransport
20
+
21
+ _PREFIX = "/api/v/1/sessions"
22
+
23
+
24
+ class SessionsAPI:
25
+ """API especializada para la gestión de sesiones OSAM."""
26
+
27
+ def __init__(self, transport: HttpTransport) -> None:
28
+ self._transport = transport
29
+
30
+ async def create(
31
+ self, auth_token: str, tunnel_token: str, destination_uid: str
32
+ ) -> dict:
33
+ """
34
+ POST /api/v/1/sessions/request
35
+
36
+ Crea una nueva sesión sobre el túnel ``tunnel_token`` hacia la
37
+ entidad destino ``destination_uid``.
38
+ """
39
+ body = await self._transport.post(
40
+ f"{_PREFIX}/request",
41
+ json={
42
+ "auth_token": auth_token,
43
+ "tunnel_token": tunnel_token,
44
+ "destination_uid": destination_uid,
45
+ },
46
+ )
47
+ return body
48
+
49
+ async def list(self, auth_token: str, tunnel_token: str) -> list:
50
+ """
51
+ POST /api/v/1/sessions/query
52
+
53
+ Lista las sesiones activas. Nota: el servidor exige ``tunnel_token``
54
+ en el payload (forma parte del modelo de request), aunque la
55
+ implementación actual del handler no lo use para filtrar — se envía
56
+ igualmente para cumplir el contrato HTTP exacto del servidor.
57
+ """
58
+ body = await self._transport.post(
59
+ f"{_PREFIX}/query",
60
+ json={"auth_token": auth_token, "tunnel_token": tunnel_token},
61
+ )
62
+ sessions = body.get("sessions", [])
63
+ if not isinstance(sessions, list):
64
+ sessions = []
65
+ return sessions
66
+
67
+ async def close(
68
+ self, auth_token: str, tunnel_token: str, session_token: str
69
+ ) -> dict:
70
+ """
71
+ POST /api/v/1/sessions/delete
72
+
73
+ Revoca la sesión identificada por ``session_token``.
74
+ """
75
+ body = await self._transport.post(
76
+ f"{_PREFIX}/delete",
77
+ json={
78
+ "auth_token": auth_token,
79
+ "tunnel_token": tunnel_token,
80
+ "session_token": session_token,
81
+ },
82
+ )
83
+ return body
@@ -0,0 +1,253 @@
1
+ # shared/api/manager/v1/transport.py
2
+ """
3
+ Capa de transporte HTTP centralizada de la SDK de OSAM.
4
+
5
+ ``HttpTransport`` es el único lugar del paquete que sabe construir URLs,
6
+ serializar/deserializar JSON, aplicar timeouts y traducir respuestas HTTP
7
+ de error en excepciones propias de la SDK (ver ``exceptions.py``).
8
+
9
+ Ningún otro módulo de ``shared/api/manager/v1`` debe importar ``httpx`` directamente
10
+ ni construir URLs a mano: todos los módulos de dominio (``identity.py``,
11
+ ``authentication.py``, ``domains.py``, etc.) reciben una instancia de
12
+ ``HttpTransport`` y delegan en ella toda la comunicación de red.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ from typing import Any, Dict, Mapping, Optional, Union
19
+
20
+ import httpx
21
+
22
+ from .exceptions import (
23
+ APIError,
24
+ AuthenticationError,
25
+ AuthorizationError,
26
+ EntityNotFoundError,
27
+ InvalidResponseError,
28
+ NetworkError,
29
+ ServerError,
30
+ ValidationError,
31
+ )
32
+
33
+ logger = logging.getLogger("osam.transport")
34
+
35
+
36
+ def _mask_secret(value: Optional[str], keep: int = 4) -> str:
37
+ """
38
+ Devuelve una versión enmascarada de un secreto (token, código, etc.)
39
+ apta para logging. Nunca se debe registrar un token completo.
40
+ """
41
+ if not value:
42
+ return "<empty>"
43
+ if len(value) <= keep * 2:
44
+ return "*" * len(value)
45
+ return f"{value[:keep]}...{value[-keep:]}"
46
+
47
+
48
+ def _extract_detail(body: Any) -> str:
49
+ """
50
+ Extrae un mensaje legible del cuerpo de error de FastAPI.
51
+
52
+ FastAPI usa de forma consistente el formato ``{"detail": ...}``.
53
+ ``detail`` normalmente es un string (HTTPException explícita) pero en
54
+ errores de validación de pydantic (422) puede ser una lista de objetos
55
+ ``{"loc": ..., "msg": ..., "type": ...}``.
56
+ """
57
+ if isinstance(body, Mapping):
58
+ detail = body.get("detail")
59
+ if isinstance(detail, str):
60
+ return detail
61
+ if isinstance(detail, list):
62
+ parts = []
63
+ for item in detail:
64
+ if isinstance(item, Mapping) and "msg" in item:
65
+ loc = ".".join(str(p) for p in item.get("loc", []))
66
+ parts.append(f"{loc}: {item['msg']}" if loc else str(item["msg"]))
67
+ else:
68
+ parts.append(str(item))
69
+ if parts:
70
+ return "; ".join(parts)
71
+ if detail is not None:
72
+ return str(detail)
73
+ return "Error desconocido devuelto por el servidor OSAM"
74
+
75
+
76
+ class HttpTransport:
77
+ """
78
+ Cliente HTTP interno y de bajo nivel usado por toda la SDK.
79
+
80
+ Responsabilidades:
81
+ * Mantener la URL base (protocolo + host + puerto).
82
+ * Aplicar timeouts y verificación TLS de forma consistente.
83
+ * Serializar el cuerpo de la petición y deserializar la respuesta JSON.
84
+ * Traducir errores de red y códigos HTTP de error en la jerarquía de
85
+ excepciones de ``exceptions.py``.
86
+
87
+ No conoce nada sobre los dominios funcionales de OSAM (identidad,
88
+ pasaportes, túneles...); eso vive en los módulos de dominio que
89
+ consumen esta clase.
90
+ """
91
+
92
+ def __init__(
93
+ self,
94
+ base_url: str,
95
+ *,
96
+ timeout: float = 10.0,
97
+ verify_ssl: Union[bool, str] = True,
98
+ default_headers: Optional[Dict[str, str]] = None,
99
+ ) -> None:
100
+ self._base_url = base_url.rstrip("/")
101
+ self._client = httpx.AsyncClient(
102
+ base_url=self._base_url,
103
+ timeout=timeout,
104
+ verify=verify_ssl,
105
+ headers=default_headers or {},
106
+ )
107
+
108
+ # -- ciclo de vida -----------------------------------------------------
109
+
110
+ async def aclose(self) -> None:
111
+ await self._client.aclose()
112
+
113
+ async def __aenter__(self) -> "HttpTransport":
114
+ return self
115
+
116
+ async def __aexit__(self, *exc_info: Any) -> None:
117
+ await self.aclose()
118
+
119
+ # -- API pública usada por los módulos de dominio -----------------------
120
+
121
+ async def get(
122
+ self,
123
+ path: str,
124
+ *,
125
+ params: Optional[Mapping[str, Any]] = None,
126
+ bearer_token: Optional[str] = None,
127
+ ) -> Dict[str, Any]:
128
+ return await self._request(
129
+ "GET", path, params=params, bearer_token=bearer_token
130
+ )
131
+
132
+ async def post(
133
+ self,
134
+ path: str,
135
+ *,
136
+ json: Optional[Mapping[str, Any]] = None,
137
+ bearer_token: Optional[str] = None,
138
+ ) -> Dict[str, Any]:
139
+ return await self._request(
140
+ "POST", path, json=json, bearer_token=bearer_token
141
+ )
142
+
143
+ # -- internals ----------------------------------------------------------
144
+
145
+ async def _request(
146
+ self,
147
+ method: str,
148
+ path: str,
149
+ *,
150
+ json: Optional[Mapping[str, Any]] = None,
151
+ params: Optional[Mapping[str, Any]] = None,
152
+ bearer_token: Optional[str] = None,
153
+ ) -> Dict[str, Any]:
154
+ headers: Dict[str, str] = {}
155
+ if bearer_token:
156
+ headers["Authorization"] = f"Bearer {bearer_token}"
157
+
158
+ logger.debug(
159
+ "OSAM %s %s%s (bearer=%s)",
160
+ method,
161
+ self._base_url,
162
+ path,
163
+ _mask_secret(bearer_token),
164
+ )
165
+
166
+ try:
167
+ response = await self._client.request(
168
+ method,
169
+ path,
170
+ json=dict(json) if json is not None else None,
171
+ params=dict(params) if params is not None else None,
172
+ headers=headers,
173
+ )
174
+ except httpx.TimeoutException as exc:
175
+ raise NetworkError(
176
+ f"Tiempo de espera agotado al llamar a {method} {path}"
177
+ ) from exc
178
+ except httpx.TransportError as exc:
179
+ raise NetworkError(
180
+ f"No se pudo establecer comunicación con el servidor OSAM "
181
+ f"({method} {path}): {exc}"
182
+ ) from exc
183
+
184
+ body = self._parse_body(response, method, path)
185
+
186
+ if response.is_success:
187
+ return body
188
+
189
+ # _raise_for_status siempre lanza una subclase de APIError.
190
+ self._raise_for_status(response.status_code, body, method, path)
191
+ raise AssertionError("unreachable") # pragma: no cover
192
+
193
+ @staticmethod
194
+ def _parse_body(
195
+ response: httpx.Response, method: str, path: str
196
+ ) -> Dict[str, Any]:
197
+ if not response.content:
198
+ return {}
199
+ try:
200
+ data = response.json()
201
+ except ValueError as exc:
202
+ raise InvalidResponseError(
203
+ f"Respuesta no es JSON válido para {method} {path} "
204
+ f"(status={response.status_code})"
205
+ ) from exc
206
+
207
+ if isinstance(data, dict):
208
+ return data
209
+ if isinstance(data, list):
210
+ # Algunos endpoints (ej. /domains/query) devuelven una lista
211
+ # cruda en lugar de un objeto. Se envuelve para mantener una
212
+ # interfaz homogénea hacia el resto de la SDK.
213
+ return {"items": data}
214
+
215
+ raise InvalidResponseError(
216
+ f"Respuesta JSON con forma inesperada para {method} {path}: "
217
+ f"se esperaba un objeto o una lista, se obtuvo {type(data).__name__}"
218
+ )
219
+
220
+ @staticmethod
221
+ def _raise_for_status(
222
+ status_code: int, body: Dict[str, Any], method: str, path: str
223
+ ) -> None:
224
+ detail = _extract_detail(body)
225
+ message = f"{method} {path} -> HTTP {status_code}: {detail}"
226
+
227
+ if status_code in (400, 422):
228
+ raise ValidationError(
229
+ message, status_code=status_code, detail=detail, response_body=body
230
+ )
231
+ if status_code == 401:
232
+ raise AuthenticationError(
233
+ message, status_code=status_code, detail=detail, response_body=body
234
+ )
235
+ if status_code == 403:
236
+ raise AuthorizationError(
237
+ message, status_code=status_code, detail=detail, response_body=body
238
+ )
239
+ if status_code == 404:
240
+ raise EntityNotFoundError(
241
+ message, status_code=status_code, detail=detail, response_body=body
242
+ )
243
+ if status_code >= 500:
244
+ raise ServerError(
245
+ message, status_code=status_code, detail=detail, response_body=body
246
+ )
247
+
248
+ # Cualquier otro código de error no contemplado explícitamente por
249
+ # la API de OSAM cae en el caso genérico, sin perder información.
250
+ raise APIError(
251
+ message, status_code=status_code, detail=detail, response_body=body
252
+ )
253
+
@@ -0,0 +1,120 @@
1
+ # shared/api/manager/v1/tunnels.py
2
+ """
3
+ Dominio: Tunnels.
4
+
5
+ Encapsula la solicitud y gestión de túneles. La API actual del servidor
6
+ expone:
7
+
8
+ * ``request`` (vigente): solicita un nuevo túnel asociado a la entidad
9
+ autenticada.
10
+ * ``open`` y ``link`` (marcados ``DEPRECATED`` explícitamente en el código
11
+ de las rutas del servidor): se mantienen en la SDK por compatibilidad,
12
+ pero cada llamada emite un ``DeprecationWarning`` para que el código
13
+ consumidor migre al flujo vigente (``request`` + ``sessions``).
14
+
15
+ No existen (todavía) endpoints de consulta de estado o renovación de un
16
+ túnel ya creado; se dejan ``get_status`` y ``renew`` como puntos de
17
+ extensión explícitos.
18
+
19
+ Endpoints cubiertos:
20
+ POST /api/v/1/services/tunnels/request
21
+ POST /api/v/1/services/tunnels/open (deprecated)
22
+ POST /api/v/1/services/tunnels/link (deprecated)
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import warnings
28
+
29
+ from .models import TunnelInfo, TunnelOperationResult
30
+ from .transport import HttpTransport
31
+
32
+ _PREFIX = "/api/v/1/services/tunnels"
33
+
34
+
35
+ class TunnelsAPI:
36
+ """API especializada para la gestión de túneles OSAM."""
37
+
38
+ def __init__(self, transport: HttpTransport) -> None:
39
+ self._transport = transport
40
+
41
+ async def request(self, auth_token: str) -> dict:
42
+ """
43
+ POST /api/v/1/services/tunnels/request
44
+
45
+ Solicita un nuevo túnel para la entidad identificada por
46
+ ``auth_token``. Requiere que la entidad ya pertenezca a, al menos,
47
+ un dominio.
48
+ """
49
+ body = await self._transport.post(
50
+ f"{_PREFIX}/request",
51
+ json={"auth_token": auth_token},
52
+ )
53
+ return body
54
+
55
+ async def open(self, session_token: str, tunnel_uid: str) -> dict:
56
+ """
57
+ POST /api/v/1/services/tunnels/open [DEPRECATED]
58
+
59
+ Mantenido por compatibilidad con integraciones existentes. Use
60
+ :meth:`request` seguido del dominio de ``sessions`` en código nuevo.
61
+ """
62
+ warnings.warn(
63
+ "TunnelsAPI.open() llama a un endpoint marcado como DEPRECATED "
64
+ "en el servidor OSAM. Use TunnelsAPI.request() junto con "
65
+ "SessionsAPI.create() en código nuevo.",
66
+ DeprecationWarning,
67
+ stacklevel=2,
68
+ )
69
+ body = await self._transport.post(
70
+ f"{_PREFIX}/open",
71
+ json={"session_token": session_token, "tunnel_uid": tunnel_uid},
72
+ )
73
+ return body
74
+
75
+ async def link(
76
+ self,
77
+ session_token: str,
78
+ source_tunnel_uid: str,
79
+ destination_tunnel_uid: str,
80
+ ) -> dict:
81
+ """
82
+ POST /api/v/1/services/tunnels/link [DEPRECATED]
83
+
84
+ Mantenido por compatibilidad con integraciones existentes.
85
+ """
86
+ warnings.warn(
87
+ "TunnelsAPI.link() llama a un endpoint marcado como DEPRECATED "
88
+ "en el servidor OSAM.",
89
+ DeprecationWarning,
90
+ stacklevel=2,
91
+ )
92
+ body = await self._transport.post(
93
+ f"{_PREFIX}/link",
94
+ json={
95
+ "session_token": session_token,
96
+ "source_tunnel_uid": source_tunnel_uid,
97
+ "destination_tunnel_uid": destination_tunnel_uid,
98
+ },
99
+ )
100
+ return body
101
+
102
+ async def get_status(self, auth_token: str, tunnel_uid: str) -> TunnelInfo:
103
+ """
104
+ Punto de extensión: el servidor de OSAM no expone todavía un
105
+ endpoint dedicado de consulta de estado de un túnel existente.
106
+ """
107
+ raise NotImplementedError(
108
+ "El servidor de OSAM no expone todavía un endpoint de consulta "
109
+ "de estado de túneles."
110
+ )
111
+
112
+ async def renew(self, auth_token: str, tunnel_uid: str) -> TunnelInfo:
113
+ """
114
+ Punto de extensión: el servidor de OSAM no expone todavía un
115
+ endpoint de renovación de un túnel existente.
116
+ """
117
+ raise NotImplementedError(
118
+ "El servidor de OSAM no expone todavía un endpoint de "
119
+ "renovación de túneles."
120
+ )
File without changes