Materialize local cache on first remote open
This commit is contained in:
@@ -1104,6 +1104,9 @@ func (u *ui) openRemoteAction() error {
|
|||||||
if err := u.state.OpenRemoteVault(client, strings.TrimSpace(u.remotePath.Text()), key); err != nil {
|
if err := u.state.OpenRemoteVault(client, strings.TrimSpace(u.remotePath.Text()), key); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := u.materializeCurrentRemoteCache(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
u.noteRecentRemote(
|
u.noteRecentRemote(
|
||||||
strings.TrimSpace(u.remoteBaseURL.Text()),
|
strings.TrimSpace(u.remoteBaseURL.Text()),
|
||||||
strings.TrimSpace(u.remotePath.Text()),
|
strings.TrimSpace(u.remotePath.Text()),
|
||||||
@@ -1192,6 +1195,9 @@ func (u *ui) startOpenRemoteAction() {
|
|||||||
}
|
}
|
||||||
return func() error {
|
return func() error {
|
||||||
manager.ApplyPreparedRemoteOpen(prepared)
|
manager.ApplyPreparedRemoteOpen(prepared)
|
||||||
|
if err := u.materializeCurrentRemoteCache(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
u.noteRecentRemote(
|
u.noteRecentRemote(
|
||||||
strings.TrimSpace(u.remoteBaseURL.Text()),
|
strings.TrimSpace(u.remoteBaseURL.Text()),
|
||||||
remotePath,
|
remotePath,
|
||||||
@@ -2097,6 +2103,42 @@ func (u *ui) saveCurrentRemoteBindingAction() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ui) materializeCurrentRemoteCache() error {
|
||||||
|
cachePath := strings.TrimSpace(u.vaultPath.Text())
|
||||||
|
if cachePath == "" {
|
||||||
|
cachePath = u.saveAsTargetPath()
|
||||||
|
}
|
||||||
|
if cachePath == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
u.vaultPath.SetText(cachePath)
|
||||||
|
if err := u.state.SaveAs(cachePath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
u.noteRecentVault(cachePath)
|
||||||
|
|
||||||
|
username := strings.TrimSpace(u.remoteUsername.Text())
|
||||||
|
password := u.remotePassword.Text()
|
||||||
|
if username == "" && password == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
input, err := u.currentRemoteBindingInput()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
binding, err := u.state.ConfigureRemoteBinding(input)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := u.state.SaveAs(cachePath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
u.selectedVaultRemoteProfileID = binding.RemoteProfileID
|
||||||
|
u.selectedVaultRemoteCredentialEntryID = binding.CredentialEntryID
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (u *ui) bootstrapSelectedVaultRemoteBinding(key vault.MasterKey) (appstate.RemoteBinding, appstate.ResolvedRemoteBinding, bool, error) {
|
func (u *ui) bootstrapSelectedVaultRemoteBinding(key vault.MasterKey) (appstate.RemoteBinding, appstate.ResolvedRemoteBinding, bool, error) {
|
||||||
if u.hasOpenVault() {
|
if u.hasOpenVault() {
|
||||||
return u.resolvedSelectedVaultRemoteBinding()
|
return u.resolvedSelectedVaultRemoteBinding()
|
||||||
|
|||||||
+191
-6
@@ -262,7 +262,12 @@ func TestUIClearingSearchResetsToCurrentSectionListing(t *testing.T) {
|
|||||||
func TestUIRunBackgroundActionIgnoresDuplicateWhileLoading(t *testing.T) {
|
func TestUIRunBackgroundActionIgnoresDuplicateWhileLoading(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
u := newUIWithSession("desktop", &session.Manager{})
|
u := newUIWithState("desktop", &session.Manager{}, statePaths{
|
||||||
|
DefaultSaveAsPath: filepath.Join(t.TempDir(), "default-save-path.kdbx"),
|
||||||
|
RecentVaultsPath: filepath.Join(t.TempDir(), "recent-vaults.json"),
|
||||||
|
RecentRemotesPath: filepath.Join(t.TempDir(), "recent-remotes.json"),
|
||||||
|
UIPreferencesPath: filepath.Join(t.TempDir(), "ui-preferences.json"),
|
||||||
|
})
|
||||||
started := make(chan struct{})
|
started := make(chan struct{})
|
||||||
release := make(chan struct{})
|
release := make(chan struct{})
|
||||||
runs := 0
|
runs := 0
|
||||||
@@ -298,7 +303,12 @@ func TestUIRunBackgroundActionIgnoresDuplicateWhileLoading(t *testing.T) {
|
|||||||
func TestUICancelLifecycleBusyStateIgnoresLateResultAndKeepsRetryAvailable(t *testing.T) {
|
func TestUICancelLifecycleBusyStateIgnoresLateResultAndKeepsRetryAvailable(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
u := newUIWithSession("desktop", &session.Manager{})
|
u := newUIWithState("desktop", &session.Manager{}, statePaths{
|
||||||
|
DefaultSaveAsPath: filepath.Join(t.TempDir(), "default-save-path.kdbx"),
|
||||||
|
RecentVaultsPath: filepath.Join(t.TempDir(), "recent-vaults.json"),
|
||||||
|
RecentRemotesPath: filepath.Join(t.TempDir(), "recent-remotes.json"),
|
||||||
|
UIPreferencesPath: filepath.Join(t.TempDir(), "ui-preferences.json"),
|
||||||
|
})
|
||||||
u.vaultPath.SetText("/tmp/example.kdbx")
|
u.vaultPath.SetText("/tmp/example.kdbx")
|
||||||
u.lastLifecycleAction = "open vault"
|
u.lastLifecycleAction = "open vault"
|
||||||
|
|
||||||
@@ -1191,7 +1201,12 @@ func TestUISaveSecuritySettingsUpdatesExistingVault(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
manager := &session.Manager{}
|
manager := &session.Manager{}
|
||||||
u := newUIWithSession("desktop", manager)
|
u := newUIWithState("desktop", manager, statePaths{
|
||||||
|
DefaultSaveAsPath: filepath.Join(t.TempDir(), "default-save-path.kdbx"),
|
||||||
|
RecentVaultsPath: filepath.Join(t.TempDir(), "recent-vaults.json"),
|
||||||
|
RecentRemotesPath: filepath.Join(t.TempDir(), "recent-remotes.json"),
|
||||||
|
UIPreferencesPath: filepath.Join(t.TempDir(), "ui-preferences.json"),
|
||||||
|
})
|
||||||
u.masterPassword.SetText("correct horse battery staple")
|
u.masterPassword.SetText("correct horse battery staple")
|
||||||
if err := u.createVaultAction(); err != nil {
|
if err := u.createVaultAction(); err != nil {
|
||||||
t.Fatalf("createVaultAction() error = %v", err)
|
t.Fatalf("createVaultAction() error = %v", err)
|
||||||
@@ -1263,7 +1278,12 @@ func TestUISaveSettingsPersistsUIPreferences(t *testing.T) {
|
|||||||
func TestUILockAndUnlockClearMasterPasswordField(t *testing.T) {
|
func TestUILockAndUnlockClearMasterPasswordField(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
u := newUIWithSession("desktop", &session.Manager{})
|
u := newUIWithState("desktop", &session.Manager{}, statePaths{
|
||||||
|
DefaultSaveAsPath: filepath.Join(t.TempDir(), "default-save-path.kdbx"),
|
||||||
|
RecentVaultsPath: filepath.Join(t.TempDir(), "recent-vaults.json"),
|
||||||
|
RecentRemotesPath: filepath.Join(t.TempDir(), "recent-remotes.json"),
|
||||||
|
UIPreferencesPath: filepath.Join(t.TempDir(), "ui-preferences.json"),
|
||||||
|
})
|
||||||
u.masterPassword.SetText("correct horse battery staple")
|
u.masterPassword.SetText("correct horse battery staple")
|
||||||
if err := u.createVaultAction(); err != nil {
|
if err := u.createVaultAction(); err != nil {
|
||||||
t.Fatalf("createVaultAction() error = %v", err)
|
t.Fatalf("createVaultAction() error = %v", err)
|
||||||
@@ -1621,7 +1641,12 @@ func TestUIStartOpenRemoteActionAppliesResultOnMainThread(t *testing.T) {
|
|||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
manager := &session.Manager{}
|
manager := &session.Manager{}
|
||||||
u := newUIWithSession("desktop", manager)
|
u := newUIWithState("desktop", manager, statePaths{
|
||||||
|
DefaultSaveAsPath: filepath.Join(t.TempDir(), "default-save-path.kdbx"),
|
||||||
|
RecentVaultsPath: filepath.Join(t.TempDir(), "recent-vaults.json"),
|
||||||
|
RecentRemotesPath: filepath.Join(t.TempDir(), "recent-remotes.json"),
|
||||||
|
UIPreferencesPath: filepath.Join(t.TempDir(), "ui-preferences.json"),
|
||||||
|
})
|
||||||
u.masterPassword.SetText(key.Password)
|
u.masterPassword.SetText(key.Password)
|
||||||
u.remoteBaseURL.SetText(server.URL)
|
u.remoteBaseURL.SetText(server.URL)
|
||||||
u.remotePath.SetText("vaults/main.kdbx")
|
u.remotePath.SetText("vaults/main.kdbx")
|
||||||
@@ -1659,7 +1684,12 @@ func TestUIOpenRemoteReportsTransportFailure(t *testing.T) {
|
|||||||
url := server.URL
|
url := server.URL
|
||||||
server.Close()
|
server.Close()
|
||||||
|
|
||||||
u := newUIWithSession("desktop", &session.Manager{})
|
u := newUIWithState("desktop", &session.Manager{}, statePaths{
|
||||||
|
DefaultSaveAsPath: filepath.Join(t.TempDir(), "default-save-path.kdbx"),
|
||||||
|
RecentVaultsPath: filepath.Join(t.TempDir(), "recent-vaults.json"),
|
||||||
|
RecentRemotesPath: filepath.Join(t.TempDir(), "recent-remotes.json"),
|
||||||
|
UIPreferencesPath: filepath.Join(t.TempDir(), "ui-preferences.json"),
|
||||||
|
})
|
||||||
u.masterPassword.SetText("correct horse battery staple")
|
u.masterPassword.SetText("correct horse battery staple")
|
||||||
u.remoteBaseURL.SetText(url)
|
u.remoteBaseURL.SetText(url)
|
||||||
u.remotePath.SetText("vaults/main.kdbx")
|
u.remotePath.SetText("vaults/main.kdbx")
|
||||||
@@ -5331,6 +5361,161 @@ func TestUIOpenRemoteVaultRestoresLastOpenedGroupForThatConnection(t *testing.T)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUIOpenRemoteActionMaterializesLocalCacheAndBinding(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
dir := t.TempDir()
|
||||||
|
cachePath := filepath.Join(dir, "remote-cache.kdbx")
|
||||||
|
paths := statePaths{
|
||||||
|
DefaultSaveAsPath: cachePath,
|
||||||
|
RecentVaultsPath: filepath.Join(dir, "recent-vaults.json"),
|
||||||
|
RecentRemotesPath: filepath.Join(dir, "recent-remotes.json"),
|
||||||
|
UIPreferencesPath: filepath.Join(dir, "ui-prefs.json"),
|
||||||
|
}
|
||||||
|
key := vault.MasterKey{Password: "correct horse battery staple"}
|
||||||
|
|
||||||
|
var encoded bytes.Buffer
|
||||||
|
if err := vault.SaveKDBXWithKey(&encoded, vault.Model{
|
||||||
|
Entries: []vault.Entry{
|
||||||
|
{ID: "entry-1", Title: "Vault Console", Path: []string{"Root", "Internet"}},
|
||||||
|
},
|
||||||
|
}, key); err != nil {
|
||||||
|
t.Fatalf("SaveKDBXWithKey() 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(encoded.Bytes())
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
u := newUIWithState("desktop", &session.Manager{}, paths)
|
||||||
|
u.lifecycleMode = "remote"
|
||||||
|
u.masterPassword.SetText(key.Password)
|
||||||
|
u.remoteBaseURL.SetText(server.URL)
|
||||||
|
u.remotePath.SetText("vault.kdbx")
|
||||||
|
u.remoteUsername.SetText("tjulian")
|
||||||
|
u.remotePassword.SetText("token-1")
|
||||||
|
|
||||||
|
if err := u.openRemoteAction(); err != nil {
|
||||||
|
t.Fatalf("openRemoteAction() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := u.vaultPath.Text(); got != cachePath {
|
||||||
|
t.Fatalf("vaultPath = %q, want %q", got, cachePath)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(cachePath); err != nil {
|
||||||
|
t.Fatalf("Stat(cachePath) error = %v", err)
|
||||||
|
}
|
||||||
|
if got := len(u.recentRemotes); got != 1 {
|
||||||
|
t.Fatalf("len(recentRemotes) = %d, want 1", got)
|
||||||
|
}
|
||||||
|
record := u.recentRemotes[0]
|
||||||
|
if record.LocalVaultPath != cachePath {
|
||||||
|
t.Fatalf("recentRemotes[0].LocalVaultPath = %q, want %q", record.LocalVaultPath, cachePath)
|
||||||
|
}
|
||||||
|
if record.RemoteProfileID == "" || record.CredentialEntryID == "" {
|
||||||
|
t.Fatalf("recentRemotes[0] = %#v, want binding ids populated", record)
|
||||||
|
}
|
||||||
|
|
||||||
|
var reopened session.Manager
|
||||||
|
if err := reopened.Open(cachePath, key); err != nil {
|
||||||
|
t.Fatalf("Open(cachePath) error = %v", err)
|
||||||
|
}
|
||||||
|
model, err := reopened.Current()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Current() error = %v", err)
|
||||||
|
}
|
||||||
|
if got := len(model.RemoteProfiles); got != 1 {
|
||||||
|
t.Fatalf("len(RemoteProfiles) = %d, want 1", got)
|
||||||
|
}
|
||||||
|
if got := model.RemoteProfiles[0].BaseURL; got != server.URL {
|
||||||
|
t.Fatalf("RemoteProfiles[0].BaseURL = %q, want %q", got, server.URL)
|
||||||
|
}
|
||||||
|
entry, err := model.EntryByID(record.CredentialEntryID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("EntryByID(%q) error = %v", record.CredentialEntryID, err)
|
||||||
|
}
|
||||||
|
if entry.Username != "tjulian" || entry.Password != "token-1" {
|
||||||
|
t.Fatalf("credential entry = %#v, want tjulian/token-1", entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUIStartOpenRemoteActionMaterializesLocalCacheAndBinding(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
dir := t.TempDir()
|
||||||
|
cachePath := filepath.Join(dir, "remote-cache.kdbx")
|
||||||
|
paths := statePaths{
|
||||||
|
DefaultSaveAsPath: cachePath,
|
||||||
|
RecentVaultsPath: filepath.Join(dir, "recent-vaults.json"),
|
||||||
|
RecentRemotesPath: filepath.Join(dir, "recent-remotes.json"),
|
||||||
|
UIPreferencesPath: filepath.Join(dir, "ui-prefs.json"),
|
||||||
|
}
|
||||||
|
key := vault.MasterKey{Password: "correct horse battery staple"}
|
||||||
|
|
||||||
|
var encoded bytes.Buffer
|
||||||
|
if err := vault.SaveKDBXWithKey(&encoded, vault.Model{
|
||||||
|
Entries: []vault.Entry{
|
||||||
|
{ID: "entry-1", Title: "Vault Console", Path: []string{"Root", "Internet"}},
|
||||||
|
},
|
||||||
|
}, key); err != nil {
|
||||||
|
t.Fatalf("SaveKDBXWithKey() 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(encoded.Bytes())
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
u := newUIWithState("desktop", &session.Manager{}, paths)
|
||||||
|
u.lifecycleMode = "remote"
|
||||||
|
u.masterPassword.SetText(key.Password)
|
||||||
|
u.remoteBaseURL.SetText(server.URL)
|
||||||
|
u.remotePath.SetText("vault.kdbx")
|
||||||
|
u.remoteUsername.SetText("tjulian")
|
||||||
|
u.remotePassword.SetText("token-1")
|
||||||
|
|
||||||
|
u.startOpenRemoteAction()
|
||||||
|
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 != cachePath {
|
||||||
|
t.Fatalf("vaultPath = %q, want %q", got, cachePath)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(cachePath); err != nil {
|
||||||
|
t.Fatalf("Stat(cachePath) error = %v", err)
|
||||||
|
}
|
||||||
|
if got := len(u.recentRemotes); got != 1 {
|
||||||
|
t.Fatalf("len(recentRemotes) = %d, want 1", got)
|
||||||
|
}
|
||||||
|
record := u.recentRemotes[0]
|
||||||
|
if record.LocalVaultPath != cachePath {
|
||||||
|
t.Fatalf("recentRemotes[0].LocalVaultPath = %q, want %q", record.LocalVaultPath, cachePath)
|
||||||
|
}
|
||||||
|
if record.RemoteProfileID == "" || record.CredentialEntryID == "" {
|
||||||
|
t.Fatalf("recentRemotes[0] = %#v, want binding ids populated", record)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDefaultStatePathsUsesProvidedStateDir(t *testing.T) {
|
func TestDefaultStatePathsUsesProvidedStateDir(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user