1166 lines
25 KiB
Go
1166 lines
25 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/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"
|
|
)
|
|
|
|
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 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 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
|
|
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) {
|
|
session, ok := s.Session.(MutableSession)
|
|
if !ok {
|
|
return apitokens.Token{}, "", fmt.Errorf("session is not mutable")
|
|
}
|
|
model, err := session.Current()
|
|
if err != nil {
|
|
return apitokens.Token{}, "", err
|
|
}
|
|
token, secret, err := apitokens.Issue(name, clientName, expiresAt, now)
|
|
if err != nil {
|
|
return apitokens.Token{}, "", err
|
|
}
|
|
apitokens.Upsert(&model, token)
|
|
session.Replace(model)
|
|
s.Dirty = true
|
|
s.recordTokenAudit(apiaudit.EventTokenIssued, token, "issued API token")
|
|
return token, secret, nil
|
|
}
|
|
|
|
func (s *State) RotateAPIToken(id string, now time.Time) (apitokens.Token, string, error) {
|
|
session, ok := s.Session.(MutableSession)
|
|
if !ok {
|
|
return apitokens.Token{}, "", fmt.Errorf("session is not mutable")
|
|
}
|
|
model, err := session.Current()
|
|
if err != nil {
|
|
return apitokens.Token{}, "", err
|
|
}
|
|
token, err := apitokens.Find(model, id)
|
|
if err != nil {
|
|
return apitokens.Token{}, "", err
|
|
}
|
|
token, secret, err := apitokens.Rotate(token, now)
|
|
if err != nil {
|
|
return apitokens.Token{}, "", err
|
|
}
|
|
apitokens.Upsert(&model, token)
|
|
session.Replace(model)
|
|
s.Dirty = true
|
|
s.recordTokenAudit(apiaudit.EventTokenRotated, token, "rotated API token")
|
|
return token, secret, nil
|
|
}
|
|
|
|
func (s *State) UpsertAPIToken(token apitokens.Token) error {
|
|
session, ok := s.Session.(MutableSession)
|
|
if !ok {
|
|
return fmt.Errorf("session is not mutable")
|
|
}
|
|
model, err := session.Current()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
apitokens.Upsert(&model, token)
|
|
session.Replace(model)
|
|
s.Dirty = true
|
|
s.recordTokenAudit(apiaudit.EventTokenUpdated, token, "updated API token")
|
|
return nil
|
|
}
|
|
|
|
func (s *State) DisableAPIToken(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
|
|
}
|
|
token, err := apitokens.Find(model, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
token = apitokens.Disable(token)
|
|
apitokens.Upsert(&model, token)
|
|
session.Replace(model)
|
|
s.Dirty = true
|
|
s.recordTokenAudit(apiaudit.EventTokenDisabled, token, "disabled API token")
|
|
return nil
|
|
}
|
|
|
|
func (s *State) RevokeAPIToken(id string, when time.Time) error {
|
|
session, ok := s.Session.(MutableSession)
|
|
if !ok {
|
|
return fmt.Errorf("session is not mutable")
|
|
}
|
|
model, err := session.Current()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
token, err := apitokens.Find(model, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
token = apitokens.Revoke(token, when)
|
|
apitokens.Upsert(&model, token)
|
|
session.Replace(model)
|
|
s.Dirty = true
|
|
s.recordTokenAudit(apiaudit.EventTokenRevoked, token, "revoked API token")
|
|
return nil
|
|
}
|
|
|
|
func (s *State) DeleteAPIToken(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
|
|
}
|
|
token, err := apitokens.Find(model, id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := apitokens.Delete(&model, id); err != nil {
|
|
return err
|
|
}
|
|
session.Replace(model)
|
|
s.Dirty = true
|
|
s.recordTokenAudit(apiaudit.EventTokenDeleted, token, "deleted API token")
|
|
return 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
|
|
}
|
|
s.Dirty = true
|
|
return nil
|
|
}
|
|
|
|
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(model.Entries, 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 model.ChildGroups(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 slices.Clone(model.RecycleBin)
|
|
case SectionAPITokens, SectionAPIAudit, SectionAbout:
|
|
return nil
|
|
default:
|
|
return slices.Clone(model.Entries)
|
|
}
|
|
}
|
|
|
|
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"}, 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 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 = ""
|
|
s.Dirty = true
|
|
return nil
|
|
}
|
|
|
|
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)
|
|
s.Dirty = true
|
|
return nil
|
|
}
|
|
|
|
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(entry)
|
|
session.Replace(model)
|
|
s.SelectedEntryID = entry.ID
|
|
s.Dirty = true
|
|
return nil
|
|
}
|
|
|
|
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
|
|
s.Dirty = true
|
|
return nil
|
|
}
|
|
|
|
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, overrides)
|
|
if err != nil {
|
|
return vault.Entry{}, err
|
|
}
|
|
|
|
session.Replace(model)
|
|
s.SelectedEntryID = entry.ID
|
|
s.Dirty = true
|
|
return 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 = ""
|
|
}
|
|
s.Dirty = true
|
|
return nil
|
|
}
|
|
|
|
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
|
|
s.Dirty = true
|
|
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)
|
|
s.Dirty = true
|
|
return nil
|
|
}
|
|
|
|
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")
|
|
}
|
|
|
|
return session.Unlock(key)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
s.Dirty = true
|
|
return nil
|
|
}
|
|
|
|
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) 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
|
|
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
|
|
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)
|
|
s.Dirty = true
|
|
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)
|
|
s.Dirty = true
|
|
return nil
|
|
}
|
|
|
|
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(s.CurrentPath, name)
|
|
session.Replace(model)
|
|
s.Dirty = true
|
|
return nil
|
|
}
|
|
|
|
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 := append([]string(nil), s.CurrentPath...)
|
|
if err := model.MoveGroup(current, parent); err != nil {
|
|
return err
|
|
}
|
|
session.Replace(model)
|
|
if len(current) > 0 {
|
|
s.CurrentPath = append(append([]string(nil), parent...), current[len(current)-1])
|
|
}
|
|
s.Dirty = true
|
|
return nil
|
|
}
|
|
|
|
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(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)
|
|
}
|
|
s.Dirty = true
|
|
return nil
|
|
}
|
|
|
|
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, path); err != nil {
|
|
return err
|
|
}
|
|
|
|
session.Replace(model)
|
|
s.Dirty = true
|
|
return nil
|
|
}
|
|
|
|
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(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 = ""
|
|
s.Dirty = true
|
|
return nil
|
|
}
|
|
|
|
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)
|
|
s.Dirty = true
|
|
return nil
|
|
}
|
|
|
|
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)
|
|
s.Dirty = true
|
|
return nil
|
|
}
|
|
|
|
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)
|
|
s.Dirty = true
|
|
return nil
|
|
}
|
|
|
|
return vault.ErrEntryNotFound
|
|
}
|