open-nomad/nomad/acl_endpoint.go

834 lines
22 KiB
Go
Raw Normal View History

package nomad
import (
"fmt"
2017-09-10 23:03:30 +00:00
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
metrics "github.com/armon/go-metrics"
memdb "github.com/hashicorp/go-memdb"
"github.com/hashicorp/nomad/helper/uuid"
"github.com/hashicorp/nomad/nomad/state"
"github.com/hashicorp/nomad/nomad/structs"
)
var (
// aclDisabled is returned when an ACL endpoint is hit but ACLs are not enabled
aclDisabled = fmt.Errorf("ACL support disabled")
)
2017-09-10 23:03:30 +00:00
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"
)
// ACL endpoint is used for manipulating ACL tokens and policies
type ACL struct {
srv *Server
}
2017-08-08 04:01:14 +00:00
// 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
}
args.Region = a.srv.config.AuthoritativeRegion
2017-08-08 04:01:14 +00:00
if done, err := a.srv.forward("ACL.UpsertPolicies", args, args, reply); done {
return err
}
2017-08-08 04:01:14 +00:00
defer metrics.MeasureSince([]string{"nomad", "acl", "upsert_policies"}, time.Now())
// Check management level permissions
2017-10-12 22:16:33 +00:00
if acl, err := a.srv.ResolveToken(args.AuthToken); err != nil {
return err
} else if acl == nil || !acl.IsManagement() {
return structs.ErrPermissionDenied
}
2017-08-08 04:01:14 +00:00
// Validate non-zero set of policies
if len(args.Policies) == 0 {
return fmt.Errorf("must specify as least one policy")
}
// Validate each policy, compute hash
2017-08-08 22:19:59 +00:00
for idx, policy := range args.Policies {
2017-08-12 21:11:49 +00:00
if err := policy.Validate(); err != nil {
return fmt.Errorf("policy %d invalid: %v", idx, err)
2017-08-08 22:19:59 +00:00
}
policy.SetHash()
2017-08-08 22:19:59 +00:00
}
2017-08-08 04:01:14 +00:00
// 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
}
args.Region = a.srv.config.AuthoritativeRegion
if done, err := a.srv.forward("ACL.DeletePolicies", args, args, reply); done {
return err
}
defer metrics.MeasureSince([]string{"nomad", "acl", "delete_policies"}, time.Now())
// Check management level permissions
2017-10-12 22:16:33 +00:00
if acl, err := a.srv.ResolveToken(args.AuthToken); 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 fmt.Errorf("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
}
if done, err := a.srv.forward("ACL.ListPolicies", args, args, reply); done {
return err
}
defer metrics.MeasureSince([]string{"nomad", "acl", "list_policies"}, time.Now())
// Check management level permissions
acl, err := a.srv.ResolveToken(args.AuthToken)
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()
var policies map[string]struct{}
if !mgt {
snap, err := a.srv.fsm.State().Snapshot()
if err != nil {
return err
}
token, err := snap.ACLTokenBySecretID(nil, args.AuthToken)
if err != nil {
return err
}
if token == nil {
return structs.ErrTokenNotFound
}
policies = make(map[string]struct{}, len(token.Policies))
for _, p := range token.Policies {
policies[p] = struct{}{}
}
}
// 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
}
policy := raw.(*structs.ACLPolicy)
if _, ok := policies[policy.Name]; ok || mgt {
reply.Policies = append(reply.Policies, policy.Stub())
}
}
// Use the last index that affected the policy table
index, err := state.Index("acl_policy")
if err != nil {
return err
}
2017-08-22 00:09:09 +00:00
// 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
}
if done, err := a.srv.forward("ACL.GetPolicy", args, args, reply); done {
return err
}
defer metrics.MeasureSince([]string{"nomad", "acl", "get_policy"}, time.Now())
// Check management level permissions
acl, err := a.srv.ResolveToken(args.AuthToken)
if err != nil {
return err
} else if acl == nil {
return structs.ErrPermissionDenied
}
// If it is not a management token determine if it can get this policy
mgt := acl.IsManagement()
if !mgt {
snap, err := a.srv.fsm.State().Snapshot()
if err != nil {
return err
}
token, err := snap.ACLTokenBySecretID(nil, args.AuthToken)
if err != nil {
return err
}
if token == nil {
return structs.ErrTokenNotFound
}
found := false
for _, p := range token.Policies {
if p == args.Name {
found = true
break
}
}
if !found {
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
} 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)
}
2017-08-12 22:44:05 +00:00
// 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
}
if done, err := a.srv.forward("ACL.GetPolicies", args, args, reply); done {
return err
}
defer metrics.MeasureSince([]string{"nomad", "acl", "get_policies"}, time.Now())
var token *structs.ACLToken
var err error
2017-10-12 22:16:33 +00:00
if args.AuthToken == "" {
// No need to look up the anonymous token
token = structs.AnonymousACLToken
} else {
// 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.
2017-10-12 22:16:33 +00:00
token, err = a.srv.State().ACLTokenBySecretID(nil, args.AuthToken)
if err != nil {
return err
}
}
if token == nil {
return structs.ErrTokenNotFound
}
if token.Type != structs.ACLManagementToken && !token.PolicySubset(args.Names) {
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.Policies = make(map[string]*structs.ACLPolicy, len(args.Names))
// Look for the policy
for _, policyName := range args.Names {
out, err := state.ACLPolicyByName(ws, policyName)
if err != nil {
return err
}
if out != nil {
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)
}
2017-08-21 01:19:26 +00:00
// 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
2017-08-21 01:19:26 +00:00
if done, err := a.srv.forward("ACL.Bootstrap", args, args, reply); done {
return err
}
defer metrics.MeasureSince([]string{"nomad", "acl", "bootstrap"}, time.Now())
2017-09-26 22:26:33 +00:00
// Always ignore the reset index from the arguments
2017-09-10 23:03:30 +00:00
args.ResetIndex = 0
2017-08-21 01:19:26 +00:00
// 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.
2017-09-10 23:03:30 +00:00
ok, resetIdx, err := state.CanBootstrapACLToken()
2017-08-21 01:19:26 +00:00
if err != nil {
return err
}
if !ok {
2017-09-10 23:03:30 +00:00
// Check if there is a reset index specified
specifiedIndex := a.fileBootstrapResetIndex()
if specifiedIndex == 0 {
return fmt.Errorf("ACL bootstrap already done (reset index: %d)", resetIdx)
} else if specifiedIndex != resetIdx {
return fmt.Errorf("Invalid bootstrap reset index (specified %d, reset index: %d)", specifiedIndex, resetIdx)
}
// Setup the reset index to allow bootstrapping again
args.ResetIndex = resetIdx
2017-08-21 01:19:26 +00:00
}
// Create a new global management token, override any parameter
args.Token = &structs.ACLToken{
AccessorID: uuid.Generate(),
SecretID: uuid.Generate(),
2017-08-21 01:19:26 +00:00
Name: "Bootstrap Token",
Type: structs.ACLManagementToken,
Global: true,
CreateTime: time.Now().UTC(),
}
args.Token.SetHash()
2017-08-21 01:19:26 +00:00
// 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 fmt.Errorf("token lookup failed: %v", err)
}
reply.Tokens = append(reply.Tokens, out)
// Update the index
reply.Index = index
return nil
}
2017-09-10 23:03:30 +00:00
// 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 := ioutil.ReadFile(path)
if err != nil {
if !os.IsNotExist(err) {
a.srv.logger.Printf("[ERR] acl.bootstrap: failed to read %q: %v", path, err)
}
return 0
}
// Attempt to parse the file
var resetIdx uint64
if _, err := fmt.Sscanf(string(raw), "%d", &resetIdx); err != nil {
a.srv.logger.Printf("[ERR] acl.bootstrap: failed to parse %q: %v", path, err)
return 0
}
// Return the reset index
a.srv.logger.Printf("[WARN] acl.bootstrap: parsed %q: reset index %d", path, resetIdx)
return resetIdx
}
2017-08-12 22:44:05 +00:00
// 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
}
// Validate non-zero set of tokens
if len(args.Tokens) == 0 {
return fmt.Errorf("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 fmt.Errorf("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
}
2017-08-12 22:44:05 +00:00
if done, err := a.srv.forward("ACL.UpsertTokens", args, args, reply); done {
return err
}
defer metrics.MeasureSince([]string{"nomad", "acl", "upsert_tokens"}, time.Now())
// Check management level permissions
2017-10-12 22:16:33 +00:00
if acl, err := a.srv.ResolveToken(args.AuthToken); 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
}
2017-08-12 22:44:05 +00:00
// Validate each token
for idx, token := range args.Tokens {
if err := token.Validate(); err != nil {
return fmt.Errorf("token %d invalid: %v", idx, err)
}
// Generate an accessor and secret ID if new
if token.AccessorID == "" {
token.AccessorID = uuid.Generate()
token.SecretID = uuid.Generate()
token.CreateTime = time.Now().UTC()
} else {
// Verify the token exists
out, err := state.ACLTokenByAccessorID(nil, token.AccessorID)
if err != nil {
return fmt.Errorf("token lookup failed: %v", err)
}
if out == nil {
return fmt.Errorf("cannot find token %s", token.AccessorID)
}
// Cannot toggle the "Global" mode
if token.Global != out.Global {
return fmt.Errorf("cannot toggle global mode of %s", token.AccessorID)
}
}
// Compute the token hash
token.SetHash()
2017-08-12 22:44:05 +00:00
}
// 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
// pickup the proper create / modify times.
state, err = a.srv.State().Snapshot()
if err != nil {
return err
}
for _, token := range args.Tokens {
out, err := state.ACLTokenByAccessorID(nil, token.AccessorID)
if err != nil {
return fmt.Errorf("token lookup failed: %v", err)
}
reply.Tokens = append(reply.Tokens, out)
}
2017-08-12 22:44:05 +00:00
// 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
}
// Validate non-zero set of tokens
if len(args.AccessorIDs) == 0 {
return fmt.Errorf("must specify as least one token")
}
2017-08-12 22:44:05 +00:00
if done, err := a.srv.forward("ACL.DeleteTokens", args, args, reply); done {
return err
}
defer metrics.MeasureSince([]string{"nomad", "acl", "delete_tokens"}, time.Now())
// Check management level permissions
2017-10-12 22:16:33 +00:00
if acl, err := a.srv.ResolveToken(args.AuthToken); 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
2018-03-11 18:30:37 +00:00
nonExistentTokens := make([]string, 0)
for _, accessor := range args.AccessorIDs {
token, err := state.ACLTokenByAccessorID(nil, accessor)
if err != nil {
return fmt.Errorf("token lookup failed: %v", err)
}
2017-10-16 19:47:13 +00:00
if token == nil {
2018-03-11 18:30:37 +00:00
nonExistentTokens = append(nonExistentTokens, accessor)
continue
}
if token.Global {
hasGlobal = true
} else {
allGlobal = false
}
}
2018-03-11 18:30:37 +00:00
if len(nonExistentTokens) != 0 {
return fmt.Errorf("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 fmt.Errorf("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
}
2017-08-12 22:44:05 +00:00
}
// 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
}
2017-08-12 22:44:05 +00:00
if done, err := a.srv.forward("ACL.ListTokens", args, args, reply); done {
return err
}
defer metrics.MeasureSince([]string{"nomad", "acl", "list_tokens"}, time.Now())
// Check management level permissions
2017-10-12 22:16:33 +00:00
if acl, err := a.srv.ResolveToken(args.AuthToken); err != nil {
return err
} else if acl == nil || !acl.IsManagement() {
return structs.ErrPermissionDenied
}
2017-08-12 22:44:05 +00:00
// 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 tokens
var err error
var iter memdb.ResultIterator
if prefix := args.QueryOptions.Prefix; prefix != "" {
iter, err = state.ACLTokenByAccessorIDPrefix(ws, prefix)
} else if args.GlobalOnly {
iter, err = state.ACLTokensByGlobal(ws, true)
2017-08-12 22:44:05 +00:00
} else {
iter, err = state.ACLTokens(ws)
}
if err != nil {
return err
}
// Convert all the tokens to a list stub
reply.Tokens = nil
for {
raw := iter.Next()
if raw == nil {
break
}
token := raw.(*structs.ACLToken)
reply.Tokens = append(reply.Tokens, token.Stub())
2017-08-12 22:44:05 +00:00
}
// 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
}
2017-08-12 22:44:05 +00:00
if done, err := a.srv.forward("ACL.GetToken", args, args, reply); done {
return err
}
defer metrics.MeasureSince([]string{"nomad", "acl", "get_token"}, time.Now())
2017-10-12 22:16:33 +00:00
acl, err := a.srv.ResolveToken(args.AuthToken)
if err != nil {
return err
}
2017-09-27 20:42:56 +00:00
// Ensure ACLs are enabled and this call is made with one
if acl == nil {
return structs.ErrPermissionDenied
}
2017-08-12 22:44:05 +00:00
// 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)
2017-08-12 22:44:05 +00:00
if err != nil {
return err
}
2017-09-27 20:42:56 +00:00
if out == nil {
// If the token doesn't resolve, only allow management tokens to
// block.
if !acl.IsManagement() {
return structs.ErrPermissionDenied
}
2017-09-27 20:42:56 +00:00
// Check management level permissions or that the secret ID matches the
// accessor ID
2017-10-12 22:16:33 +00:00
} else if !acl.IsManagement() && out.SecretID != args.AuthToken {
2017-09-27 20:42:56 +00:00
return structs.ErrPermissionDenied
}
2017-08-12 22:44:05 +00:00
// 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
}
if done, err := a.srv.forward("ACL.GetTokens", args, args, reply); done {
return err
}
defer metrics.MeasureSince([]string{"nomad", "acl", "get_tokens"}, time.Now())
// Check management level permissions
2017-10-12 22:16:33 +00:00
if acl, err := a.srv.ResolveToken(args.AuthToken); 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. This is used for enforcing ACLs by clients.
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
}
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
}