From ca48dcda5ee362ea94a09faee2ee13d894cc76ae Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Mon, 29 Sep 2025 14:19:56 -0400 Subject: [PATCH] Bug fix: resultant-acl + wildcard parsing (#9449) (#9584) * an attempt to add parsing of wilcards * test coverage and changelog * fix policy path * fix failing tesT * fix comments * add in go doc comment * update second internal ui resultant acl test Co-authored-by: Angel Garbarino Co-authored-by: Tony Wittinger --- changelog/_9449.txt | 3 ++ vault/logical_system.go | 10 ++-- vault/logical_system_integ_test.go | 3 ++ vault/logical_system_test.go | 85 ++++++++++++++++++++++++++++++ 4 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 changelog/_9449.txt diff --git a/changelog/_9449.txt b/changelog/_9449.txt new file mode 100644 index 0000000000..b53f7ec3b5 --- /dev/null +++ b/changelog/_9449.txt @@ -0,0 +1,3 @@ +```release-note:bug +core: resultant-acl now merges segment-wildcard (`+`) paths with existing prefix rules in `glob_paths`, so clients receive a complete view of glob-style permissions. This unblocks UI sidebar navigation checks and namespace access banners. +``` diff --git a/vault/logical_system.go b/vault/logical_system.go index 4c0f2b5b03..1cec418e17 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -5337,16 +5337,18 @@ func (b *SystemBackend) pathInternalUIResultantACL(ctx context.Context, req *log walkFn(exact, s, v) return false } + acl.exactRules.Walk(exactWalkFn) + resp.Data["exact_paths"] = exact + // Combine glob (prefix) and segment wildcard (+) paths into glob_paths globWalkFn := func(s string, v interface{}) bool { walkFn(glob, s, v) return false } - - acl.exactRules.Walk(exactWalkFn) acl.prefixRules.Walk(globWalkFn) - - resp.Data["exact_paths"] = exact + for s, v := range acl.segmentWildcardPaths { + walkFn(glob, s, v) + } resp.Data["glob_paths"] = glob return resp, nil diff --git a/vault/logical_system_integ_test.go b/vault/logical_system_integ_test.go index fc572d21c7..3cd1f007fc 100644 --- a/vault/logical_system_integ_test.go +++ b/vault/logical_system_integ_test.go @@ -138,6 +138,9 @@ func TestSystemBackend_InternalUIResultantACL(t *testing.T) { "update", }, }, + "identity/oidc/provider/+/authorize": map[string]interface{}{ + "capabilities": []interface{}{"read", "update"}, + }, }, "root": false, "chroot_namespace": "", diff --git a/vault/logical_system_test.go b/vault/logical_system_test.go index c10bae903a..b393dac877 100644 --- a/vault/logical_system_test.go +++ b/vault/logical_system_test.go @@ -4738,6 +4738,91 @@ func TestSystemBackend_InternalUIMount(t *testing.T) { } } +// TestSystemBackend_InternalUIResultantACL verifies that segment wildcard and prefix glob ACLs are emitted correctly in the internal UI resultant-acl endpoint. +func TestSystemBackend_InternalUIResultantACL(t *testing.T) { + ctx := namespace.RootContext(nil) + core, b, rootToken := testCoreSystemBackend(t) + + // Define a policy that includes a segment wildcard and a prefix glob. + rules := ` +name = "ui-res-acl-test" +path "+/auth/*" { + capabilities = ["read"] +} +path "sys/*" { + capabilities = ["update"] +}` + + pol, err := ParseACLPolicy(namespace.RootNamespace, rules) + require.NoError(t, err) + require.NoError(t, core.policyStore.SetPolicy(ctx, pol)) + + // Create a non-root token that has this policy attached + testMakeServiceTokenViaBackend(t, core.tokenStore, rootToken, "tokenid", "", []string{"ui-res-acl-test"}) + + // Call the endpoint as the non-root token; this endpoint evaluates the caller. + req := logical.TestRequest(t, logical.ReadOperation, "internal/ui/resultant-acl") + req.ClientToken = "tokenid" + + resp, err := b.HandleRequest(ctx, req) + require.NoError(t, err) + require.NotNil(t, resp) + require.NotNil(t, resp.Data) + + // Validate response shape. + schema.ValidateResponse( + t, + schema.GetResponseSchema(t, b.(*SystemBackend).Route(req.Path), req.Operation), + resp, + true, + ) + + // Basic flags we expect back for a non-root token we’re inspecting. + if v, ok := resp.Data["root"].(bool); ok { + require.False(t, v, "expected non-root token") + } + + // Extract glob paths and ensure both entries are present with expected caps. + globPaths, ok := resp.Data["glob_paths"].(map[string]interface{}) + require.True(t, ok, "glob_paths missing or wrong type") + + getCaps := func(m map[string]interface{}) []string { + raw := m["capabilities"] + switch a := raw.(type) { + case []string: + return a + case []interface{}: + out := make([]string, 0, len(a)) + for _, x := range a { + if s, ok := x.(string); ok { + out = append(out, s) + } + } + return out + default: + return nil + } + } + + // 1) segment wildcard preserved + segRaw, ok := globPaths["+/auth/*"] + require.True(t, ok, "segment wildcard path not found") + seg, ok := segRaw.(map[string]interface{}) + require.True(t, ok, "segment wildcard value wrong type") + require.Equal(t, []string{"read"}, getCaps(seg)) + + // 2) prefix glob preserved (the backend may normalize to "sys/*" or "sys/") + prefixKey := "sys/*" + if _, ok := globPaths["sys/"]; ok { + prefixKey = "sys/" + } + prefRaw, ok := globPaths[prefixKey] + require.True(t, ok, "prefix glob path not found") + pref, ok := prefRaw.(map[string]interface{}) + require.True(t, ok, "prefix glob value wrong type") + require.Contains(t, getCaps(pref), "update") +} + func TestSystemBackend_OpenAPI(t *testing.T) { coreConfig := &CoreConfig{ LogicalBackends: map[string]logical.Factory{