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)
|
||||
}
|
||||
|
||||
// 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 len(pc.Permissions.ControlGroup.Factors) > 0 {
|
||||
if existingPerms.ControlGroup == nil {
|
||||
|
|
|
@ -44,6 +44,13 @@ const (
|
|||
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
|
||||
|
||||
const (
|
||||
|
@ -141,6 +148,7 @@ type ControlGroup struct {
|
|||
type ControlGroupFactor struct {
|
||||
Name string
|
||||
Identity *IdentityFactor `hcl:"identity"`
|
||||
ControlledCapabilities []string `hcl:"controlled_capabilities"`
|
||||
}
|
||||
|
||||
type IdentityFactor struct {
|
||||
|
@ -431,7 +439,6 @@ func parsePaths(result *Policy, list *ast.ObjectList, performTemplating bool, en
|
|||
}
|
||||
pc.Permissions.ControlGroup.TTL = dur
|
||||
}
|
||||
|
||||
var factors []*ControlGroupFactor
|
||||
if pc.ControlGroupHCL.Factors != nil {
|
||||
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")
|
||||
}
|
||||
|
||||
// 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{
|
||||
Name: key,
|
||||
Identity: factor.Identity,
|
||||
ControlledCapabilities: factor.ControlledCapabilities,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,22 +12,18 @@ import (
|
|||
var rawPolicy = strings.TrimSpace(`
|
||||
# Developer policy
|
||||
name = "dev"
|
||||
|
||||
# Deny all paths by default
|
||||
path "*" {
|
||||
policy = "deny"
|
||||
}
|
||||
|
||||
# Allow full access to staging
|
||||
path "stage/*" {
|
||||
policy = "sudo"
|
||||
}
|
||||
|
||||
# Limited read privilege to production
|
||||
path "prod/version" {
|
||||
policy = "read"
|
||||
}
|
||||
|
||||
# Read access to foobar
|
||||
# Also tests stripping of leading slash and parsing of min/max as string and
|
||||
# integer
|
||||
|
@ -36,7 +32,6 @@ path "/foo/bar" {
|
|||
min_wrapping_ttl = 300
|
||||
max_wrapping_ttl = "1h"
|
||||
}
|
||||
|
||||
# Add capabilities for creation and sudo to foobar
|
||||
# This will be separate; they are combined when compiled into an ACL
|
||||
# Also tests reverse string/int handling to the above
|
||||
|
@ -45,7 +40,6 @@ path "foo/bar" {
|
|||
min_wrapping_ttl = "300s"
|
||||
max_wrapping_ttl = 3600
|
||||
}
|
||||
|
||||
# Check that only allowed_parameters are being added to foobar
|
||||
path "foo/bar" {
|
||||
capabilities = ["create", "sudo"]
|
||||
|
@ -54,7 +48,6 @@ path "foo/bar" {
|
|||
"zap" = []
|
||||
}
|
||||
}
|
||||
|
||||
# Check that only denied_parameters are being added to bazbar
|
||||
path "baz/bar" {
|
||||
capabilities = ["create", "sudo"]
|
||||
|
@ -63,7 +56,6 @@ path "baz/bar" {
|
|||
"zap" = []
|
||||
}
|
||||
}
|
||||
|
||||
# Check that both allowed and denied parameters are being added to bizbar
|
||||
path "biz/bar" {
|
||||
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) {
|
||||
// The wrong spelling is intended here
|
||||
_, 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
|
||||
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 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
|
||||
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 are also supported in Sentinel policies using the `controlgroup`
|
||||
|
|
Loading…
Reference in New Issue