Integrate group in admin rest API
This commit is contained in:
@ -23,7 +23,7 @@ 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
|
||||
from .models import ClientSecretGroup, SecretClientMapping, SecretGroup, SecretView
|
||||
|
||||
|
||||
class ClientManagementError(Exception):
|
||||
@ -45,6 +45,38 @@ class BackendUnavailableError(ClientManagementError):
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def add_clients_to_secret_group(
|
||||
group: SecretGroup,
|
||||
client_secret_mapping: dict[str, DetailedSecrets],
|
||||
parent: ClientSecretGroup | None = None,
|
||||
) -> ClientSecretGroup:
|
||||
"""Add client information to a secret group."""
|
||||
client_secret_group = ClientSecretGroup(
|
||||
group_name=group.name,
|
||||
path=group.path,
|
||||
description=group.description,
|
||||
parent_group=parent,
|
||||
)
|
||||
for entry in group.entries:
|
||||
secret_entries = SecretClientMapping(name=entry)
|
||||
if details := client_secret_mapping.get(entry):
|
||||
secret_entries.clients = details.clients
|
||||
client_secret_group.entries.append(secret_entries)
|
||||
for subgroup in group.children:
|
||||
client_secret_group.children.append(
|
||||
add_clients_to_secret_group(
|
||||
subgroup, client_secret_mapping, client_secret_group
|
||||
)
|
||||
)
|
||||
# We'll save a bit of memory and complexity by just adding the name of the parent, if available.
|
||||
if not parent and group.parent_group:
|
||||
client_secret_group.parent_group = ClientSecretGroup(
|
||||
group_name=group.parent_group.name,
|
||||
path=group.parent_group.path,
|
||||
)
|
||||
return client_secret_group
|
||||
|
||||
|
||||
class AdminBackend:
|
||||
"""Admin backend API."""
|
||||
|
||||
@ -277,6 +309,74 @@ class AdminBackend:
|
||||
except Exception as e:
|
||||
raise BackendUnavailableError() from e
|
||||
|
||||
async def add_secret_group(
|
||||
self,
|
||||
group_name: str,
|
||||
description: str | None = None,
|
||||
parent_group: str | None = None,
|
||||
) -> None:
|
||||
"""Add secret group."""
|
||||
with self.password_manager() as password_manager:
|
||||
password_manager.add_group(group_name, description, parent_group)
|
||||
|
||||
async def set_secret_group(self, secret_name: str, group_name: str | None) -> None:
|
||||
"""Assign a group to a secret."""
|
||||
with self.password_manager() as password_manager:
|
||||
password_manager.set_secret_group(secret_name, group_name)
|
||||
|
||||
async def move_secret_group(
|
||||
self, group_name: str, parent_group: str | None
|
||||
) -> None:
|
||||
"""Move a group.
|
||||
|
||||
If parent_group is None, it will be moved to the root.
|
||||
"""
|
||||
with self.password_manager() as password_manager:
|
||||
password_manager.move_group(group_name, parent_group)
|
||||
|
||||
async def set_group_description(self, group_name: str, description: str) -> None:
|
||||
"""Set a group description."""
|
||||
with self.password_manager() as password_manager:
|
||||
password_manager.set_group_description(group_name, description)
|
||||
|
||||
async def delete_secret_group(
|
||||
self, group_name: str, keep_entries: bool = True
|
||||
) -> None:
|
||||
"""Delete a group.
|
||||
|
||||
If keep_entries is set to False, all entries in the group will be deleted.
|
||||
"""
|
||||
with self.password_manager() as password_manager:
|
||||
password_manager.delete_group(group_name, keep_entries)
|
||||
|
||||
async def get_secret_groups(
|
||||
self,
|
||||
group_filter: str | None = None,
|
||||
regex: bool = True,
|
||||
) -> list[ClientSecretGroup]:
|
||||
"""Get secret groups.
|
||||
|
||||
The starting group can be filtered with the group_name argument, which
|
||||
may be a regular expression.
|
||||
"""
|
||||
all_secrets = await self.backend.get_detailed_secrets()
|
||||
secrets_mapping = {secret.name: secret for secret in all_secrets}
|
||||
with self.password_manager() as password_manager:
|
||||
all_groups = password_manager.get_secret_groups(group_filter, regex=regex)
|
||||
|
||||
result: list[ClientSecretGroup] = []
|
||||
for group in all_groups:
|
||||
# We have to do this recursively.
|
||||
result.append(add_clients_to_secret_group(group, secrets_mapping))
|
||||
return result
|
||||
|
||||
async def get_secret_group(self, name: str) -> ClientSecretGroup | None:
|
||||
"""Get a single secret group by name."""
|
||||
matches = await self.get_secret_groups(group_filter=name, regex=False)
|
||||
if not matches:
|
||||
return None
|
||||
return matches[0]
|
||||
|
||||
async def get_secret(self, name: str) -> SecretView | None:
|
||||
"""Get secrets from backend."""
|
||||
try:
|
||||
@ -322,11 +422,16 @@ class AdminBackend:
|
||||
await self.backend.delete_client_secret(client, name)
|
||||
|
||||
async def _add_secret(
|
||||
self, name: str, value: str, clients: list[str] | None, update: bool = False
|
||||
self,
|
||||
name: str,
|
||||
value: str,
|
||||
clients: list[str] | None,
|
||||
update: bool = False,
|
||||
group: str | None = None,
|
||||
) -> None:
|
||||
"""Add a secret."""
|
||||
with self.password_manager() as password_manager:
|
||||
password_manager.add_entry(name, value, update)
|
||||
password_manager.add_entry(name, value, update, group_name=group)
|
||||
|
||||
if update:
|
||||
secret_map = await self.backend.get_secret(name)
|
||||
@ -348,11 +453,15 @@ class AdminBackend:
|
||||
await self.backend.create_client_secret(client_name, name, encrypted)
|
||||
|
||||
async def add_secret(
|
||||
self, name: str, value: str, clients: list[str] | None = None
|
||||
self,
|
||||
name: str,
|
||||
value: str,
|
||||
clients: list[str] | None = None,
|
||||
group: str | None = None,
|
||||
) -> None:
|
||||
"""Add a secret."""
|
||||
try:
|
||||
await self._add_secret(name, value, clients)
|
||||
await self._add_secret(name=name, value=value, clients=clients, group=group)
|
||||
except ClientManagementError:
|
||||
raise
|
||||
except Exception as e:
|
||||
|
||||
@ -32,15 +32,29 @@ def create_password_db(location: Path, password: str) -> None:
|
||||
|
||||
|
||||
def _kp_group_to_secret_group(
|
||||
kp_group: pykeepass.group.Group, parent: SecretGroup | None = None
|
||||
kp_group: pykeepass.group.Group, parent: SecretGroup | None = None, depth: int | None = None
|
||||
) -> SecretGroup:
|
||||
"""Convert keepass group to secret group dataclass."""
|
||||
group_name = cast(str, kp_group.name)
|
||||
group = SecretGroup(name=group_name, description=kp_group.notes)
|
||||
path = "/".join(cast(list[str], kp_group.path))
|
||||
group = SecretGroup(name=group_name, path=path, description=kp_group.notes)
|
||||
for entry in kp_group.entries:
|
||||
group.entries.append(str(entry.title))
|
||||
if parent:
|
||||
group.parent_group = parent
|
||||
|
||||
current_depth = len(kp_group.path)
|
||||
|
||||
if not parent and current_depth > 1:
|
||||
parent = _kp_group_to_secret_group(kp_group.parentgroup, depth=current_depth)
|
||||
parent.children.append(group)
|
||||
group.parent_group = parent
|
||||
|
||||
if depth and depth == current_depth:
|
||||
return group
|
||||
|
||||
for subgroup in kp_group.subgroups:
|
||||
group.children.append(_kp_group_to_secret_group(subgroup, group))
|
||||
group.children.append(_kp_group_to_secret_group(subgroup, group, depth=depth))
|
||||
|
||||
return group
|
||||
|
||||
@ -120,7 +134,7 @@ class PasswordContext:
|
||||
|
||||
raise RuntimeError(f"Cannot get password for entry {entry_name}")
|
||||
|
||||
def get_secret_groups(self, pattern: str | None = None) -> list[SecretGroup]:
|
||||
def get_secret_groups(self, pattern: str | None = None, regex: bool = True) -> list[SecretGroup]:
|
||||
"""Get secret groups.
|
||||
|
||||
A regex pattern may be provided to filter groups.
|
||||
@ -128,7 +142,7 @@ class PasswordContext:
|
||||
if pattern:
|
||||
groups = cast(
|
||||
list[pykeepass.group.Group],
|
||||
self.keepass.find_groups(name=pattern, regex=True),
|
||||
self.keepass.find_groups(name=pattern, regex=regex),
|
||||
)
|
||||
else:
|
||||
all_groups = cast(list[pykeepass.group.Group], self.keepass.groups)
|
||||
@ -153,7 +167,7 @@ class PasswordContext:
|
||||
f"Error: Cannot find a parent group named {parent_group}"
|
||||
)
|
||||
kp_parent_group = query
|
||||
self.keepass.add_group(kp_parent_group, name, notes=description)
|
||||
self.keepass.add_group(destination_group=kp_parent_group, group_name=name, notes=description)
|
||||
self.keepass.save()
|
||||
|
||||
def set_group_description(self, name: str, description: str) -> None:
|
||||
|
||||
@ -11,6 +11,7 @@ from pydantic import (
|
||||
IPvAnyNetwork,
|
||||
)
|
||||
from sshecret.crypto import validate_public_key
|
||||
from sshecret.backend.models import ClientReference
|
||||
|
||||
|
||||
def public_key_validator(value: str) -> str:
|
||||
@ -96,6 +97,7 @@ class SecretCreate(SecretUpdate):
|
||||
clients: list[str] | None = Field(
|
||||
default=None, description="Assign the secret to a list of clients."
|
||||
)
|
||||
group: str | None = None
|
||||
|
||||
model_config: ConfigDict = ConfigDict(
|
||||
json_schema_extra={
|
||||
@ -107,6 +109,7 @@ class SecretCreate(SecretUpdate):
|
||||
},
|
||||
{
|
||||
"name": "MySecret",
|
||||
"group": "MySecretGroup",
|
||||
"value": "mysecretstring",
|
||||
},
|
||||
]
|
||||
@ -118,6 +121,34 @@ class SecretGroup(BaseModel):
|
||||
"""A secret group."""
|
||||
|
||||
name: str
|
||||
path: str
|
||||
description: str | None = None
|
||||
parent_group: "SecretGroup | None" = None
|
||||
children: list["SecretGroup"] = Field(default_factory=list)
|
||||
entries: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SecretClientMapping(BaseModel):
|
||||
"""Secret name with list of clients."""
|
||||
|
||||
name: str # name of secret
|
||||
clients: list[ClientReference] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ClientSecretGroup(BaseModel):
|
||||
"""Client secrets grouped."""
|
||||
|
||||
group_name: str
|
||||
path: str
|
||||
description: str | None = None
|
||||
parent_group: "ClientSecretGroup | None" = None
|
||||
children: list["ClientSecretGroup"] = Field(default_factory=list)
|
||||
entries: list[SecretClientMapping] = Field(default_factory=list)
|
||||
|
||||
|
||||
class SecretGroupCreate(BaseModel):
|
||||
"""Create model for creating secret groups."""
|
||||
|
||||
name: str
|
||||
description: str | None = None
|
||||
parent_group: str | None = None
|
||||
|
||||
Reference in New Issue
Block a user