Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

acl: global tokens created by auth methods now correctly replicate to secondary datacenters #9351

Merged
merged 3 commits into from
Dec 9, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 59 additions & 19 deletions agent/consul/acl_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4775,19 +4775,32 @@ func TestACLEndpoint_Login_with_MaxTokenTTL(t *testing.T) {
func TestACLEndpoint_Login_with_TokenLocality(t *testing.T) {
go t.Parallel()

dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.ACLDatacenter = "dc1"
c.ACLsEnabled = true
c.ACLMasterToken = "root"
})
defer os.RemoveAll(dir1)
defer s1.Shutdown()
codec := rpcClient(t, s1)
defer codec.Close()
_, s1, codec := testACLServerWithConfig(t, func(c *Config) {
c.ACLTokenMinExpirationTTL = 10 * time.Millisecond
c.ACLTokenMaxExpirationTTL = 5 * time.Second
}, false)

_, s2, _ := testACLServerWithConfig(t, func(c *Config) {
c.Datacenter = "dc2"
c.ACLTokenMinExpirationTTL = 10 * time.Millisecond
c.ACLTokenMaxExpirationTTL = 5 * time.Second
// token replication is required to test deleting non-local tokens in secondary dc
c.ACLTokenReplication = true
}, true)

waitForLeaderEstablishment(t, s1)
waitForLeaderEstablishment(t, s2)

joinWAN(t, s2, s1)

waitForNewACLs(t, s1)
waitForNewACLs(t, s2)

// Ensure s2 is authoritative.
waitForNewACLReplication(t, s2, structs.ACLReplicateTokens, 1, 1, 0)

acl := ACL{srv: s1}
acl2 := ACL{srv: s2}

testSessionID := testauth.StartSession()
defer testauth.ResetSession(testSessionID)
Expand All @@ -4799,18 +4812,20 @@ func TestACLEndpoint_Login_with_TokenLocality(t *testing.T) {
)

cases := map[string]struct {
tokenLocality string
expectLocal bool
tokenLocality string
expectLocal bool
deleteFromReplica bool
}{
"empty": {tokenLocality: "", expectLocal: true},
"local": {tokenLocality: "local", expectLocal: true},
"global": {tokenLocality: "global", expectLocal: false},
"empty": {tokenLocality: "", expectLocal: true},
"local": {tokenLocality: "local", expectLocal: true},
"global dc1 delete": {tokenLocality: "global", expectLocal: false, deleteFromReplica: false},
"global dc2 delete": {tokenLocality: "global", expectLocal: false, deleteFromReplica: true},
}

