Store remote sync bindings in vault data
This commit is contained in:
@@ -1,2 +1,6 @@
|
|||||||
build/
|
build/
|
||||||
*.apk
|
*.apk
|
||||||
|
packaging/archlinux/keepassgo-git/*.pkg.tar.zst
|
||||||
|
packaging/archlinux/keepassgo-git/pkg/
|
||||||
|
packaging/archlinux/keepassgo-git/src/
|
||||||
|
packaging/archlinux/keepassgo-git/keepassgo/
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package appstate
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"git.julianfamily.org/keepassgo/vault"
|
"git.julianfamily.org/keepassgo/vault"
|
||||||
)
|
)
|
||||||
@@ -25,6 +26,20 @@ type ResolvedRemoteBinding struct {
|
|||||||
Credentials vault.Entry
|
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) {
|
func (b RemoteBinding) Resolve(model vault.Model) (ResolvedRemoteBinding, error) {
|
||||||
profile, err := model.RemoteProfileByID(b.RemoteProfileID)
|
profile, err := model.RemoteProfileByID(b.RemoteProfileID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -39,3 +54,71 @@ func (b RemoteBinding) Resolve(model vault.Model) (ResolvedRemoteBinding, error)
|
|||||||
Credentials: credentials,
|
Credentials: credentials,
|
||||||
}, nil
|
}, 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,
|
||||||
|
Path: append([]string(nil), input.CredentialPath...),
|
||||||
|
})
|
||||||
|
|
||||||
|
return RemoteBinding{
|
||||||
|
LocalVaultPath: input.LocalVaultPath,
|
||||||
|
RemoteProfileID: input.RemoteProfileID,
|
||||||
|
CredentialEntryID: input.CredentialEntryID,
|
||||||
|
SyncMode: normalizeSyncMode(input.SyncMode),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeSyncMode(mode SyncMode) SyncMode {
|
||||||
|
switch mode {
|
||||||
|
case SyncModeAutomaticOnOpenSave:
|
||||||
|
return SyncModeAutomaticOnOpenSave
|
||||||
|
default:
|
||||||
|
return SyncModeManual
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -114,3 +114,98 @@ func TestRemoteBindingJSONStoresOnlyNonSecretReferences(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfigureRemoteBindingStoresProfileAndCredentialsInVault(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var model vault.Model
|
||||||
|
|
||||||
|
binding, err := ConfigureRemoteBinding(&model, RemoteBindingInput{
|
||||||
|
LocalVaultPath: "/tmp/family.kdbx",
|
||||||
|
RemoteProfileID: "family-webdav",
|
||||||
|
RemoteProfileName: "Family Vault",
|
||||||
|
BaseURL: "https://dav.example.invalid/remote.php/dav",
|
||||||
|
RemotePath: "files/family/keepass.kdbx",
|
||||||
|
CredentialEntryID: "remote-creds-1",
|
||||||
|
CredentialTitle: "WebDAV Sign-In",
|
||||||
|
Username: "tjulian",
|
||||||
|
Password: "token-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 != "tjulian" || credentials.Password != "token-1" {
|
||||||
|
t.Fatalf("stored credential entry = %#v, want tjulian/token-1", credentials)
|
||||||
|
}
|
||||||
|
|
||||||
|
if binding.LocalVaultPath != "/tmp/family.kdbx" {
|
||||||
|
t.Fatalf("binding LocalVaultPath = %q, want /tmp/family.kdbx", binding.LocalVaultPath)
|
||||||
|
}
|
||||||
|
if binding.RemoteProfileID != "family-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: "family-webdav",
|
||||||
|
BaseURL: "https://dav.example.invalid/remote.php/dav",
|
||||||
|
RemotePath: "files/family/keepass.kdbx",
|
||||||
|
CredentialEntryID: "remote-creds-1",
|
||||||
|
Password: "token-1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing_remote_base_url",
|
||||||
|
input: RemoteBindingInput{
|
||||||
|
LocalVaultPath: "/tmp/family.kdbx",
|
||||||
|
RemoteProfileID: "family-webdav",
|
||||||
|
RemotePath: "files/family/keepass.kdbx",
|
||||||
|
CredentialEntryID: "remote-creds-1",
|
||||||
|
Password: "token-1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing_credential_entry_id",
|
||||||
|
input: RemoteBindingInput{
|
||||||
|
LocalVaultPath: "/tmp/family.kdbx",
|
||||||
|
RemoteProfileID: "family-webdav",
|
||||||
|
BaseURL: "https://dav.example.invalid/remote.php/dav",
|
||||||
|
RemotePath: "files/family/keepass.kdbx",
|
||||||
|
Password: "token-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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+14
-2
@@ -4411,7 +4411,13 @@ func TestShowLocalVaultChooser(t *testing.T) {
|
|||||||
func TestShowRemoteConnectionChooser(t *testing.T) {
|
func TestShowRemoteConnectionChooser(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
u := newUIWithSession("desktop", &session.Manager{})
|
dir := t.TempDir()
|
||||||
|
u := newUIWithState("desktop", &session.Manager{}, statePaths{
|
||||||
|
DefaultSaveAsPath: filepath.Join(dir, "vault.kdbx"),
|
||||||
|
RecentVaultsPath: filepath.Join(dir, "recent-vaults.json"),
|
||||||
|
RecentRemotesPath: filepath.Join(dir, "recent-remotes.json"),
|
||||||
|
UIPreferencesPath: filepath.Join(dir, "ui-prefs.json"),
|
||||||
|
})
|
||||||
u.lifecycleMode = "remote"
|
u.lifecycleMode = "remote"
|
||||||
u.remoteBaseURL.SetText("")
|
u.remoteBaseURL.SetText("")
|
||||||
u.remotePath.SetText("")
|
u.remotePath.SetText("")
|
||||||
@@ -4442,7 +4448,13 @@ func TestShowRemoteConnectionChooser(t *testing.T) {
|
|||||||
func TestApplyingRecentRemoteRecordMarksSelectedRemoteConnection(t *testing.T) {
|
func TestApplyingRecentRemoteRecordMarksSelectedRemoteConnection(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
u := newUIWithSession("desktop", &session.Manager{})
|
dir := t.TempDir()
|
||||||
|
u := newUIWithState("desktop", &session.Manager{}, statePaths{
|
||||||
|
DefaultSaveAsPath: filepath.Join(dir, "vault.kdbx"),
|
||||||
|
RecentVaultsPath: filepath.Join(dir, "recent-vaults.json"),
|
||||||
|
RecentRemotesPath: filepath.Join(dir, "recent-remotes.json"),
|
||||||
|
UIPreferencesPath: filepath.Join(dir, "ui-prefs.json"),
|
||||||
|
})
|
||||||
if u.hasSelectedRemoteTarget() {
|
if u.hasSelectedRemoteTarget() {
|
||||||
t.Fatal("hasSelectedRemoteTarget() = true, want false before selecting a saved remote connection")
|
t.Fatal("hasSelectedRemoteTarget() = true, want false before selecting a saved remote connection")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user