Add browser search and richer URL matching
This commit is contained in:
+108
-9
@@ -275,7 +275,7 @@ func (s *Server) FindBrowserLogins(ctx context.Context, req *keepassgov1.FindBro
|
||||
|
||||
var matches []rankedBrowserMatch
|
||||
for _, entry := range displayModel.Entries {
|
||||
quality, score := classifyBrowserEntryMatch(pageHost, entry.URL)
|
||||
quality, score := classifyBrowserEntry(pageHost, entry)
|
||||
if score == 0 {
|
||||
continue
|
||||
}
|
||||
@@ -390,7 +390,7 @@ func (s *Server) GetBrowserCredential(ctx context.Context, req *keepassgov1.GetB
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
if _, score := classifyBrowserEntryMatch(pageHost, entry.URL); score == 0 {
|
||||
if _, score := classifyBrowserEntry(pageHost, entry); score == 0 {
|
||||
return nil, status.Error(codes.InvalidArgument, "entry url does not match requested page")
|
||||
}
|
||||
}
|
||||
@@ -446,19 +446,22 @@ func (s *Server) ListEntries(ctx context.Context, req *keepassgov1.ListEntriesRe
|
||||
}
|
||||
displayModel := visibleModel(model)
|
||||
internalPath := expandClientPath(displayModel, req.GetPath())
|
||||
if _, err := s.authorizePathRequest(ctx, apitokens.OperationListEntries, internalPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
model = displayModel
|
||||
var entries []vault.Entry
|
||||
if strings.TrimSpace(req.GetQuery()) != "" {
|
||||
token, err := s.authenticateRequest(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results := model.Search(req.GetQuery())
|
||||
entries = make([]vault.Entry, 0, len(results))
|
||||
for _, result := range results {
|
||||
entries = append(entries, result.Entry)
|
||||
entries, err = s.authorizedSearchEntries(ctx, model, token, internalPath, results)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if _, err := s.authorizePathRequest(ctx, apitokens.OperationListEntries, internalPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entries = model.EntriesInPath(internalPath)
|
||||
}
|
||||
|
||||
@@ -472,6 +475,49 @@ func (s *Server) ListEntries(ctx context.Context, req *keepassgov1.ListEntriesRe
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *Server) authorizedSearchEntries(ctx context.Context, model vault.Model, token apitokens.Token, path []string, results []vault.SearchResult) ([]vault.Entry, error) {
|
||||
entries := make([]vault.Entry, 0, len(results))
|
||||
var promptResource *apitokens.Resource
|
||||
for _, result := range results {
|
||||
entry := result.Entry
|
||||
if !hasPathPrefix(path, entry.Path) {
|
||||
continue
|
||||
}
|
||||
resource := apitokens.Resource{Kind: apitokens.ResourceGroup, Path: entry.Path}
|
||||
switch evaluateAuthorization(model, token, apitokens.OperationListEntries, resource) {
|
||||
case apitokens.DecisionAllow:
|
||||
entries = append(entries, entry)
|
||||
case apitokens.DecisionPrompt:
|
||||
if promptResource == nil {
|
||||
candidate := resource
|
||||
promptResource = &candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(entries) != 0 || promptResource == nil {
|
||||
return entries, nil
|
||||
}
|
||||
if _, err := s.authorizeResourceRequest(ctx, token, apitokens.OperationListEntries, *promptResource); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return authorizedSearchEntriesWithinPath(path, promptResource.Path, results), nil
|
||||
}
|
||||
|
||||
func authorizedSearchEntriesWithinPath(requestPath, approvedPath []string, results []vault.SearchResult) []vault.Entry {
|
||||
entries := make([]vault.Entry, 0, len(results))
|
||||
for _, result := range results {
|
||||
entry := result.Entry
|
||||
if !hasPathPrefix(requestPath, entry.Path) {
|
||||
continue
|
||||
}
|
||||
if !hasPathPrefix(approvedPath, entry.Path) {
|
||||
continue
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
func (s *Server) ListGroups(ctx context.Context, req *keepassgov1.ListGroupsRequest) (*keepassgov1.ListGroupsResponse, error) {
|
||||
model, locked := s.snapshotModel()
|
||||
if locked {
|
||||
@@ -1063,6 +1109,52 @@ func normalizedBrowserEntryHost(raw string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func browserURLFieldKey(key string) bool {
|
||||
if len(key) <= len("URL") || !strings.EqualFold(key[:len("URL")], "URL") {
|
||||
return false
|
||||
}
|
||||
for _, r := range key[len("URL"):] {
|
||||
if r < '0' || r > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func browserEntryURLs(entry vault.Entry) []string {
|
||||
urls := make([]string, 0, 1+len(entry.Fields))
|
||||
if raw := strings.TrimSpace(entry.URL); raw != "" {
|
||||
urls = append(urls, raw)
|
||||
}
|
||||
if len(entry.Fields) == 0 {
|
||||
return urls
|
||||
}
|
||||
keys := slices.Collect(maps.Keys(entry.Fields))
|
||||
slices.Sort(keys)
|
||||
for _, key := range keys {
|
||||
if !browserURLFieldKey(key) {
|
||||
continue
|
||||
}
|
||||
if raw := strings.TrimSpace(entry.Fields[key]); raw != "" {
|
||||
urls = append(urls, raw)
|
||||
}
|
||||
}
|
||||
return urls
|
||||
}
|
||||
|
||||
func classifyBrowserEntry(pageHost string, entry vault.Entry) (string, int) {
|
||||
bestQuality := ""
|
||||
bestScore := 0
|
||||
for _, rawURL := range browserEntryURLs(entry) {
|
||||
quality, score := classifyBrowserEntryMatch(pageHost, rawURL)
|
||||
if score > bestScore {
|
||||
bestQuality = quality
|
||||
bestScore = score
|
||||
}
|
||||
}
|
||||
return bestQuality, bestScore
|
||||
}
|
||||
|
||||
func classifyBrowserEntryMatch(pageHost, rawEntryURL string) (string, int) {
|
||||
entryHost := normalizedBrowserEntryHost(rawEntryURL)
|
||||
if entryHost == "" {
|
||||
@@ -1078,6 +1170,13 @@ func classifyBrowserEntryMatch(pageHost, rawEntryURL string) (string, int) {
|
||||
}
|
||||
}
|
||||
|
||||
func hasPathPrefix(prefix, path []string) bool {
|
||||
if len(prefix) > len(path) {
|
||||
return false
|
||||
}
|
||||
return slices.Equal(prefix, path[:len(prefix)])
|
||||
}
|
||||
|
||||
func visibleModel(model vault.Model) vault.Model {
|
||||
out := model
|
||||
out.Entries = nil
|
||||
|
||||
Reference in New Issue
Block a user