diff --git a/packages/sshecret-admin/src/sshecret_admin/frontend/templates/secrets/index.html.j2 b/packages/sshecret-admin/src/sshecret_admin/frontend/templates/secrets/index.html.j2
index b856ea6..8f0fb87 100644
--- a/packages/sshecret-admin/src/sshecret_admin/frontend/templates/secrets/index.html.j2
+++ b/packages/sshecret-admin/src/sshecret_admin/frontend/templates/secrets/index.html.j2
@@ -4,6 +4,12 @@
class="tree-entry-item"
data-type="entry"
data-name="{{ entry.name }}"
+ data-group-path="/"
+ {% if secret | default(false) %}
+ {% if secret.name == entry.name %}
+ selected=""
+ {% endif %}
+ {% endif %}
>
{{ entry.name }}
@@ -16,6 +22,13 @@
class="secret-group-list-item"
data-type="group"
data-name="{{ group.group_name }}"
+ data-group-path="{{ group.path }}"
+ {% if group_path_nodes | default(false) %}
+ {% if group.group_name in group_path_nodes %}
+ expanded=""
+ {% endif %}
+
+ {% endif %}
>
{{ group.group_name }}
@@ -81,7 +94,22 @@
- {% include '/secrets/partials/default_detail.html.j2' %}
+ {% if group_page | default(false) %}
+
+ {% include '/secrets/partials/group_detail.html.j2' %}
+
+ {% elif root_group_page | default(false) %}
+
+ {% include '/secrets/partials/edit_root.html.j2' %}
+
+ {% elif secret_page | default(false) %}
+
+ {% include '/secrets/partials/tree_detail.html.j2' %}
+
+ {% else %}
+ {% include '/secrets/partials/default_detail.html.j2' %}
+ {% endif %}
+
@@ -99,17 +127,19 @@
const type = selectedEl.dataset.type;
const name = selectedEl.dataset.name;
- console.log(`Event on ${type} ${name}`);
+ const groupPath = selectedEl.dataset.groupPath;
+ console.log(`Event on ${type} ${name} path: ${groupPath}`);
if (!type || !name) return;
let url = '';
if (type === 'entry') {
- url = `/secrets/partial/secret/${encodeURIComponent(name)}`;
+ url = `/secrets/secret/${encodeURIComponent(name)}`;
} else if (type === 'group') {
- url = `/secrets/partial/group/${encodeURIComponent(name)}`;
+ //url = `/secrets/partial/group/${encodeURIComponent(name)}`;
+ url = `/secrets/group/${encodeURIComponent(groupPath)}`;
} else if (type == 'root') {
- url = `/secrets/partial/root_group`;
+ url = `/secrets/group/`;
}
if (url) {
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 a0c1b94..74afb5e 100644
--- a/packages/sshecret-admin/src/sshecret_admin/frontend/views/secrets.py
+++ b/packages/sshecret-admin/src/sshecret_admin/frontend/views/secrets.py
@@ -64,7 +64,6 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter:
current_user: Annotated[LocalUserInfo, Depends(dependencies.get_user_info)],
):
groups = await admin.get_secret_groups()
- LOG.info("Groups: %r", groups)
return templates.TemplateResponse(
request,
"secrets/index.html.j2",
@@ -85,12 +84,13 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter:
request,
"secrets/partials/edit_root.html.j2",
{
+ "group_path_nodes": [],
"clients": clients,
},
)
@app.get("/secrets/partial/secret/{name}")
- async def get_secret_tree_detail(
+ async def get_secret_tree_detail_partial(
request: Request,
name: str,
admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
@@ -114,6 +114,118 @@ def create_router(dependencies: FrontendDependencies) -> APIRouter:
},
)
+ @app.get("/secrets/group/")
+ async def show_root_group(
+ request: Request,
+ admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
+ current_user: Annotated[LocalUserInfo, Depends(dependencies.get_user_info)],
+ ):
+ """Show the root path."""
+ clients = await admin.get_clients()
+ context: dict[str, Any] = {
+ "clients": clients,
+ "root_group_page": True,
+ }
+ headers: dict[str, str] = {}
+ if request.headers.get("HX-Request"):
+ # This is a HTMX request.
+ template_name = "secrets/partials/edit_root.html.j2"
+ headers["HX-Push-Url"] = request.url.path
+ else:
+ groups = await admin.get_secret_groups()
+ template_name = "secrets/index.html.j2"
+ context["user"] = current_user
+ context["groups"] = groups
+ context["group_path_nodes"] = ["/"]
+
+ return templates.TemplateResponse(
+ request, template_name, context, headers=headers
+ )
+
+ @app.get("/secrets/group/{group_path:path}")
+ async def show_group(
+ request: Request,
+ group_path: str,
+ admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
+ current_user: Annotated[LocalUserInfo, Depends(dependencies.get_user_info)],
+ ):
+ """Show a group."""
+ group = await admin.get_secret_group_by_path(group_path)
+ if not group:
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND, detail="Group not found"
+ )
+ clients = await admin.get_clients()
+
+ headers: dict[str, str] = {}
+ context: dict[str, Any] = {
+ "group_page": True,
+ "name": group.group_name,
+ "description": group.description,
+ "clients": clients,
+ }
+ if request.headers.get("HX-Request"):
+ # This is a HTMX request.
+ template_name = "secrets/partials/group_detail.html.j2"
+ headers["HX-Push-Url"] = request.url.path
+ else:
+ template_name = "secrets/index.html.j2"
+
+ groups = await admin.get_secret_groups()
+ context["user"] = current_user
+ context["groups"] = groups
+ context["group_path_nodes"] = group.path.split("/")
+
+ return templates.TemplateResponse(
+ request, template_name, context, headers=headers
+ )
+
+ @app.get("/secrets/secret/{name}")
+ async def get_secret_tree_detail(
+ request: Request,
+ name: str,
+ admin: Annotated[AdminBackend, Depends(dependencies.get_admin_backend)],
+ current_user: Annotated[LocalUserInfo, Depends(dependencies.get_user_info)],
+ ):
+ """Get 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"
+ )
+
+ context: dict[str, Any] = {
+ "secret": secret,
+ "groups": groups,
+ "events": events,
+ "secret_page": True,
+ }
+
+ headers: dict[str, str] = {}
+
+ if request.headers.get("HX-Request"):
+ # This is a HTMX request.
+ template_name = "secrets/partials/tree_detail.html.j2"
+ headers["HX-Push-Url"] = request.url.path
+ else:
+ group_path = ["/"]
+ if secret.group:
+ group = await admin.get_secret_group(secret.group)
+ if group:
+ group_path = group.path.split("/")
+
+ template_name = "secrets/index.html.j2"
+ context["user"] = current_user
+ context["groups"] = groups
+ context["group_path_nodes"] = group_path
+
+ return templates.TemplateResponse(
+ request, template_name, context, headers=headers
+ )
+
@app.get("/secrets/partial/group/{name}")
async def get_group_details(
request: Request,
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 5a21880..7c59777 100644
--- a/packages/sshecret-admin/src/sshecret_admin/services/admin_backend.py
+++ b/packages/sshecret-admin/src/sshecret_admin/services/admin_backend.py
@@ -411,6 +411,18 @@ class AdminBackend:
return matches.groups[0]
return None
+ async def get_secret_group_by_path(self, path: str) -> ClientSecretGroup | None:
+ """Get a group based on its path."""
+ with self.password_manager() as password_manager:
+ secret_group = password_manager.get_secret_group(path)
+
+ if not secret_group:
+ return None
+
+ all_secrets = await self.backend.get_detailed_secrets()
+ secrets_mapping = {secret.name: secret for secret in all_secrets}
+ return add_clients_to_secret_group(secret_group, secrets_mapping)
+
async def get_secret(self, name: str) -> SecretView | None:
"""Get secrets from backend."""
try:
diff --git a/packages/sshecret-admin/src/sshecret_admin/services/keepass.py b/packages/sshecret-admin/src/sshecret_admin/services/keepass.py
index 3f6ede7..04af2fd 100644
--- a/packages/sshecret-admin/src/sshecret_admin/services/keepass.py
+++ b/packages/sshecret-admin/src/sshecret_admin/services/keepass.py
@@ -32,7 +32,9 @@ def create_password_db(location: Path, password: str) -> None:
def _kp_group_to_secret_group(
- kp_group: pykeepass.group.Group, parent: SecretGroup | None = None, depth: int | 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)
@@ -143,8 +145,9 @@ class PasswordContext:
return None
return str(entry.group.name)
-
- def get_secret_groups(self, pattern: str | None = None, regex: bool = True) -> 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.
@@ -160,15 +163,35 @@ class PasswordContext:
secret_groups = [_kp_group_to_secret_group(group) for group in groups]
return secret_groups
- def get_secret_group_list(self, pattern: str | None = None, regex: bool = True) -> list[SecretGroup]:
+ def get_secret_group_list(
+ self, pattern: str | None = None, regex: bool = True
+ ) -> list[SecretGroup]:
"""Get a flat list of groups."""
if pattern:
return self.get_secret_groups(pattern, regex)
- groups = [ group for group in self.keepass.groups if not group.is_root_group ]
+ groups = [group for group in self.keepass.groups if not group.is_root_group]
secret_groups = [_kp_group_to_secret_group(group) for group in groups]
return secret_groups
+ def get_secret_group(self, path: str) -> SecretGroup | None:
+ """Get a secret group by path."""
+ elements = path.split("/")
+ final_element = elements[-1]
+
+ current = self._root_group
+ while elements:
+ groupname = elements.pop(0)
+ matches = [
+ subgroup for subgroup in current.subgroups if subgroup.name == groupname
+ ]
+ if matches:
+ current = matches[0]
+ else:
+ return None
+ if not current.is_root_group and current.name == final_element:
+ return _kp_group_to_secret_group(current)
+ return None
def get_ungrouped_secrets(self) -> list[str]:
"""Get secrets without groups."""
@@ -193,7 +216,9 @@ class PasswordContext:
f"Error: Cannot find a parent group named {parent_group}"
)
kp_parent_group = query
- self.keepass.add_group(destination_group=kp_parent_group, group_name=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: