Entidadactivecreado Mon Jun 08#network#endpoint#api#auto-provision#security#credentials#encryption#terminal
Endpoint: GET /api/network/auto-provision/profiles/{profile_id}/credentials
Firma
@router.get(
"/auto-provision/profiles/{profile_id}/credentials",
response={200: dict, 404: dict},
tags=["Auto-Provision"]
)
def get_profile_credentials(request, profile_id: int):
"""Credenciales SSH (descifradas) de un DeviceProfile para abrir el Terminal."""
Ubicación: network/api/profiles.py, línea ~250.
Propósito
Descifra y devuelve las credenciales SSH almacenadas en un DeviceProfile para que el Terminal las pre-rellene en la conexión manual sin re-pedirlas al usuario. Este endpoint es sensible: expone credenciales en claro, por lo que está gateado por permisos y organización.
Autenticación y permisos
- Autenticado: Sí, requiere login.
- Gateado por permiso:
require_perm(request, "network", "edit")— solo usuarios con permiso editar en Network. - Aislado por organización (RLS): Valida que el
DeviceProfile.organizationcoincida conget_current_org(request).
Parámetros
| Parámetro | Tipo | Ubicación | Descripción |
|---|---|---|---|
profile_id | int | Path | ID del DeviceProfile cuyas credenciales se solicitan. |
Respuesta exitosa (200)
{
"has_credentials": true,
"ip": "10.5.5.5",
"username": "admin",
"password": "topsecret",
"port": 22
}
Campos:
has_credentials(bool):truesi el profile tienessh_username+ssh_password_encryptedno vacíos.ip(str): IP del dispositivo (profile.ip_address).username(str): Usuario SSH descifrado (o""si no hay creds).password(str): Contraseña descifrada (o""si no hay creds).port(int): Puerto SSH (siempre 22 por ahora).
Respuesta sin credenciales (200 con bandera falsa)
Si el DeviceProfile no tiene credenciales guardadas:
{
"has_credentials": false,
"ip": "10.5.5.8",
"username": "",
"password": "",
"port": 22
}
Comportamiento: El frontend recibe has_credentials: false y sabe que debe solicitar las credenciales al usuario manualmente en el modal del Terminal.
Errores
| Código | Escenario | Respuesta |
|---|---|---|
| 404 | DeviceProfile no existe o pertenece a otra org | No encontrado |
| 403 | Usuario no tiene permiso network.edit | Forbidden |
| 500 | Error descifrado (clave corrupta, DB inconsistente) | Internal Server Error |
Descifrado de credenciales
Mecanismo interno:
- Se valida que
profile.ssh_password_encryptedexista y no esté vacío. - Se llama a
CredentialManager.safe_decrypt(encrypted_text, "ssh_password"). - Si el descifrado falla (error de Fernet, clave corrupta),
safe_decryptretornaNone, y el endpoint devuelvepassword: ""(no se propaga excepción; fallsafe).
Código relevante (DeviceProfile.get_ssh_password()):
def get_ssh_password(self):
"""Descifra la contraseña SSH guardada (o '' si no hay / no descifra)."""
if not self.ssh_password_encrypted:
return ""
from core.security import CredentialManager
return CredentialManager.safe_decrypt(self.ssh_password_encrypted, "ssh_password") or ""
Casos de uso
1. Terminal pre-rellena credenciales en dispositivos detectados
const creds = await fetch(`/api/network/auto-provision/profiles/123/credentials`);
if (creds.has_credentials) {
document.getElementById('manual-ssh-username').value = creds.username;
document.getElementById('manual-ssh-password').value = creds.password;
}
2. Validar que un profile tiene credenciales antes de UI
const status = await fetch(`/api/network/auto-provision/profiles/456/credentials`);
if (status.has_credentials) {
showBadge("SSH credentials stored");
} else {
showBadge("No SSH credentials");
}
Eventos relacionados (telemetría)
- Al descifrar credenciales, se no registra la contraseña en logs. El endpoint es transparente en auditoría (por diseño de seguridad).
- Si
safe_decryptfalla silenciosamente, se logs error interno pero no se expone al cliente.
Integración con Terminal
Contexto en terminal.js::quickConnectHost():
async quickConnectHost(ip, name, profileId, hasStored) {
// ... setup initial fields ...
if (hasStored && profileId && window.ApiService) {
try {
const creds = await window.ApiService.get(
`/api/network/auto-provision/profiles/${profileId}/credentials`
);
if (creds && creds.has_credentials) {
document.getElementById('manual-ssh-username').value = creds.username;
document.getElementById('manual-ssh-password').value = creds.password;
if (creds.port) document.getElementById('manual-ssh-port').value = creds.port;
}
} catch (e) {
console.warn('No stored SSH credentials for profile', profileId, e);
}
}
this.openManualConnectionModal();
}
Flujo:
- Usuario hace clic en un dispositivo detectado en Terminal.
quickConnectHost()se invoca conprofileIdyhasStored=true.- Se llama a este endpoint.
- Si las credenciales están presentes, se rellenan en el modal.
- Si no, el modal queda vacío y el usuario ingresa manualmente.
Testing
Tests en tests/api/test_terminal_hosts.py::TestProfileSshCredentials:
def test_endpoint_returns_decrypted_creds(self, admin_user, organization):
p = self._profile_with_creds(organization, ip="10.5.5.7", password="topsecret")
client = self._client_for(admin_user, organization)
resp = client.get(f"/api/network/auto-provision/profiles/{p.id}/credentials")
assert resp.status_code == 200
body = resp.json()
assert body["has_credentials"] is True
assert body["username"] == "admin"
assert body["password"] == "topsecret"
def test_viewer_cannot_get_credentials(self, viewer_user, organization):
p = self._profile_with_creds(organization, ip="10.5.5.9")
client = self._client_for(viewer_user, organization)
assert client.get(f"/api/network/auto-provision/profiles/{p.id}/credentials").status_code == 403
Notas de seguridad
- Credenciales en tránsito: Se devuelven en claro solo mediante HTTPS (validar en production).
- Permisos estrictos: Solo
network.editpuede acceder. Unviewerde red no puede ver credenciales. - RLS por org: No puedes acceder a profiles de otra organización.
- No se registran en logs: Intentar no incluir password en error messages.
- Fallsafe descifrado: Si algo falla, se devuelve
password: ""en lugar de propagar excepción.
Véase también
- [[feature—terminal—ssh-credential-inheritance]]
- [[entity—network—model—device-profile]]
- [[concept—saas—encryption-and-secrets]]
- [[concept—saas—multi-tenancy]]