Improve the admin API
This commit is contained in:
@ -6,7 +6,7 @@ Since we have a frontend and a REST API, it makes sense to have a generic librar
|
||||
import logging
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Unpack
|
||||
from typing import Literal, Unpack
|
||||
|
||||
from sshecret.backend import (
|
||||
AuditLog,
|
||||
@ -68,6 +68,7 @@ def add_clients_to_secret_group(
|
||||
if parent:
|
||||
parent_ref = parent.reference()
|
||||
client_secret_group = ClientSecretGroup(
|
||||
id=group.id,
|
||||
group_name=group.name,
|
||||
path=group.path,
|
||||
description=group.description,
|
||||
@ -397,13 +398,15 @@ class AdminBackend:
|
||||
await password_manager.set_group_description(group_name, description)
|
||||
|
||||
async def delete_secret_group(self, group_path: str) -> None:
|
||||
"""Delete a group.
|
||||
|
||||
If keep_entries is set to False, all entries in the group will be deleted.
|
||||
"""
|
||||
"""Delete a group."""
|
||||
async with self.secrets_manager() as password_manager:
|
||||
await password_manager.delete_group(group_path)
|
||||
|
||||
async def delete_secret_group_by_id(self, id: str) -> None:
|
||||
"""Delete a secret group by ID."""
|
||||
async with self.secrets_manager() as password_manager:
|
||||
await password_manager.delete_group_id(id)
|
||||
|
||||
async def get_secret_groups(
|
||||
self,
|
||||
group_filter: str | None = None,
|
||||
@ -562,6 +565,7 @@ class AdminBackend:
|
||||
clients: list[str] | None,
|
||||
update: bool = False,
|
||||
group: str | None = None,
|
||||
distinguisher: Literal["name", "id"] = "name",
|
||||
) -> None:
|
||||
"""Add a secret."""
|
||||
async with self.secrets_manager() as password_manager:
|
||||
@ -575,7 +579,11 @@ class AdminBackend:
|
||||
if not clients:
|
||||
return
|
||||
for client_name in clients:
|
||||
client = await self.get_client(client_name)
|
||||
client_id = client_name
|
||||
if distinguisher == "id":
|
||||
client_id = ("id", client_name)
|
||||
|
||||
client = await self.get_client(client_id)
|
||||
if not client:
|
||||
if update:
|
||||
raise ClientNotFoundError()
|
||||
@ -583,8 +591,8 @@ class AdminBackend:
|
||||
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)
|
||||
LOG.info("Wrote encrypted secret for client %r", client_id)
|
||||
await self.backend.create_client_secret(client_id, name, encrypted)
|
||||
|
||||
async def add_secret(
|
||||
self,
|
||||
@ -592,10 +600,17 @@ class AdminBackend:
|
||||
value: str,
|
||||
clients: list[str] | None = None,
|
||||
group: str | None = None,
|
||||
distinguisher: Literal["name", "id"] = "name",
|
||||
) -> None:
|
||||
"""Add a secret."""
|
||||
try:
|
||||
await self._add_secret(name=name, value=value, clients=clients, group=group)
|
||||
await self._add_secret(
|
||||
name=name,
|
||||
value=value,
|
||||
clients=clients,
|
||||
group=group,
|
||||
distinguisher=distinguisher,
|
||||
)
|
||||
except ClientManagementError:
|
||||
raise
|
||||
except Exception as e:
|
||||
|
||||
@ -10,10 +10,11 @@ from pydantic import (
|
||||
Field,
|
||||
IPvAnyAddress,
|
||||
IPvAnyNetwork,
|
||||
field_validator,
|
||||
model_validator,
|
||||
)
|
||||
from sshecret.crypto import validate_public_key
|
||||
from sshecret.backend.models import ClientReference
|
||||
from sshecret.backend.models import AuditFilter, ClientReference
|
||||
|
||||
|
||||
def public_key_validator(value: str) -> str:
|
||||
@ -104,6 +105,7 @@ class SecretCreate(SecretUpdate):
|
||||
clients: list[str] | None = Field(
|
||||
default=None, description="Assign the secret to a list of clients."
|
||||
)
|
||||
client_distinguisher: Literal["id", "name"] = "name"
|
||||
group: str | None = None
|
||||
|
||||
model_config: ConfigDict = ConfigDict(
|
||||
@ -128,6 +130,7 @@ class SecretCreate(SecretUpdate):
|
||||
class SecretGroup(BaseModel):
|
||||
"""A secret group."""
|
||||
|
||||
id: uuid.UUID
|
||||
name: str
|
||||
path: str
|
||||
description: str | None = None
|
||||
@ -158,6 +161,7 @@ class GroupReference(BaseModel):
|
||||
class ClientSecretGroup(BaseModel):
|
||||
"""Client secrets grouped."""
|
||||
|
||||
id: uuid.UUID
|
||||
group_name: str
|
||||
path: str
|
||||
description: str | None = None
|
||||
@ -173,7 +177,7 @@ class ClientSecretGroup(BaseModel):
|
||||
class SecretGroupCreate(BaseModel):
|
||||
"""Create model for creating secret groups."""
|
||||
|
||||
name: str
|
||||
name: str = Field(min_length=1) # blank group names are a pain!
|
||||
description: str | None = None
|
||||
parent_group: str | None = None
|
||||
|
||||
@ -185,6 +189,17 @@ class SecretGroupUdate(BaseModel):
|
||||
description: str | None = None
|
||||
parent_group: str | None = None
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def validate_name(cls, value: str | None) -> str | None:
|
||||
"""Validate name."""
|
||||
if not value:
|
||||
return None
|
||||
if "/" in value:
|
||||
raise ValueError("Name cannot be a path")
|
||||
|
||||
return value
|
||||
|
||||
|
||||
class ClientSecretGroupList(BaseModel):
|
||||
"""Secret group list."""
|
||||
@ -235,3 +250,10 @@ class GroupPath(BaseModel):
|
||||
"""Path to a group."""
|
||||
|
||||
path: str = Field(pattern="^/.*")
|
||||
|
||||
|
||||
class AuditQueryFilter(AuditFilter):
|
||||
"""Audit query filter."""
|
||||
|
||||
offset: int = 0
|
||||
limit: int = 100
|
||||
|
||||
@ -215,7 +215,7 @@ class AsyncSecretContext:
|
||||
|
||||
path = os.path.join(path, group.name)
|
||||
secret_group = SecretGroup(
|
||||
name=group.name, path=path, description=group.description
|
||||
id=group.id, name=group.name, path=path, description=group.description
|
||||
)
|
||||
group_secrets = await self._get_group_secrets(group)
|
||||
for secret in group_secrets:
|
||||
@ -715,6 +715,17 @@ class AsyncSecretContext:
|
||||
# We don't audit-log this operation currently, even though it indirectly
|
||||
# may affect secrets.
|
||||
|
||||
async def delete_group_id(self, id: str | uuid.UUID) -> None:
|
||||
"""Delete a group by ID."""
|
||||
if isinstance(id, str):
|
||||
id = uuid.UUID(id)
|
||||
group = await self._get_group_by_id(id)
|
||||
if not group:
|
||||
raise InvalidGroupNameError("Invalid or non-existing group ID.")
|
||||
await self.session.delete(group)
|
||||
|
||||
await self.session.commit()
|
||||
|
||||
async def _export_entries(self) -> list[SecretDataEntryExport]:
|
||||
"""Export entries as a pydantic object."""
|
||||
statement = (
|
||||
|
||||
Reference in New Issue
Block a user