Add explicit templates vault view
This commit is contained in:
+92
-15
@@ -32,6 +32,7 @@ const (
|
||||
)
|
||||
|
||||
const entriesRootLabel = "Root"
|
||||
const templatesRootLabel = "Templates"
|
||||
|
||||
type CurrentSession interface {
|
||||
Current() (vault.Model, error)
|
||||
@@ -402,8 +403,8 @@ func (s *State) ChildGroups() ([]string, error) {
|
||||
}
|
||||
|
||||
if s.Section != SectionEntries {
|
||||
if s.Section == SectionTemplates && len(s.CurrentPath) == 0 {
|
||||
return childGroups(s.entriesForSection(model), []string{"Templates"}), nil
|
||||
if s.Section == SectionTemplates {
|
||||
return vaultview.VaultTemplates(model).ChildGroups(templatesViewPath(s.CurrentPath)), nil
|
||||
}
|
||||
return childGroups(s.entriesForSection(model), s.CurrentPath), nil
|
||||
}
|
||||
@@ -452,7 +453,7 @@ func (s *State) currentModel() (vault.Model, error) {
|
||||
func (s *State) entriesForSection(model vault.Model) []vault.Entry {
|
||||
switch s.Section {
|
||||
case SectionTemplates:
|
||||
return slices.Clone(model.Templates)
|
||||
return logicalTemplateEntries(vaultview.VaultTemplates(model).EntriesUnderPath(nil))
|
||||
case SectionRecycleBin:
|
||||
return logicalEntries(vaultview.VaultRecycleBin(model).EntriesUnderPath(nil))
|
||||
case SectionAPITokens, SectionAPIAudit, SectionAbout:
|
||||
@@ -466,9 +467,7 @@ func (s State) SearchPathContext(entry vault.Entry) string {
|
||||
path := slices.Clone(entry.Path)
|
||||
switch s.Section {
|
||||
case SectionTemplates:
|
||||
if len(path) == 0 || path[0] != "Templates" {
|
||||
path = append([]string{"Templates"}, path...)
|
||||
}
|
||||
path = logicalTemplatePath(path)
|
||||
case SectionRecycleBin:
|
||||
path = append([]string{"Recycle Bin"}, logicalEntriesPath(path)...)
|
||||
case SectionEntries:
|
||||
@@ -555,6 +554,26 @@ func logicalEntriesPath(path []string) []string {
|
||||
return append([]string{entriesRootLabel}, append([]string(nil), path...)...)
|
||||
}
|
||||
|
||||
func logicalTemplatePath(path []string) []string {
|
||||
if len(path) == 0 {
|
||||
return []string{templatesRootLabel}
|
||||
}
|
||||
if path[0] == templatesRootLabel {
|
||||
return append([]string(nil), path...)
|
||||
}
|
||||
return append([]string{templatesRootLabel}, append([]string(nil), path...)...)
|
||||
}
|
||||
|
||||
func templatesViewPath(path []string) []string {
|
||||
if len(path) == 0 {
|
||||
return nil
|
||||
}
|
||||
if path[0] == templatesRootLabel {
|
||||
return append([]string(nil), path[1:]...)
|
||||
}
|
||||
return append([]string(nil), path...)
|
||||
}
|
||||
|
||||
func entriesViewPathForModel(model vault.Model, path []string) []string {
|
||||
if len(path) == 0 {
|
||||
return nil
|
||||
@@ -590,6 +609,25 @@ func logicalEntries(entries []vault.Entry) []vault.Entry {
|
||||
return out
|
||||
}
|
||||
|
||||
func logicalTemplateEntry(entry vault.Entry) vault.Entry {
|
||||
entry.Path = logicalTemplatePath(entry.Path)
|
||||
for i := range entry.History {
|
||||
entry.History[i] = logicalTemplateEntry(entry.History[i])
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
func logicalTemplateEntries(entries []vault.Entry) []vault.Entry {
|
||||
if len(entries) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]vault.Entry, len(entries))
|
||||
for i := range entries {
|
||||
out[i] = logicalTemplateEntry(entries[i])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func entryForModel(model vault.Model, entry vault.Entry) vault.Entry {
|
||||
entry.Path = entriesViewPathForModel(model, entry.Path)
|
||||
for i := range entry.History {
|
||||
@@ -598,6 +636,14 @@ func entryForModel(model vault.Model, entry vault.Entry) vault.Entry {
|
||||
return entry
|
||||
}
|
||||
|
||||
func templateEntryForModel(entry vault.Entry) vault.Entry {
|
||||
entry.Path = templatesViewPath(entry.Path)
|
||||
for i := range entry.History {
|
||||
entry.History[i] = templateEntryForModel(entry.History[i])
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
func usesPhysicalEntriesRoot(model vault.Model) bool {
|
||||
if len(model.Entries) == 0 && len(model.Groups) == 0 && len(model.RecycleBin) == 0 {
|
||||
return true
|
||||
@@ -663,6 +709,33 @@ func childGroups(entries []vault.Entry, path []string) []string {
|
||||
return groups
|
||||
}
|
||||
|
||||
func sectionGroupView(model vault.Model, section Section) vaultview.View {
|
||||
switch section {
|
||||
case SectionTemplates:
|
||||
return vaultview.VaultTemplates(model)
|
||||
default:
|
||||
return vaultview.VaultRoot(model)
|
||||
}
|
||||
}
|
||||
|
||||
func sectionGroupViewPath(model vault.Model, section Section, path []string) []string {
|
||||
switch section {
|
||||
case SectionTemplates:
|
||||
return templatesViewPath(path)
|
||||
default:
|
||||
return entriesViewPathForModel(model, path)
|
||||
}
|
||||
}
|
||||
|
||||
func sectionGroupLogicalPath(model vault.Model, section Section, path []string) []string {
|
||||
switch section {
|
||||
case SectionTemplates:
|
||||
return logicalTemplatePath(path)
|
||||
default:
|
||||
return logicalEntriesPathForModel(model, path)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) DeleteSelectedEntry() error {
|
||||
session, ok := s.Session.(MutableSession)
|
||||
if !ok {
|
||||
@@ -730,7 +803,7 @@ func (s *State) UpsertTemplate(entry vault.Entry) error {
|
||||
return err
|
||||
}
|
||||
|
||||
model.UpsertTemplate(entry)
|
||||
model.UpsertTemplate(vaultview.VaultTemplates(model).ToPhysicalEntry(templateEntryForModel(entry)))
|
||||
session.Replace(model)
|
||||
s.SelectedEntryID = entry.ID
|
||||
return s.markDirtyAndAutoSave()
|
||||
@@ -1124,7 +1197,8 @@ func (s *State) CreateGroup(name string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
model.CreateGroup(vaultview.VaultRoot(model).ToPhysicalPath(entriesViewPathForModel(model, s.CurrentPath)), name)
|
||||
view := sectionGroupView(model, s.Section)
|
||||
model.CreateGroup(view.ToPhysicalPath(sectionGroupViewPath(model, s.Section, s.CurrentPath)), name)
|
||||
session.Replace(model)
|
||||
return s.markDirtyAndAutoSave()
|
||||
}
|
||||
@@ -1138,15 +1212,16 @@ func (s *State) MoveCurrentGroup(parent []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
current := logicalEntriesPathForModel(model, s.CurrentPath)
|
||||
currentViewPath := entriesViewPathForModel(model, current)
|
||||
parentViewPath := entriesViewPathForModel(model, parent)
|
||||
if err := model.MoveGroup(vaultview.VaultRoot(model).ToPhysicalPath(currentViewPath), vaultview.VaultRoot(model).ToPhysicalPath(parentViewPath)); err != nil {
|
||||
view := sectionGroupView(model, s.Section)
|
||||
current := sectionGroupLogicalPath(model, s.Section, s.CurrentPath)
|
||||
currentViewPath := sectionGroupViewPath(model, s.Section, current)
|
||||
parentViewPath := sectionGroupViewPath(model, s.Section, parent)
|
||||
if err := model.MoveGroup(view.ToPhysicalPath(currentViewPath), view.ToPhysicalPath(parentViewPath)); err != nil {
|
||||
return err
|
||||
}
|
||||
session.Replace(model)
|
||||
if len(currentViewPath) > 0 {
|
||||
s.CurrentPath = logicalEntriesPathForModel(model, append(append([]string(nil), parentViewPath...), currentViewPath[len(currentViewPath)-1]))
|
||||
s.CurrentPath = sectionGroupLogicalPath(model, s.Section, append(append([]string(nil), parentViewPath...), currentViewPath[len(currentViewPath)-1]))
|
||||
}
|
||||
return s.markDirtyAndAutoSave()
|
||||
}
|
||||
@@ -1162,7 +1237,8 @@ func (s *State) RenameCurrentGroup(newName string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := model.RenameGroup(vaultview.VaultRoot(model).ToPhysicalPath(entriesViewPathForModel(model, s.CurrentPath)), newName); err != nil {
|
||||
view := sectionGroupView(model, s.Section)
|
||||
if err := model.RenameGroup(view.ToPhysicalPath(sectionGroupViewPath(model, s.Section, s.CurrentPath)), newName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1203,7 +1279,8 @@ func (s *State) DeleteCurrentGroup() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := model.DeleteGroup(vaultview.VaultRoot(model).ToPhysicalPath(entriesViewPathForModel(model, s.CurrentPath))); err != nil {
|
||||
view := sectionGroupView(model, s.Section)
|
||||
if err := model.DeleteGroup(view.ToPhysicalPath(sectionGroupViewPath(model, s.Section, s.CurrentPath))); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
+153
-24
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user