Match Advanced Sync credentials by URL
This commit is contained in:
@@ -103,6 +103,7 @@ func ConfigureRemoteBinding(model *vault.Model, input RemoteBindingInput) (Remot
|
||||
Title: input.CredentialTitle,
|
||||
Username: input.Username,
|
||||
Password: input.Password,
|
||||
URL: input.BaseURL,
|
||||
Path: append([]string(nil), input.CredentialPath...),
|
||||
})
|
||||
|
||||
|
||||
@@ -151,6 +151,9 @@ func TestConfigureRemoteBindingStoresProfileAndCredentialsInVault(t *testing.T)
|
||||
if credentials.Username != "tjulian" || credentials.Password != "token-1" {
|
||||
t.Fatalf("stored credential entry = %#v, want tjulian/token-1", credentials)
|
||||
}
|
||||
if credentials.URL != "https://dav.example.invalid/remote.php/dav" {
|
||||
t.Fatalf("stored credential entry URL = %q, want remote.php/dav URL", credentials.URL)
|
||||
}
|
||||
|
||||
if binding.LocalVaultPath != "/tmp/family.kdbx" {
|
||||
t.Fatalf("binding LocalVaultPath = %q, want /tmp/family.kdbx", binding.LocalVaultPath)
|
||||
|
||||
@@ -395,6 +395,7 @@ type ui struct {
|
||||
recentRemoteClicks []widget.Clickable
|
||||
vaultRemoteProfileClicks []widget.Clickable
|
||||
vaultRemoteCredentialClicks []widget.Clickable
|
||||
syncRemoteCredentialClicks []widget.Clickable
|
||||
removeCustomFields []widget.Clickable
|
||||
state appstate.State
|
||||
visible []entry
|
||||
@@ -444,6 +445,7 @@ type ui struct {
|
||||
syncRemotePath widget.Editor
|
||||
syncRemoteUsername widget.Editor
|
||||
syncRemotePassword widget.Editor
|
||||
selectedSyncRemoteCredentialEntryID string
|
||||
syncDialogOpen bool
|
||||
syncMenuOpen bool
|
||||
mainMenuOpen bool
|
||||
@@ -2177,6 +2179,36 @@ func (u *ui) availableRemoteCredentialEntries() []vault.Entry {
|
||||
return entries
|
||||
}
|
||||
|
||||
func normalizeRemoteCredentialURL(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
raw = strings.TrimRight(raw, "/")
|
||||
return raw
|
||||
}
|
||||
|
||||
func (u *ui) matchingAdvancedSyncRemoteCredentialEntries() []vault.Entry {
|
||||
if sanitizeSyncSourceMode(u.syncSourceMode) != syncSourceRemote {
|
||||
return nil
|
||||
}
|
||||
baseURL := normalizeRemoteCredentialURL(u.syncRemoteBaseURL.Text())
|
||||
if baseURL == "" {
|
||||
return nil
|
||||
}
|
||||
var matches []vault.Entry
|
||||
for _, entry := range u.availableRemoteCredentialEntries() {
|
||||
if normalizeRemoteCredentialURL(entry.URL) != baseURL {
|
||||
continue
|
||||
}
|
||||
matches = append(matches, entry)
|
||||
}
|
||||
return matches
|
||||
}
|
||||
|
||||
func (u *ui) applyAdvancedSyncRemoteCredentialEntry(entry vault.Entry) {
|
||||
u.selectedSyncRemoteCredentialEntryID = strings.TrimSpace(entry.ID)
|
||||
u.syncRemoteUsername.SetText(strings.TrimSpace(entry.Username))
|
||||
u.syncRemotePassword.SetText(entry.Password)
|
||||
}
|
||||
|
||||
func (u *ui) selectVaultRemoteProfile(id string) {
|
||||
id = strings.TrimSpace(id)
|
||||
u.selectedVaultRemoteProfileID = id
|
||||
@@ -4023,6 +4055,14 @@ func (u *ui) layout(gtx layout.Context) layout.Dimensions {
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := range u.syncRemoteCredentialClicks {
|
||||
for u.syncRemoteCredentialClicks[i].Clicked(gtx) {
|
||||
entries := u.matchingAdvancedSyncRemoteCredentialEntries()
|
||||
if i < len(entries) {
|
||||
u.applyAdvancedSyncRemoteCredentialEntry(entries[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
for u.openSelectedVaultRemote.Clicked(gtx) {
|
||||
if u.lifecycleBusy() {
|
||||
continue
|
||||
@@ -4766,6 +4806,10 @@ func (u *ui) approvalDialogContent(gtx layout.Context) layout.Dimensions {
|
||||
}
|
||||
|
||||
func (u *ui) syncDialogContent(gtx layout.Context) layout.Dimensions {
|
||||
matchingCredentials := u.matchingAdvancedSyncRemoteCredentialEntries()
|
||||
if len(u.syncRemoteCredentialClicks) < len(matchingCredentials) {
|
||||
u.syncRemoteCredentialClicks = make([]widget.Clickable, len(matchingCredentials))
|
||||
}
|
||||
return material.List(u.theme, &u.lifecycleList).Layout(gtx, 1, func(gtx layout.Context, _ int) layout.Dimensions {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
@@ -4814,7 +4858,7 @@ func (u *ui) syncDialogContent(gtx layout.Context) layout.Dimensions {
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
if u.syncSourceMode == syncSourceRemote {
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
children := []layout.FlexChild{
|
||||
layout.Rigid(labeledEditorHelp(u.theme, "Remote Base URL", "WebDAV base URL for the other source.", &u.syncRemoteBaseURL, false)),
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||
layout.Rigid(labeledEditorHelp(u.theme, "Remote Path", "Path to the other remote .kdbx file.", &u.syncRemotePath, false)),
|
||||
@@ -4824,6 +4868,40 @@ func (u *ui) syncDialogContent(gtx layout.Context) layout.Dimensions {
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
return u.syncPasswordField(gtx)
|
||||
}),
|
||||
}
|
||||
if len(matchingCredentials) > 0 {
|
||||
children = append(children,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(11), "Matching vault credentials")
|
||||
lbl.Color = mutedColor
|
||||
return lbl.Layout(gtx)
|
||||
}),
|
||||
)
|
||||
for i, entry := range matchingCredentials {
|
||||
i := i
|
||||
entry := entry
|
||||
children = append(children,
|
||||
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
|
||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||
label := entry.Title
|
||||
if strings.TrimSpace(entry.Username) != "" {
|
||||
label += " · " + strings.TrimSpace(entry.Username)
|
||||
}
|
||||
selected := strings.TrimSpace(u.selectedSyncRemoteCredentialEntryID) == entry.ID
|
||||
return recentSelectionCard(gtx, selected, func(gtx layout.Context) layout.Dimensions {
|
||||
return u.syncRemoteCredentialClicks[i].Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||
lbl := material.Label(u.theme, unit.Sp(13), label)
|
||||
lbl.Color = accentColor
|
||||
return lbl.Layout(gtx)
|
||||
})
|
||||
})
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||
children...,
|
||||
)
|
||||
}
|
||||
if supportsDesktopFilePicker(runtime.GOOS) {
|
||||
|
||||
@@ -5826,6 +5826,9 @@ func TestUISaveCurrentRemoteBindingActionPersistsBindingIntoVault(t *testing.T)
|
||||
if !slices.Equal(entry.Path, []string{"Crew", "Internet"}) {
|
||||
t.Fatalf("credential path = %v, want [Crew Internet]", entry.Path)
|
||||
}
|
||||
if entry.URL != "https://dav.example.invalid/remote.php/dav" {
|
||||
t.Fatalf("credential URL = %q, want remote.php/dav URL", entry.URL)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
@@ -5843,6 +5846,53 @@ func TestUISaveCurrentRemoteBindingActionPersistsBindingIntoVault(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIAdvancedSyncMatchingRemoteCredentialEntriesUsesBaseURL(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithModel("desktop", vault.Model{
|
||||
Entries: []vault.Entry{
|
||||
{ID: "entry-1", Title: "Bellagio", Username: "rustyryan", URL: "https://dav.example.invalid/remote.php/dav/", Path: []string{"Crew", "Internet"}},
|
||||
{ID: "entry-2", Title: "Vault Console", Username: "dannyocean", URL: "https://vault.crew.example.invalid", Path: []string{"Crew", "Internet"}},
|
||||
{ID: "entry-3", Title: "WebDAV Sign-In", Username: "tjulian", URL: "https://dav.example.invalid/remote.php/dav", Path: []string{"Crew", "Internet"}},
|
||||
},
|
||||
})
|
||||
u.syncSourceMode = syncSourceRemote
|
||||
u.syncRemoteBaseURL.SetText("https://dav.example.invalid/remote.php/dav")
|
||||
|
||||
got := u.matchingAdvancedSyncRemoteCredentialEntries()
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("len(matchingAdvancedSyncRemoteCredentialEntries()) = %d, want 2", len(got))
|
||||
}
|
||||
if got[0].ID != "entry-1" || got[1].ID != "entry-3" {
|
||||
t.Fatalf("matchingAdvancedSyncRemoteCredentialEntries() = %#v, want Bellagio and WebDAV Sign-In matches", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUIApplyAdvancedSyncRemoteCredentialEntryFillsCredentials(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
u := newUIWithModel("desktop", vault.Model{})
|
||||
entry := vault.Entry{
|
||||
ID: "remote-creds-1",
|
||||
Title: "WebDAV Sign-In",
|
||||
Username: "tjulian",
|
||||
Password: "token-1",
|
||||
URL: "https://dav.example.invalid/remote.php/dav",
|
||||
}
|
||||
|
||||
u.applyAdvancedSyncRemoteCredentialEntry(entry)
|
||||
|
||||
if got := u.syncRemoteUsername.Text(); got != "tjulian" {
|
||||
t.Fatalf("syncRemoteUsername = %q, want tjulian", got)
|
||||
}
|
||||
if got := u.syncRemotePassword.Text(); got != "token-1" {
|
||||
t.Fatalf("syncRemotePassword = %q, want token-1", got)
|
||||
}
|
||||
if got := u.selectedSyncRemoteCredentialEntryID; got != "remote-creds-1" {
|
||||
t.Fatalf("selectedSyncRemoteCredentialEntryID = %q, want remote-creds-1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUISaveCurrentRemoteBindingActionRequiresCompleteRemoteSignIn(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user