Integrate group in admin rest API
This commit is contained in:
@ -3,13 +3,15 @@
|
|||||||
# pyright: reportUnusedFunction=false
|
# pyright: reportUnusedFunction=false
|
||||||
import logging
|
import logging
|
||||||
from typing import Annotated
|
from typing import Annotated
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status
|
from fastapi import APIRouter, Depends, HTTPException, Query, Security, status
|
||||||
|
|
||||||
from sshecret.backend.models import Secret
|
from sshecret.backend.models import Secret
|
||||||
from sshecret_admin.core.dependencies import AdminDependencies
|
from sshecret_admin.core.dependencies import AdminDependencies
|
||||||
from sshecret_admin.services import AdminBackend
|
from sshecret_admin.services import AdminBackend
|
||||||
from sshecret_admin.services.models import (
|
from sshecret_admin.services.models import (
|
||||||
|
ClientSecretGroup,
|
||||||
SecretCreate,
|
SecretCreate,
|
||||||
|
SecretGroupCreate,
|
||||||
SecretUpdate,
|
SecretUpdate,
|
||||||
SecretView,
|
SecretView,
|
||||||
)
|
)
|
||||||
@ -19,7 +21,7 @@ LOG = logging.getLogger(__name__)
|
|||||||
|
|
||||||
def create_router(dependencies: AdminDependencies) -> APIRouter:
|
def create_router(dependencies: AdminDependencies) -> APIRouter:
|
||||||
"""Create secrets router."""
|
"""Create secrets router."""
|
||||||
app = APIRouter(dependencies=[Depends(dependencies.get_current_active_user)])
|
app = APIRouter(dependencies=[Security(dependencies.get_current_active_user)])
|
||||||
|
|
||||||
@app.get("/secrets/")
|
@app.get("/secrets/")
|
||||||
async def get_secret_names(
|
async def get_secret_names(
|
||||||
@ -34,7 +36,12 @@ def create_router(dependencies: AdminDependencies) -> APIRouter:
|
|||||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Create a secret."""
|
"""Create a secret."""
|
||||||
await admin.add_secret(secret.name, secret.get_secret(), secret.clients)
|
await admin.add_secret(
|
||||||
|
name=secret.name,
|
||||||
|
value=secret.get_secret(),
|
||||||
|
clients=secret.clients,
|
||||||
|
group=secret.group,
|
||||||
|
)
|
||||||
|
|
||||||
@app.get("/secrets/{name}")
|
@app.get("/secrets/{name}")
|
||||||
async def get_secret(
|
async def get_secret(
|
||||||
@ -67,4 +74,133 @@ def create_router(dependencies: AdminDependencies) -> APIRouter:
|
|||||||
"""Delete secret."""
|
"""Delete secret."""
|
||||||
await admin.delete_secret(name)
|
await admin.delete_secret(name)
|
||||||
|
|
||||||
|
@app.get("/secrets/groups/")
|
||||||
|
async def get_secret_groups(
|
||||||
|
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||||
|
filter_regex: Annotated[str | None, Query()] = None,
|
||||||
|
) -> list[ClientSecretGroup]:
|
||||||
|
"""Get secret groups."""
|
||||||
|
return await admin.get_secret_groups(filter_regex)
|
||||||
|
|
||||||
|
@app.get("/secrets/groups/{group_name}/")
|
||||||
|
async def get_secret_group(
|
||||||
|
group_name: str,
|
||||||
|
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||||
|
) -> ClientSecretGroup:
|
||||||
|
"""Get a specific secret group."""
|
||||||
|
results = await admin.get_secret_groups(group_name, False)
|
||||||
|
if not results:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail="No such group."
|
||||||
|
)
|
||||||
|
return results[0]
|
||||||
|
|
||||||
|
@app.post("/secrets/groups/")
|
||||||
|
async def add_secret_group(
|
||||||
|
group: SecretGroupCreate,
|
||||||
|
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||||
|
) -> ClientSecretGroup:
|
||||||
|
"""Create a secret grouping."""
|
||||||
|
await admin.add_secret_group(
|
||||||
|
group_name=group.name,
|
||||||
|
description=group.description,
|
||||||
|
parent_group=group.parent_group,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await admin.get_secret_group(group.name)
|
||||||
|
if not result:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST, detail="Group creation failed"
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
@app.delete("/secrets/groups/{group_name}/")
|
||||||
|
async def delete_secret_group(
|
||||||
|
group_name: str,
|
||||||
|
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||||
|
) -> None:
|
||||||
|
"""Remove a group.
|
||||||
|
|
||||||
|
Entries within the group will be moved to the root.
|
||||||
|
This also includes nested entries further down from the group.
|
||||||
|
"""
|
||||||
|
group = await admin.get_secret_group(group.name)
|
||||||
|
if not group:
|
||||||
|
return
|
||||||
|
await admin.delete_secret_group(group_name, keep_entries=True)
|
||||||
|
|
||||||
|
@app.post("/secrets/groups/{group_name}/{secret_name}")
|
||||||
|
async def move_secret_to_group(
|
||||||
|
group_name: str,
|
||||||
|
secret_name: str,
|
||||||
|
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||||
|
) -> None:
|
||||||
|
"""Move a secret to a group."""
|
||||||
|
groups = await admin.get_secret_groups(group_name, False)
|
||||||
|
if not groups:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail="No such group."
|
||||||
|
)
|
||||||
|
|
||||||
|
await admin.set_secret_group(secret_name, group_name)
|
||||||
|
|
||||||
|
@app.post("/secrets/group/{group_name}/parent/{parent_name}")
|
||||||
|
async def move_group(
|
||||||
|
group_name: str,
|
||||||
|
parent_name: str,
|
||||||
|
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||||
|
) -> None:
|
||||||
|
"""Move a group."""
|
||||||
|
group = await admin.get_secret_group(group_name)
|
||||||
|
if not group:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"No such group {group_name}",
|
||||||
|
)
|
||||||
|
parent_group = await admin.get_secret_group(parent_name)
|
||||||
|
if not parent_group:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"No such group {parent_name}",
|
||||||
|
)
|
||||||
|
await admin.move_secret_group(group_name, parent_name)
|
||||||
|
|
||||||
|
@app.delete("/secrets/group/{group_name}/parent/")
|
||||||
|
async def move_group_to_root(
|
||||||
|
group_name: str,
|
||||||
|
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||||
|
) -> None:
|
||||||
|
"""Move a group to the root."""
|
||||||
|
group = await admin.get_secret_group(group_name)
|
||||||
|
if not group:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"No such group {group_name}",
|
||||||
|
)
|
||||||
|
|
||||||
|
await admin.move_secret_group(group_name, None)
|
||||||
|
|
||||||
|
@app.delete("/secrets/groups/{group_name}/{secret_name}")
|
||||||
|
async def remove_secret_from_group(
|
||||||
|
group_name: str,
|
||||||
|
secret_name: str,
|
||||||
|
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||||
|
) -> None:
|
||||||
|
"""Remove a secret from a group.
|
||||||
|
|
||||||
|
Secret will be moved to the root group.
|
||||||
|
"""
|
||||||
|
groups = await admin.get_secret_groups(group_name, False)
|
||||||
|
if not groups:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND, detail="No such group."
|
||||||
|
)
|
||||||
|
group = groups[0]
|
||||||
|
matching_entries = [
|
||||||
|
entry for entry in group.entries if entry.name == secret_name
|
||||||
|
]
|
||||||
|
if not matching_entries:
|
||||||
|
return
|
||||||
|
await admin.set_secret_group(secret_name, None)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|||||||
@ -26,7 +26,7 @@ API_VERSION = "v1"
|
|||||||
def create_router(dependencies: BaseDependencies) -> APIRouter:
|
def create_router(dependencies: BaseDependencies) -> APIRouter:
|
||||||
"""Create clients router."""
|
"""Create clients router."""
|
||||||
|
|
||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/token")
|
||||||
|
|
||||||
async def get_current_user(
|
async def get_current_user(
|
||||||
token: Annotated[str, Depends(oauth2_scheme)],
|
token: Annotated[str, Depends(oauth2_scheme)],
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
<script src="https://cdn.jsdelivr.net/npm/simple-datatables@9.0.3"></script>
|
<script src="https://cdn.jsdelivr.net/npm/simple-datatables@9.0.3"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/flowbite@3.1.2/dist/flowbite.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/flowbite@3.1.2/dist/flowbite.min.js"></script>
|
||||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||||
|
<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.20.1/cdn/shoelace-autoloader.js"></script>
|
||||||
<script type="text/javascript" src="{{ url_for('static', path="js/prism.js") }}"></script>
|
<script type="text/javascript" src="{{ url_for('static', path="js/prism.js") }}"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@ -19,3 +19,14 @@
|
|||||||
href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css"
|
href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
media="(prefers-color-scheme:light)"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.20.1/cdn/themes/light.css"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
media="(prefers-color-scheme:dark)"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.20.1/cdn/themes/dark.css"
|
||||||
|
onload="document.documentElement.classList.add('sl-theme-dark');"
|
||||||
|
/>
|
||||||
|
|||||||
@ -23,7 +23,7 @@ from sshecret.crypto import encrypt_string, load_public_key
|
|||||||
|
|
||||||
from .keepass import PasswordContext, load_password_manager
|
from .keepass import PasswordContext, load_password_manager
|
||||||
from sshecret_admin.core.settings import AdminServerSettings
|
from sshecret_admin.core.settings import AdminServerSettings
|
||||||
from .models import SecretView
|
from .models import ClientSecretGroup, SecretClientMapping, SecretGroup, SecretView
|
||||||
|
|
||||||
|
|
||||||
class ClientManagementError(Exception):
|
class ClientManagementError(Exception):
|
||||||
@ -45,6 +45,38 @@ class BackendUnavailableError(ClientManagementError):
|
|||||||
LOG = logging.getLogger(__name__)
|
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:
|
class AdminBackend:
|
||||||
"""Admin backend API."""
|
"""Admin backend API."""
|
||||||
|
|
||||||
@ -277,6 +309,74 @@ class AdminBackend:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise BackendUnavailableError() from 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:
|
async def get_secret(self, name: str) -> SecretView | None:
|
||||||
"""Get secrets from backend."""
|
"""Get secrets from backend."""
|
||||||
try:
|
try:
|
||||||
@ -322,11 +422,16 @@ class AdminBackend:
|
|||||||
await self.backend.delete_client_secret(client, name)
|
await self.backend.delete_client_secret(client, name)
|
||||||
|
|
||||||
async def _add_secret(
|
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:
|
) -> None:
|
||||||
"""Add a secret."""
|
"""Add a secret."""
|
||||||
with self.password_manager() as password_manager:
|
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:
|
if update:
|
||||||
secret_map = await self.backend.get_secret(name)
|
secret_map = await self.backend.get_secret(name)
|
||||||
@ -348,11 +453,15 @@ class AdminBackend:
|
|||||||
await self.backend.create_client_secret(client_name, name, encrypted)
|
await self.backend.create_client_secret(client_name, name, encrypted)
|
||||||
|
|
||||||
async def add_secret(
|
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:
|
) -> None:
|
||||||
"""Add a secret."""
|
"""Add a secret."""
|
||||||
try:
|
try:
|
||||||
await self._add_secret(name, value, clients)
|
await self._add_secret(name=name, value=value, clients=clients, group=group)
|
||||||
except ClientManagementError:
|
except ClientManagementError:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@ -32,15 +32,29 @@ def create_password_db(location: Path, password: str) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _kp_group_to_secret_group(
|
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:
|
) -> SecretGroup:
|
||||||
"""Convert keepass group to secret group dataclass."""
|
"""Convert keepass group to secret group dataclass."""
|
||||||
group_name = cast(str, kp_group.name)
|
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:
|
if parent:
|
||||||
group.parent_group = 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:
|
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
|
return group
|
||||||
|
|
||||||
@ -120,7 +134,7 @@ class PasswordContext:
|
|||||||
|
|
||||||
raise RuntimeError(f"Cannot get password for entry {entry_name}")
|
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.
|
"""Get secret groups.
|
||||||
|
|
||||||
A regex pattern may be provided to filter groups.
|
A regex pattern may be provided to filter groups.
|
||||||
@ -128,7 +142,7 @@ class PasswordContext:
|
|||||||
if pattern:
|
if pattern:
|
||||||
groups = cast(
|
groups = cast(
|
||||||
list[pykeepass.group.Group],
|
list[pykeepass.group.Group],
|
||||||
self.keepass.find_groups(name=pattern, regex=True),
|
self.keepass.find_groups(name=pattern, regex=regex),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
all_groups = cast(list[pykeepass.group.Group], self.keepass.groups)
|
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}"
|
f"Error: Cannot find a parent group named {parent_group}"
|
||||||
)
|
)
|
||||||
kp_parent_group = query
|
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()
|
self.keepass.save()
|
||||||
|
|
||||||
def set_group_description(self, name: str, description: str) -> None:
|
def set_group_description(self, name: str, description: str) -> None:
|
||||||
|
|||||||
@ -11,6 +11,7 @@ from pydantic import (
|
|||||||
IPvAnyNetwork,
|
IPvAnyNetwork,
|
||||||
)
|
)
|
||||||
from sshecret.crypto import validate_public_key
|
from sshecret.crypto import validate_public_key
|
||||||
|
from sshecret.backend.models import ClientReference
|
||||||
|
|
||||||
|
|
||||||
def public_key_validator(value: str) -> str:
|
def public_key_validator(value: str) -> str:
|
||||||
@ -96,6 +97,7 @@ class SecretCreate(SecretUpdate):
|
|||||||
clients: list[str] | None = Field(
|
clients: list[str] | None = Field(
|
||||||
default=None, description="Assign the secret to a list of clients."
|
default=None, description="Assign the secret to a list of clients."
|
||||||
)
|
)
|
||||||
|
group: str | None = None
|
||||||
|
|
||||||
model_config: ConfigDict = ConfigDict(
|
model_config: ConfigDict = ConfigDict(
|
||||||
json_schema_extra={
|
json_schema_extra={
|
||||||
@ -107,6 +109,7 @@ class SecretCreate(SecretUpdate):
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "MySecret",
|
"name": "MySecret",
|
||||||
|
"group": "MySecretGroup",
|
||||||
"value": "mysecretstring",
|
"value": "mysecretstring",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@ -118,6 +121,34 @@ class SecretGroup(BaseModel):
|
|||||||
"""A secret group."""
|
"""A secret group."""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
|
path: str
|
||||||
description: str | None = None
|
description: str | None = None
|
||||||
parent_group: "SecretGroup | None" = None
|
parent_group: "SecretGroup | None" = None
|
||||||
children: list["SecretGroup"] = Field(default_factory=list)
|
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
|
||||||
|
|||||||
@ -124,6 +124,28 @@ def test_get_secret_groups_regex(password_database: pykeepass.PyKeePass) -> None
|
|||||||
assert len(bar_groups) == 3
|
assert len(bar_groups) == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_secret_groups_with_entries(password_database: pykeepass.PyKeePass) -> None:
|
||||||
|
"""Get secret groups with entries."""
|
||||||
|
context = PasswordContext(password_database)
|
||||||
|
context.add_group("test_group", "Test Group")
|
||||||
|
context.add_group("parent_group", "Test Group")
|
||||||
|
context.add_group("nested_group", "Test Group", "parent_group")
|
||||||
|
context.add_entry("free_entry", "test", group_name="test_group")
|
||||||
|
context.add_entry("middle_entry", "test", group_name="parent_group")
|
||||||
|
context.add_entry("lower_entry", "test", group_name="nested_group")
|
||||||
|
|
||||||
|
groups = context.get_secret_groups()
|
||||||
|
assert len(groups) == 3
|
||||||
|
for group in groups:
|
||||||
|
assert len(group.entries) == 1
|
||||||
|
if group.name == "test_group":
|
||||||
|
assert "free_entry" in group.entries
|
||||||
|
elif group.name == "parent_group":
|
||||||
|
assert "middle_entry" in group.entries
|
||||||
|
elif group.name == "nested_group":
|
||||||
|
assert "lower_entry" in group.entries
|
||||||
|
|
||||||
|
|
||||||
def test_add_group(password_database: pykeepass.PyKeePass) -> None:
|
def test_add_group(password_database: pykeepass.PyKeePass) -> None:
|
||||||
"""Test add_group."""
|
"""Test add_group."""
|
||||||
context = PasswordContext(password_database)
|
context = PasswordContext(password_database)
|
||||||
@ -231,3 +253,16 @@ def test_delete_group(password_database: pykeepass.PyKeePass) -> None:
|
|||||||
# Check if the secrets are still there.
|
# Check if the secrets are still there.
|
||||||
secrets = context.get_available_secrets()
|
secrets = context.get_available_secrets()
|
||||||
assert len(secrets) == 10
|
assert len(secrets) == 10
|
||||||
|
|
||||||
|
def test_get_specific_group(password_database: pykeepass.PyKeePass) -> None:
|
||||||
|
"""Test fetching a specific group."""
|
||||||
|
context = PasswordContext(password_database)
|
||||||
|
context.add_group("parent", "A parent group")
|
||||||
|
context.add_group("test_group", "A test group", "parent")
|
||||||
|
context.add_group("test_group_2", "A test group")
|
||||||
|
context.add_group("test_group_3", "A test group")
|
||||||
|
context.add_group("Other Group", "A test group")
|
||||||
|
results = context.get_secret_groups("test_group", False)
|
||||||
|
assert len(results) == 1
|
||||||
|
# Check if the parent reference is available.
|
||||||
|
assert results[0].parent_group is not None
|
||||||
|
|||||||
Reference in New Issue
Block a user