github.com/hernad/nomad@v1.6.112/nomad/consul_policy.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package nomad
     5  
     6  import (
     7  	"fmt"
     8  	"strings"
     9  
    10  	"github.com/hashicorp/consul/api"
    11  	"github.com/hashicorp/hcl"
    12  )
    13  
    14  const (
    15  	// consulGlobalManagementPolicyID is the built-in policy ID used by Consul
    16  	// to denote global-management tokens.
    17  	//
    18  	// https://www.consul.io/docs/security/acl/acl-system#builtin-policies
    19  	consulGlobalManagementPolicyID = "00000000-0000-0000-0000-000000000001"
    20  )
    21  
    22  // ConsulServiceRule represents a policy for a service.
    23  type ConsulServiceRule struct {
    24  	Name   string `hcl:",key"`
    25  	Policy string
    26  }
    27  
    28  // ConsulKeyRule represents a policy for the keystore.
    29  type ConsulKeyRule struct {
    30  	Name   string `hcl:",key"`
    31  	Policy string
    32  }
    33  
    34  // ConsulPolicy represents the parts of a ConsulServiceRule Policy that are
    35  // relevant to Service Identity authorizations.
    36  type ConsulPolicy struct {
    37  	Services          []*ConsulServiceRule     `hcl:"service,expand"`
    38  	ServicePrefixes   []*ConsulServiceRule     `hcl:"service_prefix,expand"`
    39  	KeyPrefixes       []*ConsulKeyRule         `hcl:"key_prefix,expand"`
    40  	Namespaces        map[string]*ConsulPolicy `hcl:"namespace,expand"`
    41  	NamespacePrefixes map[string]*ConsulPolicy `hcl:"namespace_prefix,expand"`
    42  }
    43  
    44  // parseConsulPolicy parses raw string s into a ConsulPolicy. An error is
    45  // returned if decoding the policy fails, or if the decoded policy has no
    46  // Services or ServicePrefixes defined.
    47  func parseConsulPolicy(s string) (*ConsulPolicy, error) {
    48  	cp := new(ConsulPolicy)
    49  	if err := hcl.Decode(cp, s); err != nil {
    50  		return nil, fmt.Errorf("failed to parse ACL policy: %w", err)
    51  	}
    52  	return cp, nil
    53  }
    54  
    55  // isManagementToken returns true if the Consul token is backed by the
    56  // built-in global-management policy. Such a token has complete, unrestricted
    57  // access to all of Consul.
    58  //
    59  // https://www.consul.io/docs/security/acl/acl-system#builtin-policies
    60  func (c *consulACLsAPI) isManagementToken(token *api.ACLToken) bool {
    61  	if token == nil {
    62  		return false
    63  	}
    64  
    65  	for _, policy := range token.Policies {
    66  		if policy.ID == consulGlobalManagementPolicyID {
    67  			return true
    68  		}
    69  	}
    70  	return false
    71  }
    72  
    73  // namespaceCheck is used to fail the request if the namespace of the object does
    74  // not match the namespace of the ACL token provided.
    75  //
    76  // *exception*: if token is in the default namespace, it may contain policies
    77  // that extend into other namespaces using namespace_prefix, which must bypass
    78  // this early check and validate in the service/keystore helpers
    79  //
    80  // *exception*: if token is not in a namespace, consul namespaces are not enabled
    81  // and there is nothing to validate
    82  //
    83  // If the namespaces match, whether the token is allowed to perform an operation
    84  // is checked later.
    85  func namespaceCheck(namespace string, token *api.ACLToken) error {
    86  
    87  	switch {
    88  	case namespace == token.Namespace:
    89  		// ACLs enabled, namespaces are the same
    90  		return nil
    91  
    92  	case token.Namespace == "default":
    93  		// ACLs enabled, must defer to per-object checking, since the token could
    94  		// have namespace or namespace_prefix blocks with extended policies that
    95  		// allow an operation. Using namespace or namespace_prefix blocks is only
    96  		// applicable to tokens in the "default" namespace.
    97  		//
    98  		// https://www.consul.io/docs/security/acl/acl-rules#namespace-rules
    99  		return nil
   100  
   101  	case namespace == "" && token.Namespace != "default":
   102  		// ACLs enabled with non-default token, but namespace on job not set, so
   103  		// provide a more informative error message.
   104  		return fmt.Errorf("consul ACL token requires using namespace %q", token.Namespace)
   105  
   106  	default:
   107  		return fmt.Errorf("consul ACL token cannot use namespace %q", namespace)
   108  	}
   109  }
   110  
   111  func (c *consulACLsAPI) canReadKeystore(namespace string, token *api.ACLToken) (bool, error) {
   112  	// early check the token is compatible with desired namespace
   113  	if err := namespaceCheck(namespace, token); err != nil {
   114  		return false, nil
   115  	}
   116  
   117  	// determines whether a top-level ACL policy will be applicable
   118  	//
   119  	// if the namespace is not set in the job and the token is in the default namespace,
   120  	// treat that like an exact match to preserve backwards compatibility
   121  	matches := (namespace == token.Namespace) || (namespace == "" && token.Namespace == "default")
   122  
   123  	// check each policy directly attached to the token
   124  	for _, policyRef := range token.Policies {
   125  		if allowable, err := c.policyAllowsKeystoreRead(matches, namespace, policyRef.ID); err != nil {
   126  			return false, err
   127  		} else if allowable {
   128  			return true, nil
   129  		}
   130  	}
   131  
   132  	// check each policy on each role attached to the token
   133  	for _, roleLink := range token.Roles {
   134  		role, _, err := c.aclClient.RoleRead(roleLink.ID, &api.QueryOptions{
   135  			AllowStale: false,
   136  		})
   137  		if err != nil {
   138  			return false, err
   139  		}
   140  
   141  		for _, policyLink := range role.Policies {
   142  			allowable, err := c.policyAllowsKeystoreRead(matches, namespace, policyLink.ID)
   143  			if err != nil {
   144  				return false, err
   145  			} else if allowable {
   146  				return true, nil
   147  			}
   148  		}
   149  	}
   150  
   151  	return false, nil
   152  }
   153  
   154  func (c *consulACLsAPI) canWriteService(namespace, service string, token *api.ACLToken) (bool, error) {
   155  	// early check the token is compatible with desired namespace
   156  	if err := namespaceCheck(namespace, token); err != nil {
   157  		return false, nil
   158  	}
   159  
   160  	// determines whether a top-level ACL policy will be applicable
   161  	//
   162  	// if the namespace is not set in the job and the token is in the default namespace,
   163  	// treat that like an exact match to preserve backwards compatibility
   164  	matches := (namespace == token.Namespace) || (namespace == "" && token.Namespace == "default")
   165  
   166  	// check each service identity attached to the token -
   167  	// the virtual policy for service identities enables service:write
   168  	for _, si := range token.ServiceIdentities {
   169  		if si.ServiceName == service {
   170  			return true, nil
   171  		}
   172  	}
   173  
   174  	// check each policy directly attached to the token
   175  	for _, policyRef := range token.Policies {
   176  		if allowable, err := c.policyAllowsServiceWrite(matches, namespace, service, policyRef.ID); err != nil {
   177  			return false, err
   178  		} else if allowable {
   179  			return true, nil
   180  		}
   181  	}
   182  
   183  	// check each policy on each role attached to the token
   184  	for _, roleLink := range token.Roles {
   185  		role, _, err := c.aclClient.RoleRead(roleLink.ID, &api.QueryOptions{
   186  			AllowStale: false,
   187  		})
   188  		if err != nil {
   189  			return false, err
   190  		}
   191  
   192  		for _, policyLink := range role.Policies {
   193  			allowable, wErr := c.policyAllowsServiceWrite(matches, namespace, service, policyLink.ID)
   194  			if wErr != nil {
   195  				return false, wErr
   196  			} else if allowable {
   197  				return true, nil
   198  			}
   199  		}
   200  	}
   201  
   202  	return false, nil
   203  }
   204  
   205  func (c *consulACLsAPI) policyAllowsServiceWrite(matches bool, namespace, service string, policyID string) (bool, error) {
   206  	policy, _, err := c.aclClient.PolicyRead(policyID, &api.QueryOptions{
   207  		AllowStale: false,
   208  	})
   209  	if err != nil {
   210  		return false, err
   211  	}
   212  
   213  	// compare policy to the necessary permission for service write
   214  	// e.g. service "db" { policy = "write" }
   215  	// e.g. service_prefix "" { policy == "write" }
   216  	cp, err := parseConsulPolicy(policy.Rules)
   217  	if err != nil {
   218  		return false, err
   219  	}
   220  
   221  	if cp.allowsServiceWrite(matches, namespace, service) {
   222  		return true, nil
   223  	}
   224  
   225  	return false, nil
   226  }
   227  
   228  const (
   229  	serviceNameWildcard = "*"
   230  )
   231  
   232  func (cp *ConsulPolicy) allowsServiceWrite(matches bool, namespace, task string) bool {
   233  	canWriteService := func(services []*ConsulServiceRule) bool {
   234  		for _, service := range services {
   235  			name := strings.ToLower(service.Name)
   236  			policy := strings.ToLower(service.Policy)
   237  			if policy == ConsulPolicyWrite {
   238  				if name == task || name == serviceNameWildcard {
   239  					return true
   240  				}
   241  			}
   242  		}
   243  		return false
   244  	}
   245  
   246  	canWriteServicePrefix := func(services []*ConsulServiceRule) bool {
   247  		for _, servicePrefix := range services {
   248  			prefix := strings.ToLower(servicePrefix.Name)
   249  			policy := strings.ToLower(servicePrefix.Policy)
   250  			if policy == ConsulPolicyWrite {
   251  				if strings.HasPrefix(task, prefix) {
   252  					return true
   253  				}
   254  			}
   255  		}
   256  		return false
   257  	}
   258  
   259  	if matches {
   260  		// check the top-level service/service_prefix rules
   261  		if canWriteService(cp.Services) || canWriteServicePrefix(cp.ServicePrefixes) {
   262  			return true
   263  		}
   264  	}
   265  
   266  	// for each namespace rule, if that namespace and the desired namespace
   267  	// are a match, we can then check the service/service_prefix policy rules
   268  	for ns, policy := range cp.Namespaces {
   269  		if ns == namespace {
   270  			if canWriteService(policy.Services) || canWriteServicePrefix(policy.ServicePrefixes) {
   271  				return true
   272  			}
   273  		}
   274  	}
   275  
   276  	// for each namespace_prefix rule, see if that namespace_prefix applies
   277  	// to this namespace, and if yes, also check those service/service_prefix
   278  	// policy rules
   279  	for prefix, policy := range cp.NamespacePrefixes {
   280  		if strings.HasPrefix(namespace, prefix) {
   281  			if canWriteService(policy.Services) || canWriteServicePrefix(policy.ServicePrefixes) {
   282  				return true
   283  			}
   284  		}
   285  	}
   286  
   287  	return false
   288  }
   289  
   290  func (c *consulACLsAPI) policyAllowsKeystoreRead(matches bool, namespace, policyID string) (bool, error) {
   291  	policy, _, err := c.aclClient.PolicyRead(policyID, &api.QueryOptions{
   292  		AllowStale: false,
   293  	})
   294  	if err != nil {
   295  		return false, err
   296  	}
   297  
   298  	cp, err := parseConsulPolicy(policy.Rules)
   299  	if err != nil {
   300  		return false, err
   301  	}
   302  
   303  	if cp.allowsKeystoreRead(matches, namespace) {
   304  		return true, nil
   305  	}
   306  
   307  	return false, nil
   308  }
   309  
   310  func (cp *ConsulPolicy) allowsKeystoreRead(matches bool, namespace string) bool {
   311  	canReadKeystore := func(prefixes []*ConsulKeyRule) bool {
   312  		for _, keyPrefix := range prefixes {
   313  			name := strings.ToLower(keyPrefix.Name)
   314  			policy := strings.ToLower(keyPrefix.Policy)
   315  			if name == "" {
   316  				if policy == ConsulPolicyWrite || policy == ConsulPolicyRead {
   317  					return true
   318  				}
   319  			}
   320  		}
   321  		return false
   322  	}
   323  
   324  	// check the top-level key_prefix rules, but only if the desired namespace
   325  	// matches the namespace of the consul acl token
   326  	if matches && canReadKeystore(cp.KeyPrefixes) {
   327  		return true
   328  	}
   329  
   330  	// for each namespace rule, if that namespace matches the desired namespace
   331  	// we chan then check the keystore policy
   332  	for ns, policy := range cp.Namespaces {
   333  		if ns == namespace {
   334  			if canReadKeystore(policy.KeyPrefixes) {
   335  				return true
   336  			}
   337  		}
   338  	}
   339  
   340  	// for each namespace_prefix rule, see if that namespace_prefix applies to
   341  	// this namespace, and if yes, also check those key_prefix policy rules
   342  	for prefix, policy := range cp.NamespacePrefixes {
   343  		if strings.HasPrefix(namespace, prefix) {
   344  			if canReadKeystore(policy.KeyPrefixes) {
   345  				return true
   346  			}
   347  		}
   348  	}
   349  
   350  	return false
   351  }