3052ddf8f1
When a user performs a client API call, the Nomad client will perform an RPC which looks up the ACL policies which the callers ACL token is assigned. If the ACL token includes dangling (deleted) policies, the call would previously fail with a permission denied error. This change ensures this error is not returned and that the lookup will succeed in the event of dangling policies. Co-authored-by: James Rasell <jrasell@users.noreply.github.com>
2955 lines
91 KiB
Go
2955 lines
91 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package nomad
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/armon/go-metrics"
|
|
capOIDC "github.com/hashicorp/cap/oidc"
|
|
"github.com/hashicorp/go-hclog"
|
|
"github.com/hashicorp/go-memdb"
|
|
"github.com/hashicorp/go-set"
|
|
|
|
policy "github.com/hashicorp/nomad/acl"
|
|
"github.com/hashicorp/nomad/helper"
|
|
"github.com/hashicorp/nomad/helper/uuid"
|
|
"github.com/hashicorp/nomad/lib/auth"
|
|
"github.com/hashicorp/nomad/lib/auth/jwt"
|
|
"github.com/hashicorp/nomad/lib/auth/oidc"
|
|
"github.com/hashicorp/nomad/nomad/state"
|
|
"github.com/hashicorp/nomad/nomad/state/paginator"
|
|
"github.com/hashicorp/nomad/nomad/structs"
|
|
)
|
|
|
|
var (
|
|
// aclDisabled is returned when an ACL endpoint is hit but ACLs are not enabled
|
|
aclDisabled = structs.NewErrRPCCoded(400, "ACL support disabled")
|
|
)
|
|
|
|
const (
|
|
// aclBootstrapReset is the file name to create in the data dir. It's only contents
|
|
// should be the reset index
|
|
aclBootstrapReset = "acl-bootstrap-reset"
|
|
|
|
// aclOIDCAuthURLRequestExpiryTime is the deadline used when generating an
|
|
// OIDC provider authentication URL. This is used for HTTP requests to
|
|
// external APIs.
|
|
aclOIDCAuthURLRequestExpiryTime = 60 * time.Second
|
|
|
|
// aclOIDCCallbackRequestExpiryTime is the deadline used when obtaining an
|
|
// OIDC provider token. This is used for HTTP requests to external APIs.
|
|
aclOIDCCallbackRequestExpiryTime = 60 * time.Second
|
|
|
|
// aclLoginRequestExpiryTime is the deadline used when performing HTTP
|
|
// requests to external APIs during the validation of bearer tokens.
|
|
aclLoginRequestExpiryTime = 60 * time.Second
|
|
)
|
|
|
|
// ACL endpoint is used for manipulating ACL tokens and policies
|
|
type ACL struct {
|
|
srv *Server
|
|
ctx *RPCContext
|
|
logger hclog.Logger
|
|
|
|
// oidcProviderCache is a cache of OIDC providers as defined by the
|
|
// hashicorp/cap library. When performing an OIDC login flow, this cache
|
|
// should be used to obtain a provider from an auth-method.
|
|
oidcProviderCache *oidc.ProviderCache
|
|
}
|
|
|
|
func NewACLEndpoint(srv *Server, ctx *RPCContext) *ACL {
|
|
return &ACL{
|
|
srv: srv,
|
|
ctx: ctx,
|
|
logger: srv.logger.Named("acl"),
|
|
oidcProviderCache: srv.oidcProviderCache,
|
|
}
|
|
}
|
|
|
|
// UpsertPolicies is used to create or update a set of policies
|
|
func (a *ACL) UpsertPolicies(args *structs.ACLPolicyUpsertRequest, reply *structs.GenericResponse) error {
|
|
// Ensure ACLs are enabled, and always flow modification requests to the authoritative region
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
authErr := a.srv.Authenticate(a.ctx, args)
|
|
args.Region = a.srv.config.AuthoritativeRegion
|
|
|
|
if done, err := a.srv.forward("ACL.UpsertPolicies", args, args, reply); done {
|
|
return err
|
|
}
|
|
a.srv.MeasureRPCRate("acl", structs.RateMetricWrite, args)
|
|
if authErr != nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "upsert_policies"}, time.Now())
|
|
|
|
// Check management level permissions
|
|
if acl, err := a.srv.ResolveACL(args); err != nil {
|
|
return err
|
|
} else if acl == nil || !acl.IsManagement() {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
|
|
// Validate non-zero set of policies
|
|
if len(args.Policies) == 0 {
|
|
return structs.NewErrRPCCoded(400, "must specify as least one policy")
|
|
}
|
|
|
|
// Validate each policy, compute hash
|
|
for idx, policy := range args.Policies {
|
|
if err := policy.Validate(); err != nil {
|
|
return structs.NewErrRPCCodedf(http.StatusBadRequest, "policy %d invalid: %v", idx, err)
|
|
}
|
|
policy.SetHash()
|
|
}
|
|
|
|
// Update via Raft
|
|
_, index, err := a.srv.raftApply(structs.ACLPolicyUpsertRequestType, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update the index
|
|
reply.Index = index
|
|
return nil
|
|
}
|
|
|
|
// DeletePolicies is used to delete policies
|
|
func (a *ACL) DeletePolicies(args *structs.ACLPolicyDeleteRequest, reply *structs.GenericResponse) error {
|
|
// Ensure ACLs are enabled, and always flow modification requests to the authoritative region
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
authErr := a.srv.Authenticate(a.ctx, args)
|
|
args.Region = a.srv.config.AuthoritativeRegion
|
|
|
|
if done, err := a.srv.forward("ACL.DeletePolicies", args, args, reply); done {
|
|
return err
|
|
}
|
|
a.srv.MeasureRPCRate("acl", structs.RateMetricWrite, args)
|
|
if authErr != nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "delete_policies"}, time.Now())
|
|
|
|
// Check management level permissions
|
|
if acl, err := a.srv.ResolveACL(args); err != nil {
|
|
return err
|
|
} else if acl == nil || !acl.IsManagement() {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
|
|
// Validate non-zero set of policies
|
|
if len(args.Names) == 0 {
|
|
return structs.NewErrRPCCoded(400, "must specify as least one policy")
|
|
}
|
|
|
|
// Update via Raft
|
|
_, index, err := a.srv.raftApply(structs.ACLPolicyDeleteRequestType, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update the index
|
|
reply.Index = index
|
|
return nil
|
|
}
|
|
|
|
// ListPolicies is used to list the policies
|
|
func (a *ACL) ListPolicies(args *structs.ACLPolicyListRequest, reply *structs.ACLPolicyListResponse) error {
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
authErr := a.srv.Authenticate(a.ctx, args)
|
|
if done, err := a.srv.forward("ACL.ListPolicies", args, args, reply); done {
|
|
return err
|
|
}
|
|
a.srv.MeasureRPCRate("acl", structs.RateMetricList, args)
|
|
if authErr != nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "list_policies"}, time.Now())
|
|
|
|
// Check management level permissions
|
|
acl, err := a.srv.ResolveACL(args)
|
|
if err != nil {
|
|
return err
|
|
} else if acl == nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
|
|
// If it is not a management token determine the policies that may be listed
|
|
mgt := acl.IsManagement()
|
|
tokenPolicyNames := set.New[string](0)
|
|
if !mgt {
|
|
token, err := a.requestACLToken(args.AuthToken)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if token == nil {
|
|
return structs.ErrTokenNotFound
|
|
}
|
|
|
|
// Generate a set of policy names. This is initially generated from the
|
|
// ACL role links.
|
|
tokenPolicyNames, err = a.policyNamesFromRoleLinks(token.Roles)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add the token policies which are directly referenced into the set.
|
|
tokenPolicyNames.InsertAll(token.Policies)
|
|
}
|
|
|
|
// Setup the blocking query
|
|
opts := blockingOptions{
|
|
queryOpts: &args.QueryOptions,
|
|
queryMeta: &reply.QueryMeta,
|
|
run: func(ws memdb.WatchSet, state *state.StateStore) error {
|
|
// Iterate over all the policies
|
|
var err error
|
|
var iter memdb.ResultIterator
|
|
if prefix := args.QueryOptions.Prefix; prefix != "" {
|
|
iter, err = state.ACLPolicyByNamePrefix(ws, prefix)
|
|
} else {
|
|
iter, err = state.ACLPolicies(ws)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Convert all the policies to a list stub
|
|
reply.Policies = nil
|
|
for {
|
|
raw := iter.Next()
|
|
if raw == nil {
|
|
break
|
|
}
|
|
realPolicy := raw.(*structs.ACLPolicy)
|
|
if mgt || tokenPolicyNames.Contains(realPolicy.Name) {
|
|
reply.Policies = append(reply.Policies, realPolicy.Stub())
|
|
}
|
|
}
|
|
|
|
// Use the last index that affected the policy table
|
|
index, err := state.Index("acl_policy")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Ensure we never set the index to zero, otherwise a blocking query cannot be used.
|
|
// We floor the index at one, since realistically the first write must have a higher index.
|
|
if index == 0 {
|
|
index = 1
|
|
}
|
|
reply.Index = index
|
|
return nil
|
|
}}
|
|
return a.srv.blockingRPC(&opts)
|
|
}
|
|
|
|
// GetPolicy is used to get a specific policy
|
|
func (a *ACL) GetPolicy(args *structs.ACLPolicySpecificRequest, reply *structs.SingleACLPolicyResponse) error {
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
|
|
authErr := a.srv.Authenticate(a.ctx, args)
|
|
if done, err := a.srv.forward("ACL.GetPolicy", args, args, reply); done {
|
|
return err
|
|
}
|
|
a.srv.MeasureRPCRate("acl", structs.RateMetricRead, args)
|
|
if authErr != nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "get_policy"}, time.Now())
|
|
|
|
// Check management level permissions
|
|
acl, err := a.srv.ResolveACL(args)
|
|
if err != nil {
|
|
return err
|
|
} else if acl == nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
|
|
// If the policy is the anonymous one, anyone can get it
|
|
// If it is not a management token determine if it can get this policy
|
|
mgt := acl.IsManagement()
|
|
if !mgt && args.Name != "anonymous" {
|
|
token, err := a.requestACLToken(args.AuthToken)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if token == nil {
|
|
return structs.ErrTokenNotFound
|
|
}
|
|
|
|
// Generate a set of policy names. This is initially generated from the
|
|
// ACL role links.
|
|
tokenPolicyNames, err := a.policyNamesFromRoleLinks(token.Roles)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add the token policies which are directly referenced into the set.
|
|
tokenPolicyNames.InsertAll(token.Policies)
|
|
|
|
if !tokenPolicyNames.Contains(args.Name) {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
}
|
|
|
|
// Setup the blocking query
|
|
opts := blockingOptions{
|
|
queryOpts: &args.QueryOptions,
|
|
queryMeta: &reply.QueryMeta,
|
|
run: func(ws memdb.WatchSet, state *state.StateStore) error {
|
|
// Look for the policy
|
|
out, err := state.ACLPolicyByName(ws, args.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Setup the output
|
|
reply.Policy = out
|
|
if out != nil {
|
|
reply.Index = out.ModifyIndex
|
|
rules, err := policy.Parse(out.Rules)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reply.Policy.RulesJSON = rules
|
|
} else {
|
|
// Use the last index that affected the policy table
|
|
index, err := state.Index("acl_policy")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reply.Index = index
|
|
}
|
|
return nil
|
|
}}
|
|
return a.srv.blockingRPC(&opts)
|
|
}
|
|
|
|
func (a *ACL) requestACLToken(secretID string) (*structs.ACLToken, error) {
|
|
if secretID == "" {
|
|
return structs.AnonymousACLToken, nil
|
|
}
|
|
|
|
snap, err := a.srv.fsm.State().Snapshot()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return snap.ACLTokenBySecretID(nil, secretID)
|
|
}
|
|
|
|
// GetPolicies is used to get a set of policies
|
|
func (a *ACL) GetPolicies(args *structs.ACLPolicySetRequest, reply *structs.ACLPolicySetResponse) error {
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
authErr := a.srv.Authenticate(a.ctx, args)
|
|
if done, err := a.srv.forward("ACL.GetPolicies", args, args, reply); done {
|
|
return err
|
|
}
|
|
a.srv.MeasureRPCRate("acl", structs.RateMetricList, args)
|
|
if authErr != nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "get_policies"}, time.Now())
|
|
|
|
// For client typed tokens, allow them to query any policies associated with that token.
|
|
// This is used by clients which are resolving the policies to enforce. Any associated
|
|
// policies need to be fetched so that the client can determine what to allow.
|
|
token := args.GetIdentity().GetACLToken()
|
|
if token == nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
|
|
// Generate a set of policy names. This is initially generated from the
|
|
// ACL role links.
|
|
tokenPolicyNames, err := a.policyNamesFromRoleLinks(token.Roles)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Add the token policies which are directly referenced into the set.
|
|
tokenPolicyNames.InsertAll(token.Policies)
|
|
|
|
// Setup the blocking query
|
|
opts := blockingOptions{
|
|
queryOpts: &args.QueryOptions,
|
|
queryMeta: &reply.QueryMeta,
|
|
run: func(ws memdb.WatchSet, state *state.StateStore) error {
|
|
// Setup the output
|
|
reply.Policies = make(map[string]*structs.ACLPolicy)
|
|
|
|
// Look for the policy and check whether the caller has the
|
|
// permission to view it. This endpoint is used by the replication
|
|
// process, or Nomad clients looking up a callers policies. It is
|
|
// therefore safe, to perform auth checks here and ensures we do
|
|
// not return erroneous permission denied errors when a caller uses
|
|
// a token with dangling policies.
|
|
for _, policyName := range args.Names {
|
|
out, err := state.ACLPolicyByName(ws, policyName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if out == nil {
|
|
continue
|
|
}
|
|
|
|
if token.Type != structs.ACLManagementToken && !tokenPolicyNames.Contains(policyName) {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
reply.Policies[policyName] = out
|
|
}
|
|
|
|
// Use the last index that affected the policy table
|
|
index, err := state.Index("acl_policy")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reply.Index = index
|
|
return nil
|
|
}}
|
|
return a.srv.blockingRPC(&opts)
|
|
}
|
|
|
|
// GetClaimPolicies return the ACLPolicy objects for a workload identity.
|
|
// Similar to GetPolicies except an error will *not* be returned if ACLs are
|
|
// disabled.
|
|
func (a *ACL) GetClaimPolicies(args *structs.GenericRequest, reply *structs.ACLPolicySetResponse) error {
|
|
authErr := a.srv.Authenticate(a.ctx, args)
|
|
if done, err := a.srv.forward("ACL.GetClaimPolicies", args, args, reply); done {
|
|
return err
|
|
}
|
|
a.srv.MeasureRPCRate("acl", structs.RateMetricList, args)
|
|
if authErr != nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "get_claim_policies"}, time.Now())
|
|
|
|
// Should only be called using a workload identity
|
|
claims := args.GetIdentity().Claims
|
|
if claims == nil {
|
|
// Calling this RPC without a workload identity is either a bug or an
|
|
// attacker as this RPC is not exposed to users directly.
|
|
a.logger.Debug("ACL.GetClaimPolicies called without a workload identity", "id", args.GetIdentity())
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
|
|
policies, err := a.srv.resolvePoliciesForClaims(claims)
|
|
if err != nil {
|
|
// Likely only hit if a job/alloc has been GC'd on the server but the
|
|
// client hasn't stopped it yet. Return Permission Denied as there's no way
|
|
// this call should error that leaves the claims valid.
|
|
a.logger.Warn("Policies could not be resolved for claims", "error", err, "id", args.GetIdentity())
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
|
|
reply.Policies = make(map[string]*structs.ACLPolicy, len(policies))
|
|
for _, p := range policies {
|
|
if p.ModifyIndex > reply.QueryMeta.Index {
|
|
reply.QueryMeta.Index = p.ModifyIndex
|
|
}
|
|
reply.Policies[p.Name] = p
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Bootstrap is used to bootstrap the initial token
|
|
func (a *ACL) Bootstrap(args *structs.ACLTokenBootstrapRequest, reply *structs.ACLTokenUpsertResponse) error {
|
|
// Ensure ACLs are enabled, and always flow modification requests to the authoritative region
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
args.Region = a.srv.config.AuthoritativeRegion
|
|
providedTokenID := args.BootstrapSecret
|
|
|
|
// note: we're intentionally throwing away any auth error here and only
|
|
// authenticate so that we can measure rate metrics
|
|
a.srv.Authenticate(a.ctx, args)
|
|
if done, err := a.srv.forward("ACL.Bootstrap", args, args, reply); done {
|
|
return err
|
|
}
|
|
a.srv.MeasureRPCRate("acl", structs.RateMetricWrite, args)
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "bootstrap"}, time.Now())
|
|
|
|
// Always ignore the reset index from the arguments
|
|
args.ResetIndex = 0
|
|
|
|
// Snapshot the state
|
|
state, err := a.srv.State().Snapshot()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Verify bootstrap is possible. The state store method re-verifies this,
|
|
// but we do an early check to avoid raft transactions when possible.
|
|
ok, resetIdx, err := state.CanBootstrapACLToken()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !ok {
|
|
// Check if there is a reset index specified
|
|
specifiedIndex := a.fileBootstrapResetIndex()
|
|
if specifiedIndex == 0 {
|
|
return structs.NewErrRPCCodedf(400, "ACL bootstrap already done (reset index: %d)", resetIdx)
|
|
} else if specifiedIndex != resetIdx {
|
|
return structs.NewErrRPCCodedf(400, "Invalid bootstrap reset index (specified %d, reset index: %d)", specifiedIndex, resetIdx)
|
|
}
|
|
|
|
// Setup the reset index to allow bootstrapping again
|
|
args.ResetIndex = resetIdx
|
|
}
|
|
|
|
// Create a new global management token, override any parameter
|
|
args.Token = &structs.ACLToken{
|
|
AccessorID: uuid.Generate(),
|
|
SecretID: uuid.Generate(),
|
|
Name: "Bootstrap Token",
|
|
Type: structs.ACLManagementToken,
|
|
Global: true,
|
|
CreateTime: time.Now().UTC(),
|
|
}
|
|
|
|
// if a token has been passed in from the API overwrite the generated one.
|
|
if providedTokenID != "" {
|
|
if helper.IsUUID(providedTokenID) {
|
|
args.Token.SecretID = providedTokenID
|
|
} else {
|
|
return structs.NewErrRPCCodedf(400, "invalid acl token")
|
|
}
|
|
}
|
|
|
|
args.Token.SetHash()
|
|
|
|
// Update via Raft
|
|
_, index, err := a.srv.raftApply(structs.ACLTokenBootstrapRequestType, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Populate the response. We do a lookup against the state to
|
|
// pickup the proper create / modify times.
|
|
state, err = a.srv.State().Snapshot()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
out, err := state.ACLTokenByAccessorID(nil, args.Token.AccessorID)
|
|
if err != nil {
|
|
return structs.NewErrRPCCodedf(400, "token lookup failed: %v", err)
|
|
}
|
|
reply.Tokens = append(reply.Tokens, out)
|
|
|
|
// Update the index
|
|
reply.Index = index
|
|
return nil
|
|
}
|
|
|
|
// fileBootstrapResetIndex is used to read the reset file from <data-dir>/acl-bootstrap-reset
|
|
func (a *ACL) fileBootstrapResetIndex() uint64 {
|
|
// Determine the file path to check
|
|
path := filepath.Join(a.srv.config.DataDir, aclBootstrapReset)
|
|
|
|
// Read the file
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
if !os.IsNotExist(err) {
|
|
a.logger.Error("failed to read bootstrap file", "path", path, "error", err)
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// Attempt to parse the file
|
|
var resetIdx uint64
|
|
if _, err := fmt.Sscanf(string(raw), "%d", &resetIdx); err != nil {
|
|
a.logger.Error("failed to parse bootstrap file", "path", path, "error", err)
|
|
return 0
|
|
}
|
|
|
|
// Return the reset index
|
|
a.logger.Warn("bootstrap file parsed", "path", path, "reset_index", resetIdx)
|
|
return resetIdx
|
|
}
|
|
|
|
// UpsertTokens is used to create or update a set of tokens
|
|
func (a *ACL) UpsertTokens(args *structs.ACLTokenUpsertRequest, reply *structs.ACLTokenUpsertResponse) error {
|
|
// Ensure ACLs are enabled, and always flow modification requests to the authoritative region
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
authErr := a.srv.Authenticate(a.ctx, args)
|
|
// Validate non-zero set of tokens
|
|
if len(args.Tokens) == 0 {
|
|
return structs.NewErrRPCCoded(http.StatusBadRequest, "must specify as least one token")
|
|
}
|
|
|
|
// Force the request to the authoritative region if we are creating global tokens
|
|
hasGlobal := false
|
|
allGlobal := true
|
|
for _, token := range args.Tokens {
|
|
if token.Global {
|
|
hasGlobal = true
|
|
} else {
|
|
allGlobal = false
|
|
}
|
|
}
|
|
|
|
// Disallow mixed requests with global and non-global tokens since we forward
|
|
// the entire request as a single batch.
|
|
if hasGlobal {
|
|
if !allGlobal {
|
|
return structs.NewErrRPCCoded(http.StatusBadRequest,
|
|
"cannot upsert mixed global and non-global tokens")
|
|
}
|
|
|
|
// Force the request to the authoritative region if it has global
|
|
args.Region = a.srv.config.AuthoritativeRegion
|
|
}
|
|
|
|
if done, err := a.srv.forward(structs.ACLUpsertTokensRPCMethod, args, args, reply); done {
|
|
return err
|
|
}
|
|
a.srv.MeasureRPCRate("acl", structs.RateMetricWrite, args)
|
|
if authErr != nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "upsert_tokens"}, time.Now())
|
|
|
|
// Check management level permissions
|
|
if acl, err := a.srv.ResolveACL(args); err != nil {
|
|
return err
|
|
} else if acl == nil || !acl.IsManagement() {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
|
|
// Snapshot the state so we can perform lookups against the accessor ID if
|
|
// needed. Do it here, so we only need to do this once no matter how many
|
|
// tokens we are upserting.
|
|
stateSnapshot, err := a.srv.State().Snapshot()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return a.upsertTokens(args, reply, stateSnapshot)
|
|
}
|
|
|
|
// upsertTokens is a method that contains common token upsertion logic without
|
|
// the RPC authentication, metrics, etc. Used in other RPC calls that require to
|
|
// upsert tokens.
|
|
func (a *ACL) upsertTokens(
|
|
args *structs.ACLTokenUpsertRequest,
|
|
reply *structs.ACLTokenUpsertResponse,
|
|
stateSnapshot *state.StateSnapshot,
|
|
) error {
|
|
|
|
// Validate each token
|
|
for idx, token := range args.Tokens {
|
|
|
|
// Store any existing token found, so we can perform the correct update
|
|
// validation.
|
|
var existingToken *structs.ACLToken
|
|
|
|
// If the token is being updated, perform a lookup so can can validate
|
|
// the new changes against the old.
|
|
if token.AccessorID != "" {
|
|
out, err := stateSnapshot.ACLTokenByAccessorID(nil, token.AccessorID)
|
|
if err != nil {
|
|
return structs.NewErrRPCCodedf(http.StatusInternalServerError, "token lookup failed: %v", err)
|
|
}
|
|
if out == nil {
|
|
return structs.NewErrRPCCodedf(http.StatusBadRequest, "cannot find token %s", token.AccessorID)
|
|
}
|
|
existingToken = out
|
|
}
|
|
|
|
// Canonicalize sets information needed by the validation function, so
|
|
// this order must be maintained.
|
|
token.Canonicalize()
|
|
|
|
if err := token.Validate(a.srv.config.ACLTokenMinExpirationTTL,
|
|
a.srv.config.ACLTokenMaxExpirationTTL, existingToken); err != nil {
|
|
return structs.NewErrRPCCodedf(http.StatusBadRequest, "token %d invalid: %v", idx, err)
|
|
}
|
|
|
|
var normalizedRoleLinks []*structs.ACLTokenRoleLink
|
|
uniqueRoleIDs := make(map[string]struct{})
|
|
|
|
// Iterate, check, and normalize the ACL role links that the token has.
|
|
for _, roleLink := range token.Roles {
|
|
|
|
var (
|
|
existing *structs.ACLRole
|
|
roleIdentifier string
|
|
lookupErr error
|
|
)
|
|
|
|
// In the event the caller specified the role name, we need to
|
|
// identify the immutable ID. In either case, we need to ensure the
|
|
// role exists.
|
|
switch roleLink.ID {
|
|
case "":
|
|
roleIdentifier = roleLink.Name
|
|
existing, lookupErr = stateSnapshot.GetACLRoleByName(nil, roleIdentifier)
|
|
default:
|
|
roleIdentifier = roleLink.ID
|
|
existing, lookupErr = stateSnapshot.GetACLRoleByID(nil, roleIdentifier)
|
|
}
|
|
|
|
// Handle any state lookup error or inability to locate the role
|
|
// within state.
|
|
if lookupErr != nil {
|
|
return structs.NewErrRPCCodedf(http.StatusInternalServerError, "role lookup failed: %v", lookupErr)
|
|
}
|
|
if existing == nil {
|
|
return structs.NewErrRPCCodedf(http.StatusBadRequest, "cannot find role %s", roleIdentifier)
|
|
}
|
|
|
|
// Ensure the role ID is written to the object and that the name is
|
|
// emptied as it is possible the role name is updated in the future.
|
|
roleLink.ID = existing.ID
|
|
roleLink.Name = ""
|
|
|
|
// Deduplicate role links by their ID.
|
|
if _, ok := uniqueRoleIDs[roleLink.ID]; !ok {
|
|
normalizedRoleLinks = append(normalizedRoleLinks, roleLink)
|
|
uniqueRoleIDs[roleLink.ID] = struct{}{}
|
|
}
|
|
}
|
|
|
|
// Write the normalized array of ACL role links back to the token.
|
|
token.Roles = normalizedRoleLinks
|
|
|
|
// Compute the token hash
|
|
token.SetHash()
|
|
}
|
|
|
|
// Update via Raft
|
|
_, index, err := a.srv.raftApply(structs.ACLTokenUpsertRequestType, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Populate the response. We do a lookup against the state to pick up the
|
|
// proper create / modify times.
|
|
stateSnapshot, err = a.srv.State().Snapshot()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, token := range args.Tokens {
|
|
out, err := stateSnapshot.ACLTokenByAccessorID(nil, token.AccessorID)
|
|
if err != nil {
|
|
return structs.NewErrRPCCodedf(400, "token lookup failed: %v", err)
|
|
}
|
|
reply.Tokens = append(reply.Tokens, out)
|
|
}
|
|
|
|
// Update the index
|
|
reply.Index = index
|
|
return nil
|
|
}
|
|
|
|
// DeleteTokens is used to delete tokens
|
|
func (a *ACL) DeleteTokens(args *structs.ACLTokenDeleteRequest, reply *structs.GenericResponse) error {
|
|
// Ensure ACLs are enabled, and always flow modification requests to the authoritative region
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
authErr := a.srv.Authenticate(a.ctx, args)
|
|
// Validate non-zero set of tokens
|
|
if len(args.AccessorIDs) == 0 {
|
|
return structs.NewErrRPCCoded(400, "must specify as least one token")
|
|
}
|
|
|
|
if done, err := a.srv.forward("ACL.DeleteTokens", args, args, reply); done {
|
|
return err
|
|
}
|
|
a.srv.MeasureRPCRate("acl", structs.RateMetricWrite, args)
|
|
if authErr != nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "delete_tokens"}, time.Now())
|
|
|
|
// Check management level permissions
|
|
if acl, err := a.srv.ResolveACL(args); err != nil {
|
|
return err
|
|
} else if acl == nil || !acl.IsManagement() {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
|
|
// Snapshot the state
|
|
state, err := a.srv.State().Snapshot()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Determine if we are deleting local or global tokens
|
|
hasGlobal := false
|
|
allGlobal := true
|
|
nonexistentTokens := make([]string, 0)
|
|
for _, accessor := range args.AccessorIDs {
|
|
token, err := state.ACLTokenByAccessorID(nil, accessor)
|
|
if err != nil {
|
|
return structs.NewErrRPCCodedf(400, "token lookup failed: %v", err)
|
|
}
|
|
if token == nil {
|
|
nonexistentTokens = append(nonexistentTokens, accessor)
|
|
continue
|
|
}
|
|
if token.Global {
|
|
hasGlobal = true
|
|
} else {
|
|
allGlobal = false
|
|
}
|
|
}
|
|
|
|
if len(nonexistentTokens) != 0 {
|
|
return structs.NewErrRPCCodedf(400, "Cannot delete nonexistent tokens: %v", strings.Join(nonexistentTokens, ", "))
|
|
}
|
|
|
|
// Disallow mixed requests with global and non-global tokens since we forward
|
|
// the entire request as a single batch.
|
|
if hasGlobal {
|
|
if !allGlobal {
|
|
return structs.NewErrRPCCoded(400, "cannot delete mixed global and non-global tokens")
|
|
}
|
|
|
|
// Force the request to the authoritative region if it has global
|
|
if a.srv.config.Region != a.srv.config.AuthoritativeRegion {
|
|
args.Region = a.srv.config.AuthoritativeRegion
|
|
_, err := a.srv.forward("ACL.DeleteTokens", args, args, reply)
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Update via Raft
|
|
_, index, err := a.srv.raftApply(structs.ACLTokenDeleteRequestType, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update the index
|
|
reply.Index = index
|
|
return nil
|
|
}
|
|
|
|
// ListTokens is used to list the tokens
|
|
func (a *ACL) ListTokens(args *structs.ACLTokenListRequest, reply *structs.ACLTokenListResponse) error {
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
authErr := a.srv.Authenticate(a.ctx, args)
|
|
if done, err := a.srv.forward("ACL.ListTokens", args, args, reply); done {
|
|
return err
|
|
}
|
|
a.srv.MeasureRPCRate("acl", structs.RateMetricList, args)
|
|
if authErr != nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "list_tokens"}, time.Now())
|
|
|
|
// Check management level permissions
|
|
if acl, err := a.srv.ResolveACL(args); err != nil {
|
|
return err
|
|
} else if acl == nil || !acl.IsManagement() {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
|
|
// Setup the blocking query
|
|
sort := state.SortOption(args.Reverse)
|
|
opts := blockingOptions{
|
|
queryOpts: &args.QueryOptions,
|
|
queryMeta: &reply.QueryMeta,
|
|
run: func(ws memdb.WatchSet, state *state.StateStore) error {
|
|
// Iterate over all the tokens
|
|
var err error
|
|
var iter memdb.ResultIterator
|
|
var opts paginator.StructsTokenizerOptions
|
|
|
|
if prefix := args.QueryOptions.Prefix; prefix != "" {
|
|
iter, err = state.ACLTokenByAccessorIDPrefix(ws, prefix, sort)
|
|
opts = paginator.StructsTokenizerOptions{
|
|
WithID: true,
|
|
}
|
|
} else if args.GlobalOnly {
|
|
iter, err = state.ACLTokensByGlobal(ws, true, sort)
|
|
opts = paginator.StructsTokenizerOptions{
|
|
WithID: true,
|
|
}
|
|
} else {
|
|
iter, err = state.ACLTokens(ws, sort)
|
|
opts = paginator.StructsTokenizerOptions{
|
|
WithCreateIndex: true,
|
|
WithID: true,
|
|
}
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tokenizer := paginator.NewStructsTokenizer(iter, opts)
|
|
|
|
var tokens []*structs.ACLTokenListStub
|
|
paginator, err := paginator.NewPaginator(iter, tokenizer, nil, args.QueryOptions,
|
|
func(raw interface{}) error {
|
|
token := raw.(*structs.ACLToken)
|
|
tokens = append(tokens, token.Stub())
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return structs.NewErrRPCCodedf(
|
|
http.StatusBadRequest, "failed to create result paginator: %v", err)
|
|
}
|
|
|
|
nextToken, err := paginator.Page()
|
|
if err != nil {
|
|
return structs.NewErrRPCCodedf(
|
|
http.StatusBadRequest, "failed to read result page: %v", err)
|
|
}
|
|
|
|
reply.QueryMeta.NextToken = nextToken
|
|
reply.Tokens = tokens
|
|
|
|
// Use the last index that affected the token table
|
|
index, err := state.Index("acl_token")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reply.Index = index
|
|
|
|
return nil
|
|
}}
|
|
return a.srv.blockingRPC(&opts)
|
|
}
|
|
|
|
// GetToken is used to get a specific token
|
|
func (a *ACL) GetToken(args *structs.ACLTokenSpecificRequest, reply *structs.SingleACLTokenResponse) error {
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
authErr := a.srv.Authenticate(a.ctx, args)
|
|
if done, err := a.srv.forward("ACL.GetToken", args, args, reply); done {
|
|
return err
|
|
}
|
|
a.srv.MeasureRPCRate("acl", structs.RateMetricRead, args)
|
|
if authErr != nil {
|
|
return authErr
|
|
}
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "get_token"}, time.Now())
|
|
|
|
acl, err := a.srv.ResolveACL(args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Ensure ACLs are enabled and this call is made with one
|
|
if acl == nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
|
|
// Setup the blocking query
|
|
opts := blockingOptions{
|
|
queryOpts: &args.QueryOptions,
|
|
queryMeta: &reply.QueryMeta,
|
|
run: func(ws memdb.WatchSet, state *state.StateStore) error {
|
|
// Look for the token
|
|
out, err := state.ACLTokenByAccessorID(ws, args.AccessorID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if out == nil {
|
|
// If the token doesn't resolve, only allow management tokens to
|
|
// block.
|
|
if !acl.IsManagement() {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
|
|
// Check management level permissions or that the secret ID matches the
|
|
// accessor ID
|
|
} else if !acl.IsManagement() && out.SecretID != args.AuthToken {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
|
|
// Setup the output
|
|
reply.Token = out
|
|
if out != nil {
|
|
reply.Index = out.ModifyIndex
|
|
} else {
|
|
// Use the last index that affected the token table
|
|
index, err := state.Index("acl_token")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reply.Index = index
|
|
}
|
|
return nil
|
|
}}
|
|
return a.srv.blockingRPC(&opts)
|
|
}
|
|
|
|
// GetTokens is used to get a set of token
|
|
func (a *ACL) GetTokens(args *structs.ACLTokenSetRequest, reply *structs.ACLTokenSetResponse) error {
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
authErr := a.srv.Authenticate(a.ctx, args)
|
|
if done, err := a.srv.forward("ACL.GetTokens", args, args, reply); done {
|
|
return err
|
|
}
|
|
a.srv.MeasureRPCRate("acl", structs.RateMetricList, args)
|
|
if authErr != nil {
|
|
return authErr
|
|
}
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "get_tokens"}, time.Now())
|
|
|
|
// Check management level permissions
|
|
if acl, err := a.srv.ResolveACL(args); err != nil {
|
|
return err
|
|
} else if acl == nil || !acl.IsManagement() {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
|
|
// Setup the blocking query
|
|
opts := blockingOptions{
|
|
queryOpts: &args.QueryOptions,
|
|
queryMeta: &reply.QueryMeta,
|
|
run: func(ws memdb.WatchSet, state *state.StateStore) error {
|
|
// Setup the output
|
|
reply.Tokens = make(map[string]*structs.ACLToken, len(args.AccessorIDS))
|
|
|
|
// Look for the token
|
|
for _, accessor := range args.AccessorIDS {
|
|
out, err := state.ACLTokenByAccessorID(ws, accessor)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if out != nil {
|
|
reply.Tokens[out.AccessorID] = out
|
|
}
|
|
}
|
|
|
|
// Use the last index that affected the token table
|
|
index, err := state.Index("acl_token")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reply.Index = index
|
|
return nil
|
|
}}
|
|
return a.srv.blockingRPC(&opts)
|
|
}
|
|
|
|
// ResolveToken is used to lookup a specific token by a secret ID.
|
|
//
|
|
// Deprecated: Prior to Nomad 1.5 this RPC was used by clients for
|
|
// authenticating local RPCs. Since Nomad 1.5 added workload identity support,
|
|
// clients now use the more flexible ACL.WhoAmI RPC. The /v1/acl/token/self API
|
|
// is the only remaining caller and should be switched to ACL.WhoAmI.
|
|
func (a *ACL) ResolveToken(args *structs.ResolveACLTokenRequest, reply *structs.ResolveACLTokenResponse) error {
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
if done, err := a.srv.forward("ACL.ResolveToken", args, args, reply); done {
|
|
return err
|
|
}
|
|
a.srv.MeasureRPCRate("acl", structs.RateMetricRead, args)
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "resolve_token"}, time.Now())
|
|
|
|
// Setup the query meta
|
|
a.srv.setQueryMeta(&reply.QueryMeta)
|
|
|
|
// Snapshot the state
|
|
state, err := a.srv.State().Snapshot()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Look for the token
|
|
out, err := state.ACLTokenBySecretID(nil, args.SecretID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Setup the output
|
|
reply.Token = out
|
|
if out != nil {
|
|
reply.Index = out.ModifyIndex
|
|
} else {
|
|
// Use the last index that affected the token table
|
|
index, err := state.Index("acl_token")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reply.Index = index
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *ACL) UpsertOneTimeToken(args *structs.OneTimeTokenUpsertRequest, reply *structs.OneTimeTokenUpsertResponse) error {
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
if done, err := a.srv.forward(
|
|
"ACL.UpsertOneTimeToken", args, args, reply); done {
|
|
return err
|
|
}
|
|
defer metrics.MeasureSince(
|
|
[]string{"nomad", "acl", "upsert_one_time_token"}, time.Now())
|
|
|
|
if !ServersMeetMinimumVersion(a.srv.Members(), a.srv.Region(), minOneTimeAuthenticationTokenVersion, false) {
|
|
return fmt.Errorf("All servers should be running version %v or later to use one-time authentication tokens", minOneTimeAuthenticationTokenVersion)
|
|
}
|
|
|
|
// Snapshot the state
|
|
state, err := a.srv.State().Snapshot()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Look up the token; there's no capability check as you can only
|
|
// request a OTT for your own ACL token
|
|
aclToken, err := state.ACLTokenBySecretID(nil, args.AuthToken)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if aclToken == nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
|
|
ott := &structs.OneTimeToken{
|
|
OneTimeSecretID: uuid.Generate(),
|
|
AccessorID: aclToken.AccessorID,
|
|
ExpiresAt: time.Now().Add(10 * time.Minute),
|
|
}
|
|
|
|
// Update via Raft
|
|
_, index, err := a.srv.raftApply(structs.OneTimeTokenUpsertRequestType, ott)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ott.ModifyIndex = index
|
|
ott.CreateIndex = index
|
|
reply.OneTimeToken = ott
|
|
reply.Index = index
|
|
return nil
|
|
}
|
|
|
|
// ExchangeOneTimeToken provides a one-time token's secret ID to exchange it
|
|
// for the ACL token that created that one-time token
|
|
func (a *ACL) ExchangeOneTimeToken(args *structs.OneTimeTokenExchangeRequest, reply *structs.OneTimeTokenExchangeResponse) error {
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
if done, err := a.srv.forward(
|
|
"ACL.ExchangeOneTimeToken", args, args, reply); done {
|
|
return err
|
|
}
|
|
defer metrics.MeasureSince(
|
|
[]string{"nomad", "acl", "exchange_one_time_token"}, time.Now())
|
|
|
|
if !ServersMeetMinimumVersion(a.srv.Members(), a.srv.Region(), minOneTimeAuthenticationTokenVersion, false) {
|
|
return fmt.Errorf("All servers should be running version %v or later to use one-time authentication tokens", minOneTimeAuthenticationTokenVersion)
|
|
}
|
|
|
|
// Snapshot the state
|
|
state, err := a.srv.State().Snapshot()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ott, err := state.OneTimeTokenBySecret(nil, args.OneTimeSecretID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ott == nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
if ott.ExpiresAt.Before(time.Now()) {
|
|
// we return early and leave cleaning up the expired token for GC
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
|
|
// Look for the token; it may have been deleted, in which case, 403
|
|
aclToken, err := state.ACLTokenByAccessorID(nil, ott.AccessorID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if aclToken == nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
|
|
// Expire token via raft; because this is the only write in the RPC the
|
|
// caller can safely retry with the same token if the raft write fails
|
|
_, index, err := a.srv.raftApply(structs.OneTimeTokenDeleteRequestType,
|
|
&structs.OneTimeTokenDeleteRequest{
|
|
AccessorIDs: []string{ott.AccessorID},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
reply.Token = aclToken
|
|
reply.Index = index
|
|
return nil
|
|
}
|
|
|
|
// ExpireOneTimeTokens removes all expired tokens from the state store. It is
|
|
// called only by garbage collection
|
|
func (a *ACL) ExpireOneTimeTokens(args *structs.OneTimeTokenExpireRequest, reply *structs.GenericResponse) error {
|
|
|
|
authErr := a.srv.Authenticate(a.ctx, args)
|
|
if done, err := a.srv.forward(
|
|
"ACL.ExpireOneTimeTokens", args, args, reply); done {
|
|
return err
|
|
}
|
|
a.srv.MeasureRPCRate("acl", structs.RateMetricWrite, args)
|
|
if authErr != nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
defer metrics.MeasureSince(
|
|
[]string{"nomad", "acl", "expire_one_time_tokens"}, time.Now())
|
|
|
|
if !ServersMeetMinimumVersion(a.srv.Members(), a.srv.Region(), minOneTimeAuthenticationTokenVersion, false) {
|
|
return fmt.Errorf("All servers should be running version %v or later to use one-time authentication tokens", minOneTimeAuthenticationTokenVersion)
|
|
}
|
|
|
|
// Check management level permissions
|
|
if a.srv.config.ACLEnabled {
|
|
if acl, err := a.srv.ResolveACL(args); err != nil {
|
|
return err
|
|
} else if acl == nil || !acl.IsManagement() {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
}
|
|
|
|
args.Timestamp = time.Now() // use the leader's timestamp
|
|
|
|
// Expire token via raft; because this is the only write in the RPC the
|
|
// caller can safely retry with the same token if the raft write fails
|
|
_, index, err := a.srv.raftApply(structs.OneTimeTokenExpireRequestType, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reply.Index = index
|
|
return nil
|
|
}
|
|
|
|
// UpsertRoles creates or updates ACL roles held within Nomad.
|
|
func (a *ACL) UpsertRoles(
|
|
args *structs.ACLRolesUpsertRequest,
|
|
reply *structs.ACLRolesUpsertResponse) error {
|
|
|
|
// Only allow operators to upsert ACL roles when ACLs are enabled.
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
authErr := a.srv.Authenticate(a.ctx, args)
|
|
// This endpoint always forwards to the authoritative region as ACL roles
|
|
// are global.
|
|
args.Region = a.srv.config.AuthoritativeRegion
|
|
|
|
if done, err := a.srv.forward(structs.ACLUpsertRolesRPCMethod, args, args, reply); done {
|
|
return err
|
|
}
|
|
a.srv.MeasureRPCRate("acl", structs.RateMetricWrite, args)
|
|
if authErr != nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "upsert_roles"}, time.Now())
|
|
|
|
// ACL roles can only be used once all servers, in all federated regions
|
|
// have been upgraded to 1.4.0 or greater.
|
|
if !ServersMeetMinimumVersion(a.srv.Members(), AllRegions, minACLRoleVersion, false) {
|
|
return fmt.Errorf("all servers should be running version %v or later to use ACL roles",
|
|
minACLRoleVersion)
|
|
}
|
|
|
|
// Only management level permissions can create ACL roles.
|
|
if acl, err := a.srv.ResolveACL(args); err != nil {
|
|
return err
|
|
} else if acl == nil || !acl.IsManagement() {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
|
|
// Snapshot the state so we can perform lookups against the ID and policy
|
|
// links if needed. Do it here, so we only need to do this once no matter
|
|
// how many roles we are upserting.
|
|
stateSnapshot, err := a.srv.State().Snapshot()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Validate each role.
|
|
for idx, role := range args.ACLRoles {
|
|
|
|
// Perform all the static validation of the ACL role object. Use the
|
|
// array index as we cannot be sure the error was caused by a missing
|
|
// name.
|
|
if err := role.Validate(); err != nil {
|
|
return structs.NewErrRPCCodedf(http.StatusBadRequest, "role %d invalid: %v", idx, err)
|
|
}
|
|
|
|
// If the caller has passed a role ID, this call is considered an
|
|
// update to an existing role. We should therefore ensure it is found
|
|
// within state. Otherwise, the call is considered a new creation, and
|
|
// we must ensure a role of the same name does not exist.
|
|
if role.ID == "" {
|
|
existingRole, err := stateSnapshot.GetACLRoleByName(nil, role.Name)
|
|
if err != nil {
|
|
return structs.NewErrRPCCodedf(http.StatusInternalServerError, "role lookup failed: %v", err)
|
|
}
|
|
if existingRole != nil {
|
|
return structs.NewErrRPCCodedf(http.StatusBadRequest, "role with name %s already exists", role.Name)
|
|
}
|
|
} else {
|
|
existingRole, err := stateSnapshot.GetACLRoleByID(nil, role.ID)
|
|
if err != nil {
|
|
return structs.NewErrRPCCodedf(http.StatusInternalServerError, "role lookup failed: %v", err)
|
|
}
|
|
if existingRole == nil {
|
|
return structs.NewErrRPCCodedf(http.StatusBadRequest, "cannot find role %s", role.ID)
|
|
}
|
|
}
|
|
|
|
policyNames := make(map[string]struct{})
|
|
var policiesLinks []*structs.ACLRolePolicyLink
|
|
|
|
// We need to deduplicate the ACL policy links within this role as well
|
|
// as ensure the policies exist within state.
|
|
for _, policyLink := range role.Policies {
|
|
|
|
// If the RPC does not allow for missing policies, perform a state
|
|
// look up for the policy. An error or not being able to find the
|
|
// policy is terminal. We can include the name in the error message
|
|
// as it has previously been validated.
|
|
if !args.AllowMissingPolicies {
|
|
existing, err := stateSnapshot.ACLPolicyByName(nil, policyLink.Name)
|
|
if err != nil {
|
|
return structs.NewErrRPCCodedf(http.StatusInternalServerError, "policy lookup failed: %v", err)
|
|
}
|
|
if existing == nil {
|
|
return structs.NewErrRPCCodedf(http.StatusBadRequest, "cannot find policy %s", policyLink.Name)
|
|
}
|
|
}
|
|
|
|
// If the policy name is not found within our map, this means we
|
|
// have not seen it previously. We need to add this to our
|
|
// deduplicated array and also mark the policy name as seen, so we
|
|
// skip any future policies of the same name.
|
|
if _, ok := policyNames[policyLink.Name]; !ok {
|
|
policiesLinks = append(policiesLinks, policyLink)
|
|
policyNames[policyLink.Name] = struct{}{}
|
|
}
|
|
}
|
|
|
|
// Stored the potentially updated policy links within our role.
|
|
role.Policies = policiesLinks
|
|
|
|
role.Canonicalize()
|
|
role.SetHash()
|
|
}
|
|
|
|
// Update via Raft.
|
|
_, index, err := a.srv.raftApply(structs.ACLRolesUpsertRequestType, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Populate the response. We do a lookup against the state to pick up the
|
|
// proper create / modify times.
|
|
stateSnapshot, err = a.srv.State().Snapshot()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, role := range args.ACLRoles {
|
|
lookupACLRole, err := stateSnapshot.GetACLRoleByName(nil, role.Name)
|
|
if err != nil {
|
|
return structs.NewErrRPCCodedf(400, "ACL role lookup failed: %v", err)
|
|
}
|
|
reply.ACLRoles = append(reply.ACLRoles, lookupACLRole)
|
|
}
|
|
|
|
// Update the index. There is no need to floor this as we are writing to
|
|
// state and therefore will get a non-zero index response.
|
|
reply.Index = index
|
|
return nil
|
|
}
|
|
|
|
// DeleteRolesByID is used to batch delete ACL roles using the ID as the
|
|
// deletion key.
|
|
func (a *ACL) DeleteRolesByID(
|
|
args *structs.ACLRolesDeleteByIDRequest,
|
|
reply *structs.ACLRolesDeleteByIDResponse) error {
|
|
|
|
// Only allow operators to delete ACL roles when ACLs are enabled.
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
|
|
authErr := a.srv.Authenticate(a.ctx, args)
|
|
|
|
// This endpoint always forwards to the authoritative region as ACL roles
|
|
// are global.
|
|
args.Region = a.srv.config.AuthoritativeRegion
|
|
|
|
if done, err := a.srv.forward(structs.ACLDeleteRolesByIDRPCMethod, args, args, reply); done {
|
|
return err
|
|
}
|
|
a.srv.MeasureRPCRate("acl", structs.RateMetricWrite, args)
|
|
if authErr != nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "delete_roles"}, time.Now())
|
|
|
|
// ACL roles can only be used once all servers, in all federated regions
|
|
// have been upgraded to 1.4.0 or greater.
|
|
if !ServersMeetMinimumVersion(a.srv.Members(), AllRegions, minACLRoleVersion, false) {
|
|
return fmt.Errorf("all servers should be running version %v or later to use ACL roles",
|
|
minACLRoleVersion)
|
|
}
|
|
|
|
// Only management level permissions can create ACL roles.
|
|
if acl, err := a.srv.ResolveACL(args); err != nil {
|
|
return err
|
|
} else if acl == nil || !acl.IsManagement() {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
|
|
// Update via Raft.
|
|
_, index, err := a.srv.raftApply(structs.ACLRolesDeleteByIDRequestType, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update the index. There is no need to floor this as we are writing to
|
|
// state and therefore will get a non-zero index response.
|
|
reply.Index = index
|
|
return nil
|
|
}
|
|
|
|
// ListRoles is used to list ACL roles within state. If not prefix is supplied,
|
|
// all ACL roles are listed, otherwise a prefix search is performed on the ACL
|
|
// role name.
|
|
func (a *ACL) ListRoles(
|
|
args *structs.ACLRolesListRequest,
|
|
reply *structs.ACLRolesListResponse) error {
|
|
|
|
// Only allow operators to list ACL roles when ACLs are enabled.
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
|
|
authErr := a.srv.Authenticate(a.ctx, args)
|
|
if done, err := a.srv.forward(structs.ACLListRolesRPCMethod, args, args, reply); done {
|
|
return err
|
|
}
|
|
a.srv.MeasureRPCRate("acl", structs.RateMetricList, args)
|
|
if authErr != nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "list_roles"}, time.Now())
|
|
|
|
// Resolve the token and ensure it has some form of permissions.
|
|
acl, err := a.srv.ResolveACL(args)
|
|
if err != nil {
|
|
return err
|
|
} else if acl == nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
|
|
// If the token is a management token, they can list all tokens. If not,
|
|
// the role set tracks which role links the token has and therefore which
|
|
// ones the caller can list.
|
|
isManagement := acl.IsManagement()
|
|
roleSet := &set.Set[string]{}
|
|
|
|
// If the token is not a management token, we determine which roles are
|
|
// linked to the token and therefore can be listed by the caller.
|
|
if !isManagement {
|
|
token, err := a.requestACLToken(args.AuthToken)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if token == nil {
|
|
return structs.ErrTokenNotFound
|
|
}
|
|
|
|
// Generate a set of Role IDs from the token role links.
|
|
roleSet = set.FromFunc(token.Roles, func(roleLink *structs.ACLTokenRoleLink) string { return roleLink.ID })
|
|
}
|
|
|
|
// Set up and return the blocking query.
|
|
return a.srv.blockingRPC(&blockingOptions{
|
|
queryOpts: &args.QueryOptions,
|
|
queryMeta: &reply.QueryMeta,
|
|
run: func(ws memdb.WatchSet, stateStore *state.StateStore) error {
|
|
|
|
// The iteration below appends directly to the reply object, so in
|
|
// order for blocking queries to work properly we must ensure the
|
|
// ACLRoles are reset. This allows the blocking query run function
|
|
// to work as expected.
|
|
reply.ACLRoles = nil
|
|
|
|
var (
|
|
err error
|
|
iter memdb.ResultIterator
|
|
)
|
|
|
|
// If the operator supplied a prefix, perform a prefix search.
|
|
// Otherwise, list all ACL roles in state.
|
|
switch args.QueryOptions.Prefix {
|
|
case "":
|
|
iter, err = stateStore.GetACLRoles(ws)
|
|
default:
|
|
iter, err = stateStore.GetACLRoleByIDPrefix(ws, args.QueryOptions.Prefix)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Iterate all the results and add these to our reply object. Check
|
|
// before appending to the reply that the caller is allowed to view
|
|
// the role.
|
|
for raw := iter.Next(); raw != nil; raw = iter.Next() {
|
|
|
|
role := raw.(*structs.ACLRole)
|
|
|
|
if roleSet.Contains(role.ID) || isManagement {
|
|
reply.ACLRoles = append(reply.ACLRoles, role.Stub())
|
|
}
|
|
}
|
|
|
|
// Use the index table to populate the query meta as we have no way
|
|
// of tracking the max index on deletes.
|
|
return a.srv.setReplyQueryMeta(stateStore, state.TableACLRoles, &reply.QueryMeta)
|
|
},
|
|
})
|
|
}
|
|
|
|
// GetRolesByID is used to get a set of ACL Roles as defined by their ID. This
|
|
// endpoint is used by the replication process and Nomad agent client token
|
|
// resolution.
|
|
func (a *ACL) GetRolesByID(args *structs.ACLRolesByIDRequest, reply *structs.ACLRolesByIDResponse) error {
|
|
|
|
// This endpoint is only used by the replication process which is only
|
|
// running on ACL enabled clusters, so this check should never be
|
|
// triggered.
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
authErr := a.srv.Authenticate(a.ctx, args)
|
|
if done, err := a.srv.forward(structs.ACLGetRolesByIDRPCMethod, args, args, reply); done {
|
|
return err
|
|
}
|
|
a.srv.MeasureRPCRate("acl", structs.RateMetricList, args)
|
|
if authErr != nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "get_roles_id"}, time.Now())
|
|
|
|
// For client typed tokens, allow them to query any roles associated with
|
|
// that token. This is used by Nomad agents in client mode which are
|
|
// resolving the roles to enforce.
|
|
token := args.GetIdentity().GetACLToken()
|
|
if token == nil {
|
|
return structs.ErrTokenNotFound
|
|
}
|
|
if token.Type != structs.ACLManagementToken && !token.HasRoles(args.ACLRoleIDs) {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
|
|
// Set up and return the blocking query
|
|
return a.srv.blockingRPC(&blockingOptions{
|
|
queryOpts: &args.QueryOptions,
|
|
queryMeta: &reply.QueryMeta,
|
|
run: func(ws memdb.WatchSet, stateStore *state.StateStore) error {
|
|
|
|
// Instantiate the output map to the correct maximum length.
|
|
reply.ACLRoles = make(map[string]*structs.ACLRole, len(args.ACLRoleIDs))
|
|
|
|
// Look for the ACL role and add this to our mapping if we have
|
|
// found it.
|
|
for _, roleID := range args.ACLRoleIDs {
|
|
out, err := stateStore.GetACLRoleByID(ws, roleID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if out != nil {
|
|
reply.ACLRoles[out.ID] = out
|
|
}
|
|
}
|
|
|
|
// Use the index table to populate the query meta as we have no way
|
|
// of tracking the max index on deletes.
|
|
return a.srv.setReplyQueryMeta(stateStore, state.TableACLRoles, &reply.QueryMeta)
|
|
},
|
|
})
|
|
}
|
|
|
|
// GetRoleByID is used to look up an individual ACL role using its ID.
|
|
func (a *ACL) GetRoleByID(
|
|
args *structs.ACLRoleByIDRequest,
|
|
reply *structs.ACLRoleByIDResponse) error {
|
|
|
|
// Only allow operators to read an ACL role when ACLs are enabled.
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
|
|
authErr := a.srv.Authenticate(a.ctx, args)
|
|
if done, err := a.srv.forward(structs.ACLGetRoleByIDRPCMethod, args, args, reply); done {
|
|
return err
|
|
}
|
|
a.srv.MeasureRPCRate("acl", structs.RateMetricRead, args)
|
|
if authErr != nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "get_role_id"}, time.Now())
|
|
|
|
// Resolve the token and ensure it has some form of permissions.
|
|
acl, err := a.srv.ResolveACL(args)
|
|
if err != nil {
|
|
return err
|
|
} else if acl == nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
|
|
// If the token is a management token, they can detail any token they so
|
|
// desire.
|
|
isManagement := acl.IsManagement()
|
|
|
|
// If the token is not a management token, we determine if the caller wants
|
|
// to detail a role linked to their token.
|
|
if !isManagement {
|
|
aclToken, err := a.requestACLToken(args.AuthToken)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if aclToken == nil {
|
|
return structs.ErrTokenNotFound
|
|
}
|
|
|
|
found := false
|
|
|
|
for _, roleLink := range aclToken.Roles {
|
|
if roleLink.ID == args.RoleID {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
}
|
|
|
|
// Set up and return the blocking query.
|
|
return a.srv.blockingRPC(&blockingOptions{
|
|
queryOpts: &args.QueryOptions,
|
|
queryMeta: &reply.QueryMeta,
|
|
run: func(ws memdb.WatchSet, stateStore *state.StateStore) error {
|
|
|
|
// Perform a lookup for the ACL role.
|
|
out, err := stateStore.GetACLRoleByID(ws, args.RoleID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Set the index correctly depending on whether the ACL role was
|
|
// found.
|
|
switch out {
|
|
case nil:
|
|
index, err := stateStore.Index(state.TableACLRoles)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reply.Index = index
|
|
default:
|
|
reply.Index = out.ModifyIndex
|
|
}
|
|
|
|
// We didn't encounter an error looking up the index; set the ACL
|
|
// role on the reply and exit successfully.
|
|
reply.ACLRole = out
|
|
return nil
|
|
},
|
|
})
|
|
}
|
|
|
|
// GetRoleByName is used to look up an individual ACL role using its name.
|
|
func (a *ACL) GetRoleByName(
|
|
args *structs.ACLRoleByNameRequest,
|
|
reply *structs.ACLRoleByNameResponse) error {
|
|
|
|
// Only allow operators to read an ACL role when ACLs are enabled.
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
authErr := a.srv.Authenticate(a.ctx, args)
|
|
if done, err := a.srv.forward(structs.ACLGetRoleByNameRPCMethod, args, args, reply); done {
|
|
return err
|
|
}
|
|
a.srv.MeasureRPCRate("acl", structs.RateMetricRead, args)
|
|
if authErr != nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "get_role_name"}, time.Now())
|
|
|
|
// Resolve the token and ensure it has some form of permissions.
|
|
acl, err := a.srv.ResolveACL(args)
|
|
if err != nil {
|
|
return err
|
|
} else if acl == nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
|
|
// If the token is a management token, they can detail any token they so
|
|
// desire.
|
|
isManagement := acl.IsManagement()
|
|
|
|
// If the token is not a management token, we determine if the caller wants
|
|
// to detail a role linked to their token.
|
|
if !isManagement {
|
|
aclToken, err := a.requestACLToken(args.AuthToken)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if aclToken == nil {
|
|
return structs.ErrTokenNotFound
|
|
}
|
|
|
|
found := false
|
|
|
|
for _, roleLink := range aclToken.Roles {
|
|
if roleLink.Name == args.RoleName {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
}
|
|
|
|
// Set up and return the blocking query.
|
|
return a.srv.blockingRPC(&blockingOptions{
|
|
queryOpts: &args.QueryOptions,
|
|
queryMeta: &reply.QueryMeta,
|
|
run: func(ws memdb.WatchSet, stateStore *state.StateStore) error {
|
|
|
|
// Perform a lookup for the ACL role.
|
|
out, err := stateStore.GetACLRoleByName(ws, args.RoleName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Set the index correctly depending on whether the ACL role was
|
|
// found.
|
|
switch out {
|
|
case nil:
|
|
index, err := stateStore.Index(state.TableACLRoles)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reply.Index = index
|
|
default:
|
|
reply.Index = out.ModifyIndex
|
|
}
|
|
|
|
// We didn't encounter an error looking up the index; set the ACL
|
|
// role on the reply and exit successfully.
|
|
reply.ACLRole = out
|
|
return nil
|
|
},
|
|
})
|
|
}
|
|
|
|
// policyNamesFromRoleLinks resolves the policy names which are linked via the
|
|
// passed role links. This is useful when you need to understand what polices
|
|
// an ACL token has access to and need to include role links. The function will
|
|
// not return a nil set object, so callers can use this without having to check
|
|
// this.
|
|
func (a *ACL) policyNamesFromRoleLinks(roleLinks []*structs.ACLTokenRoleLink) (*set.Set[string], error) {
|
|
|
|
numRoles := len(roleLinks)
|
|
policyNameSet := set.New[string](numRoles)
|
|
|
|
if numRoles < 1 {
|
|
return policyNameSet, nil
|
|
}
|
|
|
|
stateSnapshot, err := a.srv.State().Snapshot()
|
|
if err != nil {
|
|
return policyNameSet, err
|
|
}
|
|
|
|
// Iterate all the token role links, so we can unpack these and identify
|
|
// the ACL policies.
|
|
for _, roleLink := range roleLinks {
|
|
|
|
// 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 := stateSnapshot.GetACLRoleByID(nil, roleLink.ID)
|
|
if err != nil {
|
|
return policyNameSet, 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 {
|
|
policyByName, err := stateSnapshot.ACLPolicyByName(nil, policyLink.Name)
|
|
if err != nil {
|
|
return policyNameSet, err
|
|
}
|
|
|
|
// Ignore policies that don't exist, since they don't grant any
|
|
// more privilege.
|
|
if policyByName == nil {
|
|
continue
|
|
}
|
|
|
|
// Add the policy to the tracking array.
|
|
policyNameSet.Insert(policyByName.Name)
|
|
}
|
|
}
|
|
|
|
return policyNameSet, nil
|
|
}
|
|
|
|
// UpsertAuthMethods is used to create or update a set of auth methods
|
|
func (a *ACL) UpsertAuthMethods(
|
|
args *structs.ACLAuthMethodUpsertRequest,
|
|
reply *structs.ACLAuthMethodUpsertResponse) error {
|
|
// Ensure ACLs are enabled, and always flow modification requests to the
|
|
// authoritative region
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
authErr := a.srv.Authenticate(a.ctx, args)
|
|
args.Region = a.srv.config.AuthoritativeRegion
|
|
|
|
if done, err := a.srv.forward(structs.ACLUpsertAuthMethodsRPCMethod, args, args, reply); done {
|
|
return err
|
|
}
|
|
a.srv.MeasureRPCRate("acl", structs.RateMetricWrite, args)
|
|
if authErr != nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "upsert_auth_methods"}, time.Now())
|
|
|
|
// ACL auth methods can only be used once all servers in all federated
|
|
// regions have been upgraded to 1.5.0 or greater.
|
|
if !ServersMeetMinimumVersion(a.srv.Members(), AllRegions, minACLAuthMethodVersion, false) {
|
|
return fmt.Errorf("all servers should be running version %v or later to use ACL auth methods",
|
|
minACLAuthMethodVersion)
|
|
}
|
|
|
|
// Check management level permissions
|
|
if acl, err := a.srv.ResolveACL(args); err != nil {
|
|
return err
|
|
} else if acl == nil || !acl.IsManagement() {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
|
|
// Validate non-zero set of auth methods
|
|
if len(args.AuthMethods) == 0 {
|
|
return structs.NewErrRPCCoded(http.StatusBadRequest, "must specify as least one auth method")
|
|
}
|
|
|
|
// Snapshot the state so we can make lookups to verify default method
|
|
stateSnapshot, err := a.srv.State().Snapshot()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Validate each auth method, canonicalize, and compute hash
|
|
// merge methods in case we're doing an update
|
|
for idx, authMethod := range args.AuthMethods {
|
|
// if there's an existing method with the same name, we treat this as
|
|
// an update
|
|
existingMethod, _ := stateSnapshot.GetACLAuthMethodByName(nil, authMethod.Name)
|
|
authMethod.Merge(existingMethod)
|
|
|
|
if err := authMethod.Validate(
|
|
a.srv.config.ACLTokenMinExpirationTTL,
|
|
a.srv.config.ACLTokenMaxExpirationTTL); err != nil {
|
|
return structs.NewErrRPCCodedf(http.StatusBadRequest, "auth method %d invalid: %v", idx, err)
|
|
}
|
|
|
|
// Are we trying to upsert a default auth method? Check if there isn't
|
|
// a default one already.
|
|
if authMethod.Default {
|
|
existingMethodsDefaultMethod, _ := stateSnapshot.GetDefaultACLAuthMethod(nil)
|
|
if existingMethodsDefaultMethod != nil && existingMethodsDefaultMethod.Name != authMethod.Name {
|
|
return structs.NewErrRPCCodedf(
|
|
http.StatusBadRequest,
|
|
"default method already exists: %v", existingMethodsDefaultMethod.Name,
|
|
)
|
|
}
|
|
}
|
|
authMethod.Canonicalize()
|
|
authMethod.SetHash()
|
|
}
|
|
|
|
// Update via Raft
|
|
_, index, err := a.srv.raftApply(structs.ACLAuthMethodsUpsertRequestType, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Populate the response. We do a lookup against the state to pick up the
|
|
// proper create / modify times.
|
|
stateSnapshot, err = a.srv.State().Snapshot()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, method := range args.AuthMethods {
|
|
lookupAuthMethod, err := stateSnapshot.GetACLAuthMethodByName(nil, method.Name)
|
|
if err != nil {
|
|
return structs.NewErrRPCCodedf(400, "ACL auth method lookup failed: %v", err)
|
|
}
|
|
if lookupAuthMethod != nil {
|
|
reply.AuthMethods = append(reply.AuthMethods, lookupAuthMethod)
|
|
}
|
|
}
|
|
|
|
// Update the index
|
|
reply.Index = index
|
|
return nil
|
|
}
|
|
|
|
// DeleteAuthMethods is used to delete auth methods
|
|
func (a *ACL) DeleteAuthMethods(
|
|
args *structs.ACLAuthMethodDeleteRequest,
|
|
reply *structs.ACLAuthMethodDeleteResponse) error {
|
|
// Ensure ACLs are enabled, and always flow modification requests to the
|
|
// authoritative region
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
args.Region = a.srv.config.AuthoritativeRegion
|
|
|
|
authErr := a.srv.Authenticate(a.ctx, args)
|
|
if done, err := a.srv.forward(
|
|
structs.ACLDeleteAuthMethodsRPCMethod, args, args, reply); done {
|
|
return err
|
|
}
|
|
a.srv.MeasureRPCRate("acl", structs.RateMetricWrite, args)
|
|
if authErr != nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "delete_auth_methods_by_name"}, time.Now())
|
|
|
|
// ACL auth methods can only be used once all servers in all federated
|
|
// regions have been upgraded to 1.5.0 or greater.
|
|
if !ServersMeetMinimumVersion(a.srv.Members(), AllRegions, minACLRoleVersion, false) {
|
|
return fmt.Errorf("all servers should be running version %v or later to use ACL auth methods",
|
|
minACLAuthMethodVersion)
|
|
}
|
|
|
|
// Check management level permissions
|
|
if acl, err := a.srv.ResolveACL(args); err != nil {
|
|
return err
|
|
} else if acl == nil || !acl.IsManagement() {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
|
|
// Validate non-zero set of auth methods
|
|
if len(args.Names) == 0 {
|
|
return structs.NewErrRPCCoded(http.StatusBadRequest, "must specify as least one auth method")
|
|
}
|
|
|
|
// Update via Raft
|
|
_, index, err := a.srv.raftApply(structs.ACLAuthMethodsDeleteRequestType, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update the index
|
|
reply.Index = index
|
|
return nil
|
|
}
|
|
|
|
// ListAuthMethods returns a list of ACL auth methods
|
|
func (a *ACL) ListAuthMethods(
|
|
args *structs.ACLAuthMethodListRequest,
|
|
reply *structs.ACLAuthMethodListResponse) error {
|
|
// Only allow operators to list auth methods when ACLs are enabled.
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
|
|
if done, err := a.srv.forward(
|
|
structs.ACLListAuthMethodsRPCMethod, args, args, reply); done {
|
|
return err
|
|
}
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "list_auth_methods"}, time.Now())
|
|
|
|
// Set up and return the blocking query.
|
|
return a.srv.blockingRPC(&blockingOptions{
|
|
queryOpts: &args.QueryOptions,
|
|
queryMeta: &reply.QueryMeta,
|
|
run: func(ws memdb.WatchSet, stateStore *state.StateStore) error {
|
|
|
|
// The iteration below appends directly to the reply object, so in
|
|
// order for blocking queries to work properly we must ensure the
|
|
// auth methods are reset. This allows the blocking query run
|
|
// function to work as expected.
|
|
reply.AuthMethods = nil
|
|
|
|
iter, err := stateStore.GetACLAuthMethods(ws)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Iterate all the results and add these to our reply object.
|
|
for raw := iter.Next(); raw != nil; raw = iter.Next() {
|
|
method := raw.(*structs.ACLAuthMethod)
|
|
reply.AuthMethods = append(reply.AuthMethods, method.Stub())
|
|
}
|
|
|
|
// Use the index table to populate the query meta
|
|
return a.srv.setReplyQueryMeta(
|
|
stateStore, state.TableACLAuthMethods, &reply.QueryMeta,
|
|
)
|
|
},
|
|
})
|
|
}
|
|
|
|
func (a *ACL) GetAuthMethod(
|
|
args *structs.ACLAuthMethodGetRequest,
|
|
reply *structs.ACLAuthMethodGetResponse) error {
|
|
|
|
// Only allow operators to read an auth method when ACLs are enabled.
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
|
|
authErr := a.srv.Authenticate(a.ctx, args)
|
|
if done, err := a.srv.forward(
|
|
structs.ACLGetAuthMethodRPCMethod, args, args, reply); done {
|
|
return err
|
|
}
|
|
a.srv.MeasureRPCRate("acl", structs.RateMetricRead, args)
|
|
if authErr != nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "get_auth_method_name"}, time.Now())
|
|
|
|
// Resolve the token and ensure it has some form of permissions.
|
|
acl, err := a.srv.ResolveACL(args)
|
|
if err != nil {
|
|
return err
|
|
} else if acl == nil || !acl.IsManagement() {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
|
|
// Set up and return the blocking query.
|
|
return a.srv.blockingRPC(&blockingOptions{
|
|
queryOpts: &args.QueryOptions,
|
|
queryMeta: &reply.QueryMeta,
|
|
run: func(ws memdb.WatchSet, stateStore *state.StateStore) error {
|
|
|
|
// Perform a lookup
|
|
out, err := stateStore.GetACLAuthMethodByName(ws, args.MethodName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Set the index correctly depending on whether the auth method was
|
|
// found.
|
|
switch out {
|
|
case nil:
|
|
index, err := stateStore.Index(state.TableACLAuthMethods)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reply.Index = index
|
|
default:
|
|
reply.Index = out.ModifyIndex
|
|
}
|
|
|
|
// We didn't encounter an error looking up the index; set the auth
|
|
// method on the reply and exit successfully.
|
|
reply.AuthMethod = out
|
|
return nil
|
|
},
|
|
})
|
|
}
|
|
|
|
// GetAuthMethods is used to get a set of auth methods
|
|
func (a *ACL) GetAuthMethods(
|
|
args *structs.ACLAuthMethodsGetRequest,
|
|
reply *structs.ACLAuthMethodsGetResponse) error {
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
authErr := a.srv.Authenticate(a.ctx, args)
|
|
if done, err := a.srv.forward(
|
|
structs.ACLGetAuthMethodsRPCMethod, args, args, reply); done {
|
|
return err
|
|
}
|
|
a.srv.MeasureRPCRate("acl", structs.RateMetricList, args)
|
|
if authErr != nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "get_auth_methods"}, time.Now())
|
|
|
|
// allow only management token holders to query this endpoint
|
|
token := args.GetIdentity().GetACLToken()
|
|
if token == nil {
|
|
return structs.ErrTokenNotFound
|
|
}
|
|
if token.Type != structs.ACLManagementToken {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
|
|
// Setup the blocking query
|
|
return a.srv.blockingRPC(&blockingOptions{
|
|
queryOpts: &args.QueryOptions,
|
|
queryMeta: &reply.QueryMeta,
|
|
run: func(ws memdb.WatchSet, statestore *state.StateStore) error {
|
|
// Setup the output
|
|
reply.AuthMethods = make(map[string]*structs.ACLAuthMethod, len(args.Names))
|
|
|
|
// Look for the auth method
|
|
for _, methodName := range args.Names {
|
|
out, err := statestore.GetACLAuthMethodByName(ws, methodName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if out != nil {
|
|
reply.AuthMethods[methodName] = out
|
|
}
|
|
}
|
|
|
|
// Use the index table to populate the query meta
|
|
return a.srv.setReplyQueryMeta(
|
|
statestore, state.TableACLAuthMethods, &reply.QueryMeta,
|
|
)
|
|
}},
|
|
)
|
|
}
|
|
|
|
// WhoAmI is a RPC for debugging authentication. This endpoint returns the same
|
|
// AuthenticatedIdentity that will be used by RPC handlers, but unlike other
|
|
// endpoints will try to authenticate workload identities even if ACLs are
|
|
// disabled.
|
|
//
|
|
// TODO: At some point we might want to give this an equivalent HTTP endpoint
|
|
// once other Workload Identity work is solidified
|
|
func (a *ACL) WhoAmI(args *structs.GenericRequest, reply *structs.ACLWhoAmIResponse) error {
|
|
authErr := a.srv.Authenticate(a.ctx, args)
|
|
if done, err := a.srv.forward("ACL.WhoAmI", args, args, reply); done {
|
|
return err
|
|
}
|
|
a.srv.MeasureRPCRate("acl", structs.RateMetricRead, args)
|
|
if authErr != nil {
|
|
return authErr
|
|
}
|
|
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "whoami"}, time.Now())
|
|
|
|
if !a.srv.config.ACLEnabled {
|
|
// Authenticate never verifies claimed when ACLs are disabled, but since
|
|
// this endpoint is explicitly for resolving identities, always try to
|
|
// verify any claims.
|
|
if claims, _ := a.srv.VerifyClaim(args.AuthToken); claims != nil {
|
|
args.SetIdentity(&structs.AuthenticatedIdentity{Claims: claims})
|
|
}
|
|
}
|
|
|
|
reply.Identity = args.GetIdentity()
|
|
return nil
|
|
}
|
|
|
|
// UpsertBindingRules creates or updates ACL binding rules held within Nomad.
|
|
func (a *ACL) UpsertBindingRules(
|
|
args *structs.ACLBindingRulesUpsertRequest, reply *structs.ACLBindingRulesUpsertResponse) error {
|
|
|
|
// Ensure ACLs are enabled, and always flow modification requests to the
|
|
// authoritative region.
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
args.Region = a.srv.config.AuthoritativeRegion
|
|
|
|
authErr := a.srv.Authenticate(a.ctx, args)
|
|
if done, err := a.srv.forward(structs.ACLUpsertBindingRulesRPCMethod, args, args, reply); done {
|
|
return err
|
|
}
|
|
a.srv.MeasureRPCRate("acl", structs.RateMetricWrite, args)
|
|
if authErr != nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "upsert_binding_rules"}, time.Now())
|
|
|
|
// ACL binding rules can only be used once all servers in all federated
|
|
// regions have been upgraded to 1.5.0 or greater.
|
|
if !ServersMeetMinimumVersion(a.srv.Members(), AllRegions, minACLBindingRuleVersion, false) {
|
|
return fmt.Errorf("all servers should be running version %v or later to use ACL binding rules",
|
|
minACLBindingRuleVersion)
|
|
}
|
|
|
|
// Check management level permissions
|
|
if acl, err := a.srv.ResolveACL(args); err != nil {
|
|
return err
|
|
} else if acl == nil || !acl.IsManagement() {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
|
|
// Validate non-zero set of binding rules. This must be done outside the
|
|
// validate function as that uses a loop, which will be skipped if the
|
|
// length is zero.
|
|
if len(args.ACLBindingRules) == 0 {
|
|
return structs.NewErrRPCCoded(http.StatusBadRequest, "must specify as least one binding rule")
|
|
}
|
|
|
|
stateSnapshot, err := a.srv.State().Snapshot()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Validate each binding rules and compute the hash.
|
|
for idx, bindingRule := range args.ACLBindingRules {
|
|
|
|
// If the caller has passed a rule ID, this call is considered an
|
|
// update to an existing rule. We should therefore ensure it is found
|
|
// within state.
|
|
if bindingRule.ID != "" {
|
|
existingBindingRule, err := stateSnapshot.GetACLBindingRule(nil, bindingRule.ID)
|
|
if err != nil {
|
|
return structs.NewErrRPCCodedf(
|
|
http.StatusInternalServerError, "binding rule lookup failed: %v", err)
|
|
}
|
|
if existingBindingRule == nil {
|
|
return structs.NewErrRPCCodedf(
|
|
http.StatusBadRequest, "cannot find binding rule %s", bindingRule.ID)
|
|
}
|
|
|
|
// merge
|
|
bindingRule.Merge(existingBindingRule)
|
|
|
|
// Auth methods cannot be changed
|
|
if bindingRule.AuthMethod != existingBindingRule.AuthMethod {
|
|
return structs.NewErrRPCCoded(
|
|
http.StatusBadRequest, "cannot update auth method for binding rule, create a new rule instead",
|
|
)
|
|
}
|
|
bindingRule.AuthMethod = existingBindingRule.AuthMethod
|
|
}
|
|
|
|
// Validate only if it's not an update
|
|
if err := bindingRule.Validate(); err != nil {
|
|
return structs.NewErrRPCCodedf(http.StatusBadRequest, "binding rule %d invalid: %v", idx, err)
|
|
}
|
|
|
|
// Ensure the auth method linked to exists within state.
|
|
method, err := stateSnapshot.GetACLAuthMethodByName(nil, bindingRule.AuthMethod)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if method == nil {
|
|
return structs.NewErrRPCCodedf(
|
|
http.StatusBadRequest, "ACL auth method %s not found", bindingRule.AuthMethod)
|
|
}
|
|
|
|
// All the validation has passed, we can now canonicalize the object
|
|
// with the final internal data and set the hash.
|
|
bindingRule.Canonicalize()
|
|
bindingRule.SetHash()
|
|
}
|
|
|
|
// Update via Raft.
|
|
_, index, err := a.srv.raftApply(structs.ACLBindingRulesUpsertRequestType, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Populate the response. We do a lookup against the state to pick up the
|
|
// proper create / modify indexes.
|
|
stateSnapshot, err = a.srv.State().Snapshot()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, bindingRule := range args.ACLBindingRules {
|
|
lookupBindingRule, err := stateSnapshot.GetACLBindingRule(nil, bindingRule.ID)
|
|
if err != nil {
|
|
return structs.NewErrRPCCodedf(http.StatusInternalServerError,
|
|
"ACL binding rule lookup failed: %v", err)
|
|
}
|
|
if lookupBindingRule == nil {
|
|
return structs.NewErrRPCCoded(http.StatusInternalServerError,
|
|
"ACL binding rule lookup failed: no entry found")
|
|
}
|
|
reply.ACLBindingRules = append(reply.ACLBindingRules, lookupBindingRule)
|
|
}
|
|
|
|
// Update the index
|
|
reply.Index = index
|
|
return nil
|
|
}
|
|
|
|
// DeleteBindingRules batch deletes ACL binding rules from Nomad state.
|
|
func (a *ACL) DeleteBindingRules(
|
|
args *structs.ACLBindingRulesDeleteRequest, reply *structs.ACLBindingRulesDeleteResponse) error {
|
|
|
|
// Ensure ACLs are enabled, and always flow modification requests to the
|
|
// authoritative region.
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
args.Region = a.srv.config.AuthoritativeRegion
|
|
|
|
authErr := a.srv.Authenticate(a.ctx, args)
|
|
if done, err := a.srv.forward(structs.ACLDeleteBindingRulesRPCMethod, args, args, reply); done {
|
|
return err
|
|
}
|
|
a.srv.MeasureRPCRate("acl", structs.RateMetricWrite, args)
|
|
if authErr != nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "delete_binding_rules"}, time.Now())
|
|
|
|
// ACL binding rules can only be used once all servers in all federated
|
|
// regions have been upgraded to 1.5.0 or greater.
|
|
if !ServersMeetMinimumVersion(a.srv.Members(), AllRegions, minACLBindingRuleVersion, false) {
|
|
return fmt.Errorf("all servers should be running version %v or later to use ACL binding rules",
|
|
minACLBindingRuleVersion)
|
|
}
|
|
|
|
// Check management level permissions.
|
|
if acl, err := a.srv.ResolveACL(args); err != nil {
|
|
return err
|
|
} else if acl == nil || !acl.IsManagement() {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
|
|
// Validate non-zero set of binding rule IDs.
|
|
if len(args.ACLBindingRuleIDs) == 0 {
|
|
return structs.NewErrRPCCoded(http.StatusBadRequest, "must specify as least one binding rule")
|
|
}
|
|
|
|
// Update via Raft.
|
|
_, index, err := a.srv.raftApply(structs.ACLBindingRulesDeleteRequestType, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Update the index
|
|
reply.Index = index
|
|
return nil
|
|
}
|
|
|
|
// ListBindingRules returns a stub list of ACL binding rules.
|
|
func (a *ACL) ListBindingRules(
|
|
args *structs.ACLBindingRulesListRequest, reply *structs.ACLBindingRulesListResponse) error {
|
|
|
|
// Only allow operators to list ACL binding rules when ACLs are enabled.
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
|
|
authErr := a.srv.Authenticate(a.ctx, args)
|
|
if done, err := a.srv.forward(structs.ACLListBindingRulesRPCMethod, args, args, reply); done {
|
|
return err
|
|
}
|
|
a.srv.MeasureRPCRate("acl", structs.RateMetricList, args)
|
|
if authErr != nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "list_binding_rules"}, time.Now())
|
|
|
|
// Check management level permissions.
|
|
if acl, err := a.srv.ResolveACL(args); err != nil {
|
|
return err
|
|
} else if acl == nil || !acl.IsManagement() {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
|
|
// Set up and return the blocking query.
|
|
return a.srv.blockingRPC(&blockingOptions{
|
|
queryOpts: &args.QueryOptions,
|
|
queryMeta: &reply.QueryMeta,
|
|
run: func(ws memdb.WatchSet, stateStore *state.StateStore) error {
|
|
|
|
// The iteration below appends directly to the reply object, so in
|
|
// order for blocking queries to work properly we must ensure the
|
|
// ACLBindingRules are reset. This allows the blocking query run
|
|
// function to work as expected.
|
|
reply.ACLBindingRules = nil
|
|
|
|
iter, err := stateStore.GetACLBindingRules(ws)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Iterate all the results and add these to our reply object.
|
|
for raw := iter.Next(); raw != nil; raw = iter.Next() {
|
|
reply.ACLBindingRules = append(reply.ACLBindingRules, raw.(*structs.ACLBindingRule).Stub())
|
|
}
|
|
|
|
// Use the index table to populate the query meta as we have no way
|
|
// of tracking the max index on deletes.
|
|
return a.srv.setReplyQueryMeta(stateStore, state.TableACLBindingRules, &reply.QueryMeta)
|
|
},
|
|
})
|
|
}
|
|
|
|
// GetBindingRules is used to query for a set of ACL binding rules. This
|
|
// endpoint is used for replication purposes and is not exposed via the HTTP
|
|
// API.
|
|
func (a *ACL) GetBindingRules(
|
|
args *structs.ACLBindingRulesRequest, reply *structs.ACLBindingRulesResponse) error {
|
|
|
|
// This endpoint is only used by the replication process which is only
|
|
// running on ACL enabled clusters, so this check should never be
|
|
// triggered.
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
|
|
authErr := a.srv.Authenticate(a.ctx, args)
|
|
if done, err := a.srv.forward(structs.ACLGetBindingRulesRPCMethod, args, args, reply); done {
|
|
return err
|
|
}
|
|
a.srv.MeasureRPCRate("acl", structs.RateMetricRead, args)
|
|
if authErr != nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "get_rules"}, time.Now())
|
|
|
|
// Check management level permissions.
|
|
if acl, err := a.srv.ResolveACL(args); err != nil {
|
|
return err
|
|
} else if acl == nil || !acl.IsManagement() {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
|
|
// Set up and return the blocking query
|
|
return a.srv.blockingRPC(&blockingOptions{
|
|
queryOpts: &args.QueryOptions,
|
|
queryMeta: &reply.QueryMeta,
|
|
run: func(ws memdb.WatchSet, stateStore *state.StateStore) error {
|
|
|
|
// Instantiate the output map to the correct maximum length.
|
|
reply.ACLBindingRules = make(map[string]*structs.ACLBindingRule, len(args.ACLBindingRuleIDs))
|
|
|
|
// Look for the ACL role and add this to our mapping if we have
|
|
// found it.
|
|
for _, bindingRuleID := range args.ACLBindingRuleIDs {
|
|
out, err := stateStore.GetACLBindingRule(ws, bindingRuleID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if out != nil {
|
|
reply.ACLBindingRules[out.ID] = out
|
|
}
|
|
}
|
|
|
|
// Use the index table to populate the query meta as we have no way
|
|
// of tracking the max index on deletes.
|
|
return a.srv.setReplyQueryMeta(stateStore, state.TableACLBindingRules, &reply.QueryMeta)
|
|
},
|
|
})
|
|
}
|
|
|
|
// GetBindingRule is used to retrieve a single ACL binding rule as defined by
|
|
// its ID.
|
|
func (a *ACL) GetBindingRule(
|
|
args *structs.ACLBindingRuleRequest, reply *structs.ACLBindingRuleResponse) error {
|
|
|
|
// Only allow operators to read an ACL binding rule when ACLs are enabled.
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
|
|
authErr := a.srv.Authenticate(a.ctx, args)
|
|
if done, err := a.srv.forward(structs.ACLGetBindingRuleRPCMethod, args, args, reply); done {
|
|
return err
|
|
}
|
|
a.srv.MeasureRPCRate("acl", structs.RateMetricRead, args)
|
|
if authErr != nil {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "get_binding_rule"}, time.Now())
|
|
|
|
// Check management level permissions.
|
|
if acl, err := a.srv.ResolveACL(args); err != nil {
|
|
return err
|
|
} else if acl == nil || !acl.IsManagement() {
|
|
return structs.ErrPermissionDenied
|
|
}
|
|
|
|
// Set up and return the blocking query.
|
|
return a.srv.blockingRPC(&blockingOptions{
|
|
queryOpts: &args.QueryOptions,
|
|
queryMeta: &reply.QueryMeta,
|
|
run: func(ws memdb.WatchSet, stateStore *state.StateStore) error {
|
|
|
|
// Perform a lookup for the ACL role.
|
|
out, err := stateStore.GetACLBindingRule(ws, args.ACLBindingRuleID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Set the index correctly depending on whether the ACL binding
|
|
// rule was found.
|
|
switch out {
|
|
case nil:
|
|
index, err := stateStore.Index(state.TableACLBindingRules)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reply.Index = index
|
|
default:
|
|
reply.Index = out.ModifyIndex
|
|
}
|
|
|
|
// We didn't encounter an error looking up the index; set the ACL
|
|
// binding rule on the reply and exit successfully.
|
|
reply.ACLBindingRule = out
|
|
return nil
|
|
},
|
|
})
|
|
}
|
|
|
|
// OIDCAuthURL starts the OIDC login workflow. The response URL should be used
|
|
// by the caller to authenticate the user. Once this has been completed,
|
|
// OIDCCompleteAuth can be used for the remainder of the workflow.
|
|
func (a *ACL) OIDCAuthURL(args *structs.ACLOIDCAuthURLRequest, reply *structs.ACLOIDCAuthURLResponse) error {
|
|
|
|
// The OIDC flow can only be used when the Nomad cluster has ACL enabled.
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
|
|
// Perform the initial forwarding within the region. This ensures we
|
|
// respect stale queries.
|
|
if done, err := a.srv.forward(structs.ACLOIDCAuthURLRPCMethod, args, args, reply); done {
|
|
return err
|
|
}
|
|
|
|
// There is not a perfect place to run this defer since we potentially
|
|
// forward twice. It is likely there will be two distinct patterns to this
|
|
// timing in clusters that utilise a mixture of local and global with
|
|
// methods.
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "oidc_auth_url"}, time.Now())
|
|
|
|
// Validate the request arguments to ensure it contains all the data it
|
|
// needs. Whether the data provided is correct will be handled by the OIDC
|
|
// provider.
|
|
if err := args.Validate(); err != nil {
|
|
return structs.NewErrRPCCodedf(http.StatusBadRequest, "invalid OIDC auth-url request: %v", err)
|
|
}
|
|
|
|
// Grab a snapshot of the state, so we can query it safely.
|
|
stateSnapshot, err := a.srv.fsm.State().Snapshot()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Lookup the auth method from state, so we have the entire object
|
|
// available to us. It's important to check for nil on the auth method
|
|
// object, as it is possible the request was made with an incorrectly named
|
|
// auth method.
|
|
authMethod, err := stateSnapshot.GetACLAuthMethodByName(nil, args.AuthMethodName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if authMethod == nil {
|
|
return structs.NewErrRPCCodedf(http.StatusBadRequest, "auth-method %q not found", args.AuthMethodName)
|
|
}
|
|
|
|
// If the authentication method generates global ACL tokens, we need to
|
|
// forward the request onto the authoritative regional leader.
|
|
if authMethod.TokenLocalityIsGlobal() {
|
|
args.Region = a.srv.config.AuthoritativeRegion
|
|
|
|
if done, err := a.srv.forward(structs.ACLOIDCAuthURLRPCMethod, args, args, reply); done {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Generate our OIDC request.
|
|
oidcReqOpts := []capOIDC.Option{
|
|
capOIDC.WithNonce(args.ClientNonce),
|
|
}
|
|
|
|
if len(authMethod.Config.OIDCScopes) > 0 {
|
|
oidcReqOpts = append(oidcReqOpts, capOIDC.WithScopes(authMethod.Config.OIDCScopes...))
|
|
}
|
|
|
|
oidcReq, err := capOIDC.NewRequest(
|
|
aclOIDCAuthURLRequestExpiryTime,
|
|
args.RedirectURI,
|
|
oidcReqOpts...,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate OIDC request: %v", err)
|
|
}
|
|
|
|
// Use the cache to provide us with an OIDC provider for the auth method
|
|
// that was resolved from state.
|
|
oidcProvider, err := a.oidcProviderCache.Get(authMethod)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate OIDC provider: %v", err)
|
|
}
|
|
|
|
// Generate a context. This argument is required by the OIDC provider lib,
|
|
// but is not used in any way. This therefore acts for future proofing, if
|
|
// the provider lib uses the context.
|
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(aclOIDCAuthURLRequestExpiryTime))
|
|
defer cancel()
|
|
|
|
// Generate the URL, handling any error along with the URL.
|
|
authURL, err := oidcProvider.AuthURL(ctx, oidcReq)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate auth URL: %v", err)
|
|
}
|
|
|
|
reply.AuthURL = authURL
|
|
return nil
|
|
}
|
|
|
|
// OIDCCompleteAuth complete the OIDC login workflow. It will exchange the OIDC
|
|
// provider token for a Nomad ACL token, using the configured ACL role and
|
|
// policy claims to provide authorization.
|
|
func (a *ACL) OIDCCompleteAuth(
|
|
args *structs.ACLOIDCCompleteAuthRequest, reply *structs.ACLLoginResponse) error {
|
|
|
|
// The OIDC flow can only be used when the Nomad cluster has ACL enabled.
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
|
|
// Perform the initial forwarding within the region. This ensures we
|
|
// respect stale queries.
|
|
if done, err := a.srv.forward(structs.ACLOIDCCompleteAuthRPCMethod, args, args, reply); done {
|
|
return err
|
|
}
|
|
|
|
// There is not a perfect place to run this defer since we potentially
|
|
// forward twice. It is likely there will be two distinct patterns to this
|
|
// timing in clusters that utilise a mixture of local and global with
|
|
// methods.
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "oidc_complete_auth"}, time.Now())
|
|
|
|
// Validate the request arguments to ensure it contains all the data it
|
|
// needs. Whether the data provided is correct will be handled by the OIDC
|
|
// provider.
|
|
if err := args.Validate(); err != nil {
|
|
return structs.NewErrRPCCodedf(http.StatusBadRequest, "invalid OIDC complete-auth request: %v", err)
|
|
}
|
|
|
|
// Grab a snapshot of the state, so we can query it safely.
|
|
stateSnapshot, err := a.srv.fsm.State().Snapshot()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Lookup the auth method from state, so we have the entire object
|
|
// available to us. It's important to check for nil on the auth method
|
|
// object, as it is possible the request was made with an incorrectly named
|
|
// auth method.
|
|
authMethod, err := stateSnapshot.GetACLAuthMethodByName(nil, args.AuthMethodName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if authMethod == nil {
|
|
return structs.NewErrRPCCodedf(http.StatusBadRequest, "auth-method %q not found", args.AuthMethodName)
|
|
}
|
|
|
|
// If the authentication method generates global ACL tokens, we need to
|
|
// forward the request onto the authoritative regional leader.
|
|
if authMethod.TokenLocalityIsGlobal() {
|
|
args.Region = a.srv.config.AuthoritativeRegion
|
|
|
|
if done, err := a.srv.forward(structs.ACLOIDCCompleteAuthRPCMethod, args, args, reply); done {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Use the cache to provide us with an OIDC provider for the auth method
|
|
// that was resolved from state.
|
|
oidcProvider, err := a.oidcProviderCache.Get(authMethod)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate OIDC provider: %v", err)
|
|
}
|
|
|
|
// Build our OIDC request options and request object.
|
|
oidcReqOpts := []capOIDC.Option{
|
|
capOIDC.WithNonce(args.ClientNonce),
|
|
capOIDC.WithState(args.State),
|
|
}
|
|
|
|
if len(authMethod.Config.OIDCScopes) > 0 {
|
|
oidcReqOpts = append(oidcReqOpts, capOIDC.WithScopes(authMethod.Config.OIDCScopes...))
|
|
}
|
|
if len(authMethod.Config.BoundAudiences) > 0 {
|
|
oidcReqOpts = append(oidcReqOpts, capOIDC.WithAudiences(authMethod.Config.BoundAudiences...))
|
|
}
|
|
|
|
oidcReq, err := capOIDC.NewRequest(aclOIDCCallbackRequestExpiryTime, args.RedirectURI, oidcReqOpts...)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to generate OIDC request: %v", err)
|
|
}
|
|
|
|
// Generate a context with a deadline. This is passed to the OIDC provider
|
|
// and used when making remote HTTP requests.
|
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(aclOIDCCallbackRequestExpiryTime))
|
|
defer cancel()
|
|
|
|
// Exchange the state and code for an OIDC provider token.
|
|
oidcToken, err := oidcProvider.Exchange(ctx, oidcReq, args.State, args.Code)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to exchange token with provider: %v", err)
|
|
}
|
|
if !oidcToken.Valid() {
|
|
return errors.New("exchanged token is not valid; potentially expired or empty")
|
|
}
|
|
|
|
var idTokenClaims map[string]interface{}
|
|
if err := oidcToken.IDToken().Claims(&idTokenClaims); err != nil {
|
|
return fmt.Errorf("failed to retrieve the ID token claims: %v", err)
|
|
}
|
|
|
|
var userClaims map[string]interface{}
|
|
if userTokenSource := oidcToken.StaticTokenSource(); userTokenSource != nil {
|
|
if err := oidcProvider.UserInfo(ctx, userTokenSource, idTokenClaims["sub"].(string), &userClaims); err != nil {
|
|
return fmt.Errorf("failed to retrieve the user info claims: %v", err)
|
|
}
|
|
}
|
|
|
|
// Generate the data used by the go-bexpr selector that is an internal
|
|
// representation of the claims that can be understood by Nomad.
|
|
oidcInternalClaims, err := auth.SelectorData(authMethod, idTokenClaims, userClaims)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create a new binder object based on the current state snapshot to
|
|
// provide consistency within the RPC handler.
|
|
oidcBinder := auth.NewBinder(stateSnapshot)
|
|
|
|
// Generate the role and policy bindings that will be assigned to the ACL
|
|
// token. Ensure we have at least 1 role or policy, otherwise the RPC will
|
|
// fail anyway.
|
|
tokenBindings, err := oidcBinder.Bind(authMethod, auth.NewIdentity(authMethod.Config, oidcInternalClaims))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if tokenBindings.None() && !tokenBindings.Management {
|
|
return structs.NewErrRPCCoded(http.StatusBadRequest, "no role or policy bindings matched")
|
|
}
|
|
|
|
// Build our token RPC request. The RPC handler includes a lot of specific
|
|
// logic, so we do not want to call Raft directly or copy that here. In the
|
|
// future we should try and extract out the logic into an interface, or at
|
|
// least a separate function.
|
|
token := structs.ACLToken{
|
|
Name: "OIDC-" + authMethod.Name,
|
|
Global: authMethod.TokenLocalityIsGlobal(),
|
|
ExpirationTTL: authMethod.MaxTokenTTL,
|
|
}
|
|
|
|
if tokenBindings.Management {
|
|
token.Type = structs.ACLManagementToken
|
|
} else {
|
|
token.Type = structs.ACLClientToken
|
|
token.Policies = tokenBindings.Policies
|
|
token.Roles = tokenBindings.Roles
|
|
}
|
|
|
|
tokenUpsertRequest := structs.ACLTokenUpsertRequest{
|
|
Tokens: []*structs.ACLToken{&token},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: a.srv.Region(),
|
|
AuthToken: a.srv.getLeaderAcl(),
|
|
},
|
|
}
|
|
|
|
var tokenUpsertReply structs.ACLTokenUpsertResponse
|
|
|
|
if err := a.upsertTokens(&tokenUpsertRequest, &tokenUpsertReply, stateSnapshot); err != nil {
|
|
return err
|
|
}
|
|
|
|
// The way the UpsertTokens RPC currently works, if we get no error, then
|
|
// we will have exactly the same number of tokens returned as we sent. It
|
|
// is therefore safe to assume we have 1 token.
|
|
reply.ACLToken = tokenUpsertReply.Tokens[0]
|
|
return nil
|
|
}
|
|
|
|
// Login RPC performs non-interactive auth using a given AuthMethod. This method
|
|
// can not be used for OIDC login flow.
|
|
func (a *ACL) Login(args *structs.ACLLoginRequest, reply *structs.ACLLoginResponse) error {
|
|
|
|
// The login flow can only be used when the Nomad cluster has ACL enabled.
|
|
if !a.srv.config.ACLEnabled {
|
|
return aclDisabled
|
|
}
|
|
|
|
// Perform the initial forwarding within the region. This ensures we
|
|
// respect stale queries.
|
|
if done, err := a.srv.forward(structs.ACLLoginRPCMethod, args, args, reply); done {
|
|
return err
|
|
}
|
|
|
|
// Measure the login endpoint performance.
|
|
defer metrics.MeasureSince([]string{"nomad", "acl", "login"}, time.Now())
|
|
|
|
// This endpoint can only be used once all servers in all federated regions
|
|
// have been upgraded to minACLJWTAuthMethodVersion or greater, since JWT Auth
|
|
// method was introduced then.
|
|
if !ServersMeetMinimumVersion(a.srv.Members(), AllRegions, minACLJWTAuthMethodVersion, false) {
|
|
return fmt.Errorf("all servers should be running version %v or later to use JWT ACL auth methods",
|
|
minACLJWTAuthMethodVersion)
|
|
}
|
|
|
|
// Validate the request arguments to ensure it contains all the data it
|
|
// needs.
|
|
if err := args.Validate(); err != nil {
|
|
return structs.NewErrRPCCodedf(http.StatusBadRequest, "invalid login request: %v", err)
|
|
}
|
|
|
|
// Grab a snapshot of the state, so we can query it safely.
|
|
stateSnapshot, err := a.srv.fsm.State().Snapshot()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Lookup the auth method from state, so we have the entire object
|
|
// available to us. It's important to check for nil on the auth method
|
|
// object, as it is possible the request was made with an incorrectly named
|
|
// auth method.
|
|
authMethod, err := stateSnapshot.GetACLAuthMethodByName(nil, args.AuthMethodName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if authMethod == nil {
|
|
return structs.NewErrRPCCodedf(
|
|
http.StatusBadRequest,
|
|
"auth-method %q not found",
|
|
args.AuthMethodName,
|
|
)
|
|
}
|
|
|
|
// If the authentication method generates global ACL tokens, we need to
|
|
// forward the request onto the authoritative regional leader.
|
|
if authMethod.TokenLocalityIsGlobal() {
|
|
args.Region = a.srv.config.AuthoritativeRegion
|
|
|
|
if done, err := a.srv.forward(structs.ACLLoginRPCMethod, args, args, reply); done {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Generate a context with a deadline. This is used when making remote HTTP
|
|
// requests.
|
|
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(aclLoginRequestExpiryTime))
|
|
defer cancel()
|
|
|
|
var claims map[string]interface{}
|
|
|
|
// Validate the token depending on its method type
|
|
switch authMethod.Type {
|
|
case structs.ACLAuthMethodTypeJWT:
|
|
claims, err = jwt.Validate(ctx, args.LoginToken, authMethod.Config)
|
|
if err != nil {
|
|
return structs.NewErrRPCCodedf(
|
|
http.StatusUnauthorized,
|
|
"unable to validate provided token: %v",
|
|
err,
|
|
)
|
|
}
|
|
default:
|
|
return structs.NewErrRPCCodedf(
|
|
http.StatusBadRequest,
|
|
"unsupported auth-method type: %s",
|
|
authMethod.Type,
|
|
)
|
|
}
|
|
|
|
// Create a new binder object based on the current state snapshot to
|
|
// provide consistency within the RPC handler.
|
|
jwtBinder := auth.NewBinder(stateSnapshot)
|
|
|
|
// Generate the data used by the go-bexpr selector that is an internal
|
|
// representation of the claims that can be understood by Nomad.
|
|
jwtClaims, err := auth.SelectorData(authMethod, claims, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tokenBindings, err := jwtBinder.Bind(authMethod, auth.NewIdentity(authMethod.Config, jwtClaims))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if tokenBindings.None() && !tokenBindings.Management {
|
|
return structs.NewErrRPCCoded(http.StatusBadRequest, "no role or policy bindings matched")
|
|
}
|
|
|
|
// Build our token RPC request. The RPC handler includes a lot of specific
|
|
// logic, so we do not want to call Raft directly or copy that here. In the
|
|
// future we should try and extract out the logic into an interface, or at
|
|
// least a separate function.
|
|
token := structs.ACLToken{
|
|
Name: "JWT-" + authMethod.Name,
|
|
Global: authMethod.TokenLocalityIsGlobal(),
|
|
ExpirationTTL: authMethod.MaxTokenTTL,
|
|
}
|
|
|
|
if tokenBindings.Management {
|
|
token.Type = structs.ACLManagementToken
|
|
} else {
|
|
token.Type = structs.ACLClientToken
|
|
token.Policies = tokenBindings.Policies
|
|
token.Roles = tokenBindings.Roles
|
|
}
|
|
|
|
tokenUpsertRequest := structs.ACLTokenUpsertRequest{
|
|
Tokens: []*structs.ACLToken{&token},
|
|
WriteRequest: structs.WriteRequest{
|
|
Region: a.srv.Region(),
|
|
AuthToken: a.srv.getLeaderAcl(),
|
|
},
|
|
}
|
|
|
|
var tokenUpsertReply structs.ACLTokenUpsertResponse
|
|
|
|
if err := a.upsertTokens(&tokenUpsertRequest, &tokenUpsertReply, stateSnapshot); err != nil {
|
|
return err
|
|
}
|
|
|
|
// The way the UpsertTokens RPC currently works, if we get no error, then
|
|
// we will have exactly the same number of tokens returned as we sent. It
|
|
// is therefore safe to assume we have 1 token.
|
|
reply.ACLToken = tokenUpsertReply.Tokens[0]
|
|
|
|
return nil
|
|
}
|