Add explicit templates vault view

This commit is contained in:
Joe Julian
2026-04-13 08:50:33 -07:00
parent eccfb886ee
commit a88b8a824b
3 changed files with 284 additions and 40 deletions
+154 -25
View File
@@ -7,6 +7,7 @@ import (
)
const KeepassRoot = "keepass"
const TemplatesRoot = "Templates"
// View projects the physical vault model into a logical tree for a specific
// product surface.
@@ -28,7 +29,13 @@ func Vault(model vault.Model) View {
// VaultRoot returns the logical main-vault view rooted at the physical
// keepass storage group.
func VaultRoot(model vault.Model) View {
return rootView{model: model, rooted: usesKeepassRoot(model)}
return prefixedView{model: model, root: KeepassRoot, rooted: usesTopLevelRoot(model, KeepassRoot)}
}
// VaultTemplates returns the logical templates view rooted at the physical
// Templates storage group.
func VaultTemplates(model vault.Model) View {
return templatesView{model: model}
}
// VaultRecycleBin returns the logical recycle-bin view.
@@ -68,47 +75,48 @@ func (v physicalView) FromPhysicalEntry(entry vault.Entry) vault.Entry {
return cloneEntry(entry)
}
type rootView struct {
type prefixedView struct {
model vault.Model
root string
rooted bool
}
func (v rootView) ChildGroups(path []string) []string {
func (v prefixedView) ChildGroups(path []string) []string {
return v.model.ChildGroups(v.ToPhysicalPath(path))
}
func (v rootView) EntriesInPath(path []string) []vault.Entry {
func (v prefixedView) EntriesInPath(path []string) []vault.Entry {
return v.mapEntries(v.model.EntriesInPath(v.ToPhysicalPath(path)))
}
func (v rootView) EntriesUnderPath(path []string) []vault.Entry {
func (v prefixedView) EntriesUnderPath(path []string) []vault.Entry {
return v.mapEntries(v.model.EntriesUnderPath(v.ToPhysicalPath(path)))
}
func (v rootView) ToPhysicalPath(path []string) []string {
func (v prefixedView) ToPhysicalPath(path []string) []string {
if !v.rooted {
return clonePath(path)
}
if len(path) == 0 {
return []string{KeepassRoot}
return []string{v.root}
}
return append([]string{KeepassRoot}, clonePath(path)...)
return append([]string{v.root}, clonePath(path)...)
}
func (v rootView) FromPhysicalPath(path []string) []string {
func (v prefixedView) FromPhysicalPath(path []string) []string {
if !v.rooted {
return clonePath(path)
}
if len(path) == 0 {
return nil
}
if path[0] != KeepassRoot {
if path[0] != v.root {
return clonePath(path)
}
return clonePath(path[1:])
}
func (v rootView) ToPhysicalEntry(entry vault.Entry) vault.Entry {
func (v prefixedView) ToPhysicalEntry(entry vault.Entry) vault.Entry {
entry = cloneEntry(entry)
entry.Path = v.ToPhysicalPath(entry.Path)
for i := range entry.History {
@@ -117,7 +125,7 @@ func (v rootView) ToPhysicalEntry(entry vault.Entry) vault.Entry {
return entry
}
func (v rootView) FromPhysicalEntry(entry vault.Entry) vault.Entry {
func (v prefixedView) FromPhysicalEntry(entry vault.Entry) vault.Entry {
entry = cloneEntry(entry)
entry.Path = v.FromPhysicalPath(entry.Path)
for i := range entry.History {
@@ -126,7 +134,7 @@ func (v rootView) FromPhysicalEntry(entry vault.Entry) vault.Entry {
return entry
}
func (v rootView) mapEntries(entries []vault.Entry) []vault.Entry {
func (v prefixedView) mapEntries(entries []vault.Entry) []vault.Entry {
out := make([]vault.Entry, 0, len(entries))
for _, entry := range entries {
out = append(out, v.FromPhysicalEntry(entry))
@@ -138,6 +146,89 @@ type recycleBinView struct {
model vault.Model
}
type templatesView struct {
model vault.Model
}
func (v templatesView) ChildGroups(path []string) []string {
return groupChildren(templateGroupPaths(v.model), v.EntriesUnderPath(nil), path)
}
func (v templatesView) EntriesInPath(path []string) []vault.Entry {
return entriesInPath(v.EntriesUnderPath(nil), path)
}
func (v templatesView) EntriesUnderPath(path []string) []vault.Entry {
var out []vault.Entry
for _, entry := range v.model.Templates {
if len(path) > len(entry.Path) {
continue
}
physical := entry.Path
if len(physical) > 0 && physical[0] == TemplatesRoot {
physical = physical[1:]
}
if len(path) > len(physical) {
continue
}
if !slices.Equal(physical[:len(path)], path) {
continue
}
item := cloneEntry(entry)
item.Path = clonePath(physical)
for i := range item.History {
item.History[i].Path = v.FromPhysicalPath(item.History[i].Path)
}
out = append(out, item)
}
slices.SortFunc(out, func(a, b vault.Entry) int {
switch {
case a.Title < b.Title:
return -1
case a.Title > b.Title:
return 1
default:
return 0
}
})
return out
}
func (v templatesView) ToPhysicalPath(path []string) []string {
if len(path) == 0 {
return []string{TemplatesRoot}
}
return append([]string{TemplatesRoot}, clonePath(path)...)
}
func (v templatesView) FromPhysicalPath(path []string) []string {
if len(path) == 0 {
return nil
}
if path[0] != TemplatesRoot {
return clonePath(path)
}
return clonePath(path[1:])
}
func (v templatesView) ToPhysicalEntry(entry vault.Entry) vault.Entry {
entry = cloneEntry(entry)
entry.Path = v.ToPhysicalPath(entry.Path)
for i := range entry.History {
entry.History[i].Path = v.ToPhysicalPath(entry.History[i].Path)
}
return entry
}
func (v templatesView) FromPhysicalEntry(entry vault.Entry) vault.Entry {
entry = cloneEntry(entry)
entry.Path = v.FromPhysicalPath(entry.Path)
for i := range entry.History {
entry.History[i].Path = v.FromPhysicalPath(entry.History[i].Path)
}
return entry
}
func (v recycleBinView) ChildGroups(path []string) []string {
return childGroups(v.model.RecycleBin, path)
}
@@ -187,6 +278,10 @@ func (v recycleBinView) FromPhysicalEntry(entry vault.Entry) vault.Entry {
}
func childGroups(entries []vault.Entry, path []string) []string {
return groupChildren(nil, entries, path)
}
func groupChildren(groupPaths [][]string, entries []vault.Entry, path []string) []string {
seen := map[string]bool{}
var groups []string
for _, entry := range entries {
@@ -206,6 +301,23 @@ func childGroups(entries []vault.Entry, path []string) []string {
seen[group] = true
groups = append(groups, group)
}
for _, groupPath := range groupPaths {
if len(path) > len(groupPath) {
continue
}
if !slices.Equal(groupPath[:len(path)], path) {
continue
}
if len(groupPath) == len(path) {
continue
}
group := groupPath[len(path)]
if seen[group] {
continue
}
seen[group] = true
groups = append(groups, group)
}
slices.Sort(groups)
return groups
}
@@ -275,22 +387,39 @@ func clonePath(path []string) []string {
return slices.Clone(path)
}
func usesKeepassRoot(model vault.Model) bool {
if len(model.Entries) == 0 && len(model.Groups) == 0 && len(model.RecycleBin) == 0 {
return true
}
func templateGroupPaths(model vault.Model) [][]string {
var out [][]string
for _, group := range model.Groups {
if len(group) > 0 && group[0] == KeepassRoot {
return true
if len(group) == 0 || group[0] != TemplatesRoot {
continue
}
out = append(out, clonePath(group[1:]))
}
for _, entry := range model.Entries {
if len(entry.Path) > 0 && entry.Path[0] == KeepassRoot {
return true
}
return out
}
func usesTopLevelRoot(model vault.Model, root string) bool {
if len(model.Entries) == 0 && len(model.Groups) == 0 && len(model.RecycleBin) == 0 {
return root == KeepassRoot
}
for _, entry := range model.RecycleBin {
if len(entry.Path) > 0 && entry.Path[0] == KeepassRoot {
return groupsUseRoot(model.Groups, root) ||
entriesUseRoot(model.Entries, root) ||
entriesUseRoot(model.Templates, root) ||
entriesUseRoot(model.RecycleBin, root)
}
func groupsUseRoot(groups [][]string, root string) bool {
for _, group := range groups {
if len(group) > 0 && group[0] == root {
return true
}
}
return false
}
func entriesUseRoot(entries []vault.Entry, root string) bool {
for _, entry := range entries {
if len(entry.Path) > 0 && entry.Path[0] == root {
return true
}
}
+38
View File
@@ -78,6 +78,44 @@ func TestVaultRecycleBinProjectsRecycleTree(t *testing.T) {
}
}
func TestVaultTemplatesProjectsTemplatesStorageRoot(t *testing.T) {
t.Parallel()
model := vault.Model{
Templates: []vault.Entry{
{ID: "website-login", Title: "Website Login", Path: []string{"Templates", "Web"}},
{ID: "ssh-login", Title: "SSH Login", Path: []string{"Templates", "Infra"}},
},
Groups: [][]string{
{"Templates"},
{"Templates", "Infra"},
{"Templates", "Web"},
{"keepass"},
},
}
view := VaultTemplates(model)
if got := view.ChildGroups(nil); !slices.Equal(got, []string{"Infra", "Web"}) {
t.Fatalf("VaultTemplates(model).ChildGroups(nil) = %v, want [Infra Web]", got)
}
gotEntries := view.EntriesInPath([]string{"Web"})
if len(gotEntries) != 1 || !slices.Equal(gotEntries[0].Path, []string{"Web"}) {
t.Fatalf("VaultTemplates(model).EntriesInPath([Web]) = %#v, want logical path [Web]", gotEntries)
}
if got := view.ToPhysicalPath(nil); !slices.Equal(got, []string{"Templates"}) {
t.Fatalf("VaultTemplates(model).ToPhysicalPath(nil) = %v, want [Templates]", got)
}
if got := view.ToPhysicalPath([]string{"Web"}); !slices.Equal(got, []string{"Templates", "Web"}) {
t.Fatalf("VaultTemplates(model).ToPhysicalPath([Web]) = %v, want [Templates Web]", got)
}
if got := view.FromPhysicalPath([]string{"Templates", "Web"}); !slices.Equal(got, []string{"Web"}) {
t.Fatalf("VaultTemplates(model).FromPhysicalPath([Templates Web]) = %v, want [Web]", got)
}
}
func TestVaultReturnsPhysicalPathsUnchanged(t *testing.T) {
t.Parallel()