diff --git a/packages/sshecret-admin/src/sshecret_admin/api/endpoints/secrets.py b/packages/sshecret-admin/src/sshecret_admin/api/endpoints/secrets.py index ef50ff1..cce0b62 100644 --- a/packages/sshecret-admin/src/sshecret_admin/api/endpoints/secrets.py +++ b/packages/sshecret-admin/src/sshecret_admin/api/endpoints/secrets.py @@ -5,7 +5,6 @@ import logging from typing import Annotated from fastapi import APIRouter, Depends, HTTPException, Query, Security, status -from sshecret.backend.models import Secret from sshecret_admin.core.dependencies import AdminDependencies from sshecret_admin.services import AdminBackend from sshecret_admin.services.models import ( @@ -13,6 +12,7 @@ from sshecret_admin.services.models import ( ClientSecretGroupList, SecretCreate, SecretGroupCreate, + SecretListView, SecretUpdate, SecretView, ) @@ -27,7 +27,7 @@ def create_router(dependencies: AdminDependencies) -> APIRouter: @app.get("/secrets/") async def get_secret_names( admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)], - ) -> list[Secret]: + ) -> list[SecretListView]: """Get Secret Names.""" return await admin.get_secrets() diff --git a/packages/sshecret-admin/src/sshecret_admin/frontend/templates/secrets/partials/client_secret_details.html.j2 b/packages/sshecret-admin/src/sshecret_admin/frontend/templates/secrets/partials/client_secret_details.html.j2 index 54ce681..3586e5c 100644 --- a/packages/sshecret-admin/src/sshecret_admin/frontend/templates/secrets/partials/client_secret_details.html.j2 +++ b/packages/sshecret-admin/src/sshecret_admin/frontend/templates/secrets/partials/client_secret_details.html.j2 @@ -3,6 +3,8 @@ {% include '/secrets/partials/client_list_inner.html.j2' %} +{% if secret.secret %}
{% include '/secrets/partials/client_assign_button.html.j2' %} +{% endif %}
diff --git a/packages/sshecret-admin/src/sshecret_admin/frontend/templates/secrets/partials/group_detail.html.j2 b/packages/sshecret-admin/src/sshecret_admin/frontend/templates/secrets/partials/group_detail.html.j2 index 58e0b57..365913c 100644 --- a/packages/sshecret-admin/src/sshecret_admin/frontend/templates/secrets/partials/group_detail.html.j2 +++ b/packages/sshecret-admin/src/sshecret_admin/frontend/templates/secrets/partials/group_detail.html.j2 @@ -1,7 +1,9 @@

Group {{name}}

- {{ description }} + {% if description %} + {{ description }} + {% endif %}
diff --git a/packages/sshecret-admin/src/sshecret_admin/frontend/templates/secrets/partials/tree_detail.html.j2 b/packages/sshecret-admin/src/sshecret_admin/frontend/templates/secrets/partials/tree_detail.html.j2 index 86e5481..7dd303a 100644 --- a/packages/sshecret-admin/src/sshecret_admin/frontend/templates/secrets/partials/tree_detail.html.j2 +++ b/packages/sshecret-admin/src/sshecret_admin/frontend/templates/secrets/partials/tree_detail.html.j2 @@ -31,6 +31,12 @@

{{secret.name}}

+ {% if secret.description %} + {{ secret.description }} + {% endif %} + {% if not secret.secret %} +

This secret was created outside of sshecret-admin. It cannot be decrypted, and therefore fewer options are available here.

