diff --git a/changelog/22474.txt b/changelog/22474.txt new file mode 100644 index 0000000000..9f18050a41 --- /dev/null +++ b/changelog/22474.txt @@ -0,0 +1,3 @@ +```release-note:feature +Add subscribe capability and subscribe_event_types to policies for events. +``` diff --git a/vault/acl.go b/vault/acl.go index d223badf7d..7706b62ac1 100644 --- a/vault/acl.go +++ b/vault/acl.go @@ -322,6 +322,9 @@ func (a *ACL) Capabilities(ctx context.Context, path string) (pathCapabilities [ if capabilities&PatchCapabilityInt > 0 { pathCapabilities = append(pathCapabilities, PatchCapability) } + if capabilities&SubscribeCapabilityInt > 0 { + pathCapabilities = append(pathCapabilities, SubscribeCapability) + } // If "deny" is explicitly set or if the path has no capabilities at all, // set the path capabilities to "deny" diff --git a/vault/logical_system.go b/vault/logical_system.go index f81ed292bc..cf8ab6c7dd 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -4164,7 +4164,8 @@ func hasMountAccess(ctx context.Context, acl *ACL, path string) bool { perms.CapabilitiesBitmap&ReadCapabilityInt > 0, perms.CapabilitiesBitmap&SudoCapabilityInt > 0, perms.CapabilitiesBitmap&UpdateCapabilityInt > 0, - perms.CapabilitiesBitmap&PatchCapabilityInt > 0: + perms.CapabilitiesBitmap&PatchCapabilityInt > 0, + perms.CapabilitiesBitmap&SubscribeCapabilityInt > 0: aclCapabilitiesGiven = true @@ -4484,6 +4485,9 @@ func (b *SystemBackend) pathInternalUIResultantACL(ctx context.Context, req *log if perms.CapabilitiesBitmap&PatchCapabilityInt > 0 { capabilities = append(capabilities, PatchCapability) } + if perms.CapabilitiesBitmap&SubscribeCapabilityInt > 0 { + capabilities = append(capabilities, SubscribeCapability) + } // If "deny" is explicitly set or if the path has no capabilities at all, // set the path capabilities to "deny" diff --git a/vault/policy.go b/vault/policy.go index 173b01eaa5..3f34d45311 100644 --- a/vault/policy.go +++ b/vault/policy.go @@ -9,7 +9,7 @@ import ( "strings" "time" - multierror "github.com/hashicorp/go-multierror" + "github.com/hashicorp/go-multierror" "github.com/hashicorp/go-secure-stdlib/parseutil" "github.com/hashicorp/hcl" "github.com/hashicorp/hcl/hcl/ast" @@ -22,15 +22,16 @@ import ( ) const ( - DenyCapability = "deny" - CreateCapability = "create" - ReadCapability = "read" - UpdateCapability = "update" - DeleteCapability = "delete" - ListCapability = "list" - SudoCapability = "sudo" - RootCapability = "root" - PatchCapability = "patch" + DenyCapability = "deny" + CreateCapability = "create" + ReadCapability = "read" + UpdateCapability = "update" + DeleteCapability = "delete" + ListCapability = "list" + SudoCapability = "sudo" + RootCapability = "root" + PatchCapability = "patch" + SubscribeCapability = "subscribe" // Backwards compatibility OldDenyPathPolicy = "deny" @@ -48,6 +49,7 @@ const ( ListCapabilityInt SudoCapabilityInt PatchCapabilityInt + SubscribeCapabilityInt ) // Error constants for testing @@ -82,14 +84,15 @@ func (p PolicyType) String() string { } var cap2Int = map[string]uint32{ - DenyCapability: DenyCapabilityInt, - CreateCapability: CreateCapabilityInt, - ReadCapability: ReadCapabilityInt, - UpdateCapability: UpdateCapabilityInt, - DeleteCapability: DeleteCapabilityInt, - ListCapability: ListCapabilityInt, - SudoCapability: SudoCapabilityInt, - PatchCapability: PatchCapabilityInt, + DenyCapability: DenyCapabilityInt, + CreateCapability: CreateCapabilityInt, + ReadCapability: ReadCapabilityInt, + UpdateCapability: UpdateCapabilityInt, + DeleteCapability: DeleteCapabilityInt, + ListCapability: ListCapabilityInt, + SudoCapability: SudoCapabilityInt, + PatchCapability: PatchCapabilityInt, + SubscribeCapability: SubscribeCapabilityInt, } type egpPath struct { @@ -133,13 +136,14 @@ type PathRules struct { // These keys are used at the top level to make the HCL nicer; we store in // the ACLPermissions object though - MinWrappingTTLHCL interface{} `hcl:"min_wrapping_ttl"` - MaxWrappingTTLHCL interface{} `hcl:"max_wrapping_ttl"` - AllowedParametersHCL map[string][]interface{} `hcl:"allowed_parameters"` - DeniedParametersHCL map[string][]interface{} `hcl:"denied_parameters"` - RequiredParametersHCL []string `hcl:"required_parameters"` - MFAMethodsHCL []string `hcl:"mfa_methods"` - ControlGroupHCL *ControlGroupHCL `hcl:"control_group"` + MinWrappingTTLHCL interface{} `hcl:"min_wrapping_ttl"` + MaxWrappingTTLHCL interface{} `hcl:"max_wrapping_ttl"` + AllowedParametersHCL map[string][]interface{} `hcl:"allowed_parameters"` + DeniedParametersHCL map[string][]interface{} `hcl:"denied_parameters"` + RequiredParametersHCL []string `hcl:"required_parameters"` + MFAMethodsHCL []string `hcl:"mfa_methods"` + ControlGroupHCL *ControlGroupHCL `hcl:"control_group"` + SubscribeEventTypesHCL []string `hcl:"subscribe_event_types"` } type ControlGroupHCL struct { @@ -185,14 +189,16 @@ type ACLPermissions struct { MFAMethods []string ControlGroup *ControlGroup GrantingPoliciesMap map[uint32][]logical.PolicyInfo + SubscribeEventTypes []string } func (p *ACLPermissions) Clone() (*ACLPermissions, error) { ret := &ACLPermissions{ - CapabilitiesBitmap: p.CapabilitiesBitmap, - MinWrappingTTL: p.MinWrappingTTL, - MaxWrappingTTL: p.MaxWrappingTTL, - RequiredParameters: p.RequiredParameters[:], + CapabilitiesBitmap: p.CapabilitiesBitmap, + MinWrappingTTL: p.MinWrappingTTL, + MaxWrappingTTL: p.MaxWrappingTTL, + RequiredParameters: p.RequiredParameters[:], + SubscribeEventTypes: p.SubscribeEventTypes[:], } switch { @@ -377,6 +383,7 @@ func parsePaths(result *Policy, list *ast.ObjectList, performTemplating bool, en "max_wrapping_ttl", "mfa_methods", "control_group", + "subscribe_event_types", } if err := hclutil.CheckHCLKeys(item.Val, valid); err != nil { return multierror.Prefix(err, fmt.Sprintf("path %q:", key)) @@ -444,7 +451,7 @@ func parsePaths(result *Policy, list *ast.ObjectList, performTemplating bool, en pc.Capabilities = []string{DenyCapability} pc.Permissions.CapabilitiesBitmap = DenyCapabilityInt goto PathFinished - case CreateCapability, ReadCapability, UpdateCapability, DeleteCapability, ListCapability, SudoCapability, PatchCapability: + case CreateCapability, ReadCapability, UpdateCapability, DeleteCapability, ListCapability, SudoCapability, PatchCapability, SubscribeCapability: pc.Permissions.CapabilitiesBitmap |= cap2Int[cap] default: return fmt.Errorf("path %q: invalid capability %q", key, cap) @@ -542,6 +549,9 @@ func parsePaths(result *Policy, list *ast.ObjectList, performTemplating bool, en if len(pc.RequiredParametersHCL) > 0 { pc.Permissions.RequiredParameters = pc.RequiredParametersHCL[:] } + if len(pc.SubscribeEventTypesHCL) > 0 { + pc.Permissions.SubscribeEventTypes = pc.SubscribeEventTypesHCL[:] + } PathFinished: paths = append(paths, &pc) diff --git a/vault/policy_test.go b/vault/policy_test.go index 97a3a1b3ad..862b767c82 100644 --- a/vault/policy_test.go +++ b/vault/policy_test.go @@ -497,3 +497,32 @@ path "foo/+*" { t.Errorf("bad error: %s", err) } } + +func TestPolicy_Subscribe(t *testing.T) { + policy, err := ParseACLPolicy(namespace.RootNamespace, strings.TrimSpace(` + path "secret/*" { + capabilities = ["subscribe", "create", "read"] + } + `)) + if err != nil { + t.Fatalf("Policies should be able to use 'subscribe' capability") + } + if policy.Paths[0].Permissions.CapabilitiesBitmap&SubscribeCapabilityInt == 0 { + t.Fatalf("Subscribe capability should be present in capabilities bitmap") + } +} + +func TestPolicy_Subscribe_EventTypes(t *testing.T) { + policy, err := ParseACLPolicy(namespace.RootNamespace, strings.TrimSpace(` + path "secret/*" { + capabilities = ["subscribe"] + subscribe_event_types = ["kv-v2/data-write", "kv-v1/*"] + } + `)) + if err != nil { + t.Fatalf("Should be able to subscribe to a list of event types: %v", err) + } + if strings.Join(policy.Paths[0].Permissions.SubscribeEventTypes, ",") != "kv-v2/data-write,kv-v1/*" { + t.Fatalf("ACLPermission should reflect subscribe event types, but got %v", policy.Paths[0].Permissions.SubscribeEventTypes) + } +} diff --git a/website/content/docs/concepts/policies.mdx b/website/content/docs/concepts/policies.mdx index d4e4b03283..ba6dcc24c3 100644 --- a/website/content/docs/concepts/policies.mdx +++ b/website/content/docs/concepts/policies.mdx @@ -248,6 +248,9 @@ HTTP verbs. - `deny` - Disallows access. This always takes precedence regardless of any other defined capabilities, including `sudo`. +- `subscribe` - Allows subscribing to [events](/vault/docs/concepts/events) + for the given path. + ~> **Note:** Capabilities usually map to the HTTP verb, and not the underlying action taken. This can be a common source of confusion. Generating database credentials _creates_ database credentials, but the HTTP request is a GET which