Files
keepassgo/internal/appstate/state.go
T
2026-04-13 07:29:51 -07:00

1301 lines
30 KiB
Go

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"
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 && len(s.CurrentPath) == 0 {
return childGroups(s.entriesForSection(model), []string{"Templates"}), 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 slices.Clone(model.Templates)
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:
if len(path) == 0 || path[0] != "Templates" {
path = append([]string{"Templates"}, 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 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 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 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 (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(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
}
model.CreateGroup(vaultview.VaultRoot(model).ToPhysicalPath(entriesViewPathForModel(model, 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
}
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 {
return err
}
session.Replace(model)
if len(currentViewPath) > 0 {
s.CurrentPath = logicalEntriesPathForModel(model, 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
}
if err := model.RenameGroup(vaultview.VaultRoot(model).ToPhysicalPath(entriesViewPathForModel(model, 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
}
if err := model.DeleteGroup(vaultview.VaultRoot(model).ToPhysicalPath(entriesViewPathForModel(model, 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
}