+ {% endif %}
+ {% if secret.secret %}
@@ -103,6 +110,7 @@ {% endif %} +{% endif %} diff --git a/packages/sshecret-admin/src/sshecret_admin/frontend/views/secrets.py b/packages/sshecret-admin/src/sshecret_admin/frontend/views/secrets.py index 74afb5e..0f44457 100644 --- a/packages/sshecret-admin/src/sshecret_admin/frontend/views/secrets.py +++ b/packages/sshecret-admin/src/sshecret_admin/frontend/views/secrets.py @@ -64,6 +64,7 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter: current_user: Annotated[LocalUserInfo, Depends(dependencies.get_user_info)], ): groups = await admin.get_secret_groups() + LOG.info("Groups: %s", groups.model_dump_json(indent=2)) return templates.TemplateResponse( request, "secrets/index.html.j2", @@ -73,46 +74,46 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter: }, ) - @app.get("/secrets/partial/root_group") - async def get_root_group( - request: Request, - admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)], - ): - """Get root group.""" - clients = await admin.get_clients() - return templates.TemplateResponse( - request, - "secrets/partials/edit_root.html.j2", - { - "group_path_nodes": [], - "clients": clients, - }, - ) + # @app.get("/secrets/partial/root_group") + # async def get_root_group( + # request: Request, + # admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)], + # ): + # """Get root group.""" + # clients = await admin.get_clients() + # return templates.TemplateResponse( + # request, + # "secrets/partials/edit_root.html.j2", + # { + # "group_path_nodes": [], + # "clients": clients, + # }, + # ) - @app.get("/secrets/partial/secret/{name}") - async def get_secret_tree_detail_partial( - request: Request, - name: str, - admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)], - ): - """Get partial secret detail.""" - secret = await admin.get_secret(name) - groups = await admin.get_secret_groups(flat=True) - events = await admin.get_audit_log_detailed(limit=10, secret_name=name) + # @app.get("/secrets/partial/secret/{name}") + # async def get_secret_tree_detail_partial( + # request: Request, + # name: str, + # admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)], + # ): + # """Get partial secret detail.""" + # secret = await admin.get_secret(name) + # groups = await admin.get_secret_groups(flat=True) + # events = await admin.get_audit_log_detailed(limit=10, secret_name=name) - if not secret: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, detail="Secret not found" - ) - return templates.TemplateResponse( - request, - "secrets/partials/tree_detail.html.j2", - { - "secret": secret, - "groups": groups, - "events": events, - }, - ) + # if not secret: + # raise HTTPException( + # status_code=status.HTTP_404_NOT_FOUND, detail="Secret not found" + # ) + # return templates.TemplateResponse( + # request, + # "secrets/partials/tree_detail.html.j2", + # { + # "secret": secret, + # "groups": groups, + # "events": events, + # }, + # ) @app.get("/secrets/group/") async def show_root_group( @@ -573,7 +574,7 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter: admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)], ): """Add a secret to a client.""" - await admin.create_client_secret(client, name) + await admin.create_client_secret(("id", client), name) secret = await admin.get_secret(name) if not secret: raise HTTPException( diff --git a/packages/sshecret-admin/src/sshecret_admin/services/admin_backend.py b/packages/sshecret-admin/src/sshecret_admin/services/admin_backend.py index 7c59777..288da77 100644 --- a/packages/sshecret-admin/src/sshecret_admin/services/admin_backend.py +++ b/packages/sshecret-admin/src/sshecret_admin/services/admin_backend.py @@ -26,6 +26,7 @@ from .models import ( ClientSecretGroup, ClientSecretGroupList, SecretClientMapping, + SecretListView, SecretGroup, SecretView, ) @@ -269,23 +270,32 @@ class AdminBackend: except Exception as e: raise BackendUnavailableError() from e - async def _get_secrets(self) -> list[Secret]: + async def _get_secrets(self) -> list[SecretListView]: """Get secrets. This fetches the secret to client mapping from backend, and adds secrets from the password manager. """ + backend_secrets = await self.backend.get_secrets() with self.password_manager() as password_manager: - all_secrets = password_manager.get_available_secrets() + admin_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=[])) + secrets: dict[str, SecretListView] = {} + for secret in backend_secrets: + secrets[secret.name] = SecretListView( + name=secret.name, unmanaged=True, clients=secret.clients + ) - return secrets + for secret_name in admin_secrets: + if secret_name in secrets: + secrets[secret_name].unmanaged = False + continue + secrets[secret_name] = SecretListView( + name=secret_name, unmanaged=False, clients=[] + ) - async def get_secrets(self) -> list[Secret]: + return list(secrets.values()) + + async def get_secrets(self) -> list[SecretListView]: """Get secrets from backend.""" try: return await self._get_secrets() @@ -385,6 +395,8 @@ class AdminBackend: ) ungrouped = password_manager.get_ungrouped_secrets() + all_admin_secrets = password_manager.get_available_secrets() + group_result: list[ClientSecretGroup] = [] for group in all_groups: # We have to do this recursively. @@ -401,7 +413,19 @@ class AdminBackend: mapping.clients = client_mapping.clients ungrouped_clients.append(mapping) + # We need to process unmanaged secrets too. + unmanaged_secrets = [ + secret for secret in all_secrets if secret.name not in all_admin_secrets + ] + for secret in unmanaged_secrets: + ungrouped_clients.append( + SecretClientMapping( + name=secret.name, unmanaged=True, clients=secret.clients + ) + ) + result.ungrouped = ungrouped_clients + return result async def get_secret_group(self, name: str) -> ClientSecretGroup | None: @@ -436,13 +460,13 @@ class AdminBackend: self, name: str, secret_id: str | None = None ) -> SecretView | None: """Get a secret, including the actual unencrypted value and clients.""" + secret: str | None = None with self.password_manager() as password_manager: secret = password_manager.get_secret(name) secret_group = password_manager.get_entry_group(name) - if not secret: - return None secret_view = SecretView(name=name, secret=secret, group=secret_group) + idname: KeySpec = name if secret_id: idname = ("id", secret_id) @@ -529,11 +553,13 @@ class AdminBackend: except Exception as e: raise BackendUnavailableError() from e - async def _create_client_secret(self, client_name: str, secret_name: str) -> None: + async def _create_client_secret( + self, client_idname: KeySpec, secret_name: str + ) -> None: """Create client secret.""" - client = await self.get_client(client_name) + client = await self.get_client(client_idname) if not client: - raise ClientNotFoundError() + raise ClientNotFoundError(client_idname) with self.password_manager() as password_manager: secret = password_manager.get_secret(secret_name) @@ -542,12 +568,14 @@ class AdminBackend: 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) + await self.backend.create_client_secret(client_idname, secret_name, encrypted) - async def create_client_secret(self, client_name: str, secret_name: str) -> None: + async def create_client_secret( + self, client_idname: KeySpec, secret_name: str + ) -> None: """Create client secret.""" try: - await self._create_client_secret(client_name, secret_name) + await self._create_client_secret(client_idname, secret_name) except ClientManagementError: raise except Exception as e: diff --git a/packages/sshecret-admin/src/sshecret_admin/services/models.py b/packages/sshecret-admin/src/sshecret_admin/services/models.py index 6799397..9d20f11 100644 --- a/packages/sshecret-admin/src/sshecret_admin/services/models.py +++ b/packages/sshecret-admin/src/sshecret_admin/services/models.py @@ -25,6 +25,7 @@ class SecretListView(BaseModel): """Model containing a list of all available secrets.""" name: str + unmanaged: bool = False clients: list[str] = Field(default_factory=list) # Clients that have access to it. @@ -32,7 +33,7 @@ class SecretView(BaseModel): """Model containing a secret, including its clear-text value.""" name: str - secret: str + secret: str | None group: str | None = None clients: list[str] = Field(default_factory=list) # Clients that have access to it. @@ -134,6 +135,7 @@ class SecretClientMapping(BaseModel): """Secret name with list of clients.""" name: str # name of secret + unmanaged: bool = False clients: list[ClientReference] = Field(default_factory=list)