Fix scoped gRPC persistence and autosave behavior

This commit is contained in:
Joe Julian
2026-04-11 11:03:05 -07:00
parent 0de682a3af
commit 675aeebdeb
9 changed files with 551 additions and 85 deletions
+126 -42
View File
@@ -249,6 +249,7 @@ func (s *Server) FindBrowserLogins(ctx context.Context, req *keepassgov1.FindBro
if locked {
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
}
displayModel := visibleModel(model)
token, err := s.authenticateRequest(ctx)
if err != nil {
return nil, err
@@ -259,7 +260,7 @@ func (s *Server) FindBrowserLogins(ctx context.Context, req *keepassgov1.FindBro
}
var matches []rankedBrowserMatch
for _, entry := range visibleModel(model).Entries {
for _, entry := range displayModel.Entries {
quality, score := classifyBrowserEntryMatch(pageHost, entry.URL)
if score == 0 {
continue
@@ -425,11 +426,13 @@ func (s *Server) ListEntries(ctx context.Context, req *keepassgov1.ListEntriesRe
if locked {
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
}
if _, err := s.authorizePathRequest(ctx, apitokens.OperationListEntries, req.GetPath()); err != nil {
displayModel := visibleModel(model)
internalPath := expandClientPath(displayModel, req.GetPath())
if _, err := s.authorizePathRequest(ctx, apitokens.OperationListEntries, internalPath); err != nil {
return nil, err
}
model = visibleModel(model)
model = displayModel
var entries []vault.Entry
if strings.TrimSpace(req.GetQuery()) != "" {
results := model.Search(req.GetQuery())
@@ -438,14 +441,14 @@ func (s *Server) ListEntries(ctx context.Context, req *keepassgov1.ListEntriesRe
entries = append(entries, result.Entry)
}
} else {
entries = model.EntriesInPath(req.GetPath())
entries = model.EntriesInPath(internalPath)
}
resp := &keepassgov1.ListEntriesResponse{
Entries: make([]*keepassgov1.Entry, 0, len(entries)),
}
for _, entry := range entries {
resp.Entries = append(resp.Entries, entryToProto(entry))
resp.Entries = append(resp.Entries, entryToProtoWithModel(model, entry))
}
return resp, nil
@@ -456,44 +459,50 @@ func (s *Server) ListGroups(ctx context.Context, req *keepassgov1.ListGroupsRequ
if locked {
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
}
if _, err := s.authorizePathRequest(ctx, apitokens.OperationListGroups, req.GetPath()); err != nil {
displayModel := visibleModel(model)
internalPath := expandClientPath(displayModel, req.GetPath())
if _, err := s.authorizePathRequest(ctx, apitokens.OperationListGroups, internalPath); err != nil {
return nil, err
}
return &keepassgov1.ListGroupsResponse{
Names: visibleModel(model).ChildGroups(req.GetPath()),
Names: displayModel.ChildGroups(internalPath),
}, nil
}
func (s *Server) CreateGroup(ctx context.Context, req *keepassgov1.CreateGroupRequest) (*keepassgov1.CreateGroupResponse, error) {
if _, err := s.authorizePathRequest(ctx, apitokens.OperationMutateGroup, req.GetParentPath()); err != nil {
model, locked := s.snapshotModel()
if locked {
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
}
parentPath := expandClientPath(visibleModel(model), req.GetParentPath())
if _, err := s.authorizePathRequest(ctx, apitokens.OperationMutateGroup, parentPath); err != nil {
return nil, err
}
s.mu.Lock()
defer s.mu.Unlock()
if s.locked {
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
}
s.model.CreateGroup(req.GetParentPath(), req.GetName())
s.model.CreateGroup(parentPath, req.GetName())
s.dirty = true
s.syncMutationLocked()
return &keepassgov1.CreateGroupResponse{}, nil
}
func (s *Server) RenameGroup(ctx context.Context, req *keepassgov1.RenameGroupRequest) (*keepassgov1.RenameGroupResponse, error) {
if _, err := s.authorizePathRequest(ctx, apitokens.OperationMutateGroup, req.GetPath()); err != nil {
model, locked := s.snapshotModel()
if locked {
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
}
groupPath := expandClientPath(visibleModel(model), req.GetPath())
if _, err := s.authorizePathRequest(ctx, apitokens.OperationMutateGroup, groupPath); err != nil {
return nil, err
}
s.mu.Lock()
defer s.mu.Unlock()
if s.locked {
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
}
if err := s.model.RenameGroup(req.GetPath(), req.GetNewName()); err != nil {
if err := s.model.RenameGroup(groupPath, req.GetNewName()); err != nil {
if errors.Is(err, vault.ErrEntryNotFound) {
return nil, status.Error(codes.NotFound, err.Error())
}
@@ -506,17 +515,19 @@ func (s *Server) RenameGroup(ctx context.Context, req *keepassgov1.RenameGroupRe
}
func (s *Server) DeleteGroup(ctx context.Context, req *keepassgov1.DeleteGroupRequest) (*keepassgov1.DeleteGroupResponse, error) {
if _, err := s.authorizePathRequest(ctx, apitokens.OperationMutateGroup, req.GetPath()); err != nil {
model, locked := s.snapshotModel()
if locked {
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
}
groupPath := expandClientPath(visibleModel(model), req.GetPath())
if _, err := s.authorizePathRequest(ctx, apitokens.OperationMutateGroup, groupPath); err != nil {
return nil, err
}
s.mu.Lock()
defer s.mu.Unlock()
if s.locked {
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
}
if err := s.model.DeleteGroup(req.GetPath()); err != nil {
if err := s.model.DeleteGroup(groupPath); err != nil {
switch {
case errors.Is(err, vault.ErrEntryNotFound):
return nil, status.Error(codes.NotFound, err.Error())
@@ -537,22 +548,22 @@ func (s *Server) UpsertEntry(ctx context.Context, req *keepassgov1.UpsertEntryRe
return nil, status.Error(codes.InvalidArgument, "missing entry")
}
entry := entryFromProto(req.GetEntry())
if _, err := s.authorizeEntryRequest(ctx, apitokens.OperationMutateEntry, entry); err != nil {
model, locked := s.snapshotModel()
if locked {
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
}
entry := entryFromProtoWithModel(visibleModel(model), req.GetEntry())
if _, err := s.authorizeUpsertEntryRequest(ctx, entry); err != nil {
return nil, err
}
s.mu.Lock()
if s.locked {
s.mu.Unlock()
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
}
s.model.UpsertEntry(entry)
s.dirty = true
s.syncMutationLocked()
s.mu.Unlock()
return &keepassgov1.UpsertEntryResponse{Entry: entryToProto(entry)}, nil
return &keepassgov1.UpsertEntryResponse{Entry: entryToProtoWithModel(visibleModel(model), entry)}, nil
}
func (s *Server) DeleteEntry(ctx context.Context, req *keepassgov1.DeleteEntryRequest) (*keepassgov1.DeleteEntryResponse, error) {
@@ -612,7 +623,7 @@ func (s *Server) RestoreEntry(ctx context.Context, req *keepassgov1.RestoreEntry
s.dirty = true
s.syncMutationLocked()
return &keepassgov1.RestoreEntryResponse{Entry: entryToProto(restored)}, nil
return &keepassgov1.RestoreEntryResponse{Entry: entryToProtoWithModel(visibleModel(model), restored)}, nil
}
func (s *Server) ListEntryHistory(ctx context.Context, req *keepassgov1.ListEntryHistoryRequest) (*keepassgov1.ListEntryHistoryResponse, error) {
@@ -633,7 +644,7 @@ func (s *Server) ListEntryHistory(ctx context.Context, req *keepassgov1.ListEntr
Entries: make([]*keepassgov1.Entry, 0, len(entry.History)),
}
for _, historical := range entry.History {
resp.Entries = append(resp.Entries, entryToProto(historical))
resp.Entries = append(resp.Entries, entryToProtoWithModel(visibleModel(model), historical))
}
return resp, nil
}
@@ -666,7 +677,7 @@ func (s *Server) RestoreEntryHistory(ctx context.Context, req *keepassgov1.Resto
}
s.dirty = true
s.syncMutationLocked()
return &keepassgov1.RestoreEntryHistoryResponse{Entry: entryToProto(entry)}, nil
return &keepassgov1.RestoreEntryHistoryResponse{Entry: entryToProtoWithModel(visibleModel(s.model), entry)}, nil
}
func (s *Server) ListTemplates(ctx context.Context, _ *keepassgov1.ListTemplatesRequest) (*keepassgov1.ListTemplatesResponse, error) {
@@ -684,7 +695,7 @@ func (s *Server) ListTemplates(ctx context.Context, _ *keepassgov1.ListTemplates
Templates: make([]*keepassgov1.Entry, 0, len(s.model.Templates)),
}
for _, template := range s.model.Templates {
resp.Templates = append(resp.Templates, entryToProto(template))
resp.Templates = append(resp.Templates, entryToProtoWithModel(visibleModel(s.model), template))
}
return resp, nil
@@ -705,12 +716,12 @@ func (s *Server) UpsertTemplate(ctx context.Context, req *keepassgov1.UpsertTemp
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
}
entry := entryFromProto(req.GetTemplate())
entry := entryFromProtoWithModel(visibleModel(s.model), req.GetTemplate())
s.model.UpsertTemplate(entry)
s.dirty = true
s.syncMutationLocked()
return &keepassgov1.UpsertTemplateResponse{Template: entryToProto(entry)}, nil
return &keepassgov1.UpsertTemplateResponse{Template: entryToProtoWithModel(visibleModel(s.model), entry)}, nil
}
func (s *Server) DeleteTemplate(ctx context.Context, req *keepassgov1.DeleteTemplateRequest) (*keepassgov1.DeleteTemplateResponse, error) {
@@ -743,7 +754,8 @@ func (s *Server) InstantiateTemplate(ctx context.Context, req *keepassgov1.Insta
if _, err := s.authorizeTemplateRequest(ctx, apitokens.OperationListTemplates, req.GetTemplateId()); err != nil {
return nil, err
}
if _, err := s.authorizePathRequest(ctx, apitokens.OperationMutateEntry, req.GetOverrides().GetPath()); err != nil {
overridePath := expandClientPath(visibleModel(s.model), req.GetOverrides().GetPath())
if _, err := s.authorizePathRequest(ctx, apitokens.OperationMutateEntry, overridePath); err != nil {
return nil, err
}
@@ -754,7 +766,8 @@ func (s *Server) InstantiateTemplate(ctx context.Context, req *keepassgov1.Insta
return nil, status.Error(codes.FailedPrecondition, "vault is locked")
}
entry, err := s.model.InstantiateTemplate(req.GetTemplateId(), entryFromProto(req.GetOverrides()))
overrides := entryFromProtoWithModel(visibleModel(s.model), req.GetOverrides())
entry, err := s.model.InstantiateTemplate(req.GetTemplateId(), overrides)
if err != nil {
if errors.Is(err, vault.ErrEntryNotFound) {
return nil, status.Error(codes.NotFound, err.Error())
@@ -764,7 +777,7 @@ func (s *Server) InstantiateTemplate(ctx context.Context, req *keepassgov1.Insta
s.dirty = true
s.syncMutationLocked()
return &keepassgov1.InstantiateTemplateResponse{Entry: entryToProto(entry)}, nil
return &keepassgov1.InstantiateTemplateResponse{Entry: entryToProtoWithModel(visibleModel(s.model), entry)}, nil
}
func (s *Server) ListAttachments(ctx context.Context, req *keepassgov1.ListAttachmentsRequest) (*keepassgov1.ListAttachmentsResponse, error) {
@@ -932,7 +945,7 @@ func (s *Server) GeneratePassword(ctx context.Context, req *keepassgov1.Generate
return &keepassgov1.GeneratePasswordResponse{Password: password}, nil
}
func entryToProto(entry vault.Entry) *keepassgov1.Entry {
func entryToProtoWithModel(model vault.Model, entry vault.Entry) *keepassgov1.Entry {
return &keepassgov1.Entry{
Id: entry.ID,
Title: entry.Title,
@@ -941,12 +954,12 @@ func entryToProto(entry vault.Entry) *keepassgov1.Entry {
Url: entry.URL,
Notes: entry.Notes,
Tags: append([]string(nil), entry.Tags...),
Path: append([]string(nil), entry.Path...),
Path: collapseInternalPath(model, entry.Path),
Fields: maps.Clone(entry.Fields),
}
}
func entryFromProto(entry *keepassgov1.Entry) vault.Entry {
func entryFromProtoWithModel(model vault.Model, entry *keepassgov1.Entry) vault.Entry {
return vault.Entry{
ID: entry.GetId(),
Title: entry.GetTitle(),
@@ -955,11 +968,44 @@ func entryFromProto(entry *keepassgov1.Entry) vault.Entry {
URL: entry.GetUrl(),
Notes: entry.GetNotes(),
Tags: append([]string(nil), entry.GetTags()...),
Path: append([]string(nil), entry.GetPath()...),
Path: expandClientPath(model, entry.GetPath()),
Fields: maps.Clone(entry.GetFields()),
}
}
func hiddenVaultRoot(model vault.Model) string {
if len(model.EntriesInPath(nil)) != 0 {
return ""
}
groups := model.ChildGroups(nil)
if len(groups) != 1 {
return ""
}
return groups[0]
}
func expandClientPath(model vault.Model, path []string) []string {
root := hiddenVaultRoot(model)
if root == "" {
return append([]string(nil), path...)
}
if len(path) == 0 {
return []string{root}
}
if path[0] == root {
return append([]string(nil), path...)
}
return append([]string{root}, path...)
}
func collapseInternalPath(model vault.Model, path []string) []string {
root := hiddenVaultRoot(model)
if root == "" || len(path) == 0 || path[0] != root {
return append([]string(nil), path...)
}
return append([]string(nil), path[1:]...)
}
func findEntryByID(model vault.Model, id string) (vault.Entry, error) {
for _, entry := range model.Entries {
if entry.ID == id {
@@ -1103,6 +1149,44 @@ func (s *Server) authorizeEntryRequest(ctx context.Context, op apitokens.Operati
return s.authorizeResourceRequest(ctx, token, op, apitokens.Resource{Kind: apitokens.ResourceEntry, EntryID: entry.ID, Path: entry.Path})
}
func (s *Server) authorizeUpsertEntryRequest(ctx context.Context, entry vault.Entry) (apitokens.Token, error) {
token, err := s.authenticateRequest(ctx)
if err != nil {
return apitokens.Token{}, err
}
model, locked := s.snapshotModel()
if locked {
return apitokens.Token{}, status.Error(codes.FailedPrecondition, "vault is locked")
}
existing, err := findEntryByID(model, entry.ID)
switch {
case err == nil:
if _, err := s.authorizeResourceRequest(ctx, token, apitokens.OperationMutateEntry, apitokens.Resource{
Kind: apitokens.ResourceEntry,
EntryID: existing.ID,
Path: existing.Path,
}); err != nil {
return apitokens.Token{}, err
}
if !slices.Equal(existing.Path, entry.Path) {
if _, err := s.authorizeResourceRequest(ctx, token, apitokens.OperationMutateEntry, apitokens.Resource{
Kind: apitokens.ResourceGroup,
Path: entry.Path,
}); err != nil {
return apitokens.Token{}, err
}
}
return token, nil
case errors.Is(err, vault.ErrEntryNotFound):
return s.authorizeResourceRequest(ctx, token, apitokens.OperationMutateEntry, apitokens.Resource{
Kind: apitokens.ResourceGroup,
Path: entry.Path,
})
default:
return apitokens.Token{}, status.Errorf(codes.Internal, "lookup existing entry: %v", err)
}
}
func (s *Server) authorizeTemplateRequest(ctx context.Context, op apitokens.Operation, templateID string) (apitokens.Token, error) {
token, err := s.authenticateRequest(ctx)
if err != nil {