oss part of control groups upgrade (#11772)
* oss part of control groups upgrade * changelog and docs * formatting * formatting
This commit is contained in:
parent
3b27017b0b
commit
1782b4e880
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:feature
|
||||||
|
core: Add controlled capabilities to control group policy stanza
|
||||||
|
```
|
|
@ -246,7 +246,8 @@ func NewACL(ctx context.Context, policies []*Policy) (*ACL, error) {
|
||||||
existingPerms.MFAMethods = strutil.RemoveDuplicates(existingPerms.MFAMethods, false)
|
existingPerms.MFAMethods = strutil.RemoveDuplicates(existingPerms.MFAMethods, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// No need to dedupe this list since any authorization can satisfy any factor
|
// No need to dedupe this list since any authorization can satisfy any factor, so long as
|
||||||
|
// the factor matches the specified permission requested.
|
||||||
if pc.Permissions.ControlGroup != nil {
|
if pc.Permissions.ControlGroup != nil {
|
||||||
if len(pc.Permissions.ControlGroup.Factors) > 0 {
|
if len(pc.Permissions.ControlGroup.Factors) > 0 {
|
||||||
if existingPerms.ControlGroup == nil {
|
if existingPerms.ControlGroup == nil {
|
||||||
|
|
|
@ -44,6 +44,13 @@ const (
|
||||||
SudoCapabilityInt
|
SudoCapabilityInt
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Error constants for testing
|
||||||
|
const (
|
||||||
|
// ControlledCapabilityPolicySubsetError is thrown when a control group's controlled capabilities
|
||||||
|
// are not a subset of the policy's capabilities.
|
||||||
|
ControlledCapabilityPolicySubsetError = "control group factor capabilities must be a subset of the policy's capabilities"
|
||||||
|
)
|
||||||
|
|
||||||
type PolicyType uint32
|
type PolicyType uint32
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -141,6 +148,7 @@ type ControlGroup struct {
|
||||||
type ControlGroupFactor struct {
|
type ControlGroupFactor struct {
|
||||||
Name string
|
Name string
|
||||||
Identity *IdentityFactor `hcl:"identity"`
|
Identity *IdentityFactor `hcl:"identity"`
|
||||||
|
ControlledCapabilities []string `hcl:"controlled_capabilities"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type IdentityFactor struct {
|
type IdentityFactor struct {
|
||||||
|
@ -431,7 +439,6 @@ func parsePaths(result *Policy, list *ast.ObjectList, performTemplating bool, en
|
||||||
}
|
}
|
||||||
pc.Permissions.ControlGroup.TTL = dur
|
pc.Permissions.ControlGroup.TTL = dur
|
||||||
}
|
}
|
||||||
|
|
||||||
var factors []*ControlGroupFactor
|
var factors []*ControlGroupFactor
|
||||||
if pc.ControlGroupHCL.Factors != nil {
|
if pc.ControlGroupHCL.Factors != nil {
|
||||||
for key, factor := range pc.ControlGroupHCL.Factors {
|
for key, factor := range pc.ControlGroupHCL.Factors {
|
||||||
|
@ -446,9 +453,27 @@ func parsePaths(result *Policy, list *ast.ObjectList, performTemplating bool, en
|
||||||
return errors.New("must provide more than one identity group and approvals > 0")
|
return errors.New("must provide more than one identity group and approvals > 0")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure that configured ControlledCapabilities for factor are a subset of the
|
||||||
|
// Capabilities of the policy.
|
||||||
|
if len(factor.ControlledCapabilities) > 0 {
|
||||||
|
var found bool
|
||||||
|
for _, controlledCapability := range factor.ControlledCapabilities {
|
||||||
|
found = false
|
||||||
|
for _, policyCap := range pc.Capabilities {
|
||||||
|
if controlledCapability == policyCap {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return errors.New(ControlledCapabilityPolicySubsetError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
factors = append(factors, &ControlGroupFactor{
|
factors = append(factors, &ControlGroupFactor{
|
||||||
Name: key,
|
Name: key,
|
||||||
Identity: factor.Identity,
|
Identity: factor.Identity,
|
||||||
|
ControlledCapabilities: factor.ControlledCapabilities,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,22 +12,18 @@ import (
|
||||||
var rawPolicy = strings.TrimSpace(`
|
var rawPolicy = strings.TrimSpace(`
|
||||||
# Developer policy
|
# Developer policy
|
||||||
name = "dev"
|
name = "dev"
|
||||||
|
|
||||||
# Deny all paths by default
|
# Deny all paths by default
|
||||||
path "*" {
|
path "*" {
|
||||||
policy = "deny"
|
policy = "deny"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Allow full access to staging
|
# Allow full access to staging
|
||||||
path "stage/*" {
|
path "stage/*" {
|
||||||
policy = "sudo"
|
policy = "sudo"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Limited read privilege to production
|
# Limited read privilege to production
|
||||||
path "prod/version" {
|
path "prod/version" {
|
||||||
policy = "read"
|
policy = "read"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Read access to foobar
|
# Read access to foobar
|
||||||
# Also tests stripping of leading slash and parsing of min/max as string and
|
# Also tests stripping of leading slash and parsing of min/max as string and
|
||||||
# integer
|
# integer
|
||||||
|
@ -36,7 +32,6 @@ path "/foo/bar" {
|
||||||
min_wrapping_ttl = 300
|
min_wrapping_ttl = 300
|
||||||
max_wrapping_ttl = "1h"
|
max_wrapping_ttl = "1h"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Add capabilities for creation and sudo to foobar
|
# Add capabilities for creation and sudo to foobar
|
||||||
# This will be separate; they are combined when compiled into an ACL
|
# This will be separate; they are combined when compiled into an ACL
|
||||||
# Also tests reverse string/int handling to the above
|
# Also tests reverse string/int handling to the above
|
||||||
|
@ -45,7 +40,6 @@ path "foo/bar" {
|
||||||
min_wrapping_ttl = "300s"
|
min_wrapping_ttl = "300s"
|
||||||
max_wrapping_ttl = 3600
|
max_wrapping_ttl = 3600
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check that only allowed_parameters are being added to foobar
|
# Check that only allowed_parameters are being added to foobar
|
||||||
path "foo/bar" {
|
path "foo/bar" {
|
||||||
capabilities = ["create", "sudo"]
|
capabilities = ["create", "sudo"]
|
||||||
|
@ -54,7 +48,6 @@ path "foo/bar" {
|
||||||
"zap" = []
|
"zap" = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check that only denied_parameters are being added to bazbar
|
# Check that only denied_parameters are being added to bazbar
|
||||||
path "baz/bar" {
|
path "baz/bar" {
|
||||||
capabilities = ["create", "sudo"]
|
capabilities = ["create", "sudo"]
|
||||||
|
@ -63,7 +56,6 @@ path "baz/bar" {
|
||||||
"zap" = []
|
"zap" = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check that both allowed and denied parameters are being added to bizbar
|
# Check that both allowed and denied parameters are being added to bizbar
|
||||||
path "biz/bar" {
|
path "biz/bar" {
|
||||||
capabilities = ["create", "sudo"]
|
capabilities = ["create", "sudo"]
|
||||||
|
@ -351,6 +343,69 @@ nope = "yes"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestPolicy_ParseControlGroupWrongCaps makes sure an appropriate error is
|
||||||
|
// thrown when a factor's controlled_capabilities are not a subset of
|
||||||
|
// the path capabilities.
|
||||||
|
func TestPolicy_ParseControlGroupWrongCaps(t *testing.T) {
|
||||||
|
_, err := ParseACLPolicy(namespace.RootNamespace, strings.TrimSpace(`
|
||||||
|
name = "controlgroups"
|
||||||
|
path "secret/*" {
|
||||||
|
capabilities = ["create", "read"]
|
||||||
|
control_group = {
|
||||||
|
max_ttl = "1h"
|
||||||
|
factor "ops_manager" {
|
||||||
|
controlled_capabilities = ["read", "write"]
|
||||||
|
identity {
|
||||||
|
group_names = ["blah"]
|
||||||
|
approvals = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`))
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Bad policy was successfully parsed")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), ControlledCapabilityPolicySubsetError) {
|
||||||
|
t.Fatalf("Wrong error returned when control group's controlled capabilities are not a subset of the path capabilities: error was %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPolicy_ParseControlGroup(t *testing.T) {
|
||||||
|
pol, err := ParseACLPolicy(namespace.RootNamespace, strings.TrimSpace(`
|
||||||
|
name = "controlgroups"
|
||||||
|
path "secret/*" {
|
||||||
|
capabilities = ["create", "read"]
|
||||||
|
control_group = {
|
||||||
|
max_ttl = "1h"
|
||||||
|
factor "ops_manager" {
|
||||||
|
controlled_capabilities = ["create"]
|
||||||
|
identity {
|
||||||
|
group_names = ["blah"]
|
||||||
|
approvals = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Policy could not be parsed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point paths haven't been merged yet. We must simply make sure
|
||||||
|
// that each factor has the correct associated permissions.
|
||||||
|
|
||||||
|
permFactors := pol.Paths[0].Permissions.ControlGroup.Factors
|
||||||
|
|
||||||
|
if len(permFactors) != 1 {
|
||||||
|
t.Fatalf("Expected 1 control group factor: got %d", len(permFactors))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(permFactors[0].ControlledCapabilities) != 1 && permFactors[0].ControlledCapabilities[0] != "create" {
|
||||||
|
t.Fatalf("controlled_capabilities on the first factor was not correct: %+v", permFactors[0].ControlledCapabilities)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPolicy_ParseBadPath(t *testing.T) {
|
func TestPolicy_ParseBadPath(t *testing.T) {
|
||||||
// The wrong spelling is intended here
|
// The wrong spelling is intended here
|
||||||
_, err := ParseACLPolicy(namespace.RootNamespace, strings.TrimSpace(`
|
_, err := ParseACLPolicy(namespace.RootNamespace, strings.TrimSpace(`
|
||||||
|
|
|
@ -25,6 +25,20 @@ Control Groups can verify the following factors:
|
||||||
- `Identity Groups` - Require an authorizer to be in a specific set of identity
|
- `Identity Groups` - Require an authorizer to be in a specific set of identity
|
||||||
groups.
|
groups.
|
||||||
|
|
||||||
|
### Controlled capabilities
|
||||||
|
Control group factors can be configured to trigger the control group workflow
|
||||||
|
on specific capabilities. This is done with the `controlled_capabilities` field.
|
||||||
|
Not specifying the `controlled_capabilities` field will necessitate the factor to be
|
||||||
|
checked for all operations to the specified policy path. The `controlled_capabilities`
|
||||||
|
field can differ per factor, so that different factors can be required for different
|
||||||
|
operations.
|
||||||
|
|
||||||
|
Finally, the capabilities in the `controlled_capabilities` stanza must be a subset of the
|
||||||
|
`capabilities` specifed in the policy itself. For example, a policy giving only `read` access to
|
||||||
|
the path `secret/foo` cannot specify a control group factor with `list` as a controlled capability.
|
||||||
|
|
||||||
|
Please see the following section for example ACL Policies.
|
||||||
|
|
||||||
## Control Groups In ACL Policies
|
## Control Groups In ACL Policies
|
||||||
|
|
||||||
Control Group requirements on paths are specified as `control_group` along
|
Control Group requirements on paths are specified as `control_group` along
|
||||||
|
@ -76,6 +90,84 @@ group authorizes the request. If an authorizer is a member of both the
|
||||||
"managers" and "superusers" group, one authorization for both factors will be
|
"managers" and "superusers" group, one authorization for both factors will be
|
||||||
satisfied.
|
satisfied.
|
||||||
|
|
||||||
|
```
|
||||||
|
path "secret/foo" {
|
||||||
|
capabilities = ["write","read"]
|
||||||
|
control_group = {
|
||||||
|
factor "admin" {
|
||||||
|
controlled_capabilities = ["write"]
|
||||||
|
identity {
|
||||||
|
group_names = ["admin"]
|
||||||
|
approvals = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The above policy grants `read` access to `secret/foo` for anyone that has a vault token
|
||||||
|
with this policy. It grants `write` access to `secret/foo` only after one member from the
|
||||||
|
admin group authorizes the request.
|
||||||
|
|
||||||
|
```
|
||||||
|
path "kv/*" {
|
||||||
|
capabilities = ["create", "update","delete","list","sudo"]
|
||||||
|
control_group = {
|
||||||
|
factor "admin" {
|
||||||
|
controlled_capabilities = ["delete","list","sudo"]
|
||||||
|
identity {
|
||||||
|
group_names = ["admin"]
|
||||||
|
approvals = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
path "kv/*" {
|
||||||
|
capabilities = ["create"]
|
||||||
|
control_group = {
|
||||||
|
factor "superuser" {
|
||||||
|
identity {
|
||||||
|
group_names = ["superuser"]
|
||||||
|
approvals = 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Because the second path stanza has a control group factor with no `controlled_capabilities` field,
|
||||||
|
any token with this policy will be required to get 2 approvals from the `superuser` group before executing
|
||||||
|
any operation against `kv/*`. In addition, by virtue of the `controlled_capablities` field in the first
|
||||||
|
path stanza, `delete`,`list`, and `sudo` operations will require an additional approval from the `admin` group.
|
||||||
|
|
||||||
|
```
|
||||||
|
path "kv/*" {
|
||||||
|
capabilities = ["read", "list", "create"]
|
||||||
|
control_group = {
|
||||||
|
controlled_capabilities = ["read"]
|
||||||
|
factor "admin" {
|
||||||
|
identity {
|
||||||
|
group_names = ["admin"]
|
||||||
|
approvals = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
factor "superuser" {
|
||||||
|
controlled_capabilities = ["create"]
|
||||||
|
identity {
|
||||||
|
group_names = ["superuser"]
|
||||||
|
approvals = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In this case, `read` will require one admin approval and `create` will require
|
||||||
|
one superuser approval and one admin approval. `List` will require no extra approvals
|
||||||
|
from any of the control group factors, and a token with this policy will not be required
|
||||||
|
to go through the control group workflow in order to execute a read operation against `kv/*`.
|
||||||
|
|
||||||
## Control Groups in Sentinel
|
## Control Groups in Sentinel
|
||||||
|
|
||||||
Control Groups are also supported in Sentinel policies using the `controlgroup`
|
Control Groups are also supported in Sentinel policies using the `controlgroup`
|
||||||
|
|
Loading…
Reference in New Issue