package appstate import ( "errors" "fmt" "slices" "strings" "time" "git.julianfamily.org/keepassgo/internal/apiapproval" "git.julianfamily.org/keepassgo/internal/apiaudit" "git.julianfamily.org/keepassgo/internal/apitokens" "git.julianfamily.org/keepassgo/internal/vault" "git.julianfamily.org/keepassgo/internal/vaultview" "git.julianfamily.org/keepassgo/internal/webdav" ) type Section string var ( ErrAttachmentAlreadyExists = errors.New("attachment already exists") ErrAttachmentNotFound = errors.New("attachment not found") ) const ( SectionEntries Section = "" SectionTemplates Section = "templates" SectionRecycleBin Section = "recycle-bin" SectionAPITokens Section = "api-tokens" SectionAPIAudit Section = "api-audit" SectionAbout Section = "about" ) const entriesRootLabel = "Root" const templatesRootLabel = "Templates" type CurrentSession interface { Current() (vault.Model, error) } type MutableSession interface { CurrentSession Replace(vault.Model) } type LockableSession interface { CurrentSession Lock() error Unlock(vault.MasterKey) error } type MasterKeyChangeableSession interface { CurrentSession ChangeMasterKey(vault.MasterKey) error } type SaveableSession interface { CurrentSession Save() error } type AutoSaveableSession interface { SaveableSession HasSaveTarget() bool } type RemoteAwareSession interface { IsRemote() bool } type SynchronizableSession interface { CurrentSession Synchronize() error } type AdvancedSynchronizableSession interface { CurrentSession SynchronizeFromLocal(string) error SynchronizeFromLocalBytes(string, []byte) error SynchronizeToLocal(string) error SynchronizeFromRemote(webdav.Client, string) error SynchronizeToRemote(webdav.Client, string) error } type CreateableSession interface { CurrentSession Create(vault.Model, vault.MasterKey) error } type OpenableSession interface { CurrentSession Open(string, vault.MasterKey) error } type SaveAsSession interface { CurrentSession SaveAs(string) error } type RemoteOpenableSession interface { CurrentSession OpenRemote(webdav.Client, string, vault.MasterKey) error } type WarningSession interface { ConsumeWarning() string } type SecurityConfigurableSession interface { ConfigureSecurity(vault.SecuritySettings) error SecuritySettings() vault.SecuritySettings } type ApprovalManager interface { Pending() []apiapproval.Request Resolve(string, apiapproval.Outcome) (apiapproval.Request, *apitokens.PolicyRule, error) } type State struct { Session CurrentSession Approvals ApprovalManager AuditLog *apiaudit.Log AutoSaveRemote bool Section Section CurrentPath []string SearchQuery string SelectedEntryID string Dirty bool StatusMessage string ErrorMessage string } func (s *State) PendingApprovals() []apiapproval.Request { if s.Approvals == nil { return nil } return s.Approvals.Pending() } func (s *State) ResolveApproval(id string, outcome apiapproval.Outcome) error { if s.Approvals == nil { return fmt.Errorf("approval manager is not configured") } _, _, err := s.Approvals.Resolve(id, outcome) return err } func (s *State) APITokens() ([]apitokens.Token, error) { model, err := s.currentModel() if err != nil { return nil, err } return apitokens.Entries(model) } func (s *State) RemoteProfiles() ([]vault.RemoteProfile, error) { model, err := s.currentModel() if err != nil { return nil, err } profiles := slices.Clone(model.RemoteProfiles) slices.SortFunc(profiles, func(a, b vault.RemoteProfile) int { switch { case a.Name < b.Name: return -1 case a.Name > b.Name: return 1 case a.ID < b.ID: return -1 case a.ID > b.ID: return 1 default: return 0 } }) return profiles, nil } func (s *State) RemoteCredentialEntries() ([]vault.Entry, error) { model, err := s.currentModel() if err != nil { return nil, err } entries := slices.Clone(model.Entries) slices.SortFunc(entries, func(a, b vault.Entry) int { switch { case a.Title < b.Title: return -1 case a.Title > b.Title: return 1 case a.ID < b.ID: return -1 case a.ID > b.ID: return 1 default: return 0 } }) return entries, nil } func (s *State) IssueAPIToken(name, clientName string, expiresAt *time.Time, now time.Time) (apitokens.Token, string, error) { result, err := s.mutateAPITokens(apiaudit.EventTokenIssued, "issued API token", func(model *vault.Model) (tokenMutationResult, error) { token, secret, err := apitokens.Issue(name, clientName, expiresAt, now) if err != nil { return tokenMutationResult{}, err } apitokens.Upsert(model, token) return tokenMutationResult{token: token, secret: secret}, nil }) if err != nil { return apitokens.Token{}, "", err } return result.token, result.secret, nil } func (s *State) RotateAPIToken(id string, now time.Time) (apitokens.Token, string, error) { result, err := s.mutateAPITokens(apiaudit.EventTokenRotated, "rotated API token", func(model *vault.Model) (tokenMutationResult, error) { token, err := apitokens.Find(*model, id) if err != nil { return tokenMutationResult{}, err } token, secret, err := apitokens.Rotate(token, now) if err != nil { return tokenMutationResult{}, err } apitokens.Upsert(model, token) return tokenMutationResult{token: token, secret: secret}, nil }) if err != nil { return apitokens.Token{}, "", err } return result.token, result.secret, nil } func (s *State) UpsertAPIToken(token apitokens.Token) error { _, err := s.mutateAPITokens(apiaudit.EventTokenUpdated, "updated API token", func(model *vault.Model) (tokenMutationResult, error) { apitokens.Upsert(model, token) return tokenMutationResult{token: token}, nil }) return err } func (s *State) DisableAPIToken(id string) error { _, err := s.mutateAPITokens(apiaudit.EventTokenDisabled, "disabled API token", func(model *vault.Model) (tokenMutationResult, error) { token, err := apitokens.Find(*model, id) if err != nil { return tokenMutationResult{}, err } token = apitokens.Disable(token) apitokens.Upsert(model, token) return tokenMutationResult{token: token}, nil }) return err } func (s *State) RevokeAPIToken(id string, when time.Time) error { _, err := s.mutateAPITokens(apiaudit.EventTokenRevoked, "revoked API token", func(model *vault.Model) (tokenMutationResult, error) { token, err := apitokens.Find(*model, id) if err != nil { return tokenMutationResult{}, err } token = apitokens.Revoke(token, when) apitokens.Upsert(model, token) return tokenMutationResult{token: token}, nil }) return err } func (s *State) DeleteAPIToken(id string) error { _, err := s.mutateAPITokens(apiaudit.EventTokenDeleted, "deleted API token", func(model *vault.Model) (tokenMutationResult, error) { token, err := apitokens.Find(*model, id) if err != nil { return tokenMutationResult{}, err } if err := apitokens.Delete(model, id); err != nil { return tokenMutationResult{}, err } return tokenMutationResult{token: token}, nil }) return err } type tokenMutationResult struct { token apitokens.Token secret string } func (s *State) mutateAPITokens(eventType apiaudit.EventType, message string, mutate func(*vault.Model) (tokenMutationResult, error)) (tokenMutationResult, error) { session, ok := s.Session.(MutableSession) if !ok { return tokenMutationResult{}, fmt.Errorf("session is not mutable") } model, err := session.Current() if err != nil { return tokenMutationResult{}, err } result, err := mutate(&model) if err != nil { return tokenMutationResult{}, err } session.Replace(model) if err := s.markDirtyAndAutoSave(); err != nil { return tokenMutationResult{}, err } s.recordTokenAudit(eventType, result.token, message) return result, nil } func (s *State) recordTokenAudit(eventType apiaudit.EventType, token apitokens.Token, message string) { if s.AuditLog == nil { return } s.AuditLog.Record(apiaudit.Event{ Type: eventType, TokenID: token.ID, TokenName: token.Name, ClientName: token.ClientName, Resource: apitokens.Resource{ Kind: apitokens.ResourceEntry, Path: apitokens.EntryPath, EntryID: token.ID, }, Message: message, }) } func (s *State) SecuritySettings() (vault.SecuritySettings, error) { security, ok := s.Session.(SecurityConfigurableSession) if !ok { return vault.SecuritySettings{}, fmt.Errorf("session does not expose security settings") } return security.SecuritySettings(), nil } func (s *State) ConfigureSecurity(settings vault.SecuritySettings) error { security, ok := s.Session.(SecurityConfigurableSession) if !ok { return fmt.Errorf("session does not expose security settings") } if err := security.ConfigureSecurity(settings); err != nil { return err } return s.markDirtyAndAutoSave() } func (s *State) ShowSection(section Section) { s.Section = section s.CurrentPath = nil s.SelectedEntryID = "" } func (s *State) SetSearchQuery(query string) { s.SearchQuery = query } func (s *State) BeginNewEntry() { s.SelectedEntryID = "" s.StatusMessage = "" s.ErrorMessage = "" } func (s *State) SetActionResult(label string, err error) { if err != nil { s.ErrorMessage = err.Error() s.StatusMessage = "" return } s.ErrorMessage = "" s.StatusMessage = label + " complete" } func (s *State) VisibleEntries() ([]vault.Entry, error) { model, err := s.currentModel() if err != nil { return nil, err } entries := s.entriesForSection(model) if strings.TrimSpace(s.SearchQuery) != "" { return filterEntries(entries, s.SearchQuery), nil } if s.Section == SectionEntries { return entriesInPath(entries, logicalEntriesPathForModel(model, s.CurrentPath)), nil } if s.Section == SectionRecycleBin || len(s.CurrentPath) == 0 { return entries, nil } return entriesInPath(entries, s.CurrentPath), nil } func (s *State) ChildGroups() ([]string, error) { if strings.TrimSpace(s.SearchQuery) != "" { return nil, nil } model, err := s.currentModel() if err != nil { return nil, err } if s.Section != SectionEntries { if s.Section == SectionTemplates { return vaultview.VaultTemplates(model).ChildGroups(templatesViewPath(s.CurrentPath)), nil } return childGroups(s.entriesForSection(model), s.CurrentPath), nil } return vaultview.VaultRoot(model).ChildGroups(entriesViewPathForModel(model, s.CurrentPath)), nil } func (s *State) SelectVisibleIndex(index int) error { entries, err := s.VisibleEntries() if err != nil { return err } if index < 0 || index >= len(entries) { return fmt.Errorf("visible index %d out of range", index) } s.SelectedEntryID = entries[index].ID return nil } func (s *State) ToggleVisibleIndex(index int) error { entries, err := s.VisibleEntries() if err != nil { return err } if index < 0 || index >= len(entries) { return fmt.Errorf("visible index %d out of range", index) } if s.SelectedEntryID == entries[index].ID { s.SelectedEntryID = "" return nil } s.SelectedEntryID = entries[index].ID return nil } func (s *State) currentModel() (vault.Model, error) { if s.Session == nil { return vault.Model{}, nil } return s.Session.Current() } func (s *State) entriesForSection(model vault.Model) []vault.Entry { switch s.Section { case SectionTemplates: return logicalTemplateEntries(vaultview.VaultTemplates(model).EntriesUnderPath(nil)) case SectionRecycleBin: return logicalEntries(vaultview.VaultRecycleBin(model).EntriesUnderPath(nil)) case SectionAPITokens, SectionAPIAudit, SectionAbout: return nil default: return logicalEntries(vaultview.VaultRoot(model).EntriesUnderPath(nil)) } } func (s State) SearchPathContext(entry vault.Entry) string { path := slices.Clone(entry.Path) switch s.Section { case SectionTemplates: path = logicalTemplatePath(path) case SectionRecycleBin: path = append([]string{"Recycle Bin"}, logicalEntriesPath(path)...) case SectionEntries: path = logicalEntriesPath(path) } return strings.Join(path, " / ") } func entriesInPath(entries []vault.Entry, path []string) []vault.Entry { var out []vault.Entry for _, entry := range entries { if slices.Equal(entry.Path, path) { out = append(out, entry) } } 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 filterEntries(entries []vault.Entry, query string) []vault.Entry { query = strings.TrimSpace(strings.ToLower(query)) if query == "" { return nil } var out []vault.Entry for _, entry := range entries { haystack := strings.ToLower( entry.Title + " " + entry.Username + " " + entry.URL + " " + strings.Join(entry.Path, " "), ) if !strings.Contains(haystack, query) { continue } out = append(out, entry) } 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 logicalEntriesPathForModel(model vault.Model, path []string) []string { if len(path) == 0 { return []string{entriesRootLabel} } if path[0] == entriesRootLabel { return append([]string(nil), path...) } if usesPhysicalEntriesRoot(model) && path[0] == vaultview.KeepassRoot { path = path[1:] } return append([]string{entriesRootLabel}, append([]string(nil), path...)...) } func logicalEntriesPath(path []string) []string { if len(path) == 0 { return []string{entriesRootLabel} } if path[0] == entriesRootLabel { return append([]string(nil), path...) } if path[0] == vaultview.KeepassRoot { path = path[1:] } 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 } switch { case usesPhysicalEntriesRoot(model) && path[0] == entriesRootLabel: return append([]string(nil), path[1:]...) case usesLogicalEntriesRoot(model): return append([]string(nil), path...) case path[0] == entriesRootLabel: return append([]string(nil), path[1:]...) default: return append([]string(nil), path...) } } func logicalEntry(entry vault.Entry) vault.Entry { entry.Path = logicalEntriesPath(entry.Path) for i := range entry.History { entry.History[i] = logicalEntry(entry.History[i]) } return entry } func logicalEntries(entries []vault.Entry) []vault.Entry { if len(entries) == 0 { return nil } out := make([]vault.Entry, len(entries)) for i := range entries { out[i] = logicalEntry(entries[i]) } 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 { entry.History[i] = entryForModel(model, entry.History[i]) } 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 } for _, group := range model.Groups { if len(group) > 0 && group[0] == vaultview.KeepassRoot { return true } } for _, entry := range model.Entries { if len(entry.Path) > 0 && entry.Path[0] == vaultview.KeepassRoot { return true } } for _, entry := range model.RecycleBin { if len(entry.Path) > 0 && entry.Path[0] == vaultview.KeepassRoot { return true } } return false } func usesLogicalEntriesRoot(model vault.Model) bool { for _, group := range model.Groups { if len(group) > 0 && group[0] == entriesRootLabel { return true } } for _, entry := range model.Entries { if len(entry.Path) > 0 && entry.Path[0] == entriesRootLabel { return true } } for _, entry := range model.RecycleBin { if len(entry.Path) > 0 && entry.Path[0] == entriesRootLabel { return true } } return false } func childGroups(entries []vault.Entry, path []string) []string { seen := map[string]bool{} var groups []string for _, entry := range entries { if len(path) > len(entry.Path) { continue } if !slices.Equal(entry.Path[:len(path)], path) { continue } if len(entry.Path) == len(path) { continue } group := entry.Path[len(path)] if seen[group] { continue } seen[group] = true groups = append(groups, group) } slices.Sort(groups) 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 { return fmt.Errorf("session is not mutable") } model, err := session.Current() if err != nil { return err } if err := model.DeleteEntry(s.SelectedEntryID); err != nil { return err } session.Replace(model) s.SelectedEntryID = "" return s.markDirtyAndAutoSave() } func (s *State) RestoreEntry(id string) error { session, ok := s.Session.(MutableSession) if !ok { return fmt.Errorf("session is not mutable") } model, err := session.Current() if err != nil { return err } if err := model.RestoreEntry(id); err != nil { return err } session.Replace(model) return s.markDirtyAndAutoSave() } func (s *State) UpsertEntry(entry vault.Entry) error { session, ok := s.Session.(MutableSession) if !ok { return fmt.Errorf("session is not mutable") } model, err := session.Current() if err != nil { return err } model.UpsertEntry(vaultview.VaultRoot(model).ToPhysicalEntry(entryForModel(model, entry))) session.Replace(model) s.SelectedEntryID = entry.ID return s.markDirtyAndAutoSave() } func (s *State) UpsertTemplate(entry vault.Entry) error { session, ok := s.Session.(MutableSession) if !ok { return fmt.Errorf("session is not mutable") } model, err := session.Current() if err != nil { return err } model.UpsertTemplate(vaultview.VaultTemplates(model).ToPhysicalEntry(templateEntryForModel(entry))) session.Replace(model) s.SelectedEntryID = entry.ID return s.markDirtyAndAutoSave() } func (s *State) InstantiateTemplate(templateID string, overrides vault.Entry) (vault.Entry, error) { session, ok := s.Session.(MutableSession) if !ok { return vault.Entry{}, fmt.Errorf("session is not mutable") } model, err := session.Current() if err != nil { return vault.Entry{}, err } entry, err := model.InstantiateTemplate(templateID, vaultview.VaultRoot(model).ToPhysicalEntry(entryForModel(model, overrides))) if err != nil { return vault.Entry{}, err } session.Replace(model) s.SelectedEntryID = entry.ID if err := s.markDirtyAndAutoSave(); err != nil { return vault.Entry{}, err } return logicalEntry(entry), nil } func (s *State) DeleteTemplate(id string) error { session, ok := s.Session.(MutableSession) if !ok { return fmt.Errorf("session is not mutable") } model, err := session.Current() if err != nil { return err } if err := model.DeleteTemplate(id); err != nil { return err } session.Replace(model) if s.SelectedEntryID == id { s.SelectedEntryID = "" } return s.markDirtyAndAutoSave() } func (s *State) DuplicateSelectedEntry(duplicateID string) (vault.Entry, error) { session, ok := s.Session.(MutableSession) if !ok { return vault.Entry{}, fmt.Errorf("session is not mutable") } model, err := session.Current() if err != nil { return vault.Entry{}, err } duplicate, err := model.DuplicateEntry(s.SelectedEntryID, duplicateID) if err != nil { return vault.Entry{}, err } session.Replace(model) s.SelectedEntryID = duplicate.ID if err := s.markDirtyAndAutoSave(); err != nil { return vault.Entry{}, err } return duplicate, nil } func (s *State) RestoreSelectedEntryVersion(historyIndex int) error { session, ok := s.Session.(MutableSession) if !ok { return fmt.Errorf("session is not mutable") } model, err := session.Current() if err != nil { return err } if err := model.RestoreEntryVersion(s.SelectedEntryID, historyIndex); err != nil { return err } session.Replace(model) return s.markDirtyAndAutoSave() } func (s *State) Lock() error { session, ok := s.Session.(LockableSession) if !ok { return fmt.Errorf("session is not lockable") } if err := session.Lock(); err != nil { return err } s.SelectedEntryID = "" return nil } func (s *State) Unlock(key vault.MasterKey) error { session, ok := s.Session.(LockableSession) if !ok { return fmt.Errorf("session is not lockable") } if err := session.Unlock(key); err != nil { return err } if warningSession, ok := s.Session.(WarningSession); ok { s.StatusMessage = warningSession.ConsumeWarning() } return nil } func (s *State) ChangeMasterKey(key vault.MasterKey) error { session, ok := s.Session.(MasterKeyChangeableSession) if !ok { return fmt.Errorf("session does not support master key changes") } if err := session.ChangeMasterKey(key); err != nil { return err } return s.markDirtyAndAutoSave() } func (s *State) EnterGroup(name string) { s.CurrentPath = append(append([]string(nil), s.CurrentPath...), name) s.SelectedEntryID = "" } func (s *State) NavigateToPath(path []string) { s.CurrentPath = append([]string(nil), path...) s.SelectedEntryID = "" } func (s *State) Save() error { session, ok := s.Session.(SaveableSession) if !ok { return fmt.Errorf("session is not saveable") } if err := session.Save(); err != nil { return err } s.Dirty = false return nil } func (s *State) markDirtyAndAutoSave() error { s.Dirty = true session, ok := s.Session.(SaveableSession) if !ok { return nil } if autosave, ok := s.Session.(AutoSaveableSession); ok && !autosave.HasSaveTarget() { return nil } if remote, ok := s.Session.(RemoteAwareSession); ok && remote.IsRemote() && !s.AutoSaveRemote { return nil } if err := session.Save(); err != nil { return err } s.Dirty = false return nil } func (s *State) Synchronize() error { session, ok := s.Session.(SynchronizableSession) if !ok { return fmt.Errorf("session is not synchronizable") } if err := session.Synchronize(); err != nil { return err } s.Dirty = false return nil } func (s *State) SynchronizeFromLocal(path string) error { session, ok := s.Session.(AdvancedSynchronizableSession) if !ok { return fmt.Errorf("session is not advanced-synchronizable") } if err := session.SynchronizeFromLocal(path); err != nil { return err } s.Dirty = false return nil } func (s *State) SynchronizeFromLocalBytes(name string, content []byte) error { session, ok := s.Session.(AdvancedSynchronizableSession) if !ok { return fmt.Errorf("session is not advanced-synchronizable") } if err := session.SynchronizeFromLocalBytes(name, content); err != nil { return err } s.Dirty = false return nil } func (s *State) SynchronizeToLocal(path string) error { session, ok := s.Session.(AdvancedSynchronizableSession) if !ok { return fmt.Errorf("session is not advanced-synchronizable") } if err := session.SynchronizeToLocal(path); err != nil { return err } s.Dirty = true return nil } func (s *State) SynchronizeFromRemote(client webdav.Client, path string) error { session, ok := s.Session.(AdvancedSynchronizableSession) if !ok { return fmt.Errorf("session is not advanced-synchronizable") } if err := session.SynchronizeFromRemote(client, path); err != nil { return err } s.Dirty = false return nil } func (s *State) SynchronizeToRemote(client webdav.Client, path string) error { session, ok := s.Session.(AdvancedSynchronizableSession) if !ok { return fmt.Errorf("session is not advanced-synchronizable") } if err := session.SynchronizeToRemote(client, path); err != nil { return err } s.Dirty = true return nil } func (s *State) CreateVault(key vault.MasterKey) error { session, ok := s.Session.(CreateableSession) if !ok { return fmt.Errorf("session is not createable") } if err := session.Create(vault.Model{}, key); err != nil { return err } s.CurrentPath = nil s.SelectedEntryID = "" s.Dirty = false return nil } func (s *State) OpenVault(path string, key vault.MasterKey) error { session, ok := s.Session.(OpenableSession) if !ok { return fmt.Errorf("session is not openable") } if err := session.Open(path, key); err != nil { return err } s.CurrentPath = nil s.SelectedEntryID = "" s.Dirty = false if warningSession, ok := s.Session.(WarningSession); ok { s.StatusMessage = warningSession.ConsumeWarning() } return nil } func (s *State) SaveAs(path string) error { session, ok := s.Session.(SaveAsSession) if !ok { return fmt.Errorf("session is not save-as capable") } if err := session.SaveAs(path); err != nil { return err } s.Dirty = false return nil } func (s *State) OpenRemoteVault(client webdav.Client, path string, key vault.MasterKey) error { session, ok := s.Session.(RemoteOpenableSession) if !ok { return fmt.Errorf("session is not remote-openable") } if err := session.OpenRemote(client, path, key); err != nil { return err } s.CurrentPath = nil s.SelectedEntryID = "" s.Dirty = false if warningSession, ok := s.Session.(WarningSession); ok { s.StatusMessage = warningSession.ConsumeWarning() } return nil } func (s *State) OpenBoundRemoteVault(binding RemoteBinding, key vault.MasterKey) error { model, err := s.currentModel() if err != nil { return err } resolved, err := binding.Resolve(model) if err != nil { return err } client := webdav.Client{ BaseURL: resolved.Profile.BaseURL, Username: resolved.Credentials.Username, Password: resolved.Credentials.Password, } return s.OpenRemoteVault(client, resolved.Profile.Path, key) } func (s *State) ConfigureRemoteBinding(input RemoteBindingInput) (RemoteBinding, error) { session, ok := s.Session.(MutableSession) if !ok { return RemoteBinding{}, fmt.Errorf("session is not mutable") } model, err := session.Current() if err != nil { return RemoteBinding{}, err } binding, err := ConfigureRemoteBinding(&model, input) if err != nil { return RemoteBinding{}, err } session.Replace(model) if err := s.markDirtyAndAutoSave(); err != nil { return RemoteBinding{}, err } return binding, nil } func (s *State) RemoveRemoteBinding(binding RemoteBinding) error { session, ok := s.Session.(MutableSession) if !ok { return fmt.Errorf("session is not mutable") } model, err := session.Current() if err != nil { return err } if err := RemoveRemoteBinding(&model, binding); err != nil { return err } session.Replace(model) return s.markDirtyAndAutoSave() } func (s *State) CreateGroup(name string) error { session, ok := s.Session.(MutableSession) if !ok { return fmt.Errorf("session is not mutable") } model, err := session.Current() if err != nil { return err } view := sectionGroupView(model, s.Section) model.CreateGroup(view.ToPhysicalPath(sectionGroupViewPath(model, s.Section, s.CurrentPath)), name) session.Replace(model) return s.markDirtyAndAutoSave() } func (s *State) MoveCurrentGroup(parent []string) error { session, ok := s.Session.(MutableSession) if !ok { return fmt.Errorf("session is not mutable") } model, err := session.Current() if err != nil { return err } 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 = sectionGroupLogicalPath(model, s.Section, append(append([]string(nil), parentViewPath...), currentViewPath[len(currentViewPath)-1])) } return s.markDirtyAndAutoSave() } func (s *State) RenameCurrentGroup(newName string) error { session, ok := s.Session.(MutableSession) if !ok { return fmt.Errorf("session is not mutable") } model, err := session.Current() if err != nil { return err } view := sectionGroupView(model, s.Section) if err := model.RenameGroup(view.ToPhysicalPath(sectionGroupViewPath(model, s.Section, s.CurrentPath)), newName); err != nil { return err } session.Replace(model) if len(s.CurrentPath) > 0 { s.CurrentPath = append(append([]string(nil), s.CurrentPath[:len(s.CurrentPath)-1]...), newName) } return s.markDirtyAndAutoSave() } func (s *State) MoveSelectedEntry(path []string) error { session, ok := s.Session.(MutableSession) if !ok { return fmt.Errorf("session is not mutable") } model, err := session.Current() if err != nil { return err } if err := model.MoveEntry(s.SelectedEntryID, vaultview.VaultRoot(model).ToPhysicalPath(entriesViewPathForModel(model, path))); err != nil { return err } session.Replace(model) return s.markDirtyAndAutoSave() } func (s *State) DeleteCurrentGroup() error { session, ok := s.Session.(MutableSession) if !ok { return fmt.Errorf("session is not mutable") } model, err := session.Current() if err != nil { return err } view := sectionGroupView(model, s.Section) if err := model.DeleteGroup(view.ToPhysicalPath(sectionGroupViewPath(model, s.Section, s.CurrentPath))); err != nil { return err } session.Replace(model) if len(s.CurrentPath) > 0 { s.CurrentPath = append([]string(nil), s.CurrentPath[:len(s.CurrentPath)-1]...) } s.SelectedEntryID = "" return s.markDirtyAndAutoSave() } func (s *State) AddAttachmentToSelectedEntry(name string, content []byte) error { session, ok := s.Session.(MutableSession) if !ok { return fmt.Errorf("session is not mutable") } model, err := session.Current() if err != nil { return err } for i := range model.Entries { if model.Entries[i].ID != s.SelectedEntryID { continue } if model.Entries[i].Attachments == nil { model.Entries[i].Attachments = map[string][]byte{} } if _, exists := model.Entries[i].Attachments[name]; exists { return ErrAttachmentAlreadyExists } model.Entries[i].Attachments[name] = append([]byte(nil), content...) session.Replace(model) return s.markDirtyAndAutoSave() } return vault.ErrEntryNotFound } func (s *State) ReplaceAttachmentOnSelectedEntry(name string, content []byte) error { session, ok := s.Session.(MutableSession) if !ok { return fmt.Errorf("session is not mutable") } model, err := session.Current() if err != nil { return err } for i := range model.Entries { if model.Entries[i].ID != s.SelectedEntryID { continue } if _, exists := model.Entries[i].Attachments[name]; !exists { return ErrAttachmentNotFound } model.Entries[i].Attachments[name] = append([]byte(nil), content...) session.Replace(model) return s.markDirtyAndAutoSave() } return vault.ErrEntryNotFound } func (s *State) DeleteAttachmentFromSelectedEntry(name string) error { session, ok := s.Session.(MutableSession) if !ok { return fmt.Errorf("session is not mutable") } model, err := session.Current() if err != nil { return err } for i := range model.Entries { if model.Entries[i].ID != s.SelectedEntryID { continue } if _, exists := model.Entries[i].Attachments[name]; !exists { return ErrAttachmentNotFound } delete(model.Entries[i].Attachments, name) if len(model.Entries[i].Attachments) == 0 { model.Entries[i].Attachments = nil } session.Replace(model) return s.markDirtyAndAutoSave() } return vault.ErrEntryNotFound }