Bootstrap remote opens from local cache vaults
This commit is contained in:
@@ -1080,23 +1080,21 @@ func (u *ui) openRemoteAction() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if u.hasOpenVault() {
|
if binding, resolved, ok, err := u.bootstrapSelectedVaultRemoteBinding(key); err != nil {
|
||||||
if binding, resolved, ok, err := u.resolvedSelectedVaultRemoteBinding(); err != nil {
|
return err
|
||||||
|
} else if ok {
|
||||||
|
if err := u.state.OpenBoundRemoteVault(binding, key); err != nil {
|
||||||
return err
|
return err
|
||||||
} else if ok {
|
|
||||||
if err := u.state.OpenBoundRemoteVault(binding, key); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
u.remoteBaseURL.SetText(resolved.Profile.BaseURL)
|
|
||||||
u.remotePath.SetText(resolved.Profile.Path)
|
|
||||||
u.noteRecentRemote(resolved.Profile.BaseURL, resolved.Profile.Path)
|
|
||||||
u.resetPasswordPeek()
|
|
||||||
u.restoreRecentRemoteGroup(resolved.Profile.BaseURL, resolved.Profile.Path)
|
|
||||||
u.loadSecuritySettingsFromSession()
|
|
||||||
u.editingEntry = false
|
|
||||||
u.filter()
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
u.remoteBaseURL.SetText(resolved.Profile.BaseURL)
|
||||||
|
u.remotePath.SetText(resolved.Profile.Path)
|
||||||
|
u.noteRecentRemote(resolved.Profile.BaseURL, resolved.Profile.Path)
|
||||||
|
u.resetPasswordPeek()
|
||||||
|
u.restoreRecentRemoteGroup(resolved.Profile.BaseURL, resolved.Profile.Path)
|
||||||
|
u.loadSecuritySettingsFromSession()
|
||||||
|
u.editingEntry = false
|
||||||
|
u.filter()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
client := webdav.Client{
|
client := webdav.Client{
|
||||||
BaseURL: strings.TrimSpace(u.remoteBaseURL.Text()),
|
BaseURL: strings.TrimSpace(u.remoteBaseURL.Text()),
|
||||||
@@ -1137,24 +1135,57 @@ func (u *ui) startOpenRemoteAction() {
|
|||||||
Password: u.remotePassword.Text(),
|
Password: u.remotePassword.Text(),
|
||||||
}
|
}
|
||||||
remotePath := strings.TrimSpace(u.remotePath.Text())
|
remotePath := strings.TrimSpace(u.remotePath.Text())
|
||||||
if u.hasOpenVault() {
|
u.lastLifecycleAction = "open remote vault"
|
||||||
if _, resolved, ok, err := u.resolvedSelectedVaultRemoteBinding(); err != nil {
|
u.runBackgroundAction("open remote vault", func() (func() error, error) {
|
||||||
u.state.ErrorMessage = u.describeActionError("open remote vault", err)
|
binding, bindingOK := u.selectedVaultRemoteBinding()
|
||||||
u.requestMasterPassFocus = true
|
if bindingOK && !u.hasOpenVault() && strings.TrimSpace(binding.LocalVaultPath) != "" {
|
||||||
return
|
preparedLocal, err := session.PrepareLocalOpen(binding.LocalVaultPath, key)
|
||||||
} else if ok {
|
if err != nil {
|
||||||
client = webdav.Client{
|
return nil, err
|
||||||
|
}
|
||||||
|
resolved, err := binding.Resolve(preparedLocal.Model)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
preparedRemote, err := session.PrepareRemoteOpen(webdav.Client{
|
||||||
BaseURL: resolved.Profile.BaseURL,
|
BaseURL: resolved.Profile.BaseURL,
|
||||||
Username: resolved.Credentials.Username,
|
Username: resolved.Credentials.Username,
|
||||||
Password: resolved.Credentials.Password,
|
Password: resolved.Credentials.Password,
|
||||||
|
}, resolved.Profile.Path, key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return func() error {
|
||||||
|
manager.ApplyPreparedLocalOpen(preparedLocal)
|
||||||
|
u.vaultPath.SetText(binding.LocalVaultPath)
|
||||||
|
u.noteRecentVault(binding.LocalVaultPath)
|
||||||
|
u.restoreRecentVaultGroup(binding.LocalVaultPath)
|
||||||
|
manager.ApplyPreparedRemoteOpen(preparedRemote)
|
||||||
|
u.remoteBaseURL.SetText(resolved.Profile.BaseURL)
|
||||||
|
u.remotePath.SetText(resolved.Profile.Path)
|
||||||
|
u.noteRecentRemote(resolved.Profile.BaseURL, resolved.Profile.Path)
|
||||||
|
u.resetPasswordPeek()
|
||||||
|
u.restoreRecentRemoteGroup(resolved.Profile.BaseURL, resolved.Profile.Path)
|
||||||
|
u.loadSecuritySettingsFromSession()
|
||||||
|
u.editingEntry = false
|
||||||
|
u.filter()
|
||||||
|
return nil
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
if u.hasOpenVault() {
|
||||||
|
if _, resolved, ok, err := u.resolvedSelectedVaultRemoteBinding(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if ok {
|
||||||
|
client = webdav.Client{
|
||||||
|
BaseURL: resolved.Profile.BaseURL,
|
||||||
|
Username: resolved.Credentials.Username,
|
||||||
|
Password: resolved.Credentials.Password,
|
||||||
|
}
|
||||||
|
remotePath = resolved.Profile.Path
|
||||||
|
u.remoteBaseURL.SetText(resolved.Profile.BaseURL)
|
||||||
|
u.remotePath.SetText(resolved.Profile.Path)
|
||||||
}
|
}
|
||||||
remotePath = resolved.Profile.Path
|
|
||||||
u.remoteBaseURL.SetText(resolved.Profile.BaseURL)
|
|
||||||
u.remotePath.SetText(resolved.Profile.Path)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
u.lastLifecycleAction = "open remote vault"
|
|
||||||
u.runBackgroundAction("open remote vault", func() (func() error, error) {
|
|
||||||
prepared, err := session.PrepareRemoteOpen(client, remotePath, key)
|
prepared, err := session.PrepareRemoteOpen(client, remotePath, key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -2066,6 +2097,33 @@ func (u *ui) saveCurrentRemoteBindingAction() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ui) bootstrapSelectedVaultRemoteBinding(key vault.MasterKey) (appstate.RemoteBinding, appstate.ResolvedRemoteBinding, bool, error) {
|
||||||
|
if u.hasOpenVault() {
|
||||||
|
return u.resolvedSelectedVaultRemoteBinding()
|
||||||
|
}
|
||||||
|
|
||||||
|
binding, ok := u.selectedVaultRemoteBinding()
|
||||||
|
if !ok || strings.TrimSpace(binding.LocalVaultPath) == "" {
|
||||||
|
return appstate.RemoteBinding{}, appstate.ResolvedRemoteBinding{}, false, nil
|
||||||
|
}
|
||||||
|
if err := u.state.OpenVault(binding.LocalVaultPath, key); err != nil {
|
||||||
|
return appstate.RemoteBinding{}, appstate.ResolvedRemoteBinding{}, false, err
|
||||||
|
}
|
||||||
|
u.vaultPath.SetText(binding.LocalVaultPath)
|
||||||
|
u.noteRecentVault(binding.LocalVaultPath)
|
||||||
|
u.restoreRecentVaultGroup(binding.LocalVaultPath)
|
||||||
|
|
||||||
|
model, err := u.state.Session.Current()
|
||||||
|
if err != nil {
|
||||||
|
return appstate.RemoteBinding{}, appstate.ResolvedRemoteBinding{}, false, err
|
||||||
|
}
|
||||||
|
resolved, err := binding.Resolve(model)
|
||||||
|
if err != nil {
|
||||||
|
return appstate.RemoteBinding{}, appstate.ResolvedRemoteBinding{}, false, err
|
||||||
|
}
|
||||||
|
return binding, resolved, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ui) resolvedSelectedVaultRemoteBinding() (appstate.RemoteBinding, appstate.ResolvedRemoteBinding, bool, error) {
|
func (u *ui) resolvedSelectedVaultRemoteBinding() (appstate.RemoteBinding, appstate.ResolvedRemoteBinding, bool, error) {
|
||||||
binding, ok := u.selectedVaultRemoteBinding()
|
binding, ok := u.selectedVaultRemoteBinding()
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
+172
-7
@@ -1725,6 +1725,83 @@ func TestUIOpenRemoteActionUsesSelectedVaultBinding(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUIOpenRemoteActionBootstrapsFromLocalVaultBinding(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
key := vault.MasterKey{Password: "correct horse battery staple"}
|
||||||
|
localPath := filepath.Join(t.TempDir(), "family.kdbx")
|
||||||
|
|
||||||
|
remoteModel := vault.Model{
|
||||||
|
Entries: []vault.Entry{{
|
||||||
|
ID: "entry-1",
|
||||||
|
Title: "Vault Console",
|
||||||
|
Username: "dannyocean",
|
||||||
|
Password: "remote-token",
|
||||||
|
Path: []string{"Root", "Internet"},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
var remoteBytes bytes.Buffer
|
||||||
|
if err := vault.SaveKDBXWithKey(&remoteBytes, remoteModel, key); err != nil {
|
||||||
|
t.Fatalf("SaveKDBXWithKey(remote) error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
username, password, ok := r.BasicAuth()
|
||||||
|
if !ok || username != "tjulian" || password != "token-1" {
|
||||||
|
t.Fatalf("BasicAuth() = (%q, %q, %t), want (tjulian, token-1, true)", username, password, ok)
|
||||||
|
}
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
t.Fatalf("method = %s, want GET", r.Method)
|
||||||
|
}
|
||||||
|
w.Header().Set("ETag", "\"v1\"")
|
||||||
|
_, _ = w.Write(remoteBytes.Bytes())
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
localModel := vault.Model{}
|
||||||
|
if _, err := appstate.ConfigureRemoteBinding(&localModel, appstate.RemoteBindingInput{
|
||||||
|
LocalVaultPath: localPath,
|
||||||
|
RemoteProfileID: "family-webdav",
|
||||||
|
RemoteProfileName: "family.kdbx · dav.example.invalid",
|
||||||
|
BaseURL: server.URL,
|
||||||
|
RemotePath: "files/family/keepass.kdbx",
|
||||||
|
CredentialEntryID: "remote-creds-1",
|
||||||
|
CredentialTitle: "WebDAV Sign-In · tjulian",
|
||||||
|
Username: "tjulian",
|
||||||
|
Password: "token-1",
|
||||||
|
CredentialPath: []string{"Crew", "Internet"},
|
||||||
|
SyncMode: appstate.SyncModeAutomaticOnOpenSave,
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("ConfigureRemoteBinding(localModel) error = %v", err)
|
||||||
|
}
|
||||||
|
writeKDBXMainTestFile(t, localPath, localModel, key)
|
||||||
|
|
||||||
|
u := newUIWithSession("desktop", &session.Manager{})
|
||||||
|
u.masterPassword.SetText(key.Password)
|
||||||
|
u.applyRecentRemoteRecord(recentRemoteRecord{
|
||||||
|
BaseURL: server.URL,
|
||||||
|
Path: "files/family/keepass.kdbx",
|
||||||
|
LocalVaultPath: localPath,
|
||||||
|
RemoteProfileID: "family-webdav",
|
||||||
|
CredentialEntryID: "remote-creds-1",
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := u.openRemoteAction(); err != nil {
|
||||||
|
t.Fatalf("openRemoteAction() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := u.vaultPath.Text(); got != localPath {
|
||||||
|
t.Fatalf("vaultPath = %q, want %q", got, localPath)
|
||||||
|
}
|
||||||
|
current, err := u.state.Session.Current()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Session.Current() error = %v", err)
|
||||||
|
}
|
||||||
|
if got := current.EntriesInPath([]string{"Root", "Internet"}); len(got) != 1 || got[0].Title != "Vault Console" {
|
||||||
|
t.Fatalf("EntriesInPath(Root/Internet) = %#v, want Vault Console", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUIStartOpenRemoteActionUsesSelectedVaultBinding(t *testing.T) {
|
func TestUIStartOpenRemoteActionUsesSelectedVaultBinding(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -1798,6 +1875,92 @@ func TestUIStartOpenRemoteActionUsesSelectedVaultBinding(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUIStartOpenRemoteActionBootstrapsFromLocalVaultBinding(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
key := vault.MasterKey{Password: "correct horse battery staple"}
|
||||||
|
localPath := filepath.Join(t.TempDir(), "family.kdbx")
|
||||||
|
|
||||||
|
remoteModel := vault.Model{
|
||||||
|
Entries: []vault.Entry{{
|
||||||
|
ID: "entry-1",
|
||||||
|
Title: "Vault Console",
|
||||||
|
Username: "dannyocean",
|
||||||
|
Password: "remote-token",
|
||||||
|
Path: []string{"Root", "Internet"},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
var remoteBytes bytes.Buffer
|
||||||
|
if err := vault.SaveKDBXWithKey(&remoteBytes, remoteModel, key); err != nil {
|
||||||
|
t.Fatalf("SaveKDBXWithKey(remote) error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
username, password, ok := r.BasicAuth()
|
||||||
|
if !ok || username != "tjulian" || password != "token-1" {
|
||||||
|
t.Fatalf("BasicAuth() = (%q, %q, %t), want (tjulian, token-1, true)", username, password, ok)
|
||||||
|
}
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
t.Fatalf("method = %s, want GET", r.Method)
|
||||||
|
}
|
||||||
|
w.Header().Set("ETag", "\"v1\"")
|
||||||
|
_, _ = w.Write(remoteBytes.Bytes())
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
localModel := vault.Model{}
|
||||||
|
if _, err := appstate.ConfigureRemoteBinding(&localModel, appstate.RemoteBindingInput{
|
||||||
|
LocalVaultPath: localPath,
|
||||||
|
RemoteProfileID: "family-webdav",
|
||||||
|
RemoteProfileName: "family.kdbx · dav.example.invalid",
|
||||||
|
BaseURL: server.URL,
|
||||||
|
RemotePath: "files/family/keepass.kdbx",
|
||||||
|
CredentialEntryID: "remote-creds-1",
|
||||||
|
CredentialTitle: "WebDAV Sign-In · tjulian",
|
||||||
|
Username: "tjulian",
|
||||||
|
Password: "token-1",
|
||||||
|
CredentialPath: []string{"Crew", "Internet"},
|
||||||
|
SyncMode: appstate.SyncModeAutomaticOnOpenSave,
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("ConfigureRemoteBinding(localModel) error = %v", err)
|
||||||
|
}
|
||||||
|
writeKDBXMainTestFile(t, localPath, localModel, key)
|
||||||
|
|
||||||
|
manager := &session.Manager{}
|
||||||
|
u := newUIWithSession("desktop", manager)
|
||||||
|
u.masterPassword.SetText(key.Password)
|
||||||
|
u.applyRecentRemoteRecord(recentRemoteRecord{
|
||||||
|
BaseURL: server.URL,
|
||||||
|
Path: "files/family/keepass.kdbx",
|
||||||
|
LocalVaultPath: localPath,
|
||||||
|
RemoteProfileID: "family-webdav",
|
||||||
|
CredentialEntryID: "remote-creds-1",
|
||||||
|
})
|
||||||
|
|
||||||
|
u.startOpenRemoteAction()
|
||||||
|
|
||||||
|
if got := u.loadingMessage; got != "Open remote vault..." {
|
||||||
|
t.Fatalf("loadingMessage after start = %q, want %q", got, "Open remote vault...")
|
||||||
|
}
|
||||||
|
|
||||||
|
result := waitForBackgroundResult(t, u)
|
||||||
|
u.applyBackgroundResult(result)
|
||||||
|
|
||||||
|
if got := u.state.ErrorMessage; got != "" {
|
||||||
|
t.Fatalf("ErrorMessage after apply = %q, want empty", got)
|
||||||
|
}
|
||||||
|
if got := u.vaultPath.Text(); got != localPath {
|
||||||
|
t.Fatalf("vaultPath = %q, want %q", got, localPath)
|
||||||
|
}
|
||||||
|
current, err := u.state.Session.Current()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Session.Current() error = %v", err)
|
||||||
|
}
|
||||||
|
if got := current.EntriesInPath([]string{"Root", "Internet"}); len(got) != 1 || got[0].Title != "Vault Console" {
|
||||||
|
t.Fatalf("EntriesInPath(Root/Internet) = %#v, want Vault Console", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUIRemoteSaveConflictShowsVisibleErrorAndKeepsDirtyState(t *testing.T) {
|
func TestUIRemoteSaveConflictShowsVisibleErrorAndKeepsDirtyState(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -5134,9 +5297,14 @@ func TestUIOpenRemoteVaultRestoresLastOpenedGroupForThatConnection(t *testing.T)
|
|||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
first := newUIWithSession("desktop", &session.Manager{})
|
paths := statePaths{
|
||||||
first.recentRemotesPath = statePath
|
DefaultSaveAsPath: filepath.Join(dir, "vault.kdbx"),
|
||||||
first.recentRemotes = nil
|
RecentVaultsPath: filepath.Join(dir, "recent-vaults.json"),
|
||||||
|
RecentRemotesPath: statePath,
|
||||||
|
UIPreferencesPath: filepath.Join(dir, "ui-prefs.json"),
|
||||||
|
}
|
||||||
|
|
||||||
|
first := newUIWithState("desktop", &session.Manager{}, paths)
|
||||||
first.lifecycleMode = "remote"
|
first.lifecycleMode = "remote"
|
||||||
first.masterPassword.SetText("correct horse battery staple")
|
first.masterPassword.SetText("correct horse battery staple")
|
||||||
first.remoteBaseURL.SetText(server.URL)
|
first.remoteBaseURL.SetText(server.URL)
|
||||||
@@ -5149,10 +5317,7 @@ func TestUIOpenRemoteVaultRestoresLastOpenedGroupForThatConnection(t *testing.T)
|
|||||||
first.syncedPath = []string{"Root", "Internet"}
|
first.syncedPath = []string{"Root", "Internet"}
|
||||||
first.noteCurrentRemotePath()
|
first.noteCurrentRemotePath()
|
||||||
|
|
||||||
reopened := newUIWithSession("desktop", &session.Manager{})
|
reopened := newUIWithState("desktop", &session.Manager{}, paths)
|
||||||
reopened.recentRemotesPath = statePath
|
|
||||||
reopened.recentRemotes = nil
|
|
||||||
reopened.loadRecentRemotes()
|
|
||||||
reopened.lifecycleMode = "remote"
|
reopened.lifecycleMode = "remote"
|
||||||
reopened.masterPassword.SetText("correct horse battery staple")
|
reopened.masterPassword.SetText("correct horse battery staple")
|
||||||
reopened.remoteBaseURL.SetText(server.URL)
|
reopened.remoteBaseURL.SetText(server.URL)
|
||||||
|
|||||||
Reference in New Issue
Block a user