Share hidden vault root logic across UI and API

This commit is contained in:
Joe Julian
2026-04-11 11:26:00 -07:00
parent ebb8d4f4ff
commit c8f91b300b
5 changed files with 121 additions and 28 deletions
+3 -13
View File
@@ -19,6 +19,7 @@ import (
"git.julianfamily.org/keepassgo/internal/passwords"
"git.julianfamily.org/keepassgo/internal/session"
"git.julianfamily.org/keepassgo/internal/vault"
"git.julianfamily.org/keepassgo/internal/vaultview"
"git.julianfamily.org/keepassgo/internal/webdav"
keepassgov1 "git.julianfamily.org/keepassgo/proto/keepassgo/v1"
"google.golang.org/grpc/codes"
@@ -973,19 +974,8 @@ func entryFromProtoWithModel(model vault.Model, entry *keepassgov1.Entry) vault.
}
}
func hiddenVaultRoot(model vault.Model) string {
if len(model.EntriesInPath(nil)) != 0 {
return ""
}
groups := model.ChildGroups(nil)
if len(groups) != 1 {
return ""
}
return groups[0]
}
func expandClientPath(model vault.Model, path []string) []string {
root := hiddenVaultRoot(model)
root := vaultview.HiddenRoot(model)
if root == "" {
return append([]string(nil), path...)
}
@@ -999,7 +989,7 @@ func expandClientPath(model vault.Model, path []string) []string {
}
func collapseInternalPath(model vault.Model, path []string) []string {
root := hiddenVaultRoot(model)
root := vaultview.HiddenRoot(model)
if root == "" || len(path) == 0 || path[0] != root {
return append([]string(nil), path...)
}
+67
View File
@@ -265,6 +265,46 @@ func TestVaultServiceListEntriesHidesSingleInternalVaultRoot(t *testing.T) {
}
}
func TestVaultServiceListEntriesHidesSingleInternalVaultRootWhenRecycleBinExists(t *testing.T) {
t.Parallel()
client, _, cleanup := newTestClientForModel(t, vault.Model{
Entries: []vault.Entry{
{
ID: "codex-nextcloud",
Title: "Nextcloud (codex)",
Username: "jjulian",
Password: "secret-1",
URL: "https://nextcloud.example.invalid",
Path: []string{"keepass", "Joe", "codex"},
},
testAPITokenEntry(t,
apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationListEntries, Resource: apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"keepass", "Joe", "codex"}}},
),
},
Groups: [][]string{
{"keepass"},
{"keepass", "Joe"},
{"keepass", "Joe", "codex"},
{"Recycle Bin"},
},
})
defer cleanup()
resp, err := client.ListEntries(tokenContext(defaultTestTokenSecret), &keepassgov1.ListEntriesRequest{
Path: []string{"Joe", "codex"},
})
if err != nil {
t.Fatalf("ListEntries() error = %v", err)
}
if len(resp.Entries) != 1 {
t.Fatalf("len(ListEntries().Entries) = %d, want 1", len(resp.Entries))
}
if got := resp.Entries[0].Path; !slices.Equal(got, []string{"Joe", "codex"}) {
t.Fatalf("ListEntries().Entries[0].Path = %v, want [Joe codex]", got)
}
}
func TestVaultServiceListGroupsHidesSingleInternalVaultRoot(t *testing.T) {
t.Parallel()
@@ -291,6 +331,33 @@ func TestVaultServiceListGroupsHidesSingleInternalVaultRoot(t *testing.T) {
}
}
func TestVaultServiceListGroupsHidesSingleInternalVaultRootWhenRecycleBinExists(t *testing.T) {
t.Parallel()
client, _, cleanup := newTestClientForModel(t, vault.Model{
Entries: []vault.Entry{
testAPITokenEntry(t,
apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationListGroups, Resource: apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"keepass"}}},
),
},
Groups: [][]string{
{"keepass"},
{"keepass", "Joe"},
{"keepass", "Shared"},
{"Recycle Bin"},
},
})
defer cleanup()
resp, err := client.ListGroups(tokenContext(defaultTestTokenSecret), &keepassgov1.ListGroupsRequest{})
if err != nil {
t.Fatalf("ListGroups() error = %v", err)
}
if !slices.Equal(resp.Names, []string{"Joe", "Shared"}) {
t.Fatalf("ListGroups().Names = %v, want [Joe Shared]", resp.Names)
}
}
func TestVaultServiceGetsBrowserCredentialForAuthorizedClients(t *testing.T) {
t.Parallel()
+2 -15
View File
@@ -18,6 +18,7 @@ import (
"git.julianfamily.org/keepassgo/internal/autofillcache"
"git.julianfamily.org/keepassgo/internal/session"
"git.julianfamily.org/keepassgo/internal/vault"
"git.julianfamily.org/keepassgo/internal/vaultview"
"git.julianfamily.org/keepassgo/internal/webdav"
)
@@ -1266,21 +1267,7 @@ func (u *ui) hiddenVaultRoot() string {
if err != nil {
return ""
}
if len(model.EntriesInPath(nil)) != 0 {
return ""
}
groups := model.ChildGroups(nil)
roots := make([]string, 0, len(groups))
for _, group := range groups {
if group == "Recycle Bin" {
continue
}
roots = append(roots, group)
}
if len(roots) != 1 {
return ""
}
return roots[0]
return vaultview.HiddenRoot(model)
}
func (u *ui) enterHiddenVaultRoot() {
+23
View File
@@ -0,0 +1,23 @@
package vaultview
import "git.julianfamily.org/keepassgo/internal/vault"
// HiddenRoot returns the single synthetic top-level vault group that should be
// treated as an internal storage root rather than as a user-visible group.
func HiddenRoot(model vault.Model) string {
if len(model.EntriesInPath(nil)) != 0 {
return ""
}
groups := model.ChildGroups(nil)
roots := make([]string, 0, len(groups))
for _, group := range groups {
if group == "Recycle Bin" {
continue
}
roots = append(roots, group)
}
if len(roots) != 1 {
return ""
}
return roots[0]
}
+26
View File
@@ -0,0 +1,26 @@
package vaultview
import (
"testing"
"git.julianfamily.org/keepassgo/internal/vault"
)
func TestHiddenRootIgnoresRecycleBin(t *testing.T) {
t.Parallel()
model := vault.Model{
Entries: []vault.Entry{
{ID: "entry-1", Title: "Vault Console", Path: []string{"keepass", "Crew", "Internet"}},
},
Groups: [][]string{
{"keepass"},
{"keepass", "Crew"},
{"Recycle Bin"},
},
}
if got := HiddenRoot(model); got != "keepass" {
t.Fatalf("HiddenRoot() = %q, want %q", got, "keepass")
}
}