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 }