Skip to content

Commit

Permalink
Merge pull request #6100 from hashicorp/f-host-volumes
Browse files Browse the repository at this point in the history
Host Volumes Support: Rollup Edition
  • Loading branch information
endocrimes committed Aug 12, 2019
2 parents 07ce33a + c486143 commit 0c80fcb
Show file tree
Hide file tree
Showing 33 changed files with 1,406 additions and 141 deletions.
135 changes: 122 additions & 13 deletions acl/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)
Expand All @@ -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
}

Expand All @@ -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
}
Expand All @@ -179,7 +234,45 @@ 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
}

// Check if the capability has been granted
if len(capabilities) == 0 {
return false
}

return !capabilities.Check(PolicyDeny)
}

// 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
}
Expand All @@ -192,31 +285,47 @@ func (a *ACL) AllowNamespace(ns string) bool {
return !capabilities.Check(PolicyDeny)
}

// matchingCapabilitySet looks for a capabilitySet that matches the namespace,
// 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 {
return raw.(capabilitySet), true
}

// 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 {
Expand All @@ -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,
}
Expand Down
56 changes: 53 additions & 3 deletions acl/acl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
})
}
Expand Down
Loading

0 comments on commit 0c80fcb

Please sign in to comment.