Complete admin package restructuring
This commit is contained in:
@ -0,0 +1,8 @@
|
||||
"""Services module.
|
||||
|
||||
This module contains business logic.
|
||||
"""
|
||||
|
||||
from .admin_backend import AdminBackend
|
||||
|
||||
__all__ = ["AdminBackend"]
|
||||
@ -0,0 +1,430 @@
|
||||
"""API for working with the clients.
|
||||
|
||||
Since we have a frontend and a REST API, it makes sense to have a generic library to work with the clients.
|
||||
"""
|
||||
|
||||
import logging
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager
|
||||
|
||||
from sshecret.backend import AuditLog, Client, ClientFilter, Secret, SshecretBackend, Operation, SubSystem
|
||||
from sshecret.backend.models import DetailedSecrets
|
||||
from sshecret.backend.api import AuditAPI
|
||||
from sshecret.crypto import encrypt_string, load_public_key
|
||||
|
||||
from .keepass import PasswordContext, load_password_manager
|
||||
from sshecret_admin.core.settings import AdminServerSettings
|
||||
from .models import SecretView
|
||||
|
||||
|
||||
class ClientManagementError(Exception):
|
||||
"""Base exception for client management operations."""
|
||||
|
||||
|
||||
class ClientNotFoundError(ClientManagementError):
|
||||
"""Client not found."""
|
||||
|
||||
|
||||
class SecretNotFoundError(ClientManagementError):
|
||||
"""Secret not found."""
|
||||
|
||||
|
||||
class BackendUnavailableError(ClientManagementError):
|
||||
"""Backend unavailable."""
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AdminBackend:
|
||||
"""Admin backend API."""
|
||||
|
||||
def __init__(self, settings: AdminServerSettings, keepass_password: str) -> None:
|
||||
"""Create client management API."""
|
||||
self.settings: AdminServerSettings = settings
|
||||
self.backend: SshecretBackend = SshecretBackend(
|
||||
str(settings.backend_url), settings.backend_token
|
||||
)
|
||||
self.keepass_password: str = keepass_password
|
||||
|
||||
@contextmanager
|
||||
def password_manager(self) -> Iterator[PasswordContext]:
|
||||
"""Open the password manager."""
|
||||
with load_password_manager(self.settings, self.keepass_password) as kp:
|
||||
yield kp
|
||||
|
||||
async def _get_clients(self, filter: ClientFilter | None = None) -> list[Client]:
|
||||
"""Get clients from backend."""
|
||||
return await self.backend.get_clients(filter)
|
||||
|
||||
async def get_clients(self, filter: ClientFilter | None = None) -> list[Client]:
|
||||
"""Get clients from backend."""
|
||||
try:
|
||||
return await self._get_clients(filter)
|
||||
except ClientManagementError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise BackendUnavailableError() from e
|
||||
|
||||
async def _get_client(self, name: str) -> Client | None:
|
||||
"""Get a client from the backend."""
|
||||
return await self.backend.get_client(name)
|
||||
|
||||
async def _verify_client_exists(self, name: str) -> None:
|
||||
"""Check that a client exists."""
|
||||
client = await self.backend.get_client(name)
|
||||
if not client:
|
||||
raise ClientNotFoundError()
|
||||
return None
|
||||
|
||||
async def verify_client_exists(self, name: str) -> None:
|
||||
"""Check that a client exists."""
|
||||
try:
|
||||
await self._verify_client_exists(name)
|
||||
except ClientManagementError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise BackendUnavailableError() from e
|
||||
|
||||
async def get_client(self, name: str) -> Client | None:
|
||||
"""Get a client from the backend."""
|
||||
try:
|
||||
return await self._get_client(name)
|
||||
except ClientManagementError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise BackendUnavailableError() from e
|
||||
|
||||
async def _create_client(
|
||||
self,
|
||||
name: str,
|
||||
public_key: str,
|
||||
description: str | None = None,
|
||||
sources: list[str] | None = None,
|
||||
) -> Client:
|
||||
"""Create client."""
|
||||
await self.backend.create_client(name, public_key, description)
|
||||
if sources:
|
||||
await self.backend.update_client_sources(name, sources)
|
||||
client = await self.get_client(name)
|
||||
|
||||
if not client:
|
||||
raise ClientNotFoundError()
|
||||
|
||||
return client
|
||||
|
||||
async def create_client(
|
||||
self,
|
||||
name: str,
|
||||
public_key: str,
|
||||
description: str | None = None,
|
||||
sources: list[str] | None = None,
|
||||
) -> Client:
|
||||
"""Create client."""
|
||||
try:
|
||||
return await self._create_client(name, public_key, description, sources)
|
||||
except ClientManagementError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise BackendUnavailableError() from e
|
||||
|
||||
async def _update_client_public_key(
|
||||
self, name: str, new_key: str, password_manager: PasswordContext
|
||||
) -> list[str]:
|
||||
"""Update client public key."""
|
||||
LOG.info(
|
||||
"Updating client %s public key. This will invalidate all existing secrets."
|
||||
)
|
||||
client = await self.get_client(name)
|
||||
if not client:
|
||||
raise ClientNotFoundError()
|
||||
await self.backend.update_client_key(name, new_key)
|
||||
updated_secrets: list[str] = []
|
||||
for secret in client.secrets:
|
||||
LOG.debug("Re-encrypting secret %s for client %s", secret, name)
|
||||
secret_value = password_manager.get_secret(secret)
|
||||
if not secret_value:
|
||||
LOG.warning(
|
||||
"Referenced secret %s does not exist! Skipping.", secret_value
|
||||
)
|
||||
continue
|
||||
rsa_public_key = load_public_key(client.public_key.encode())
|
||||
encrypted = encrypt_string(secret_value, rsa_public_key)
|
||||
LOG.debug("Sending new encrypted value to backend.")
|
||||
await self.backend.create_client_secret(name, secret, encrypted)
|
||||
updated_secrets.append(secret)
|
||||
|
||||
return updated_secrets
|
||||
|
||||
async def update_client_public_key(self, name: str, new_key: str) -> list[str]:
|
||||
"""Update client public key."""
|
||||
try:
|
||||
with self.password_manager() as password_manager:
|
||||
return await self._update_client_public_key(
|
||||
name, new_key, password_manager
|
||||
)
|
||||
except ClientManagementError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise BackendUnavailableError() from e
|
||||
|
||||
async def _update_client(self, new_client: Client) -> Client:
|
||||
"""Update a client object."""
|
||||
existing_client = await self.get_client(new_client.name)
|
||||
if not existing_client:
|
||||
raise ClientNotFoundError()
|
||||
await self.backend.update_client(new_client)
|
||||
if new_client.public_key != existing_client.public_key:
|
||||
await self.update_client_public_key(new_client.name, new_client.public_key)
|
||||
|
||||
updated_client = await self.get_client(new_client.name)
|
||||
if not updated_client:
|
||||
raise ClientNotFoundError()
|
||||
return updated_client
|
||||
|
||||
async def update_client(self, new_client: Client) -> Client:
|
||||
"""Update a client object."""
|
||||
try:
|
||||
return await self._update_client(new_client)
|
||||
except ClientManagementError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise BackendUnavailableError() from e
|
||||
|
||||
async def update_client_sources(self, name: str, sources: list[str]) -> None:
|
||||
"""Update client sources."""
|
||||
try:
|
||||
await self.backend.update_client_sources(name, sources)
|
||||
except Exception as e:
|
||||
raise BackendUnavailableError() from e
|
||||
|
||||
async def _delete_client(self, name: str) -> None:
|
||||
"""Delete client."""
|
||||
await self.backend.delete_client(name)
|
||||
|
||||
async def delete_client(self, name: str) -> None:
|
||||
"""Delete client."""
|
||||
try:
|
||||
await self._delete_client(name)
|
||||
except ClientManagementError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise BackendUnavailableError() from e
|
||||
|
||||
async def delete_client_secret(self, client_name: str, secret_name: str) -> None:
|
||||
"""Delete a secret from a client."""
|
||||
try:
|
||||
await self.backend.delete_client_secret(client_name, secret_name)
|
||||
except Exception as e:
|
||||
raise BackendUnavailableError() from e
|
||||
|
||||
async def _get_secrets(self) -> list[Secret]:
|
||||
"""Get secrets.
|
||||
|
||||
This fetches the secret to client mapping from backend, and adds secrets from the password manager.
|
||||
"""
|
||||
with self.password_manager() as password_manager:
|
||||
all_secrets = password_manager.get_available_secrets()
|
||||
|
||||
secrets = await self.backend.get_secrets()
|
||||
backend_secret_names = [secret.name for secret in secrets]
|
||||
for secret in all_secrets:
|
||||
if secret not in backend_secret_names:
|
||||
secrets.append(Secret(name=secret, clients=[]))
|
||||
|
||||
return secrets
|
||||
|
||||
async def get_secrets(self) -> list[Secret]:
|
||||
"""Get secrets from backend."""
|
||||
try:
|
||||
return await self._get_secrets()
|
||||
except ClientManagementError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise BackendUnavailableError() from e
|
||||
|
||||
async def _get_detailed_secrets(self) -> list[DetailedSecrets]:
|
||||
"""Get detailed secrets.
|
||||
|
||||
This fetches the secret to client mapping from backend, and adds secrets from the password manager.
|
||||
"""
|
||||
with self.password_manager() as password_manager:
|
||||
all_secrets = password_manager.get_available_secrets()
|
||||
|
||||
secrets = await self.backend.get_detailed_secrets()
|
||||
backend_secret_names = [secret.name for secret in secrets]
|
||||
for secret in all_secrets:
|
||||
if secret not in backend_secret_names:
|
||||
secrets.append(DetailedSecrets(name=secret, ids=[], clients=[]))
|
||||
|
||||
return secrets
|
||||
|
||||
async def get_detailed_secrets(self) -> list[DetailedSecrets]:
|
||||
"""Get detailed secrets from backend."""
|
||||
try:
|
||||
return await self._get_detailed_secrets()
|
||||
except ClientManagementError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise BackendUnavailableError() from e
|
||||
|
||||
async def get_secret(self, name: str) -> SecretView | None:
|
||||
"""Get secrets from backend."""
|
||||
try:
|
||||
return await self._get_secret(name)
|
||||
except ClientManagementError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise BackendUnavailableError() from e
|
||||
|
||||
async def _get_secret(self, name: str) -> SecretView | None:
|
||||
"""Get a secret, including the actual unencrypted value and clients."""
|
||||
with self.password_manager() as password_manager:
|
||||
secret = password_manager.get_secret(name)
|
||||
|
||||
if not secret:
|
||||
return None
|
||||
secret_view = SecretView(name=name, secret=secret)
|
||||
secret_mapping = await self.backend.get_secret(name)
|
||||
if secret_mapping:
|
||||
secret_view.clients = secret_mapping.clients
|
||||
|
||||
return secret_view
|
||||
|
||||
async def delete_secret(self, name: str) -> None:
|
||||
"""Delete a secret."""
|
||||
try:
|
||||
return await self._delete_secret(name)
|
||||
except ClientManagementError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise BackendUnavailableError() from e
|
||||
|
||||
async def _delete_secret(self, name: str) -> None:
|
||||
"""Delete a secret."""
|
||||
with self.password_manager() as password_manager:
|
||||
password_manager.delete_entry(name)
|
||||
|
||||
secret_mapping = await self.backend.get_secret(name)
|
||||
if not secret_mapping:
|
||||
return
|
||||
for client in secret_mapping.clients:
|
||||
LOG.info("Deleting secret %s from client %s", name, client)
|
||||
await self.backend.delete_client_secret(client, name)
|
||||
|
||||
async def _add_secret(
|
||||
self, name: str, value: str, clients: list[str] | None, update: bool = False
|
||||
) -> None:
|
||||
"""Add a secret."""
|
||||
with self.password_manager() as password_manager:
|
||||
password_manager.add_entry(name, value, update)
|
||||
|
||||
if update:
|
||||
secret_map = await self.backend.get_secret(name)
|
||||
if secret_map:
|
||||
clients = secret_map.clients
|
||||
|
||||
if not clients:
|
||||
return
|
||||
for client_name in clients:
|
||||
client = await self.get_client(client_name)
|
||||
if not client:
|
||||
if update:
|
||||
raise ClientNotFoundError()
|
||||
LOG.warning("Requested client %s not found!", client_name)
|
||||
continue
|
||||
public_key = load_public_key(client.public_key.encode())
|
||||
encrypted = encrypt_string(value, public_key)
|
||||
LOG.info("Wrote encrypted secret for client %s", client_name)
|
||||
await self.backend.create_client_secret(client_name, name, encrypted)
|
||||
|
||||
async def add_secret(
|
||||
self, name: str, value: str, clients: list[str] | None = None
|
||||
) -> None:
|
||||
"""Add a secret."""
|
||||
try:
|
||||
await self._add_secret(name, value, clients)
|
||||
except ClientManagementError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise BackendUnavailableError() from e
|
||||
|
||||
async def update_secret(self, name: str, value: str) -> None:
|
||||
"""Update secrets."""
|
||||
try:
|
||||
await self._add_secret(name, value, None, True)
|
||||
except ClientManagementError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise BackendUnavailableError() from e
|
||||
|
||||
async def _create_client_secret(self, client_name: str, secret_name: str) -> None:
|
||||
"""Create client secret."""
|
||||
client = await self.get_client(client_name)
|
||||
if not client:
|
||||
raise ClientNotFoundError()
|
||||
|
||||
with self.password_manager() as password_manager:
|
||||
secret = password_manager.get_secret(secret_name)
|
||||
if not secret:
|
||||
raise SecretNotFoundError()
|
||||
|
||||
public_key = load_public_key(client.public_key.encode())
|
||||
encrypted = encrypt_string(secret, public_key)
|
||||
await self.backend.create_client_secret(client_name, secret_name, encrypted)
|
||||
|
||||
async def create_client_secret(self, client_name: str, secret_name: str) -> None:
|
||||
"""Create client secret."""
|
||||
try:
|
||||
await self._create_client_secret(client_name, secret_name)
|
||||
except ClientManagementError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise BackendUnavailableError() from e
|
||||
|
||||
@property
|
||||
def audit(self) -> AuditAPI:
|
||||
"""Resolve audit API."""
|
||||
return self.backend.audit(SubSystem.ADMIN)
|
||||
|
||||
async def get_audit_log(
|
||||
self,
|
||||
offset: int = 0,
|
||||
limit: int = 100,
|
||||
client_name: str | None = None,
|
||||
subsystem: str | None = None,
|
||||
) -> list[AuditLog]:
|
||||
"""Get audit log from backend."""
|
||||
return await self.audit.get(offset, limit, client_name, subsystem)
|
||||
|
||||
async def write_audit_message(
|
||||
self,
|
||||
operation: Operation,
|
||||
message: str,
|
||||
origin: str,
|
||||
client: Client | None = None,
|
||||
secret_name: str | None = None,
|
||||
**data: str,
|
||||
) -> None:
|
||||
"""Write an audit message."""
|
||||
await self.audit.write_async(
|
||||
operation=operation,
|
||||
message=message,
|
||||
origin=origin,
|
||||
client=client,
|
||||
secret=None,
|
||||
secret_name=secret_name,
|
||||
**data,
|
||||
)
|
||||
|
||||
async def write_audit_log(self, entry: AuditLog) -> None:
|
||||
"""Write to the audit log."""
|
||||
if not entry.subsystem:
|
||||
entry.subsystem = SubSystem.ADMIN
|
||||
|
||||
await self.audit.write_model_async(entry)
|
||||
#await self.backend.add_audit_log(entry)
|
||||
|
||||
async def get_audit_log_count(self) -> int:
|
||||
"""Get audit log count."""
|
||||
return await self.audit.count()
|
||||
116
packages/sshecret-admin/src/sshecret_admin/services/keepass.py
Normal file
116
packages/sshecret-admin/src/sshecret_admin/services/keepass.py
Normal file
@ -0,0 +1,116 @@
|
||||
"""Keepass password manager."""
|
||||
|
||||
import logging
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
import pykeepass
|
||||
from .master_password import decrypt_master_password
|
||||
from sshecret_admin.core.settings import AdminServerSettings
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
NO_USERNAME = "NO_USERNAME"
|
||||
|
||||
|
||||
DEFAULT_LOCATION = "keepass.kdbx"
|
||||
|
||||
|
||||
def create_password_db(location: Path, password: str) -> None:
|
||||
"""Create the password database."""
|
||||
LOG.info("Creating password database at %s", location)
|
||||
pykeepass.create_database(str(location.absolute()), password=password)
|
||||
|
||||
|
||||
class PasswordContext:
|
||||
"""Password Context class."""
|
||||
|
||||
def __init__(self, keepass: pykeepass.PyKeePass) -> None:
|
||||
"""Initialize password context."""
|
||||
self.keepass: pykeepass.PyKeePass = keepass
|
||||
|
||||
def add_entry(self, entry_name: str, secret: str, overwrite: bool = False) -> None:
|
||||
"""Add an entry.
|
||||
|
||||
Specify overwrite=True to overwrite the existing secret value, if it exists.
|
||||
"""
|
||||
entry = cast(
|
||||
"pykeepass.entry.Entry | None",
|
||||
self.keepass.find_entries(title=entry_name, first=True),
|
||||
)
|
||||
if entry and overwrite:
|
||||
entry.password = secret
|
||||
elif entry:
|
||||
raise ValueError("Error: A secret with this name already exists.")
|
||||
LOG.debug("Add secret entry to keepass: %s", entry_name)
|
||||
entry = self.keepass.add_entry(
|
||||
destination_group=self.keepass.root_group,
|
||||
title=entry_name,
|
||||
username=NO_USERNAME,
|
||||
password=secret,
|
||||
)
|
||||
self.keepass.save()
|
||||
|
||||
def get_secret(self, entry_name: str) -> str | None:
|
||||
"""Get the secret value."""
|
||||
entry = cast(
|
||||
"pykeepass.entry.Entry | None",
|
||||
self.keepass.find_entries(title=entry_name, first=True),
|
||||
)
|
||||
if not entry:
|
||||
return None
|
||||
|
||||
LOG.warning("Secret name %s accessed", entry_name)
|
||||
if password := cast(str, entry.password):
|
||||
return str(password)
|
||||
|
||||
raise RuntimeError(f"Cannot get password for entry {entry_name}")
|
||||
|
||||
def get_available_secrets(self) -> list[str]:
|
||||
"""Get the names of all secrets in the database."""
|
||||
entries = self.keepass.entries
|
||||
if not entries:
|
||||
return []
|
||||
return [str(entry.title) for entry in entries]
|
||||
|
||||
def delete_entry(self, entry_name: str) -> None:
|
||||
"""Delete entry."""
|
||||
entry = cast(
|
||||
"pykeepass.entry.Entry | None",
|
||||
self.keepass.find_entries(title=entry_name, first=True),
|
||||
)
|
||||
if not entry:
|
||||
return
|
||||
entry.delete()
|
||||
self.keepass.save()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _password_context(location: Path, password: str) -> Iterator[PasswordContext]:
|
||||
"""Open the password context."""
|
||||
database = pykeepass.PyKeePass(str(location.absolute()), password=password)
|
||||
context = PasswordContext(database)
|
||||
yield context
|
||||
|
||||
|
||||
@contextmanager
|
||||
def load_password_manager(
|
||||
settings: AdminServerSettings,
|
||||
encrypted_password: str,
|
||||
location: str = DEFAULT_LOCATION,
|
||||
) -> Iterator[PasswordContext]:
|
||||
"""Load password manager.
|
||||
|
||||
This function decrypts the password, and creates the password database if it
|
||||
has not yet been created.
|
||||
"""
|
||||
db_location = Path(location)
|
||||
password = decrypt_master_password(settings=settings, encrypted=encrypted_password)
|
||||
if not db_location.exists():
|
||||
create_password_db(db_location, password)
|
||||
|
||||
with _password_context(db_location, password) as context:
|
||||
yield context
|
||||
@ -0,0 +1,82 @@
|
||||
"""Functions related to handling the password database master password."""
|
||||
|
||||
import secrets
|
||||
from pathlib import Path
|
||||
from sshecret.crypto import (
|
||||
create_private_rsa_key,
|
||||
load_private_key,
|
||||
encrypt_string,
|
||||
decode_string,
|
||||
)
|
||||
from sshecret_admin.core.settings import AdminServerSettings
|
||||
|
||||
KEY_FILENAME = "sshecret-admin-key"
|
||||
|
||||
|
||||
def setup_master_password(
|
||||
settings: AdminServerSettings,
|
||||
filename: str = KEY_FILENAME,
|
||||
regenerate: bool = False,
|
||||
) -> str | None:
|
||||
"""Setup master password.
|
||||
|
||||
If regenerate is True, a new key will be generated.
|
||||
|
||||
This method should run just after setting up the database.
|
||||
"""
|
||||
created = _initial_key_setup(settings, filename, regenerate)
|
||||
if not created:
|
||||
return None
|
||||
|
||||
return _generate_master_password(settings, filename)
|
||||
|
||||
|
||||
def decrypt_master_password(
|
||||
settings: AdminServerSettings, encrypted: str, filename: str = KEY_FILENAME
|
||||
) -> str:
|
||||
"""Retrieve master password."""
|
||||
keyfile = Path(filename)
|
||||
if not keyfile.exists():
|
||||
raise RuntimeError("Error: Private key has not been generated yet.")
|
||||
|
||||
private_key = load_private_key(KEY_FILENAME, password=settings.secret_key)
|
||||
return decode_string(encrypted, private_key)
|
||||
|
||||
|
||||
def _generate_password() -> str:
|
||||
"""Generate a password."""
|
||||
return secrets.token_urlsafe(32)
|
||||
|
||||
|
||||
def _initial_key_setup(
|
||||
settings: AdminServerSettings,
|
||||
filename: str = KEY_FILENAME,
|
||||
regenerate: bool = False,
|
||||
) -> bool:
|
||||
"""Set up initial keys."""
|
||||
keyfile = Path(filename)
|
||||
|
||||
if keyfile.exists() and not regenerate:
|
||||
return False
|
||||
|
||||
assert settings.secret_key is not None, (
|
||||
"Error: Could not load a secret key from environment."
|
||||
)
|
||||
create_private_rsa_key(keyfile, password=settings.secret_key)
|
||||
return True
|
||||
|
||||
|
||||
def _generate_master_password(
|
||||
settings: AdminServerSettings, filename: str = KEY_FILENAME
|
||||
) -> str:
|
||||
"""Generate master password for password database.
|
||||
|
||||
Returns the encrypted string, base64 encoded.
|
||||
"""
|
||||
keyfile = Path(filename)
|
||||
if not keyfile.exists():
|
||||
raise RuntimeError("Error: Private key has not been generated yet.")
|
||||
private_key = load_private_key(filename, password=settings.secret_key)
|
||||
public_key = private_key.public_key()
|
||||
master_password = _generate_password()
|
||||
return encrypt_string(master_password, public_key)
|
||||
112
packages/sshecret-admin/src/sshecret_admin/services/models.py
Normal file
112
packages/sshecret-admin/src/sshecret_admin/services/models.py
Normal file
@ -0,0 +1,112 @@
|
||||
"""Models for the API."""
|
||||
|
||||
import secrets
|
||||
from typing import Annotated, Literal
|
||||
from pydantic import (
|
||||
AfterValidator,
|
||||
BaseModel,
|
||||
ConfigDict,
|
||||
Field,
|
||||
IPvAnyAddress,
|
||||
IPvAnyNetwork,
|
||||
)
|
||||
from sshecret.crypto import validate_public_key
|
||||
|
||||
|
||||
def public_key_validator(value: str) -> str:
|
||||
"""Public key validator."""
|
||||
if validate_public_key(value):
|
||||
return value
|
||||
raise ValueError("Error: Public key must be a valid RSA public key.")
|
||||
|
||||
|
||||
class SecretListView(BaseModel):
|
||||
"""Model containing a list of all available secrets."""
|
||||
|
||||
name: str
|
||||
clients: list[str] = Field(default_factory=list) # Clients that have access to it.
|
||||
|
||||
|
||||
class SecretView(BaseModel):
|
||||
"""Model containing a secret, including its clear-text value."""
|
||||
|
||||
name: str
|
||||
secret: str
|
||||
clients: list[str] = Field(default_factory=list) # Clients that have access to it.
|
||||
|
||||
|
||||
class UpdateKeyModel(BaseModel):
|
||||
"""Model for updating client public key."""
|
||||
|
||||
public_key: Annotated[str, AfterValidator(public_key_validator)]
|
||||
|
||||
|
||||
class UpdateKeyResponse(BaseModel):
|
||||
"""Response model after updating the public key."""
|
||||
|
||||
public_key: str
|
||||
updated_secrets: list[str] = Field(default_factory=list)
|
||||
detail: str | None = None
|
||||
|
||||
|
||||
class UpdatePoliciesRequest(BaseModel):
|
||||
"""Update policy request."""
|
||||
|
||||
sources: list[IPvAnyAddress | IPvAnyNetwork]
|
||||
|
||||
|
||||
class ClientCreate(BaseModel):
|
||||
"""Model to create a client."""
|
||||
|
||||
name: str
|
||||
public_key: Annotated[str, AfterValidator(public_key_validator)]
|
||||
sources: list[IPvAnyAddress | IPvAnyNetwork] = Field(default_factory=list)
|
||||
|
||||
|
||||
class AutoGenerateOpts(BaseModel):
|
||||
"""Option to auto-generate a password."""
|
||||
|
||||
auto_generate: Literal[True]
|
||||
length: int = 32
|
||||
|
||||
|
||||
class SecretUpdate(BaseModel):
|
||||
"""Model to update a secret."""
|
||||
|
||||
value: str | AutoGenerateOpts = Field(
|
||||
description="Secret as string value or auto-generated with optional length",
|
||||
examples=["MySecretString", {"auto_generate": True, "length": 32}]
|
||||
)
|
||||
|
||||
def get_secret(self) -> str:
|
||||
"""Get secret.
|
||||
|
||||
This returns the specified one, or generates one according to auto-generation.
|
||||
"""
|
||||
if isinstance(self.value, str):
|
||||
return self.value
|
||||
secret = secrets.token_urlsafe(self.value.length)
|
||||
return secret
|
||||
|
||||
|
||||
class SecretCreate(SecretUpdate):
|
||||
"""Model to create a secret."""
|
||||
|
||||
name: str
|
||||
clients: list[str] | None = Field(default=None, description="Assign the secret to a list of clients.")
|
||||
|
||||
model_config: ConfigDict = ConfigDict(
|
||||
json_schema_extra={
|
||||
"examples": [
|
||||
{
|
||||
"name": "MySecret",
|
||||
"clients": ["client-1", "client-2"],
|
||||
"value": { "auto_generate": True, "length": 32 }
|
||||
},
|
||||
{
|
||||
"name": "MySecret",
|
||||
"value": "mysecretstring",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user