Move app packages under internal
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
package appstate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.julianfamily.org/keepassgo/internal/vault"
|
||||
)
|
||||
|
||||
type SyncMode string
|
||||
|
||||
const (
|
||||
SyncModeManual SyncMode = "manual"
|
||||
SyncModeAutomaticOnOpenSave SyncMode = "automatic_on_open_save"
|
||||
)
|
||||
|
||||
type RemoteBinding struct {
|
||||
LocalVaultPath string `json:"localVaultPath"`
|
||||
RemoteProfileID string `json:"remoteProfileId"`
|
||||
CredentialEntryID string `json:"credentialEntryId"`
|
||||
SyncMode SyncMode `json:"syncMode,omitempty"`
|
||||
}
|
||||
|
||||
type ResolvedRemoteBinding struct {
|
||||
Profile vault.RemoteProfile
|
||||
Credentials vault.Entry
|
||||
}
|
||||
|
||||
type RemoteBindingInput struct {
|
||||
LocalVaultPath string
|
||||
RemoteProfileID string
|
||||
RemoteProfileName string
|
||||
BaseURL string
|
||||
RemotePath string
|
||||
CredentialEntryID string
|
||||
CredentialTitle string
|
||||
Username string
|
||||
Password string
|
||||
CredentialPath []string
|
||||
SyncMode SyncMode
|
||||
}
|
||||
|
||||
func (b RemoteBinding) Resolve(model vault.Model) (ResolvedRemoteBinding, error) {
|
||||
profile, err := model.RemoteProfileByID(b.RemoteProfileID)
|
||||
if err != nil {
|
||||
return ResolvedRemoteBinding{}, fmt.Errorf("resolve remote profile: %w", err)
|
||||
}
|
||||
credentials, err := model.EntryByID(b.CredentialEntryID)
|
||||
if err != nil {
|
||||
return ResolvedRemoteBinding{}, fmt.Errorf("resolve remote credentials: %w", err)
|
||||
}
|
||||
return ResolvedRemoteBinding{
|
||||
Profile: profile,
|
||||
Credentials: credentials,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func ConfigureRemoteBinding(model *vault.Model, input RemoteBindingInput) (RemoteBinding, error) {
|
||||
if model == nil {
|
||||
return RemoteBinding{}, fmt.Errorf("model is required")
|
||||
}
|
||||
|
||||
input.LocalVaultPath = strings.TrimSpace(input.LocalVaultPath)
|
||||
input.RemoteProfileID = strings.TrimSpace(input.RemoteProfileID)
|
||||
input.RemoteProfileName = strings.TrimSpace(input.RemoteProfileName)
|
||||
input.BaseURL = strings.TrimSpace(input.BaseURL)
|
||||
input.RemotePath = strings.TrimSpace(input.RemotePath)
|
||||
input.CredentialEntryID = strings.TrimSpace(input.CredentialEntryID)
|
||||
input.CredentialTitle = strings.TrimSpace(input.CredentialTitle)
|
||||
input.Username = strings.TrimSpace(input.Username)
|
||||
|
||||
switch {
|
||||
case input.LocalVaultPath == "":
|
||||
return RemoteBinding{}, fmt.Errorf("local vault path is required")
|
||||
case input.RemoteProfileID == "":
|
||||
return RemoteBinding{}, fmt.Errorf("remote profile id is required")
|
||||
case input.BaseURL == "":
|
||||
return RemoteBinding{}, fmt.Errorf("remote base URL is required")
|
||||
case input.RemotePath == "":
|
||||
return RemoteBinding{}, fmt.Errorf("remote path is required")
|
||||
case input.CredentialEntryID == "":
|
||||
return RemoteBinding{}, fmt.Errorf("credential entry id is required")
|
||||
case input.Password == "":
|
||||
return RemoteBinding{}, fmt.Errorf("credential password is required")
|
||||
}
|
||||
|
||||
if input.RemoteProfileName == "" {
|
||||
input.RemoteProfileName = input.RemoteProfileID
|
||||
}
|
||||
if input.CredentialTitle == "" {
|
||||
input.CredentialTitle = "Remote Sign-In"
|
||||
}
|
||||
|
||||
model.UpsertRemoteProfile(vault.RemoteProfile{
|
||||
ID: input.RemoteProfileID,
|
||||
Name: input.RemoteProfileName,
|
||||
Backend: vault.RemoteBackendWebDAV,
|
||||
BaseURL: input.BaseURL,
|
||||
Path: input.RemotePath,
|
||||
})
|
||||
model.UpsertEntry(vault.Entry{
|
||||
ID: input.CredentialEntryID,
|
||||
Title: input.CredentialTitle,
|
||||
Username: input.Username,
|
||||
Password: input.Password,
|
||||
URL: input.BaseURL,
|
||||
Path: append([]string(nil), input.CredentialPath...),
|
||||
})
|
||||
|
||||
return RemoteBinding{
|
||||
LocalVaultPath: input.LocalVaultPath,
|
||||
RemoteProfileID: input.RemoteProfileID,
|
||||
CredentialEntryID: input.CredentialEntryID,
|
||||
SyncMode: normalizeSyncMode(input.SyncMode),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func RemoveRemoteBinding(model *vault.Model, binding RemoteBinding) error {
|
||||
if model == nil {
|
||||
return fmt.Errorf("model is required")
|
||||
}
|
||||
if strings.TrimSpace(binding.RemoteProfileID) == "" {
|
||||
return fmt.Errorf("remote profile id is required")
|
||||
}
|
||||
if strings.TrimSpace(binding.CredentialEntryID) == "" {
|
||||
return fmt.Errorf("credential entry id is required")
|
||||
}
|
||||
|
||||
model.RemoveRemoteProfileByID(binding.RemoteProfileID)
|
||||
model.RemoveEntryByID(binding.CredentialEntryID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeSyncMode(mode SyncMode) SyncMode {
|
||||
switch mode {
|
||||
case SyncModeAutomaticOnOpenSave:
|
||||
return SyncModeAutomaticOnOpenSave
|
||||
default:
|
||||
return SyncModeManual
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
package appstate
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.julianfamily.org/keepassgo/internal/vault"
|
||||
)
|
||||
|
||||
func TestRemoteBindingResolveUsesVaultProfileAndCredentialEntry(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
model := vault.Model{
|
||||
Entries: []vault.Entry{
|
||||
{
|
||||
ID: "linuscaldwell-webdav",
|
||||
Title: "Bellagio WebDAV Sign-In",
|
||||
Username: "linuscaldwell",
|
||||
Password: "bellagio-pass-1",
|
||||
Path: []string{"Crew", "Internet"},
|
||||
},
|
||||
},
|
||||
RemoteProfiles: []vault.RemoteProfile{
|
||||
{
|
||||
ID: "bellagio-webdav",
|
||||
Name: "Bellagio Vault",
|
||||
Backend: vault.RemoteBackendWebDAV,
|
||||
BaseURL: "https://dav.example.invalid/remote.php/dav",
|
||||
Path: "files/bellagio/keepass.kdbx",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
binding := RemoteBinding{
|
||||
LocalVaultPath: "/tmp/bellagio.kdbx",
|
||||
RemoteProfileID: "bellagio-webdav",
|
||||
CredentialEntryID: "linuscaldwell-webdav",
|
||||
SyncMode: SyncModeAutomaticOnOpenSave,
|
||||
}
|
||||
|
||||
resolved, err := binding.Resolve(model)
|
||||
if err != nil {
|
||||
t.Fatalf("Resolve() error = %v", err)
|
||||
}
|
||||
if got := resolved.Profile.BaseURL; got != "https://dav.example.invalid/remote.php/dav" {
|
||||
t.Fatalf("resolved profile base URL = %q, want remote.php/dav URL", got)
|
||||
}
|
||||
if got := resolved.Profile.Path; got != "files/bellagio/keepass.kdbx" {
|
||||
t.Fatalf("resolved profile path = %q, want files/bellagio/keepass.kdbx", got)
|
||||
}
|
||||
if got := resolved.Credentials.Username; got != "linuscaldwell" {
|
||||
t.Fatalf("resolved credentials username = %q, want linuscaldwell", got)
|
||||
}
|
||||
if got := resolved.Credentials.Password; got != "bellagio-pass-1" {
|
||||
t.Fatalf("resolved credentials password = %q, want bellagio-pass-1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoteBindingResolveFailsWhenVaultReferenceIsMissing(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
model := vault.Model{
|
||||
Entries: []vault.Entry{
|
||||
{ID: "linuscaldwell-webdav", Title: "Bellagio WebDAV Sign-In"},
|
||||
},
|
||||
}
|
||||
|
||||
_, err := (RemoteBinding{
|
||||
LocalVaultPath: "/tmp/bellagio.kdbx",
|
||||
RemoteProfileID: "bellagio-webdav",
|
||||
CredentialEntryID: "missing-creds",
|
||||
}).Resolve(model)
|
||||
if !errors.Is(err, vault.ErrRemoteProfileNotFound) {
|
||||
t.Fatalf("Resolve() error = %v, want ErrRemoteProfileNotFound first", err)
|
||||
}
|
||||
|
||||
model.RemoteProfiles = []vault.RemoteProfile{{
|
||||
ID: "bellagio-webdav",
|
||||
Name: "Bellagio Vault",
|
||||
Backend: vault.RemoteBackendWebDAV,
|
||||
BaseURL: "https://dav.example.invalid/remote.php/dav",
|
||||
Path: "files/bellagio/keepass.kdbx",
|
||||
}}
|
||||
|
||||
_, err = (RemoteBinding{
|
||||
LocalVaultPath: "/tmp/bellagio.kdbx",
|
||||
RemoteProfileID: "bellagio-webdav",
|
||||
CredentialEntryID: "missing-creds",
|
||||
}).Resolve(model)
|
||||
if !errors.Is(err, vault.ErrEntryNotFound) {
|
||||
t.Fatalf("Resolve() error = %v, want ErrEntryNotFound", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoteBindingJSONStoresOnlyNonSecretReferences(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
content, err := json.Marshal(RemoteBinding{
|
||||
LocalVaultPath: "/tmp/bellagio.kdbx",
|
||||
RemoteProfileID: "bellagio-webdav",
|
||||
CredentialEntryID: "remote-creds-1",
|
||||
SyncMode: SyncModeAutomaticOnOpenSave,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("json.Marshal(RemoteBinding) error = %v", err)
|
||||
}
|
||||
|
||||
text := string(content)
|
||||
for _, disallowed := range []string{"bellagio-pass-1", "password", "username", "baseUrl"} {
|
||||
if strings.Contains(text, disallowed) {
|
||||
t.Fatalf("binding JSON %q unexpectedly contains %q", text, disallowed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigureRemoteBindingStoresProfileAndCredentialsInVault(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var model vault.Model
|
||||
|
||||
binding, err := ConfigureRemoteBinding(&model, RemoteBindingInput{
|
||||
LocalVaultPath: "/tmp/bellagio.kdbx",
|
||||
RemoteProfileID: "bellagio-webdav",
|
||||
RemoteProfileName: "Bellagio Vault",
|
||||
BaseURL: "https://dav.example.invalid/remote.php/dav",
|
||||
RemotePath: "files/bellagio/keepass.kdbx",
|
||||
CredentialEntryID: "remote-creds-1",
|
||||
CredentialTitle: "Bellagio WebDAV Sign-In",
|
||||
Username: "linuscaldwell",
|
||||
Password: "bellagio-pass-1",
|
||||
CredentialPath: []string{"Crew", "Internet"},
|
||||
SyncMode: SyncModeAutomaticOnOpenSave,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("ConfigureRemoteBinding() error = %v", err)
|
||||
}
|
||||
|
||||
if len(model.RemoteProfiles) != 1 {
|
||||
t.Fatalf("len(RemoteProfiles) = %d, want 1", len(model.RemoteProfiles))
|
||||
}
|
||||
if got := model.RemoteProfiles[0].BaseURL; got != "https://dav.example.invalid/remote.php/dav" {
|
||||
t.Fatalf("stored remote profile base URL = %q, want remote.php/dav URL", got)
|
||||
}
|
||||
|
||||
credentials, err := model.EntryByID("remote-creds-1")
|
||||
if err != nil {
|
||||
t.Fatalf("EntryByID(remote-creds-1) error = %v", err)
|
||||
}
|
||||
if credentials.Username != "linuscaldwell" || credentials.Password != "bellagio-pass-1" {
|
||||
t.Fatalf("stored credential entry = %#v, want linuscaldwell/bellagio-pass-1", credentials)
|
||||
}
|
||||
if credentials.URL != "https://dav.example.invalid/remote.php/dav" {
|
||||
t.Fatalf("stored credential entry URL = %q, want remote.php/dav URL", credentials.URL)
|
||||
}
|
||||
|
||||
if binding.LocalVaultPath != "/tmp/bellagio.kdbx" {
|
||||
t.Fatalf("binding LocalVaultPath = %q, want /tmp/bellagio.kdbx", binding.LocalVaultPath)
|
||||
}
|
||||
if binding.RemoteProfileID != "bellagio-webdav" || binding.CredentialEntryID != "remote-creds-1" {
|
||||
t.Fatalf("binding = %#v, want only vault references", binding)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigureRemoteBindingRejectsIncompleteInput(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
input RemoteBindingInput
|
||||
}{
|
||||
{
|
||||
name: "missing_local_vault_path",
|
||||
input: RemoteBindingInput{
|
||||
RemoteProfileID: "bellagio-webdav",
|
||||
BaseURL: "https://dav.example.invalid/remote.php/dav",
|
||||
RemotePath: "files/bellagio/keepass.kdbx",
|
||||
CredentialEntryID: "remote-creds-1",
|
||||
Password: "bellagio-pass-1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing_remote_base_url",
|
||||
input: RemoteBindingInput{
|
||||
LocalVaultPath: "/tmp/bellagio.kdbx",
|
||||
RemoteProfileID: "bellagio-webdav",
|
||||
RemotePath: "files/bellagio/keepass.kdbx",
|
||||
CredentialEntryID: "remote-creds-1",
|
||||
Password: "bellagio-pass-1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing_credential_entry_id",
|
||||
input: RemoteBindingInput{
|
||||
LocalVaultPath: "/tmp/bellagio.kdbx",
|
||||
RemoteProfileID: "bellagio-webdav",
|
||||
BaseURL: "https://dav.example.invalid/remote.php/dav",
|
||||
RemotePath: "files/bellagio/keepass.kdbx",
|
||||
Password: "bellagio-pass-1",
|
||||
},
|
||||
},
|
||||
} {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var model vault.Model
|
||||
if _, err := ConfigureRemoteBinding(&model, tc.input); err == nil {
|
||||
t.Fatalf("ConfigureRemoteBinding(%#v) error = nil, want validation error", tc.input)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveRemoteBindingRemovesProfileAndCredentialsFromVault(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
model := vault.Model{
|
||||
Entries: []vault.Entry{{
|
||||
ID: "remote-creds-1",
|
||||
Title: "Bellagio WebDAV Sign-In",
|
||||
Username: "linuscaldwell",
|
||||
Password: "bellagio-pass-1",
|
||||
}},
|
||||
RemoteProfiles: []vault.RemoteProfile{{
|
||||
ID: "bellagio-webdav",
|
||||
Name: "Bellagio Vault",
|
||||
Backend: vault.RemoteBackendWebDAV,
|
||||
BaseURL: "https://dav.example.invalid/remote.php/dav",
|
||||
Path: "files/bellagio/keepass.kdbx",
|
||||
}},
|
||||
}
|
||||
|
||||
err := RemoveRemoteBinding(&model, RemoteBinding{
|
||||
LocalVaultPath: "/tmp/bellagio.kdbx",
|
||||
RemoteProfileID: "bellagio-webdav",
|
||||
CredentialEntryID: "remote-creds-1",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("RemoveRemoteBinding() error = %v", err)
|
||||
}
|
||||
|
||||
if got := len(model.RemoteProfiles); got != 0 {
|
||||
t.Fatalf("len(RemoteProfiles) = %d, want 0", got)
|
||||
}
|
||||
if _, err := model.EntryByID("remote-creds-1"); !errors.Is(err, vault.ErrEntryNotFound) {
|
||||
t.Fatalf("EntryByID(remote-creds-1) error = %v, want ErrEntryNotFound", err)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user