Store remote sync bindings in vault data
This commit is contained in:
@@ -2,6 +2,7 @@ package appstate
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.julianfamily.org/keepassgo/vault"
|
||||
)
|
||||
@@ -25,6 +26,20 @@ type ResolvedRemoteBinding struct {
|
||||
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 {
|
||||
@@ -39,3 +54,71 @@ func (b RemoteBinding) Resolve(model vault.Model) (ResolvedRemoteBinding, error)
|
||||
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,
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user