github.com/ilhicas/nomad@v0.11.8/acl/acl.go (about)

     1  package acl
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  	"strings"
     7  
     8  	iradix "github.com/hashicorp/go-immutable-radix"
     9  	glob "github.com/ryanuber/go-glob"
    10  )
    11  
    12  // ManagementACL is a singleton used for management tokens
    13  var ManagementACL *ACL
    14  
    15  func init() {
    16  	var err error
    17  	ManagementACL, err = NewACL(true, nil)
    18  	if err != nil {
    19  		panic(fmt.Errorf("failed to setup management ACL: %v", err))
    20  	}
    21  }
    22  
    23  // capabilitySet is a type wrapper to help managing a set of capabilities
    24  type capabilitySet map[string]struct{}
    25  
    26  func (c capabilitySet) Check(k string) bool {
    27  	_, ok := c[k]
    28  	return ok
    29  }
    30  
    31  func (c capabilitySet) Set(k string) {
    32  	c[k] = struct{}{}
    33  }
    34  
    35  func (c capabilitySet) Clear() {
    36  	for cap := range c {
    37  		delete(c, cap)
    38  	}
    39  }
    40  
    41  // ACL object is used to convert a set of policies into a structure that
    42  // can be efficiently evaluated to determine if an action is allowed.
    43  type ACL struct {
    44  	// management tokens are allowed to do anything
    45  	management bool
    46  
    47  	// namespaces maps a namespace to a capabilitySet
    48  	namespaces *iradix.Tree
    49  
    50  	// wildcardNamespaces maps a glob pattern of a namespace to a capabilitySet
    51  	// We use an iradix for the purposes of ordered iteration.
    52  	wildcardNamespaces *iradix.Tree
    53  
    54  	// hostVolumes maps a named host volume to a capabilitySet
    55  	hostVolumes *iradix.Tree
    56  
    57  	// wildcardHostVolumes maps a glob pattern of host volume names to a capabilitySet
    58  	// We use an iradix for the purposes of ordered iteration.
    59  	wildcardHostVolumes *iradix.Tree
    60  
    61  	agent    string
    62  	node     string
    63  	operator string
    64  	quota    string
    65  	plugin   string
    66  }
    67  
    68  // maxPrivilege returns the policy which grants the most privilege
    69  // This handles the case of Deny always taking maximum precedence.
    70  func maxPrivilege(a, b string) string {
    71  	switch {
    72  	case a == PolicyDeny || b == PolicyDeny:
    73  		return PolicyDeny
    74  	case a == PolicyWrite || b == PolicyWrite:
    75  		return PolicyWrite
    76  	case a == PolicyRead || b == PolicyRead:
    77  		return PolicyRead
    78  	case a == PolicyList || b == PolicyList:
    79  		return PolicyList
    80  	default:
    81  		return ""
    82  	}
    83  }
    84  
    85  // NewACL compiles a set of policies into an ACL object
    86  func NewACL(management bool, policies []*Policy) (*ACL, error) {
    87  	// Hot-path management tokens
    88  	if management {
    89  		return &ACL{management: true}, nil
    90  	}
    91  
    92  	// Create the ACL object
    93  	acl := &ACL{}
    94  	nsTxn := iradix.New().Txn()
    95  	wnsTxn := iradix.New().Txn()
    96  	hvTxn := iradix.New().Txn()
    97  	whvTxn := iradix.New().Txn()
    98  
    99  	for _, policy := range policies {
   100  	NAMESPACES:
   101  		for _, ns := range policy.Namespaces {
   102  			// Should the namespace be matched using a glob?
   103  			globDefinition := strings.Contains(ns.Name, "*")
   104  
   105  			// Check for existing capabilities
   106  			var capabilities capabilitySet
   107  
   108  			if globDefinition {
   109  				raw, ok := wnsTxn.Get([]byte(ns.Name))
   110  				if ok {
   111  					capabilities = raw.(capabilitySet)
   112  				} else {
   113  					capabilities = make(capabilitySet)
   114  					wnsTxn.Insert([]byte(ns.Name), capabilities)
   115  				}
   116  			} else {
   117  				raw, ok := nsTxn.Get([]byte(ns.Name))
   118  				if ok {
   119  					capabilities = raw.(capabilitySet)
   120  				} else {
   121  					capabilities = make(capabilitySet)
   122  					nsTxn.Insert([]byte(ns.Name), capabilities)
   123  				}
   124  			}
   125  
   126  			// Deny always takes precedence
   127  			if capabilities.Check(NamespaceCapabilityDeny) {
   128  				continue NAMESPACES
   129  			}
   130  
   131  			// Add in all the capabilities
   132  			for _, cap := range ns.Capabilities {
   133  				if cap == NamespaceCapabilityDeny {
   134  					// Overwrite any existing capabilities
   135  					capabilities.Clear()
   136  					capabilities.Set(NamespaceCapabilityDeny)
   137  					continue NAMESPACES
   138  				}
   139  				capabilities.Set(cap)
   140  			}
   141  		}
   142  
   143  	HOSTVOLUMES:
   144  		for _, hv := range policy.HostVolumes {
   145  			// Should the volume be matched using a glob?
   146  			globDefinition := strings.Contains(hv.Name, "*")
   147  
   148  			// Check for existing capabilities
   149  			var capabilities capabilitySet
   150  
   151  			if globDefinition {
   152  				raw, ok := whvTxn.Get([]byte(hv.Name))
   153  				if ok {
   154  					capabilities = raw.(capabilitySet)
   155  				} else {
   156  					capabilities = make(capabilitySet)
   157  					whvTxn.Insert([]byte(hv.Name), capabilities)
   158  				}
   159  			} else {
   160  				raw, ok := hvTxn.Get([]byte(hv.Name))
   161  				if ok {
   162  					capabilities = raw.(capabilitySet)
   163  				} else {
   164  					capabilities = make(capabilitySet)
   165  					hvTxn.Insert([]byte(hv.Name), capabilities)
   166  				}
   167  			}
   168  
   169  			// Deny always takes precedence
   170  			if capabilities.Check(HostVolumeCapabilityDeny) {
   171  				continue
   172  			}
   173  
   174  			// Add in all the capabilities
   175  			for _, cap := range hv.Capabilities {
   176  				if cap == HostVolumeCapabilityDeny {
   177  					// Overwrite any existing capabilities
   178  					capabilities.Clear()
   179  					capabilities.Set(HostVolumeCapabilityDeny)
   180  					continue HOSTVOLUMES
   181  				}
   182  				capabilities.Set(cap)
   183  			}
   184  		}
   185  
   186  		// Take the maximum privilege for agent, node, and operator
   187  		if policy.Agent != nil {
   188  			acl.agent = maxPrivilege(acl.agent, policy.Agent.Policy)
   189  		}
   190  		if policy.Node != nil {
   191  			acl.node = maxPrivilege(acl.node, policy.Node.Policy)
   192  		}
   193  		if policy.Operator != nil {
   194  			acl.operator = maxPrivilege(acl.operator, policy.Operator.Policy)
   195  		}
   196  		if policy.Quota != nil {
   197  			acl.quota = maxPrivilege(acl.quota, policy.Quota.Policy)
   198  		}
   199  		if policy.Plugin != nil {
   200  			acl.plugin = maxPrivilege(acl.plugin, policy.Plugin.Policy)
   201  		}
   202  	}
   203  
   204  	// Finalize the namespaces
   205  	acl.namespaces = nsTxn.Commit()
   206  	acl.wildcardNamespaces = wnsTxn.Commit()
   207  	acl.hostVolumes = hvTxn.Commit()
   208  	acl.wildcardHostVolumes = whvTxn.Commit()
   209  
   210  	return acl, nil
   211  }
   212  
   213  // AllowNsOp is shorthand for AllowNamespaceOperation
   214  func (a *ACL) AllowNsOp(ns string, op string) bool {
   215  	return a.AllowNamespaceOperation(ns, op)
   216  }
   217  
   218  // AllowNamespaceOperation checks if a given operation is allowed for a namespace
   219  func (a *ACL) AllowNamespaceOperation(ns string, op string) bool {
   220  	// Hot path management tokens
   221  	if a.management {
   222  		return true
   223  	}
   224  
   225  	// Check for a matching capability set
   226  	capabilities, ok := a.matchingNamespaceCapabilitySet(ns)
   227  	if !ok {
   228  		return false
   229  	}
   230  
   231  	// Check if the capability has been granted
   232  	return capabilities.Check(op)
   233  }
   234  
   235  // AllowNamespace checks if any operations are allowed for a namespace
   236  func (a *ACL) AllowNamespace(ns string) bool {
   237  	// Hot path management tokens
   238  	if a.management {
   239  		return true
   240  	}
   241  
   242  	// Check for a matching capability set
   243  	capabilities, ok := a.matchingNamespaceCapabilitySet(ns)
   244  	if !ok {
   245  		return false
   246  	}
   247  
   248  	// Check if the capability has been granted
   249  	if len(capabilities) == 0 {
   250  		return false
   251  	}
   252  
   253  	return !capabilities.Check(PolicyDeny)
   254  }
   255  
   256  // AllowHostVolumeOperation checks if a given operation is allowed for a host volume
   257  func (a *ACL) AllowHostVolumeOperation(hv string, op string) bool {
   258  	// Hot path management tokens
   259  	if a.management {
   260  		return true
   261  	}
   262  
   263  	// Check for a matching capability set
   264  	capabilities, ok := a.matchingHostVolumeCapabilitySet(hv)
   265  	if !ok {
   266  		return false
   267  	}
   268  
   269  	// Check if the capability has been granted
   270  	return capabilities.Check(op)
   271  }
   272  
   273  // AllowHostVolume checks if any operations are allowed for a HostVolume
   274  func (a *ACL) AllowHostVolume(ns string) bool {
   275  	// Hot path management tokens
   276  	if a.management {
   277  		return true
   278  	}
   279  
   280  	// Check for a matching capability set
   281  	capabilities, ok := a.matchingHostVolumeCapabilitySet(ns)
   282  	if !ok {
   283  		return false
   284  	}
   285  
   286  	// Check if the capability has been granted
   287  	if len(capabilities) == 0 {
   288  		return false
   289  	}
   290  
   291  	return !capabilities.Check(PolicyDeny)
   292  }
   293  
   294  // matchingNamespaceCapabilitySet looks for a capabilitySet that matches the namespace,
   295  // if no concrete definitions are found, then we return the closest matching
   296  // glob.
   297  // The closest matching glob is the one that has the smallest character
   298  // difference between the namespace and the glob.
   299  func (a *ACL) matchingNamespaceCapabilitySet(ns string) (capabilitySet, bool) {
   300  	// Check for a concrete matching capability set
   301  	raw, ok := a.namespaces.Get([]byte(ns))
   302  	if ok {
   303  		return raw.(capabilitySet), true
   304  	}
   305  
   306  	// We didn't find a concrete match, so lets try and evaluate globs.
   307  	return a.findClosestMatchingGlob(a.wildcardNamespaces, ns)
   308  }
   309  
   310  // matchingHostVolumeCapabilitySet looks for a capabilitySet that matches the host volume name,
   311  // if no concrete definitions are found, then we return the closest matching
   312  // glob.
   313  // The closest matching glob is the one that has the smallest character
   314  // difference between the volume name and the glob.
   315  func (a *ACL) matchingHostVolumeCapabilitySet(name string) (capabilitySet, bool) {
   316  	// Check for a concrete matching capability set
   317  	raw, ok := a.hostVolumes.Get([]byte(name))
   318  	if ok {
   319  		return raw.(capabilitySet), true
   320  	}
   321  
   322  	// We didn't find a concrete match, so lets try and evaluate globs.
   323  	return a.findClosestMatchingGlob(a.wildcardHostVolumes, name)
   324  }
   325  
   326  type matchingGlob struct {
   327  	name          string
   328  	difference    int
   329  	capabilitySet capabilitySet
   330  }
   331  
   332  func (a *ACL) findClosestMatchingGlob(radix *iradix.Tree, ns string) (capabilitySet, bool) {
   333  	// First, find all globs that match.
   334  	matchingGlobs := findAllMatchingWildcards(radix, ns)
   335  
   336  	// If none match, let's return.
   337  	if len(matchingGlobs) == 0 {
   338  		return capabilitySet{}, false
   339  	}
   340  
   341  	// If a single matches, lets be efficient and return early.
   342  	if len(matchingGlobs) == 1 {
   343  		return matchingGlobs[0].capabilitySet, true
   344  	}
   345  
   346  	// Stable sort the matched globs, based on the character difference between
   347  	// the glob definition and the requested namespace. This allows us to be
   348  	// more consistent about results based on the policy definition.
   349  	sort.SliceStable(matchingGlobs, func(i, j int) bool {
   350  		return matchingGlobs[i].difference <= matchingGlobs[j].difference
   351  	})
   352  
   353  	return matchingGlobs[0].capabilitySet, true
   354  }
   355  
   356  func findAllMatchingWildcards(radix *iradix.Tree, name string) []matchingGlob {
   357  	var matches []matchingGlob
   358  
   359  	nsLen := len(name)
   360  
   361  	radix.Root().Walk(func(bk []byte, iv interface{}) bool {
   362  		k := string(bk)
   363  		v := iv.(capabilitySet)
   364  
   365  		isMatch := glob.Glob(k, name)
   366  		if isMatch {
   367  			pair := matchingGlob{
   368  				name:          k,
   369  				difference:    nsLen - len(k) + strings.Count(k, glob.GLOB),
   370  				capabilitySet: v,
   371  			}
   372  			matches = append(matches, pair)
   373  		}
   374  
   375  		// We always want to walk the entire tree, never terminate early.
   376  		return false
   377  	})
   378  
   379  	return matches
   380  }
   381  
   382  // AllowAgentRead checks if read operations are allowed for an agent
   383  func (a *ACL) AllowAgentRead() bool {
   384  	switch {
   385  	case a.management:
   386  		return true
   387  	case a.agent == PolicyWrite:
   388  		return true
   389  	case a.agent == PolicyRead:
   390  		return true
   391  	default:
   392  		return false
   393  	}
   394  }
   395  
   396  // AllowAgentWrite checks if write operations are allowed for an agent
   397  func (a *ACL) AllowAgentWrite() bool {
   398  	switch {
   399  	case a.management:
   400  		return true
   401  	case a.agent == PolicyWrite:
   402  		return true
   403  	default:
   404  		return false
   405  	}
   406  }
   407  
   408  // AllowNodeRead checks if read operations are allowed for a node
   409  func (a *ACL) AllowNodeRead() bool {
   410  	switch {
   411  	case a.management:
   412  		return true
   413  	case a.node == PolicyWrite:
   414  		return true
   415  	case a.node == PolicyRead:
   416  		return true
   417  	default:
   418  		return false
   419  	}
   420  }
   421  
   422  // AllowNodeWrite checks if write operations are allowed for a node
   423  func (a *ACL) AllowNodeWrite() bool {
   424  	switch {
   425  	case a.management:
   426  		return true
   427  	case a.node == PolicyWrite:
   428  		return true
   429  	default:
   430  		return false
   431  	}
   432  }
   433  
   434  // AllowOperatorRead checks if read operations are allowed for a operator
   435  func (a *ACL) AllowOperatorRead() bool {
   436  	switch {
   437  	case a.management:
   438  		return true
   439  	case a.operator == PolicyWrite:
   440  		return true
   441  	case a.operator == PolicyRead:
   442  		return true
   443  	default:
   444  		return false
   445  	}
   446  }
   447  
   448  // AllowOperatorWrite checks if write operations are allowed for a operator
   449  func (a *ACL) AllowOperatorWrite() bool {
   450  	switch {
   451  	case a.management:
   452  		return true
   453  	case a.operator == PolicyWrite:
   454  		return true
   455  	default:
   456  		return false
   457  	}
   458  }
   459  
   460  // AllowQuotaRead checks if read operations are allowed for all quotas
   461  func (a *ACL) AllowQuotaRead() bool {
   462  	switch {
   463  	case a.management:
   464  		return true
   465  	case a.quota == PolicyWrite:
   466  		return true
   467  	case a.quota == PolicyRead:
   468  		return true
   469  	default:
   470  		return false
   471  	}
   472  }
   473  
   474  // AllowQuotaWrite checks if write operations are allowed for quotas
   475  func (a *ACL) AllowQuotaWrite() bool {
   476  	switch {
   477  	case a.management:
   478  		return true
   479  	case a.quota == PolicyWrite:
   480  		return true
   481  	default:
   482  		return false
   483  	}
   484  }
   485  
   486  // AllowPluginRead checks if read operations are allowed for all plugins
   487  func (a *ACL) AllowPluginRead() bool {
   488  	switch {
   489  	// ACL is nil only if ACLs are disabled
   490  	case a == nil:
   491  		return true
   492  	case a.management:
   493  		return true
   494  	case a.plugin == PolicyRead:
   495  		return true
   496  	default:
   497  		return false
   498  	}
   499  }
   500  
   501  // AllowPluginList checks if list operations are allowed for all plugins
   502  func (a *ACL) AllowPluginList() bool {
   503  	switch {
   504  	// ACL is nil only if ACLs are disabled
   505  	case a == nil:
   506  		return true
   507  	case a.management:
   508  		return true
   509  	case a.plugin == PolicyList:
   510  		return true
   511  	case a.plugin == PolicyRead:
   512  		return true
   513  	default:
   514  		return false
   515  	}
   516  }
   517  
   518  // IsManagement checks if this represents a management token
   519  func (a *ACL) IsManagement() bool {
   520  	return a.management
   521  }
   522  
   523  // NamespaceValidator returns a func that wraps ACL.AllowNamespaceOperation in
   524  // a list of operations. Returns true (allowed) if acls are disabled or if
   525  // *any* capabilities match.
   526  func NamespaceValidator(ops ...string) func(*ACL, string) bool {
   527  	return func(acl *ACL, ns string) bool {
   528  		// Always allow if ACLs are disabled.
   529  		if acl == nil {
   530  			return true
   531  		}
   532  
   533  		for _, op := range ops {
   534  			if acl.AllowNamespaceOperation(ns, op) {
   535  				// An operation is allowed, return true
   536  				return true
   537  			}
   538  		}
   539  
   540  		// No operations are allowed by this ACL, return false
   541  		return false
   542  	}
   543  }