Fix hidden root navigation and browser fill matching

This commit is contained in:
Joe Julian
2026-04-11 11:53:42 -07:00
parent c8f91b300b
commit e16067b345
9 changed files with 186 additions and 25 deletions
+21 -5
View File
@@ -1027,12 +1027,28 @@ func normalizedBrowserHost(raw string) (string, error) {
return host, nil
}
func classifyBrowserEntryMatch(pageHost, rawEntryURL string) (string, int) {
parsed, err := url.Parse(strings.TrimSpace(rawEntryURL))
if err != nil {
return "", 0
func normalizedBrowserEntryHost(raw string) string {
raw = strings.TrimSpace(raw)
if raw == "" {
return ""
}
entryHost := strings.ToLower(parsed.Hostname())
parsed, err := url.Parse(raw)
if err == nil {
if host := strings.ToLower(parsed.Hostname()); host != "" {
return host
}
}
if !strings.Contains(raw, "://") {
parsed, err = url.Parse("https://" + raw)
if err == nil {
return strings.ToLower(parsed.Hostname())
}
}
return ""
}
func classifyBrowserEntryMatch(pageHost, rawEntryURL string) (string, int) {
entryHost := normalizedBrowserEntryHost(rawEntryURL)
if entryHost == "" {
return "", 0
}
+34
View File
@@ -184,6 +184,40 @@ func TestVaultServiceFindsBrowserLoginsForAuthorizedClients(t *testing.T) {
}
}
func TestVaultServiceFindsBrowserLoginsForSchemeLessEntryURLs(t *testing.T) {
t.Parallel()
client, _, cleanup := newTestClientForModel(t, vault.Model{
Entries: []vault.Entry{
{
ID: "gitlab",
Title: "GitLab",
Username: "jjulian",
Password: "secret",
URL: "gitlab.com",
Path: []string{"Root", "Internet"},
},
testAPITokenEntry(t,
apitokens.PolicyRule{Effect: apitokens.EffectAllow, Operation: apitokens.OperationListEntries, Resource: apitokens.Resource{Kind: apitokens.ResourceGroup, Path: []string{"Root"}}},
),
},
})
defer cleanup()
resp, err := client.FindBrowserLogins(tokenContext(defaultTestTokenSecret), &keepassgov1.FindBrowserLoginsRequest{
PageUrl: "https://gitlab.com/users/sign_in",
})
if err != nil {
t.Fatalf("FindBrowserLogins() error = %v", err)
}
if len(resp.Matches) != 1 {
t.Fatalf("len(FindBrowserLogins().Matches) = %d, want 1", len(resp.Matches))
}
if resp.Matches[0].Id != "gitlab" {
t.Fatalf("FindBrowserLogins().Matches[0].Id = %q, want gitlab", resp.Matches[0].Id)
}
}
func TestVaultServiceFindsBrowserLoginsWithinAuthorizedGroupScope(t *testing.T) {
t.Parallel()
+2 -2
View File
@@ -2871,7 +2871,7 @@ func (u *ui) groupBar(gtx layout.Context) layout.Dimensions {
return layout.Inset{Bottom: unit.Dp(6)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
for u.groupClicks[idx].Clicked(gtx) {
u.state.EnterGroup(name)
u.currentPath = append([]string(nil), u.state.CurrentPath...)
u.adoptStateCurrentPath()
u.filter()
}
return tonedButton(gtx, u.theme, &u.groupClicks[idx], name)
@@ -2902,7 +2902,7 @@ func (u *ui) groupBar(gtx layout.Context) layout.Dimensions {
return layout.Inset{Bottom: unit.Dp(6)}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
for u.groupClicks[idx].Clicked(gtx) {
u.state.EnterGroup(name)
u.currentPath = append([]string(nil), u.state.CurrentPath...)
u.adoptStateCurrentPath()
u.filter()
}
return tonedButton(gtx, u.theme, &u.groupClicks[idx], name)
+1 -2
View File
@@ -275,8 +275,7 @@ func (u *ui) deleteCurrentGroupAction() error {
return err
}
u.clearDeleteGroupConfirmation()
u.currentPath = append([]string(nil), u.state.CurrentPath...)
u.syncedPath = append([]string(nil), u.state.CurrentPath...)
u.adoptStateCurrentPath()
u.filter()
return nil
}
+65 -3
View File
@@ -23,6 +23,7 @@ import (
detaillayout "git.julianfamily.org/keepassgo/internal/appui/detail/layout"
"git.julianfamily.org/keepassgo/internal/clipboard"
"git.julianfamily.org/keepassgo/internal/session"
"git.julianfamily.org/keepassgo/internal/vault"
)
func (u *ui) bannerSurface() uiBanner {
@@ -552,10 +553,72 @@ func (u *ui) setCurrentPath(path []string) {
u.clearDeleteGroupConfirmation()
}
func copyPath(path []string) []string {
return append([]string(nil), path...)
}
func pathExistsInModel(model vault.Model, path []string) bool {
return len(model.EntriesInPath(path)) > 0 || len(model.ChildGroups(path)) > 0 || hasExactGroup(model, path)
}
func normalizeEntriesPathWithoutModel(path []string, root string) []string {
if root == "" {
return copyPath(path)
}
if len(path) == 0 {
return []string{root}
}
if path[0] == "Root" {
return append([]string{root}, path[1:]...)
}
return copyPath(path)
}
func (u *ui) normalizedEntriesPath(path []string) []string {
if u.state.Section != appstate.SectionEntries {
return copyPath(path)
}
root := u.hiddenVaultRoot()
model, err := u.state.Session.Current()
if err != nil {
return normalizeEntriesPathWithoutModel(path, root)
}
if len(path) == 0 {
if root == "" {
return nil
}
return []string{root}
}
if path[0] == "Root" && root != "" {
candidate := append([]string{root}, path[1:]...)
if pathExistsInModel(model, candidate) {
return candidate
}
}
if (len(path) == 1 && root != "" && path[0] == root) || pathExistsInModel(model, path) {
return copyPath(path)
}
if root == "" {
return copyPath(path)
}
return []string{root}
}
func (u *ui) adoptStateCurrentPath() {
path := u.normalizedEntriesPath(u.state.CurrentPath)
u.currentPath = append([]string(nil), path...)
u.state.CurrentPath = append([]string(nil), path...)
u.syncedPath = append([]string(nil), path...)
u.syncPhoneGroupBrowser(path)
if len(u.deleteGroupPath) > 0 && !slices.Equal(u.deleteGroupPath, u.currentPath) {
u.clearDeleteGroupConfirmation()
}
}
func (u *ui) syncCurrentPath() {
switch {
case slices.Equal(u.currentPath, u.syncedPath) && !slices.Equal(u.state.CurrentPath, u.syncedPath):
u.currentPath = append([]string(nil), u.state.CurrentPath...)
u.adoptStateCurrentPath()
case !slices.Equal(u.currentPath, u.syncedPath) && slices.Equal(u.state.CurrentPath, u.syncedPath):
u.state.CurrentPath = append([]string(nil), u.currentPath...)
case !slices.Equal(u.currentPath, u.syncedPath) && !slices.Equal(u.state.CurrentPath, u.syncedPath):
@@ -1217,8 +1280,7 @@ func (u *ui) handleGroupClicks(gtx layout.Context) {
for u.moveGroup.Clicked(gtx) {
u.clearDeleteGroupConfirmation()
u.runAction("move group", u.moveCurrentGroupAction)
u.currentPath = append([]string(nil), u.state.CurrentPath...)
u.syncedPath = append([]string(nil), u.state.CurrentPath...)
u.adoptStateCurrentPath()
u.filter()
}
for u.toggleGroupControls.Clicked(gtx) {
+6 -6
View File
@@ -47,7 +47,7 @@ func (u *ui) createVaultAction() error {
u.noteRecentVault(u.saveAsTargetPath())
}
u.resetPasswordPeek()
u.currentPath = append([]string(nil), u.state.CurrentPath...)
u.adoptStateCurrentPath()
u.loadSecuritySettingsFromSession()
u.editingEntry = false
u.filter()
@@ -69,7 +69,7 @@ func (u *ui) openVaultAction() error {
}
u.noteRecentVault(path)
u.resetPasswordPeek()
u.currentPath = append([]string(nil), u.state.CurrentPath...)
u.adoptStateCurrentPath()
u.restoreRecentVaultGroup(path)
u.syncSavedRemoteBindingSelection()
if err := u.synchronizeSelectedRemoteBindingOnOpen(); err != nil {
@@ -111,7 +111,7 @@ func (u *ui) startOpenVaultAction() {
manager.ApplyPreparedLocalOpen(prepared)
u.noteRecentVault(path)
u.resetPasswordPeek()
u.currentPath = append([]string(nil), u.state.CurrentPath...)
u.adoptStateCurrentPath()
u.restoreRecentVaultGroup(path)
u.syncSavedRemoteBindingSelection()
if err := u.synchronizeSelectedRemoteBindingOnOpen(); err != nil {
@@ -329,7 +329,7 @@ func (u *ui) lockAction() error {
return err
}
u.requestMasterPassFocus = true
u.currentPath = append([]string(nil), u.state.CurrentPath...)
u.adoptStateCurrentPath()
u.resetPasswordPeek()
u.editingEntry = false
u.filter()
@@ -346,7 +346,7 @@ func (u *ui) unlockAction() error {
return err
}
u.resetPasswordPeek()
u.currentPath = append([]string(nil), u.state.CurrentPath...)
u.adoptStateCurrentPath()
u.loadSecuritySettingsFromSession()
u.editingEntry = false
u.filter()
@@ -375,7 +375,7 @@ func (u *ui) startUnlockAction() {
return func() error {
manager.ApplyPreparedUnlock(prepared)
u.resetPasswordPeek()
u.currentPath = append([]string(nil), u.state.CurrentPath...)
u.adoptStateCurrentPath()
u.loadSecuritySettingsFromSession()
u.editingEntry = false
u.filter()
+31
View File
@@ -5192,6 +5192,37 @@ func TestUIShowEntriesSectionRestoresHiddenRootAfterLeavingEntries(t *testing.T)
}
}
func TestUISyncCurrentPathNormalizesHiddenRootAfterSectionSwitch(t *testing.T) {
t.Parallel()
u := newUIWithModel("desktop", vault.Model{
Entries: []vault.Entry{
{ID: "1", Title: "Vault Console", Path: []string{"keepass", "Crew", "Internet"}},
},
Groups: [][]string{
{"keepass"},
{"keepass", "Crew"},
{"Recycle Bin"},
},
})
u.showEntriesSection()
u.showAPITokensSection()
u.state.Section = appstate.SectionEntries
u.state.CurrentPath = []string{"Root"}
u.currentPath = nil
u.syncedPath = nil
u.syncCurrentPath()
if got := u.currentPath; !slices.Equal(got, []string{"keepass"}) {
t.Fatalf("currentPath after syncCurrentPath() = %v, want [keepass]", got)
}
if got := u.displayPath(); len(got) != 0 {
t.Fatalf("displayPath() after syncCurrentPath() = %v, want root slash path", got)
}
}
func TestUIShowEntriesSectionRestoresEntriesViewState(t *testing.T) {
t.Parallel()