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.
- openshell_shared-0.1.2/PKG-INFO +59 -0
- openshell_shared-0.1.2/README.md +46 -0
- openshell_shared-0.1.2/api/__init__.py +1 -0
- openshell_shared-0.1.2/api/manager/__init__.py +1 -0
- openshell_shared-0.1.2/api/manager/v1/__init__.py +78 -0
- openshell_shared-0.1.2/api/manager/v1/authentication.py +278 -0
- openshell_shared-0.1.2/api/manager/v1/client.py +111 -0
- openshell_shared-0.1.2/api/manager/v1/domains.py +64 -0
- openshell_shared-0.1.2/api/manager/v1/entities.py +97 -0
- openshell_shared-0.1.2/api/manager/v1/exceptions.py +98 -0
- openshell_shared-0.1.2/api/manager/v1/identity.py +44 -0
- openshell_shared-0.1.2/api/manager/v1/models.py +342 -0
- openshell_shared-0.1.2/api/manager/v1/passports.py +131 -0
- openshell_shared-0.1.2/api/manager/v1/sessions.py +83 -0
- openshell_shared-0.1.2/api/manager/v1/transport.py +253 -0
- openshell_shared-0.1.2/api/manager/v1/tunnels.py +120 -0
- openshell_shared-0.1.2/cryptography/__init__.py +0 -0
- openshell_shared-0.1.2/cryptography/certificate.py +390 -0
- openshell_shared-0.1.2/cryptography/encoding.py +0 -0
- openshell_shared-0.1.2/cryptography/identity.py +124 -0
- openshell_shared-0.1.2/cryptography/keys.py +463 -0
- openshell_shared-0.1.2/cryptography/signatures.py +63 -0
- openshell_shared-0.1.2/cryptography/utils.py +0 -0
- openshell_shared-0.1.2/domain/__init__.py +0 -0
- openshell_shared-0.1.2/domain/domain.py +80 -0
- openshell_shared-0.1.2/domain/membership.py +21 -0
- openshell_shared-0.1.2/domain/permissions.py +14 -0
- openshell_shared-0.1.2/domain/policies.py +2 -0
- openshell_shared-0.1.2/identity/__init__.py +0 -0
- openshell_shared-0.1.2/identity/identification.py +64 -0
- openshell_shared-0.1.2/identity/store.py +150 -0
- openshell_shared-0.1.2/modules/__init__.py +0 -0
- openshell_shared-0.1.2/modules/shell/__init__.py +0 -0
- openshell_shared-0.1.2/modules/shell/client.py +361 -0
- openshell_shared-0.1.2/modules/shell/models.py +61 -0
- openshell_shared-0.1.2/modules/shell/protocol.py +249 -0
- openshell_shared-0.1.2/modules/shell/server.py +511 -0
- openshell_shared-0.1.2/modules/shell/session.py +339 -0
- openshell_shared-0.1.2/modules/utils.py +212 -0
- openshell_shared-0.1.2/openshell_shared.egg-info/PKG-INFO +59 -0
- openshell_shared-0.1.2/openshell_shared.egg-info/SOURCES.txt +65 -0
- openshell_shared-0.1.2/openshell_shared.egg-info/dependency_links.txt +1 -0
- openshell_shared-0.1.2/openshell_shared.egg-info/requires.txt +4 -0
- openshell_shared-0.1.2/openshell_shared.egg-info/top_level.txt +9 -0
- openshell_shared-0.1.2/protocols/__init__.py +0 -0
- openshell_shared-0.1.2/protocols/negotiation/challenge.py +127 -0
- openshell_shared-0.1.2/protocols/negotiation/models.py +28 -0
- openshell_shared-0.1.2/pyproject.toml +31 -0
- openshell_shared-0.1.2/setup.cfg +4 -0
- openshell_shared-0.1.2/standards/__init__.py +0 -0
- openshell_shared-0.1.2/standards/certificates/__init__.py +0 -0
- openshell_shared-0.1.2/standards/certificates/status.py +12 -0
- openshell_shared-0.1.2/standards/certificates/types.py +11 -0
- openshell_shared-0.1.2/standards/entities/__init__.py +0 -0
- openshell_shared-0.1.2/standards/entities/types.py +14 -0
- openshell_shared-0.1.2/standards/events/__init__.py +0 -0
- openshell_shared-0.1.2/standards/events/schemas/__init__.py +0 -0
- openshell_shared-0.1.2/standards/events/schemas/entity_registered.py +13 -0
- openshell_shared-0.1.2/standards/events/types.py +18 -0
- openshell_shared-0.1.2/standards/passports/__init__.py +0 -0
- openshell_shared-0.1.2/standards/passports/types.py +5 -0
- openshell_shared-0.1.2/standards/permissions/__init__.py +0 -0
- openshell_shared-0.1.2/standards/permissions/types.py +14 -0
- openshell_shared-0.1.2/standards/roles/__init__.py +0 -0
- openshell_shared-0.1.2/standards/roles/types.py +8 -0
- openshell_shared-0.1.2/standards/transports/__init__.py +0 -0
- 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
|