open-consul/agent/consul/acl_endpoint.go
Matt Keeler 99e0a124cb
New ACLs (#4791)
This PR is almost a complete rewrite of the ACL system within Consul. It brings the features more in line with other HashiCorp products. Obviously there is quite a bit left to do here but most of it is related docs, testing and finishing the last few commands in the CLI. I will update the PR description and check off the todos as I finish them over the next few days/week.
Description

At a high level this PR is mainly to split ACL tokens from Policies and to split the concepts of Authorization from Identities. A lot of this PR is mostly just to support CRUD operations on ACLTokens and ACLPolicies. These in and of themselves are not particularly interesting. The bigger conceptual changes are in how tokens get resolved, how backwards compatibility is handled and the separation of policy from identity which could lead the way to allowing for alternative identity providers.

On the surface and with a new cluster the ACL system will look very similar to that of Nomads. Both have tokens and policies. Both have local tokens. The ACL management APIs for both are very similar. I even ripped off Nomad's ACL bootstrap resetting procedure. There are a few key differences though.

    Nomad requires token and policy replication where Consul only requires policy replication with token replication being opt-in. In Consul local tokens only work with token replication being enabled though.
    All policies in Nomad are globally applicable. In Consul all policies are stored and replicated globally but can be scoped to a subset of the datacenters. This allows for more granular access management.
    Unlike Nomad, Consul has legacy baggage in the form of the original ACL system. The ramifications of this are:
        A server running the new system must still support other clients using the legacy system.
        A client running the new system must be able to use the legacy RPCs when the servers in its datacenter are running the legacy system.
        The primary ACL DC's servers running in legacy mode needs to be a gate that keeps everything else in the entire multi-DC cluster running in legacy mode.

So not only does this PR implement the new ACL system but has a legacy mode built in for when the cluster isn't ready for new ACLs. Also detecting that new ACLs can be used is automatic and requires no configuration on the part of administrators. This process is detailed more in the "Transitioning from Legacy to New ACL Mode" section below.
2018-10-19 12:04:07 -04:00

952 lines
26 KiB
Go

package consul
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"time"
"github.com/armon/go-metrics"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/consul/state"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/lib"
"github.com/hashicorp/go-memdb"
"github.com/hashicorp/go-uuid"
)
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"
)
// Regex for matching
var validPolicyName = regexp.MustCompile(`^[A-Za-z0-9\-_]{1,128}$`)
// ACL endpoint is used to manipulate ACLs
type ACL struct {
srv *Server
}
// fileBootstrapResetIndex retrieves the reset index specified by the adminstrator from
// the file on disk.
//
// Q: What is the bootstrap reset index?
// A: If you happen to lose acess to all tokens capable of ACL management you need a way
// to get back into your system. This allows an admin to write the current
// bootstrap "index" into a special file on disk to override the mechanism preventing
// a second token bootstrap. The index will be retrieved by a API call to /v1/acl/bootstrap
// When already bootstrapped this API will return the reset index necessary within
// the error response. Once set in the file, the bootstrap API can be used again to
// get a new token.
//
// Q: Why is the reset index not in the config?
// A: We want to be able to remove the reset index once we have used it. This prevents
// accidentally allowing bootstrapping yet again after a snapshot restore.
//
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("[DEBUG] acl.bootstrap: parsed %q: reset index %d", path, resetIdx)
return resetIdx
}
func (a *ACL) removeBootstrapResetFile() {
if err := os.Remove(filepath.Join(a.srv.config.DataDir, aclBootstrapReset)); err != nil {
a.srv.logger.Printf("[WARN] acl.bootstrap: failed to remove bootstrap file: %v", err)
}
}
func (a *ACL) aclPreCheck() error {
if !a.srv.ACLsEnabled() {
return acl.ErrDisabled
}
if a.srv.UseLegacyACLs() {
return fmt.Errorf("The ACL system is currently in legacy mode.")
}
return nil
}
// Bootstrap is used to perform a one-time ACL bootstrap operation on
// a cluster to get the first management token.
func (a *ACL) BootstrapTokens(args *structs.DCSpecificRequest, reply *structs.ACLToken) error {
if err := a.aclPreCheck(); err != nil {
return err
}
if done, err := a.srv.forward("ACL.BootstrapTokens", args, args, reply); done {
return err
}
// Verify we are allowed to serve this request
if !a.srv.InACLDatacenter() {
return acl.ErrDisabled
}
// By doing some pre-checks we can head off later bootstrap attempts
// without having to run them through Raft, which should curb abuse.
state := a.srv.fsm.State()
allowed, resetIdx, err := state.CanBootstrapACLToken()
if err != nil {
return err
}
var specifiedIndex uint64 = 0
if !allowed {
// Check if there is a reset index specified
specifiedIndex = a.fileBootstrapResetIndex()
if specifiedIndex == 0 {
return fmt.Errorf("ACL bootstrap no longer allowed (reset index: %d)", resetIdx)
} else if specifiedIndex != resetIdx {
return fmt.Errorf("Invalid bootstrap reset index (specified %d, reset index: %d)", specifiedIndex, resetIdx)
}
}
// remove the bootstrap override file now that we have the index from it and it was valid.
// whether bootstrapping works or not is irrelevant as we really don't want this file hanging around
// in case a snapshot restore is done. In that case we don't want to accidentally allow re-bootstrapping
// just becuase the file was unchanged.
a.removeBootstrapResetFile()
accessor, err := lib.GenerateUUID(a.srv.checkTokenUUID)
if err != nil {
return err
}
secret, err := lib.GenerateUUID(a.srv.checkTokenUUID)
if err != nil {
return err
}
req := structs.ACLTokenBootstrapRequest{
Token: structs.ACLToken{
AccessorID: accessor,
SecretID: secret,
Description: "Bootstrap Token (Global Management)",
Policies: []structs.ACLTokenPolicyLink{
{
ID: structs.ACLPolicyGlobalManagementID,
},
},
CreateTime: time.Now(),
Local: false,
// DEPRECATED (ACL-Legacy-Compat) - This is used so that the bootstrap token is still visible via the v1 acl APIs
Type: structs.ACLTokenTypeManagement,
},
ResetIndex: specifiedIndex,
}
req.Token.SetHash(true)
resp, err := a.srv.raftApply(structs.ACLBootstrapRequestType, &req)
if err != nil {
return err
}
if err, ok := resp.(error); ok {
return err
}
if _, token, err := state.ACLTokenGetByAccessor(nil, accessor); err == nil {
*reply = *token
}
a.srv.logger.Printf("[INFO] consul.acl: ACL bootstrap completed")
return nil
}
func (a *ACL) TokenRead(args *structs.ACLTokenReadRequest, reply *structs.ACLTokenResponse) error {
if err := a.aclPreCheck(); err != nil {
return err
}
// clients will not know whether the server has local token store. In the case
// where it doesnt' we will transparently forward requests.
if !a.srv.LocalTokensEnabled() {
args.Datacenter = a.srv.config.ACLDatacenter
}
if done, err := a.srv.forward("ACL.TokenRead", args, args, reply); done {
return err
}
var rule acl.Authorizer
if args.TokenIDType == structs.ACLTokenAccessor {
var err error
// Only ACLRead privileges are required to list tokens
// However if you do not have ACLWrite as well the token
// secrets will be redacted
if rule, err = a.srv.ResolveToken(args.Token); err != nil {
return err
} else if rule == nil || !rule.ACLRead() {
return acl.ErrPermissionDenied
}
}
return a.srv.blockingQuery(&args.QueryOptions, &reply.QueryMeta,
func(ws memdb.WatchSet, state *state.Store) error {
var index uint64
var token *structs.ACLToken
var err error
if args.TokenIDType == structs.ACLTokenAccessor {
index, token, err = state.ACLTokenGetByAccessor(ws, args.TokenID)
if token != nil {
a.srv.filterACLWithAuthorizer(rule, &token)
}
} else {
index, token, err = state.ACLTokenGetBySecret(ws, args.TokenID)
}
if err != nil {
return err
}
reply.Index, reply.Token = index, token
return nil
})
}
func (a *ACL) TokenClone(args *structs.ACLTokenUpsertRequest, reply *structs.ACLToken) error {
if err := a.aclPreCheck(); err != nil {
return err
}
// clients will not know whether the server has local token store. In the case
// where it doesnt' we will transparently forward requests.
if !a.srv.LocalTokensEnabled() {
args.Datacenter = a.srv.config.ACLDatacenter
}
if done, err := a.srv.forward("ACL.TokenClone", args, args, reply); done {
return err
}
defer metrics.MeasureSince([]string{"acl", "token", "clone"}, time.Now())
if rule, err := a.srv.ResolveToken(args.Token); err != nil {
return err
} else if rule == nil || !rule.ACLWrite() {
return acl.ErrPermissionDenied
}
_, token, err := a.srv.fsm.State().ACLTokenGetByAccessor(nil, args.ACLToken.AccessorID)
if err != nil {
return err
} else if token == nil {
return acl.ErrNotFound
} else if !a.srv.InACLDatacenter() && !token.Local {
// global token writes must be forwarded to the primary DC
args.Datacenter = a.srv.config.ACLDatacenter
return a.srv.forwardDC("ACL.TokenClone", a.srv.config.ACLDatacenter, args, reply)
}
if token.Rules != "" {
return fmt.Errorf("Cannot clone a legacy ACL with this endpoint")
}
cloneReq := structs.ACLTokenUpsertRequest{
Datacenter: args.Datacenter,
ACLToken: structs.ACLToken{
Policies: token.Policies,
Local: token.Local,
Description: token.Description,
},
WriteRequest: args.WriteRequest,
}
if args.ACLToken.Description != "" {
cloneReq.ACLToken.Description = args.ACLToken.Description
}
return a.tokenUpsertInternal(&cloneReq, reply, false)
}
func (a *ACL) TokenUpsert(args *structs.ACLTokenUpsertRequest, reply *structs.ACLToken) error {
if err := a.aclPreCheck(); err != nil {
return err
}
// Global token creation/modification always goes to the ACL DC
if !args.ACLToken.Local {
args.Datacenter = a.srv.config.ACLDatacenter
} else if !a.srv.LocalTokensEnabled() {
return fmt.Errorf("Local tokens are disabled")
}
if done, err := a.srv.forward("ACL.TokenUpsert", args, args, reply); done {
return err
}
defer metrics.MeasureSince([]string{"acl", "token", "upsert"}, time.Now())
// Verify token is permitted to modify ACLs
if rule, err := a.srv.ResolveToken(args.Token); err != nil {
return err
} else if rule == nil || !rule.ACLWrite() {
return acl.ErrPermissionDenied
}
return a.tokenUpsertInternal(args, reply, false)
}
func (a *ACL) tokenUpsertInternal(args *structs.ACLTokenUpsertRequest, reply *structs.ACLToken, upgrade bool) error {
token := &args.ACLToken
if !a.srv.LocalTokensEnabled() {
// local token operations
return fmt.Errorf("Cannot upsert tokens within this datacenter")
} else if !a.srv.InACLDatacenter() && !token.Local {
return fmt.Errorf("Cannot upsert global tokens within this datacenter")
}
state := a.srv.fsm.State()
if token.AccessorID == "" {
// Token Create
var err error
// Generate the AccessorID
token.AccessorID, err = lib.GenerateUUID(a.srv.checkTokenUUID)
if err != nil {
return err
}
// Generate the SecretID - not supporting non-UUID secrets
token.SecretID, err = lib.GenerateUUID(a.srv.checkTokenUUID)
if err != nil {
return err
}
token.CreateTime = time.Now()
} else {
// Token Update
if _, err := uuid.ParseUUID(token.AccessorID); err != nil {
return fmt.Errorf("AccessorID is not a valid UUID")
}
// DEPRECATED (ACL-Legacy-Compat) - maybe get rid of this in the future
// and instead do a ParseUUID check. New tokens will not have
// secrets generated by users but rather they will always be UUIDs.
// However if users just continue the upgrade cycle they may still
// have tokens using secrets that are not UUIDS
// The RootAuthorizer checks that the SecretID is not "allow", "deny"
// or "manage" as a precaution against something accidentally using
// one of these root policies by setting the secret to it.
if acl.RootAuthorizer(token.SecretID) != nil {
return acl.PermissionDeniedError{Cause: "Cannot modify root ACL"}
}
// Verify the token exists
_, existing, err := state.ACLTokenGetByAccessor(nil, token.AccessorID)
if err != nil {
return fmt.Errorf("Failed to lookup the acl token %q: %v", token.AccessorID, err)
}
if existing == nil {
return fmt.Errorf("Cannot find token %q", token.AccessorID)
}
if token.SecretID == "" {
token.SecretID = existing.SecretID
} else if existing.SecretID != token.SecretID {
return fmt.Errorf("Changing a tokens SecretID is not permitted")
}
// Cannot toggle the "Global" mode
if token.Local != existing.Local {
return fmt.Errorf("cannot toggle local mode of %s", token.AccessorID)
}
if upgrade {
token.CreateTime = time.Now()
} else {
token.CreateTime = existing.CreateTime
}
}
policyIDs := make(map[string]struct{})
var policies []structs.ACLTokenPolicyLink
// Validate all the policy names and convert them to policy IDs
for _, link := range token.Policies {
if link.ID == "" {
_, policy, err := state.ACLPolicyGetByName(nil, link.Name)
if err != nil {
return fmt.Errorf("Error looking up policy for name %q: %v", link.Name, err)
}
if policy == nil {
return fmt.Errorf("No such ACL policy with name %q", link.Name)
}
link.ID = policy.ID
}
// Do not store the policy name within raft/memdb as the policy could be renamed in the future.
link.Name = ""
// dedup policy links by id
if _, ok := policyIDs[link.ID]; !ok {
policies = append(policies, link)
policyIDs[link.ID] = struct{}{}
}
}
token.Policies = policies
if token.Rules != "" {
return fmt.Errorf("Rules cannot be specified for this token")
}
if token.Type != "" {
return fmt.Errorf("Type cannot be specified for this token")
}
token.SetHash(true)
req := &structs.ACLTokenBatchUpsertRequest{
Tokens: structs.ACLTokens{token},
AllowCreate: true,
}
resp, err := a.srv.raftApply(structs.ACLTokenUpsertRequestType, req)
if err != nil {
return fmt.Errorf("Failed to apply token write request: %v", err)
}
// Purge the identity from the cache to prevent using the previous definition of the identity
a.srv.acls.cache.RemoveIdentity(token.SecretID)
if respErr, ok := resp.(error); ok {
return respErr
}
if _, updatedToken, err := a.srv.fsm.State().ACLTokenGetByAccessor(nil, token.AccessorID); err == nil && token != nil {
*reply = *updatedToken
} else {
return fmt.Errorf("Failed to retrieve the token after insertion")
}
return nil
}
func (a *ACL) TokenDelete(args *structs.ACLTokenDeleteRequest, reply *string) error {
if err := a.aclPreCheck(); err != nil {
return err
}
if !a.srv.LocalTokensEnabled() {
args.Datacenter = a.srv.config.ACLDatacenter
}
if done, err := a.srv.forward("ACL.TokenDelete", args, args, reply); done {
return err
}
defer metrics.MeasureSince([]string{"acl", "token", "delete"}, time.Now())
// Verify token is permitted to modify ACLs
if rule, err := a.srv.ResolveToken(args.Token); err != nil {
return err
} else if rule == nil || !rule.ACLWrite() {
return acl.ErrPermissionDenied
}
if _, err := uuid.ParseUUID(args.TokenID); err != nil {
return fmt.Errorf("Accessor ID is missing or an invalid UUID")
}
if args.TokenID == structs.ACLTokenAnonymousID {
return fmt.Errorf("Delete operation not permitted on the anonymous token")
}
// grab the token here so we can invalidate our cache later on
_, token, err := a.srv.fsm.State().ACLTokenGetByAccessor(nil, args.TokenID)
if err != nil {
return err
}
if !a.srv.InACLDatacenter() && !token.Local {
args.Datacenter = a.srv.config.ACLDatacenter
return a.srv.forwardDC("ACL.TokenDelete", a.srv.config.ACLDatacenter, args, reply)
}
req := &structs.ACLTokenBatchDeleteRequest{
TokenIDs: []string{args.TokenID},
}
resp, err := a.srv.raftApply(structs.ACLTokenDeleteRequestType, req)
if err != nil {
return fmt.Errorf("Failed to apply token delete request: %v", err)
}
// Purge the identity from the cache to prevent using the previous definition of the identity
if token != nil {
a.srv.acls.cache.RemoveIdentity(token.SecretID)
}
if respErr, ok := resp.(error); ok {
return respErr
}
if reply != nil && token != nil {
*reply = token.AccessorID
}
return nil
}
func (a *ACL) TokenList(args *structs.ACLTokenListRequest, reply *structs.ACLTokenListResponse) error {
if err := a.aclPreCheck(); err != nil {
return err
}
if !a.srv.LocalTokensEnabled() {
if args.Datacenter != a.srv.config.ACLDatacenter {
args.Datacenter = a.srv.config.ACLDatacenter
args.IncludeLocal = false
args.IncludeGlobal = true
}
args.Datacenter = a.srv.config.ACLDatacenter
}
if done, err := a.srv.forward("ACL.TokenList", args, args, reply); done {
return err
}
rule, err := a.srv.ResolveToken(args.Token)
if err != nil {
return err
} else if rule == nil || !rule.ACLRead() {
return acl.ErrPermissionDenied
}
return a.srv.blockingQuery(&args.QueryOptions, &reply.QueryMeta,
func(ws memdb.WatchSet, state *state.Store) error {
index, tokens, err := state.ACLTokenList(ws, args.IncludeLocal, args.IncludeGlobal, args.Policy)
if err != nil {
return err
}
stubs := make([]*structs.ACLTokenListStub, 0, len(tokens))
for _, token := range tokens {
stubs = append(stubs, token.Stub())
}
reply.Index, reply.Tokens = index, stubs
return nil
})
}
func (a *ACL) TokenBatchRead(args *structs.ACLTokenBatchReadRequest, reply *structs.ACLTokensResponse) error {
if err := a.aclPreCheck(); err != nil {
return err
}
if !a.srv.LocalTokensEnabled() {
args.Datacenter = a.srv.config.ACLDatacenter
}
if done, err := a.srv.forward("ACL.TokenBatchRead", args, args, reply); done {
return err
}
rule, err := a.srv.ResolveToken(args.Token)
if err != nil {
return err
} else if rule == nil || !rule.ACLRead() {
return acl.ErrPermissionDenied
}
return a.srv.blockingQuery(&args.QueryOptions, &reply.QueryMeta,
func(ws memdb.WatchSet, state *state.Store) error {
index, tokens, err := state.ACLTokenBatchRead(ws, args.AccessorIDs)
if err != nil {
return err
}
a.srv.filterACLWithAuthorizer(rule, &tokens)
reply.Index, reply.Tokens = index, tokens
return nil
})
}
func (a *ACL) PolicyRead(args *structs.ACLPolicyReadRequest, reply *structs.ACLPolicyResponse) error {
if err := a.aclPreCheck(); err != nil {
return err
}
if done, err := a.srv.forward("ACL.PolicyRead", args, args, reply); done {
return err
}
if rule, err := a.srv.ResolveToken(args.Token); err != nil {
return err
} else if rule == nil || !rule.ACLRead() {
return acl.ErrPermissionDenied
}
return a.srv.blockingQuery(&args.QueryOptions, &reply.QueryMeta,
func(ws memdb.WatchSet, state *state.Store) error {
index, policy, err := state.ACLPolicyGetByID(ws, args.PolicyID)
if err != nil {
return err
}
reply.Index, reply.Policy = index, policy
return nil
})
}
func (a *ACL) PolicyBatchRead(args *structs.ACLPolicyBatchReadRequest, reply *structs.ACLPoliciesResponse) error {
if err := a.aclPreCheck(); err != nil {
return err
}
if done, err := a.srv.forward("ACL.PolicyBatchRead", args, args, reply); done {
return err
}
if rule, err := a.srv.ResolveToken(args.Token); err != nil {
return err
} else if rule == nil || !rule.ACLRead() {
return acl.ErrPermissionDenied
}
return a.srv.blockingQuery(&args.QueryOptions, &reply.QueryMeta,
func(ws memdb.WatchSet, state *state.Store) error {
index, policies, err := state.ACLPolicyBatchRead(ws, args.PolicyIDs)
if err != nil {
return err
}
reply.Index, reply.Policies = index, policies
return nil
})
}
func (a *ACL) PolicyUpsert(args *structs.ACLPolicyUpsertRequest, reply *structs.ACLPolicy) error {
if err := a.aclPreCheck(); err != nil {
return err
}
if !a.srv.InACLDatacenter() {
args.Datacenter = a.srv.config.ACLDatacenter
}
if done, err := a.srv.forward("ACL.PolicyUpsert", args, args, reply); done {
return err
}
defer metrics.MeasureSince([]string{"acl", "policy", "upsert"}, time.Now())
// Verify token is permitted to modify ACLs
if rule, err := a.srv.ResolveToken(args.Token); err != nil {
return err
} else if rule == nil || !rule.ACLWrite() {
return acl.ErrPermissionDenied
}
policy := &args.Policy
state := a.srv.fsm.State()
// Almost all of the checks here are also done in the state store. However,
// we want to prevent the raft operations when we know they are going to fail
// so we still do them here.
// ensure a name is set
if policy.Name == "" {
return fmt.Errorf("Invalid Policy: no Name is set")
}
if !validPolicyName.MatchString(policy.Name) {
return fmt.Errorf("Invalid Policy: invalid Name. Only alphanumeric characters, '-' and '_' are allowed")
}
if policy.ID == "" {
// with no policy ID one will be generated
var err error
policy.ID, err = lib.GenerateUUID(a.srv.checkPolicyUUID)
if err != nil {
return err
}
// validate the name is unique
if _, existing, err := state.ACLPolicyGetByName(nil, policy.Name); err != nil {
return fmt.Errorf("acl policy lookup by name failed: %v", err)
} else if existing != nil {
return fmt.Errorf("Invalid Policy: A Policy with Name %q already exists", policy.Name)
}
} else {
if _, err := uuid.ParseUUID(policy.ID); err != nil {
return fmt.Errorf("Policy ID invalid UUID")
}
// Verify the policy exists
_, existing, err := state.ACLPolicyGetByID(nil, policy.ID)
if err != nil {
return fmt.Errorf("acl policy lookup failed: %v", err)
} else if existing == nil {
return fmt.Errorf("cannot find policy %s", policy.ID)
}
if existing.Name != policy.Name {
if _, nameMatch, err := state.ACLPolicyGetByName(nil, policy.Name); err != nil {
return fmt.Errorf("acl policy lookup by name failed: %v", err)
} else if nameMatch != nil {
return fmt.Errorf("Invalid Policy: A policy with name %q already exists", policy.Name)
}
}
if policy.ID == structs.ACLPolicyGlobalManagementID {
if policy.Datacenters != nil || len(policy.Datacenters) > 0 {
return fmt.Errorf("Changing the Datacenters of the builtin global-management policy is not permitted")
}
if policy.Rules != existing.Rules {
return fmt.Errorf("Changing the Rules for the builtin global-management policy is not permitted")
}
}
}
// validate the rules
_, err := acl.NewPolicyFromSource("", 0, policy.Rules, policy.Syntax, a.srv.sentinel)
if err != nil {
return err
}
// calcualte the hash for this policy
policy.SetHash(true)
req := &structs.ACLPolicyBatchUpsertRequest{
Policies: structs.ACLPolicies{policy},
}
resp, err := a.srv.raftApply(structs.ACLPolicyUpsertRequestType, req)
if err != nil {
return fmt.Errorf("Failed to apply policy upsert request: %v", err)
}
// Remove from the cache to prevent stale cache usage
a.srv.acls.cache.RemovePolicy(policy.ID)
if respErr, ok := resp.(error); ok {
return respErr
}
if _, policy, err := a.srv.fsm.State().ACLPolicyGetByID(nil, policy.ID); err == nil && policy != nil {
*reply = *policy
}
return nil
}
func (a *ACL) PolicyDelete(args *structs.ACLPolicyDeleteRequest, reply *string) error {
if err := a.aclPreCheck(); err != nil {
return err
}
if !a.srv.InACLDatacenter() {
args.Datacenter = a.srv.config.ACLDatacenter
}
if done, err := a.srv.forward("ACL.PolicyDelete", args, args, reply); done {
return err
}
defer metrics.MeasureSince([]string{"acl", "policy", "delete"}, time.Now())
// Verify token is permitted to modify ACLs
if rule, err := a.srv.ResolveToken(args.Token); err != nil {
return err
} else if rule == nil || !rule.ACLWrite() {
return acl.ErrPermissionDenied
}
_, policy, err := a.srv.fsm.State().ACLPolicyGetByID(nil, args.PolicyID)
if err != nil {
return err
}
if policy == nil {
return nil
}
if policy.ID == structs.ACLPolicyGlobalManagementID {
return fmt.Errorf("Delete operation not permitted on the builtin global-management policy")
}
req := structs.ACLPolicyBatchDeleteRequest{
PolicyIDs: []string{args.PolicyID},
}
resp, err := a.srv.raftApply(structs.ACLPolicyDeleteRequestType, &req)
if err != nil {
return fmt.Errorf("Failed to apply policy delete request: %v", err)
}
a.srv.acls.cache.RemovePolicy(policy.ID)
if resp == nil {
return nil
}
if respErr, ok := resp.(error); ok {
return respErr
}
if policy != nil {
*reply = policy.Name
}
return nil
}
func (a *ACL) PolicyList(args *structs.ACLPolicyListRequest, reply *structs.ACLPolicyListResponse) error {
if err := a.aclPreCheck(); err != nil {
return err
}
if done, err := a.srv.forward("ACL.PolicyList", args, args, reply); done {
return err
}
if rule, err := a.srv.ResolveToken(args.Token); err != nil {
return err
} else if rule == nil || !rule.ACLRead() {
return acl.ErrPermissionDenied
}
return a.srv.blockingQuery(&args.QueryOptions, &reply.QueryMeta,
func(ws memdb.WatchSet, state *state.Store) error {
index, policies, err := state.ACLPolicyList(ws, args.DCScope)
if err != nil {
return err
}
var stubs structs.ACLPolicyListStubs
for _, policy := range policies {
stubs = append(stubs, policy.Stub())
}
reply.Index, reply.Policies = index, stubs
return nil
})
}
// PolicyResolve is used to retrieve a subset of the policies associated with a given token
// The policy ids in the args simply act as a filter on the policy set assigned to the token
func (a *ACL) PolicyResolve(args *structs.ACLPolicyBatchReadRequest, reply *structs.ACLPoliciesResponse) error {
if err := a.aclPreCheck(); err != nil {
return err
}
if done, err := a.srv.forward("ACL.PolicyResolve", args, args, reply); done {
return err
}
// get full list of policies for this token
policies, err := a.srv.acls.resolveTokenToPolicies(args.Token)
if err != nil {
return err
}
idMap := make(map[string]*structs.ACLPolicy)
for _, policy := range policies {
idMap[policy.ID] = policy
}
for _, policyID := range args.PolicyIDs {
if policy, ok := idMap[policyID]; ok {
reply.Policies = append(reply.Policies, policy)
}
}
a.srv.setQueryMeta(&reply.QueryMeta)
return nil
}
// makeACLETag returns an ETag for the given parent and policy.
func makeACLETag(parent string, policy *acl.Policy) string {
return fmt.Sprintf("%s:%s", parent, policy.ID)
}
// GetPolicy is used to retrieve a compiled policy object with a TTL. Does not
// support a blocking query.
func (a *ACL) GetPolicy(args *structs.ACLPolicyResolveLegacyRequest, reply *structs.ACLPolicyResolveLegacyResponse) error {
if done, err := a.srv.forward("ACL.GetPolicy", args, args, reply); done {
return err
}
// Verify we are allowed to serve this request
if a.srv.config.ACLDatacenter != a.srv.config.Datacenter {
return acl.ErrDisabled
}
// Get the policy via the cache
parent := a.srv.config.ACLDefaultPolicy
policy, err := a.srv.acls.GetMergedPolicyForToken(args.ACL)
if err != nil {
return err
}
// translates the structures internals to most closely match what could be expressed in the original rule language
policy = policy.ConvertToLegacy()
// Generate an ETag
etag := makeACLETag(parent, policy)
// Setup the response
reply.ETag = etag
reply.TTL = a.srv.config.ACLTokenTTL
a.srv.setQueryMeta(&reply.QueryMeta)
// Only send the policy on an Etag mis-match
if args.ETag != etag {
reply.Parent = parent
reply.Policy = policy
}
return nil
}
// ReplicationStatus is used to retrieve the current ACL replication status.
func (a *ACL) ReplicationStatus(args *structs.DCSpecificRequest,
reply *structs.ACLReplicationStatus) error {
// This must be sent to the leader, so we fix the args since we are
// re-using a structure where we don't support all the options.
args.RequireConsistent = true
args.AllowStale = false
if done, err := a.srv.forward("ACL.ReplicationStatus", args, args, reply); done {
return err
}
// There's no ACL token required here since this doesn't leak any
// sensitive information, and we don't want people to have to use
// management tokens if they are querying this via a health check.
// Poll the latest status.
a.srv.aclReplicationStatusLock.RLock()
*reply = a.srv.aclReplicationStatus
a.srv.aclReplicationStatusLock.RUnlock()
return nil
}