Save remote bindings from the sync menu
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
@@ -276,6 +278,7 @@ type ui struct {
|
|||||||
toggleMainMenu widget.Clickable
|
toggleMainMenu widget.Clickable
|
||||||
openAdvancedSync widget.Clickable
|
openAdvancedSync widget.Clickable
|
||||||
openSelectedVaultRemote widget.Clickable
|
openSelectedVaultRemote widget.Clickable
|
||||||
|
saveCurrentRemoteBinding widget.Clickable
|
||||||
openSecuritySettings widget.Clickable
|
openSecuritySettings widget.Clickable
|
||||||
openRemotePrefsHelp widget.Clickable
|
openRemotePrefsHelp widget.Clickable
|
||||||
closeAdvancedSync widget.Clickable
|
closeAdvancedSync widget.Clickable
|
||||||
@@ -1985,6 +1988,66 @@ func (u *ui) selectedVaultRemoteBinding() (appstate.RemoteBinding, bool) {
|
|||||||
}, true
|
}, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func remoteBindingSuffix(baseURL, path, username string) string {
|
||||||
|
sum := sha256.Sum256([]byte(strings.TrimSpace(baseURL) + "\n" + strings.TrimSpace(path) + "\n" + strings.TrimSpace(username)))
|
||||||
|
return hex.EncodeToString(sum[:8])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) currentRemoteBindingInput() (appstate.RemoteBindingInput, error) {
|
||||||
|
baseURL := strings.TrimSpace(u.remoteBaseURL.Text())
|
||||||
|
remotePath := strings.TrimSpace(u.remotePath.Text())
|
||||||
|
username := strings.TrimSpace(u.remoteUsername.Text())
|
||||||
|
password := u.remotePassword.Text()
|
||||||
|
localVaultPath := strings.TrimSpace(u.vaultPath.Text())
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case localVaultPath == "":
|
||||||
|
return appstate.RemoteBindingInput{}, fmt.Errorf("local vault path is required")
|
||||||
|
case baseURL == "":
|
||||||
|
return appstate.RemoteBindingInput{}, fmt.Errorf("remote base URL is required")
|
||||||
|
case remotePath == "":
|
||||||
|
return appstate.RemoteBindingInput{}, fmt.Errorf("remote path is required")
|
||||||
|
case username == "":
|
||||||
|
return appstate.RemoteBindingInput{}, fmt.Errorf("remote username is required")
|
||||||
|
case password == "":
|
||||||
|
return appstate.RemoteBindingInput{}, fmt.Errorf("remote password is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
suffix := remoteBindingSuffix(baseURL, remotePath, username)
|
||||||
|
credentialTitle := "WebDAV Sign-In"
|
||||||
|
if username != "" {
|
||||||
|
credentialTitle += " · " + username
|
||||||
|
}
|
||||||
|
|
||||||
|
return appstate.RemoteBindingInput{
|
||||||
|
LocalVaultPath: localVaultPath,
|
||||||
|
RemoteProfileID: "remote-profile-" + suffix,
|
||||||
|
RemoteProfileName: friendlyRecentRemoteLabel(recentRemoteRecord{BaseURL: baseURL, Path: remotePath}),
|
||||||
|
BaseURL: baseURL,
|
||||||
|
RemotePath: remotePath,
|
||||||
|
CredentialEntryID: "remote-credential-" + suffix,
|
||||||
|
CredentialTitle: credentialTitle,
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
CredentialPath: append([]string(nil), u.currentPath...),
|
||||||
|
SyncMode: appstate.SyncModeAutomaticOnOpenSave,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ui) saveCurrentRemoteBindingAction() error {
|
||||||
|
input, err := u.currentRemoteBindingInput()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
binding, err := u.state.ConfigureRemoteBinding(input)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
u.selectedVaultRemoteProfileID = binding.RemoteProfileID
|
||||||
|
u.selectedVaultRemoteCredentialEntryID = binding.CredentialEntryID
|
||||||
|
return 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 {
|
||||||
@@ -3341,6 +3404,9 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
|
|||||||
}
|
}
|
||||||
u.startOpenRemoteAction()
|
u.startOpenRemoteAction()
|
||||||
}
|
}
|
||||||
|
for u.saveCurrentRemoteBinding.Clicked(gtx) {
|
||||||
|
u.runAction("save remote binding", u.saveCurrentRemoteBindingAction)
|
||||||
|
}
|
||||||
for u.clearVaultSelection.Clicked(gtx) {
|
for u.clearVaultSelection.Clicked(gtx) {
|
||||||
if u.lifecycleBusy() {
|
if u.lifecycleBusy() {
|
||||||
continue
|
continue
|
||||||
@@ -4531,6 +4597,26 @@ func (u *ui) syncMenu(gtx layout.Context) layout.Dimensions {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if u.hasOpenVault() {
|
||||||
|
baseURL := strings.TrimSpace(u.remoteBaseURL.Text())
|
||||||
|
remotePath := strings.TrimSpace(u.remotePath.Text())
|
||||||
|
username := strings.TrimSpace(u.remoteUsername.Text())
|
||||||
|
password := u.remotePassword.Text()
|
||||||
|
if baseURL != "" && remotePath != "" && username != "" && password != "" {
|
||||||
|
rows = append(rows,
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
lbl := material.Label(u.theme, unit.Sp(11), "Store the current remote sign-in in this vault")
|
||||||
|
lbl.Color = mutedColor
|
||||||
|
return lbl.Layout(gtx)
|
||||||
|
}),
|
||||||
|
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||||
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
|
return tonedButton(gtx, u.theme, &u.saveCurrentRemoteBinding, "Save Current Remote In Vault")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, rows...)
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, rows...)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4743,6 +4743,67 @@ func TestUISelectVaultRemoteCredentialEntryUpdatesSelection(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUISaveCurrentRemoteBindingActionPersistsBindingIntoVault(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
u := newUIWithModel("desktop", vault.Model{})
|
||||||
|
u.currentPath = []string{"Crew", "Internet"}
|
||||||
|
u.vaultPath.SetText("/tmp/family.kdbx")
|
||||||
|
u.remoteBaseURL.SetText("https://dav.example.invalid/remote.php/dav")
|
||||||
|
u.remotePath.SetText("files/family/keepass.kdbx")
|
||||||
|
u.remoteUsername.SetText("tjulian")
|
||||||
|
u.remotePassword.SetText("token-1")
|
||||||
|
|
||||||
|
if err := u.saveCurrentRemoteBindingAction(); err != nil {
|
||||||
|
t.Fatalf("saveCurrentRemoteBindingAction() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
profiles := u.availableRemoteProfiles()
|
||||||
|
if len(profiles) != 1 {
|
||||||
|
t.Fatalf("len(availableRemoteProfiles()) = %d, want 1", len(profiles))
|
||||||
|
}
|
||||||
|
if profiles[0].BaseURL != "https://dav.example.invalid/remote.php/dav" {
|
||||||
|
t.Fatalf("saved profile = %#v, want persisted base URL", profiles[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
entries := u.availableRemoteCredentialEntries()
|
||||||
|
var found bool
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.Username == "tjulian" && entry.Password == "token-1" {
|
||||||
|
found = true
|
||||||
|
if !slices.Equal(entry.Path, []string{"Crew", "Internet"}) {
|
||||||
|
t.Fatalf("credential path = %v, want [Crew Internet]", entry.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Fatalf("availableRemoteCredentialEntries() = %#v, want persisted tjulian/token-1 entry", entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := u.selectedVaultRemoteProfileID; got == "" {
|
||||||
|
t.Fatal("selectedVaultRemoteProfileID = empty, want selected saved profile")
|
||||||
|
}
|
||||||
|
if got := u.selectedVaultRemoteCredentialEntryID; got == "" {
|
||||||
|
t.Fatal("selectedVaultRemoteCredentialEntryID = empty, want selected saved credential entry")
|
||||||
|
}
|
||||||
|
if !u.state.Dirty {
|
||||||
|
t.Fatal("state.Dirty = false, want true after saving binding into vault")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUISaveCurrentRemoteBindingActionRequiresCompleteRemoteSignIn(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
u := newUIWithModel("desktop", vault.Model{})
|
||||||
|
u.vaultPath.SetText("/tmp/family.kdbx")
|
||||||
|
u.remoteBaseURL.SetText("https://dav.example.invalid/remote.php/dav")
|
||||||
|
u.remotePath.SetText("files/family/keepass.kdbx")
|
||||||
|
|
||||||
|
if err := u.saveCurrentRemoteBindingAction(); err == nil {
|
||||||
|
t.Fatal("saveCurrentRemoteBindingAction() error = nil, want validation error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSwitchToLifecycleSelectionResetsLockedLocalSession(t *testing.T) {
|
func TestSwitchToLifecycleSelectionResetsLockedLocalSession(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user