99e0a124cb
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.
805 lines
21 KiB
Go
805 lines
21 KiB
Go
package state
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/hashicorp/consul/agent/structs"
|
|
"github.com/hashicorp/go-memdb"
|
|
)
|
|
|
|
type TokenPoliciesIndex struct {
|
|
}
|
|
|
|
func (s *TokenPoliciesIndex) FromObject(obj interface{}) (bool, [][]byte, error) {
|
|
token, ok := obj.(*structs.ACLToken)
|
|
if !ok {
|
|
return false, nil, fmt.Errorf("object is not an ACLTokenPolicyLink")
|
|
}
|
|
|
|
links := token.Policies
|
|
|
|
numLinks := len(links)
|
|
if numLinks == 0 {
|
|
return false, nil, nil
|
|
}
|
|
|
|
vals := make([][]byte, 0, numLinks)
|
|
for _, link := range links {
|
|
vals = append(vals, []byte(link.ID+"\x00"))
|
|
}
|
|
|
|
return true, vals, nil
|
|
}
|
|
|
|
func (s *TokenPoliciesIndex) FromArgs(args ...interface{}) ([]byte, error) {
|
|
if len(args) != 1 {
|
|
return nil, fmt.Errorf("must provide only a single argument")
|
|
}
|
|
arg, ok := args[0].(string)
|
|
if !ok {
|
|
return nil, fmt.Errorf("argument must be a string: %#v", args[0])
|
|
}
|
|
// Add the null character as a terminator
|
|
arg += "\x00"
|
|
return []byte(arg), nil
|
|
}
|
|
|
|
func (s *TokenPoliciesIndex) PrefixFromArgs(args ...interface{}) ([]byte, error) {
|
|
val, err := s.FromArgs(args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Strip the null terminator, the rest is a prefix
|
|
n := len(val)
|
|
if n > 0 {
|
|
return val[:n-1], nil
|
|
}
|
|
return val, nil
|
|
}
|
|
|
|
func tokensTableSchema() *memdb.TableSchema {
|
|
return &memdb.TableSchema{
|
|
Name: "acl-tokens",
|
|
Indexes: map[string]*memdb.IndexSchema{
|
|
"accessor": &memdb.IndexSchema{
|
|
Name: "accessor",
|
|
AllowMissing: true,
|
|
Unique: true,
|
|
Indexer: &memdb.UUIDFieldIndex{
|
|
Field: "AccessorID",
|
|
},
|
|
},
|
|
"id": &memdb.IndexSchema{
|
|
Name: "id",
|
|
AllowMissing: false,
|
|
Unique: true,
|
|
Indexer: &memdb.StringFieldIndex{
|
|
Field: "SecretID",
|
|
Lowercase: false,
|
|
},
|
|
},
|
|
"policies": &memdb.IndexSchema{
|
|
Name: "policies",
|
|
// Need to allow missing for the anonymous token
|
|
AllowMissing: true,
|
|
Unique: false,
|
|
Indexer: &TokenPoliciesIndex{},
|
|
},
|
|
"local": &memdb.IndexSchema{
|
|
Name: "local",
|
|
AllowMissing: false,
|
|
Unique: false,
|
|
Indexer: &memdb.ConditionalIndex{
|
|
Conditional: func(obj interface{}) (bool, error) {
|
|
if token, ok := obj.(*structs.ACLToken); ok {
|
|
return token.Local, nil
|
|
}
|
|
return false, nil
|
|
},
|
|
},
|
|
},
|
|
|
|
//DEPRECATED (ACL-Legacy-Compat) - This index is only needed while we support upgrading v1 to v2 acls
|
|
// This table indexes all the ACL tokens that do not have an AccessorID
|
|
"needs-upgrade": &memdb.IndexSchema{
|
|
Name: "needs-upgrade",
|
|
AllowMissing: false,
|
|
Unique: false,
|
|
Indexer: &memdb.ConditionalIndex{
|
|
Conditional: func(obj interface{}) (bool, error) {
|
|
if token, ok := obj.(*structs.ACLToken); ok {
|
|
return token.AccessorID == "", nil
|
|
}
|
|
return false, nil
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func policiesTableSchema() *memdb.TableSchema {
|
|
return &memdb.TableSchema{
|
|
Name: "acl-policies",
|
|
Indexes: map[string]*memdb.IndexSchema{
|
|
"id": &memdb.IndexSchema{
|
|
Name: "id",
|
|
AllowMissing: false,
|
|
Unique: true,
|
|
Indexer: &memdb.UUIDFieldIndex{
|
|
Field: "ID",
|
|
},
|
|
},
|
|
"name": &memdb.IndexSchema{
|
|
Name: "name",
|
|
AllowMissing: false,
|
|
Unique: true,
|
|
Indexer: &memdb.StringFieldIndex{
|
|
Field: "Name",
|
|
// TODO (ACL-V2) - should we coerce to lowercase?
|
|
Lowercase: true,
|
|
},
|
|
},
|
|
"datacenters": &memdb.IndexSchema{
|
|
Name: "datacenters",
|
|
AllowMissing: true,
|
|
Unique: false,
|
|
Indexer: &memdb.StringSliceFieldIndex{
|
|
Field: "Datacenters",
|
|
Lowercase: false,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func init() {
|
|
registerSchema(tokensTableSchema)
|
|
registerSchema(policiesTableSchema)
|
|
}
|
|
|
|
// ACLTokens is used when saving a snapshot
|
|
func (s *Snapshot) ACLTokens() (memdb.ResultIterator, error) {
|
|
// DEPRECATED (ACL-Legacy-Compat) - This could use the "id" index when we remove v1 compat
|
|
iter, err := s.tx.Get("acl-tokens", "id")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return iter, nil
|
|
}
|
|
|
|
// ACLToken is used when restoring from a snapshot. For general inserts, use ACL.
|
|
func (s *Restore) ACLToken(token *structs.ACLToken) error {
|
|
if err := s.tx.Insert("acl-tokens", token); err != nil {
|
|
return fmt.Errorf("failed restoring acl token: %s", err)
|
|
}
|
|
|
|
if err := indexUpdateMaxTxn(s.tx, token.ModifyIndex, "acl-tokens"); err != nil {
|
|
return fmt.Errorf("failed updating index: %s", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ACLPolicies is used when saving a snapshot
|
|
func (s *Snapshot) ACLPolicies() (memdb.ResultIterator, error) {
|
|
iter, err := s.tx.Get("acl-policies", "id")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return iter, nil
|
|
}
|
|
|
|
func (s *Restore) ACLPolicy(policy *structs.ACLPolicy) error {
|
|
if err := s.tx.Insert("acl-policies", policy); err != nil {
|
|
return fmt.Errorf("failed restoring acl policy: %s", err)
|
|
}
|
|
|
|
if err := indexUpdateMaxTxn(s.tx, policy.ModifyIndex, "acl-policies"); err != nil {
|
|
return fmt.Errorf("failed updating index: %s", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ACLBootstrap is used to perform a one-time ACL bootstrap operation on a
|
|
// cluster to get the first management token.
|
|
func (s *Store) ACLBootstrap(idx, resetIndex uint64, token *structs.ACLToken, legacy bool) error {
|
|
tx := s.db.Txn(true)
|
|
defer tx.Abort()
|
|
|
|
// We must have initialized before this will ever be possible.
|
|
existing, err := tx.First("index", "id", "acl-token-bootstrap")
|
|
if err != nil {
|
|
fmt.Errorf("bootstrap check failed: %v", err)
|
|
}
|
|
if existing != nil {
|
|
if resetIndex == 0 {
|
|
return structs.ACLBootstrapNotAllowedErr
|
|
} else if resetIndex != existing.(*IndexEntry).Value {
|
|
return structs.ACLBootstrapInvalidResetIndexErr
|
|
}
|
|
}
|
|
|
|
if err := s.aclTokenSetTxn(tx, idx, token, true, false, legacy); err != nil {
|
|
return fmt.Errorf("failed inserting bootstrap token: %v", err)
|
|
}
|
|
if err := indexUpdateMaxTxn(tx, idx, "acl-tokens"); err != nil {
|
|
return fmt.Errorf("failed updating index: %s", err)
|
|
}
|
|
if err := tx.Insert("index", &IndexEntry{"acl-token-bootstrap", idx}); err != nil {
|
|
return fmt.Errorf("failed to mark ACL bootstrapping as complete: %v", err)
|
|
}
|
|
tx.Commit()
|
|
return nil
|
|
}
|
|
|
|
// CanBootstrapACLToken checks if bootstrapping is possible and returns the reset index
|
|
func (s *Store) CanBootstrapACLToken() (bool, uint64, error) {
|
|
txn := s.db.Txn(false)
|
|
|
|
// Lookup the bootstrap sentinel
|
|
out, err := txn.First("index", "id", "acl-token-bootstrap")
|
|
if err != nil {
|
|
return false, 0, err
|
|
}
|
|
|
|
// No entry, we haven't bootstrapped yet
|
|
if out == nil {
|
|
return true, 0, nil
|
|
}
|
|
|
|
// Return the reset index if we've already bootstrapped
|
|
return false, out.(*IndexEntry).Value, nil
|
|
}
|
|
|
|
func (s *Store) resolveTokenPolicyLinks(tx *memdb.Txn, token *structs.ACLToken, allowMissing bool) error {
|
|
for linkIndex, link := range token.Policies {
|
|
if link.ID != "" {
|
|
policy, err := s.getPolicyWithTxn(tx, nil, link.ID, "id")
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if policy != nil {
|
|
// the name doesn't matter here
|
|
token.Policies[linkIndex].Name = policy.Name
|
|
} else if !allowMissing {
|
|
return fmt.Errorf("No such policy with ID: %s", link.ID)
|
|
}
|
|
} else {
|
|
return fmt.Errorf("Encountered a Token with policies linked by Name in the state store")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ACLTokenSet is used to insert an ACL rule into the state store.
|
|
func (s *Store) ACLTokenSet(idx uint64, token *structs.ACLToken, legacy bool) error {
|
|
tx := s.db.Txn(true)
|
|
defer tx.Abort()
|
|
|
|
// Call set on the ACL
|
|
if err := s.aclTokenSetTxn(tx, idx, token, true, false, legacy); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := indexUpdateMaxTxn(tx, idx, "acl-tokens"); err != nil {
|
|
return fmt.Errorf("failed updating index: %s", err)
|
|
}
|
|
|
|
tx.Commit()
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) ACLTokensUpsert(idx uint64, tokens structs.ACLTokens, allowCreate bool) error {
|
|
tx := s.db.Txn(true)
|
|
defer tx.Abort()
|
|
|
|
for _, token := range tokens {
|
|
// this is only used when doing batch insertions for upgrades and replication. Therefore
|
|
// we take whatever those said.
|
|
if err := s.aclTokenSetTxn(tx, idx, token, allowCreate, true, false); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := indexUpdateMaxTxn(tx, idx, "acl-tokens"); err != nil {
|
|
return fmt.Errorf("failed updating index: %s", err)
|
|
}
|
|
|
|
tx.Commit()
|
|
return nil
|
|
}
|
|
|
|
// aclTokenSetTxn is the inner method used to insert an ACL token with the
|
|
// proper indexes into the state store.
|
|
func (s *Store) aclTokenSetTxn(tx *memdb.Txn, idx uint64, token *structs.ACLToken, allowCreate, allowMissingPolicyIDs, legacy bool) error {
|
|
// Check that the ID is set
|
|
if token.SecretID == "" {
|
|
return ErrMissingACLTokenSecret
|
|
}
|
|
|
|
if !legacy && token.AccessorID == "" {
|
|
return ErrMissingACLTokenAccessor
|
|
}
|
|
|
|
// Check for an existing ACL
|
|
// DEPRECATED (ACL-Legacy-Compat) - transition to using accessor index instead of secret once v1 compat is removed
|
|
existing, err := tx.First("acl-tokens", "id", token.SecretID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed token lookup: %s", err)
|
|
}
|
|
|
|
if existing == nil && !allowCreate {
|
|
return nil
|
|
}
|
|
|
|
if legacy && existing != nil {
|
|
original := existing.(*structs.ACLToken)
|
|
if len(original.Policies) > 0 {
|
|
return fmt.Errorf("failed inserting acl token: cannot use legacy endpoint to modify a non-legacy token")
|
|
}
|
|
|
|
token.AccessorID = original.AccessorID
|
|
}
|
|
|
|
if err := s.resolveTokenPolicyLinks(tx, token, allowMissingPolicyIDs); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Set the indexes
|
|
if existing != nil {
|
|
token.CreateIndex = existing.(*structs.ACLToken).CreateIndex
|
|
token.ModifyIndex = idx
|
|
} else {
|
|
token.CreateIndex = idx
|
|
token.ModifyIndex = idx
|
|
}
|
|
|
|
// Insert the ACL
|
|
if err := tx.Insert("acl-tokens", token); err != nil {
|
|
return fmt.Errorf("failed inserting acl token: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ACLTokenGetBySecret is used to look up an existing ACL token by its SecretID.
|
|
func (s *Store) ACLTokenGetBySecret(ws memdb.WatchSet, secret string) (uint64, *structs.ACLToken, error) {
|
|
return s.aclTokenGet(ws, secret, "id")
|
|
}
|
|
|
|
// ACLTokenGetByAccessor is used to look up an existing ACL token by its AccessorID.
|
|
func (s *Store) ACLTokenGetByAccessor(ws memdb.WatchSet, accessor string) (uint64, *structs.ACLToken, error) {
|
|
return s.aclTokenGet(ws, accessor, "accessor")
|
|
}
|
|
|
|
// aclTokenGet looks up a token using one of the indexes provided
|
|
func (s *Store) aclTokenGet(ws memdb.WatchSet, value, index string) (uint64, *structs.ACLToken, error) {
|
|
tx := s.db.Txn(false)
|
|
defer tx.Abort()
|
|
|
|
token, err := s.aclTokenGetTxn(tx, ws, value, index)
|
|
if err != nil {
|
|
return 0, nil, fmt.Errorf("failed acl token lookup: %v", err)
|
|
}
|
|
|
|
idx := maxIndexTxn(tx, "acl-tokens")
|
|
return idx, token, nil
|
|
}
|
|
|
|
func (s *Store) ACLTokenBatchRead(ws memdb.WatchSet, accessors []string) (uint64, structs.ACLTokens, error) {
|
|
tx := s.db.Txn(false)
|
|
defer tx.Abort()
|
|
|
|
tokens := make(structs.ACLTokens, 0)
|
|
for _, accessor := range accessors {
|
|
token, err := s.aclTokenGetTxn(tx, ws, accessor, "accessor")
|
|
if err != nil {
|
|
return 0, nil, fmt.Errorf("failed acl token lookup: %v", err)
|
|
}
|
|
|
|
// token == nil is valid and will indic
|
|
if token != nil {
|
|
tokens = append(tokens, token)
|
|
}
|
|
}
|
|
|
|
idx := maxIndexTxn(tx, "acl-tokens")
|
|
|
|
return idx, tokens, nil
|
|
}
|
|
|
|
func (s *Store) aclTokenGetTxn(tx *memdb.Txn, ws memdb.WatchSet, value, index string) (*structs.ACLToken, error) {
|
|
watchCh, rawToken, err := tx.FirstWatch("acl-tokens", index, value)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed acl token lookup: %v", err)
|
|
}
|
|
ws.Add(watchCh)
|
|
|
|
if rawToken != nil {
|
|
token := rawToken.(*structs.ACLToken)
|
|
if err := s.resolveTokenPolicyLinks(tx, token, true); err != nil {
|
|
return nil, err
|
|
}
|
|
return token, nil
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
// ACLTokenList is used to list out all of the ACLs in the state store.
|
|
func (s *Store) ACLTokenList(ws memdb.WatchSet, local, global bool, policy string) (uint64, structs.ACLTokens, error) {
|
|
tx := s.db.Txn(false)
|
|
defer tx.Abort()
|
|
|
|
var iter memdb.ResultIterator
|
|
var err error
|
|
|
|
// Note global == local works when both are true or false. It is not valid to set both
|
|
// to false but for defaulted structs (zero values for both) we want it to list out
|
|
// all tokens so our checks just ensure that global == local
|
|
|
|
if policy != "" {
|
|
fmt.Println("Listing by policy")
|
|
iter, err = tx.Get("acl-tokens", "policies", policy)
|
|
if err == nil && global != local {
|
|
iter = memdb.NewFilterIterator(iter, func(raw interface{}) bool {
|
|
token, ok := raw.(*structs.ACLToken)
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
if global && !token.Local {
|
|
return true
|
|
} else if local && token.Local {
|
|
return true
|
|
}
|
|
|
|
fmt.Printf("Filtering")
|
|
return false
|
|
})
|
|
}
|
|
} else if global == local {
|
|
iter, err = tx.Get("acl-tokens", "id")
|
|
} else if global {
|
|
iter, err = tx.Get("acl-tokens", "local", false)
|
|
} else {
|
|
iter, err = tx.Get("acl-tokens", "local", true)
|
|
}
|
|
|
|
if err != nil {
|
|
return 0, nil, fmt.Errorf("failed acl token lookup: %v", err)
|
|
}
|
|
ws.Add(iter.WatchCh())
|
|
|
|
var result structs.ACLTokens
|
|
for raw := iter.Next(); raw != nil; raw = iter.Next() {
|
|
token := raw.(*structs.ACLToken)
|
|
if err := s.resolveTokenPolicyLinks(tx, token, true); err != nil {
|
|
return 0, nil, err
|
|
}
|
|
result = append(result, token)
|
|
}
|
|
|
|
// Get the table index.
|
|
idx := maxIndexTxn(tx, "acl-tokens")
|
|
|
|
return idx, result, nil
|
|
}
|
|
|
|
func (s *Store) ACLTokenListUpgradeable(max int) (structs.ACLTokens, <-chan struct{}, error) {
|
|
tx := s.db.Txn(false)
|
|
defer tx.Abort()
|
|
|
|
iter, err := tx.Get("acl-tokens", "needs-upgrade", true)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("failed acl token listing: %v", err)
|
|
}
|
|
|
|
var tokens structs.ACLTokens
|
|
i := 0
|
|
for token := iter.Next(); token != nil; token = iter.Next() {
|
|
tokens = append(tokens, token.(*structs.ACLToken))
|
|
i += 1
|
|
if i >= max {
|
|
return tokens, nil, nil
|
|
}
|
|
}
|
|
|
|
return tokens, iter.WatchCh(), nil
|
|
}
|
|
|
|
// ACLTokenDeleteSecret is used to remove an existing ACL from the state store. If
|
|
// the ACL does not exist this is a no-op and no error is returned.
|
|
func (s *Store) ACLTokenDeleteSecret(idx uint64, secret string) error {
|
|
return s.aclTokenDelete(idx, secret, "id")
|
|
}
|
|
|
|
// ACLTokenDeleteAccessor is used to remove an existing ACL from the state store. If
|
|
// the ACL does not exist this is a no-op and no error is returned.
|
|
func (s *Store) ACLTokenDeleteAccessor(idx uint64, accessor string) error {
|
|
return s.aclTokenDelete(idx, accessor, "accessor")
|
|
}
|
|
|
|
func (s *Store) ACLTokensDelete(idx uint64, tokenIDs []string) error {
|
|
tx := s.db.Txn(true)
|
|
defer tx.Abort()
|
|
|
|
for _, tokenID := range tokenIDs {
|
|
if err := s.aclTokenDeleteTxn(tx, idx, tokenID, "accessor"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
tx.Commit()
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) aclTokenDelete(idx uint64, value, index string) error {
|
|
tx := s.db.Txn(true)
|
|
defer tx.Abort()
|
|
|
|
s.aclTokenDeleteTxn(tx, idx, value, index)
|
|
|
|
tx.Commit()
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) aclTokenDeleteTxn(tx *memdb.Txn, idx uint64, value, index string) error {
|
|
// Look up the existing token
|
|
token, err := tx.First("acl-tokens", index, value)
|
|
if err != nil {
|
|
return fmt.Errorf("failed acl token lookup: %v", err)
|
|
}
|
|
|
|
if token == nil {
|
|
return nil
|
|
}
|
|
|
|
if err := tx.Delete("acl-tokens", token); err != nil {
|
|
return fmt.Errorf("failed deleting acl token: %v", err)
|
|
}
|
|
if err := indexUpdateMaxTxn(tx, idx, "acl-tokens"); err != nil {
|
|
return fmt.Errorf("failed updating index: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) ACLPoliciesUpsert(idx uint64, policies structs.ACLPolicies) error {
|
|
tx := s.db.Txn(true)
|
|
defer tx.Abort()
|
|
|
|
for _, policy := range policies {
|
|
if err := s.aclPolicySetTxn(tx, idx, policy); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if err := indexUpdateMaxTxn(tx, idx, "acl-policies"); err != nil {
|
|
return fmt.Errorf("failed updating index: %s", err)
|
|
}
|
|
|
|
tx.Commit()
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) ACLPolicySet(idx uint64, policy *structs.ACLPolicy) error {
|
|
tx := s.db.Txn(true)
|
|
defer tx.Abort()
|
|
|
|
if err := s.aclPolicySetTxn(tx, idx, policy); err != nil {
|
|
return err
|
|
}
|
|
if err := indexUpdateMaxTxn(tx, idx, "acl-policies"); err != nil {
|
|
return fmt.Errorf("failed updating index: %s", err)
|
|
}
|
|
|
|
tx.Commit()
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) aclPolicySetTxn(tx *memdb.Txn, idx uint64, policy *structs.ACLPolicy) error {
|
|
// Check that the ID is set
|
|
if policy.ID == "" {
|
|
return ErrMissingACLPolicyID
|
|
}
|
|
|
|
if policy.Name == "" {
|
|
return ErrMissingACLPolicyName
|
|
}
|
|
|
|
var policyMatch *structs.ACLPolicy
|
|
existing, err := tx.First("acl-policies", "id", policy.ID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed acl policy lookup: %v", err)
|
|
}
|
|
|
|
if existing != nil {
|
|
policyMatch = existing.(*structs.ACLPolicy)
|
|
|
|
if policy.ID == structs.ACLPolicyGlobalManagementID {
|
|
// Only the name and description are modifiable
|
|
if policy.Rules != policyMatch.Rules {
|
|
return fmt.Errorf("Changing the Rules for the builtin global-management policy is not permitted")
|
|
}
|
|
|
|
if policy.Datacenters != nil && len(policy.Datacenters) != 0 {
|
|
return fmt.Errorf("Changing the Datacenters of the builtin global-management policy is not permitted")
|
|
}
|
|
}
|
|
}
|
|
|
|
// ensure the name is unique (cannot conflict with another policy with a different ID)
|
|
nameMatch, err := tx.First("acl-policies", "name", policy.Name)
|
|
if err != nil {
|
|
return fmt.Errorf("failed acl policy lookup: %v", err)
|
|
}
|
|
if nameMatch != nil && policy.ID != nameMatch.(*structs.ACLPolicy).ID {
|
|
return fmt.Errorf("A policy with name %q already exists", policy.Name)
|
|
}
|
|
|
|
// Set the indexes
|
|
if existing != nil {
|
|
policy.CreateIndex = existing.(*structs.ACLPolicy).CreateIndex
|
|
policy.ModifyIndex = idx
|
|
} else {
|
|
policy.CreateIndex = idx
|
|
policy.ModifyIndex = idx
|
|
}
|
|
|
|
// Insert the ACL
|
|
if err := tx.Insert("acl-policies", policy); err != nil {
|
|
return fmt.Errorf("failed inserting acl policy: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) ACLPolicyGetByID(ws memdb.WatchSet, id string) (uint64, *structs.ACLPolicy, error) {
|
|
return s.aclPolicyGet(ws, id, "id")
|
|
}
|
|
|
|
func (s *Store) ACLPolicyGetByName(ws memdb.WatchSet, name string) (uint64, *structs.ACLPolicy, error) {
|
|
return s.aclPolicyGet(ws, name, "name")
|
|
}
|
|
|
|
func (s *Store) ACLPolicyBatchRead(ws memdb.WatchSet, ids []string) (uint64, structs.ACLPolicies, error) {
|
|
tx := s.db.Txn(false)
|
|
defer tx.Abort()
|
|
|
|
policies := make(structs.ACLPolicies, 0)
|
|
for _, pid := range ids {
|
|
policy, err := s.getPolicyWithTxn(tx, ws, pid, "id")
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
|
|
if policy != nil {
|
|
policies = append(policies, policy)
|
|
}
|
|
}
|
|
|
|
idx := maxIndexTxn(tx, "acl-policies")
|
|
|
|
return idx, policies, nil
|
|
}
|
|
|
|
func (s *Store) getPolicyWithTxn(tx *memdb.Txn, ws memdb.WatchSet, value, index string) (*structs.ACLPolicy, error) {
|
|
watchCh, policy, err := tx.FirstWatch("acl-policies", index, value)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed acl policy lookup: %v", err)
|
|
}
|
|
ws.Add(watchCh)
|
|
|
|
if err != nil || policy == nil {
|
|
return nil, err
|
|
}
|
|
|
|
return policy.(*structs.ACLPolicy), nil
|
|
}
|
|
|
|
func (s *Store) aclPolicyGet(ws memdb.WatchSet, value, index string) (uint64, *structs.ACLPolicy, error) {
|
|
tx := s.db.Txn(false)
|
|
defer tx.Abort()
|
|
|
|
policy, err := s.getPolicyWithTxn(tx, ws, value, index)
|
|
if err != nil {
|
|
return 0, nil, err
|
|
}
|
|
|
|
idx := maxIndexTxn(tx, "acl-policies")
|
|
|
|
return idx, policy, nil
|
|
}
|
|
|
|
func (s *Store) ACLPolicyList(ws memdb.WatchSet, datacenter string) (uint64, structs.ACLPolicies, error) {
|
|
tx := s.db.Txn(false)
|
|
defer tx.Abort()
|
|
|
|
var iter memdb.ResultIterator
|
|
var err error
|
|
|
|
if datacenter != "" {
|
|
iter, err = tx.Get("acl-policies", "datacenters", datacenter)
|
|
} else {
|
|
iter, err = tx.Get("acl-policies", "id")
|
|
}
|
|
|
|
if err != nil {
|
|
return 0, nil, fmt.Errorf("failed acl policy lookup: %v", err)
|
|
}
|
|
ws.Add(iter.WatchCh())
|
|
|
|
var result structs.ACLPolicies
|
|
for policy := iter.Next(); policy != nil; policy = iter.Next() {
|
|
result = append(result, policy.(*structs.ACLPolicy))
|
|
}
|
|
|
|
// Get the table index.
|
|
idx := maxIndexTxn(tx, "acl-policies")
|
|
|
|
return idx, result, nil
|
|
}
|
|
|
|
func (s *Store) ACLPolicyDeleteByID(idx uint64, id string) error {
|
|
return s.aclPolicyDelete(idx, id, "id")
|
|
}
|
|
|
|
func (s *Store) ACLPolicyDeleteByName(idx uint64, name string) error {
|
|
return s.aclPolicyDelete(idx, name, "name")
|
|
}
|
|
|
|
func (s *Store) ACLPoliciesDelete(idx uint64, policyIDs []string) error {
|
|
tx := s.db.Txn(true)
|
|
defer tx.Abort()
|
|
|
|
for _, policyID := range policyIDs {
|
|
s.aclPolicyDeleteTxn(tx, idx, policyID, "id")
|
|
}
|
|
|
|
if err := indexUpdateMaxTxn(tx, idx, "acl-policies"); err != nil {
|
|
return fmt.Errorf("failed updating index: %v", err)
|
|
}
|
|
tx.Commit()
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) aclPolicyDelete(idx uint64, value, index string) error {
|
|
tx := s.db.Txn(true)
|
|
defer tx.Abort()
|
|
|
|
if err := s.aclPolicyDeleteTxn(tx, idx, value, index); err != nil {
|
|
return err
|
|
}
|
|
if err := indexUpdateMaxTxn(tx, idx, "acl-policies"); err != nil {
|
|
return fmt.Errorf("failed updating index: %v", err)
|
|
}
|
|
|
|
tx.Commit()
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) aclPolicyDeleteTxn(tx *memdb.Txn, idx uint64, value, index string) error {
|
|
// Look up the existing token
|
|
rawPolicy, err := tx.First("acl-policies", index, value)
|
|
if err != nil {
|
|
return fmt.Errorf("failed acl policy lookup: %v", err)
|
|
}
|
|
|
|
if rawPolicy == nil {
|
|
return nil
|
|
}
|
|
|
|
policy := rawPolicy.(*structs.ACLPolicy)
|
|
|
|
if policy.ID == structs.ACLPolicyGlobalManagementID {
|
|
return fmt.Errorf("Deletion of the builtin global-management policy is not permitted")
|
|
}
|
|
|
|
if err := tx.Delete("acl-policies", policy); err != nil {
|
|
return fmt.Errorf("failed deleting acl policy: %v", err)
|
|
}
|
|
return nil
|
|
}
|