Hide physical keepass paths in token and approval UX

This commit is contained in:
Joe Julian
2026-04-13 07:18:33 -07:00
parent 9882d3fc04
commit 6790399e24
7 changed files with 140 additions and 14 deletions
+25 -6
View File
@@ -1221,6 +1221,7 @@ func (s *Server) authorizeTemplateRequest(ctx context.Context, op apitokens.Oper
func (s *Server) authorizeResourceRequest(ctx context.Context, token apitokens.Token, op apitokens.Operation, resource apitokens.Resource) (apitokens.Token, error) {
model, _ := s.snapshotModel()
displayResource := displayAuthorizationResource(resource)
switch evaluateAuthorization(model, token, op, resource) {
case apitokens.DecisionAllow:
return token, nil
@@ -1233,9 +1234,9 @@ func (s *Server) authorizeResourceRequest(ctx context.Context, token apitokens.T
TokenName: token.Name,
ClientName: token.ClientName,
Operation: op,
Resource: resource,
Resource: displayResource,
})
result, err := s.approvals.Request(ctx, token, op, resource)
result, err := s.approvals.Request(ctx, token, op, displayResource)
if result.Rule != nil {
if persistErr := s.persistApprovalRule(token.ID, *result.Rule); persistErr != nil {
return apitokens.Token{}, status.Errorf(codes.Internal, "persist approval decision: %v", persistErr)
@@ -1249,7 +1250,7 @@ func (s *Server) authorizeResourceRequest(ctx context.Context, token apitokens.T
TokenName: token.Name,
ClientName: token.ClientName,
Operation: op,
Resource: resource,
Resource: displayResource,
})
return token, nil
case errors.Is(err, apiapproval.ErrRequestDenied):
@@ -1259,7 +1260,7 @@ func (s *Server) authorizeResourceRequest(ctx context.Context, token apitokens.T
TokenName: token.Name,
ClientName: token.ClientName,
Operation: op,
Resource: resource,
Resource: displayResource,
})
return apitokens.Token{}, status.Error(codes.PermissionDenied, "access denied by user approval")
case errors.Is(err, apiapproval.ErrRequestCanceled):
@@ -1269,7 +1270,7 @@ func (s *Server) authorizeResourceRequest(ctx context.Context, token apitokens.T
TokenName: token.Name,
ClientName: token.ClientName,
Operation: op,
Resource: resource,
Resource: displayResource,
})
return apitokens.Token{}, status.Error(codes.Unauthenticated, "authorization request canceled")
case errors.Is(err, apiapproval.ErrRequestTimedOut):
@@ -1279,7 +1280,7 @@ func (s *Server) authorizeResourceRequest(ctx context.Context, token apitokens.T
TokenName: token.Name,
ClientName: token.ClientName,
Operation: op,
Resource: resource,
Resource: displayResource,
})
return apitokens.Token{}, status.Error(codes.DeadlineExceeded, "authorization request timed out")
case errors.Is(err, context.Canceled):
@@ -1355,6 +1356,11 @@ func canonicalizeAuthorizationResource(model vault.Model, resource apitokens.Res
return resource
}
func displayAuthorizationResource(resource apitokens.Resource) apitokens.Resource {
resource.Path = displayAuthorizationPath(resource.Path)
return resource
}
func canonicalAuthorizationPath(model vault.Model, path []string) []string {
if len(path) == 0 {
return nil
@@ -1374,6 +1380,19 @@ func canonicalAuthorizationPath(model vault.Model, path []string) []string {
return vaultview.VaultRoot(model).ToPhysicalPath(path)
}
func displayAuthorizationPath(path []string) []string {
if len(path) == 0 {
return nil
}
if path[0] == vaultview.KeepassRoot {
return append([]string{"Root"}, append([]string(nil), path[1:]...)...)
}
if path[0] == "Root" {
return append([]string(nil), path...)
}
return append([]string(nil), path...)
}
func copyOperation(target string) apitokens.Operation {
switch clipboard.Target(target) {
case clipboard.TargetUsername:
+52
View File
@@ -396,6 +396,58 @@ func TestVaultServiceFindsBrowserLoginsRechecksChildPoliciesAfterPrompt(t *testi
}
}
func TestVaultServiceApprovalRequestsUseLogicalRootPathForPhysicalVault(t *testing.T) {
t.Parallel()
model := vault.Model{
Entries: []vault.Entry{
{
ID: "codex-nextcloud",
Title: "Nextcloud (codex)",
Username: "jjulian",
Password: "secret-1",
URL: "https://nextcloud.example.invalid",
Path: []string{"keepass", "Joe", "codex"},
},
testAPITokenEntry(t),
},
Groups: [][]string{
{"keepass"},
{"keepass", "Joe"},
{"keepass", "Joe", "codex"},
},
}
client, _, service, cleanup := newTestHarnessForModel(t, model)
defer cleanup()
service.approvals = apiapproval.NewBroker(time.Minute)
respCh := make(chan *keepassgov1.ListEntriesResponse, 1)
errCh := make(chan error, 1)
go func() {
resp, err := client.ListEntries(tokenContext(defaultTestTokenSecret), &keepassgov1.ListEntriesRequest{
Path: []string{"Joe", "codex"},
})
respCh <- resp
errCh <- err
}()
pending := waitForServerPendingApproval(t, service, 1)[0]
if got := pending.Resource.Path; !slices.Equal(got, []string{"Root", "Joe", "codex"}) {
t.Fatalf("pending.Resource.Path = %v, want [Root Joe codex]", got)
}
if _, _, err := service.ResolveApproval(pending.ID, apiapproval.OutcomeAllowOnce); err != nil {
t.Fatalf("ResolveApproval(allow once) error = %v", err)
}
if err := <-errCh; err != nil {
t.Fatalf("ListEntries() error = %v", err)
}
resp := <-respCh
if len(resp.GetEntries()) != 1 || resp.GetEntries()[0].GetId() != "codex-nextcloud" {
t.Fatalf("ListEntries().Entries = %#v, want codex-nextcloud after approval", resp.GetEntries())
}
}
func TestVaultServiceDoesNotMatchSpecificBrowserEntryToParentDomain(t *testing.T) {
t.Parallel()