for name, tc := range cases {
tc := tc
t.Run(name, func(t *testing.T) {
method, err := upsertTestCustomizedAuthMethod(codec, "root", "dc1", func(method *structs.ACLAuthMethod) {
method, err := upsertTestCustomizedAuthMethod(codec, TestDefaultMasterToken, "dc1", func(method *structs.ACLAuthMethod) {
method.TokenLocality = tc.tokenLocality
method.Config = map[string]interface{}{
"SessionID": testSessionID,
Expand All @@ -4819,7 +4834,7 @@ func TestACLEndpoint_Login_with_TokenLocality(t *testing.T) {
require.NoError(t, err)

_, err = upsertTestBindingRule(
codec, "root", "dc1", method.Name,
codec, TestDefaultMasterToken, "dc1", method.Name,
"",
structs.BindingRuleBindTypeService,
"web",
Expand All @@ -4841,7 +4856,7 @@ func TestACLEndpoint_Login_with_TokenLocality(t *testing.T) {

secretID := resp.SecretID

got := &resp
got := resp.Clone()
got.CreateIndex = 0
got.ModifyIndex = 0
got.AccessorID = ""
Expand All @@ -4863,13 +4878,38 @@ func TestACLEndpoint_Login_with_TokenLocality(t *testing.T) {
require.Equal(t, got, expect)

// Now turn around and nuke it.
var (
logoutACL ACL
logoutDC string
)
if tc.deleteFromReplica {
// wait for it to show up
retry.Run(t, func(r *retry.R) {
var resp structs.ACLTokenResponse
require.NoError(r, acl2.TokenRead(&structs.ACLTokenGetRequest{
TokenID: secretID,
TokenIDType: structs.ACLTokenSecret,
Datacenter: "dc2",
EnterpriseMeta: *defaultEntMeta,
QueryOptions: structs.QueryOptions{Token: TestDefaultMasterToken},
}, &resp))
require.NotNil(r, resp.Token, "cannot lookup token with secretID %q", secretID)
})

logoutACL = acl2
logoutDC = "dc2"
} else {
logoutACL = acl
logoutDC = "dc1"
}

logoutReq := structs.ACLLogoutRequest{
Datacenter: "dc1",
Datacenter: logoutDC,
WriteRequest: structs.WriteRequest{Token: secretID},
}

var ignored bool
require.NoError(t, acl.Logout(&logoutReq, &ignored))
require.NoError(t, logoutACL.Logout(&logoutReq, &ignored))
})
}
}
Expand Down
32 changes: 32 additions & 0 deletions agent/consul/acl_replication_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"time"

"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/consul/authmethod/testauth"
"github.com/hashicorp/consul/agent/structs"
tokenStore "github.com/hashicorp/consul/agent/token"
"github.com/hashicorp/consul/sdk/testutil/retry"
Expand Down Expand Up @@ -349,6 +350,32 @@ func TestACLReplication_Tokens(t *testing.T) {
tokens = append(tokens, &token)
}

// Create an auth method in the primary that can create global tokens
// so that we ensure that these replicate correctly.
testSessionID := testauth.StartSession()
defer testauth.ResetSession(testSessionID)
testauth.InstallSessionToken(testSessionID, "fake-token", "default", "demo", "abc123")
method1, err := upsertTestCustomizedAuthMethod(client, "root", "dc1", func(method *structs.ACLAuthMethod) {
method.TokenLocality = "global"
method.Config = map[string]interface{}{
"SessionID": testSessionID,
}
})
require.NoError(t, err)
_, err = upsertTestBindingRule(client, "root", "dc1", method1.Name, "", structs.BindingRuleBindTypeService, "demo")
require.NoError(t, err)

// Create one token via this process.
methodToken := structs.ACLToken{}
require.NoError(t, s1.RPC("ACL.Login", &structs.ACLLoginRequest{
Auth: &structs.ACLLoginParams{
AuthMethod: method1.Name,
BearerToken: "fake-token",
},
Datacenter: "dc1",
}, &methodToken))
tokens = append(tokens, &methodToken)

checkSame := func(t *retry.R) {
// only account for global tokens - local tokens shouldn't be replicated
index, remote, err := s1.fsm.State().ACLTokenList(nil, false, true, "", "", "", nil, nil)
Expand All @@ -359,6 +386,11 @@ func TestACLReplication_Tokens(t *testing.T) {
require.Len(t, local, len(remote))
for i, token := range remote {
require.Equal(t, token.Hash, local[i].Hash)

if token.AccessorID == methodToken.AccessorID {
require.Equal(t, method1.Name, token.AuthMethod)
require.Equal(t, method1.Name, local[i].AuthMethod)
}
}

s2.aclReplicationStatusLock.RLock()
Expand Down
1 change: 1 addition & 0 deletions agent/consul/acl_replication_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ func (r *aclTokenReplicator) UpdateLocalBatch(ctx context.Context, srv *Server,
Tokens: r.updated[start:end],
CAS: false,
AllowMissingLinks: true,
FromReplication: true,
}

resp, err := srv.raftApply(structs.ACLTokenSetRequestType, &req)
Expand Down
10 changes: 9 additions & 1 deletion agent/consul/fsm/commands_oss.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/armon/go-metrics"
"github.com/armon/go-metrics/prometheus"
"github.com/hashicorp/consul/agent/consul/state"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/api"
)
Expand Down Expand Up @@ -509,7 +510,14 @@ func (c *FSM) applyACLTokenSetOperation(buf []byte, index uint64) interface{} {
defer metrics.MeasureSinceWithLabels([]string{"fsm", "acl", "token"}, time.Now(),
[]metrics.Label{{Name: "op", Value: "upsert"}})

return c.state.ACLTokenBatchSet(index, req.Tokens, req.CAS, req.AllowMissingLinks, req.ProhibitUnprivileged)
opts := state.ACLTokenSetOptions{
CAS: req.CAS,
AllowMissingPolicyAndRoleIDs: req.AllowMissingLinks,
ProhibitUnprivileged: req.ProhibitUnprivileged,
Legacy: false,
FromReplication: req.FromReplication,
}
return c.state.ACLTokenBatchSet(index, req.Tokens, opts)
}

func (c *FSM) applyACLTokenDeleteOperation(buf []byte, index uint64) interface{} {
Expand Down
54 changes: 39 additions & 15 deletions agent/consul/state/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ func (s *Store) ACLBootstrap(idx, resetIndex uint64, token *structs.ACLToken, le
}
}

if err := aclTokenSetTxn(tx, idx, token, false, false, false, legacy); err != nil {
if err := aclTokenSetTxn(tx, idx, token, ACLTokenSetOptions{Legacy: legacy}); err != nil {
return fmt.Errorf("failed inserting bootstrap token: %v", err)
}
if err := tx.Insert("index", &IndexEntry{"acl-token-bootstrap", idx}); err != nil {
Expand Down Expand Up @@ -632,19 +632,31 @@ func (s *Store) ACLTokenSet(idx uint64, token *structs.ACLToken, legacy bool) er
defer tx.Abort()

// Call set on the ACL
if err := aclTokenSetTxn(tx, idx, token, false, false, false, legacy); err != nil {
if err := aclTokenSetTxn(tx, idx, token, ACLTokenSetOptions{Legacy: legacy}); err != nil {
return err
}

return tx.Commit()
}

func (s *Store) ACLTokenBatchSet(idx uint64, tokens structs.ACLTokens, cas, allowMissingPolicyAndRoleIDs, prohibitUnprivileged bool) error {
type ACLTokenSetOptions struct {
CAS bool
AllowMissingPolicyAndRoleIDs bool
ProhibitUnprivileged bool
Legacy bool
FromReplication bool
}

func (s *Store) ACLTokenBatchSet(idx uint64, tokens structs.ACLTokens, opts ACLTokenSetOptions) error {
if opts.Legacy {
return fmt.Errorf("failed inserting acl token: cannot use this endpoint to persist legacy tokens")
}

tx := s.db.WriteTxn(idx)
defer tx.Abort()

for _, token := range tokens {
if err := aclTokenSetTxn(tx, idx, token, cas, allowMissingPolicyAndRoleIDs, prohibitUnprivileged, false); err != nil {
if err := aclTokenSetTxn(tx, idx, token, opts); err != nil {
return err
}
}
Expand All @@ -654,16 +666,20 @@ func (s *Store) ACLTokenBatchSet(idx uint64, tokens structs.ACLTokens, cas, allo

// aclTokenSetTxn is the inner method used to insert an ACL token with the
// proper indexes into the state store.
func aclTokenSetTxn(tx *txn, idx uint64, token *structs.ACLToken, cas, allowMissingPolicyAndRoleIDs, prohibitUnprivileged, legacy bool) error {
func aclTokenSetTxn(tx *txn, idx uint64, token *structs.ACLToken, opts ACLTokenSetOptions) error {
// Check that the ID is set
if token.SecretID == "" {
return ErrMissingACLTokenSecret
}

if !legacy && token.AccessorID == "" {
if !opts.Legacy && token.AccessorID == "" {
return ErrMissingACLTokenAccessor
}

if opts.FromReplication && token.Local {
return fmt.Errorf("Cannot replicate local tokens")
}

// DEPRECATED (ACL-Legacy-Compat)
if token.Rules != "" {
// When we update a legacy acl token we may have to correct old HCL to
Expand All @@ -688,7 +704,7 @@ func aclTokenSetTxn(tx *txn, idx uint64, token *structs.ACLToken, cas, allowMiss
original = existing.(*structs.ACLToken)
}

if cas {
if opts.CAS {
// set-if-unset case
if token.ModifyIndex == 0 && original != nil {
return nil
Expand All @@ -703,7 +719,7 @@ func aclTokenSetTxn(tx *txn, idx uint64, token *structs.ACLToken, cas, allowMiss
}
}

if legacy && original != nil {
if opts.Legacy && original != nil {
if original.UsesNonLegacyFields() {
return fmt.Errorf("failed inserting acl token: cannot use legacy endpoint to modify a non-legacy token")
}
Expand All @@ -716,16 +732,16 @@ func aclTokenSetTxn(tx *txn, idx uint64, token *structs.ACLToken, cas, allowMiss
}

var numValidPolicies int
if numValidPolicies, err = resolveTokenPolicyLinks(tx, token, allowMissingPolicyAndRoleIDs); err != nil {
if numValidPolicies, err = resolveTokenPolicyLinks(tx, token, opts.AllowMissingPolicyAndRoleIDs); err != nil {
return err
}

var numValidRoles int
if numValidRoles, err = resolveTokenRoleLinks(tx, token, allowMissingPolicyAndRoleIDs); err != nil {
if numValidRoles, err = resolveTokenRoleLinks(tx, token, opts.AllowMissingPolicyAndRoleIDs); err != nil {
return err
}

if token.AuthMethod != "" {
if token.AuthMethod != "" && !opts.FromReplication {
method, err := getAuthMethodWithTxn(tx, nil, token.AuthMethod, token.ACLAuthMethodEnterpriseMeta.ToEnterpriseMeta())
if err != nil {
return err
Expand All @@ -749,7 +765,7 @@ func aclTokenSetTxn(tx *txn, idx uint64, token *structs.ACLToken, cas, allowMiss
}
}

if prohibitUnprivileged {
if opts.ProhibitUnprivileged {
if numValidRoles == 0 && numValidPolicies == 0 && len(token.ServiceIdentities) == 0 && len(token.NodeIdentities) == 0 {
return ErrTokenHasNoPrivileges
}
Expand Down Expand Up @@ -1059,7 +1075,7 @@ func aclTokenDeleteTxn(tx *txn, idx uint64, value, index string, entMeta *struct
return aclTokenDeleteWithToken(tx, token.(*structs.ACLToken), idx)
}

func aclTokenDeleteAllForAuthMethodTxn(tx *txn, idx uint64, methodName string, methodMeta *structs.EnterpriseMeta) error {
func aclTokenDeleteAllForAuthMethodTxn(tx *txn, idx uint64, methodName string, methodGlobalLocality bool, methodMeta *structs.EnterpriseMeta) error {
// collect all the tokens linked with the given auth method.
iter, err := aclTokenListByAuthMethod(tx, methodName, methodMeta, structs.WildcardEnterpriseMeta())
if err != nil {
Expand All @@ -1069,7 +1085,15 @@ func aclTokenDeleteAllForAuthMethodTxn(tx *txn, idx uint64, methodName string, m
var tokens structs.ACLTokens
for raw := iter.Next(); raw != nil; raw = iter.Next() {
token := raw.(*structs.ACLToken)
tokens = append(tokens, token)
tokenIsGlobal := !token.Local

// Need to ensure that if we have an auth method named "blah" in the
// primary and secondary datacenters, and the primary instance has
// TokenLocality==global that when we delete the secondary instance we
// don't also blow away replicated tokens from the primary.
if methodGlobalLocality == tokenIsGlobal {
tokens = append(tokens, token)
}
}

if len(tokens) > 0 {
Expand Down Expand Up @@ -1877,7 +1901,7 @@ func aclAuthMethodDeleteTxn(tx *txn, idx uint64, name string, entMeta *structs.E
return err
}

if err := aclTokenDeleteAllForAuthMethodTxn(tx, idx, method.Name, entMeta); err != nil {
if err := aclTokenDeleteAllForAuthMethodTxn(tx, idx, method.Name, method.TokenLocality == "global", entMeta); err != nil {
return err
}

Expand Down
Loading