acl: Add HostVolume ACLs
This adds an initial implementation of ACLs for HostVolumes. Because HostVolumes are a cluster-wide resource, they cannot be tied to a namespace, thus here we allow similar wildcard definitions based on their names, tied to a set of capabilities. Initially, the only available capabilities are deny, or mount. These may be extended in the future to allow read-fs, mount-readonly and similar capabilities.
This commit is contained in:
parent
7208a7ab88
commit
5f734652f2
135
acl/acl.go
135
acl/acl.go
|
@ -51,6 +51,13 @@ type ACL struct {
|
|||
// We use an iradix for the purposes of ordered iteration.
|
||||
wildcardNamespaces *iradix.Tree
|
||||
|
||||
// hostVolumes maps a named host volume to a capabilitySet
|
||||
hostVolumes *iradix.Tree
|
||||
|
||||
// wildcardHostVolumes maps a glob pattern of host volume names to a capabilitySet
|
||||
// We use an iradix for the purposes of ordered iteration.
|
||||
wildcardHostVolumes *iradix.Tree
|
||||
|
||||
agent string
|
||||
node string
|
||||
operator string
|
||||
|
@ -83,6 +90,8 @@ func NewACL(management bool, policies []*Policy) (*ACL, error) {
|
|||
acl := &ACL{}
|
||||
nsTxn := iradix.New().Txn()
|
||||
wnsTxn := iradix.New().Txn()
|
||||
hvTxn := iradix.New().Txn()
|
||||
whvTxn := iradix.New().Txn()
|
||||
|
||||
for _, policy := range policies {
|
||||
NAMESPACES:
|
||||
|
@ -128,6 +137,49 @@ func NewACL(management bool, policies []*Policy) (*ACL, error) {
|
|||
}
|
||||
}
|
||||
|
||||
HOSTVOLUMES:
|
||||
for _, hv := range policy.HostVolumes {
|
||||
// Should the volume be matched using a glob?
|
||||
globDefinition := strings.Contains(hv.Name, "*")
|
||||
|
||||
// Check for existing capabilities
|
||||
var capabilities capabilitySet
|
||||
|
||||
if globDefinition {
|
||||
raw, ok := whvTxn.Get([]byte(hv.Name))
|
||||
if ok {
|
||||
capabilities = raw.(capabilitySet)
|
||||
} else {
|
||||
capabilities = make(capabilitySet)
|
||||
whvTxn.Insert([]byte(hv.Name), capabilities)
|
||||
}
|
||||
} else {
|
||||
raw, ok := hvTxn.Get([]byte(hv.Name))
|
||||
if ok {
|
||||
capabilities = raw.(capabilitySet)
|
||||
} else {
|
||||
capabilities = make(capabilitySet)
|
||||
hvTxn.Insert([]byte(hv.Name), capabilities)
|
||||
}
|
||||
}
|
||||
|
||||
// Deny always takes precedence
|
||||
if capabilities.Check(HostVolumeCapabilityDeny) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Add in all the capabilities
|
||||
for _, cap := range hv.Capabilities {
|
||||
if cap == HostVolumeCapabilityDeny {
|
||||
// Overwrite any existing capabilities
|
||||
capabilities.Clear()
|
||||
capabilities.Set(HostVolumeCapabilityDeny)
|
||||
continue HOSTVOLUMES
|
||||
}
|
||||
capabilities.Set(cap)
|
||||
}
|
||||
}
|
||||
|
||||
// Take the maximum privilege for agent, node, and operator
|
||||
if policy.Agent != nil {
|
||||
acl.agent = maxPrivilege(acl.agent, policy.Agent.Policy)
|
||||
|
@ -146,6 +198,9 @@ func NewACL(management bool, policies []*Policy) (*ACL, error) {
|
|||
// Finalize the namespaces
|
||||
acl.namespaces = nsTxn.Commit()
|
||||
acl.wildcardNamespaces = wnsTxn.Commit()
|
||||
acl.hostVolumes = hvTxn.Commit()
|
||||
acl.wildcardHostVolumes = whvTxn.Commit()
|
||||
|
||||
return acl, nil
|
||||
}
|
||||
|
||||
|
@ -162,7 +217,7 @@ func (a *ACL) AllowNamespaceOperation(ns string, op string) bool {
|
|||
}
|
||||
|
||||
// Check for a matching capability set
|
||||
capabilities, ok := a.matchingCapabilitySet(ns)
|
||||
capabilities, ok := a.matchingNamespaceCapabilitySet(ns)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
@ -179,7 +234,7 @@ func (a *ACL) AllowNamespace(ns string) bool {
|
|||
}
|
||||
|
||||
// Check for a matching capability set
|
||||
capabilities, ok := a.matchingCapabilitySet(ns)
|
||||
capabilities, ok := a.matchingNamespaceCapabilitySet(ns)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
@ -192,12 +247,50 @@ func (a *ACL) AllowNamespace(ns string) bool {
|
|||
return !capabilities.Check(PolicyDeny)
|
||||
}
|
||||
|
||||
// matchingCapabilitySet looks for a capabilitySet that matches the namespace,
|
||||
// AllowHostVolumeOperation checks if a given operation is allowed for a host volume
|
||||
func (a *ACL) AllowHostVolumeOperation(hv string, op string) bool {
|
||||
// Hot path management tokens
|
||||
if a.management {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for a matching capability set
|
||||
capabilities, ok := a.matchingHostVolumeCapabilitySet(hv)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if the capability has been granted
|
||||
return capabilities.Check(op)
|
||||
}
|
||||
|
||||
// AllowHostVolume checks if any operations are allowed for a HostVolume
|
||||
func (a *ACL) AllowHostVolume(ns string) bool {
|
||||
// Hot path management tokens
|
||||
if a.management {
|
||||
return true
|
||||
}
|
||||
|
||||
// Check for a matching capability set
|
||||
capabilities, ok := a.matchingHostVolumeCapabilitySet(ns)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check if the capability has been granted
|
||||
if len(capabilities) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return !capabilities.Check(PolicyDeny)
|
||||
}
|
||||
|
||||
// matchingNamespaceCapabilitySet looks for a capabilitySet that matches the namespace,
|
||||
// if no concrete definitions are found, then we return the closest matching
|
||||
// glob.
|
||||
// The closest matching glob is the one that has the smallest character
|
||||
// difference between the namespace and the glob.
|
||||
func (a *ACL) matchingCapabilitySet(ns string) (capabilitySet, bool) {
|
||||
func (a *ACL) matchingNamespaceCapabilitySet(ns string) (capabilitySet, bool) {
|
||||
// Check for a concrete matching capability set
|
||||
raw, ok := a.namespaces.Get([]byte(ns))
|
||||
if ok {
|
||||
|
@ -205,18 +298,34 @@ func (a *ACL) matchingCapabilitySet(ns string) (capabilitySet, bool) {
|
|||
}
|
||||
|
||||
// We didn't find a concrete match, so lets try and evaluate globs.
|
||||
return a.findClosestMatchingGlob(ns)
|
||||
return a.findClosestMatchingGlob(a.wildcardNamespaces, ns)
|
||||
}
|
||||
|
||||
// matchingHostVolumeCapabilitySet looks for a capabilitySet that matches the host volume name,
|
||||
// if no concrete definitions are found, then we return the closest matching
|
||||
// glob.
|
||||
// The closest matching glob is the one that has the smallest character
|
||||
// difference between the volume name and the glob.
|
||||
func (a *ACL) matchingHostVolumeCapabilitySet(name string) (capabilitySet, bool) {
|
||||
// Check for a concrete matching capability set
|
||||
raw, ok := a.hostVolumes.Get([]byte(name))
|
||||
if ok {
|
||||
return raw.(capabilitySet), true
|
||||
}
|
||||
|
||||
// We didn't find a concrete match, so lets try and evaluate globs.
|
||||
return a.findClosestMatchingGlob(a.wildcardHostVolumes, name)
|
||||
}
|
||||
|
||||
type matchingGlob struct {
|
||||
ns string
|
||||
name string
|
||||
difference int
|
||||
capabilitySet capabilitySet
|
||||
}
|
||||
|
||||
func (a *ACL) findClosestMatchingGlob(ns string) (capabilitySet, bool) {
|
||||
func (a *ACL) findClosestMatchingGlob(radix *iradix.Tree, ns string) (capabilitySet, bool) {
|
||||
// First, find all globs that match.
|
||||
matchingGlobs := a.findAllMatchingWildcards(ns)
|
||||
matchingGlobs := findAllMatchingWildcards(radix, ns)
|
||||
|
||||
// If none match, let's return.
|
||||
if len(matchingGlobs) == 0 {
|
||||
|
@ -238,19 +347,19 @@ func (a *ACL) findClosestMatchingGlob(ns string) (capabilitySet, bool) {
|
|||
return matchingGlobs[0].capabilitySet, true
|
||||
}
|
||||
|
||||
func (a *ACL) findAllMatchingWildcards(ns string) []matchingGlob {
|
||||
func findAllMatchingWildcards(radix *iradix.Tree, name string) []matchingGlob {
|
||||
var matches []matchingGlob
|
||||
|
||||
nsLen := len(ns)
|
||||
nsLen := len(name)
|
||||
|
||||
a.wildcardNamespaces.Root().Walk(func(bk []byte, iv interface{}) bool {
|
||||
radix.Root().Walk(func(bk []byte, iv interface{}) bool {
|
||||
k := string(bk)
|
||||
v := iv.(capabilitySet)
|
||||
|
||||
isMatch := glob.Glob(k, ns)
|
||||
isMatch := glob.Glob(k, name)
|
||||
if isMatch {
|
||||
pair := matchingGlob{
|
||||
ns: k,
|
||||
name: k,
|
||||
difference: nsLen - len(k) + strings.Count(k, glob.GLOB),
|
||||
capabilitySet: v,
|
||||
}
|
||||
|
|
|
@ -314,6 +314,56 @@ func TestWildcardNamespaceMatching(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestWildcardHostVolumeMatching(t *testing.T) {
|
||||
tests := []struct {
|
||||
Policy string
|
||||
Allow bool
|
||||
}{
|
||||
{ // Wildcard matches
|
||||
Policy: `host_volume "prod-api-*" { policy = "write" }`,
|
||||
Allow: true,
|
||||
},
|
||||
{ // Non globbed volumes are not wildcards
|
||||
Policy: `host_volume "prod-api" { policy = "write" }`,
|
||||
Allow: false,
|
||||
},
|
||||
{ // Concrete matches take precedence
|
||||
Policy: `host_volume "prod-api-services" { policy = "deny" }
|
||||
host_volume "prod-api-*" { policy = "write" }`,
|
||||
Allow: false,
|
||||
},
|
||||
{
|
||||
Policy: `host_volume "prod-api-*" { policy = "deny" }
|
||||
host_volume "prod-api-services" { policy = "write" }`,
|
||||
Allow: true,
|
||||
},
|
||||
{ // The closest character match wins
|
||||
Policy: `host_volume "*-api-services" { policy = "deny" }
|
||||
host_volume "prod-api-*" { policy = "write" }`, // 4 vs 8 chars
|
||||
Allow: false,
|
||||
},
|
||||
{
|
||||
Policy: `host_volume "prod-api-*" { policy = "write" }
|
||||
host_volume "*-api-services" { policy = "deny" }`, // 4 vs 8 chars
|
||||
Allow: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.Policy, func(t *testing.T) {
|
||||
assert := assert.New(t)
|
||||
|
||||
policy, err := Parse(tc.Policy)
|
||||
assert.NoError(err)
|
||||
assert.NotNil(policy.HostVolumes)
|
||||
|
||||
acl, err := NewACL(false, []*Policy{policy})
|
||||
assert.Nil(err)
|
||||
|
||||
assert.Equal(tc.Allow, acl.AllowHostVolume("prod-api-services"))
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestACL_matchingCapabilitySet_returnsAllMatches(t *testing.T) {
|
||||
tests := []struct {
|
||||
Policy string
|
||||
|
@ -351,8 +401,8 @@ func TestACL_matchingCapabilitySet_returnsAllMatches(t *testing.T) {
|
|||
assert.Nil(err)
|
||||
|
||||
var namespaces []string
|
||||
for _, cs := range acl.findAllMatchingWildcards(tc.NS) {
|
||||
namespaces = append(namespaces, cs.ns)
|
||||
for _, cs := range findAllMatchingWildcards(acl.wildcardNamespaces, tc.NS) {
|
||||
namespaces = append(namespaces, cs.name)
|
||||
}
|
||||
|
||||
assert.Equal(tc.MatchingGlobs, namespaces)
|
||||
|
@ -404,7 +454,7 @@ func TestACL_matchingCapabilitySet_difference(t *testing.T) {
|
|||
acl, err := NewACL(false, []*Policy{policy})
|
||||
assert.Nil(err)
|
||||
|
||||
matches := acl.findAllMatchingWildcards(tc.NS)
|
||||
matches := findAllMatchingWildcards(acl.wildcardNamespaces, tc.NS)
|
||||
assert.Equal(tc.Difference, matches[0].difference)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ const (
|
|||
// The Policy stanza is a short hand for granting several of these. When capabilities are
|
||||
// combined we take the union of all capabilities. If the deny capability is present, it
|
||||
// takes precedence and overwrites all other capabilities.
|
||||
|
||||
NamespaceCapabilityDeny = "deny"
|
||||
NamespaceCapabilityListJobs = "list-jobs"
|
||||
NamespaceCapabilityReadJob = "read-job"
|
||||
|
@ -38,20 +39,36 @@ var (
|
|||
validNamespace = regexp.MustCompile("^[a-zA-Z0-9-*]{1,128}$")
|
||||
)
|
||||
|
||||
const (
|
||||
// The following are the fine-grained capabilities that can be granted for a volume set.
|
||||
// The Policy stanza is a short hand for granting several of these. When capabilities are
|
||||
// combined we take the union of all capabilities. If the deny capability is present, it
|
||||
// takes precedence and overwrites all other capabilities.
|
||||
|
||||
HostVolumeCapabilityDeny = "deny"
|
||||
HostVolumeCapabilityMount = "mount"
|
||||
)
|
||||
|
||||
var (
|
||||
validVolume = regexp.MustCompile("^[a-zA-Z0-9-*]{1,128}$")
|
||||
)
|
||||
|
||||
// Policy represents a parsed HCL or JSON policy.
|
||||
type Policy struct {
|
||||
Namespaces []*NamespacePolicy `hcl:"namespace,expand"`
|
||||
Agent *AgentPolicy `hcl:"agent"`
|
||||
Node *NodePolicy `hcl:"node"`
|
||||
Operator *OperatorPolicy `hcl:"operator"`
|
||||
Quota *QuotaPolicy `hcl:"quota"`
|
||||
Raw string `hcl:"-"`
|
||||
Namespaces []*NamespacePolicy `hcl:"namespace,expand"`
|
||||
HostVolumes []*HostVolumePolicy `hcl:"host_volume,expand"`
|
||||
Agent *AgentPolicy `hcl:"agent"`
|
||||
Node *NodePolicy `hcl:"node"`
|
||||
Operator *OperatorPolicy `hcl:"operator"`
|
||||
Quota *QuotaPolicy `hcl:"quota"`
|
||||
Raw string `hcl:"-"`
|
||||
}
|
||||
|
||||
// IsEmpty checks to make sure that at least one policy has been set and is not
|
||||
// comprised of only a raw policy.
|
||||
func (p *Policy) IsEmpty() bool {
|
||||
return len(p.Namespaces) == 0 &&
|
||||
len(p.HostVolumes) == 0 &&
|
||||
p.Agent == nil &&
|
||||
p.Node == nil &&
|
||||
p.Operator == nil &&
|
||||
|
@ -65,6 +82,13 @@ type NamespacePolicy struct {
|
|||
Capabilities []string
|
||||
}
|
||||
|
||||
// HostVolumePolicy is the policy for a specific named host volume
|
||||
type HostVolumePolicy struct {
|
||||
Name string `hcl:",key"`
|
||||
Policy string
|
||||
Capabilities []string
|
||||
}
|
||||
|
||||
type AgentPolicy struct {
|
||||
Policy string
|
||||
}
|
||||
|
@ -134,6 +158,28 @@ func expandNamespacePolicy(policy string) []string {
|
|||
}
|
||||
}
|
||||
|
||||
func isHostVolumeCapabilityValid(cap string) bool {
|
||||
switch cap {
|
||||
case HostVolumeCapabilityDeny, HostVolumeCapabilityMount:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func expandHostVolumePolicy(policy string) []string {
|
||||
switch policy {
|
||||
case PolicyDeny:
|
||||
return []string{HostVolumeCapabilityDeny}
|
||||
case PolicyRead:
|
||||
return []string{HostVolumeCapabilityDeny}
|
||||
case PolicyWrite:
|
||||
return []string{HostVolumeCapabilityMount}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Parse is used to parse the specified ACL rules into an
|
||||
// intermediary set of policies, before being compiled into
|
||||
// the ACL
|
||||
|
@ -178,6 +224,27 @@ func Parse(rules string) (*Policy, error) {
|
|||
}
|
||||
}
|
||||
|
||||
for _, hv := range p.HostVolumes {
|
||||
if !validVolume.MatchString(hv.Name) {
|
||||
return nil, fmt.Errorf("Invalid host volume name: %#v", hv)
|
||||
}
|
||||
if hv.Policy != "" && !isPolicyValid(hv.Policy) {
|
||||
return nil, fmt.Errorf("Invalid host volume policy: %#v", hv)
|
||||
}
|
||||
for _, cap := range hv.Capabilities {
|
||||
if !isHostVolumeCapabilityValid(cap) {
|
||||
return nil, fmt.Errorf("Invalid host volume capability '%s': %#v", cap, hv)
|
||||
}
|
||||
}
|
||||
|
||||
// Expand the short hand policy to the capabilities and
|
||||
// add to any existing capabilities
|
||||
if hv.Policy != "" {
|
||||
extraCap := expandHostVolumePolicy(hv.Policy)
|
||||
hv.Capabilities = append(hv.Capabilities, extraCap...)
|
||||
}
|
||||
}
|
||||
|
||||
if p.Agent != nil && !isPolicyValid(p.Agent.Policy) {
|
||||
return nil, fmt.Errorf("Invalid agent policy: %#v", p.Agent)
|
||||
}
|
||||
|
|
|
@ -199,6 +199,34 @@ func TestParse(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
`
|
||||
host_volume "production-tls-*" {
|
||||
capabilities = ["mount"]
|
||||
}
|
||||
`,
|
||||
"",
|
||||
&Policy{
|
||||
HostVolumes: []*HostVolumePolicy{
|
||||
{
|
||||
Name: "production-tls-*",
|
||||
Policy: "",
|
||||
Capabilities: []string{
|
||||
HostVolumeCapabilityMount,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
`
|
||||
host_volume "volume has a space" {
|
||||
capabilities = ["mount"]
|
||||
}
|
||||
`,
|
||||
"Invalid host volume name",
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
for idx, tc := range tcases {
|
||||
|
|
Loading…
Reference in New Issue