Permission Ceiling for Agents (#12932) (#13041)

This commit is contained in:
Vault Automation 2026-03-16 12:36:27 -04:00 committed by GitHub
parent 3b25846e75
commit cdc616e098
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 172 additions and 3 deletions

View File

@ -350,11 +350,17 @@ func (c *Core) fetchACLTokenEntryAndEntity(ctx context.Context, req *logical.Req
if secondEntity != nil {
c.logger.Debug("building separate ACL for second entity", "entity_id", secondEntity.ID)
secondEntityPolicyNames = make(map[string][]string)
_, secondEntityIdentityPolicies, err := c.fetchEntityAndDerivedPolicies(ctx, tokenNS, secondEntity.ID, false)
secondEntityIdentityPolicies, err := c.fetchCeilingPolicies(ctx, secondEntity)
if err != nil {
return nil, nil, nil, nil, err
}
allowOnly, err := c.allPoliciesAllowOnly(ctx, secondEntityIdentityPolicies)
if err != nil {
c.logger.Error("failed to fetch second entity policies", "error", err)
return nil, nil, nil, nil, ErrInternalError
}
if !allowOnly {
return nil, nil, nil, nil, logical.ErrPermissionDenied
}
// Store second entity policies separately - do NOT merge with primary entity's policies
for nsID, nsPolicies := range secondEntityIdentityPolicies {
secondEntityPolicyNames[nsID] = policyutil.SanitizePolicies(nsPolicies, false)
@ -401,7 +407,7 @@ func (c *Core) fetchACLTokenEntryAndEntity(ctx context.Context, req *logical.Req
return nil, nil, nil, nil, ErrInternalError
}
if secondEntity != nil && len(secondEntityPolicyNames) > 0 {
if secondEntity != nil {
newAcl, err := c.performSecondaryEntityTokenChecks(tokenCtx, acl, secondEntity, secondEntityPolicyNames)
if err != nil {
return nil, nil, nil, nil, err
@ -2980,3 +2986,69 @@ func (c *Core) checkSSCTokenInternal(ctx context.Context, token string, isPerfSt
// status code.
return "", logical.ErrMissingRequiredState
}
// allPoliciesAllowOnly is a helper function that checks if all policies in
// a given set have only "allow" capabilities, and not "deny" or "sudo".
//
// Example of allow-only policy:
//
// path "secret/data/team/public/*" {
// capabilities = ["read"]
// }
//
// Example of a policy that is not allow-only:
//
// path "secret/data/team/*" {
// capabilities = ["read"]
// }
//
// path "secret/data/team/private/*" {
// capabilities = ["deny"]
// }
func (c *Core) allPoliciesAllowOnly(ctx context.Context, policyNamesByNamespace map[string][]string) (bool, error) {
for nsID, policyNames := range policyNamesByNamespace {
policyNS, err := NamespaceByID(ctx, nsID, c)
if err != nil {
return false, err
}
if policyNS == nil {
return false, namespace.ErrNoNamespace
}
policyCtx := namespace.ContextWithNamespace(ctx, policyNS)
for _, policyName := range policyNames {
policy, err := c.policyStore.GetPolicy(policyCtx, policyName, PolicyTypeACL)
if err != nil {
return false, err
}
if policy == nil {
return false, fmt.Errorf("policy %q not found in namespace %q", policyName, policyNS.Path)
}
if !policyIsAllowOnly(policy) {
return false, nil
}
}
}
return true, nil
}
// policyIsAllowOnly is a helper function that checks if a policy has only "allow" capabilities, and not "deny" or "sudo".
func policyIsAllowOnly(policy *Policy) bool {
if policy == nil || policy.Name == "root" {
return false
}
for _, pathRules := range policy.Paths {
if pathRules == nil || pathRules.Permissions == nil {
continue
}
capabilities := pathRules.Permissions.CapabilitiesBitmap
if capabilities&DenyCapabilityInt != 0 || capabilities&SudoCapabilityInt != 0 {
return false
}
}
return true
}

View File

@ -40,3 +40,7 @@ func getEnterpriseTokenAuthorizationDetails(_ map[string]interface{}) []logical.
func (c *Core) performSecondaryEntityTokenChecks(_ context.Context, _ *ACL, _ *identity.Entity, _ map[string][]string) (*ACL, error) {
return nil, errors.New("not implemented")
}
func (c *Core) fetchCeilingPolicies(ctx context.Context, entity *identity.Entity) (map[string][]string, error) {
return nil, errors.New("not implemented")
}

View File

@ -643,6 +643,99 @@ func TestRequestHandling_fetchACLTokenEntryAndEntity_NilRequest(t *testing.T) {
require.Equal(t, ErrInternalError, err)
}
// Test_allPoliciesAllowOnly tests a helper function that checks if all policies in
// a given set have only "allow" capabilities, and not "deny" or "sudo"
func Test_allPoliciesAllowOnly(t *testing.T) {
t.Parallel()
c, _, _ := TestCoreUnsealed(t)
ctx := namespace.RootContext(context.Background())
allowPolicy, err := ParseACLPolicy(namespace.RootNamespace, `
path "secret/data/*" {
capabilities = ["read", "list"]
}
`)
require.NoError(t, err)
allowPolicy.Name = "allow-only"
require.NoError(t, c.policyStore.SetPolicy(ctx, allowPolicy))
denyPolicy, err := ParseACLPolicy(namespace.RootNamespace, `
path "secret/data/*" {
capabilities = ["deny"]
}
`)
require.NoError(t, err)
denyPolicy.Name = "deny-policy"
require.NoError(t, c.policyStore.SetPolicy(ctx, denyPolicy))
sudoPolicy, err := ParseACLPolicy(namespace.RootNamespace, `
path "secret/data/*" {
capabilities = ["read", "sudo"]
}
`)
require.NoError(t, err)
sudoPolicy.Name = "sudo-policy"
require.NoError(t, c.policyStore.SetPolicy(ctx, sudoPolicy))
tests := map[string]struct {
policyNamesByNamespace map[string][]string
expected bool
wantErr string
}{
"all allow only": {
policyNamesByNamespace: map[string][]string{
namespace.RootNamespaceID: {"allow-only"},
},
expected: true,
},
"deny policy": {
policyNamesByNamespace: map[string][]string{
namespace.RootNamespaceID: {"deny-policy"},
},
expected: false,
},
"sudo policy": {
policyNamesByNamespace: map[string][]string{
namespace.RootNamespaceID: {"sudo-policy"},
},
expected: false,
},
"root policy": {
policyNamesByNamespace: map[string][]string{
namespace.RootNamespaceID: {"root"},
},
expected: false,
},
"missing policy": {
policyNamesByNamespace: map[string][]string{
namespace.RootNamespaceID: {"missing-policy"},
},
expected: false,
wantErr: "policy \"missing-policy\" not found",
},
"missing namespace": {
policyNamesByNamespace: map[string][]string{
"missing-namespace": {"allow-only"},
},
expected: false,
wantErr: namespace.ErrNoNamespace.Error(),
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
actual, err := c.allPoliciesAllowOnly(ctx, tc.policyNamesByNamespace)
if tc.wantErr != "" {
require.ErrorContains(t, err, tc.wantErr)
} else {
require.NoError(t, err)
}
require.Equal(t, tc.expected, actual)
})
}
}
// TestAuth_AuthorizationDetails_CopiedFromRequest verifies that logical.Auth.AuthorizationDetails
// matches the authorization details already carried on the request.
func TestAuth_AuthorizationDetails_CopiedFromRequest(t *testing.T) {