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,
|
Title: input.CredentialTitle,
|
||||||
Username: input.Username,
|
Username: input.Username,
|
||||||
Password: input.Password,
|
Password: input.Password,
|
||||||
|
URL: input.BaseURL,
|
||||||
Path: append([]string(nil), input.CredentialPath...),
|
Path: append([]string(nil), input.CredentialPath...),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -151,6 +151,9 @@ func TestConfigureRemoteBindingStoresProfileAndCredentialsInVault(t *testing.T)
|
|||||||
if credentials.Username != "tjulian" || credentials.Password != "token-1" {
|
if credentials.Username != "tjulian" || credentials.Password != "token-1" {
|
||||||
t.Fatalf("stored credential entry = %#v, want tjulian/token-1", credentials)
|
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" {
|
if binding.LocalVaultPath != "/tmp/family.kdbx" {
|
||||||
t.Fatalf("binding LocalVaultPath = %q, want /tmp/family.kdbx", binding.LocalVaultPath)
|
t.Fatalf("binding LocalVaultPath = %q, want /tmp/family.kdbx", binding.LocalVaultPath)
|
||||||
|
|||||||
@@ -395,6 +395,7 @@ type ui struct {
|
|||||||
recentRemoteClicks []widget.Clickable
|
recentRemoteClicks []widget.Clickable
|
||||||
vaultRemoteProfileClicks []widget.Clickable
|
vaultRemoteProfileClicks []widget.Clickable
|
||||||
vaultRemoteCredentialClicks []widget.Clickable
|
vaultRemoteCredentialClicks []widget.Clickable
|
||||||
|
syncRemoteCredentialClicks []widget.Clickable
|
||||||
removeCustomFields []widget.Clickable
|
removeCustomFields []widget.Clickable
|
||||||
state appstate.State
|
state appstate.State
|
||||||
visible []entry
|
visible []entry
|
||||||
@@ -444,6 +445,7 @@ type ui struct {
|
|||||||
syncRemotePath widget.Editor
|
syncRemotePath widget.Editor
|
||||||
syncRemoteUsername widget.Editor
|
syncRemoteUsername widget.Editor
|
||||||
syncRemotePassword widget.Editor
|
syncRemotePassword widget.Editor
|
||||||
|
selectedSyncRemoteCredentialEntryID string
|
||||||
syncDialogOpen bool
|
syncDialogOpen bool
|
||||||
syncMenuOpen bool
|
syncMenuOpen bool
|
||||||
mainMenuOpen bool
|
mainMenuOpen bool
|
||||||
@@ -2177,6 +2179,36 @@ func (u *ui) availableRemoteCredentialEntries() []vault.Entry {
|
|||||||
return entries
|
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) {
|
func (u *ui) selectVaultRemoteProfile(id string) {
|
||||||
id = strings.TrimSpace(id)
|
id = strings.TrimSpace(id)
|
||||||
u.selectedVaultRemoteProfileID = 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) {
|
for u.openSelectedVaultRemote.Clicked(gtx) {
|
||||||
if u.lifecycleBusy() {
|
if u.lifecycleBusy() {
|
||||||
continue
|
continue
|
||||||
@@ -4766,6 +4806,10 @@ func (u *ui) approvalDialogContent(gtx layout.Context) layout.Dimensions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (u *ui) syncDialogContent(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 material.List(u.theme, &u.lifecycleList).Layout(gtx, 1, func(gtx layout.Context, _ int) layout.Dimensions {
|
||||||
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
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(layout.Spacer{Height: unit.Dp(12)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
if u.syncSourceMode == syncSourceRemote {
|
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(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(layout.Spacer{Height: unit.Dp(6)}.Layout),
|
||||||
layout.Rigid(labeledEditorHelp(u.theme, "Remote Path", "Path to the other remote .kdbx file.", &u.syncRemotePath, false)),
|
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 {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
return u.syncPasswordField(gtx)
|
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) {
|
if supportsDesktopFilePicker(runtime.GOOS) {
|
||||||
|
|||||||
@@ -5826,6 +5826,9 @@ func TestUISaveCurrentRemoteBindingActionPersistsBindingIntoVault(t *testing.T)
|
|||||||
if !slices.Equal(entry.Path, []string{"Crew", "Internet"}) {
|
if !slices.Equal(entry.Path, []string{"Crew", "Internet"}) {
|
||||||
t.Fatalf("credential path = %v, want [Crew Internet]", entry.Path)
|
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 {
|
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) {
|
func TestUISaveCurrentRemoteBindingActionRequiresCompleteRemoteSignIn(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user