Standardize IDs, fix group APIs, fix tests
This commit is contained in:
@ -1,7 +1,4 @@
|
||||
"""Client-related endpoints factory.
|
||||
|
||||
# TODO: Settle on name/keyspec pattern
|
||||
"""
|
||||
"""Client-related endpoints factory."""
|
||||
|
||||
# pyright: reportUnusedFunction=false
|
||||
|
||||
@ -20,10 +17,15 @@ from sshecret_admin.services.models import (
|
||||
UpdatePoliciesRequest,
|
||||
)
|
||||
|
||||
from sshecret.backend.identifiers import ClientIdParam, FlexID, KeySpec
|
||||
from sshecret.backend.models import ClientQueryResult, ClientReference, FilterType
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
def _id(identifier: str) -> KeySpec:
|
||||
"""Parse ID."""
|
||||
parsed = FlexID.from_string(identifier)
|
||||
return parsed.keyspec
|
||||
|
||||
def query_filter_to_client_filter(query_filter: ClientListParams) -> ClientFilter:
|
||||
"""Convert query filter to client filter."""
|
||||
@ -95,11 +97,11 @@ def create_router(dependencies: AdminDependencies) -> APIRouter:
|
||||
|
||||
@app.get("/clients/{id}")
|
||||
async def get_client(
|
||||
id: str,
|
||||
id: ClientIdParam,
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
) -> Client:
|
||||
"""Get a client."""
|
||||
client = await admin.get_client(("id", id))
|
||||
client = await admin.get_client(_id(id))
|
||||
if not client:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Item not found"
|
||||
@ -109,12 +111,12 @@ def create_router(dependencies: AdminDependencies) -> APIRouter:
|
||||
|
||||
@app.put("/clients/{id}")
|
||||
async def update_client(
|
||||
id: str,
|
||||
id: ClientIdParam,
|
||||
updated: ClientCreate,
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
) -> Client:
|
||||
"""Update a client."""
|
||||
client = await admin.get_client(("id", id))
|
||||
client = await admin.get_client(_id(id))
|
||||
|
||||
if not client:
|
||||
raise HTTPException(
|
||||
@ -132,20 +134,20 @@ def create_router(dependencies: AdminDependencies) -> APIRouter:
|
||||
|
||||
@app.delete("/clients/{id}")
|
||||
async def delete_client(
|
||||
id: str,
|
||||
id: ClientIdParam,
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
) -> None:
|
||||
"""Delete a client."""
|
||||
await admin.delete_client(("id", id))
|
||||
await admin.delete_client(_id(id))
|
||||
|
||||
@app.delete("/clients/{id}/secrets/{secret_name}")
|
||||
async def delete_secret_from_client(
|
||||
id: str,
|
||||
id: ClientIdParam,
|
||||
secret_name: str,
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
) -> None:
|
||||
"""Delete a secret from a client."""
|
||||
client = await admin.get_client(("id", id))
|
||||
client = await admin.get_client(_id(id))
|
||||
if not client:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Item not found"
|
||||
@ -164,7 +166,7 @@ def create_router(dependencies: AdminDependencies) -> APIRouter:
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
) -> Client:
|
||||
"""Update the client access policies."""
|
||||
client = await admin.get_client(("id", id))
|
||||
client = await admin.get_client(_id(id))
|
||||
if not client:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Item not found"
|
||||
@ -182,7 +184,7 @@ def create_router(dependencies: AdminDependencies) -> APIRouter:
|
||||
|
||||
@app.put("/clients/{id}/public-key")
|
||||
async def update_client_public_key(
|
||||
id: str,
|
||||
id: ClientIdParam,
|
||||
updated: UpdateKeyModel,
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
) -> UpdateKeyResponse:
|
||||
@ -193,7 +195,7 @@ def create_router(dependencies: AdminDependencies) -> APIRouter:
|
||||
"""
|
||||
# Let's first ensure that the key is actually updated.
|
||||
updated_secrets = await admin.update_client_public_key(
|
||||
("id", id), updated.public_key
|
||||
_id(id), updated.public_key
|
||||
)
|
||||
return UpdateKeyResponse(
|
||||
public_key=updated.public_key, updated_secrets=updated_secrets
|
||||
@ -201,11 +203,11 @@ def create_router(dependencies: AdminDependencies) -> APIRouter:
|
||||
|
||||
@app.put("/clients/{id}/secrets/{secret_name}")
|
||||
async def add_secret_to_client(
|
||||
id: str,
|
||||
id: ClientIdParam,
|
||||
secret_name: str,
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
) -> None:
|
||||
"""Add secret to a client."""
|
||||
await admin.create_client_secret(("id", id), secret_name)
|
||||
await admin.create_client_secret(_id(id), secret_name)
|
||||
|
||||
return app
|
||||
|
||||
@ -10,12 +10,19 @@ from sshecret_admin.services import AdminBackend
|
||||
from sshecret_admin.services.models import (
|
||||
ClientSecretGroup,
|
||||
ClientSecretGroupList,
|
||||
GroupPath,
|
||||
SecretCreate,
|
||||
SecretGroupAssign,
|
||||
SecretGroupCreate,
|
||||
SecretGroupUdate,
|
||||
SecretListView,
|
||||
SecretUpdate,
|
||||
SecretView,
|
||||
)
|
||||
from sshecret_admin.services.secret_manager import (
|
||||
InvalidGroupNameError,
|
||||
InvalidSecretNameError,
|
||||
)
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
@ -81,20 +88,50 @@ def create_router(dependencies: AdminDependencies) -> APIRouter:
|
||||
filter_regex: Annotated[str | None, Query()] = None,
|
||||
) -> ClientSecretGroupList:
|
||||
"""Get secret groups."""
|
||||
return await admin.get_secret_groups(filter_regex)
|
||||
result = await admin.get_secret_groups(filter_regex)
|
||||
return result
|
||||
|
||||
@app.get("/secrets/groups/{group_name}/")
|
||||
@app.get("/secrets/groups/{group_path:path}/")
|
||||
async def get_secret_group(
|
||||
group_name: str,
|
||||
group_path: str,
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
) -> ClientSecretGroup:
|
||||
"""Get a specific secret group."""
|
||||
results = await admin.get_secret_groups(group_name, False)
|
||||
results = await admin.get_secret_group_by_path(group_path)
|
||||
if not results:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="No such group."
|
||||
)
|
||||
return results.groups[0]
|
||||
return results
|
||||
|
||||
@app.put("/secrets/groups/{group_path:path}/")
|
||||
async def update_secret_group(
|
||||
group_path: str,
|
||||
group: SecretGroupUdate,
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
) -> ClientSecretGroup:
|
||||
"""Update a secret group."""
|
||||
existing_group = await admin.lookup_secret_group(group_path)
|
||||
if not existing_group:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="No such group."
|
||||
)
|
||||
|
||||
params: dict[str, str] = {}
|
||||
if name := group.name:
|
||||
params["name"] = name
|
||||
|
||||
if description := group.description:
|
||||
params["description"] = description
|
||||
|
||||
if parent := group.parent_group:
|
||||
params["parent"] = parent
|
||||
|
||||
new_group = await admin.update_secret_group(
|
||||
group_path,
|
||||
**params,
|
||||
)
|
||||
return new_group
|
||||
|
||||
@app.post("/secrets/groups/")
|
||||
async def add_secret_group(
|
||||
@ -108,16 +145,16 @@ def create_router(dependencies: AdminDependencies) -> APIRouter:
|
||||
parent_group=group.parent_group,
|
||||
)
|
||||
|
||||
result = await admin.get_secret_group(group.name)
|
||||
result = await admin.lookup_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}/")
|
||||
@app.delete("/secrets/groups/{group_path:path}/")
|
||||
async def delete_secret_group(
|
||||
group_name: str,
|
||||
group_path: str,
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
) -> None:
|
||||
"""Remove a group.
|
||||
@ -125,83 +162,55 @@ def create_router(dependencies: AdminDependencies) -> APIRouter:
|
||||
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)
|
||||
group = await admin.get_secret_group_by_path(group_path)
|
||||
if not group:
|
||||
return
|
||||
await admin.delete_secret_group(group_name)
|
||||
await admin.delete_secret_group(group_path)
|
||||
|
||||
@app.post("/secrets/groups/{group_name}/{secret_name}")
|
||||
async def move_secret_to_group(
|
||||
group_name: str,
|
||||
secret_name: str,
|
||||
@app.post("/secrets/set-group")
|
||||
async def assign_secret_group(
|
||||
assignment: SecretGroupAssign,
|
||||
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:
|
||||
"""Assign a secret to a group or root."""
|
||||
try:
|
||||
await admin.set_secret_group(assignment.secret_name, assignment.group_path)
|
||||
except InvalidSecretNameError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="No such group."
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Secret not fount"
|
||||
)
|
||||
except InvalidGroupNameError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Invalid group name"
|
||||
)
|
||||
|
||||
await admin.set_secret_group(secret_name, group_name)
|
||||
|
||||
@app.post("/secrets/group/{group_name}/parent/{parent_name}")
|
||||
@app.post("/secrets/move-group/{group_name:path}")
|
||||
async def move_group(
|
||||
group_name: str,
|
||||
parent_name: str,
|
||||
destination: GroupPath,
|
||||
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
|
||||
) -> None:
|
||||
"""Move a group."""
|
||||
group = await admin.get_secret_group(group_name)
|
||||
group = await admin.lookup_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)
|
||||
parent_path: str | None = destination.path
|
||||
if destination.path == "/" or not destination.path:
|
||||
# / means root
|
||||
parent_path = None
|
||||
|
||||
@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}",
|
||||
)
|
||||
LOG.debug("Moving group %s to %r", group_name, parent_path)
|
||||
|
||||
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.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)
|
||||
if parent_path:
|
||||
parent_group = await admin.get_secret_group_by_path(destination.path)
|
||||
if not parent_group:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"No such group {parent_path}",
|
||||
)
|
||||
await admin.move_secret_group(group_name, parent_path)
|
||||
|
||||
return app
|
||||
|
||||
@ -14,7 +14,7 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from sshecret_admin.services.admin_backend import AdminBackend
|
||||
from sshecret_admin.core.dependencies import BaseDependencies, AdminDependencies
|
||||
from sshecret_admin.auth import PasswordDB, User, decode_token
|
||||
from sshecret_admin.auth import User, decode_token
|
||||
from sshecret_admin.auth.constants import LOCAL_ISSUER
|
||||
|
||||
from .endpoints import auth, clients, secrets
|
||||
@ -93,18 +93,10 @@ def create_router(dependencies: BaseDependencies) -> APIRouter:
|
||||
|
||||
async def get_admin_backend(
|
||||
request: Request,
|
||||
session: Annotated[Session, Depends(dependencies.get_db_session)],
|
||||
):
|
||||
"""Get admin backend API."""
|
||||
username = get_optional_username(request)
|
||||
origin = get_client_origin(request)
|
||||
password_db = session.scalars(
|
||||
select(PasswordDB).where(PasswordDB.id == 1)
|
||||
).first()
|
||||
if not password_db:
|
||||
raise HTTPException(
|
||||
500, detail="Error: The password manager has not yet been set up."
|
||||
)
|
||||
admin = AdminBackend(
|
||||
dependencies.settings,
|
||||
username=username,
|
||||
|
||||
Reference in New Issue
Block a user