258 lines
6.6 KiB
Go
258 lines
6.6 KiB
Go
package nomad
|
|
|
|
import (
|
|
"fmt"
|
|
"time"
|
|
|
|
metrics "github.com/armon/go-metrics"
|
|
lru "github.com/hashicorp/golang-lru"
|
|
"github.com/hashicorp/nomad/acl"
|
|
"github.com/hashicorp/nomad/nomad/state"
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
|
)
|
|
|
|
// ResolveToken is used to translate an ACL Token Secret ID into
|
|
// an ACL object, nil if ACLs are disabled, or an error.
|
|
func (s *Server) ResolveToken(secretID string) (*acl.ACL, error) {
|
|
// Fast-path if ACLs are disabled
|
|
if !s.config.ACLEnabled {
|
|
return nil, nil
|
|
}
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "resolveToken"}, time.Now())
|
|
|
|
// Check if the secret ID is the leader secret ID, in which case treat it as
|
|
// a management token.
|
|
if leaderAcl := s.getLeaderAcl(); leaderAcl != "" && secretID == leaderAcl {
|
|
return acl.ManagementACL, nil
|
|
}
|
|
|
|
// Snapshot the state
|
|
snap, err := s.fsm.State().Snapshot()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Resolve the ACL
|
|
return resolveTokenFromSnapshotCache(snap, s.aclCache, secretID)
|
|
}
|
|
|
|
// VerifyClaim asserts that the token is valid and that the resulting
|
|
// allocation ID belongs to a non-terminal allocation
|
|
func (s *Server) VerifyClaim(token string) (*structs.IdentityClaims, error) {
|
|
|
|
claims, err := s.encrypter.VerifyClaim(token)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
snap, err := s.fsm.State().Snapshot()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
alloc, err := snap.AllocByID(nil, claims.AllocationID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if alloc == nil || alloc.Job == nil {
|
|
return nil, fmt.Errorf("allocation does not exist")
|
|
}
|
|
|
|
// the claims for terminal allocs are always treated as expired
|
|
if alloc.TerminalStatus() {
|
|
return nil, fmt.Errorf("allocation is terminal")
|
|
}
|
|
|
|
return claims, nil
|
|
}
|
|
|
|
func (s *Server) ResolveClaims(claims *structs.IdentityClaims) (*acl.ACL, error) {
|
|
|
|
policies, err := s.resolvePoliciesForClaims(claims)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(policies) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// Compile and cache the ACL object
|
|
aclObj, err := structs.CompileACLObject(s.aclCache, policies)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return aclObj, nil
|
|
}
|
|
|
|
// resolveTokenFromSnapshotCache is used to resolve an ACL object from a
|
|
// snapshot of state, using a cache to avoid parsing and ACL construction when
|
|
// possible. It is split from resolveToken to simplify testing.
|
|
func resolveTokenFromSnapshotCache(snap *state.StateSnapshot, cache *lru.TwoQueueCache, secretID string) (*acl.ACL, error) {
|
|
// Lookup the ACL Token
|
|
var token *structs.ACLToken
|
|
var err error
|
|
|
|
// Handle anonymous requests
|
|
if secretID == "" {
|
|
token = structs.AnonymousACLToken
|
|
} else {
|
|
token, err = snap.ACLTokenBySecretID(nil, secretID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if token == nil {
|
|
return nil, structs.ErrTokenNotFound
|
|
}
|
|
if token.IsExpired(time.Now().UTC()) {
|
|
return nil, structs.ErrTokenExpired
|
|
}
|
|
}
|
|
|
|
// Check if this is a management token
|
|
if token.Type == structs.ACLManagementToken {
|
|
return acl.ManagementACL, nil
|
|
}
|
|
|
|
// Store all policies detailed in the token request, this includes the
|
|
// named policies and those referenced within the role link.
|
|
policies := make([]*structs.ACLPolicy, 0, len(token.Policies)+len(token.Roles))
|
|
|
|
// Iterate all the token policies and add these to our policy tracking
|
|
// array.
|
|
for _, policyName := range token.Policies {
|
|
policy, err := snap.ACLPolicyByName(nil, policyName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if policy == nil {
|
|
// Ignore policies that don't exist, since they don't grant any
|
|
// more privilege.
|
|
continue
|
|
}
|
|
|
|
// Add the policy to the tracking array.
|
|
policies = append(policies, policy)
|
|
}
|
|
|
|
// Iterate all the token role links, so we can unpack these and identify
|
|
// the ACL policies.
|
|
for _, roleLink := range token.Roles {
|
|
|
|
// Any error reading the role means we cannot move forward. We just
|
|
// ignore any roles that have been detailed but are not within our
|
|
// state.
|
|
role, err := snap.GetACLRoleByID(nil, roleLink.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if role == nil {
|
|
continue
|
|
}
|
|
|
|
// Unpack the policies held within the ACL role to form a single list
|
|
// of ACL policies that this token has available.
|
|
for _, policyLink := range role.Policies {
|
|
policy, err := snap.ACLPolicyByName(nil, policyLink.Name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Ignore policies that don't exist, since they don't grant any
|
|
// more privilege.
|
|
if policy == nil {
|
|
continue
|
|
}
|
|
|
|
// Add the policy to the tracking array.
|
|
policies = append(policies, policy)
|
|
}
|
|
}
|
|
|
|
// Compile and cache the ACL object
|
|
aclObj, err := structs.CompileACLObject(cache, policies)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return aclObj, nil
|
|
}
|
|
|
|
// ResolveSecretToken is used to translate an ACL Token Secret ID into
|
|
// an ACLToken object, nil if ACLs are disabled, or an error.
|
|
func (s *Server) ResolveSecretToken(secretID string) (*structs.ACLToken, error) {
|
|
// TODO(Drew) Look into using ACLObject cache or create a separate cache
|
|
|
|
// Fast-path if ACLs are disabled
|
|
if !s.config.ACLEnabled {
|
|
return nil, nil
|
|
}
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "resolveSecretToken"}, time.Now())
|
|
|
|
snap, err := s.fsm.State().Snapshot()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Lookup the ACL Token
|
|
var token *structs.ACLToken
|
|
// Handle anonymous requests
|
|
if secretID == "" {
|
|
token = structs.AnonymousACLToken
|
|
} else {
|
|
token, err = snap.ACLTokenBySecretID(nil, secretID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if token == nil {
|
|
return nil, structs.ErrTokenNotFound
|
|
}
|
|
if token.IsExpired(time.Now().UTC()) {
|
|
return nil, structs.ErrTokenExpired
|
|
}
|
|
}
|
|
|
|
return token, nil
|
|
}
|
|
|
|
func (s *Server) resolvePoliciesForClaims(claims *structs.IdentityClaims) ([]*structs.ACLPolicy, error) {
|
|
|
|
snap, err := s.fsm.State().Snapshot()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
alloc, err := snap.AllocByID(nil, claims.AllocationID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if alloc == nil || alloc.Job == nil {
|
|
return nil, fmt.Errorf("allocation does not exist")
|
|
}
|
|
|
|
// Find any policies attached to the job
|
|
iter, err := snap.ACLPolicyByJob(nil, alloc.Namespace, alloc.Job.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
policies := []*structs.ACLPolicy{}
|
|
for {
|
|
raw := iter.Next()
|
|
if raw == nil {
|
|
break
|
|
}
|
|
policy := raw.(*structs.ACLPolicy)
|
|
if policy.JobACL == nil {
|
|
continue
|
|
}
|
|
|
|
switch {
|
|
case policy.JobACL.Group == "":
|
|
policies = append(policies, policy)
|
|
case policy.JobACL.Group != alloc.TaskGroup:
|
|
continue // don't bother checking task
|
|
case policy.JobACL.Task == "":
|
|
policies = append(policies, policy)
|
|
case policy.JobACL.Task == claims.TaskName:
|
|
policies = append(policies, policy)
|
|
}
|
|
}
|
|
|
|
return policies, nil
|
|
}
|