Merge pull request #5617 from hashicorp/f-acl-ux

Secure ACL Introduction for Kubernetes
pull/5656/head
R.B. Boyer 2019-04-26 15:34:26 -05:00 committed by GitHub
commit c6722fc43d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
141 changed files with 32398 additions and 1065 deletions

View File

@ -254,6 +254,7 @@ func (s *HTTPServer) ACLPolicyRead(resp http.ResponseWriter, req *http.Request,
}
if out.Policy == nil {
// TODO(rb): should this return a normal 404?
return nil, acl.ErrNotFound
}
@ -268,15 +269,35 @@ func (s *HTTPServer) ACLPolicyCreate(resp http.ResponseWriter, req *http.Request
return s.ACLPolicyWrite(resp, req, "")
}
// fixCreateTimeAndHash is used to help in decoding the CreateTime and Hash
// fixTimeAndHashFields is used to help in decoding the ExpirationTTL, ExpirationTime, CreateTime, and Hash
// attributes from the ACL Token/Policy create/update requests. It is needed
// to help mapstructure decode things properly when decodeBody is used.
func fixCreateTimeAndHash(raw interface{}) error {
func fixTimeAndHashFields(raw interface{}) error {
rawMap, ok := raw.(map[string]interface{})
if !ok {
return nil
}
if val, ok := rawMap["ExpirationTTL"]; ok {
if sval, ok := val.(string); ok {
d, err := time.ParseDuration(sval)
if err != nil {
return err
}
rawMap["ExpirationTTL"] = d
}
}
if val, ok := rawMap["ExpirationTime"]; ok {
if sval, ok := val.(string); ok {
t, err := time.Parse(time.RFC3339, sval)
if err != nil {
return err
}
rawMap["ExpirationTime"] = t
}
}
if val, ok := rawMap["CreateTime"]; ok {
if sval, ok := val.(string); ok {
t, err := time.Parse(time.RFC3339, sval)
@ -301,7 +322,7 @@ func (s *HTTPServer) ACLPolicyWrite(resp http.ResponseWriter, req *http.Request,
}
s.parseToken(req, &args.Token)
if err := decodeBody(req, &args.Policy, fixCreateTimeAndHash); err != nil {
if err := decodeBody(req, &args.Policy, fixTimeAndHashFields); err != nil {
return nil, BadRequestError{Reason: fmt.Sprintf("Policy decoding failed: %v", err)}
}
@ -354,6 +375,8 @@ func (s *HTTPServer) ACLTokenList(resp http.ResponseWriter, req *http.Request) (
}
args.Policy = req.URL.Query().Get("policy")
args.Role = req.URL.Query().Get("role")
args.AuthMethod = req.URL.Query().Get("authmethod")
var out structs.ACLTokenListResponse
defer setMeta(resp, &out.QueryMeta)
@ -472,7 +495,7 @@ func (s *HTTPServer) ACLTokenSet(resp http.ResponseWriter, req *http.Request, to
}
s.parseToken(req, &args.Token)
if err := decodeBody(req, &args.ACLToken, fixCreateTimeAndHash); err != nil {
if err := decodeBody(req, &args.ACLToken, fixTimeAndHashFields); err != nil {
return nil, BadRequestError{Reason: fmt.Sprintf("Token decoding failed: %v", err)}
}
@ -513,7 +536,7 @@ func (s *HTTPServer) ACLTokenClone(resp http.ResponseWriter, req *http.Request,
Datacenter: s.agent.config.Datacenter,
}
if err := decodeBody(req, &args.ACLToken, fixCreateTimeAndHash); err != nil && err.Error() != "EOF" {
if err := decodeBody(req, &args.ACLToken, fixTimeAndHashFields); err != nil && err.Error() != "EOF" {
return nil, BadRequestError{Reason: fmt.Sprintf("Token decoding failed: %v", err)}
}
s.parseToken(req, &args.Token)
@ -528,3 +551,480 @@ func (s *HTTPServer) ACLTokenClone(resp http.ResponseWriter, req *http.Request,
return &out, nil
}
func (s *HTTPServer) ACLRoleList(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled(resp, req) {
return nil, nil
}
var args structs.ACLRoleListRequest
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
return nil, nil
}
if args.Datacenter == "" {
args.Datacenter = s.agent.config.Datacenter
}
args.Policy = req.URL.Query().Get("policy")
var out structs.ACLRoleListResponse
defer setMeta(resp, &out.QueryMeta)
if err := s.agent.RPC("ACL.RoleList", &args, &out); err != nil {
return nil, err
}
// make sure we return an array and not nil
if out.Roles == nil {
out.Roles = make(structs.ACLRoles, 0)
}
return out.Roles, nil
}
func (s *HTTPServer) ACLRoleCRUD(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled(resp, req) {
return nil, nil
}
var fn func(resp http.ResponseWriter, req *http.Request, roleID string) (interface{}, error)
switch req.Method {
case "GET":
fn = s.ACLRoleReadByID
case "PUT":
fn = s.ACLRoleWrite
case "DELETE":
fn = s.ACLRoleDelete
default:
return nil, MethodNotAllowedError{req.Method, []string{"GET", "PUT", "DELETE"}}
}
roleID := strings.TrimPrefix(req.URL.Path, "/v1/acl/role/")
if roleID == "" && req.Method != "PUT" {
return nil, BadRequestError{Reason: "Missing role ID"}
}
return fn(resp, req, roleID)
}
func (s *HTTPServer) ACLRoleReadByName(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled(resp, req) {
return nil, nil
}
roleName := strings.TrimPrefix(req.URL.Path, "/v1/acl/role/name/")
if roleName == "" {
return nil, BadRequestError{Reason: "Missing role Name"}
}
return s.ACLRoleRead(resp, req, "", roleName)
}
func (s *HTTPServer) ACLRoleReadByID(resp http.ResponseWriter, req *http.Request, roleID string) (interface{}, error) {
return s.ACLRoleRead(resp, req, roleID, "")
}
func (s *HTTPServer) ACLRoleRead(resp http.ResponseWriter, req *http.Request, roleID, roleName string) (interface{}, error) {
args := structs.ACLRoleGetRequest{
Datacenter: s.agent.config.Datacenter,
RoleID: roleID,
RoleName: roleName,
}
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
return nil, nil
}
if args.Datacenter == "" {
args.Datacenter = s.agent.config.Datacenter
}
var out structs.ACLRoleResponse
defer setMeta(resp, &out.QueryMeta)
if err := s.agent.RPC("ACL.RoleRead", &args, &out); err != nil {
return nil, err
}
if out.Role == nil {
resp.WriteHeader(http.StatusNotFound)
return nil, nil
}
return out.Role, nil
}
func (s *HTTPServer) ACLRoleCreate(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled(resp, req) {
return nil, nil
}
return s.ACLRoleWrite(resp, req, "")
}
func (s *HTTPServer) ACLRoleWrite(resp http.ResponseWriter, req *http.Request, roleID string) (interface{}, error) {
args := structs.ACLRoleSetRequest{
Datacenter: s.agent.config.Datacenter,
}
s.parseToken(req, &args.Token)
if err := decodeBody(req, &args.Role, fixTimeAndHashFields); err != nil {
return nil, BadRequestError{Reason: fmt.Sprintf("Role decoding failed: %v", err)}
}
if args.Role.ID != "" && args.Role.ID != roleID {
return nil, BadRequestError{Reason: "Role ID in URL and payload do not match"}
} else if args.Role.ID == "" {
args.Role.ID = roleID
}
var out structs.ACLRole
if err := s.agent.RPC("ACL.RoleSet", args, &out); err != nil {
return nil, err
}
return &out, nil
}
func (s *HTTPServer) ACLRoleDelete(resp http.ResponseWriter, req *http.Request, roleID string) (interface{}, error) {
args := structs.ACLRoleDeleteRequest{
Datacenter: s.agent.config.Datacenter,
RoleID: roleID,
}
s.parseToken(req, &args.Token)
var ignored string
if err := s.agent.RPC("ACL.RoleDelete", args, &ignored); err != nil {
return nil, err
}
return true, nil
}
func (s *HTTPServer) ACLBindingRuleList(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled(resp, req) {
return nil, nil
}
var args structs.ACLBindingRuleListRequest
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
return nil, nil
}
if args.Datacenter == "" {
args.Datacenter = s.agent.config.Datacenter
}
args.AuthMethod = req.URL.Query().Get("authmethod")
var out structs.ACLBindingRuleListResponse
defer setMeta(resp, &out.QueryMeta)
if err := s.agent.RPC("ACL.BindingRuleList", &args, &out); err != nil {
return nil, err
}
// make sure we return an array and not nil
if out.BindingRules == nil {
out.BindingRules = make(structs.ACLBindingRules, 0)
}
return out.BindingRules, nil
}
func (s *HTTPServer) ACLBindingRuleCRUD(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled(resp, req) {
return nil, nil
}
var fn func(resp http.ResponseWriter, req *http.Request, bindingRuleID string) (interface{}, error)
switch req.Method {
case "GET":
fn = s.ACLBindingRuleRead
case "PUT":
fn = s.ACLBindingRuleWrite
case "DELETE":
fn = s.ACLBindingRuleDelete
default:
return nil, MethodNotAllowedError{req.Method, []string{"GET", "PUT", "DELETE"}}
}
bindingRuleID := strings.TrimPrefix(req.URL.Path, "/v1/acl/binding-rule/")
if bindingRuleID == "" && req.Method != "PUT" {
return nil, BadRequestError{Reason: "Missing binding rule ID"}
}
return fn(resp, req, bindingRuleID)
}
func (s *HTTPServer) ACLBindingRuleRead(resp http.ResponseWriter, req *http.Request, bindingRuleID string) (interface{}, error) {
args := structs.ACLBindingRuleGetRequest{
Datacenter: s.agent.config.Datacenter,
BindingRuleID: bindingRuleID,
}
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
return nil, nil
}
if args.Datacenter == "" {
args.Datacenter = s.agent.config.Datacenter
}
var out structs.ACLBindingRuleResponse
defer setMeta(resp, &out.QueryMeta)
if err := s.agent.RPC("ACL.BindingRuleRead", &args, &out); err != nil {
return nil, err
}
if out.BindingRule == nil {
resp.WriteHeader(http.StatusNotFound)
return nil, nil
}
return out.BindingRule, nil
}
func (s *HTTPServer) ACLBindingRuleCreate(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled(resp, req) {
return nil, nil
}
return s.ACLBindingRuleWrite(resp, req, "")
}
func (s *HTTPServer) ACLBindingRuleWrite(resp http.ResponseWriter, req *http.Request, bindingRuleID string) (interface{}, error) {
args := structs.ACLBindingRuleSetRequest{
Datacenter: s.agent.config.Datacenter,
}
s.parseToken(req, &args.Token)
if err := decodeBody(req, &args.BindingRule, fixTimeAndHashFields); err != nil {
return nil, BadRequestError{Reason: fmt.Sprintf("BindingRule decoding failed: %v", err)}
}
if args.BindingRule.ID != "" && args.BindingRule.ID != bindingRuleID {
return nil, BadRequestError{Reason: "BindingRule ID in URL and payload do not match"}
} else if args.BindingRule.ID == "" {
args.BindingRule.ID = bindingRuleID
}
var out structs.ACLBindingRule
if err := s.agent.RPC("ACL.BindingRuleSet", args, &out); err != nil {
return nil, err
}
return &out, nil
}
func (s *HTTPServer) ACLBindingRuleDelete(resp http.ResponseWriter, req *http.Request, bindingRuleID string) (interface{}, error) {
args := structs.ACLBindingRuleDeleteRequest{
Datacenter: s.agent.config.Datacenter,
BindingRuleID: bindingRuleID,
}
s.parseToken(req, &args.Token)
var ignored bool
if err := s.agent.RPC("ACL.BindingRuleDelete", args, &ignored); err != nil {
return nil, err
}
return true, nil
}
func (s *HTTPServer) ACLAuthMethodList(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled(resp, req) {
return nil, nil
}
var args structs.ACLAuthMethodListRequest
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
return nil, nil
}
if args.Datacenter == "" {
args.Datacenter = s.agent.config.Datacenter
}
var out structs.ACLAuthMethodListResponse
defer setMeta(resp, &out.QueryMeta)
if err := s.agent.RPC("ACL.AuthMethodList", &args, &out); err != nil {
return nil, err
}
// make sure we return an array and not nil
if out.AuthMethods == nil {
out.AuthMethods = make(structs.ACLAuthMethodListStubs, 0)
}
return out.AuthMethods, nil
}
func (s *HTTPServer) ACLAuthMethodCRUD(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled(resp, req) {
return nil, nil
}
var fn func(resp http.ResponseWriter, req *http.Request, methodName string) (interface{}, error)
switch req.Method {
case "GET":
fn = s.ACLAuthMethodRead
case "PUT":
fn = s.ACLAuthMethodWrite
case "DELETE":
fn = s.ACLAuthMethodDelete
default:
return nil, MethodNotAllowedError{req.Method, []string{"GET", "PUT", "DELETE"}}
}
methodName := strings.TrimPrefix(req.URL.Path, "/v1/acl/auth-method/")
if methodName == "" && req.Method != "PUT" {
return nil, BadRequestError{Reason: "Missing auth method name"}
}
return fn(resp, req, methodName)
}
func (s *HTTPServer) ACLAuthMethodRead(resp http.ResponseWriter, req *http.Request, methodName string) (interface{}, error) {
args := structs.ACLAuthMethodGetRequest{
Datacenter: s.agent.config.Datacenter,
AuthMethodName: methodName,
}
if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done {
return nil, nil
}
if args.Datacenter == "" {
args.Datacenter = s.agent.config.Datacenter
}
var out structs.ACLAuthMethodResponse
defer setMeta(resp, &out.QueryMeta)
if err := s.agent.RPC("ACL.AuthMethodRead", &args, &out); err != nil {
return nil, err
}
if out.AuthMethod == nil {
resp.WriteHeader(http.StatusNotFound)
return nil, nil
}
fixupAuthMethodConfig(out.AuthMethod)
return out.AuthMethod, nil
}
func (s *HTTPServer) ACLAuthMethodCreate(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled(resp, req) {
return nil, nil
}
return s.ACLAuthMethodWrite(resp, req, "")
}
func (s *HTTPServer) ACLAuthMethodWrite(resp http.ResponseWriter, req *http.Request, methodName string) (interface{}, error) {
args := structs.ACLAuthMethodSetRequest{
Datacenter: s.agent.config.Datacenter,
}
s.parseToken(req, &args.Token)
if err := decodeBody(req, &args.AuthMethod, fixTimeAndHashFields); err != nil {
return nil, BadRequestError{Reason: fmt.Sprintf("AuthMethod decoding failed: %v", err)}
}
if methodName != "" {
if args.AuthMethod.Name != "" && args.AuthMethod.Name != methodName {
return nil, BadRequestError{Reason: "AuthMethod Name in URL and payload do not match"}
} else if args.AuthMethod.Name == "" {
args.AuthMethod.Name = methodName
}
}
var out structs.ACLAuthMethod
if err := s.agent.RPC("ACL.AuthMethodSet", args, &out); err != nil {
return nil, err
}
fixupAuthMethodConfig(&out)
return &out, nil
}
func (s *HTTPServer) ACLAuthMethodDelete(resp http.ResponseWriter, req *http.Request, methodName string) (interface{}, error) {
args := structs.ACLAuthMethodDeleteRequest{
Datacenter: s.agent.config.Datacenter,
AuthMethodName: methodName,
}
s.parseToken(req, &args.Token)
var ignored bool
if err := s.agent.RPC("ACL.AuthMethodDelete", args, &ignored); err != nil {
return nil, err
}
return true, nil
}
func (s *HTTPServer) ACLLogin(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled(resp, req) {
return nil, nil
}
args := &structs.ACLLoginRequest{
Datacenter: s.agent.config.Datacenter,
}
s.parseDC(req, &args.Datacenter)
if err := decodeBody(req, &args.Auth, nil); err != nil {
return nil, BadRequestError{Reason: fmt.Sprintf("Failed to decode request body:: %v", err)}
}
var out structs.ACLToken
if err := s.agent.RPC("ACL.Login", args, &out); err != nil {
return nil, err
}
return &out, nil
}
func (s *HTTPServer) ACLLogout(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
if s.checkACLDisabled(resp, req) {
return nil, nil
}
args := structs.ACLLogoutRequest{
Datacenter: s.agent.config.Datacenter,
}
s.parseDC(req, &args.Datacenter)
s.parseToken(req, &args.Token)
if args.Token == "" {
return nil, acl.ErrNotFound
}
var ignored bool
if err := s.agent.RPC("ACL.Logout", &args, &ignored); err != nil {
return nil, err
}
return true, nil
}
// A hack to fix up the config types inside of the map[string]interface{}
// so that they get formatted correctly during json.Marshal. Without this,
// string values that get converted to []uint8 end up getting output back
// to the user in base64-encoded form.
func fixupAuthMethodConfig(method *structs.ACLAuthMethod) {
for k, v := range method.Config {
if raw, ok := v.([]uint8); ok {
strVal := structs.Uint8ToString(raw)
method.Config[k] = strVal
}
}
}

View File

@ -8,6 +8,8 @@ import (
"net/http/httptest"
"testing"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/consul/authmethod/testauth"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/testrpc"
"github.com/stretchr/testify/require"
@ -40,6 +42,17 @@ func TestACL_Disabled_Response(t *testing.T) {
{"ACLTokenCreate", a.srv.ACLTokenCreate},
{"ACLTokenSelf", a.srv.ACLTokenSelf},
{"ACLTokenCRUD", a.srv.ACLTokenCRUD},
{"ACLRoleList", a.srv.ACLRoleList},
{"ACLRoleCreate", a.srv.ACLRoleCreate},
{"ACLRoleCRUD", a.srv.ACLRoleCRUD},
{"ACLBindingRuleList", a.srv.ACLBindingRuleList},
{"ACLBindingRuleCreate", a.srv.ACLBindingRuleCreate},
{"ACLBindingRuleCRUD", a.srv.ACLBindingRuleCRUD},
{"ACLAuthMethodList", a.srv.ACLAuthMethodList},
{"ACLAuthMethodCreate", a.srv.ACLAuthMethodCreate},
{"ACLAuthMethodCRUD", a.srv.ACLAuthMethodCRUD},
{"ACLLogin", a.srv.ACLLogin},
{"ACLLogout", a.srv.ACLLogout},
}
testrpc.WaitForLeader(t, a.RPC, "dc1")
for _, tt := range tests {
@ -119,6 +132,7 @@ func TestACL_HTTP(t *testing.T) {
idMap := make(map[string]string)
policyMap := make(map[string]*structs.ACLPolicy)
roleMap := make(map[string]*structs.ACLRole)
tokenMap := make(map[string]*structs.ACLToken)
// This is all done as a subtest for a couple reasons
@ -220,7 +234,7 @@ func TestACL_HTTP(t *testing.T) {
policyMap[policy.ID] = policy
})
t.Run("Update Name ID Mistmatch", func(t *testing.T) {
t.Run("Update Name ID Mismatch", func(t *testing.T) {
policyInput := &structs.ACLPolicy{
ID: "ac7560be-7f11-4d6d-bfcf-15633c2090fd",
Name: "read-all-nodes",
@ -355,6 +369,222 @@ func TestACL_HTTP(t *testing.T) {
})
})
t.Run("Role", func(t *testing.T) {
t.Run("Create", func(t *testing.T) {
roleInput := &structs.ACLRole{
Name: "test",
Description: "test",
Policies: []structs.ACLRolePolicyLink{
structs.ACLRolePolicyLink{
ID: idMap["policy-test"],
Name: policyMap[idMap["policy-test"]].Name,
},
structs.ACLRolePolicyLink{
ID: idMap["policy-read-all-nodes"],
Name: policyMap[idMap["policy-read-all-nodes"]].Name,
},
},
}
req, _ := http.NewRequest("PUT", "/v1/acl/role?token=root", jsonBody(roleInput))
resp := httptest.NewRecorder()
obj, err := a.srv.ACLRoleCreate(resp, req)
require.NoError(t, err)
role, ok := obj.(*structs.ACLRole)
require.True(t, ok)
// 36 = length of the string form of uuids
require.Len(t, role.ID, 36)
require.Equal(t, roleInput.Name, role.Name)
require.Equal(t, roleInput.Description, role.Description)
require.Equal(t, roleInput.Policies, role.Policies)
require.True(t, role.CreateIndex > 0)
require.Equal(t, role.CreateIndex, role.ModifyIndex)
require.NotNil(t, role.Hash)
require.NotEqual(t, role.Hash, []byte{})
idMap["role-test"] = role.ID
roleMap[role.ID] = role
})
t.Run("Name Chars", func(t *testing.T) {
roleInput := &structs.ACLRole{
Name: "service-id-web",
ServiceIdentities: []*structs.ACLServiceIdentity{
&structs.ACLServiceIdentity{
ServiceName: "web",
},
},
}
req, _ := http.NewRequest("PUT", "/v1/acl/role?token=root", jsonBody(roleInput))
resp := httptest.NewRecorder()
obj, err := a.srv.ACLRoleCreate(resp, req)
require.NoError(t, err)
role, ok := obj.(*structs.ACLRole)
require.True(t, ok)
// 36 = length of the string form of uuids
require.Len(t, role.ID, 36)
require.Equal(t, roleInput.Name, role.Name)
require.Equal(t, roleInput.Description, role.Description)
require.Equal(t, roleInput.ServiceIdentities, role.ServiceIdentities)
require.True(t, role.CreateIndex > 0)
require.Equal(t, role.CreateIndex, role.ModifyIndex)
require.NotNil(t, role.Hash)
require.NotEqual(t, role.Hash, []byte{})
idMap["role-service-id-web"] = role.ID
roleMap[role.ID] = role
})
t.Run("Update Name ID Mismatch", func(t *testing.T) {
roleInput := &structs.ACLRole{
ID: "ac7560be-7f11-4d6d-bfcf-15633c2090fd",
Name: "test",
Description: "test",
ServiceIdentities: []*structs.ACLServiceIdentity{
&structs.ACLServiceIdentity{
ServiceName: "db",
},
},
}
req, _ := http.NewRequest("PUT", "/v1/acl/role/"+idMap["role-test"]+"?token=root", jsonBody(roleInput))
resp := httptest.NewRecorder()
_, err := a.srv.ACLRoleCRUD(resp, req)
require.Error(t, err)
_, ok := err.(BadRequestError)
require.True(t, ok)
})
t.Run("Role CRUD Missing ID in URL", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/v1/acl/role/?token=root", nil)
resp := httptest.NewRecorder()
_, err := a.srv.ACLRoleCRUD(resp, req)
require.Error(t, err)
_, ok := err.(BadRequestError)
require.True(t, ok)
})
t.Run("Update", func(t *testing.T) {
roleInput := &structs.ACLRole{
Name: "test",
Description: "test",
ServiceIdentities: []*structs.ACLServiceIdentity{
&structs.ACLServiceIdentity{
ServiceName: "web-indexer",
},
},
}
req, _ := http.NewRequest("PUT", "/v1/acl/role/"+idMap["role-test"]+"?token=root", jsonBody(roleInput))
resp := httptest.NewRecorder()
obj, err := a.srv.ACLRoleCRUD(resp, req)
require.NoError(t, err)
role, ok := obj.(*structs.ACLRole)
require.True(t, ok)
// 36 = length of the string form of uuids
require.Len(t, role.ID, 36)
require.Equal(t, roleInput.Name, role.Name)
require.Equal(t, roleInput.Description, role.Description)
require.Equal(t, roleInput.Policies, role.Policies)
require.Equal(t, roleInput.ServiceIdentities, role.ServiceIdentities)
require.True(t, role.CreateIndex > 0)
require.True(t, role.CreateIndex < role.ModifyIndex)
require.NotNil(t, role.Hash)
require.NotEqual(t, role.Hash, []byte{})
idMap["role-test"] = role.ID
roleMap[role.ID] = role
})
t.Run("ID Supplied", func(t *testing.T) {
roleInput := &structs.ACLRole{
ID: "12123d01-37f1-47e6-b55b-32328652bd38",
Name: "with-id",
Description: "test",
ServiceIdentities: []*structs.ACLServiceIdentity{
&structs.ACLServiceIdentity{
ServiceName: "foobar",
},
},
}
req, _ := http.NewRequest("PUT", "/v1/acl/role?token=root", jsonBody(roleInput))
resp := httptest.NewRecorder()
_, err := a.srv.ACLRoleCreate(resp, req)
require.Error(t, err)
_, ok := err.(BadRequestError)
require.True(t, ok)
})
t.Run("Invalid payload", func(t *testing.T) {
body := bytes.NewBuffer(nil)
body.Write([]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9})
req, _ := http.NewRequest("PUT", "/v1/acl/role?token=root", body)
resp := httptest.NewRecorder()
_, err := a.srv.ACLRoleCreate(resp, req)
require.Error(t, err)
_, ok := err.(BadRequestError)
require.True(t, ok)
})
t.Run("Delete", func(t *testing.T) {
req, _ := http.NewRequest("DELETE", "/v1/acl/role/"+idMap["role-service-id-web"]+"?token=root", nil)
resp := httptest.NewRecorder()
_, err := a.srv.ACLRoleCRUD(resp, req)
require.NoError(t, err)
delete(roleMap, idMap["role-service-id-web"])
delete(idMap, "role-service-id-web")
})
t.Run("List", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/v1/acl/roles?token=root", nil)
resp := httptest.NewRecorder()
raw, err := a.srv.ACLRoleList(resp, req)
require.NoError(t, err)
roles, ok := raw.(structs.ACLRoles)
require.True(t, ok)
// 1 we just created
require.Len(t, roles, 1)
for roleID, expected := range roleMap {
found := false
for _, actual := range roles {
if actual.ID == roleID {
require.Equal(t, expected.Name, actual.Name)
require.Equal(t, expected.Policies, actual.Policies)
require.Equal(t, expected.ServiceIdentities, actual.ServiceIdentities)
require.Equal(t, expected.Hash, actual.Hash)
require.Equal(t, expected.CreateIndex, actual.CreateIndex)
require.Equal(t, expected.ModifyIndex, actual.ModifyIndex)
found = true
break
}
}
require.True(t, found)
}
})
t.Run("Read", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/v1/acl/role/"+idMap["role-test"]+"?token=root", nil)
resp := httptest.NewRecorder()
raw, err := a.srv.ACLRoleCRUD(resp, req)
require.NoError(t, err)
role, ok := raw.(*structs.ACLRole)
require.True(t, ok)
require.Equal(t, roleMap[idMap["role-test"]], role)
})
})
t.Run("Token", func(t *testing.T) {
t.Run("Create", func(t *testing.T) {
tokenInput := &structs.ACLToken{
@ -594,3 +824,504 @@ func TestACL_HTTP(t *testing.T) {
})
})
}
func TestACL_LoginProcedure_HTTP(t *testing.T) {
// This tests AuthMethods, BindingRules, Login, and Logout.
t.Parallel()
a := NewTestAgent(t, t.Name(), TestACLConfig())
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
idMap := make(map[string]string)
methodMap := make(map[string]*structs.ACLAuthMethod)
ruleMap := make(map[string]*structs.ACLBindingRule)
tokenMap := make(map[string]*structs.ACLToken)
testSessionID := testauth.StartSession()
defer testauth.ResetSession(testSessionID)
// This is all done as a subtest for a couple reasons
// 1. It uses only 1 test agent and these are
// somewhat expensive to bring up and tear down often
// 2. Instead of having to bring up a new agent and prime
// the ACL system with some data before running the test
// we can intelligently order these tests so we can still
// test everything with less actual operations and do
// so in a manner that is less prone to being flaky
// 3. While this test will be large it should
t.Run("AuthMethod", func(t *testing.T) {
t.Run("Create", func(t *testing.T) {
methodInput := &structs.ACLAuthMethod{
Name: "test",
Type: "testing",
Description: "test",
Config: map[string]interface{}{
"SessionID": testSessionID,
},
}
req, _ := http.NewRequest("PUT", "/v1/acl/auth-method?token=root", jsonBody(methodInput))
resp := httptest.NewRecorder()
obj, err := a.srv.ACLAuthMethodCreate(resp, req)
require.NoError(t, err)
method, ok := obj.(*structs.ACLAuthMethod)
require.True(t, ok)
require.Equal(t, methodInput.Name, method.Name)
require.Equal(t, methodInput.Type, method.Type)
require.Equal(t, methodInput.Description, method.Description)
require.Equal(t, methodInput.Config, method.Config)
require.True(t, method.CreateIndex > 0)
require.Equal(t, method.CreateIndex, method.ModifyIndex)
methodMap[method.Name] = method
})
t.Run("Create other", func(t *testing.T) {
methodInput := &structs.ACLAuthMethod{
Name: "other",
Type: "testing",
Description: "test",
Config: map[string]interface{}{
"SessionID": testSessionID,
},
}
req, _ := http.NewRequest("PUT", "/v1/acl/auth-method?token=root", jsonBody(methodInput))
resp := httptest.NewRecorder()
obj, err := a.srv.ACLAuthMethodCreate(resp, req)
require.NoError(t, err)
method, ok := obj.(*structs.ACLAuthMethod)
require.True(t, ok)
require.Equal(t, methodInput.Name, method.Name)
require.Equal(t, methodInput.Type, method.Type)
require.Equal(t, methodInput.Description, method.Description)
require.Equal(t, methodInput.Config, method.Config)
require.True(t, method.CreateIndex > 0)
require.Equal(t, method.CreateIndex, method.ModifyIndex)
methodMap[method.Name] = method
})
t.Run("Update Name URL Mismatch", func(t *testing.T) {
methodInput := &structs.ACLAuthMethod{
Name: "test",
Type: "testing",
Description: "test",
Config: map[string]interface{}{
"SessionID": testSessionID,
},
}
req, _ := http.NewRequest("PUT", "/v1/acl/auth-method/not-test?token=root", jsonBody(methodInput))
resp := httptest.NewRecorder()
_, err := a.srv.ACLAuthMethodCRUD(resp, req)
require.Error(t, err)
_, ok := err.(BadRequestError)
require.True(t, ok)
})
t.Run("Update", func(t *testing.T) {
methodInput := &structs.ACLAuthMethod{
Name: "test",
Type: "testing",
Description: "updated description",
Config: map[string]interface{}{
"SessionID": testSessionID,
},
}
req, _ := http.NewRequest("PUT", "/v1/acl/auth-method/test?token=root", jsonBody(methodInput))
resp := httptest.NewRecorder()
obj, err := a.srv.ACLAuthMethodCRUD(resp, req)
require.NoError(t, err)
method, ok := obj.(*structs.ACLAuthMethod)
require.True(t, ok)
require.Equal(t, methodInput.Name, method.Name)
require.Equal(t, methodInput.Type, method.Type)
require.Equal(t, methodInput.Description, method.Description)
require.Equal(t, methodInput.Config, method.Config)
require.True(t, method.CreateIndex > 0)
require.True(t, method.CreateIndex < method.ModifyIndex)
methodMap[method.Name] = method
})
t.Run("Invalid payload", func(t *testing.T) {
body := bytes.NewBuffer(nil)
body.Write([]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9})
req, _ := http.NewRequest("PUT", "/v1/acl/auth-method?token=root", body)
resp := httptest.NewRecorder()
_, err := a.srv.ACLAuthMethodCreate(resp, req)
require.Error(t, err)
_, ok := err.(BadRequestError)
require.True(t, ok)
})
t.Run("List", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/v1/acl/auth-methods?token=root", nil)
resp := httptest.NewRecorder()
raw, err := a.srv.ACLAuthMethodList(resp, req)
require.NoError(t, err)
methods, ok := raw.(structs.ACLAuthMethodListStubs)
require.True(t, ok)
// 2 we just created
require.Len(t, methods, 2)
for methodName, expected := range methodMap {
found := false
for _, actual := range methods {
if actual.Name == methodName {
require.Equal(t, expected.Name, actual.Name)
require.Equal(t, expected.Type, actual.Type)
require.Equal(t, expected.Description, actual.Description)
require.Equal(t, expected.CreateIndex, actual.CreateIndex)
require.Equal(t, expected.ModifyIndex, actual.ModifyIndex)
found = true
break
}
}
require.True(t, found)
}
})
t.Run("Delete", func(t *testing.T) {
req, _ := http.NewRequest("DELETE", "/v1/acl/auth-method/other?token=root", nil)
resp := httptest.NewRecorder()
_, err := a.srv.ACLAuthMethodCRUD(resp, req)
require.NoError(t, err)
delete(methodMap, "other")
})
t.Run("Read", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/v1/acl/auth-method/test?token=root", nil)
resp := httptest.NewRecorder()
raw, err := a.srv.ACLAuthMethodCRUD(resp, req)
require.NoError(t, err)
method, ok := raw.(*structs.ACLAuthMethod)
require.True(t, ok)
require.Equal(t, methodMap["test"], method)
})
})
t.Run("BindingRule", func(t *testing.T) {
t.Run("Create", func(t *testing.T) {
ruleInput := &structs.ACLBindingRule{
Description: "test",
AuthMethod: "test",
Selector: "serviceaccount.namespace==default",
BindType: structs.BindingRuleBindTypeService,
BindName: "web",
}
req, _ := http.NewRequest("PUT", "/v1/acl/binding-rule?token=root", jsonBody(ruleInput))
resp := httptest.NewRecorder()
obj, err := a.srv.ACLBindingRuleCreate(resp, req)
require.NoError(t, err)
rule, ok := obj.(*structs.ACLBindingRule)
require.True(t, ok)
// 36 = length of the string form of uuids
require.Len(t, rule.ID, 36)
require.Equal(t, ruleInput.Description, rule.Description)
require.Equal(t, ruleInput.AuthMethod, rule.AuthMethod)
require.Equal(t, ruleInput.Selector, rule.Selector)
require.Equal(t, ruleInput.BindType, rule.BindType)
require.Equal(t, ruleInput.BindName, rule.BindName)
require.True(t, rule.CreateIndex > 0)
require.Equal(t, rule.CreateIndex, rule.ModifyIndex)
idMap["rule-test"] = rule.ID
ruleMap[rule.ID] = rule
})
t.Run("Create other", func(t *testing.T) {
ruleInput := &structs.ACLBindingRule{
Description: "other",
AuthMethod: "test",
Selector: "serviceaccount.namespace==default",
BindType: structs.BindingRuleBindTypeRole,
BindName: "fancy-role",
}
req, _ := http.NewRequest("PUT", "/v1/acl/binding-rule?token=root", jsonBody(ruleInput))
resp := httptest.NewRecorder()
obj, err := a.srv.ACLBindingRuleCreate(resp, req)
require.NoError(t, err)
rule, ok := obj.(*structs.ACLBindingRule)
require.True(t, ok)
// 36 = length of the string form of uuids
require.Len(t, rule.ID, 36)
require.Equal(t, ruleInput.Description, rule.Description)
require.Equal(t, ruleInput.AuthMethod, rule.AuthMethod)
require.Equal(t, ruleInput.Selector, rule.Selector)
require.Equal(t, ruleInput.BindType, rule.BindType)
require.Equal(t, ruleInput.BindName, rule.BindName)
require.True(t, rule.CreateIndex > 0)
require.Equal(t, rule.CreateIndex, rule.ModifyIndex)
idMap["rule-other"] = rule.ID
ruleMap[rule.ID] = rule
})
t.Run("BindingRule CRUD Missing ID in URL", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/v1/acl/binding-rule/?token=root", nil)
resp := httptest.NewRecorder()
_, err := a.srv.ACLBindingRuleCRUD(resp, req)
require.Error(t, err)
_, ok := err.(BadRequestError)
require.True(t, ok)
})
t.Run("Update", func(t *testing.T) {
ruleInput := &structs.ACLBindingRule{
Description: "updated",
AuthMethod: "test",
Selector: "serviceaccount.namespace==default",
BindType: structs.BindingRuleBindTypeService,
BindName: "${serviceaccount.name}",
}
req, _ := http.NewRequest("PUT", "/v1/acl/binding-rule/"+idMap["rule-test"]+"?token=root", jsonBody(ruleInput))
resp := httptest.NewRecorder()
obj, err := a.srv.ACLBindingRuleCRUD(resp, req)
require.NoError(t, err)
rule, ok := obj.(*structs.ACLBindingRule)
require.True(t, ok)
// 36 = length of the string form of uuids
require.Len(t, rule.ID, 36)
require.Equal(t, ruleInput.Description, rule.Description)
require.Equal(t, ruleInput.AuthMethod, rule.AuthMethod)
require.Equal(t, ruleInput.Selector, rule.Selector)
require.Equal(t, ruleInput.BindType, rule.BindType)
require.Equal(t, ruleInput.BindName, rule.BindName)
require.True(t, rule.CreateIndex > 0)
require.True(t, rule.CreateIndex < rule.ModifyIndex)
idMap["rule-test"] = rule.ID
ruleMap[rule.ID] = rule
})
t.Run("ID Supplied", func(t *testing.T) {
ruleInput := &structs.ACLBindingRule{
ID: "12123d01-37f1-47e6-b55b-32328652bd38",
Description: "with-id",
AuthMethod: "test",
Selector: "serviceaccount.namespace==default",
BindType: structs.BindingRuleBindTypeService,
BindName: "vault",
}
req, _ := http.NewRequest("PUT", "/v1/acl/binding-rule?token=root", jsonBody(ruleInput))
resp := httptest.NewRecorder()
_, err := a.srv.ACLBindingRuleCreate(resp, req)
require.Error(t, err)
_, ok := err.(BadRequestError)
require.True(t, ok)
})
t.Run("Invalid payload", func(t *testing.T) {
body := bytes.NewBuffer(nil)
body.Write([]byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9})
req, _ := http.NewRequest("PUT", "/v1/acl/binding-rule?token=root", body)
resp := httptest.NewRecorder()
_, err := a.srv.ACLBindingRuleCreate(resp, req)
require.Error(t, err)
_, ok := err.(BadRequestError)
require.True(t, ok)
})
t.Run("List", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/v1/acl/binding-rules?token=root", nil)
resp := httptest.NewRecorder()
raw, err := a.srv.ACLBindingRuleList(resp, req)
require.NoError(t, err)
rules, ok := raw.(structs.ACLBindingRules)
require.True(t, ok)
// 2 we just created
require.Len(t, rules, 2)
for ruleID, expected := range ruleMap {
found := false
for _, actual := range rules {
if actual.ID == ruleID {
require.Equal(t, expected.Description, actual.Description)
require.Equal(t, expected.AuthMethod, actual.AuthMethod)
require.Equal(t, expected.Selector, actual.Selector)
require.Equal(t, expected.BindType, actual.BindType)
require.Equal(t, expected.BindName, actual.BindName)
require.Equal(t, expected.CreateIndex, actual.CreateIndex)
require.Equal(t, expected.ModifyIndex, actual.ModifyIndex)
found = true
break
}
}
require.True(t, found)
}
})
t.Run("Delete", func(t *testing.T) {
req, _ := http.NewRequest("DELETE", "/v1/acl/binding-rule/"+idMap["rule-other"]+"?token=root", nil)
resp := httptest.NewRecorder()
_, err := a.srv.ACLBindingRuleCRUD(resp, req)
require.NoError(t, err)
delete(ruleMap, idMap["rule-other"])
delete(idMap, "rule-other")
})
t.Run("Read", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/v1/acl/binding-rule/"+idMap["rule-test"]+"?token=root", nil)
resp := httptest.NewRecorder()
raw, err := a.srv.ACLBindingRuleCRUD(resp, req)
require.NoError(t, err)
rule, ok := raw.(*structs.ACLBindingRule)
require.True(t, ok)
require.Equal(t, ruleMap[idMap["rule-test"]], rule)
})
})
testauth.InstallSessionToken(testSessionID, "token1", "default", "demo1", "abc123")
testauth.InstallSessionToken(testSessionID, "token2", "default", "demo2", "def456")
t.Run("Login", func(t *testing.T) {
t.Run("Create Token 1", func(t *testing.T) {
loginInput := &structs.ACLLoginParams{
AuthMethod: "test",
BearerToken: "token1",
Meta: map[string]string{"foo": "bar"},
}
req, _ := http.NewRequest("POST", "/v1/acl/login?token=root", jsonBody(loginInput))
resp := httptest.NewRecorder()
obj, err := a.srv.ACLLogin(resp, req)
require.NoError(t, err)
token, ok := obj.(*structs.ACLToken)
require.True(t, ok)
// 36 = length of the string form of uuids
require.Len(t, token.AccessorID, 36)
require.Len(t, token.SecretID, 36)
require.Equal(t, `token created via login: {"foo":"bar"}`, token.Description)
require.True(t, token.Local)
require.Len(t, token.Policies, 0)
require.Len(t, token.Roles, 0)
require.Len(t, token.ServiceIdentities, 1)
require.Equal(t, "demo1", token.ServiceIdentities[0].ServiceName)
require.Len(t, token.ServiceIdentities[0].Datacenters, 0)
require.True(t, token.CreateIndex > 0)
require.Equal(t, token.CreateIndex, token.ModifyIndex)
require.NotNil(t, token.Hash)
require.NotEqual(t, token.Hash, []byte{})
idMap["token-test-1"] = token.AccessorID
tokenMap[token.AccessorID] = token
})
t.Run("Create Token 2", func(t *testing.T) {
loginInput := &structs.ACLLoginParams{
AuthMethod: "test",
BearerToken: "token2",
Meta: map[string]string{"blah": "woot"},
}
req, _ := http.NewRequest("POST", "/v1/acl/login?token=root", jsonBody(loginInput))
resp := httptest.NewRecorder()
obj, err := a.srv.ACLLogin(resp, req)
require.NoError(t, err)
token, ok := obj.(*structs.ACLToken)
require.True(t, ok)
// 36 = length of the string form of uuids
require.Len(t, token.AccessorID, 36)
require.Len(t, token.SecretID, 36)
require.Equal(t, `token created via login: {"blah":"woot"}`, token.Description)
require.True(t, token.Local)
require.Len(t, token.Policies, 0)
require.Len(t, token.Roles, 0)
require.Len(t, token.ServiceIdentities, 1)
require.Equal(t, "demo2", token.ServiceIdentities[0].ServiceName)
require.Len(t, token.ServiceIdentities[0].Datacenters, 0)
require.True(t, token.CreateIndex > 0)
require.Equal(t, token.CreateIndex, token.ModifyIndex)
require.NotNil(t, token.Hash)
require.NotEqual(t, token.Hash, []byte{})
idMap["token-test-2"] = token.AccessorID
tokenMap[token.AccessorID] = token
})
t.Run("List Tokens by (incorrect) Method", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/v1/acl/tokens?token=root&authmethod=other", nil)
resp := httptest.NewRecorder()
raw, err := a.srv.ACLTokenList(resp, req)
require.NoError(t, err)
tokens, ok := raw.(structs.ACLTokenListStubs)
require.True(t, ok)
require.Len(t, tokens, 0)
})
t.Run("List Tokens by (correct) Method", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/v1/acl/tokens?token=root&authmethod=test", nil)
resp := httptest.NewRecorder()
raw, err := a.srv.ACLTokenList(resp, req)
require.NoError(t, err)
tokens, ok := raw.(structs.ACLTokenListStubs)
require.True(t, ok)
require.Len(t, tokens, 2)
for tokenID, expected := range tokenMap {
found := false
for _, actual := range tokens {
if actual.AccessorID == tokenID {
require.Equal(t, expected.Description, actual.Description)
require.Equal(t, expected.Policies, actual.Policies)
require.Equal(t, expected.Roles, actual.Roles)
require.Equal(t, expected.ServiceIdentities, actual.ServiceIdentities)
require.Equal(t, expected.Local, actual.Local)
require.Equal(t, expected.CreateTime, actual.CreateTime)
require.Equal(t, expected.Hash, actual.Hash)
require.Equal(t, expected.CreateIndex, actual.CreateIndex)
require.Equal(t, expected.ModifyIndex, actual.ModifyIndex)
found = true
break
}
}
require.True(t, found)
}
})
t.Run("Logout", func(t *testing.T) {
tok := tokenMap[idMap["token-test-1"]]
req, _ := http.NewRequest("POST", "/v1/acl/logout?token="+tok.SecretID, nil)
resp := httptest.NewRecorder()
_, err := a.srv.ACLLogout(resp, req)
require.NoError(t, err)
})
t.Run("Token is gone after Logout", func(t *testing.T) {
req, _ := http.NewRequest("GET", "/v1/acl/token/"+idMap["token-test-1"]+"?token=root", nil)
resp := httptest.NewRecorder()
_, err := a.srv.ACLTokenCRUD(resp, req)
require.Error(t, err)
require.True(t, acl.IsErrNotFound(err), err.Error())
})
})
}

View File

@ -1001,6 +1001,9 @@ func (a *Agent) consulConfig() (*consul.Config, error) {
if a.config.ACLPolicyTTL != 0 {
base.ACLPolicyTTL = a.config.ACLPolicyTTL
}
if a.config.ACLRoleTTL != 0 {
base.ACLRoleTTL = a.config.ACLRoleTTL
}
if a.config.ACLDefaultPolicy != "" {
base.ACLDefaultPolicy = a.config.ACLDefaultPolicy
}

View File

@ -702,6 +702,7 @@ func (b *Builder) Build() (rt RuntimeConfig, err error) {
ACLReplicationToken: b.stringValWithDefault(c.ACL.Tokens.Replication, b.stringVal(c.ACLReplicationToken)),
ACLTokenTTL: b.durationValWithDefault("acl.token_ttl", c.ACL.TokenTTL, b.durationVal("acl_ttl", c.ACLTTL)),
ACLPolicyTTL: b.durationVal("acl.policy_ttl", c.ACL.PolicyTTL),
ACLRoleTTL: b.durationVal("acl.role_ttl", c.ACL.RoleTTL),
ACLToken: b.stringValWithDefault(c.ACL.Tokens.Default, b.stringVal(c.ACLToken)),
ACLTokenReplication: b.boolValWithDefault(c.ACL.TokenReplication, b.boolValWithDefault(c.EnableACLReplication, enableTokenReplication)),
ACLEnableTokenPersistence: b.boolValWithDefault(c.ACL.EnableTokenPersistence, false),

View File

@ -635,6 +635,7 @@ type ACL struct {
Enabled *bool `json:"enabled,omitempty" hcl:"enabled" mapstructure:"enabled"`
TokenReplication *bool `json:"enable_token_replication,omitempty" hcl:"enable_token_replication" mapstructure:"enable_token_replication"`
PolicyTTL *string `json:"policy_ttl,omitempty" hcl:"policy_ttl" mapstructure:"policy_ttl"`
RoleTTL *string `json:"role_ttl,omitempty" hcl:"role_ttl" mapstructure:"role_ttl"`
TokenTTL *string `json:"token_ttl,omitempty" hcl:"token_ttl" mapstructure:"token_ttl"`
DownPolicy *string `json:"down_policy,omitempty" hcl:"down_policy" mapstructure:"down_policy"`
DefaultPolicy *string `json:"default_policy,omitempty" hcl:"default_policy" mapstructure:"default_policy"`

View File

@ -155,6 +155,12 @@ type RuntimeConfig struct {
// hcl: acl.token_ttl = "duration"
ACLPolicyTTL time.Duration
// ACLRoleTTL is used to control the time-to-live of cached ACL roles. This has
// a major impact on performance. By default, it is set to 30 seconds.
//
// hcl: acl.role_ttl = "duration"
ACLRoleTTL time.Duration
// ACLToken is the default token used to make requests if a per-request
// token is not provided. If not configured the 'anonymous' token is used.
//

View File

@ -2901,6 +2901,7 @@ func TestFullConfig(t *testing.T) {
"enable_key_list_policy": false,
"enable_token_persistence": true,
"policy_ttl": "1123s",
"role_ttl": "9876s",
"token_ttl": "3321s",
"enable_token_replication" : true,
"tokens" : {
@ -3464,6 +3465,7 @@ func TestFullConfig(t *testing.T) {
enable_key_list_policy = false
enable_token_persistence = true
policy_ttl = "1123s"
role_ttl = "9876s"
token_ttl = "3321s"
enable_token_replication = true
tokens = {
@ -4145,6 +4147,7 @@ func TestFullConfig(t *testing.T) {
ACLReplicationToken: "5795983a",
ACLTokenTTL: 3321 * time.Second,
ACLPolicyTTL: 1123 * time.Second,
ACLRoleTTL: 9876 * time.Second,
ACLToken: "418fdff1",
ACLTokenReplication: true,
AdvertiseAddrLAN: ipAddr("17.99.29.16"),
@ -4975,6 +4978,7 @@ func TestSanitize(t *testing.T) {
"ACLMasterToken": "hidden",
"ACLPolicyTTL": "0s",
"ACLReplicationToken": "hidden",
"ACLRoleTTL": "0s",
"ACLTokenReplication": false,
"ACLTokenTTL": "0s",
"ACLToken": "hidden",

View File

@ -4,10 +4,11 @@ import (
"fmt"
"log"
"os"
"sort"
"sync"
"time"
"github.com/armon/go-metrics"
metrics "github.com/armon/go-metrics"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/api"
@ -31,9 +32,16 @@ const (
// with all tokens in it.
aclUpgradeBatchSize = 128
// aclUpgradeRateLimit is the number of batch upgrade requests per second.
// aclUpgradeRateLimit is the number of batch upgrade requests per second allowed.
aclUpgradeRateLimit rate.Limit = 1.0
// aclTokenReapingRateLimit is the number of batch token reaping requests per second allowed.
aclTokenReapingRateLimit rate.Limit = 1.0
// aclTokenReapingBurst is the number of batch token reaping requests per second
// that can burst after a period of idleness.
aclTokenReapingBurst = 5
// aclBatchDeleteSize is the number of deletions to send in a single batch operation. 4096 should produce a batch that is <150KB
// in size but should be sufficiently large to handle 1 replication round in a single batch
aclBatchDeleteSize = 4096
@ -57,6 +65,10 @@ const (
// Maximum number of re-resolution requests to be made if the token is modified between
// resolving the token and resolving its policies that would remove one of its policies.
tokenPolicyResolutionMaxRetries = 5
// Maximum number of re-resolution requests to be made if the token is modified between
// resolving the token and resolving its roles that would remove one of its roles.
tokenRoleResolutionMaxRetries = 5
)
func minTTL(a time.Duration, b time.Duration) time.Duration {
@ -85,15 +97,16 @@ type ACLResolverDelegate interface {
UseLegacyACLs() bool
ResolveIdentityFromToken(token string) (bool, structs.ACLIdentity, error)
ResolvePolicyFromID(policyID string) (bool, *structs.ACLPolicy, error)
ResolveRoleFromID(roleID string) (bool, *structs.ACLRole, error)
RPC(method string, args interface{}, reply interface{}) error
}
type policyTokenError struct {
type policyOrRoleTokenError struct {
Err error
token string
}
func (e policyTokenError) Error() string {
func (e policyOrRoleTokenError) Error() string {
return e.Err.Error()
}
@ -121,19 +134,21 @@ type ACLResolverConfig struct {
// Supports:
// - Resolving tokens locally via the ACLResolverDelegate
// - Resolving policies locally via the ACLResolverDelegate
// - Resolving roles locally via the ACLResolverDelegate
// - Resolving legacy tokens remotely via a ACL.GetPolicy RPC
// - Resolving tokens remotely via an ACL.TokenRead RPC
// - Resolving policies remotely via an ACL.PolicyResolve RPC
// - Resolving roles remotely via an ACL.RoleResolve RPC
//
// Remote Resolution:
// Remote resolution can be done syncrhonously or asynchronously depending
// Remote resolution can be done synchronously or asynchronously depending
// on the ACLDownPolicy in the Config passed to the resolver.
//
// When the down policy is set to async-cache and we have already cached values
// then go routines will be spawned to perform the RPCs in the background
// and then will update the cache with either the positive or negative result.
//
// When the down policy is set to extend-cache or the token/policy is not already
// When the down policy is set to extend-cache or the token/policy/role is not already
// cached then the same go routines are spawned to do the RPCs in the background.
// However in this mode channels are created to receive the results of the RPC
// and are registered with the resolver. Those channels are immediately read/blocked
@ -149,6 +164,7 @@ type ACLResolver struct {
cache *structs.ACLCaches
identityGroup singleflight.Group
policyGroup singleflight.Group
roleGroup singleflight.Group
legacyGroup singleflight.Group
down acl.Authorizer
@ -431,25 +447,8 @@ func (r *ACLResolver) fetchAndCachePoliciesForIdentity(identity structs.ACLIdent
return out, nil
}
if acl.IsErrNotFound(err) {
// make sure to indicate that this identity is no longer valid within
// the cache
r.cache.PutIdentity(identity.SecretToken(), nil)
// Do not touch the policy cache. Getting a top level ACL not found error
// only indicates that the secret token used in the request
// no longer exists
return nil, &policyTokenError{acl.ErrNotFound, identity.SecretToken()}
}
if acl.IsErrPermissionDenied(err) {
// invalidate our ID cache so that identity resolution will take place
// again in the future
r.cache.RemoveIdentity(identity.SecretToken())
// Do not remove from the policy cache for permission denied
// what this does indicate is that our view of the token is out of date
return nil, &policyTokenError{acl.ErrPermissionDenied, identity.SecretToken()}
if handledErr := r.maybeHandleIdentityErrorDuringFetch(identity, err); handledErr != nil {
return nil, handledErr
}
// other RPC error - use cache if available
@ -475,6 +474,88 @@ func (r *ACLResolver) fetchAndCachePoliciesForIdentity(identity structs.ACLIdent
return out, nil
}
func (r *ACLResolver) fetchAndCacheRolesForIdentity(identity structs.ACLIdentity, roleIDs []string, cached map[string]*structs.RoleCacheEntry) (map[string]*structs.ACLRole, error) {
req := structs.ACLRoleBatchGetRequest{
Datacenter: r.delegate.ACLDatacenter(false),
RoleIDs: roleIDs,
QueryOptions: structs.QueryOptions{
Token: identity.SecretToken(),
AllowStale: true,
},
}
var resp structs.ACLRoleBatchResponse
err := r.delegate.RPC("ACL.RoleResolve", &req, &resp)
if err == nil {
out := make(map[string]*structs.ACLRole)
for _, role := range resp.Roles {
out[role.ID] = role
}
for _, roleID := range roleIDs {
if role, ok := out[roleID]; ok {
r.cache.PutRole(roleID, role)
} else {
r.cache.PutRole(roleID, nil)
}
}
return out, nil
}
if handledErr := r.maybeHandleIdentityErrorDuringFetch(identity, err); handledErr != nil {
return nil, handledErr
}
// other RPC error - use cache if available
extendCache := r.config.ACLDownPolicy == "extend-cache" || r.config.ACLDownPolicy == "async-cache"
out := make(map[string]*structs.ACLRole)
insufficientCache := false
for _, roleID := range roleIDs {
if entry, ok := cached[roleID]; extendCache && ok {
r.cache.PutRole(roleID, entry.Role)
if entry.Role != nil {
out[roleID] = entry.Role
}
} else {
r.cache.PutRole(roleID, nil)
insufficientCache = true
}
}
if insufficientCache {
return nil, ACLRemoteError{Err: err}
}
return out, nil
}
func (r *ACLResolver) maybeHandleIdentityErrorDuringFetch(identity structs.ACLIdentity, err error) error {
if acl.IsErrNotFound(err) {
// make sure to indicate that this identity is no longer valid within
// the cache
r.cache.PutIdentity(identity.SecretToken(), nil)
// Do not touch the cache. Getting a top level ACL not found error
// only indicates that the secret token used in the request
// no longer exists
return &policyOrRoleTokenError{acl.ErrNotFound, identity.SecretToken()}
}
if acl.IsErrPermissionDenied(err) {
// invalidate our ID cache so that identity resolution will take place
// again in the future
r.cache.RemoveIdentity(identity.SecretToken())
// Do not remove from the cache for permission denied
// what this does indicate is that our view of the token is out of date
return &policyOrRoleTokenError{acl.ErrPermissionDenied, identity.SecretToken()}
}
return nil
}
func (r *ACLResolver) filterPoliciesByScope(policies structs.ACLPolicies) structs.ACLPolicies {
var out structs.ACLPolicies
for _, policy := range policies {
@ -496,7 +577,10 @@ func (r *ACLResolver) filterPoliciesByScope(policies structs.ACLPolicies) struct
func (r *ACLResolver) resolvePoliciesForIdentity(identity structs.ACLIdentity) (structs.ACLPolicies, error) {
policyIDs := identity.PolicyIDs()
if len(policyIDs) == 0 {
roleIDs := identity.RoleIDs()
serviceIdentities := identity.ServiceIdentityList()
if len(policyIDs) == 0 && len(serviceIdentities) == 0 && len(roleIDs) == 0 {
policy := identity.EmbeddedPolicy()
if policy != nil {
return []*structs.ACLPolicy{policy}, nil
@ -506,9 +590,116 @@ func (r *ACLResolver) resolvePoliciesForIdentity(identity structs.ACLIdentity) (
return nil, nil
}
// Collect all of the roles tied to this token.
roles, err := r.collectRolesForIdentity(identity, roleIDs)
if err != nil {
return nil, err
}
// Merge the policies and service identities across Token and Role fields.
for _, role := range roles {
for _, link := range role.Policies {
policyIDs = append(policyIDs, link.ID)
}
serviceIdentities = append(serviceIdentities, role.ServiceIdentities...)
}
// Now deduplicate any policies or service identities that occur more than once.
policyIDs = dedupeStringSlice(policyIDs)
serviceIdentities = dedupeServiceIdentities(serviceIdentities)
// Generate synthetic policies for all service identities in effect.
syntheticPolicies := r.synthesizePoliciesForServiceIdentities(serviceIdentities)
// For the new ACLs policy replication is mandatory for correct operation on servers. Therefore
// we only attempt to resolve policies locally
policies := make([]*structs.ACLPolicy, 0, len(policyIDs))
policies, err := r.collectPoliciesForIdentity(identity, policyIDs, len(syntheticPolicies))
if err != nil {
return nil, err
}
policies = append(policies, syntheticPolicies...)
filtered := r.filterPoliciesByScope(policies)
return filtered, nil
}
func (r *ACLResolver) synthesizePoliciesForServiceIdentities(serviceIdentities []*structs.ACLServiceIdentity) []*structs.ACLPolicy {
if len(serviceIdentities) == 0 {
return nil
}
syntheticPolicies := make([]*structs.ACLPolicy, 0, len(serviceIdentities))
for _, s := range serviceIdentities {
syntheticPolicies = append(syntheticPolicies, s.SyntheticPolicy())
}
return syntheticPolicies
}
func dedupeServiceIdentities(in []*structs.ACLServiceIdentity) []*structs.ACLServiceIdentity {
// From: https://github.com/golang/go/wiki/SliceTricks#in-place-deduplicate-comparable
if len(in) <= 1 {
return in
}
sort.Slice(in, func(i, j int) bool {
return in[i].ServiceName < in[j].ServiceName
})
j := 0
for i := 1; i < len(in); i++ {
if in[j].ServiceName == in[i].ServiceName {
// Prefer increasing scope.
if len(in[j].Datacenters) == 0 || len(in[i].Datacenters) == 0 {
in[j].Datacenters = nil
} else {
in[j].Datacenters = mergeStringSlice(in[j].Datacenters, in[i].Datacenters)
}
continue
}
j++
in[j] = in[i]
}
// Discard the skipped items.
for i := j + 1; i < len(in); i++ {
in[i] = nil
}
return in[:j+1]
}
func mergeStringSlice(a, b []string) []string {
out := make([]string, 0, len(a)+len(b))
out = append(out, a...)
out = append(out, b...)
return dedupeStringSlice(out)
}
func dedupeStringSlice(in []string) []string {
// From: https://github.com/golang/go/wiki/SliceTricks#in-place-deduplicate-comparable
if len(in) <= 1 {
return in
}
sort.Strings(in)
j := 0
for i := 1; i < len(in); i++ {
if in[j] == in[i] {
continue
}
j++
in[j] = in[i]
}
return in[:j+1]
}
func (r *ACLResolver) collectPoliciesForIdentity(identity structs.ACLIdentity, policyIDs []string, extraCap int) ([]*structs.ACLPolicy, error) {
policies := make([]*structs.ACLPolicy, 0, len(policyIDs)+extraCap)
// Get all associated policies
var missing []string
@ -538,7 +729,7 @@ func (r *ACLResolver) resolvePoliciesForIdentity(identity structs.ACLIdentity) (
}
if entry.Policy == nil {
// this happens when we cache a negative response for the policies existence
// this happens when we cache a negative response for the policy's existence
continue
}
@ -552,7 +743,7 @@ func (r *ACLResolver) resolvePoliciesForIdentity(identity structs.ACLIdentity) (
// Hot-path if we have no missing or expired policies
if len(missing)+len(expired) == 0 {
return r.filterPoliciesByScope(policies), nil
return policies, nil
}
hasMissing := len(missing) > 0
@ -572,7 +763,7 @@ func (r *ACLResolver) resolvePoliciesForIdentity(identity structs.ACLIdentity) (
if !waitForResult {
// waitForResult being false requires that all the policies were cached already
policies = append(policies, expired...)
return r.filterPoliciesByScope(policies), nil
return policies, nil
}
res := <-waitChan
@ -589,7 +780,100 @@ func (r *ACLResolver) resolvePoliciesForIdentity(identity structs.ACLIdentity) (
}
}
return r.filterPoliciesByScope(policies), nil
return policies, nil
}
func (r *ACLResolver) resolveRolesForIdentity(identity structs.ACLIdentity) (structs.ACLRoles, error) {
return r.collectRolesForIdentity(identity, identity.RoleIDs())
}
func (r *ACLResolver) collectRolesForIdentity(identity structs.ACLIdentity, roleIDs []string) (structs.ACLRoles, error) {
if len(roleIDs) == 0 {
return nil, nil
}
// For the new ACLs policy & role replication is mandatory for correct operation
// on servers. Therefore we only attempt to resolve roles locally
roles := make([]*structs.ACLRole, 0, len(roleIDs))
var missing []string
var expired []*structs.ACLRole
expCacheMap := make(map[string]*structs.RoleCacheEntry)
for _, roleID := range roleIDs {
if done, role, err := r.delegate.ResolveRoleFromID(roleID); done {
if err != nil && !acl.IsErrNotFound(err) {
return nil, err
}
if role != nil {
roles = append(roles, role)
} else {
r.logger.Printf("[WARN] acl: role %q not found for identity %q", roleID, identity.ID())
}
continue
}
// create the missing list which we can execute an RPC to get all the missing roles at once
entry := r.cache.GetRole(roleID)
if entry == nil {
missing = append(missing, roleID)
continue
}
if entry.Role == nil {
// this happens when we cache a negative response for the role's existence
continue
}
if entry.Age() >= r.config.ACLRoleTTL {
expired = append(expired, entry.Role)
expCacheMap[roleID] = entry
} else {
roles = append(roles, entry.Role)
}
}
// Hot-path if we have no missing or expired roles
if len(missing)+len(expired) == 0 {
return roles, nil
}
hasMissing := len(missing) > 0
fetchIDs := missing
for _, role := range expired {
fetchIDs = append(fetchIDs, role.ID)
}
waitChan := r.roleGroup.DoChan(identity.SecretToken(), func() (interface{}, error) {
roles, err := r.fetchAndCacheRolesForIdentity(identity, fetchIDs, expCacheMap)
return roles, err
})
waitForResult := hasMissing || r.config.ACLDownPolicy != "async-cache"
if !waitForResult {
// waitForResult being false requires that all the roles were cached already
roles = append(roles, expired...)
return roles, nil
}
res := <-waitChan
if res.Err != nil {
return nil, res.Err
}
if res.Val != nil {
foundRoles := res.Val.(map[string]*structs.ACLRole)
for _, role := range foundRoles {
roles = append(roles, role)
}
}
return roles, nil
}
func (r *ACLResolver) resolveTokenToPolicies(token string) (structs.ACLPolicies, error) {
@ -608,6 +892,8 @@ func (r *ACLResolver) resolveTokenToIdentityAndPolicies(token string) (structs.A
return nil, nil, err
} else if identity == nil {
return nil, nil, acl.ErrNotFound
} else if identity.IsExpired(time.Now()) {
return nil, nil, acl.ErrNotFound
}
lastIdentity = identity
@ -618,13 +904,52 @@ func (r *ACLResolver) resolveTokenToIdentityAndPolicies(token string) (structs.A
}
lastErr = err
if tokenErr, ok := err.(*policyTokenError); ok {
if tokenErr, ok := err.(*policyOrRoleTokenError); ok {
if acl.IsErrNotFound(err) && tokenErr.token == identity.SecretToken() {
// token was deleted while resolving policies
return nil, nil, acl.ErrNotFound
}
// other types of policyTokenErrors should cause retrying the whole token
// other types of policyOrRoleTokenErrors should cause retrying the whole token
// resolution process
} else {
return identity, nil, err
}
}
return lastIdentity, nil, lastErr
}
func (r *ACLResolver) resolveTokenToIdentityAndRoles(token string) (structs.ACLIdentity, structs.ACLRoles, error) {
var lastErr error
var lastIdentity structs.ACLIdentity
for i := 0; i < tokenRoleResolutionMaxRetries; i++ {
// Resolve the token to an ACLIdentity
identity, err := r.resolveIdentityFromToken(token)
if err != nil {
return nil, nil, err
} else if identity == nil {
return nil, nil, acl.ErrNotFound
} else if identity.IsExpired(time.Now()) {
return nil, nil, acl.ErrNotFound
}
lastIdentity = identity
roles, err := r.resolveRolesForIdentity(identity)
if err == nil {
return identity, roles, nil
}
lastErr = err
if tokenErr, ok := err.(*policyOrRoleTokenError); ok {
if acl.IsErrNotFound(err) && tokenErr.token == identity.SecretToken() {
// token was deleted while resolving roles
return nil, nil, acl.ErrNotFound
}
// other types of policyOrRoleTokenErrors should cause retrying the whole token
// resolution process
} else {
return identity, nil, err

View File

@ -0,0 +1,169 @@
package consul
import (
"fmt"
"github.com/hashicorp/consul/agent/consul/authmethod"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/go-bexpr"
// register this as a builtin auth method
_ "github.com/hashicorp/consul/agent/consul/authmethod/kubeauth"
)
type authMethodValidatorEntry struct {
Validator authmethod.Validator
ModifyIndex uint64 // the raft index when this last changed
}
// loadAuthMethodValidator returns an authmethod.Validator for the given auth
// method configuration. If the cache is up to date as-of the provided index
// then the cached version is returned, otherwise a new validator is created
// and cached.
func (s *Server) loadAuthMethodValidator(idx uint64, method *structs.ACLAuthMethod) (authmethod.Validator, error) {
if prevIdx, v, ok := s.getCachedAuthMethodValidator(method.Name); ok && idx <= prevIdx {
return v, nil
}
v, err := authmethod.NewValidator(method)
if err != nil {
return nil, fmt.Errorf("auth method validator for %q could not be initialized: %v", method.Name, err)
}
v = s.getOrReplaceAuthMethodValidator(method.Name, idx, v)
return v, nil
}
// getCachedAuthMethodValidator returns an AuthMethodValidator for
// the given name exclusively from the cache. If one is not found in the cache
// nil is returned.
func (s *Server) getCachedAuthMethodValidator(name string) (uint64, authmethod.Validator, bool) {
s.aclAuthMethodValidatorLock.RLock()
defer s.aclAuthMethodValidatorLock.RUnlock()
if s.aclAuthMethodValidators != nil {
v, ok := s.aclAuthMethodValidators[name]
if ok {
return v.ModifyIndex, v.Validator, true
}
}
return 0, nil, false
}
// getOrReplaceAuthMethodValidator updates the cached validator with the
// provided one UNLESS it has been updated by another goroutine in which case
// the updated one is returned.
func (s *Server) getOrReplaceAuthMethodValidator(name string, idx uint64, v authmethod.Validator) authmethod.Validator {
s.aclAuthMethodValidatorLock.Lock()
defer s.aclAuthMethodValidatorLock.Unlock()
if s.aclAuthMethodValidators == nil {
s.aclAuthMethodValidators = make(map[string]*authMethodValidatorEntry)
}
prev, ok := s.aclAuthMethodValidators[name]
if ok {
if prev.ModifyIndex >= idx {
return prev.Validator
}
}
s.logger.Printf("[DEBUG] acl: updating cached auth method validator for %q", name)
s.aclAuthMethodValidators[name] = &authMethodValidatorEntry{
Validator: v,
ModifyIndex: idx,
}
return v
}
// purgeAuthMethodValidators resets the cache of validators.
func (s *Server) purgeAuthMethodValidators() {
s.aclAuthMethodValidatorLock.Lock()
s.aclAuthMethodValidators = make(map[string]*authMethodValidatorEntry)
s.aclAuthMethodValidatorLock.Unlock()
}
// evaluateRoleBindings evaluates all current binding rules associated with the
// given auth method against the verified data returned from the authentication
// process.
//
// A list of role links and service identities are returned.
func (s *Server) evaluateRoleBindings(
validator authmethod.Validator,
verifiedFields map[string]string,
) ([]*structs.ACLServiceIdentity, []structs.ACLTokenRoleLink, error) {
// Only fetch rules that are relevant for this method.
_, rules, err := s.fsm.State().ACLBindingRuleList(nil, validator.Name())
if err != nil {
return nil, nil, err
} else if len(rules) == 0 {
return nil, nil, nil
}
// Convert the fields into something suitable for go-bexpr.
selectableVars := validator.MakeFieldMapSelectable(verifiedFields)
// Find all binding rules that match the provided fields.
var matchingRules []*structs.ACLBindingRule
for _, rule := range rules {
if doesBindingRuleMatch(rule, selectableVars) {
matchingRules = append(matchingRules, rule)
}
}
if len(matchingRules) == 0 {
return nil, nil, nil
}
// For all matching rules compute the attributes of a token.
var (
roleLinks []structs.ACLTokenRoleLink
serviceIdentities []*structs.ACLServiceIdentity
)
for _, rule := range matchingRules {
bindName, valid, err := computeBindingRuleBindName(rule.BindType, rule.BindName, verifiedFields)
if err != nil {
return nil, nil, fmt.Errorf("cannot compute %q bind name for bind target: %v", rule.BindType, err)
} else if !valid {
return nil, nil, fmt.Errorf("computed %q bind name for bind target is invalid: %q", rule.BindType, bindName)
}
switch rule.BindType {
case structs.BindingRuleBindTypeService:
serviceIdentities = append(serviceIdentities, &structs.ACLServiceIdentity{
ServiceName: bindName,
})
case structs.BindingRuleBindTypeRole:
roleLinks = append(roleLinks, structs.ACLTokenRoleLink{
Name: bindName,
})
default:
// skip unknown bind type; don't grant privileges
}
}
return serviceIdentities, roleLinks, nil
}
// doesBindingRuleMatch checks that a single binding rule matches the provided
// vars.
func doesBindingRuleMatch(rule *structs.ACLBindingRule, selectableVars interface{}) bool {
if rule.Selector == "" {
return true // catch-all
}
eval, err := bexpr.CreateEvaluatorForType(rule.Selector, nil, selectableVars)
if err != nil {
return false // fails to match if selector is invalid
}
result, err := eval.Evaluate(selectableVars)
if err != nil {
return false // fails to match if evaluation fails
}
return result
}

View File

@ -0,0 +1,48 @@
package consul
import (
"testing"
"github.com/hashicorp/consul/agent/structs"
"github.com/stretchr/testify/require"
)
func TestDoesBindingRuleMatch(t *testing.T) {
type matchable struct {
A string `bexpr:"a"`
C string `bexpr:"c"`
}
for _, test := range []struct {
name string
selector string
details interface{}
ok bool
}{
{"no fields",
"a==b", nil, false},
{"1 term ok",
"a==b", &matchable{A: "b"}, true},
{"1 term no field",
"a==b", &matchable{C: "d"}, false},
{"1 term wrong value",
"a==b", &matchable{A: "z"}, false},
{"2 terms ok",
"a==b and c==d", &matchable{A: "b", C: "d"}, true},
{"2 terms one missing field",
"a==b and c==d", &matchable{A: "b"}, false},
{"2 terms one wrong value",
"a==b and c==d", &matchable{A: "z", C: "d"}, false},
///////////////////////////////
{"no fields (no selectors)",
"", nil, true},
{"1 term ok (no selectors)",
"", &matchable{A: "b"}, true},
} {
t.Run(test.name, func(t *testing.T) {
rule := structs.ACLBindingRule{Selector: test.selector}
ok := doesBindingRuleMatch(&rule, test.details)
require.Equal(t, test.ok, ok)
})
}
}

View File

@ -25,6 +25,8 @@ var clientACLCacheConfig *structs.ACLCachesConfig = &structs.ACLCachesConfig{
ParsedPolicies: 128,
// Authorizers - number of compiled multi-policy effective policies that can be cached
Authorizers: 256,
// Roles - number of ACL roles that can be cached
Roles: 128,
}
func (c *Client) UseLegacyACLs() bool {
@ -96,6 +98,11 @@ func (c *Client) ResolvePolicyFromID(policyID string) (bool, *structs.ACLPolicy,
return false, nil, nil
}
func (c *Client) ResolveRoleFromID(roleID string) (bool, *structs.ACLRole, error) {
// clients do no local role resolution at the moment
return false, nil, nil
}
func (c *Client) ResolveToken(token string) (acl.Authorizer, error) {
return c.acls.ResolveToken(token)
}

File diff suppressed because it is too large Load Diff

View File

@ -93,8 +93,9 @@ func aclApplyInternal(srv *Server, args *structs.ACLRequest, reply *string) erro
return fmt.Errorf("Invalid ACL Type")
}
// No need to check expiration times as those did not exist in legacy tokens.
_, existing, _ := srv.fsm.State().ACLTokenGetBySecret(nil, args.ACL.ID)
if existing != nil && len(existing.Policies) > 0 {
if existing != nil && existing.UsesNonLegacyFields() {
return fmt.Errorf("Cannot use legacy endpoint to modify a non-legacy token")
}
@ -210,8 +211,13 @@ func (a *ACL) Get(args *structs.ACLSpecificRequest,
return err
}
// converting an ACLToken to an ACL will return nil and an error
// Converting an ACLToken to an ACL will return nil and an error
// (which we ignore) when it is unconvertible.
//
// This also means we won't have to check expiration times since
// any legacy tokens never had expiration times and no non-legacy
// tokens can be converted.
var acl *structs.ACL
if token != nil {
acl, _ = token.Convert()
@ -249,13 +255,18 @@ func (a *ACL) List(args *structs.DCSpecificRequest,
return a.srv.blockingQuery(&args.QueryOptions,
&reply.QueryMeta,
func(ws memdb.WatchSet, state *state.Store) error {
index, tokens, err := state.ACLTokenList(ws, false, true, "")
index, tokens, err := state.ACLTokenList(ws, false, true, "", "", "")
if err != nil {
return err
}
now := time.Now()
var acls structs.ACLs
for _, token := range tokens {
if token.IsExpired(now) {
continue
}
if acl, err := token.Convert(); err == nil && acl != nil {
acls = append(acls, acl)
}

File diff suppressed because it is too large Load Diff

View File

@ -3,10 +3,11 @@ package consul
import (
"bytes"
"context"
"errors"
"fmt"
"time"
"github.com/armon/go-metrics"
metrics "github.com/armon/go-metrics"
"github.com/hashicorp/consul/agent/structs"
)
@ -15,123 +16,108 @@ const (
aclReplicationMaxRetryBackoff = 64
)
func diffACLPolicies(local structs.ACLPolicies, remote structs.ACLPolicyListStubs, lastRemoteIndex uint64) ([]string, []string) {
local.Sort()
remote.Sort()
// aclTypeReplicator allows the machinery of acl replication to be shared between
// types with minimal code duplication (barring generics magically popping into
// existence).
//
// Concrete implementations of this interface should internally contain a
// pointer to the server so that data lookups can occur, and they should
// maintain the smallest quantity of type-specific state they can.
//
// Implementations of this interface are short-lived and recreated on every
// iteration.
type aclTypeReplicator interface {
// Type is variant of replication in use. Used for updating the replication
// status tracker.
Type() structs.ACLReplicationType
var deletions []string
var updates []string
var localIdx int
var remoteIdx int
for localIdx, remoteIdx = 0, 0; localIdx < len(local) && remoteIdx < len(remote); {
if local[localIdx].ID == remote[remoteIdx].ID {
// policy is in both the local and remote state - need to check raft indices and the Hash
if remote[remoteIdx].ModifyIndex > lastRemoteIndex && !bytes.Equal(remote[remoteIdx].Hash, local[localIdx].Hash) {
updates = append(updates, remote[remoteIdx].ID)
}
// increment both indices when equal
localIdx += 1
remoteIdx += 1
} else if local[localIdx].ID < remote[remoteIdx].ID {
// policy no longer in remoted state - needs deleting
deletions = append(deletions, local[localIdx].ID)
// SingularNoun is the singular form of the item being replicated.
SingularNoun() string
// increment just the local index
localIdx += 1
} else {
// local state doesn't have this policy - needs updating
updates = append(updates, remote[remoteIdx].ID)
// PluralNoun is the plural form of the item being replicated.
PluralNoun() string
// increment just the remote index
remoteIdx += 1
}
}
// FetchRemote retrieves items newer than the provided index from the
// remote datacenter (for diffing purposes).
FetchRemote(srv *Server, lastRemoteIndex uint64) (int, uint64, error)
for ; localIdx < len(local); localIdx += 1 {
deletions = append(deletions, local[localIdx].ID)
}
// FetchLocal retrieves items from the current datacenter (for diffing
// purposes).
FetchLocal(srv *Server) (int, uint64, error)
for ; remoteIdx < len(remote); remoteIdx += 1 {
updates = append(updates, remote[remoteIdx].ID)
}
// SortState sorts the internal working state output of FetchRemote and
// FetchLocal so that a sane diff can be performed.
SortState() (lenLocal, lenRemote int)
return deletions, updates
// LocalMeta allows for type-agnostic metadata from the sorted local state
// can be retrieved for the purposes of diffing.
LocalMeta(i int) (id string, modIndex uint64, hash []byte)
// RemoteMeta allows for type-agnostic metadata from the sorted remote
// state can be retrieved for the purposes of diffing.
RemoteMeta(i int) (id string, modIndex uint64, hash []byte)
// FetchUpdated retrieves the specific items from the remote (during the
// correction phase).
FetchUpdated(srv *Server, updates []string) (int, error)
// LenPendingUpdates should be the size of the data retrieved in
// FetchUpdated.
LenPendingUpdates() int
// PendingUpdateIsRedacted returns true if the update contains redacted
// data. Really only valid for tokens.
PendingUpdateIsRedacted(i int) bool
// PendingUpdateEstimatedSize is the item's EstimatedSize in the state
// populated by FetchUpdated.
PendingUpdateEstimatedSize(i int) int
// UpdateLocalBatch applies a portion of the state populated by
// FetchUpdated to the current datacenter.
UpdateLocalBatch(ctx context.Context, srv *Server, start, end int) error
// DeleteLocalBatch removes items from the current datacenter.
DeleteLocalBatch(srv *Server, batch []string) error
}
func (s *Server) deleteLocalACLPolicies(deletions []string, ctx context.Context) (bool, error) {
ticker := time.NewTicker(time.Second / time.Duration(s.config.ACLReplicationApplyLimit))
defer ticker.Stop()
var errContainsRedactedData = errors.New("replication results contain redacted data")
for i := 0; i < len(deletions); i += aclBatchDeleteSize {
req := structs.ACLPolicyBatchDeleteRequest{}
if i+aclBatchDeleteSize > len(deletions) {
req.PolicyIDs = deletions[i:]
} else {
req.PolicyIDs = deletions[i : i+aclBatchDeleteSize]
}
resp, err := s.raftApply(structs.ACLPolicyDeleteRequestType, &req)
if err != nil {
return false, fmt.Errorf("Failed to apply policy deletions: %v", err)
}
if respErr, ok := resp.(error); ok && err != nil {
return false, fmt.Errorf("Failed to apply policy deletions: %v", respErr)
}
if i+aclBatchDeleteSize < len(deletions) {
select {
case <-ctx.Done():
return true, nil
case <-ticker.C:
// do nothing - ready for the next batch
}
}
func (s *Server) fetchACLRolesBatch(roleIDs []string) (*structs.ACLRoleBatchResponse, error) {
req := structs.ACLRoleBatchGetRequest{
Datacenter: s.config.ACLDatacenter,
RoleIDs: roleIDs,
QueryOptions: structs.QueryOptions{
AllowStale: true,
Token: s.tokens.ReplicationToken(),
},
}
return false, nil
var response structs.ACLRoleBatchResponse
if err := s.RPC("ACL.RoleBatchRead", &req, &response); err != nil {
return nil, err
}
return &response, nil
}
func (s *Server) updateLocalACLPolicies(policies structs.ACLPolicies, ctx context.Context) (bool, error) {
ticker := time.NewTicker(time.Second / time.Duration(s.config.ACLReplicationApplyLimit))
defer ticker.Stop()
func (s *Server) fetchACLRoles(lastRemoteIndex uint64) (*structs.ACLRoleListResponse, error) {
defer metrics.MeasureSince([]string{"leader", "replication", "acl", "role", "fetch"}, time.Now())
// outer loop handles submitting a batch
for batchStart := 0; batchStart < len(policies); {
// inner loop finds the last element to include in this batch.
batchSize := 0
batchEnd := batchStart
for ; batchEnd < len(policies) && batchSize < aclBatchUpsertSize; batchEnd += 1 {
batchSize += policies[batchEnd].EstimateSize()
}
req := structs.ACLPolicyBatchSetRequest{
Policies: policies[batchStart:batchEnd],
}
resp, err := s.raftApply(structs.ACLPolicySetRequestType, &req)
if err != nil {
return false, fmt.Errorf("Failed to apply policy upserts: %v", err)
}
if respErr, ok := resp.(error); ok && respErr != nil {
return false, fmt.Errorf("Failed to apply policy upsert: %v", respErr)
}
s.logger.Printf("[DEBUG] acl: policy replication - upserted 1 batch with %d policies of size %d", batchEnd-batchStart, batchSize)
// policies[batchEnd] wasn't include as the slicing doesn't include the element at the stop index
batchStart = batchEnd
// prevent waiting if we are done
if batchEnd < len(policies) {
select {
case <-ctx.Done():
return true, nil
case <-ticker.C:
// nothing to do - just rate limiting
}
}
req := structs.ACLRoleListRequest{
Datacenter: s.config.ACLDatacenter,
QueryOptions: structs.QueryOptions{
AllowStale: true,
MinQueryIndex: lastRemoteIndex,
Token: s.tokens.ReplicationToken(),
},
}
return false, nil
var response structs.ACLRoleListResponse
if err := s.RPC("ACL.RoleList", &req, &response); err != nil {
return nil, err
}
return &response, nil
}
func (s *Server) fetchACLPoliciesBatch(policyIDs []string) (*structs.ACLPolicyBatchResponse, error) {
@ -171,66 +157,72 @@ func (s *Server) fetchACLPolicies(lastRemoteIndex uint64) (*structs.ACLPolicyLis
return &response, nil
}
type tokenDiffResults struct {
type itemDiffResults struct {
LocalDeletes []string
LocalUpserts []string
LocalSkipped int
RemoteSkipped int
}
func diffACLTokens(local structs.ACLTokens, remote structs.ACLTokenListStubs, lastRemoteIndex uint64) tokenDiffResults {
// Note: items with empty AccessorIDs will bubble up to the top.
local.Sort()
remote.Sort()
func diffACLType(tr aclTypeReplicator, lastRemoteIndex uint64) itemDiffResults {
// Note: items with empty IDs will bubble up to the top (like legacy, unmigrated Tokens)
var res tokenDiffResults
lenLocal, lenRemote := tr.SortState()
var res itemDiffResults
var localIdx int
var remoteIdx int
for localIdx, remoteIdx = 0, 0; localIdx < len(local) && remoteIdx < len(remote); {
if local[localIdx].AccessorID == "" {
for localIdx, remoteIdx = 0, 0; localIdx < lenLocal && remoteIdx < lenRemote; {
localID, _, localHash := tr.LocalMeta(localIdx)
remoteID, remoteMod, remoteHash := tr.RemoteMeta(remoteIdx)
if localID == "" {
res.LocalSkipped++
localIdx += 1
continue
}
if remote[remoteIdx].AccessorID == "" {
if remoteID == "" {
res.RemoteSkipped++
remoteIdx += 1
continue
}
if local[localIdx].AccessorID == remote[remoteIdx].AccessorID {
// policy is in both the local and remote state - need to check raft indices and Hash
if remote[remoteIdx].ModifyIndex > lastRemoteIndex && !bytes.Equal(remote[remoteIdx].Hash, local[localIdx].Hash) {
res.LocalUpserts = append(res.LocalUpserts, remote[remoteIdx].AccessorID)
if localID == remoteID {
// item is in both the local and remote state - need to check raft indices and the Hash
if remoteMod > lastRemoteIndex && !bytes.Equal(remoteHash, localHash) {
res.LocalUpserts = append(res.LocalUpserts, remoteID)
}
// increment both indices when equal
localIdx += 1
remoteIdx += 1
} else if local[localIdx].AccessorID < remote[remoteIdx].AccessorID {
// policy no longer in remoted state - needs deleting
res.LocalDeletes = append(res.LocalDeletes, local[localIdx].AccessorID)
} else if localID < remoteID {
// item no longer in remote state - needs deleting
res.LocalDeletes = append(res.LocalDeletes, localID)
// increment just the local index
localIdx += 1
} else {
// local state doesn't have this policy - needs updating
res.LocalUpserts = append(res.LocalUpserts, remote[remoteIdx].AccessorID)
// local state doesn't have this item - needs updating
res.LocalUpserts = append(res.LocalUpserts, remoteID)
// increment just the remote index
remoteIdx += 1
}
}
for ; localIdx < len(local); localIdx += 1 {
if local[localIdx].AccessorID != "" {
res.LocalDeletes = append(res.LocalDeletes, local[localIdx].AccessorID)
for ; localIdx < lenLocal; localIdx += 1 {
localID, _, _ := tr.LocalMeta(localIdx)
if localID != "" {
res.LocalDeletes = append(res.LocalDeletes, localID)
} else {
res.LocalSkipped++
}
}
for ; remoteIdx < len(remote); remoteIdx += 1 {
if remote[remoteIdx].AccessorID != "" {
res.LocalUpserts = append(res.LocalUpserts, remote[remoteIdx].AccessorID)
for ; remoteIdx < lenRemote; remoteIdx += 1 {
remoteID, _, _ := tr.RemoteMeta(remoteIdx)
if remoteID != "" {
res.LocalUpserts = append(res.LocalUpserts, remoteID)
} else {
res.RemoteSkipped++
}
@ -239,25 +231,21 @@ func diffACLTokens(local structs.ACLTokens, remote structs.ACLTokenListStubs, la
return res
}
func (s *Server) deleteLocalACLTokens(deletions []string, ctx context.Context) (bool, error) {
func (s *Server) deleteLocalACLType(ctx context.Context, tr aclTypeReplicator, deletions []string) (bool, error) {
ticker := time.NewTicker(time.Second / time.Duration(s.config.ACLReplicationApplyLimit))
defer ticker.Stop()
for i := 0; i < len(deletions); i += aclBatchDeleteSize {
req := structs.ACLTokenBatchDeleteRequest{}
var batch []string
if i+aclBatchDeleteSize > len(deletions) {
req.TokenIDs = deletions[i:]
batch = deletions[i:]
} else {
req.TokenIDs = deletions[i : i+aclBatchDeleteSize]
batch = deletions[i : i+aclBatchDeleteSize]
}
resp, err := s.raftApply(structs.ACLTokenDeleteRequestType, &req)
if err != nil {
return false, fmt.Errorf("Failed to apply token deletions: %v", err)
}
if respErr, ok := resp.(error); ok && err != nil {
return false, fmt.Errorf("Failed to apply token deletions: %v", respErr)
if err := tr.DeleteLocalBatch(s, batch); err != nil {
return false, fmt.Errorf("Failed to apply %s deletions: %v", tr.SingularNoun(), err)
}
if i+aclBatchDeleteSize < len(deletions) {
@ -273,47 +261,50 @@ func (s *Server) deleteLocalACLTokens(deletions []string, ctx context.Context) (
return false, nil
}
func (s *Server) updateLocalACLTokens(tokens structs.ACLTokens, ctx context.Context) (bool, error) {
func (s *Server) updateLocalACLType(ctx context.Context, tr aclTypeReplicator) (bool, error) {
ticker := time.NewTicker(time.Second / time.Duration(s.config.ACLReplicationApplyLimit))
defer ticker.Stop()
lenPending := tr.LenPendingUpdates()
// outer loop handles submitting a batch
for batchStart := 0; batchStart < len(tokens); {
for batchStart := 0; batchStart < lenPending; {
// inner loop finds the last element to include in this batch.
batchSize := 0
batchEnd := batchStart
for ; batchEnd < len(tokens) && batchSize < aclBatchUpsertSize; batchEnd += 1 {
if tokens[batchEnd].SecretID == redactedToken {
return false, fmt.Errorf("Detected redacted token secrets: stopping token update round - verify that the replication token in use has acl:write permissions.")
for ; batchEnd < lenPending && batchSize < aclBatchUpsertSize; batchEnd += 1 {
if tr.PendingUpdateIsRedacted(batchEnd) {
return false, fmt.Errorf(
"Detected redacted %s secrets: stopping %s update round - verify that the replication token in use has acl:write permissions.",
tr.SingularNoun(),
tr.SingularNoun(),
)
}
batchSize += tokens[batchEnd].EstimateSize()
batchSize += tr.PendingUpdateEstimatedSize(batchEnd)
}
req := structs.ACLTokenBatchSetRequest{
Tokens: tokens[batchStart:batchEnd],
CAS: false,
}
resp, err := s.raftApply(structs.ACLTokenSetRequestType, &req)
err := tr.UpdateLocalBatch(ctx, s, batchStart, batchEnd)
if err != nil {
return false, fmt.Errorf("Failed to apply token upserts: %v", err)
}
if respErr, ok := resp.(error); ok && respErr != nil {
return false, fmt.Errorf("Failed to apply token upserts: %v", respErr)
return false, fmt.Errorf("Failed to apply %s upserts: %v", tr.SingularNoun(), err)
}
s.logger.Printf(
"[DEBUG] acl: %s replication - upserted 1 batch with %d %s of size %d",
tr.SingularNoun(),
batchEnd-batchStart,
tr.PluralNoun(),
batchSize,
)
s.logger.Printf("[DEBUG] acl: token replication - upserted 1 batch with %d tokens of size %d", batchEnd-batchStart, batchSize)
// tokens[batchEnd] wasn't include as the slicing doesn't include the element at the stop index
// items[batchEnd] wasn't include as the slicing doesn't include the element at the stop index
batchStart = batchEnd
// prevent waiting if we are done
if batchEnd < len(tokens) {
if batchEnd < lenPending {
select {
case <-ctx.Done():
return true, nil
case <-ticker.C:
// nothing to do - just rate limiting here
// nothing to do - just rate limiting
}
}
}
@ -359,95 +350,28 @@ func (s *Server) fetchACLTokens(lastRemoteIndex uint64) (*structs.ACLTokenListRe
return &response, nil
}
func (s *Server) replicateACLPolicies(lastRemoteIndex uint64, ctx context.Context) (uint64, bool, error) {
remote, err := s.fetchACLPolicies(lastRemoteIndex)
if err != nil {
return 0, false, fmt.Errorf("failed to retrieve remote ACL policies: %v", err)
}
s.logger.Printf("[DEBUG] acl: finished fetching policies tokens: %d", len(remote.Policies))
// Need to check if we should be stopping. This will be common as the fetching process is a blocking
// RPC which could have been hanging around for a long time and during that time leadership could
// have been lost.
select {
case <-ctx.Done():
return 0, true, nil
default:
// do nothing
}
// Measure everything after the remote query, which can block for long
// periods of time. This metric is a good measure of how expensive the
// replication process is.
defer metrics.MeasureSince([]string{"leader", "replication", "acl", "policy", "apply"}, time.Now())
_, local, err := s.fsm.State().ACLPolicyList(nil)
if err != nil {
return 0, false, fmt.Errorf("failed to retrieve local ACL policies: %v", err)
}
// If the remote index ever goes backwards, it's a good indication that
// the remote side was rebuilt and we should do a full sync since we
// can't make any assumptions about what's going on.
if remote.QueryMeta.Index < lastRemoteIndex {
s.logger.Printf("[WARN] consul: ACL policy replication remote index moved backwards (%d to %d), forcing a full ACL policy sync", lastRemoteIndex, remote.QueryMeta.Index)
lastRemoteIndex = 0
}
s.logger.Printf("[DEBUG] acl: policy replication - local: %d, remote: %d", len(local), len(remote.Policies))
// Calculate the changes required to bring the state into sync and then
// apply them.
deletions, updates := diffACLPolicies(local, remote.Policies, lastRemoteIndex)
s.logger.Printf("[DEBUG] acl: policy replication - deletions: %d, updates: %d", len(deletions), len(updates))
var policies *structs.ACLPolicyBatchResponse
if len(updates) > 0 {
policies, err = s.fetchACLPoliciesBatch(updates)
if err != nil {
return 0, false, fmt.Errorf("failed to retrieve ACL policy updates: %v", err)
}
s.logger.Printf("[DEBUG] acl: policy replication - downloaded %d policies", len(policies.Policies))
}
if len(deletions) > 0 {
s.logger.Printf("[DEBUG] acl: policy replication - performing deletions")
exit, err := s.deleteLocalACLPolicies(deletions, ctx)
if exit {
return 0, true, nil
}
if err != nil {
return 0, false, fmt.Errorf("failed to delete local ACL policies: %v", err)
}
s.logger.Printf("[DEBUG] acl: policy replication - finished deletions")
}
if len(updates) > 0 {
s.logger.Printf("[DEBUG] acl: policy replication - performing updates")
exit, err := s.updateLocalACLPolicies(policies.Policies, ctx)
if exit {
return 0, true, nil
}
if err != nil {
return 0, false, fmt.Errorf("failed to update local ACL policies: %v", err)
}
s.logger.Printf("[DEBUG] acl: policy replication - finished updates")
}
// Return the index we got back from the remote side, since we've synced
// up with the remote state as of that index.
return remote.QueryMeta.Index, false, nil
func (s *Server) replicateACLTokens(ctx context.Context, lastRemoteIndex uint64) (uint64, bool, error) {
tr := &aclTokenReplicator{}
return s.replicateACLType(ctx, tr, lastRemoteIndex)
}
func (s *Server) replicateACLTokens(lastRemoteIndex uint64, ctx context.Context) (uint64, bool, error) {
remote, err := s.fetchACLTokens(lastRemoteIndex)
func (s *Server) replicateACLPolicies(ctx context.Context, lastRemoteIndex uint64) (uint64, bool, error) {
tr := &aclPolicyReplicator{}
return s.replicateACLType(ctx, tr, lastRemoteIndex)
}
func (s *Server) replicateACLRoles(ctx context.Context, lastRemoteIndex uint64) (uint64, bool, error) {
tr := &aclRoleReplicator{}
return s.replicateACLType(ctx, tr, lastRemoteIndex)
}
func (s *Server) replicateACLType(ctx context.Context, tr aclTypeReplicator, lastRemoteIndex uint64) (uint64, bool, error) {
lenRemote, remoteIndex, err := tr.FetchRemote(s, lastRemoteIndex)
if err != nil {
return 0, false, fmt.Errorf("failed to retrieve remote ACL tokens: %v", err)
return 0, false, fmt.Errorf("failed to retrieve remote ACL %s: %v", tr.PluralNoun(), err)
}
s.logger.Printf("[DEBUG] acl: finished fetching remote tokens: %d", len(remote.Tokens))
s.logger.Printf("[DEBUG] acl: finished fetching %s: %d", tr.PluralNoun(), lenRemote)
// Need to check if we should be stopping. This will be common as the fetching process is a blocking
// RPC which could have been hanging around for a long time and during that time leadership could
@ -462,73 +386,99 @@ func (s *Server) replicateACLTokens(lastRemoteIndex uint64, ctx context.Context)
// Measure everything after the remote query, which can block for long
// periods of time. This metric is a good measure of how expensive the
// replication process is.
defer metrics.MeasureSince([]string{"leader", "replication", "acl", "token", "apply"}, time.Now())
defer metrics.MeasureSince([]string{"leader", "replication", "acl", tr.SingularNoun(), "apply"}, time.Now())
_, local, err := s.fsm.State().ACLTokenList(nil, false, true, "")
lenLocal, _, err := tr.FetchLocal(s)
if err != nil {
return 0, false, fmt.Errorf("failed to retrieve local ACL tokens: %v", err)
return 0, false, fmt.Errorf("failed to retrieve local ACL %s: %v", tr.PluralNoun(), err)
}
// If the remote index ever goes backwards, it's a good indication that
// the remote side was rebuilt and we should do a full sync since we
// can't make any assumptions about what's going on.
if remote.QueryMeta.Index < lastRemoteIndex {
s.logger.Printf("[WARN] consul: ACL token replication remote index moved backwards (%d to %d), forcing a full ACL token sync", lastRemoteIndex, remote.QueryMeta.Index)
if remoteIndex < lastRemoteIndex {
s.logger.Printf(
"[WARN] consul: ACL %s replication remote index moved backwards (%d to %d), forcing a full ACL %s sync",
tr.SingularNoun(),
lastRemoteIndex,
remoteIndex,
tr.SingularNoun(),
)
lastRemoteIndex = 0
}
s.logger.Printf("[DEBUG] acl: token replication - local: %d, remote: %d", len(local), len(remote.Tokens))
// Calculate the changes required to bring the state into sync and then
// apply them.
res := diffACLTokens(local, remote.Tokens, lastRemoteIndex)
s.logger.Printf(
"[DEBUG] acl: %s replication - local: %d, remote: %d",
tr.SingularNoun(),
lenLocal,
lenRemote,
)
// Calculate the changes required to bring the state into sync and then apply them.
res := diffACLType(tr, lastRemoteIndex)
if res.LocalSkipped > 0 || res.RemoteSkipped > 0 {
s.logger.Printf("[DEBUG] acl: token replication - deletions: %d, updates: %d, skipped: %d, skippedRemote: %d",
len(res.LocalDeletes), len(res.LocalUpserts), res.LocalSkipped, res.RemoteSkipped)
s.logger.Printf(
"[DEBUG] acl: %s replication - deletions: %d, updates: %d, skipped: %d, skippedRemote: %d",
tr.SingularNoun(),
len(res.LocalDeletes),
len(res.LocalUpserts),
res.LocalSkipped,
res.RemoteSkipped,
)
} else {
s.logger.Printf("[DEBUG] acl: token replication - deletions: %d, updates: %d", len(res.LocalDeletes), len(res.LocalUpserts))
s.logger.Printf(
"[DEBUG] acl: %s replication - deletions: %d, updates: %d",
tr.SingularNoun(),
len(res.LocalDeletes),
len(res.LocalUpserts),
)
}
var tokens *structs.ACLTokenBatchResponse
if len(res.LocalUpserts) > 0 {
tokens, err = s.fetchACLTokensBatch(res.LocalUpserts)
if err != nil {
return 0, false, fmt.Errorf("failed to retrieve ACL token updates: %v", err)
} else if tokens.Redacted {
return 0, false, fmt.Errorf("failed to retrieve unredacted tokens - replication token in use does not grant acl:write")
lenUpdated, err := tr.FetchUpdated(s, res.LocalUpserts)
if err == errContainsRedactedData {
return 0, false, fmt.Errorf("failed to retrieve unredacted %s - replication token in use does not grant acl:write", tr.PluralNoun())
} else if err != nil {
return 0, false, fmt.Errorf("failed to retrieve ACL %s updates: %v", tr.SingularNoun(), err)
}
s.logger.Printf("[DEBUG] acl: token replication - downloaded %d tokens", len(tokens.Tokens))
s.logger.Printf(
"[DEBUG] acl: %s replication - downloaded %d %s",
tr.SingularNoun(),
lenUpdated,
tr.PluralNoun(),
)
}
if len(res.LocalDeletes) > 0 {
s.logger.Printf("[DEBUG] acl: token replication - performing deletions")
s.logger.Printf(
"[DEBUG] acl: %s replication - performing deletions",
tr.SingularNoun(),
)
exit, err := s.deleteLocalACLTokens(res.LocalDeletes, ctx)
exit, err := s.deleteLocalACLType(ctx, tr, res.LocalDeletes)
if exit {
return 0, true, nil
}
if err != nil {
return 0, false, fmt.Errorf("failed to delete local ACL tokens: %v", err)
return 0, false, fmt.Errorf("failed to delete local ACL %s: %v", tr.PluralNoun(), err)
}
s.logger.Printf("[DEBUG] acl: token replication - finished deletions")
s.logger.Printf("[DEBUG] acl: %s replication - finished deletions", tr.SingularNoun())
}
if len(res.LocalUpserts) > 0 {
s.logger.Printf("[DEBUG] acl: token replication - performing updates")
exit, err := s.updateLocalACLTokens(tokens.Tokens, ctx)
s.logger.Printf("[DEBUG] acl: %s replication - performing updates", tr.SingularNoun())
exit, err := s.updateLocalACLType(ctx, tr)
if exit {
return 0, true, nil
}
if err != nil {
return 0, false, fmt.Errorf("failed to update local ACL tokens: %v", err)
return 0, false, fmt.Errorf("failed to update local ACL %s: %v", tr.PluralNoun(), err)
}
s.logger.Printf("[DEBUG] acl: token replication - finished updates")
s.logger.Printf("[DEBUG] acl: %s replication - finished updates", tr.SingularNoun())
}
// Return the index we got back from the remote side, since we've synced
// up with the remote state as of that index.
return remote.QueryMeta.Index, false, nil
return remoteIndex, false, nil
}
// IsACLReplicationEnabled returns true if ACL replication is enabled.
@ -546,20 +496,23 @@ func (s *Server) updateACLReplicationStatusError() {
s.aclReplicationStatus.LastError = time.Now().Round(time.Second).UTC()
}
func (s *Server) updateACLReplicationStatusIndex(index uint64) {
func (s *Server) updateACLReplicationStatusIndex(replicationType structs.ACLReplicationType, index uint64) {
s.aclReplicationStatusLock.Lock()
defer s.aclReplicationStatusLock.Unlock()
s.aclReplicationStatus.LastSuccess = time.Now().Round(time.Second).UTC()
s.aclReplicationStatus.ReplicatedIndex = index
}
func (s *Server) updateACLReplicationStatusTokenIndex(index uint64) {
s.aclReplicationStatusLock.Lock()
defer s.aclReplicationStatusLock.Unlock()
s.aclReplicationStatus.LastSuccess = time.Now().Round(time.Second).UTC()
s.aclReplicationStatus.ReplicatedTokenIndex = index
switch replicationType {
case structs.ACLReplicateLegacy:
s.aclReplicationStatus.ReplicatedIndex = index
case structs.ACLReplicateTokens:
s.aclReplicationStatus.ReplicatedTokenIndex = index
case structs.ACLReplicatePolicies:
s.aclReplicationStatus.ReplicatedIndex = index
case structs.ACLReplicateRoles:
s.aclReplicationStatus.ReplicatedRoleIndex = index
default:
panic("unknown replication type: " + replicationType.SingularNoun())
}
}
func (s *Server) initReplicationStatus() {
@ -582,6 +535,21 @@ func (s *Server) updateACLReplicationStatusRunning(replicationType structs.ACLRe
s.aclReplicationStatusLock.Lock()
defer s.aclReplicationStatusLock.Unlock()
// The running state represents which type of overall replication has been
// configured. Though there are various types of internal plumbing for acl
// replication, to the end user there are only 3 distinctly configurable
// variants: legacy, policy, token. Roles replicate with policies so we
// round that up here.
if replicationType == structs.ACLReplicateRoles {
replicationType = structs.ACLReplicatePolicies
}
s.aclReplicationStatus.Running = true
s.aclReplicationStatus.ReplicationType = replicationType
}
func (s *Server) getACLReplicationStatusRunningType() (structs.ACLReplicationType, bool) {
s.aclReplicationStatusLock.RLock()
defer s.aclReplicationStatusLock.RUnlock()
return s.aclReplicationStatus.ReplicationType, s.aclReplicationStatus.Running
}

View File

@ -6,7 +6,7 @@ import (
"sort"
"time"
"github.com/armon/go-metrics"
metrics "github.com/armon/go-metrics"
"github.com/hashicorp/consul/agent/structs"
)
@ -138,13 +138,18 @@ func reconcileLegacyACLs(local, remote structs.ACLs, lastRemoteIndex uint64) str
// FetchLocalACLs returns the ACLs in the local state store.
func (s *Server) fetchLocalLegacyACLs() (structs.ACLs, error) {
_, local, err := s.fsm.State().ACLTokenList(nil, false, true, "")
_, local, err := s.fsm.State().ACLTokenList(nil, false, true, "", "", "")
if err != nil {
return nil, err
}
now := time.Now()
var acls structs.ACLs
for _, token := range local {
if token.IsExpired(now) {
continue
}
if acl, err := token.Convert(); err == nil && acl != nil {
acls = append(acls, acl)
}

View File

@ -335,6 +335,10 @@ func TestACLReplication_IsACLReplicationEnabled(t *testing.T) {
}
}
// Note that this test is testing that legacy token data is replicated, NOT
// directly testing the legacy acl replication goroutine code.
//
// Actually testing legacy replication is difficult to do without old binaries.
func TestACLReplication_LegacyTokens(t *testing.T) {
t.Parallel()
dir1, s1 := testServerWithConfig(t, func(c *Config) {
@ -367,6 +371,12 @@ func TestACLReplication_LegacyTokens(t *testing.T) {
testrpc.WaitForLeader(t, s1.RPC, "dc1")
testrpc.WaitForLeader(t, s1.RPC, "dc2")
// Wait for legacy acls to be disabled so we are clear that
// legacy replication isn't meddling.
waitForNewACLs(t, s1)
waitForNewACLs(t, s2)
waitForNewACLReplication(t, s2, structs.ACLReplicateTokens)
// Create a bunch of new tokens.
var id string
for i := 0; i < 50; i++ {
@ -386,14 +396,15 @@ func TestACLReplication_LegacyTokens(t *testing.T) {
}
checkSame := func() error {
index, remote, err := s1.fsm.State().ACLTokenList(nil, true, true, "")
index, remote, err := s1.fsm.State().ACLTokenList(nil, true, true, "", "", "")
if err != nil {
return err
}
_, local, err := s2.fsm.State().ACLTokenList(nil, true, true, "")
_, local, err := s2.fsm.State().ACLTokenList(nil, true, true, "", "", "")
if err != nil {
return err
}
if got, want := len(remote), len(local); got != want {
return fmt.Errorf("got %d remote ACLs want %d", got, want)
}

View File

@ -3,6 +3,7 @@ package consul
import (
"fmt"
"os"
"strconv"
"testing"
"time"
@ -15,6 +16,11 @@ import (
)
func TestACLReplication_diffACLPolicies(t *testing.T) {
diffACLPolicies := func(local structs.ACLPolicies, remote structs.ACLPolicyListStubs, lastRemoteIndex uint64) ([]string, []string) {
tr := &aclPolicyReplicator{local: local, remote: remote}
res := diffACLType(tr, lastRemoteIndex)
return res.LocalDeletes, res.LocalUpserts
}
local := structs.ACLPolicies{
&structs.ACLPolicy{
ID: "44ef9aec-7654-4401-901b-4d4a8b3c80fc",
@ -127,6 +133,15 @@ func TestACLReplication_diffACLPolicies(t *testing.T) {
}
func TestACLReplication_diffACLTokens(t *testing.T) {
diffACLTokens := func(
local structs.ACLTokens,
remote structs.ACLTokenListStubs,
lastRemoteIndex uint64,
) itemDiffResults {
tr := &aclTokenReplicator{local: local, remote: remote}
return diffACLType(tr, lastRemoteIndex)
}
local := structs.ACLTokens{
// When a just-upgraded (1.3->1.4+) secondary DC is replicating from an
// upgraded primary DC (1.4+), the local state for tokens predating the
@ -307,6 +322,12 @@ func TestACLReplication_Tokens(t *testing.T) {
testrpc.WaitForLeader(t, s1.RPC, "dc1")
testrpc.WaitForLeader(t, s1.RPC, "dc2")
// Wait for legacy acls to be disabled so we are clear that
// legacy replication isn't meddling.
waitForNewACLs(t, s1)
waitForNewACLs(t, s2)
waitForNewACLReplication(t, s2, structs.ACLReplicateTokens)
// Create a bunch of new tokens and policies
var tokens structs.ACLTokens
for i := 0; i < 50; i++ {
@ -328,11 +349,11 @@ func TestACLReplication_Tokens(t *testing.T) {
tokens = append(tokens, &token)
}
checkSame := func(t *retry.R) error {
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, "")
index, remote, err := s1.fsm.State().ACLTokenList(nil, false, true, "", "", "")
require.NoError(t, err)
_, local, err := s2.fsm.State().ACLTokenList(nil, false, true, "")
_, local, err := s2.fsm.State().ACLTokenList(nil, false, true, "", "", "")
require.NoError(t, err)
require.Len(t, local, len(remote))
@ -340,18 +361,15 @@ func TestACLReplication_Tokens(t *testing.T) {
require.Equal(t, token.Hash, local[i].Hash)
}
var status structs.ACLReplicationStatus
s2.aclReplicationStatusLock.RLock()
status = s2.aclReplicationStatus
status := s2.aclReplicationStatus
s2.aclReplicationStatusLock.RUnlock()
if !status.Enabled || !status.Running ||
status.ReplicationType != structs.ACLReplicateTokens ||
status.ReplicatedTokenIndex != index ||
status.SourceDatacenter != "dc1" {
return fmt.Errorf("ACL replication status differs")
}
return nil
require.True(t, status.Enabled)
require.True(t, status.Running)
require.Equal(t, status.ReplicationType, structs.ACLReplicateTokens)
require.Equal(t, status.ReplicatedTokenIndex, index)
require.Equal(t, status.SourceDatacenter, "dc1")
}
// Wait for the replica to converge.
retry.Run(t, func(r *retry.R) {
@ -426,7 +444,7 @@ func TestACLReplication_Tokens(t *testing.T) {
})
// verify dc2 local tokens didn't get blown away
_, local, err := s2.fsm.State().ACLTokenList(nil, true, false, "")
_, local, err := s2.fsm.State().ACLTokenList(nil, true, false, "", "", "")
require.NoError(t, err)
require.Len(t, local, 50)
@ -479,6 +497,12 @@ func TestACLReplication_Policies(t *testing.T) {
testrpc.WaitForLeader(t, s1.RPC, "dc1")
testrpc.WaitForLeader(t, s1.RPC, "dc2")
// Wait for legacy acls to be disabled so we are clear that
// legacy replication isn't meddling.
waitForNewACLs(t, s1)
waitForNewACLs(t, s2)
waitForNewACLReplication(t, s2, structs.ACLReplicatePolicies)
// Create a bunch of new policies
var policies structs.ACLPolicies
for i := 0; i < 50; i++ {
@ -496,7 +520,7 @@ func TestACLReplication_Policies(t *testing.T) {
policies = append(policies, &policy)
}
checkSame := func(t *retry.R) error {
checkSame := func(t *retry.R) {
// only account for global tokens - local tokens shouldn't be replicated
index, remote, err := s1.fsm.State().ACLPolicyList(nil)
require.NoError(t, err)
@ -508,18 +532,15 @@ func TestACLReplication_Policies(t *testing.T) {
require.Equal(t, policy.Hash, local[i].Hash)
}
var status structs.ACLReplicationStatus
s2.aclReplicationStatusLock.RLock()
status = s2.aclReplicationStatus
status := s2.aclReplicationStatus
s2.aclReplicationStatusLock.RUnlock()
if !status.Enabled || !status.Running ||
status.ReplicationType != structs.ACLReplicatePolicies ||
status.ReplicatedIndex != index ||
status.SourceDatacenter != "dc1" {
return fmt.Errorf("ACL replication status differs")
}
return nil
require.True(t, status.Enabled)
require.True(t, status.Running)
require.Equal(t, status.ReplicationType, structs.ACLReplicatePolicies)
require.Equal(t, status.ReplicatedIndex, index)
require.Equal(t, status.SourceDatacenter, "dc1")
}
// Wait for the replica to converge.
retry.Run(t, func(r *retry.R) {
@ -709,3 +730,249 @@ func TestACLReplication_TokensRedacted(t *testing.T) {
require.True(r, status.LastError.After(minErrorTime), "Replication LastError not after the minErrorTime")
})
}
func TestACLReplication_AllTypes(t *testing.T) {
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()
testrpc.WaitForLeader(t, s1.RPC, "dc1")
client := rpcClient(t, s1)
defer client.Close()
dir2, s2 := testServerWithConfig(t, func(c *Config) {
c.Datacenter = "dc2"
c.ACLDatacenter = "dc1"
c.ACLsEnabled = true
c.ACLTokenReplication = true
c.ACLReplicationRate = 100
c.ACLReplicationBurst = 100
c.ACLReplicationApplyLimit = 1000000
})
s2.tokens.UpdateReplicationToken("root", tokenStore.TokenSourceConfig)
testrpc.WaitForLeader(t, s2.RPC, "dc2")
defer os.RemoveAll(dir2)
defer s2.Shutdown()
// Try to join.
joinWAN(t, s2, s1)
testrpc.WaitForLeader(t, s1.RPC, "dc1")
testrpc.WaitForLeader(t, s1.RPC, "dc2")
// Wait for legacy acls to be disabled so we are clear that
// legacy replication isn't meddling.
waitForNewACLs(t, s1)
waitForNewACLs(t, s2)
waitForNewACLReplication(t, s2, structs.ACLReplicateTokens)
const (
numItems = 50
numItemsThatAreLocal = 10
)
// Create some data.
policyIDs, roleIDs, tokenIDs := createACLTestData(t, s1, "b1", numItems, numItemsThatAreLocal)
checkSameTokens := 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, "", "", "")
require.NoError(t, err)
// Query for all of them, so that we can prove that no globals snuck in.
_, local, err := s2.fsm.State().ACLTokenList(nil, true, true, "", "", "")
require.NoError(t, err)
require.Len(t, remote, len(local))
for i, token := range remote {
require.Equal(t, token.Hash, local[i].Hash)
}
s2.aclReplicationStatusLock.RLock()
status := s2.aclReplicationStatus
s2.aclReplicationStatusLock.RUnlock()
require.True(t, status.Enabled)
require.True(t, status.Running)
require.Equal(t, status.ReplicationType, structs.ACLReplicateTokens)
require.Equal(t, status.ReplicatedTokenIndex, index)
require.Equal(t, status.SourceDatacenter, "dc1")
}
checkSamePolicies := func(t *retry.R) {
index, remote, err := s1.fsm.State().ACLPolicyList(nil)
require.NoError(t, err)
_, local, err := s2.fsm.State().ACLPolicyList(nil)
require.NoError(t, err)
require.Len(t, remote, len(local))
for i, policy := range remote {
require.Equal(t, policy.Hash, local[i].Hash)
}
s2.aclReplicationStatusLock.RLock()
status := s2.aclReplicationStatus
s2.aclReplicationStatusLock.RUnlock()
require.True(t, status.Enabled)
require.True(t, status.Running)
require.Equal(t, status.ReplicationType, structs.ACLReplicateTokens)
require.Equal(t, status.ReplicatedIndex, index)
require.Equal(t, status.SourceDatacenter, "dc1")
}
checkSameRoles := func(t *retry.R) {
index, remote, err := s1.fsm.State().ACLRoleList(nil, "")
require.NoError(t, err)
_, local, err := s2.fsm.State().ACLRoleList(nil, "")
require.NoError(t, err)
require.Len(t, remote, len(local))
for i, role := range remote {
require.Equal(t, role.Hash, local[i].Hash)
}
s2.aclReplicationStatusLock.RLock()
status := s2.aclReplicationStatus
s2.aclReplicationStatusLock.RUnlock()
require.True(t, status.Enabled)
require.True(t, status.Running)
require.Equal(t, status.ReplicationType, structs.ACLReplicateTokens)
require.Equal(t, status.ReplicatedRoleIndex, index)
require.Equal(t, status.SourceDatacenter, "dc1")
}
checkSame := func(t *retry.R) {
checkSameTokens(t)
checkSamePolicies(t)
checkSameRoles(t)
}
// Wait for the replica to converge.
retry.Run(t, func(r *retry.R) {
checkSame(r)
})
// Create additional data to replicate.
_, _, _ = createACLTestData(t, s1, "b2", numItems, numItemsThatAreLocal)
// Wait for the replica to converge.
retry.Run(t, func(r *retry.R) {
checkSame(r)
})
// Delete one piece of each type of data from batch 1.
const itemToDelete = numItems - 1
{
id := tokenIDs[itemToDelete]
arg := structs.ACLTokenDeleteRequest{
Datacenter: "dc1",
TokenID: id,
WriteRequest: structs.WriteRequest{Token: "root"},
}
var dontCare string
if err := s1.RPC("ACL.TokenDelete", &arg, &dontCare); err != nil {
t.Fatalf("err: %v", err)
}
}
{
id := roleIDs[itemToDelete]
arg := structs.ACLRoleDeleteRequest{
Datacenter: "dc1",
RoleID: id,
WriteRequest: structs.WriteRequest{Token: "root"},
}
var dontCare string
if err := s1.RPC("ACL.RoleDelete", &arg, &dontCare); err != nil {
t.Fatalf("err: %v", err)
}
}
{
id := policyIDs[itemToDelete]
arg := structs.ACLPolicyDeleteRequest{
Datacenter: "dc1",
PolicyID: id,
WriteRequest: structs.WriteRequest{Token: "root"},
}
var dontCare string
if err := s1.RPC("ACL.PolicyDelete", &arg, &dontCare); err != nil {
t.Fatalf("err: %v", err)
}
}
// Wait for the replica to converge.
retry.Run(t, func(r *retry.R) {
checkSame(r)
})
}
func createACLTestData(t *testing.T, srv *Server, namePrefix string, numObjects, numItemsThatAreLocal int) (policyIDs, roleIDs, tokenIDs []string) {
require.True(t, numItemsThatAreLocal <= numObjects, 0, "numItemsThatAreLocal <= numObjects")
// Create some policies.
for i := 0; i < numObjects; i++ {
str := strconv.Itoa(i)
arg := structs.ACLPolicySetRequest{
Datacenter: "dc1",
Policy: structs.ACLPolicy{
Name: namePrefix + "-policy-" + str,
Description: namePrefix + "-policy " + str,
Rules: testACLPolicyNew,
},
WriteRequest: structs.WriteRequest{Token: "root"},
}
var out structs.ACLPolicy
if err := srv.RPC("ACL.PolicySet", &arg, &out); err != nil {
t.Fatalf("err: %v", err)
}
policyIDs = append(policyIDs, out.ID)
}
// Create some roles.
for i := 0; i < numObjects; i++ {
str := strconv.Itoa(i)
arg := structs.ACLRoleSetRequest{
Datacenter: "dc1",
Role: structs.ACLRole{
Name: namePrefix + "-role-" + str,
Description: namePrefix + "-role " + str,
Policies: []structs.ACLRolePolicyLink{
{ID: policyIDs[i]},
},
},
WriteRequest: structs.WriteRequest{Token: "root"},
}
var out structs.ACLRole
if err := srv.RPC("ACL.RoleSet", &arg, &out); err != nil {
t.Fatalf("err: %v", err)
}
roleIDs = append(roleIDs, out.ID)
}
// Create a bunch of new tokens.
for i := 0; i < numObjects; i++ {
str := strconv.Itoa(i)
arg := structs.ACLTokenSetRequest{
Datacenter: "dc1",
ACLToken: structs.ACLToken{
Description: namePrefix + "-token " + str,
Policies: []structs.ACLTokenPolicyLink{
{ID: policyIDs[i]},
},
Roles: []structs.ACLTokenRoleLink{
{ID: roleIDs[i]},
},
Local: (i < numItemsThatAreLocal),
},
WriteRequest: structs.WriteRequest{Token: "root"},
}
var out structs.ACLToken
if err := srv.RPC("ACL.TokenSet", &arg, &out); err != nil {
t.Fatalf("err: %v", err)
}
tokenIDs = append(tokenIDs, out.AccessorID)
}
return policyIDs, roleIDs, tokenIDs
}

View File

@ -0,0 +1,370 @@
package consul
import (
"context"
"fmt"
"github.com/hashicorp/consul/agent/structs"
)
type aclTokenReplicator struct {
local structs.ACLTokens
remote structs.ACLTokenListStubs
updated []*structs.ACLToken
}
var _ aclTypeReplicator = (*aclTokenReplicator)(nil)
func (r *aclTokenReplicator) Type() structs.ACLReplicationType { return structs.ACLReplicateTokens }
func (r *aclTokenReplicator) SingularNoun() string { return "token" }
func (r *aclTokenReplicator) PluralNoun() string { return "tokens" }
func (r *aclTokenReplicator) FetchRemote(srv *Server, lastRemoteIndex uint64) (int, uint64, error) {
r.remote = nil
remote, err := srv.fetchACLTokens(lastRemoteIndex)
if err != nil {
return 0, 0, err
}
r.remote = remote.Tokens
return len(remote.Tokens), remote.QueryMeta.Index, nil
}
func (r *aclTokenReplicator) FetchLocal(srv *Server) (int, uint64, error) {
r.local = nil
idx, local, err := srv.fsm.State().ACLTokenList(nil, false, true, "", "", "")
if err != nil {
return 0, 0, err
}
// Do not filter by expiration times. Wait until the tokens are explicitly
// deleted.
r.local = local
return len(local), idx, nil
}
func (r *aclTokenReplicator) SortState() (int, int) {
r.local.Sort()
r.remote.Sort()
return len(r.local), len(r.remote)
}
func (r *aclTokenReplicator) LocalMeta(i int) (id string, modIndex uint64, hash []byte) {
v := r.local[i]
return v.AccessorID, v.ModifyIndex, v.Hash
}
func (r *aclTokenReplicator) RemoteMeta(i int) (id string, modIndex uint64, hash []byte) {
v := r.remote[i]
return v.AccessorID, v.ModifyIndex, v.Hash
}
func (r *aclTokenReplicator) FetchUpdated(srv *Server, updates []string) (int, error) {
r.updated = nil
if len(updates) > 0 {
tokens, err := srv.fetchACLTokensBatch(updates)
if err != nil {
return 0, err
} else if tokens.Redacted {
return 0, errContainsRedactedData
}
// Do not filter by expiration times. Wait until the tokens are
// explicitly deleted.
r.updated = tokens.Tokens
}
return len(r.updated), nil
}
func (r *aclTokenReplicator) DeleteLocalBatch(srv *Server, batch []string) error {
req := structs.ACLTokenBatchDeleteRequest{
TokenIDs: batch,
}
resp, err := srv.raftApply(structs.ACLTokenDeleteRequestType, &req)
if err != nil {
return err
}
if respErr, ok := resp.(error); ok && err != nil {
return respErr
}
return nil
}
func (r *aclTokenReplicator) LenPendingUpdates() int {
return len(r.updated)
}
func (r *aclTokenReplicator) PendingUpdateEstimatedSize(i int) int {
return r.updated[i].EstimateSize()
}
func (r *aclTokenReplicator) PendingUpdateIsRedacted(i int) bool {
return r.updated[i].SecretID == redactedToken
}
func (r *aclTokenReplicator) UpdateLocalBatch(ctx context.Context, srv *Server, start, end int) error {
req := structs.ACLTokenBatchSetRequest{
Tokens: r.updated[start:end],
CAS: false,
}
resp, err := srv.raftApply(structs.ACLTokenSetRequestType, &req)
if err != nil {
return err
}
if respErr, ok := resp.(error); ok && err != nil {
return respErr
}
return nil
}
///////////////////////
type aclPolicyReplicator struct {
local structs.ACLPolicies
remote structs.ACLPolicyListStubs
updated []*structs.ACLPolicy
}
var _ aclTypeReplicator = (*aclPolicyReplicator)(nil)
func (r *aclPolicyReplicator) Type() structs.ACLReplicationType { return structs.ACLReplicatePolicies }
func (r *aclPolicyReplicator) SingularNoun() string { return "policy" }
func (r *aclPolicyReplicator) PluralNoun() string { return "policies" }
func (r *aclPolicyReplicator) FetchRemote(srv *Server, lastRemoteIndex uint64) (int, uint64, error) {
r.remote = nil
remote, err := srv.fetchACLPolicies(lastRemoteIndex)
if err != nil {
return 0, 0, err
}
r.remote = remote.Policies
return len(remote.Policies), remote.QueryMeta.Index, nil
}
func (r *aclPolicyReplicator) FetchLocal(srv *Server) (int, uint64, error) {
r.local = nil
idx, local, err := srv.fsm.State().ACLPolicyList(nil)
if err != nil {
return 0, 0, err
}
r.local = local
return len(local), idx, nil
}
func (r *aclPolicyReplicator) SortState() (int, int) {
r.local.Sort()
r.remote.Sort()
return len(r.local), len(r.remote)
}
func (r *aclPolicyReplicator) LocalMeta(i int) (id string, modIndex uint64, hash []byte) {
v := r.local[i]
return v.ID, v.ModifyIndex, v.Hash
}
func (r *aclPolicyReplicator) RemoteMeta(i int) (id string, modIndex uint64, hash []byte) {
v := r.remote[i]
return v.ID, v.ModifyIndex, v.Hash
}
func (r *aclPolicyReplicator) FetchUpdated(srv *Server, updates []string) (int, error) {
r.updated = nil
if len(updates) > 0 {
policies, err := srv.fetchACLPoliciesBatch(updates)
if err != nil {
return 0, err
}
r.updated = policies.Policies
}
return len(r.updated), nil
}
func (r *aclPolicyReplicator) DeleteLocalBatch(srv *Server, batch []string) error {
req := structs.ACLPolicyBatchDeleteRequest{
PolicyIDs: batch,
}
resp, err := srv.raftApply(structs.ACLPolicyDeleteRequestType, &req)
if err != nil {
return err
}
if respErr, ok := resp.(error); ok && err != nil {
return respErr
}
return nil
}
func (r *aclPolicyReplicator) LenPendingUpdates() int {
return len(r.updated)
}
func (r *aclPolicyReplicator) PendingUpdateEstimatedSize(i int) int {
return r.updated[i].EstimateSize()
}
func (r *aclPolicyReplicator) PendingUpdateIsRedacted(i int) bool {
return false
}
func (r *aclPolicyReplicator) UpdateLocalBatch(ctx context.Context, srv *Server, start, end int) error {
req := structs.ACLPolicyBatchSetRequest{
Policies: r.updated[start:end],
}
resp, err := srv.raftApply(structs.ACLPolicySetRequestType, &req)
if err != nil {
return err
}
if respErr, ok := resp.(error); ok && err != nil {
return respErr
}
return nil
}
////////////////////////////////
type aclRoleReplicator struct {
local structs.ACLRoles
remote structs.ACLRoles
updated []*structs.ACLRole
}
var _ aclTypeReplicator = (*aclRoleReplicator)(nil)
func (r *aclRoleReplicator) Type() structs.ACLReplicationType { return structs.ACLReplicateRoles }
func (r *aclRoleReplicator) SingularNoun() string { return "role" }
func (r *aclRoleReplicator) PluralNoun() string { return "roles" }
func (r *aclRoleReplicator) FetchRemote(srv *Server, lastRemoteIndex uint64) (int, uint64, error) {
r.remote = nil
remote, err := srv.fetchACLRoles(lastRemoteIndex)
if err != nil {
return 0, 0, err
}
r.remote = remote.Roles
return len(remote.Roles), remote.QueryMeta.Index, nil
}
func (r *aclRoleReplicator) FetchLocal(srv *Server) (int, uint64, error) {
r.local = nil
idx, local, err := srv.fsm.State().ACLRoleList(nil, "")
if err != nil {
return 0, 0, err
}
r.local = local
return len(local), idx, nil
}
func (r *aclRoleReplicator) SortState() (int, int) {
r.local.Sort()
r.remote.Sort()
return len(r.local), len(r.remote)
}
func (r *aclRoleReplicator) LocalMeta(i int) (id string, modIndex uint64, hash []byte) {
v := r.local[i]
return v.ID, v.ModifyIndex, v.Hash
}
func (r *aclRoleReplicator) RemoteMeta(i int) (id string, modIndex uint64, hash []byte) {
v := r.remote[i]
return v.ID, v.ModifyIndex, v.Hash
}
func (r *aclRoleReplicator) FetchUpdated(srv *Server, updates []string) (int, error) {
r.updated = nil
if len(updates) > 0 {
// Since ACLRoles do not have a "list entry" variation, all of the data
// to replicate a role is already present in the "r.remote" list.
//
// We avoid a second query by just repurposing the data we already have
// access to in a way that is compatible with the generic ACL type
// replicator.
keep := make(map[string]struct{})
for _, id := range updates {
keep[id] = struct{}{}
}
subset := make([]*structs.ACLRole, 0, len(updates))
for _, role := range r.remote {
if _, ok := keep[role.ID]; ok {
subset = append(subset, role)
}
}
if len(subset) != len(keep) { // only possible via programming bug
for _, role := range subset {
delete(keep, role.ID)
}
missing := make([]string, 0, len(keep))
for id, _ := range keep {
missing = append(missing, id)
}
return 0, fmt.Errorf("role replication trying to replicated uncached roles with IDs: %v", missing)
}
r.updated = subset
}
return len(r.updated), nil
}
func (r *aclRoleReplicator) DeleteLocalBatch(srv *Server, batch []string) error {
req := structs.ACLRoleBatchDeleteRequest{
RoleIDs: batch,
}
resp, err := srv.raftApply(structs.ACLRoleDeleteRequestType, &req)
if err != nil {
return err
}
if respErr, ok := resp.(error); ok && err != nil {
return respErr
}
return nil
}
func (r *aclRoleReplicator) LenPendingUpdates() int {
return len(r.updated)
}
func (r *aclRoleReplicator) PendingUpdateEstimatedSize(i int) int {
return r.updated[i].EstimateSize()
}
func (r *aclRoleReplicator) PendingUpdateIsRedacted(i int) bool {
return false
}
func (r *aclRoleReplicator) UpdateLocalBatch(ctx context.Context, srv *Server, start, end int) error {
req := structs.ACLRoleBatchSetRequest{
Roles: r.updated[start:end],
}
resp, err := srv.raftApply(structs.ACLRoleSetRequestType, &req)
if err != nil {
return err
}
if respErr, ok := resp.(error); ok && err != nil {
return respErr
}
return nil
}

View File

@ -2,6 +2,7 @@ package consul
import (
"sync/atomic"
"time"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/structs"
@ -12,7 +13,7 @@ var serverACLCacheConfig *structs.ACLCachesConfig = &structs.ACLCachesConfig{
// The server's ACL caching has a few underlying assumptions:
//
// 1 - All policies can be resolved locally. Hence we do not cache any
// unparsed policies as we have memdb for that.
// unparsed policies/roles as we have memdb for that.
// 2 - While there could be many identities being used within a DC the
// number of distinct policies and combined multi-policy authorizers
// will be much less.
@ -25,10 +26,16 @@ var serverACLCacheConfig *structs.ACLCachesConfig = &structs.ACLCachesConfig{
Policies: 0,
ParsedPolicies: 512,
Authorizers: 1024,
Roles: 0,
}
func (s *Server) checkTokenUUID(id string) (bool, error) {
state := s.fsm.State()
// We won't check expiration times here. If we generate a UUID that matches
// a token that hasn't been reaped yet, then we won't be able to insert the
// new token due to a collision.
if _, token, err := state.ACLTokenGetByAccessor(nil, id); err != nil {
return false, err
} else if token != nil {
@ -55,6 +62,28 @@ func (s *Server) checkPolicyUUID(id string) (bool, error) {
return !structs.ACLIDReserved(id), nil
}
func (s *Server) checkRoleUUID(id string) (bool, error) {
state := s.fsm.State()
if _, role, err := state.ACLRoleGetByID(nil, id); err != nil {
return false, err
} else if role != nil {
return false, nil
}
return !structs.ACLIDReserved(id), nil
}
func (s *Server) checkBindingRuleUUID(id string) (bool, error) {
state := s.fsm.State()
if _, rule, err := state.ACLBindingRuleGetByID(nil, id); err != nil {
return false, err
} else if rule != nil {
return false, nil
}
return !structs.ACLIDReserved(id), nil
}
func (s *Server) updateACLAdvertisement() {
// One thing to note is that once in new ACL mode the server will
// never transition to legacy ACL mode. This is not currently a
@ -145,7 +174,7 @@ func (s *Server) ResolveIdentityFromToken(token string) (bool, structs.ACLIdenti
index, aclToken, err := s.fsm.State().ACLTokenGetBySecret(nil, token)
if err != nil {
return true, nil, err
} else if aclToken != nil {
} else if aclToken != nil && !aclToken.IsExpired(time.Now()) {
return true, aclToken, nil
}
@ -166,6 +195,20 @@ func (s *Server) ResolvePolicyFromID(policyID string) (bool, *structs.ACLPolicy,
return s.InACLDatacenter() || index > 0, policy, acl.ErrNotFound
}
func (s *Server) ResolveRoleFromID(roleID string) (bool, *structs.ACLRole, error) {
index, role, err := s.fsm.State().ACLRoleGetByID(nil, roleID)
if err != nil {
return true, nil, err
} else if role != nil {
return true, role, nil
}
// If the max index of the roles table is non-zero then we have acls, until then
// we may need to allow remote resolution. This is particularly useful to allow updating
// the replication token via the API in a non-primary dc.
return s.InACLDatacenter() || index > 0, role, acl.ErrNotFound
}
func (s *Server) ResolveToken(token string) (acl.Authorizer, error) {
return s.acls.ResolveToken(token)
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,144 @@
package consul
import (
"context"
"fmt"
"time"
"github.com/hashicorp/consul/agent/structs"
"golang.org/x/time/rate"
)
func (s *Server) startACLTokenReaping() {
s.aclTokenReapLock.Lock()
defer s.aclTokenReapLock.Unlock()
if s.aclTokenReapEnabled {
return
}
ctx, cancel := context.WithCancel(context.Background())
s.aclTokenReapCancel = cancel
// Do a quick check for config settings that would imply the goroutine
// below will just spin forever.
//
// We can only check the config settings here that cannot change without a
// restart, so we omit the check for a non-empty replication token as that
// can be changed at runtime.
if !s.InACLDatacenter() && !s.config.ACLTokenReplication {
return
}
go func() {
limiter := rate.NewLimiter(aclTokenReapingRateLimit, aclTokenReapingBurst)
for {
if err := limiter.Wait(ctx); err != nil {
return
}
if s.LocalTokensEnabled() {
if _, err := s.reapExpiredLocalACLTokens(); err != nil {
s.logger.Printf("[ERR] acl: error reaping expired local ACL tokens: %v", err)
}
}
if s.InACLDatacenter() {
if _, err := s.reapExpiredGlobalACLTokens(); err != nil {
s.logger.Printf("[ERR] acl: error reaping expired global ACL tokens: %v", err)
}
}
}
}()
s.aclTokenReapEnabled = true
}
func (s *Server) stopACLTokenReaping() {
s.aclTokenReapLock.Lock()
defer s.aclTokenReapLock.Unlock()
if !s.aclTokenReapEnabled {
return
}
s.aclTokenReapCancel()
s.aclTokenReapCancel = nil
s.aclTokenReapEnabled = false
}
func (s *Server) reapExpiredGlobalACLTokens() (int, error) {
return s.reapExpiredACLTokens(false, true)
}
func (s *Server) reapExpiredLocalACLTokens() (int, error) {
return s.reapExpiredACLTokens(true, false)
}
func (s *Server) reapExpiredACLTokens(local, global bool) (int, error) {
if !s.ACLsEnabled() {
return 0, nil
}
if s.UseLegacyACLs() {
return 0, nil
}
if local == global {
return 0, fmt.Errorf("cannot reap both local and global tokens in the same request")
}
locality := localityName(local)
minExpiredTime, err := s.fsm.State().ACLTokenMinExpirationTime(local)
if err != nil {
return 0, err
}
now := time.Now()
if minExpiredTime.After(now) {
return 0, nil // nothing to do
}
tokens, _, err := s.fsm.State().ACLTokenListExpired(local, now, aclBatchDeleteSize)
if err != nil {
return 0, err
}
if len(tokens) == 0 {
return 0, nil
}
var (
secretIDs []string
req structs.ACLTokenBatchDeleteRequest
)
for _, token := range tokens {
if token.Local != local {
return 0, fmt.Errorf("expired index for local=%v returned a mismatched token with local=%v: %s", local, token.Local, token.AccessorID)
}
req.TokenIDs = append(req.TokenIDs, token.AccessorID)
secretIDs = append(secretIDs, token.SecretID)
}
s.logger.Printf("[INFO] acl: deleting %d expired %s tokens", len(req.TokenIDs), locality)
resp, err := s.raftApply(structs.ACLTokenDeleteRequestType, &req)
if err != nil {
return 0, fmt.Errorf("Failed to apply token expiration deletions: %v", err)
}
// Purge the identities from the cache
for _, secretID := range secretIDs {
s.acls.cache.RemoveIdentity(secretID)
}
if respErr, ok := resp.(error); ok {
return 0, respErr
}
return len(req.TokenIDs), nil
}
func localityName(local bool) string {
if local {
return "local"
}
return "global"
}

View File

@ -0,0 +1,219 @@
package consul
import (
"os"
"testing"
"time"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/testrpc"
"github.com/stretchr/testify/require"
)
func TestACLTokenReap_Primary(t *testing.T) {
t.Parallel()
t.Run("global", func(t *testing.T) {
t.Parallel()
testACLTokenReap_Primary(t, false, true)
})
t.Run("local", func(t *testing.T) {
t.Parallel()
testACLTokenReap_Primary(t, true, false)
})
}
func testACLTokenReap_Primary(t *testing.T, local, global bool) {
// -------------------------------------------
// A word of caution when testing reapExpiredACLTokens():
//
// The underlying memdb index used for reaping has a minimum granularity of
// 1 second as it delegates to `time.Unix()`. This test will have to be
// deliberately slow to allow for necessary sleeps. If you try to make it
// operate faster (using expiration ttls of milliseconds) it will be flaky.
// -------------------------------------------
t.Helper()
require.NotEqual(t, local, global)
dir1, s1 := testServerWithConfig(t, func(c *Config) {
c.ACLDatacenter = "dc1"
c.ACLsEnabled = true
c.ACLMasterToken = "root"
c.ACLTokenMinExpirationTTL = 10 * time.Millisecond
c.ACLTokenMaxExpirationTTL = 8 * time.Second
})
defer os.RemoveAll(dir1)
defer s1.Shutdown()
testrpc.WaitForLeader(t, s1.RPC, "dc1")
codec := rpcClient(t, s1)
defer codec.Close()
acl := ACL{srv: s1}
masterTokenAccessorID, err := retrieveTestTokenAccessorForSecret(codec, "root", "dc1", "root")
require.NoError(t, err)
listTokens := func() (localTokens, globalTokens []string, err error) {
req := structs.ACLTokenListRequest{
Datacenter: "dc1",
QueryOptions: structs.QueryOptions{Token: "root"},
}
var res structs.ACLTokenListResponse
err = acl.TokenList(&req, &res)
if err != nil {
return nil, nil, err
}
for _, tok := range res.Tokens {
if tok.Local {
localTokens = append(localTokens, tok.AccessorID)
} else {
globalTokens = append(globalTokens, tok.AccessorID)
}
}
return localTokens, globalTokens, nil
}
requireTokenMatch := func(t *testing.T, expect []string) {
t.Helper()
var expectLocal, expectGlobal []string
// The master token and the anonymous token are always going to be
// present and global.
expectGlobal = append(expectGlobal, masterTokenAccessorID)
expectGlobal = append(expectGlobal, structs.ACLTokenAnonymousID)
if local {
expectLocal = append(expectLocal, expect...)
} else {
expectGlobal = append(expectGlobal, expect...)
}
localTokens, globalTokens, err := listTokens()
require.NoError(t, err)
require.ElementsMatch(t, expectLocal, localTokens)
require.ElementsMatch(t, expectGlobal, globalTokens)
}
// initial sanity check
requireTokenMatch(t, []string{})
t.Run("no tokens", func(t *testing.T) {
n, err := s1.reapExpiredACLTokens(local, global)
require.NoError(t, err)
require.Equal(t, 0, n)
requireTokenMatch(t, []string{})
})
// 2 normal
token1, err := upsertTestToken(codec, "root", "dc1", func(token *structs.ACLToken) {
token.Local = local
})
require.NoError(t, err)
token2, err := upsertTestToken(codec, "root", "dc1", func(token *structs.ACLToken) {
token.Local = local
})
require.NoError(t, err)
requireTokenMatch(t, []string{
token1.AccessorID,
token2.AccessorID,
})
t.Run("only normal tokens", func(t *testing.T) {
n, err := s1.reapExpiredACLTokens(local, global)
require.NoError(t, err)
require.Equal(t, 0, n)
requireTokenMatch(t, []string{
token1.AccessorID,
token2.AccessorID,
})
})
// 2 expiring
token3, err := upsertTestToken(codec, "root", "dc1", func(token *structs.ACLToken) {
token.ExpirationTTL = 1 * time.Second
token.Local = local
})
require.NoError(t, err)
token4, err := upsertTestToken(codec, "root", "dc1", func(token *structs.ACLToken) {
token.ExpirationTTL = 5 * time.Second
token.Local = local
})
require.NoError(t, err)
// 2 more normal
token5, err := upsertTestToken(codec, "root", "dc1", func(token *structs.ACLToken) {
token.Local = local
})
require.NoError(t, err)
token6, err := upsertTestToken(codec, "root", "dc1", func(token *structs.ACLToken) {
token.Local = local
})
require.NoError(t, err)
requireTokenMatch(t, []string{
token1.AccessorID,
token2.AccessorID,
token3.AccessorID,
token4.AccessorID,
token5.AccessorID,
token6.AccessorID,
})
t.Run("mixed but nothing expired yet", func(t *testing.T) {
n, err := s1.reapExpiredACLTokens(local, global)
require.NoError(t, err)
require.Equal(t, 0, n)
requireTokenMatch(t, []string{
token1.AccessorID,
token2.AccessorID,
token3.AccessorID,
token4.AccessorID,
token5.AccessorID,
token6.AccessorID,
})
})
time.Sleep(token3.ExpirationTime.Sub(time.Now()) + 10*time.Millisecond)
t.Run("one should be reaped", func(t *testing.T) {
n, err := s1.reapExpiredACLTokens(local, global)
require.NoError(t, err)
require.Equal(t, 1, n)
requireTokenMatch(t, []string{
token1.AccessorID,
token2.AccessorID,
// token3.AccessorID,
token4.AccessorID,
token5.AccessorID,
token6.AccessorID,
})
})
time.Sleep(token4.ExpirationTime.Sub(time.Now()) + 10*time.Millisecond)
t.Run("two should be reaped", func(t *testing.T) {
n, err := s1.reapExpiredACLTokens(local, global)
require.NoError(t, err)
require.Equal(t, 1, n)
requireTokenMatch(t, []string{
token1.AccessorID,
token2.AccessorID,
// token3.AccessorID,
// token4.AccessorID,
token5.AccessorID,
token6.AccessorID,
})
})
}

View File

@ -0,0 +1,112 @@
package authmethod
import (
"fmt"
"sort"
"sync"
"github.com/hashicorp/consul/agent/structs"
"github.com/mitchellh/mapstructure"
)
type ValidatorFactory func(method *structs.ACLAuthMethod) (Validator, error)
type Validator interface {
// Name returns the name of the auth method backing this validator.
Name() string
// ValidateLogin takes raw user-provided auth method metadata and ensures
// it is sane, provably correct, and currently valid. Relevant identifying
// data is extracted and returned for immediate use by the role binding
// process.
//
// Depending upon the method, it may make sense to use these calls to
// continue to extend the life of the underlying token.
//
// Returns auth method specific metadata suitable for the Role Binding
// process.
ValidateLogin(loginToken string) (map[string]string, error)
// AvailableFields returns a slice of all fields that are returned as a
// result of ValidateLogin. These are valid fields for use in any
// BindingRule tied to this auth method.
AvailableFields() []string
// MakeFieldMapSelectable converts a field map as returned by ValidateLogin
// into a structure suitable for selection with a binding rule.
MakeFieldMapSelectable(fieldMap map[string]string) interface{}
}
var (
typesMu sync.RWMutex
types = make(map[string]ValidatorFactory)
)
// Register makes an auth method with the given type available for use. If
// Register is called twice with the same name or if validator is nil, it
// panics.
func Register(name string, factory ValidatorFactory) {
typesMu.Lock()
defer typesMu.Unlock()
if factory == nil {
panic("authmethod: Register factory is nil for type " + name)
}
if _, dup := types[name]; dup {
panic("authmethod: Register called twice for type " + name)
}
types[name] = factory
}
func IsRegisteredType(typeName string) bool {
typesMu.RLock()
_, ok := types[typeName]
typesMu.RUnlock()
return ok
}
// NewValidator instantiates a new Validator for the given auth method
// configuration. If no auth method is registered with the provided type an
// error is returned.
func NewValidator(method *structs.ACLAuthMethod) (Validator, error) {
typesMu.RLock()
factory, ok := types[method.Type]
typesMu.RUnlock()
if !ok {
return nil, fmt.Errorf("no auth method registered with type: %s", method.Type)
}
return factory(method)
}
// Types returns a sorted list of the names of the registered types.
func Types() []string {
typesMu.RLock()
defer typesMu.RUnlock()
var list []string
for name := range types {
list = append(list, name)
}
sort.Strings(list)
return list
}
// ParseConfig parses the config block for a auth method.
func ParseConfig(rawConfig map[string]interface{}, out interface{}) error {
decodeConf := &mapstructure.DecoderConfig{
Result: out,
WeaklyTypedInput: true,
ErrorUnused: true,
}
decoder, err := mapstructure.NewDecoder(decodeConf)
if err != nil {
return err
}
if err := decoder.Decode(rawConfig); err != nil {
return fmt.Errorf("error decoding config: %s", err)
}
return nil
}

View File

@ -0,0 +1,202 @@
package kubeauth
import (
"errors"
"fmt"
"strings"
"github.com/hashicorp/consul/agent/consul/authmethod"
"github.com/hashicorp/consul/agent/structs"
cleanhttp "github.com/hashicorp/go-cleanhttp"
"gopkg.in/square/go-jose.v2/jwt"
authv1 "k8s.io/api/authentication/v1"
client_metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
k8s "k8s.io/client-go/kubernetes"
client_authv1 "k8s.io/client-go/kubernetes/typed/authentication/v1"
client_corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
client_rest "k8s.io/client-go/rest"
cert "k8s.io/client-go/util/cert"
)
func init() {
// register this as an available auth method type
authmethod.Register("kubernetes", func(method *structs.ACLAuthMethod) (authmethod.Validator, error) {
v, err := NewValidator(method)
if err != nil {
return nil, err
}
return v, nil
})
}
const (
serviceAccountNamespaceField = "serviceaccount.namespace"
serviceAccountNameField = "serviceaccount.name"
serviceAccountUIDField = "serviceaccount.uid"
serviceAccountServiceNameAnnotation = "consul.hashicorp.com/service-name"
)
type Config struct {
// Host must be a host string, a host:port pair, or a URL to the base of
// the Kubernetes API server.
Host string `json:",omitempty"`
// PEM encoded CA cert for use by the TLS client used to talk with the
// Kubernetes API. Every line must end with a newline: \n
CACert string `json:",omitempty"`
// A service account JWT used to access the TokenReview API to validate
// other JWTs during login. It also must be able to read ServiceAccount
// annotations.
ServiceAccountJWT string `json:",omitempty"`
}
// Validator is the wrapper around the relevant portions of the Kubernetes API
// that also conforms to the authmethod.Validator interface.
type Validator struct {
name string
config *Config
saGetter client_corev1.ServiceAccountsGetter
trGetter client_authv1.TokenReviewsGetter
}
func NewValidator(method *structs.ACLAuthMethod) (*Validator, error) {
if method.Type != "kubernetes" {
return nil, fmt.Errorf("%q is not a kubernetes auth method", method.Name)
}
var config Config
if err := authmethod.ParseConfig(method.Config, &config); err != nil {
return nil, err
}
if config.Host == "" {
return nil, fmt.Errorf("Config.Host is required")
}
if config.CACert == "" {
return nil, fmt.Errorf("Config.CACert is required")
}
if _, err := cert.ParseCertsPEM([]byte(config.CACert)); err != nil {
return nil, fmt.Errorf("error parsing kubernetes ca cert: %v", err)
}
// This is the bearer token we give the apiserver to use the API.
if config.ServiceAccountJWT == "" {
return nil, fmt.Errorf("Config.ServiceAccountJWT is required")
}
if _, err := jwt.ParseSigned(config.ServiceAccountJWT); err != nil {
return nil, fmt.Errorf("Config.ServiceAccountJWT is not a valid JWT: %v", err)
}
transport := cleanhttp.DefaultTransport()
client, err := k8s.NewForConfig(&client_rest.Config{
Host: config.Host,
BearerToken: config.ServiceAccountJWT,
Dial: transport.DialContext,
TLSClientConfig: client_rest.TLSClientConfig{
CAData: []byte(config.CACert),
},
ContentConfig: client_rest.ContentConfig{
ContentType: "application/json",
},
})
if err != nil {
return nil, err
}
return &Validator{
name: method.Name,
config: &config,
saGetter: client.CoreV1(),
trGetter: client.AuthenticationV1(),
}, nil
}
func (v *Validator) Name() string { return v.name }
func (v *Validator) ValidateLogin(loginToken string) (map[string]string, error) {
if _, err := jwt.ParseSigned(loginToken); err != nil {
return nil, fmt.Errorf("failed to parse and validate JWT: %v", err)
}
// Check TokenReview for the bulk of the work.
trResp, err := v.trGetter.TokenReviews().Create(&authv1.TokenReview{
Spec: authv1.TokenReviewSpec{
Token: loginToken,
},
})
if err != nil {
return nil, err
} else if trResp.Status.Error != "" {
return nil, fmt.Errorf("lookup failed: %s", trResp.Status.Error)
}
if !trResp.Status.Authenticated {
return nil, errors.New("lookup failed: service account jwt not valid")
}
// The username is of format: system:serviceaccount:(NAMESPACE):(SERVICEACCOUNT)
parts := strings.Split(trResp.Status.User.Username, ":")
if len(parts) != 4 {
return nil, errors.New("lookup failed: unexpected username format")
}
// Validate the user that comes back from token review is a service account
if parts[0] != "system" || parts[1] != "serviceaccount" {
return nil, errors.New("lookup failed: username returned is not a service account")
}
var (
saNamespace = parts[2]
saName = parts[3]
saUID = string(trResp.Status.User.UID)
)
// Check to see if there is an override name on the ServiceAccount object.
sa, err := v.saGetter.ServiceAccounts(saNamespace).Get(saName, client_metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("annotation lookup failed: %v", err)
}
annotations := sa.GetObjectMeta().GetAnnotations()
if serviceNameOverride, ok := annotations[serviceAccountServiceNameAnnotation]; ok {
saName = serviceNameOverride
}
return map[string]string{
serviceAccountNamespaceField: saNamespace,
serviceAccountNameField: saName,
serviceAccountUIDField: saUID,
}, nil
}
func (p *Validator) AvailableFields() []string {
return []string{
serviceAccountNamespaceField,
serviceAccountNameField,
serviceAccountUIDField,
}
}
func (v *Validator) MakeFieldMapSelectable(fieldMap map[string]string) interface{} {
return &k8sFieldDetails{
ServiceAccount: k8sFieldDetailsServiceAccount{
Namespace: fieldMap[serviceAccountNamespaceField],
Name: fieldMap[serviceAccountNameField],
UID: fieldMap[serviceAccountUIDField],
},
}
}
type k8sFieldDetails struct {
ServiceAccount k8sFieldDetailsServiceAccount `bexpr:"serviceaccount"`
}
type k8sFieldDetailsServiceAccount struct {
Namespace string `bexpr:"namespace"`
Name string `bexpr:"name"`
UID string `bexpr:"uid"`
}

View File

@ -0,0 +1,144 @@
package kubeauth
import (
"testing"
"github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/agent/structs"
"github.com/stretchr/testify/require"
)
func TestValidateLogin(t *testing.T) {
testSrv := StartTestAPIServer(t)
defer testSrv.Stop()
testSrv.AuthorizeJWT(goodJWT_A)
testSrv.SetAllowedServiceAccount(
"default",
"demo",
"76091af4-4b56-11e9-ac4b-708b11801cbe",
"",
goodJWT_B,
)
method := &structs.ACLAuthMethod{
Name: "test-k8s",
Description: "k8s test",
Type: "kubernetes",
Config: map[string]interface{}{
"Host": testSrv.Addr(),
"CACert": testSrv.CACert(),
"ServiceAccountJWT": goodJWT_A,
},
}
validator, err := NewValidator(method)
require.NoError(t, err)
t.Run("invalid bearer token", func(t *testing.T) {
_, err := validator.ValidateLogin("invalid")
require.Error(t, err)
})
t.Run("valid bearer token", func(t *testing.T) {
fields, err := validator.ValidateLogin(goodJWT_B)
require.NoError(t, err)
require.Equal(t, map[string]string{
"serviceaccount.namespace": "default",
"serviceaccount.name": "demo",
"serviceaccount.uid": "76091af4-4b56-11e9-ac4b-708b11801cbe",
}, fields)
})
// annotate the account
testSrv.SetAllowedServiceAccount(
"default",
"demo",
"76091af4-4b56-11e9-ac4b-708b11801cbe",
"alternate-name",
goodJWT_B,
)
t.Run("valid bearer token with annotation", func(t *testing.T) {
fields, err := validator.ValidateLogin(goodJWT_B)
require.NoError(t, err)
require.Equal(t, map[string]string{
"serviceaccount.namespace": "default",
"serviceaccount.name": "alternate-name",
"serviceaccount.uid": "76091af4-4b56-11e9-ac4b-708b11801cbe",
}, fields)
})
}
func TestNewValidator(t *testing.T) {
ca := connect.TestCA(t, nil)
type AM = *structs.ACLAuthMethod
makeAuthMethod := func(f func(method AM)) *structs.ACLAuthMethod {
method := &structs.ACLAuthMethod{
Name: "test-k8s",
Description: "k8s test",
Type: "kubernetes",
Config: map[string]interface{}{
"Host": "https://abc:8443",
"CACert": ca.RootCert,
"ServiceAccountJWT": goodJWT_A,
},
}
if f != nil {
f(method)
}
return method
}
for _, test := range []struct {
name string
method *structs.ACLAuthMethod
ok bool
}{
// bad
{"wrong type", makeAuthMethod(func(method AM) {
method.Type = "invalid"
}), false},
{"extra config", makeAuthMethod(func(method AM) {
method.Config["extra"] = "config"
}), false},
{"wrong type of config", makeAuthMethod(func(method AM) {
method.Config["Host"] = []int{12345}
}), false},
{"missing host", makeAuthMethod(func(method AM) {
delete(method.Config, "Host")
}), false},
{"missing ca cert", makeAuthMethod(func(method AM) {
delete(method.Config, "CACert")
}), false},
{"invalid ca cert", makeAuthMethod(func(method AM) {
method.Config["CACert"] = "invalid"
}), false},
{"invalid jwt", makeAuthMethod(func(method AM) {
method.Config["ServiceAccountJWT"] = "invalid"
}), false},
{"garbage host", makeAuthMethod(func(method AM) {
method.Config["Host"] = "://:12345"
}), false},
// good
{"normal", makeAuthMethod(nil), true},
} {
t.Run(test.name, func(t *testing.T) {
v, err := NewValidator(test.method)
if test.ok {
require.NoError(t, err)
require.NotNil(t, v)
} else {
require.NotNil(t, err)
require.Nil(t, v)
}
})
}
}
// 'default/admin'
const goodJWT_A = "eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImFkbWluLXRva2VuLXFsejQyIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImFkbWluIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiNzM4YmMyNTEtNjUzMi0xMWU5LWI2N2YtNDhlNmM4YjhlY2I1Iiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmRlZmF1bHQ6YWRtaW4ifQ.ixMlnWrAG7NVuTTKu8cdcYfM7gweS3jlKaEsIBNGOVEjPE7rtXtgMkAwjQTdYR08_0QBjkgzy5fQC5ZNyglSwONJ-bPaXGvhoH1cTnRi1dz9H_63CfqOCvQP1sbdkMeRxNTGVAyWZT76rXoCUIfHP4LY2I8aab0KN9FTIcgZRF0XPTtT70UwGIrSmRpxW38zjiy2ymWL01cc5VWGhJqVysmWmYk3wNp0h5N57H_MOrz4apQR4pKaamzskzjLxO55gpbmZFC76qWuUdexAR7DT2fpbHLOw90atN_NlLMY-VrXyW3-Ei5EhYaVreMB9PSpKwkrA4jULITohV-sxpa1LA"
// 'default/demo'
const goodJWT_B = "eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImRlbW8tdG9rZW4ta21iOW4iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZGVtbyIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6Ijc2MDkxYWY0LTRiNTYtMTFlOS1hYzRiLTcwOGIxMTgwMWNiZSIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmRlbW8ifQ.ZiAHjijBAOsKdum0Aix6lgtkLkGo9_Tu87dWQ5Zfwnn3r2FejEWDAnftTft1MqqnMzivZ9Wyyki5ZjQRmTAtnMPJuHC-iivqY4Wh4S6QWCJ1SivBv5tMZR79t5t8mE7R1-OHwst46spru1pps9wt9jsA04d3LpV0eeKYgdPTVaQKklxTm397kIMUugA6yINIBQ3Rh8eQqBgNwEmL4iqyYubzHLVkGkoP9MJikFI05vfRiHtYr-piXz6JFDzXMQj9rW6xtMmrBSn79ChbyvC5nz-Nj2rJPnHsb_0rDUbmXY5PpnMhBpdSH-CbZ4j8jsiib6DtaGJhVZeEQ1GjsFAZwQ"

View File

@ -0,0 +1,532 @@
package kubeauth
import (
"bytes"
"encoding/json"
"encoding/pem"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"regexp"
"strings"
"sync"
"testing"
"time"
"github.com/stretchr/testify/require"
authv1 "k8s.io/api/authentication/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)
// TestAPIServer is a way to mock the Kubernetes API server as it is used by
// the consul kubernetes auth method.
//
// - POST /apis/authentication.k8s.io/v1/tokenreviews
// - GET /api/v1/namespaces/<NAMESPACE>/serviceaccounts/<NAME>
//
type TestAPIServer struct {
t *testing.T
srv *httptest.Server
caCert string
mu sync.Mutex
authorizedJWT string // token review and sa read
allowedServiceAccountJWT string // general service account
replyStatus *authv1.TokenReview // general service account
replyRead *corev1.ServiceAccount // general service account
}
// StartTestAPIServer creates a disposable TestAPIServer and binds it to a
// random free port.
func StartTestAPIServer(t *testing.T) *TestAPIServer {
s := &TestAPIServer{t: t}
s.srv = httptest.NewTLSServer(s)
bs := s.srv.TLS.Certificates[0].Certificate[0]
var buf bytes.Buffer
require.NoError(t, pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: bs}))
s.caCert = buf.String()
return s
}
// AuthorizeJWT whitelists the given JWT as able to use the API server.
func (s *TestAPIServer) AuthorizeJWT(jwt string) {
s.mu.Lock()
defer s.mu.Unlock()
s.authorizedJWT = jwt
}
// SetAllowedServiceAccount configures the singular known Service Account
// installed in this API server. If any of namespace/name/uid/jwt are empty
// it removes anything previously configured.
//
// It is up to the caller to ensure that the provided JWT matches the other
// data.
func (s *TestAPIServer) SetAllowedServiceAccount(
namespace, name, uid, overrideAnnotation, jwt string,
) {
s.mu.Lock()
defer s.mu.Unlock()
if namespace == "" || name == "" || uid == "" || jwt == "" {
s.allowedServiceAccountJWT = ""
s.replyStatus = nil
s.replyRead = nil
return
}
s.allowedServiceAccountJWT = jwt
s.replyRead = createReadServiceAccountFound(namespace, name, uid, overrideAnnotation, jwt)
s.replyStatus = createTokenReviewFound(namespace, name, uid, jwt)
}
// Stop stops the running TestAPIServer.
func (s *TestAPIServer) Stop() {
s.srv.Close()
}
// Addr returns the current base URL for the running webserver.
func (s *TestAPIServer) Addr() string { return s.srv.URL }
// CACert returns the pem-encoded CA certificate used by the HTTPS server.
func (s *TestAPIServer) CACert() string { return s.caCert }
var readServiceAccountPathRE = regexp.MustCompile("^/api/v1/namespaces/([^/]+)/serviceaccounts/([^/]+)$")
func (s *TestAPIServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
s.mu.Lock()
defer s.mu.Unlock()
w.Header().Set("content-type", "application/json")
if req.URL.Path == "/apis/authentication.k8s.io/v1/tokenreviews" {
s.handleTokenReview(w, req)
return
}
if m := readServiceAccountPathRE.FindStringSubmatch(req.URL.Path); m != nil {
namespace, err := url.QueryUnescape(m[1])
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
name, err := url.QueryUnescape(m[2])
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
s.handleReadServiceAccount(namespace, name, w, req)
return
}
w.WriteHeader(http.StatusNotFound)
}
func writeJSON(w http.ResponseWriter, out interface{}) error {
enc := json.NewEncoder(w)
return enc.Encode(out)
}
func (s *TestAPIServer) handleTokenReview(w http.ResponseWriter, req *http.Request) {
if req.Method != "POST" {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
if auth, anon := s.isAuthenticated(req); !auth {
var out interface{}
if anon {
out = createTokenReviewForbidden_NoAuthz()
} else {
out = createTokenReviewForbidden("default", "fake-account")
}
w.WriteHeader(http.StatusForbidden)
if err := writeJSON(w, out); err != nil {
w.WriteHeader(http.StatusInternalServerError)
}
return
}
if req.Body == nil {
w.WriteHeader(http.StatusBadRequest)
return
}
defer req.Body.Close()
b, err := ioutil.ReadAll(req.Body)
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
var trReq authv1.TokenReview
if err := json.Unmarshal(b, &trReq); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
reviewingJWT := trReq.Spec.Token
var out interface{}
if s.replyStatus == nil || reviewingJWT != s.allowedServiceAccountJWT {
out = createTokenReviewNotFound(reviewingJWT)
} else {
out = s.replyStatus
}
w.WriteHeader(http.StatusCreated)
if err := writeJSON(w, out); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
}
func (s *TestAPIServer) handleReadServiceAccount(
namespace, name string,
w http.ResponseWriter,
req *http.Request,
) {
if req.Method != "GET" {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
var out interface{}
if auth, anon := s.isAuthenticated(req); !auth {
if anon {
out = createReadServiceAccountForbidden_NoAuthz()
} else {
out = createReadServiceAccountForbidden(namespace, name)
}
w.WriteHeader(http.StatusForbidden)
} else if s.replyRead == nil {
out = createReadServiceAccountNotFound(namespace, name)
w.WriteHeader(http.StatusNotFound)
} else if s.replyRead.Namespace != namespace || s.replyRead.Name != name {
out = createReadServiceAccountNotFound(namespace, name)
w.WriteHeader(http.StatusNotFound)
} else {
out = s.replyRead
w.WriteHeader(http.StatusOK)
}
if err := writeJSON(w, out); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
}
func (s *TestAPIServer) isAuthenticated(req *http.Request) (auth, anonymous bool) {
authz := req.Header.Get("Authorization")
if !strings.HasPrefix(authz, "Bearer ") {
return false, true
}
jwt := strings.TrimPrefix(authz, "Bearer ")
return s.authorizedJWT == jwt, false
}
func createTokenReviewForbidden_NoAuthz() *metav1.Status {
/*
STATUS: 403
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "tokenreviews.authentication.k8s.io is forbidden: User \"system:anonymous\" cannot create resource \"tokenreviews\" in API group \"authentication.k8s.io\" at the cluster scope",
"reason": "Forbidden",
"details": {
"group": "authentication.k8s.io",
"kind": "tokenreviews"
},
"code": 403
}
*/
return createStatus(
metav1.StatusFailure,
"tokenreviews.authentication.k8s.io is forbidden: User \"system:anonymous\" cannot create resource \"tokenreviews\" in API group \"authentication.k8s.io\" in the cluster scope",
metav1.StatusReasonForbidden,
&metav1.StatusDetails{
Group: "authentication.k8s.io",
Kind: "tokenreviews",
},
403,
)
}
func createTokenReviewForbidden(namespace, name string) *metav1.Status {
/*
STATUS: 403
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "tokenreviews.authentication.k8s.io is forbidden: User \"system:serviceaccount:default:admin\" cannot create resource \"tokenreviews\" in API group \"authentication.k8s.io\" at the cluster scope",
"reason": "Forbidden",
"details": {
"group": "authentication.k8s.io",
"kind": "tokenreviews"
},
"code": 403
}
*/
return createStatus(
metav1.StatusFailure,
"tokenreviews.authentication.k8s.io is forbidden: User \"system:serviceaccount:"+namespace+":"+name+"\" cannot create resource \"tokenreviews\" in API group \"authentication.k8s.io\" in the cluster scope",
metav1.StatusReasonForbidden,
&metav1.StatusDetails{
Group: "authentication.k8s.io",
Kind: "tokenreviews",
},
403,
)
}
func createTokenReviewNotFound(jwt string) *authv1.TokenReview {
/*
STATUS: 201
{
"kind": "TokenReview",
"apiVersion": "authentication.k8s.io/v1",
"metadata": {
"creationTimestamp": null
},
"spec": {
"token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImZha2UtdG9rZW4tano2YnYiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZmFrZSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjgxYTY1Mjg2LTU3YzEtMTFlOS1iYzJhLTQ4ZTZjOGI4ZWNiNSIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmZha2UifQ.DqjUXe34SzCP4NCwbhqV9EuksfzmTSLhJzkE_URyufeGJDn-Gw0_JS-_KmxZSdAO0XXNzB1tJNM1NCVW-V6YbThnPUw5WY4V2J6U1W72c2dzNBx_ipBxGBZ632ZnpViIRu6tL2guT36lWa8YnMDF_OY8sHhl_3kJ6MRxNxY41vAuf45mohi3gri46Kpzc3pf1g6PJ-0oogvUsZ2nBFv1mIdciGBV0zejMKc5Bnxur1L-hEQ9EgZrJ7o0yQRCWYgam_yo_M38EsB8b-suTzQJMA-pRgApOb9dHIV6YAE_b3g_pGkJjrPYzV4IJC1CiPfdz1SAjm7e0ARXtZmaoPltjQ"
},
"status": {
"user": {},
"error": "[invalid bearer token, Token has been invalidated]"
}
}
*/
return &authv1.TokenReview{
TypeMeta: metav1.TypeMeta{
Kind: "TokenReview",
APIVersion: "authentication.k8s.io/v1",
},
ObjectMeta: metav1.ObjectMeta{},
Spec: authv1.TokenReviewSpec{
Token: jwt,
},
Status: authv1.TokenReviewStatus{
User: authv1.UserInfo{},
Error: "[invalid bearer token, Token has been invalidated]",
},
}
}
func createTokenReviewFound(namespace, name, uid, jwt string) *authv1.TokenReview {
/*
STATUS: 201
{
"kind": "TokenReview",
"apiVersion": "authentication.k8s.io/v1",
"metadata": {
"creationTimestamp": null
},
"spec": {
"token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImRlbW8tdG9rZW4tbTljdm4iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZGVtbyIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjlmZjUxZmY0LTU1N2UtMTFlOS05Njg3LTQ4ZTZjOGI4ZWNiNSIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmRlbW8ifQ.UJEphtrN261gy9WCl4ZKjm2PRDLDkc3Xg9VcDGfzyroOqFQ6sog5dVAb9voc5Nc0-H5b1yGwxDViEMucwKvZpA5pi7VEx_OskK-KTWXSmafM0Xg_AvzpU9Ed5TSRno-OhXaAraxdjXoC4myh1ay2DMeHUusJg_ibqcYJrWx-6MO1bH_ObORtAKhoST_8fzkqNAlZmsQ87FinQvYN5mzDXYukl-eeRdBgQUBkWvEb-Ju6cc0-QE4sUQ4IH_fs0fUyX_xc0om0SZGWLP909FTz4V8LxV8kr6L7irxROiS1jn3Fvyc9ur1PamVf3JOPPrOyfmKbaGRiWJM32b3buQw7cg"
},
"status": {
"authenticated": true,
"user": {
"username": "system:serviceaccount:default:demo",
"uid": "9ff51ff4-557e-11e9-9687-48e6c8b8ecb5",
"groups": [
"system:serviceaccounts",
"system:serviceaccounts:default",
"system:authenticated"
]
}
}
}
*/
return &authv1.TokenReview{
TypeMeta: metav1.TypeMeta{
Kind: "TokenReview",
APIVersion: "authentication.k8s.io/v1",
},
ObjectMeta: metav1.ObjectMeta{},
Spec: authv1.TokenReviewSpec{
Token: jwt,
},
Status: authv1.TokenReviewStatus{
Authenticated: true,
User: authv1.UserInfo{
Username: "system:serviceaccount:" + namespace + ":" + name,
UID: uid,
Groups: []string{
"system:serviceaccounts",
"system:serviceaccounts:default",
"system:authenticated",
},
},
},
}
}
func createReadServiceAccountForbidden(namespace, name string) *metav1.Status {
/*
STATUS: 403
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "serviceaccounts \"demo\" is forbidden: User \"system:serviceaccount:default:admin\" cannot get resource \"serviceaccounts\" in API group \"\" in the namespace \"default\"",
"reason": "Forbidden",
"details": {
"name": "demo",
"kind": "serviceaccounts"
},
"code": 403
}
*/
return createStatus(
metav1.StatusFailure,
"serviceaccounts \""+name+"\" is forbidden: User \"system:serviceaccount:"+namespace+":"+name+"\" cannot get resource \"serviceaccounts\" in API group \"\" in the namespace \""+namespace+"\"",
metav1.StatusReasonForbidden,
&metav1.StatusDetails{
Kind: "serviceaccounts",
Name: name,
},
403,
)
}
func createReadServiceAccountForbidden_NoAuthz() *metav1.Status {
// missing bearer token header 403
/*
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "serviceaccounts \"demo\" is forbidden: User \"system:anonymous\" cannot get resource \"serviceaccounts\" in API group \"\" in the namespace \"default\"",
"reason": "Forbidden",
"details": {
"name": "demo",
"kind": "serviceaccounts"
},
"code": 403
}
*/
return createStatus(
metav1.StatusFailure,
"serviceaccounts \"PLACEHOLDER\" is forbidden: User \"system:anonymous\" cannot get resource \"serviceaccounts\" in API group \"\" in the namespace \"default\"",
metav1.StatusReasonForbidden,
&metav1.StatusDetails{
Kind: "serviceaccounts",
Name: "PLACEHOLDER",
},
403,
)
}
func createReadServiceAccountNotFound(namespace, name string) *metav1.Status {
/*
STATUS: 404
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "serviceaccounts \"demo\" not found",
"reason": "NotFound",
"details": {
"name": "demo",
"kind": "serviceaccounts"
},
"code": 404
}
*/
return createStatus(
metav1.StatusFailure,
"serviceaccounts \""+name+"\" not found",
metav1.StatusReasonNotFound,
&metav1.StatusDetails{
Kind: "serviceaccounts",
Name: name,
},
404,
)
}
func createReadServiceAccountFound(namespace, name, uid, overrideAnnotation, jwt string) *corev1.ServiceAccount {
/*
STATUS: 200
{
"kind": "ServiceAccount",
"apiVersion": "v1",
"metadata": {
"name": "demo",
"namespace": "default",
"selfLink": "/api/v1/namespaces/default/serviceaccounts/demo",
"uid": "9ff51ff4-557e-11e9-9687-48e6c8b8ecb5",
"resourceVersion": "2101",
"creationTimestamp": "2019-04-02T19:36:34Z",
"annotations": {
"consul.hashicorp.com/service-name": "actual",
"kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"ServiceAccount\",\"metadata\":{\"annotations\":{\"consul.hashicorp.com/service-name\":\"actual\"},\"name\":\"demo\",\"namespace\":\"default\"}}\n"
}
},
"secrets": [
{
"name": "demo-token-m9cvn"
}
]
}
*/
sa := &corev1.ServiceAccount{
TypeMeta: metav1.TypeMeta{
Kind: "ServiceAccount",
APIVersion: "v1",
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
SelfLink: "/api/v1/namespaces/" + namespace + "/serviceaccounts/" + name,
UID: types.UID(uid),
ResourceVersion: "123",
CreationTimestamp: metav1.Time{Time: time.Now()},
},
Secrets: []corev1.ObjectReference{
corev1.ObjectReference{
Name: name + "-token-m9cvn",
},
},
}
if overrideAnnotation != "" {
sa.ObjectMeta.Annotations = map[string]string{
"consul.hashicorp.com/service-name": overrideAnnotation,
}
}
return sa
}
func createStatus(status, message string, reason metav1.StatusReason, details *metav1.StatusDetails, code int32) *metav1.Status {
return &metav1.Status{
TypeMeta: metav1.TypeMeta{
Kind: "Status",
APIVersion: "v1",
},
ListMeta: metav1.ListMeta{},
Status: status,
Message: message,
Reason: reason,
Details: details,
Code: code,
}
}

View File

@ -0,0 +1,166 @@
package testauth
import (
"fmt"
"sync"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/consul/authmethod"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/go-uuid"
)
func init() {
authmethod.Register("testing", newValidator)
}
var (
tokenDatabaseMu sync.Mutex
tokenDatabase map[string]map[string]map[string]string // session => token => fieldmap
)
func StartSession() string {
sessionID, err := uuid.GenerateUUID()
if err != nil {
panic(err)
}
return sessionID
}
func ResetSession(sessionID string) {
tokenDatabaseMu.Lock()
defer tokenDatabaseMu.Unlock()
if tokenDatabase != nil {
delete(tokenDatabase, sessionID)
}
}
func InstallSessionToken(sessionID string, token string, namespace, name, uid string) {
fields := map[string]string{
serviceAccountNamespaceField: namespace,
serviceAccountNameField: name,
serviceAccountUIDField: uid,
}
tokenDatabaseMu.Lock()
defer tokenDatabaseMu.Unlock()
if tokenDatabase == nil {
tokenDatabase = make(map[string]map[string]map[string]string)
}
sdb, ok := tokenDatabase[sessionID]
if !ok {
sdb = make(map[string]map[string]string)
tokenDatabase[sessionID] = sdb
}
sdb[token] = fields
}
func GetSessionToken(sessionID string, token string) (map[string]string, bool) {
tokenDatabaseMu.Lock()
defer tokenDatabaseMu.Unlock()
if tokenDatabase == nil {
return nil, false
}
sdb, ok := tokenDatabase[sessionID]
if !ok {
return nil, false
}
fields, ok := sdb[token]
if !ok {
return nil, false
}
fmCopy := make(map[string]string)
for k, v := range fields {
fmCopy[k] = v
}
return fmCopy, true
}
type Config struct {
SessionID string // unique identifier for this set of tokens in the database
}
func newValidator(method *structs.ACLAuthMethod) (authmethod.Validator, error) {
if method.Type != "testing" {
return nil, fmt.Errorf("%q is not a testing auth method", method.Name)
}
var config Config
if err := authmethod.ParseConfig(method.Config, &config); err != nil {
return nil, err
}
if config.SessionID == "" {
// If you don't explicitly create one, we create a random one but you
// won't have access to it. Useful if you are testing everything EXCEPT
// ValidateToken().
config.SessionID = StartSession()
}
return &Validator{
name: method.Name,
config: &config,
}, nil
}
type Validator struct {
name string
config *Config
}
func (v *Validator) Name() string { return v.name }
// ValidateLogin takes raw user-provided auth method metadata and ensures it is
// sane, provably correct, and currently valid. Relevant identifying data is
// extracted and returned for immediate use by the role binding process.
//
// Depending upon the method, it may make sense to use these calls to continue
// to extend the life of the underlying token.
//
// Returns auth method specific metadata suitable for the Role Binding process.
func (v *Validator) ValidateLogin(loginToken string) (map[string]string, error) {
fields, valid := GetSessionToken(v.config.SessionID, loginToken)
if !valid {
return nil, acl.ErrNotFound
}
return fields, nil
}
func (v *Validator) AvailableFields() []string { return availableFields }
const (
serviceAccountNamespaceField = "serviceaccount.namespace"
serviceAccountNameField = "serviceaccount.name"
serviceAccountUIDField = "serviceaccount.uid"
)
var availableFields = []string{
serviceAccountNamespaceField,
serviceAccountNameField,
serviceAccountUIDField,
}
// MakeFieldMapSelectable converts a field map as returned by ValidateLogin
// into a structure suitable for selection with a binding rule.
func (v *Validator) MakeFieldMapSelectable(fieldMap map[string]string) interface{} {
return &selectableVars{
ServiceAccount: selectableServiceAccount{
Namespace: fieldMap[serviceAccountNamespaceField],
Name: fieldMap[serviceAccountNameField],
UID: fieldMap[serviceAccountUIDField],
},
}
}
type selectableVars struct {
ServiceAccount selectableServiceAccount `bexpr:"serviceaccount"`
}
type selectableServiceAccount struct {
Namespace string `bexpr:"namespace"`
Name string `bexpr:"name"`
UID string `bexpr:"uid"`
}

View File

@ -243,6 +243,11 @@ type Config struct {
// a substantial cost.
ACLPolicyTTL time.Duration
// ACLRoleTTL controls the time-to-live of cached ACL roles.
// It can be set to zero to disable caching, but this adds
// a substantial cost.
ACLRoleTTL time.Duration
// ACLDisabledTTL is the time between checking if ACLs should be
// enabled. This
ACLDisabledTTL time.Duration
@ -313,6 +318,16 @@ type Config struct {
// Minimum Session TTL
SessionTTLMin time.Duration
// maxTokenExpirationDuration is the maximum difference allowed between
// ACLToken CreateTime and ExpirationTime values if ExpirationTime is set
// on a token.
ACLTokenMaxExpirationTTL time.Duration
// ACLTokenMinExpirationTTL is the minimum difference allowed between
// ACLToken CreateTime and ExpirationTime values if ExpirationTime is set
// on a token.
ACLTokenMinExpirationTTL time.Duration
// ServerUp callback can be used to trigger a notification that
// a Consul server is now up and known about.
ServerUp func()
@ -460,6 +475,7 @@ func DefaultConfig() *Config {
SerfFloodInterval: 60 * time.Second,
ReconcileInterval: 60 * time.Second,
ProtocolVersion: ProtocolVersion2Compatible,
ACLRoleTTL: 30 * time.Second,
ACLPolicyTTL: 30 * time.Second,
ACLTokenTTL: 30 * time.Second,
ACLDefaultPolicy: "allow",
@ -473,6 +489,8 @@ func DefaultConfig() *Config {
TombstoneTTL: 15 * time.Minute,
TombstoneTTLGranularity: 30 * time.Second,
SessionTTLMin: 10 * time.Second,
ACLTokenMinExpirationTTL: 1 * time.Minute,
ACLTokenMaxExpirationTTL: 24 * time.Hour,
// These are tuned to provide a total throughput of 128 updates
// per second. If you update these, you should update the client-

View File

@ -4,7 +4,7 @@ import (
"fmt"
"time"
"github.com/armon/go-metrics"
metrics "github.com/armon/go-metrics"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/api"
)
@ -30,6 +30,12 @@ func init() {
registerCommand(structs.ACLPolicyDeleteRequestType, (*FSM).applyACLPolicyDeleteOperation)
registerCommand(structs.ConnectCALeafRequestType, (*FSM).applyConnectCALeafOperation)
registerCommand(structs.ConfigEntryRequestType, (*FSM).applyConfigEntryOperation)
registerCommand(structs.ACLRoleSetRequestType, (*FSM).applyACLRoleSetOperation)
registerCommand(structs.ACLRoleDeleteRequestType, (*FSM).applyACLRoleDeleteOperation)
registerCommand(structs.ACLBindingRuleSetRequestType, (*FSM).applyACLBindingRuleSetOperation)
registerCommand(structs.ACLBindingRuleDeleteRequestType, (*FSM).applyACLBindingRuleDeleteOperation)
registerCommand(structs.ACLAuthMethodSetRequestType, (*FSM).applyACLAuthMethodSetOperation)
registerCommand(structs.ACLAuthMethodDeleteRequestType, (*FSM).applyACLAuthMethodDeleteOperation)
}
func (c *FSM) applyRegister(buf []byte, index uint64) interface{} {
@ -165,6 +171,7 @@ func (c *FSM) applyACLOperation(buf []byte, index uint64) interface{} {
return err
}
// No need to check expiration times as those did not exist in legacy tokens.
if _, token, err := c.state.ACLTokenGetBySecret(nil, req.ACL.ID); err != nil {
return err
} else {
@ -451,3 +458,69 @@ func (c *FSM) applyConfigEntryOperation(buf []byte, index uint64) interface{} {
return fmt.Errorf("invalid config entry operation type: %v", req.Op)
}
}
func (c *FSM) applyACLRoleSetOperation(buf []byte, index uint64) interface{} {
var req structs.ACLRoleBatchSetRequest
if err := structs.Decode(buf, &req); err != nil {
panic(fmt.Errorf("failed to decode request: %v", err))
}
defer metrics.MeasureSinceWithLabels([]string{"fsm", "acl", "role"}, time.Now(),
[]metrics.Label{{Name: "op", Value: "upsert"}})
return c.state.ACLRoleBatchSet(index, req.Roles)
}
func (c *FSM) applyACLRoleDeleteOperation(buf []byte, index uint64) interface{} {
var req structs.ACLRoleBatchDeleteRequest
if err := structs.Decode(buf, &req); err != nil {
panic(fmt.Errorf("failed to decode request: %v", err))
}
defer metrics.MeasureSinceWithLabels([]string{"fsm", "acl", "role"}, time.Now(),
[]metrics.Label{{Name: "op", Value: "delete"}})
return c.state.ACLRoleBatchDelete(index, req.RoleIDs)
}
func (c *FSM) applyACLBindingRuleSetOperation(buf []byte, index uint64) interface{} {
var req structs.ACLBindingRuleBatchSetRequest
if err := structs.Decode(buf, &req); err != nil {
panic(fmt.Errorf("failed to decode request: %v", err))
}
defer metrics.MeasureSinceWithLabels([]string{"fsm", "acl", "bindingrule"}, time.Now(),
[]metrics.Label{{Name: "op", Value: "upsert"}})
return c.state.ACLBindingRuleBatchSet(index, req.BindingRules)
}
func (c *FSM) applyACLBindingRuleDeleteOperation(buf []byte, index uint64) interface{} {
var req structs.ACLBindingRuleBatchDeleteRequest
if err := structs.Decode(buf, &req); err != nil {
panic(fmt.Errorf("failed to decode request: %v", err))
}
defer metrics.MeasureSinceWithLabels([]string{"fsm", "acl", "bindingrule"}, time.Now(),
[]metrics.Label{{Name: "op", Value: "delete"}})
return c.state.ACLBindingRuleBatchDelete(index, req.BindingRuleIDs)
}
func (c *FSM) applyACLAuthMethodSetOperation(buf []byte, index uint64) interface{} {
var req structs.ACLAuthMethodBatchSetRequest
if err := structs.Decode(buf, &req); err != nil {
panic(fmt.Errorf("failed to decode request: %v", err))
}
defer metrics.MeasureSinceWithLabels([]string{"fsm", "acl", "authmethod"}, time.Now(),
[]metrics.Label{{Name: "op", Value: "upsert"}})
return c.state.ACLAuthMethodBatchSet(index, req.AuthMethods)
}
func (c *FSM) applyACLAuthMethodDeleteOperation(buf []byte, index uint64) interface{} {
var req structs.ACLAuthMethodBatchDeleteRequest
if err := structs.Decode(buf, &req); err != nil {
panic(fmt.Errorf("failed to decode request: %v", err))
}
defer metrics.MeasureSinceWithLabels([]string{"fsm", "acl", "authmethod"}, time.Now(),
[]metrics.Label{{Name: "op", Value: "delete"}})
return c.state.ACLAuthMethodBatchDelete(index, req.AuthMethodNames)
}

View File

@ -28,6 +28,9 @@ func init() {
registerRestorer(structs.ACLTokenSetRequestType, restoreToken)
registerRestorer(structs.ACLPolicySetRequestType, restorePolicy)
registerRestorer(structs.ConfigEntryRequestType, restoreConfigEntry)
registerRestorer(structs.ACLRoleSetRequestType, restoreRole)
registerRestorer(structs.ACLBindingRuleSetRequestType, restoreBindingRule)
registerRestorer(structs.ACLAuthMethodSetRequestType, restoreAuthMethod)
}
func persistOSS(s *snapshot, sink raft.SnapshotSink, encoder *codec.Encoder) error {
@ -178,6 +181,8 @@ func (s *snapshot) persistACLs(sink raft.SnapshotSink,
return err
}
// Don't check expiration times. Wait for explicit deletions.
for token := tokens.Next(); token != nil; token = tokens.Next() {
if _, err := sink.Write([]byte{byte(structs.ACLTokenSetRequestType)}); err != nil {
return err
@ -201,6 +206,48 @@ func (s *snapshot) persistACLs(sink raft.SnapshotSink,
}
}
roles, err := s.state.ACLRoles()
if err != nil {
return err
}
for role := roles.Next(); role != nil; role = roles.Next() {
if _, err := sink.Write([]byte{byte(structs.ACLRoleSetRequestType)}); err != nil {
return err
}
if err := encoder.Encode(role.(*structs.ACLRole)); err != nil {
return err
}
}
rules, err := s.state.ACLBindingRules()
if err != nil {
return err
}
for rule := rules.Next(); rule != nil; rule = rules.Next() {
if _, err := sink.Write([]byte{byte(structs.ACLBindingRuleSetRequestType)}); err != nil {
return err
}
if err := encoder.Encode(rule.(*structs.ACLBindingRule)); err != nil {
return err
}
}
methods, err := s.state.ACLAuthMethods()
if err != nil {
return err
}
for method := methods.Next(); method != nil; method = rules.Next() {
if _, err := sink.Write([]byte{byte(structs.ACLAuthMethodSetRequestType)}); err != nil {
return err
}
if err := encoder.Encode(method.(*structs.ACLAuthMethod)); err != nil {
return err
}
}
return nil
}
@ -601,3 +648,27 @@ func restoreConfigEntry(header *snapshotHeader, restore *state.Restore, decoder
}
return restore.ConfigEntry(req.Entry)
}
func restoreRole(header *snapshotHeader, restore *state.Restore, decoder *codec.Decoder) error {
var req structs.ACLRole
if err := decoder.Decode(&req); err != nil {
return err
}
return restore.ACLRole(&req)
}
func restoreBindingRule(header *snapshotHeader, restore *state.Restore, decoder *codec.Decoder) error {
var req structs.ACLBindingRule
if err := decoder.Decode(&req); err != nil {
return err
}
return restore.ACLBindingRule(&req)
}
func restoreAuthMethod(header *snapshotHeader, restore *state.Restore, decoder *codec.Decoder) error {
var req structs.ACLAuthMethod
if err := decoder.Decode(&req); err != nil {
return err
}
return restore.ACLAuthMethod(&req)
}

View File

@ -86,7 +86,7 @@ func TestFSM_SnapshotRestore_OSS(t *testing.T) {
})
session := &structs.Session{ID: generateUUID(), Node: "foo"}
fsm.state.SessionCreate(9, session)
policy := structs.ACLPolicy{
policy := &structs.ACLPolicy{
ID: structs.ACLPolicyGlobalManagementID,
Name: "global-management",
Description: "Builtin Policy that grants unlimited access",
@ -94,7 +94,20 @@ func TestFSM_SnapshotRestore_OSS(t *testing.T) {
Syntax: acl.SyntaxCurrent,
}
policy.SetHash(true)
require.NoError(fsm.state.ACLPolicySet(1, &policy))
require.NoError(fsm.state.ACLPolicySet(1, policy))
role := &structs.ACLRole{
ID: "86dedd19-8fae-4594-8294-4e6948a81f9a",
Name: "some-role",
Description: "test snapshot role",
ServiceIdentities: []*structs.ACLServiceIdentity{
&structs.ACLServiceIdentity{
ServiceName: "example",
},
},
}
role.SetHash(true)
require.NoError(fsm.state.ACLRoleSet(1, role))
token := &structs.ACLToken{
AccessorID: "30fca056-9fbb-4455-b94a-bf0e2bc575d6",
@ -112,6 +125,26 @@ func TestFSM_SnapshotRestore_OSS(t *testing.T) {
}
require.NoError(fsm.state.ACLBootstrap(10, 0, token, false))
method := &structs.ACLAuthMethod{
Name: "some-method",
Type: "testing",
Description: "test snapshot auth method",
Config: map[string]interface{}{
"SessionID": "952ebfa8-2a42-46f0-bcd3-fd98a842000e",
},
}
require.NoError(fsm.state.ACLAuthMethodSet(1, method))
bindingRule := &structs.ACLBindingRule{
ID: "85184c52-5997-4a84-9817-5945f2632a17",
Description: "test snapshot binding rule",
AuthMethod: "some-method",
Selector: "serviceaccount.namespace==default",
BindType: structs.BindingRuleBindTypeService,
BindName: "${serviceaccount.name}",
}
require.NoError(fsm.state.ACLBindingRuleSet(1, bindingRule))
fsm.state.KVSSet(11, &structs.DirEntry{
Key: "/remove",
Value: []byte("foo"),
@ -314,21 +347,40 @@ func TestFSM_SnapshotRestore_OSS(t *testing.T) {
t.Fatalf("bad index: %d", idx)
}
// Verify ACL Token is restored
_, a, err := fsm2.state.ACLTokenGetByAccessor(nil, token.AccessorID)
// Verify ACL Binding Rule is restored
_, bindingRule2, err := fsm2.state.ACLBindingRuleGetByID(nil, bindingRule.ID)
require.NoError(err)
require.Equal(token.AccessorID, a.AccessorID)
require.Equal(token.ModifyIndex, a.ModifyIndex)
require.Equal(bindingRule, bindingRule2)
// Verify ACL Auth Method is restored
_, method2, err := fsm2.state.ACLAuthMethodGetByName(nil, method.Name)
require.NoError(err)
require.Equal(method, method2)
// Verify ACL Token is restored
_, token2, err := fsm2.state.ACLTokenGetByAccessor(nil, token.AccessorID)
require.NoError(err)
{
// time.Time is tricky to compare generically when it takes a ser/deserialization round trip.
require.True(token.CreateTime.Equal(token2.CreateTime))
token2.CreateTime = token.CreateTime
}
require.Equal(token, token2)
// Verify the acl-token-bootstrap index was restored
canBootstrap, index, err := fsm2.state.CanBootstrapACLToken()
require.False(canBootstrap)
require.True(index > 0)
// Verify ACL Role is restored
_, role2, err := fsm2.state.ACLRoleGetByID(nil, role.ID)
require.NoError(err)
require.Equal(role, role2)
// Verify ACL Policy is restored
_, policy2, err := fsm2.state.ACLPolicyGetByID(nil, structs.ACLPolicyGlobalManagementID)
require.NoError(err)
require.Equal(policy.Name, policy2.Name)
require.Equal(policy, policy2)
// Verify tombstones are restored
func() {

View File

@ -11,7 +11,7 @@ import (
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/sdk/testutil/retry"
"github.com/hashicorp/consul/types"
"github.com/hashicorp/net-rpc-msgpackrpc"
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
"github.com/hashicorp/raft"
"github.com/hashicorp/serf/serf"
"github.com/stretchr/testify/require"
@ -166,6 +166,21 @@ func waitForNewACLs(t *testing.T, server *Server) {
require.False(t, server.UseLegacyACLs(), "Server cannot use new ACLs")
}
func waitForNewACLReplication(t *testing.T, server *Server, expectedReplicationType structs.ACLReplicationType) {
var (
replTyp structs.ACLReplicationType
running bool
)
retry.Run(t, func(r *retry.R) {
replTyp, running = server.getACLReplicationStatusRunningType()
require.Equal(r, expectedReplicationType, replTyp, "Server not running new replicator yet")
require.True(r, running, "Server not running new replicator yet")
})
require.Equal(t, expectedReplicationType, replTyp, "Server not running new replicator yet")
require.True(t, running, "Server not running new replicator yet")
}
func seeEachOther(a, b []serf.Member, addra, addrb string) bool {
return serfMembersContains(a, addrb) && serfMembersContains(b, addra)
}

View File

@ -10,7 +10,7 @@ import (
"sync/atomic"
"time"
"github.com/armon/go-metrics"
metrics "github.com/armon/go-metrics"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/consul/agent/connect"
ca "github.com/hashicorp/consul/agent/connect/ca"
@ -22,7 +22,7 @@ import (
"github.com/hashicorp/consul/types"
memdb "github.com/hashicorp/go-memdb"
uuid "github.com/hashicorp/go-uuid"
"github.com/hashicorp/go-version"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/raft"
"github.com/hashicorp/serf/serf"
"golang.org/x/time/rate"
@ -308,6 +308,8 @@ func (s *Server) revokeLeadership() error {
s.setCAProvider(nil, nil)
s.stopACLTokenReaping()
s.stopACLUpgrade()
s.resetConsistentReadReady()
@ -329,6 +331,7 @@ func (s *Server) initializeLegacyACL() error {
if err != nil {
return fmt.Errorf("failed to get anonymous token: %v", err)
}
// Ignoring expiration times to avoid an insertion collision.
if token == nil {
req := structs.ACLRequest{
Datacenter: authDC,
@ -352,6 +355,7 @@ func (s *Server) initializeLegacyACL() error {
if err != nil {
return fmt.Errorf("failed to get master token: %v", err)
}
// Ignoring expiration times to avoid an insertion collision.
if token == nil {
req := structs.ACLRequest{
Datacenter: authDC,
@ -423,6 +427,10 @@ func (s *Server) initializeACLs(upgrade bool) error {
// leader.
s.acls.cache.Purge()
// Purge the auth method validators since they could've changed while we
// were not leader.
s.purgeAuthMethodValidators()
// Remove any token affected by CVE-2019-8336
if !s.InACLDatacenter() {
_, token, err := s.fsm.State().ACLTokenGetBySecret(nil, redactedToken)
@ -482,6 +490,7 @@ func (s *Server) initializeACLs(upgrade bool) error {
if err != nil {
return fmt.Errorf("failed to get master token: %v", err)
}
// Ignoring expiration times to avoid an insertion collision.
if token == nil {
accessor, err := lib.GenerateUUID(s.checkTokenUUID)
if err != nil {
@ -543,6 +552,7 @@ func (s *Server) initializeACLs(upgrade bool) error {
if err != nil {
return fmt.Errorf("failed to get anonymous token: %v", err)
}
// Ignoring expiration times to avoid an insertion collision.
if token == nil {
// DEPRECATED (ACL-Legacy-Compat) - Don't need to query for previous "anonymous" token
// check for legacy token that needs an upgrade
@ -550,6 +560,7 @@ func (s *Server) initializeACLs(upgrade bool) error {
if err != nil {
return fmt.Errorf("failed to get anonymous token: %v", err)
}
// Ignoring expiration times to avoid an insertion collision.
// the token upgrade routine will take care of upgrading the token if a legacy version exists
if legacyToken == nil {
@ -572,6 +583,7 @@ func (s *Server) initializeACLs(upgrade bool) error {
s.logger.Printf("[INFO] consul: Created ACL anonymous token from configuration")
}
}
// launch the upgrade go routine to generate accessors for everything
s.startACLUpgrade()
} else {
if s.UseLegacyACLs() && !upgrade {
@ -588,7 +600,7 @@ func (s *Server) initializeACLs(upgrade bool) error {
s.startACLReplication()
}
// launch the upgrade go routine to generate accessors for everything
s.startACLTokenReaping()
return nil
}
@ -617,6 +629,7 @@ func (s *Server) startACLUpgrade() {
if err != nil {
s.logger.Printf("[WARN] acl: encountered an error while searching for tokens without accessor ids: %v", err)
}
// No need to check expiration time here, as that only exists for v2 tokens.
if len(tokens) == 0 {
ws := memdb.NewWatchSet()
@ -649,7 +662,10 @@ func (s *Server) startACLUpgrade() {
}
// Assign the global-management policy to legacy management tokens
if len(newToken.Policies) == 0 && newToken.Type == structs.ACLTokenTypeManagement {
if len(newToken.Policies) == 0 &&
len(newToken.ServiceIdentities) == 0 &&
len(newToken.Roles) == 0 &&
newToken.Type == structs.ACLTokenTypeManagement {
newToken.Policies = append(newToken.Policies, structs.ACLTokenPolicyLink{ID: structs.ACLPolicyGlobalManagementID})
}
@ -727,7 +743,7 @@ func (s *Server) startLegacyACLReplication() {
s.logger.Printf("[WARN] consul: Legacy ACL replication error (will retry if still leader): %v", err)
} else {
lastRemoteIndex = index
s.updateACLReplicationStatusIndex(index)
s.updateACLReplicationStatusIndex(structs.ACLReplicateLegacy, index)
s.logger.Printf("[DEBUG] consul: Legacy ACL replication completed through remote index %d", index)
}
}
@ -749,8 +765,22 @@ func (s *Server) startACLReplication() {
ctx, cancel := context.WithCancel(context.Background())
s.aclReplicationCancel = cancel
replicationType := structs.ACLReplicatePolicies
s.startACLReplicator(ctx, structs.ACLReplicatePolicies, s.replicateACLPolicies)
s.startACLReplicator(ctx, structs.ACLReplicateRoles, s.replicateACLRoles)
if s.config.ACLTokenReplication {
s.startACLReplicator(ctx, structs.ACLReplicateTokens, s.replicateACLTokens)
s.updateACLReplicationStatusRunning(structs.ACLReplicateTokens)
} else {
s.updateACLReplicationStatusRunning(structs.ACLReplicatePolicies)
}
s.aclReplicationEnabled = true
}
type replicateFunc func(ctx context.Context, lastRemoteIndex uint64) (uint64, bool, error)
func (s *Server) startACLReplicator(ctx context.Context, replicationType structs.ACLReplicationType, replicateFunc replicateFunc) {
go func() {
var failedAttempts uint
limiter := rate.NewLimiter(rate.Limit(s.config.ACLReplicationRate), s.config.ACLReplicationBurst)
@ -765,7 +795,7 @@ func (s *Server) startACLReplication() {
continue
}
index, exit, err := s.replicateACLPolicies(lastRemoteIndex, ctx)
index, exit, err := replicateFunc(ctx, lastRemoteIndex)
if exit {
return
}
@ -773,7 +803,7 @@ func (s *Server) startACLReplication() {
if err != nil {
lastRemoteIndex = 0
s.updateACLReplicationStatusError()
s.logger.Printf("[WARN] consul: ACL policy replication error (will retry if still leader): %v", err)
s.logger.Printf("[WARN] consul: ACL %s replication error (will retry if still leader): %v", replicationType.SingularNoun(), err)
if (1 << failedAttempts) < aclReplicationMaxRetryBackoff {
failedAttempts++
}
@ -786,65 +816,14 @@ func (s *Server) startACLReplication() {
}
} else {
lastRemoteIndex = index
s.updateACLReplicationStatusIndex(index)
s.logger.Printf("[DEBUG] consul: ACL policy replication completed through remote index %d", index)
s.updateACLReplicationStatusIndex(replicationType, index)
s.logger.Printf("[DEBUG] consul: ACL %s replication completed through remote index %d", replicationType.SingularNoun(), index)
failedAttempts = 0
}
}
}()
s.logger.Printf("[INFO] acl: started ACL Policy replication")
if s.config.ACLTokenReplication {
replicationType = structs.ACLReplicateTokens
go func() {
var failedAttempts uint
limiter := rate.NewLimiter(rate.Limit(s.config.ACLReplicationRate), s.config.ACLReplicationBurst)
var lastRemoteIndex uint64
for {
if err := limiter.Wait(ctx); err != nil {
return
}
if s.tokens.ReplicationToken() == "" {
continue
}
index, exit, err := s.replicateACLTokens(lastRemoteIndex, ctx)
if exit {
return
}
if err != nil {
lastRemoteIndex = 0
s.updateACLReplicationStatusError()
s.logger.Printf("[WARN] consul: ACL token replication error (will retry if still leader): %v", err)
if (1 << failedAttempts) < aclReplicationMaxRetryBackoff {
failedAttempts++
}
select {
case <-ctx.Done():
return
case <-time.After((1 << failedAttempts) * time.Second):
// do nothing
}
} else {
lastRemoteIndex = index
s.updateACLReplicationStatusTokenIndex(index)
s.logger.Printf("[DEBUG] consul: ACL token replication completed through remote index %d", index)
failedAttempts = 0
}
}
}()
s.logger.Printf("[INFO] acl: started ACL Token replication")
}
s.updateACLReplicationStatusRunning(replicationType)
s.aclReplicationEnabled = true
s.logger.Printf("[INFO] acl: started ACL %s replication", replicationType.SingularNoun())
}
func (s *Server) stopACLReplication() {

View File

@ -109,6 +109,15 @@ type Server struct {
aclReplicationLock sync.RWMutex
aclReplicationEnabled bool
// aclTokenReapCancel is used to shut down the ACL Token expiration reap
// goroutine when we lose leadership.
aclTokenReapCancel context.CancelFunc
aclTokenReapLock sync.RWMutex
aclTokenReapEnabled bool
aclAuthMethodValidators map[string]*authMethodValidatorEntry
aclAuthMethodValidatorLock sync.RWMutex
// DEPRECATED (ACL-Legacy-Compat) - only needed while we support both
// useNewACLs is used to determine whether we can use new ACLs or not
useNewACLs int32

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@ import (
"fmt"
"github.com/hashicorp/consul/types"
"github.com/hashicorp/go-memdb"
memdb "github.com/hashicorp/go-memdb"
)
var (
@ -37,6 +37,30 @@ var (
// policy with an empty Name.
ErrMissingACLPolicyName = errors.New("Missing ACL Policy Name")
// ErrMissingACLRoleID is returned when a role set is called on
// a role with an empty ID.
ErrMissingACLRoleID = errors.New("Missing ACL Role ID")
// ErrMissingACLRoleName is returned when a role set is called on
// a role with an empty Name.
ErrMissingACLRoleName = errors.New("Missing ACL Role Name")
// ErrMissingACLBindingRuleID is returned when a binding rule set
// is called on a binding rule with an empty ID.
ErrMissingACLBindingRuleID = errors.New("Missing ACL Binding Rule ID")
// ErrMissingACLBindingRuleAuthMethod is returned when a binding rule set
// is called on a binding rule with an empty AuthMethod.
ErrMissingACLBindingRuleAuthMethod = errors.New("Missing ACL Binding Rule Auth Method")
// ErrMissingACLAuthMethodName is returned when an auth method set is
// called on an auth method with an empty Name.
ErrMissingACLAuthMethodName = errors.New("Missing ACL Auth Method Name")
// ErrMissingACLAuthMethodType is returned when an auth method set is
// called on an auth method with an empty Type.
ErrMissingACLAuthMethodType = errors.New("Missing ACL Auth Method Type")
// ErrMissingQueryID is returned when a Query set is called on
// a Query with an empty ID.
ErrMissingQueryID = errors.New("Missing Query ID")

View File

@ -6,10 +6,13 @@ import (
"net"
"runtime"
"strconv"
"strings"
"github.com/hashicorp/consul/agent/metadata"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/go-version"
"github.com/hashicorp/hil"
"github.com/hashicorp/hil/ast"
"github.com/hashicorp/serf/serf"
)
@ -322,3 +325,42 @@ func ServersGetACLMode(members []serf.Member, leader string, datacenter string)
return
}
// InterpolateHIL processes the string as if it were HIL and interpolates only
// the provided string->string map as possible variables.
func InterpolateHIL(s string, vars map[string]string) (string, error) {
if strings.Index(s, "${") == -1 {
// Skip going to the trouble of parsing something that has no HIL.
return s, nil
}
tree, err := hil.Parse(s)
if err != nil {
return "", err
}
vm := make(map[string]ast.Variable)
for k, v := range vars {
vm[k] = ast.Variable{
Type: ast.TypeString,
Value: v,
}
}
config := &hil.EvalConfig{
GlobalScope: &ast.BasicScope{
VarMap: vm,
},
}
result, err := hil.Eval(tree, config)
if err != nil {
return "", err
}
if result.Type != hil.TypeString {
return "", fmt.Errorf("generated unexpected hil type: %s", result.Type)
}
return result.Value.(string), nil
}

View File

@ -9,6 +9,7 @@ import (
"github.com/hashicorp/go-version"
"github.com/hashicorp/serf/serf"
"github.com/stretchr/testify/require"
)
func TestGetPrivateIP(t *testing.T) {
@ -403,3 +404,133 @@ func TestServersMeetMinimumVersion(t *testing.T) {
}
}
}
func TestInterpolateHIL(t *testing.T) {
for _, test := range []struct {
name string
in string
vars map[string]string
exp string
ok bool
}{
// valid HIL
{
"empty",
"",
map[string]string{},
"",
true,
},
{
"no vars",
"nothing",
map[string]string{},
"nothing",
true,
},
{
"just var",
"${item}",
map[string]string{"item": "value"},
"value",
true,
},
{
"var in middle",
"before ${item}after",
map[string]string{"item": "value"},
"before valueafter",
true,
},
{
"two vars",
"before ${item}after ${more}",
map[string]string{"item": "value", "more": "xyz"},
"before valueafter xyz",
true,
},
{
"missing map val",
"${item}",
map[string]string{"item": ""},
"",
true,
},
// "weird" HIL, but not technically invalid
{
"just end",
"}",
map[string]string{},
"}",
true,
},
{
"var without start",
" item }",
map[string]string{"item": "value"},
" item }",
true,
},
{
"two vars missing second start",
"before ${ item }after more }",
map[string]string{"item": "value", "more": "xyz"},
"before valueafter more }",
true,
},
// invalid HIL
{
"just start",
"${",
map[string]string{},
"",
false,
},
{
"backwards",
"}${",
map[string]string{},
"",
false,
},
{
"no varname",
"${}",
map[string]string{},
"",
false,
},
{
"missing map key",
"${item}",
map[string]string{},
"",
false,
},
{
"var without end",
"${ item ",
map[string]string{"item": "value"},
"",
false,
},
{
"two vars missing first end",
"before ${ item after ${ more }",
map[string]string{"item": "value", "more": "xyz"},
"",
false,
},
} {
t.Run(test.name, func(t *testing.T) {
out, err := InterpolateHIL(test.in, test.vars)
if test.ok {
require.NoError(t, err)
require.Equal(t, test.exp, out)
} else {
require.NotNil(t, err)
require.Equal(t, out, "")
}
})
}
}

View File

@ -10,10 +10,22 @@ func init() {
registerEndpoint("/v1/acl/info/", []string{"GET"}, (*HTTPServer).ACLGet)
registerEndpoint("/v1/acl/clone/", []string{"PUT"}, (*HTTPServer).ACLClone)
registerEndpoint("/v1/acl/list", []string{"GET"}, (*HTTPServer).ACLList)
registerEndpoint("/v1/acl/login", []string{"POST"}, (*HTTPServer).ACLLogin)
registerEndpoint("/v1/acl/logout", []string{"POST"}, (*HTTPServer).ACLLogout)
registerEndpoint("/v1/acl/replication", []string{"GET"}, (*HTTPServer).ACLReplicationStatus)
registerEndpoint("/v1/acl/policies", []string{"GET"}, (*HTTPServer).ACLPolicyList)
registerEndpoint("/v1/acl/policy", []string{"PUT"}, (*HTTPServer).ACLPolicyCreate)
registerEndpoint("/v1/acl/policy/", []string{"GET", "PUT", "DELETE"}, (*HTTPServer).ACLPolicyCRUD)
registerEndpoint("/v1/acl/roles", []string{"GET"}, (*HTTPServer).ACLRoleList)
registerEndpoint("/v1/acl/role", []string{"PUT"}, (*HTTPServer).ACLRoleCreate)
registerEndpoint("/v1/acl/role/name/", []string{"GET"}, (*HTTPServer).ACLRoleReadByName)
registerEndpoint("/v1/acl/role/", []string{"GET", "PUT", "DELETE"}, (*HTTPServer).ACLRoleCRUD)
registerEndpoint("/v1/acl/binding-rules", []string{"GET"}, (*HTTPServer).ACLBindingRuleList)
registerEndpoint("/v1/acl/binding-rule", []string{"PUT"}, (*HTTPServer).ACLBindingRuleCreate)
registerEndpoint("/v1/acl/binding-rule/", []string{"GET", "PUT", "DELETE"}, (*HTTPServer).ACLBindingRuleCRUD)
registerEndpoint("/v1/acl/auth-methods", []string{"GET"}, (*HTTPServer).ACLAuthMethodList)
registerEndpoint("/v1/acl/auth-method", []string{"PUT"}, (*HTTPServer).ACLAuthMethodCreate)
registerEndpoint("/v1/acl/auth-method/", []string{"GET", "PUT", "DELETE"}, (*HTTPServer).ACLAuthMethodCRUD)
registerEndpoint("/v1/acl/rules/translate", []string{"POST"}, (*HTTPServer).ACLRulesTranslate)
registerEndpoint("/v1/acl/rules/translate/", []string{"GET"}, (*HTTPServer).ACLRulesTranslateLegacyToken)
registerEndpoint("/v1/acl/tokens", []string{"GET"}, (*HTTPServer).ACLTokenList)

View File

@ -4,6 +4,7 @@ import (
"encoding/binary"
"errors"
"fmt"
"hash"
"hash/fnv"
"sort"
"strings"
@ -84,6 +85,22 @@ session_prefix "" {
// This is the policy ID for anonymous access. This is configurable by the
// user.
ACLTokenAnonymousID = "00000000-0000-0000-0000-000000000002"
// aclPolicyTemplateServiceIdentity is the template used for synthesizing
// policies for service identities.
aclPolicyTemplateServiceIdentity = `
service "%s" {
policy = "write"
}
service "%s-sidecar-proxy" {
policy = "write"
}
service_prefix "" {
policy = "read"
}
node_prefix "" {
policy = "read"
}`
)
func ACLIDReserved(id string) bool {
@ -112,7 +129,10 @@ type ACLIdentity interface {
ID() string
SecretToken() string
PolicyIDs() []string
RoleIDs() []string
EmbeddedPolicy() *ACLPolicy
ServiceIdentityList() []*ACLServiceIdentity
IsExpired(asOf time.Time) bool
}
type ACLTokenPolicyLink struct {
@ -120,6 +140,65 @@ type ACLTokenPolicyLink struct {
Name string `hash:"ignore"`
}
type ACLTokenRoleLink struct {
ID string
Name string `hash:"ignore"`
}
// ACLServiceIdentity represents a high-level grant of all necessary privileges
// to assume the identity of the named Service in the Catalog and within
// Connect.
type ACLServiceIdentity struct {
ServiceName string
// Datacenters that the synthetic policy will be valid within.
// - No wildcards allowed
// - If empty then the synthetic policy is valid within all datacenters
//
// Only valid for global tokens. It is an error to specify this for local tokens.
Datacenters []string `json:",omitempty"`
}
func (s *ACLServiceIdentity) Clone() *ACLServiceIdentity {
s2 := *s
s2.Datacenters = cloneStringSlice(s.Datacenters)
return &s2
}
func (s *ACLServiceIdentity) AddToHash(h hash.Hash) {
h.Write([]byte(s.ServiceName))
for _, dc := range s.Datacenters {
h.Write([]byte(dc))
}
}
func (s *ACLServiceIdentity) EstimateSize() int {
size := len(s.ServiceName)
for _, dc := range s.Datacenters {
size += len(dc)
}
return size
}
func (s *ACLServiceIdentity) SyntheticPolicy() *ACLPolicy {
// Given that we validate this string name before persisting, we do not
// have to escape it before doing the following interpolation.
rules := fmt.Sprintf(aclPolicyTemplateServiceIdentity, s.ServiceName, s.ServiceName)
hasher := fnv.New128a()
hashID := fmt.Sprintf("%x", hasher.Sum([]byte(rules)))
policy := &ACLPolicy{}
policy.ID = hashID
policy.Name = fmt.Sprintf("synthetic-policy-%s", hashID)
policy.Description = "synthetic policy"
policy.Rules = rules
policy.Syntax = acl.SyntaxCurrent
policy.Datacenters = s.Datacenters
policy.SetHash(true)
return policy
}
type ACLToken struct {
// This is the UUID used for tracking and management purposes
AccessorID string
@ -130,10 +209,18 @@ type ACLToken struct {
// Human readable string to display for the token (Optional)
Description string
// List of policy links - nil/empty for legacy tokens
// List of policy links - nil/empty for legacy tokens or if service identities are in use.
// Note this is the list of IDs and not the names. Prior to token creation
// the list of policy names gets validated and the policy IDs get stored herein
Policies []ACLTokenPolicyLink
Policies []ACLTokenPolicyLink `json:",omitempty"`
// List of role links. Note this is the list of IDs and not the names.
// Prior to token creation the list of role names gets validated and the
// role IDs get stored herein
Roles []ACLTokenRoleLink `json:",omitempty"`
// List of services to generate synthetic policies for.
ServiceIdentities []*ACLServiceIdentity `json:",omitempty"`
// Type is the V1 Token Type
// DEPRECATED (ACL-Legacy-Compat) - remove once we no longer support v1 ACL compat
@ -150,6 +237,26 @@ type ACLToken struct {
// to the ACL datacenter and replicated to others.
Local bool
// AuthMethod is the name of the auth method used to create this token.
AuthMethod string `json:",omitempty"`
// ExpirationTime represents the point after which a token should be
// considered revoked and is eligible for destruction. The zero value
// represents NO expiration.
//
// This is a pointer value so that the zero value is omitted properly
// during json serialization. time.Time does not respect json omitempty
// directives unfortunately.
ExpirationTime *time.Time `json:",omitempty"`
// ExpirationTTL is a convenience field for helping set ExpirationTime to a
// value of CreateTime+ExpirationTTL. This can only be set during
// TokenCreate and is cleared and used to initialize the ExpirationTime
// field before being persisted to the state store or raft log.
//
// This is a string version of a time.Duration like "2m".
ExpirationTTL time.Duration `json:",omitempty"`
// The time when this token was created
CreateTime time.Time `json:",omitempty"`
@ -167,11 +274,23 @@ type ACLToken struct {
func (t *ACLToken) Clone() *ACLToken {
t2 := *t
t2.Policies = nil
t2.Roles = nil
t2.ServiceIdentities = nil
if len(t.Policies) > 0 {
t2.Policies = make([]ACLTokenPolicyLink, len(t.Policies))
copy(t2.Policies, t.Policies)
}
if len(t.Roles) > 0 {
t2.Roles = make([]ACLTokenRoleLink, len(t.Roles))
copy(t2.Roles, t.Roles)
}
if len(t.ServiceIdentities) > 0 {
t2.ServiceIdentities = make([]*ACLServiceIdentity, len(t.ServiceIdentities))
for i, s := range t.ServiceIdentities {
t2.ServiceIdentities[i] = s.Clone()
}
}
return &t2
}
@ -184,13 +303,62 @@ func (t *ACLToken) SecretToken() string {
}
func (t *ACLToken) PolicyIDs() []string {
var ids []string
if len(t.Policies) == 0 {
return nil
}
ids := make([]string, 0, len(t.Policies))
for _, link := range t.Policies {
ids = append(ids, link.ID)
}
return ids
}
func (t *ACLToken) RoleIDs() []string {
if len(t.Roles) == 0 {
return nil
}
ids := make([]string, 0, len(t.Roles))
for _, link := range t.Roles {
ids = append(ids, link.ID)
}
return ids
}
func (t *ACLToken) ServiceIdentityList() []*ACLServiceIdentity {
if len(t.ServiceIdentities) == 0 {
return nil
}
out := make([]*ACLServiceIdentity, 0, len(t.ServiceIdentities))
for _, s := range t.ServiceIdentities {
out = append(out, s.Clone())
}
return out
}
func (t *ACLToken) IsExpired(asOf time.Time) bool {
if asOf.IsZero() || !t.HasExpirationTime() {
return false
}
return t.ExpirationTime.Before(asOf)
}
func (t *ACLToken) HasExpirationTime() bool {
return t.ExpirationTime != nil && !t.ExpirationTime.IsZero()
}
func (t *ACLToken) UsesNonLegacyFields() bool {
return len(t.Policies) > 0 ||
len(t.ServiceIdentities) > 0 ||
len(t.Roles) > 0 ||
t.Type == "" ||
t.HasExpirationTime() ||
t.ExpirationTTL != 0 ||
t.AuthMethod != ""
}
func (t *ACLToken) EmbeddedPolicy() *ACLPolicy {
// DEPRECATED (ACL-Legacy-Compat)
//
@ -229,6 +397,14 @@ func (t *ACLToken) SetHash(force bool) []byte {
panic(err)
}
// Any non-immutable "content" fields should be involved with the
// overall hash. The IDs are immutable which is why they aren't here.
// The raft indices are metadata similar to the hash which is why they
// aren't incorporated. CreateTime is similarly immutable
//
// The Hash is really only used for replication to determine if a token
// has changed and should be updated locally.
// Write all the user set fields
hash.Write([]byte(t.Description))
hash.Write([]byte(t.Type))
@ -244,6 +420,14 @@ func (t *ACLToken) SetHash(force bool) []byte {
hash.Write([]byte(link.ID))
}
for _, link := range t.Roles {
hash.Write([]byte(link.ID))
}
for _, srvid := range t.ServiceIdentities {
srvid.AddToHash(hash)
}
// Finalize the hash
hashVal := hash.Sum(nil)
@ -254,11 +438,17 @@ func (t *ACLToken) SetHash(force bool) []byte {
}
func (t *ACLToken) EstimateSize() int {
// 33 = 16 (RaftIndex) + 8 (Hash) + 8 (CreateTime) + 1 (Local)
size := 33 + len(t.AccessorID) + len(t.SecretID) + len(t.Description) + len(t.Type) + len(t.Rules)
// 41 = 16 (RaftIndex) + 8 (Hash) + 8 (ExpirationTime) + 8 (CreateTime) + 1 (Local)
size := 41 + len(t.AccessorID) + len(t.SecretID) + len(t.Description) + len(t.Type) + len(t.Rules) + len(t.AuthMethod)
for _, link := range t.Policies {
size += len(link.ID) + len(link.Name)
}
for _, link := range t.Roles {
size += len(link.ID) + len(link.Name)
}
for _, srvid := range t.ServiceIdentities {
size += srvid.EstimateSize()
}
return size
}
@ -266,30 +456,38 @@ func (t *ACLToken) EstimateSize() int {
type ACLTokens []*ACLToken
type ACLTokenListStub struct {
AccessorID string
Description string
Policies []ACLTokenPolicyLink
Local bool
CreateTime time.Time `json:",omitempty"`
Hash []byte
CreateIndex uint64
ModifyIndex uint64
Legacy bool `json:",omitempty"`
AccessorID string
Description string
Policies []ACLTokenPolicyLink `json:",omitempty"`
Roles []ACLTokenRoleLink `json:",omitempty"`
ServiceIdentities []*ACLServiceIdentity `json:",omitempty"`
Local bool
AuthMethod string `json:",omitempty"`
ExpirationTime *time.Time `json:",omitempty"`
CreateTime time.Time `json:",omitempty"`
Hash []byte
CreateIndex uint64
ModifyIndex uint64
Legacy bool `json:",omitempty"`
}
type ACLTokenListStubs []*ACLTokenListStub
func (token *ACLToken) Stub() *ACLTokenListStub {
return &ACLTokenListStub{
AccessorID: token.AccessorID,
Description: token.Description,
Policies: token.Policies,
Local: token.Local,
CreateTime: token.CreateTime,
Hash: token.Hash,
CreateIndex: token.CreateIndex,
ModifyIndex: token.ModifyIndex,
Legacy: token.Rules != "",
AccessorID: token.AccessorID,
Description: token.Description,
Policies: token.Policies,
Roles: token.Roles,
ServiceIdentities: token.ServiceIdentities,
Local: token.Local,
AuthMethod: token.AuthMethod,
ExpirationTime: token.ExpirationTime,
CreateTime: token.CreateTime,
Hash: token.Hash,
CreateIndex: token.CreateIndex,
ModifyIndex: token.ModifyIndex,
Legacy: token.Rules != "",
}
}
@ -343,11 +541,7 @@ type ACLPolicy struct {
func (p *ACLPolicy) Clone() *ACLPolicy {
p2 := *p
p2.Datacenters = nil
if len(p.Datacenters) > 0 {
p2.Datacenters = make([]string, len(p.Datacenters))
copy(p2.Datacenters, p.Datacenters)
}
p2.Datacenters = cloneStringSlice(p.Datacenters)
return &p2
}
@ -384,6 +578,14 @@ func (p *ACLPolicy) SetHash(force bool) []byte {
panic(err)
}
// Any non-immutable "content" fields should be involved with the
// overall hash. The ID is immutable which is why it isn't here. The
// raft indices are metadata similar to the hash which is why they
// aren't incorporated. CreateTime is similarly immutable
//
// The Hash is really only used for replication to determine if a policy
// has changed and should be updated locally.
// Write all the user set fields
hash.Write([]byte(p.Name))
hash.Write([]byte(p.Description))
@ -414,7 +616,7 @@ func (p *ACLPolicy) EstimateSize() int {
return size
}
// ACLPolicyListHash returns a consistent hash for a set of policies.
// HashKey returns a consistent hash for a set of policies.
func (policies ACLPolicies) HashKey() string {
cacheKeyHash, err := blake2b.New256(nil)
if err != nil {
@ -500,14 +702,288 @@ func (policies ACLPolicies) Merge(cache *ACLCaches, sentinel sentinel.Evaluator)
return acl.MergePolicies(parsed), nil
}
type ACLRoles []*ACLRole
// HashKey returns a consistent hash for a set of roles.
func (roles ACLRoles) HashKey() string {
cacheKeyHash, err := blake2b.New256(nil)
if err != nil {
panic(err)
}
for _, role := range roles {
cacheKeyHash.Write([]byte(role.ID))
// including the modify index prevents a role set from being
// cached if one of the roles has changed
binary.Write(cacheKeyHash, binary.BigEndian, role.ModifyIndex)
}
return fmt.Sprintf("%x", cacheKeyHash.Sum(nil))
}
func (roles ACLRoles) Sort() {
sort.Slice(roles, func(i, j int) bool {
return roles[i].ID < roles[j].ID
})
}
type ACLRolePolicyLink struct {
ID string
Name string `hash:"ignore"`
}
type ACLRole struct {
// ID is the internal UUID associated with the role
ID string
// Name is the unique name to reference the role by.
Name string
// Description is a human readable description (Optional)
Description string
// List of policy links.
// Note this is the list of IDs and not the names. Prior to role creation
// the list of policy names gets validated and the policy IDs get stored herein
Policies []ACLRolePolicyLink `json:",omitempty"`
// List of services to generate synthetic policies for.
ServiceIdentities []*ACLServiceIdentity `json:",omitempty"`
// Hash of the contents of the role
// This does not take into account the ID (which is immutable)
// nor the raft metadata.
//
// This is needed mainly for replication purposes. When replicating from
// one DC to another keeping the content Hash will allow us to avoid
// unnecessary calls to the authoritative DC
Hash []byte
// Embedded Raft Metadata
RaftIndex `hash:"ignore"`
}
func (r *ACLRole) Clone() *ACLRole {
r2 := *r
r2.Policies = nil
r2.ServiceIdentities = nil
if len(r.Policies) > 0 {
r2.Policies = make([]ACLRolePolicyLink, len(r.Policies))
copy(r2.Policies, r.Policies)
}
if len(r.ServiceIdentities) > 0 {
r2.ServiceIdentities = make([]*ACLServiceIdentity, len(r.ServiceIdentities))
for i, s := range r.ServiceIdentities {
r2.ServiceIdentities[i] = s.Clone()
}
}
return &r2
}
func (r *ACLRole) SetHash(force bool) []byte {
if force || r.Hash == nil {
// Initialize a 256bit Blake2 hash (32 bytes)
hash, err := blake2b.New256(nil)
if err != nil {
panic(err)
}
// Any non-immutable "content" fields should be involved with the
// overall hash. The ID is immutable which is why it isn't here. The
// raft indices are metadata similar to the hash which is why they
// aren't incorporated. CreateTime is similarly immutable
//
// The Hash is really only used for replication to determine if a role
// has changed and should be updated locally.
// Write all the user set fields
hash.Write([]byte(r.Name))
hash.Write([]byte(r.Description))
for _, link := range r.Policies {
hash.Write([]byte(link.ID))
}
for _, srvid := range r.ServiceIdentities {
srvid.AddToHash(hash)
}
// Finalize the hash
hashVal := hash.Sum(nil)
// Set and return the hash
r.Hash = hashVal
}
return r.Hash
}
func (r *ACLRole) EstimateSize() int {
// This is just an estimate. There is other data structure overhead
// pointers etc that this does not account for.
// 60 = 36 (uuid) + 16 (RaftIndex) + 8 (Hash)
size := 60 + len(r.Name) + len(r.Description)
for _, link := range r.Policies {
size += len(link.ID) + len(link.Name)
}
for _, srvid := range r.ServiceIdentities {
size += srvid.EstimateSize()
}
return size
}
const (
// BindingRuleBindTypeService is the binding rule bind type that
// assigns a Service Identity to the token that is created using the value
// of the computed BindName as the ServiceName like:
//
// &ACLToken{
// ...other fields...
// ServiceIdentities: []*ACLServiceIdentity{
// &ACLServiceIdentity{
// ServiceName: "<computed BindName>",
// },
// },
// }
BindingRuleBindTypeService = "service"
// BindingRuleBindTypeRole is the binding rule bind type that only allows
// the binding rule to function if a role with the given name (BindName)
// exists at login-time. If it does the token that is created is directly
// linked to that role like:
//
// &ACLToken{
// ...other fields...
// Roles: []ACLTokenRoleLink{
// { Name: "<computed BindName>" }
// }
// }
//
// If it does not exist at login-time the rule is ignored.
BindingRuleBindTypeRole = "role"
)
type ACLBindingRule struct {
// ID is the internal UUID associated with the binding rule
ID string
// Description is a human readable description (Optional)
Description string
// AuthMethod is the name of the auth method for which this rule applies.
AuthMethod string
// Selector is an expression that matches against verified identity
// attributes returned from the auth method during login.
Selector string
// BindType adjusts how this binding rule is applied at login time. The
// valid values are:
//
// - BindingRuleBindTypeService = "service"
// - BindingRuleBindTypeRole = "role"
BindType string
// BindName is the target of the binding. Can be lightly templated using
// HIL ${foo} syntax from available field names. How it is used depends
// upon the BindType.
BindName string
// Embedded Raft Metadata
RaftIndex `hash:"ignore"`
}
func (r *ACLBindingRule) Clone() *ACLBindingRule {
r2 := *r
return &r2
}
type ACLBindingRules []*ACLBindingRule
func (rules ACLBindingRules) Sort() {
sort.Slice(rules, func(i, j int) bool {
return rules[i].ID < rules[j].ID
})
}
type ACLAuthMethodListStub struct {
Name string
Description string
Type string
CreateIndex uint64
ModifyIndex uint64
}
func (p *ACLAuthMethod) Stub() *ACLAuthMethodListStub {
return &ACLAuthMethodListStub{
Name: p.Name,
Description: p.Description,
Type: p.Type,
CreateIndex: p.CreateIndex,
ModifyIndex: p.ModifyIndex,
}
}
type ACLAuthMethods []*ACLAuthMethod
type ACLAuthMethodListStubs []*ACLAuthMethodListStub
func (methods ACLAuthMethods) Sort() {
sort.Slice(methods, func(i, j int) bool {
return methods[i].Name < methods[j].Name
})
}
func (methods ACLAuthMethodListStubs) Sort() {
sort.Slice(methods, func(i, j int) bool {
return methods[i].Name < methods[j].Name
})
}
type ACLAuthMethod struct {
// Name is a unique identifier for this specific auth method.
//
// Immutable once set and only settable during create.
Name string
// Type is the type of the auth method this is.
//
// Immutable once set and only settable during create.
Type string
// Description is just an optional bunch of explanatory text.
Description string
// Configuration is arbitrary configuration for the auth method. This
// should only contain primitive values and containers (such as lists and
// maps).
Config map[string]interface{}
// Embedded Raft Metadata
RaftIndex `hash:"ignore"`
}
type ACLReplicationType string
const (
ACLReplicateLegacy ACLReplicationType = "legacy"
ACLReplicatePolicies ACLReplicationType = "policies"
ACLReplicateRoles ACLReplicationType = "roles"
ACLReplicateTokens ACLReplicationType = "tokens"
)
func (t ACLReplicationType) SingularNoun() string {
switch t {
case ACLReplicateLegacy:
return "legacy"
case ACLReplicatePolicies:
return "policy"
case ACLReplicateRoles:
return "role"
case ACLReplicateTokens:
return "token"
default:
return "<UNKNOWN>"
}
}
// ACLReplicationStatus provides information about the health of the ACL
// replication system.
type ACLReplicationStatus struct {
@ -516,6 +992,7 @@ type ACLReplicationStatus struct {
SourceDatacenter string
ReplicationType ACLReplicationType
ReplicatedIndex uint64
ReplicatedRoleIndex uint64
ReplicatedTokenIndex uint64
LastSuccess time.Time
LastError time.Time
@ -561,6 +1038,8 @@ type ACLTokenListRequest struct {
IncludeLocal bool // Whether local tokens should be included
IncludeGlobal bool // Whether global tokens should be included
Policy string // Policy filter
Role string // Role filter
AuthMethod string // Auth Method filter
Datacenter string // The datacenter to perform the request within
QueryOptions
}
@ -719,3 +1198,259 @@ type ACLPolicyBatchSetRequest struct {
type ACLPolicyBatchDeleteRequest struct {
PolicyIDs []string
}
func cloneStringSlice(s []string) []string {
if len(s) == 0 {
return nil
}
out := make([]string, len(s))
copy(out, s)
return out
}
// ACLRoleSetRequest is used at the RPC layer for creation and update requests
type ACLRoleSetRequest struct {
Role ACLRole // The role to upsert
Datacenter string // The datacenter to perform the request within
WriteRequest
}
func (r *ACLRoleSetRequest) RequestDatacenter() string {
return r.Datacenter
}
// ACLRoleDeleteRequest is used at the RPC layer deletion requests
type ACLRoleDeleteRequest struct {
RoleID string // id of the role to delete
Datacenter string // The datacenter to perform the request within
WriteRequest
}
func (r *ACLRoleDeleteRequest) RequestDatacenter() string {
return r.Datacenter
}
// ACLRoleGetRequest is used at the RPC layer to perform role read operations
type ACLRoleGetRequest struct {
RoleID string // id used for the role lookup (one of RoleID or RoleName is allowed)
RoleName string // name used for the role lookup (one of RoleID or RoleName is allowed)
Datacenter string // The datacenter to perform the request within
QueryOptions
}
func (r *ACLRoleGetRequest) RequestDatacenter() string {
return r.Datacenter
}
// ACLRoleListRequest is used at the RPC layer to request a listing of roles
type ACLRoleListRequest struct {
Policy string // Policy filter
Datacenter string // The datacenter to perform the request within
QueryOptions
}
func (r *ACLRoleListRequest) RequestDatacenter() string {
return r.Datacenter
}
type ACLRoleListResponse struct {
Roles ACLRoles
QueryMeta
}
// ACLRoleBatchGetRequest is used at the RPC layer to request a subset of
// the roles associated with the token used for retrieval
type ACLRoleBatchGetRequest struct {
RoleIDs []string // List of role ids to fetch
Datacenter string // The datacenter to perform the request within
QueryOptions
}
func (r *ACLRoleBatchGetRequest) RequestDatacenter() string {
return r.Datacenter
}
// ACLRoleResponse returns a single role + metadata
type ACLRoleResponse struct {
Role *ACLRole
QueryMeta
}
type ACLRoleBatchResponse struct {
Roles []*ACLRole
QueryMeta
}
// ACLRoleBatchSetRequest is used at the Raft layer for batching
// multiple role creations and updates
//
// This is particularly useful during replication
type ACLRoleBatchSetRequest struct {
Roles ACLRoles
}
// ACLRoleBatchDeleteRequest is used at the Raft layer for batching
// multiple role deletions
//
// This is particularly useful during replication
type ACLRoleBatchDeleteRequest struct {
RoleIDs []string
}
// ACLBindingRuleSetRequest is used at the RPC layer for creation and update requests
type ACLBindingRuleSetRequest struct {
BindingRule ACLBindingRule // The rule to upsert
Datacenter string // The datacenter to perform the request within
WriteRequest
}
func (r *ACLBindingRuleSetRequest) RequestDatacenter() string {
return r.Datacenter
}
// ACLBindingRuleDeleteRequest is used at the RPC layer deletion requests
type ACLBindingRuleDeleteRequest struct {
BindingRuleID string // id of the rule to delete
Datacenter string // The datacenter to perform the request within
WriteRequest
}
func (r *ACLBindingRuleDeleteRequest) RequestDatacenter() string {
return r.Datacenter
}
// ACLBindingRuleGetRequest is used at the RPC layer to perform rule read operations
type ACLBindingRuleGetRequest struct {
BindingRuleID string // id used for the rule lookup
Datacenter string // The datacenter to perform the request within
QueryOptions
}
func (r *ACLBindingRuleGetRequest) RequestDatacenter() string {
return r.Datacenter
}
// ACLBindingRuleListRequest is used at the RPC layer to request a listing of rules
type ACLBindingRuleListRequest struct {
AuthMethod string // optional filter
Datacenter string // The datacenter to perform the request within
QueryOptions
}
func (r *ACLBindingRuleListRequest) RequestDatacenter() string {
return r.Datacenter
}
type ACLBindingRuleListResponse struct {
BindingRules ACLBindingRules
QueryMeta
}
// ACLBindingRuleResponse returns a single binding + metadata
type ACLBindingRuleResponse struct {
BindingRule *ACLBindingRule
QueryMeta
}
// ACLBindingRuleBatchSetRequest is used at the Raft layer for batching
// multiple rule creations and updates
type ACLBindingRuleBatchSetRequest struct {
BindingRules ACLBindingRules
}
// ACLBindingRuleBatchDeleteRequest is used at the Raft layer for batching
// multiple rule deletions
type ACLBindingRuleBatchDeleteRequest struct {
BindingRuleIDs []string
}
// ACLAuthMethodSetRequest is used at the RPC layer for creation and update requests
type ACLAuthMethodSetRequest struct {
AuthMethod ACLAuthMethod // The auth method to upsert
Datacenter string // The datacenter to perform the request within
WriteRequest
}
func (r *ACLAuthMethodSetRequest) RequestDatacenter() string {
return r.Datacenter
}
// ACLAuthMethodDeleteRequest is used at the RPC layer deletion requests
type ACLAuthMethodDeleteRequest struct {
AuthMethodName string // name of the auth method to delete
Datacenter string // The datacenter to perform the request within
WriteRequest
}
func (r *ACLAuthMethodDeleteRequest) RequestDatacenter() string {
return r.Datacenter
}
// ACLAuthMethodGetRequest is used at the RPC layer to perform rule read operations
type ACLAuthMethodGetRequest struct {
AuthMethodName string // name used for the auth method lookup
Datacenter string // The datacenter to perform the request within
QueryOptions
}
func (r *ACLAuthMethodGetRequest) RequestDatacenter() string {
return r.Datacenter
}
// ACLAuthMethodListRequest is used at the RPC layer to request a listing of auth methods
type ACLAuthMethodListRequest struct {
Datacenter string // The datacenter to perform the request within
QueryOptions
}
func (r *ACLAuthMethodListRequest) RequestDatacenter() string {
return r.Datacenter
}
type ACLAuthMethodListResponse struct {
AuthMethods ACLAuthMethodListStubs
QueryMeta
}
// ACLAuthMethodResponse returns a single auth method + metadata
type ACLAuthMethodResponse struct {
AuthMethod *ACLAuthMethod
QueryMeta
}
// ACLAuthMethodBatchSetRequest is used at the Raft layer for batching
// multiple auth method creations and updates
type ACLAuthMethodBatchSetRequest struct {
AuthMethods ACLAuthMethods
}
// ACLAuthMethodBatchDeleteRequest is used at the Raft layer for batching
// multiple auth method deletions
type ACLAuthMethodBatchDeleteRequest struct {
AuthMethodNames []string
}
type ACLLoginParams struct {
AuthMethod string
BearerToken string
Meta map[string]string `json:",omitempty"`
}
type ACLLoginRequest struct {
Auth *ACLLoginParams
Datacenter string // The datacenter to perform the request within
WriteRequest
}
func (r *ACLLoginRequest) RequestDatacenter() string {
return r.Datacenter
}
type ACLLogoutRequest struct {
Datacenter string // The datacenter to perform the request within
WriteRequest
}
func (r *ACLLogoutRequest) RequestDatacenter() string {
return r.Datacenter
}

View File

@ -4,7 +4,7 @@ import (
"time"
"github.com/hashicorp/consul/acl"
"github.com/hashicorp/golang-lru"
lru "github.com/hashicorp/golang-lru"
)
type ACLCachesConfig struct {
@ -12,6 +12,7 @@ type ACLCachesConfig struct {
Policies int
ParsedPolicies int
Authorizers int
Roles int
}
type ACLCaches struct {
@ -19,6 +20,7 @@ type ACLCaches struct {
parsedPolicies *lru.TwoQueueCache // policy content hash -> acl.Policy
policies *lru.TwoQueueCache // policy ID -> ACLPolicy
authorizers *lru.TwoQueueCache // token secret -> acl.Authorizer
roles *lru.TwoQueueCache // role ID -> ACLRole
}
type IdentityCacheEntry struct {
@ -58,6 +60,15 @@ func (e *AuthorizerCacheEntry) Age() time.Duration {
return time.Since(e.CacheTime)
}
type RoleCacheEntry struct {
Role *ACLRole
CacheTime time.Time
}
func (e *RoleCacheEntry) Age() time.Duration {
return time.Since(e.CacheTime)
}
func NewACLCaches(config *ACLCachesConfig) (*ACLCaches, error) {
cache := &ACLCaches{}
@ -97,6 +108,15 @@ func NewACLCaches(config *ACLCachesConfig) (*ACLCaches, error) {
cache.authorizers = authCache
}
if config != nil && config.Roles > 0 {
roleCache, err := lru.New2Q(config.Roles)
if err != nil {
return nil, err
}
cache.roles = roleCache
}
return cache, nil
}
@ -152,6 +172,19 @@ func (c *ACLCaches) GetAuthorizer(id string) *AuthorizerCacheEntry {
return nil
}
// GetRole fetches a role from the cache by id and returns it
func (c *ACLCaches) GetRole(roleID string) *RoleCacheEntry {
if c == nil || c.roles == nil {
return nil
}
if raw, ok := c.roles.Get(roleID); ok {
return raw.(*RoleCacheEntry)
}
return nil
}
// PutIdentity adds a new identity to the cache
func (c *ACLCaches) PutIdentity(id string, ident ACLIdentity) {
if c == nil || c.identities == nil {
@ -193,6 +226,14 @@ func (c *ACLCaches) PutAuthorizerWithTTL(id string, authorizer acl.Authorizer, t
c.authorizers.Add(id, &AuthorizerCacheEntry{Authorizer: authorizer, CacheTime: time.Now(), TTL: ttl})
}
func (c *ACLCaches) PutRole(roleID string, role *ACLRole) {
if c == nil || c.roles == nil {
return
}
c.roles.Add(roleID, &RoleCacheEntry{Role: role, CacheTime: time.Now()})
}
func (c *ACLCaches) RemoveIdentity(id string) {
if c != nil && c.identities != nil {
c.identities.Remove(id)
@ -205,6 +246,12 @@ func (c *ACLCaches) RemovePolicy(policyID string) {
}
}
func (c *ACLCaches) RemoveRole(roleID string) {
if c != nil && c.roles != nil {
c.roles.Remove(roleID)
}
}
func (c *ACLCaches) Purge() {
if c != nil {
if c.identities != nil {
@ -219,5 +266,8 @@ func (c *ACLCaches) Purge() {
if c.authorizers != nil {
c.authorizers.Purge()
}
if c.roles != nil {
c.roles.Purge()
}
}
}

View File

@ -16,7 +16,7 @@ func TestStructs_ACLCaches(t *testing.T) {
t.Run("Valid Sizes", func(t *testing.T) {
t.Parallel()
// 1 isn't valid due to a bug in golang-lru library
config := ACLCachesConfig{2, 2, 2, 2}
config := ACLCachesConfig{2, 2, 2, 2, 2}
cache, err := NewACLCaches(&config)
require.NoError(t, err)
@ -30,7 +30,7 @@ func TestStructs_ACLCaches(t *testing.T) {
t.Run("Zero Sizes", func(t *testing.T) {
t.Parallel()
// 1 isn't valid due to a bug in golang-lru library
config := ACLCachesConfig{0, 0, 0, 0}
config := ACLCachesConfig{0, 0, 0, 0, 0}
cache, err := NewACLCaches(&config)
require.NoError(t, err)
@ -102,4 +102,20 @@ func TestStructs_ACLCaches(t *testing.T) {
require.NotNil(t, entry.Authorizer)
require.True(t, entry.Authorizer == acl.DenyAll())
})
t.Run("Roles", func(t *testing.T) {
t.Parallel()
// 1 isn't valid due to a bug in golang-lru library
config := ACLCachesConfig{Roles: 4}
cache, err := NewACLCaches(&config)
require.NoError(t, err)
require.NotNil(t, cache)
cache.PutRole("foo", &ACLRole{})
entry := cache.GetRole("foo")
require.NotNil(t, entry)
require.NotNil(t, entry.Role)
})
}

View File

@ -73,14 +73,15 @@ func (a *ACL) Convert() *ACLToken {
}
return &ACLToken{
AccessorID: "",
SecretID: a.ID,
Description: a.Name,
Policies: nil,
Type: a.Type,
Rules: a.Rules,
Local: false,
RaftIndex: a.RaftIndex,
AccessorID: "",
SecretID: a.ID,
Description: a.Name,
Policies: nil,
ServiceIdentities: nil,
Type: a.Type,
Rules: a.Rules,
Local: false,
RaftIndex: a.RaftIndex,
}
}

View File

@ -140,6 +140,70 @@ func TestStructs_ACLToken_EmbeddedPolicy(t *testing.T) {
})
}
func TestStructs_ACLServiceIdentity_SyntheticPolicy(t *testing.T) {
t.Parallel()
for _, test := range []struct {
serviceName string
datacenters []string
expectRules string
}{
{"web", nil, `
service "web" {
policy = "write"
}
service "web-sidecar-proxy" {
policy = "write"
}
service_prefix "" {
policy = "read"
}
node_prefix "" {
policy = "read"
}`},
{"companion-cube-99", []string{"dc1", "dc2"}, `
service "companion-cube-99" {
policy = "write"
}
service "companion-cube-99-sidecar-proxy" {
policy = "write"
}
service_prefix "" {
policy = "read"
}
node_prefix "" {
policy = "read"
}`},
} {
name := test.serviceName
if len(test.datacenters) > 0 {
name += " [" + strings.Join(test.datacenters, ", ") + "]"
}
t.Run(name, func(t *testing.T) {
svcid := &ACLServiceIdentity{
ServiceName: test.serviceName,
Datacenters: test.datacenters,
}
expect := &ACLPolicy{
Syntax: acl.SyntaxCurrent,
Datacenters: test.datacenters,
Description: "synthetic policy",
Rules: test.expectRules,
}
got := svcid.SyntheticPolicy()
require.NotEmpty(t, got.ID)
require.True(t, strings.HasPrefix(got.Name, "synthetic-policy-"))
// strip irrelevant fields before equality
got.ID = ""
got.Name = ""
got.Hash = nil
require.Equal(t, expect, got)
})
}
}
func TestStructs_ACLToken_SetHash(t *testing.T) {
t.Parallel()
@ -208,7 +272,7 @@ func TestStructs_ACLToken_EstimateSize(t *testing.T) {
// this test is very contrived. Basically just tests that the
// math is okay and returns the value.
require.Equal(t, 120, token.EstimateSize())
require.Equal(t, 128, token.EstimateSize())
}
func TestStructs_ACLToken_Stub(t *testing.T) {
@ -451,6 +515,7 @@ func TestStructs_ACLPolicies_resolveWithCache(t *testing.T) {
Policies: 0,
ParsedPolicies: 4,
Authorizers: 0,
Roles: 0,
}
cache, err := NewACLCaches(&config)
require.NoError(t, err)
@ -543,6 +608,7 @@ func TestStructs_ACLPolicies_Compile(t *testing.T) {
Policies: 0,
ParsedPolicies: 4,
Authorizers: 2,
Roles: 0,
}
cache, err := NewACLCaches(&config)
require.NoError(t, err)

View File

@ -33,29 +33,35 @@ type RaftIndex struct {
// These are serialized between Consul servers and stored in Consul snapshots,
// so entries must only ever be added.
const (
RegisterRequestType MessageType = 0
DeregisterRequestType = 1
KVSRequestType = 2
SessionRequestType = 3
ACLRequestType = 4 // DEPRECATED (ACL-Legacy-Compat)
TombstoneRequestType = 5
CoordinateBatchUpdateType = 6
PreparedQueryRequestType = 7
TxnRequestType = 8
AutopilotRequestType = 9
AreaRequestType = 10
ACLBootstrapRequestType = 11
IntentionRequestType = 12
ConnectCARequestType = 13
ConnectCAProviderStateType = 14
ConnectCAConfigType = 15 // FSM snapshots only.
IndexRequestType = 16 // FSM snapshots only.
ACLTokenSetRequestType = 17
ACLTokenDeleteRequestType = 18
ACLPolicySetRequestType = 19
ACLPolicyDeleteRequestType = 20
ConnectCALeafRequestType = 21
ConfigEntryRequestType = 22
RegisterRequestType MessageType = 0
DeregisterRequestType = 1
KVSRequestType = 2
SessionRequestType = 3
ACLRequestType = 4 // DEPRECATED (ACL-Legacy-Compat)
TombstoneRequestType = 5
CoordinateBatchUpdateType = 6
PreparedQueryRequestType = 7
TxnRequestType = 8
AutopilotRequestType = 9
AreaRequestType = 10
ACLBootstrapRequestType = 11
IntentionRequestType = 12
ConnectCARequestType = 13
ConnectCAProviderStateType = 14
ConnectCAConfigType = 15 // FSM snapshots only.
IndexRequestType = 16 // FSM snapshots only.
ACLTokenSetRequestType = 17
ACLTokenDeleteRequestType = 18
ACLPolicySetRequestType = 19
ACLPolicyDeleteRequestType = 20
ConnectCALeafRequestType = 21
ConfigEntryRequestType = 22
ACLRoleSetRequestType = 23
ACLRoleDeleteRequestType = 24
ACLBindingRuleSetRequestType = 25
ACLBindingRuleDeleteRequestType = 26
ACLAuthMethodSetRequestType = 27
ACLAuthMethodDeleteRequestType = 28
)
const (

View File

@ -4,7 +4,10 @@ import (
"fmt"
"io"
"io/ioutil"
"net/url"
"time"
"github.com/mitchellh/mapstructure"
)
const (
@ -19,18 +22,26 @@ type ACLTokenPolicyLink struct {
ID string
Name string
}
type ACLTokenRoleLink struct {
ID string
Name string
}
// ACLToken represents an ACL Token
type ACLToken struct {
CreateIndex uint64
ModifyIndex uint64
AccessorID string
SecretID string
Description string
Policies []*ACLTokenPolicyLink
Local bool
CreateTime time.Time `json:",omitempty"`
Hash []byte `json:",omitempty"`
CreateIndex uint64
ModifyIndex uint64
AccessorID string
SecretID string
Description string
Policies []*ACLTokenPolicyLink `json:",omitempty"`
Roles []*ACLTokenRoleLink `json:",omitempty"`
ServiceIdentities []*ACLServiceIdentity `json:",omitempty"`
Local bool
ExpirationTTL time.Duration `json:",omitempty"`
ExpirationTime *time.Time `json:",omitempty"`
CreateTime time.Time `json:",omitempty"`
Hash []byte `json:",omitempty"`
// DEPRECATED (ACL-Legacy-Compat)
// Rules will only be present for legacy tokens returned via the new APIs
@ -38,15 +49,18 @@ type ACLToken struct {
}
type ACLTokenListEntry struct {
CreateIndex uint64
ModifyIndex uint64
AccessorID string
Description string
Policies []*ACLTokenPolicyLink
Local bool
CreateTime time.Time
Hash []byte
Legacy bool
CreateIndex uint64
ModifyIndex uint64
AccessorID string
Description string
Policies []*ACLTokenPolicyLink `json:",omitempty"`
Roles []*ACLTokenRoleLink `json:",omitempty"`
ServiceIdentities []*ACLServiceIdentity `json:",omitempty"`
Local bool
ExpirationTime *time.Time `json:",omitempty"`
CreateTime time.Time
Hash []byte
Legacy bool
}
// ACLEntry is used to represent a legacy ACL token
@ -67,11 +81,20 @@ type ACLReplicationStatus struct {
SourceDatacenter string
ReplicationType string
ReplicatedIndex uint64
ReplicatedRoleIndex uint64
ReplicatedTokenIndex uint64
LastSuccess time.Time
LastError time.Time
}
// ACLServiceIdentity represents a high-level grant of all necessary privileges
// to assume the identity of the named Service in the Catalog and within
// Connect.
type ACLServiceIdentity struct {
ServiceName string
Datacenters []string `json:",omitempty"`
}
// ACLPolicy represents an ACL Policy.
type ACLPolicy struct {
ID string
@ -94,6 +117,113 @@ type ACLPolicyListEntry struct {
ModifyIndex uint64
}
type ACLRolePolicyLink struct {
ID string
Name string
}
// ACLRole represents an ACL Role.
type ACLRole struct {
ID string
Name string
Description string
Policies []*ACLRolePolicyLink `json:",omitempty"`
ServiceIdentities []*ACLServiceIdentity `json:",omitempty"`
Hash []byte
CreateIndex uint64
ModifyIndex uint64
}
// BindingRuleBindType is the type of binding rule mechanism used.
type BindingRuleBindType string
const (
// BindingRuleBindTypeService binds to a service identity with the given name.
BindingRuleBindTypeService BindingRuleBindType = "service"
// BindingRuleBindTypeRole binds to pre-existing roles with the given name.
BindingRuleBindTypeRole BindingRuleBindType = "role"
)
type ACLBindingRule struct {
ID string
Description string
AuthMethod string
Selector string
BindType BindingRuleBindType
BindName string
CreateIndex uint64
ModifyIndex uint64
}
type ACLAuthMethod struct {
Name string
Type string
Description string
// Configuration is arbitrary configuration for the auth method. This
// should only contain primitive values and containers (such as lists and
// maps).
Config map[string]interface{}
CreateIndex uint64
ModifyIndex uint64
}
type ACLAuthMethodListEntry struct {
Name string
Type string
Description string
CreateIndex uint64
ModifyIndex uint64
}
// ParseKubernetesAuthMethodConfig takes a raw config map and returns a parsed
// KubernetesAuthMethodConfig.
func ParseKubernetesAuthMethodConfig(raw map[string]interface{}) (*KubernetesAuthMethodConfig, error) {
var config KubernetesAuthMethodConfig
decodeConf := &mapstructure.DecoderConfig{
Result: &config,
WeaklyTypedInput: true,
}
decoder, err := mapstructure.NewDecoder(decodeConf)
if err != nil {
return nil, err
}
if err := decoder.Decode(raw); err != nil {
return nil, fmt.Errorf("error decoding config: %s", err)
}
return &config, nil
}
// KubernetesAuthMethodConfig is the config for the built-in Consul auth method
// for Kubernetes.
type KubernetesAuthMethodConfig struct {
Host string `json:",omitempty"`
CACert string `json:",omitempty"`
ServiceAccountJWT string `json:",omitempty"`
}
// RenderToConfig converts this into a map[string]interface{} suitable for use
// in the ACLAuthMethod.Config field.
func (c *KubernetesAuthMethodConfig) RenderToConfig() map[string]interface{} {
return map[string]interface{}{
"Host": c.Host,
"CACert": c.CACert,
"ServiceAccountJWT": c.ServiceAccountJWT,
}
}
type ACLLoginParams struct {
AuthMethod string
BearerToken string
Meta map[string]string `json:",omitempty"`
}
// ACL can be used to query the ACL endpoints
type ACL struct {
c *Client
@ -460,7 +590,7 @@ func (a *ACL) PolicyCreate(policy *ACLPolicy, q *WriteOptions) (*ACLPolicy, *Wri
// existing policy ID
func (a *ACL) PolicyUpdate(policy *ACLPolicy, q *WriteOptions) (*ACLPolicy, *WriteMeta, error) {
if policy.ID == "" {
return nil, nil, fmt.Errorf("Must specify an ID in Policy Creation")
return nil, nil, fmt.Errorf("Must specify an ID in Policy Update")
}
r := a.c.newRequest("PUT", "/v1/acl/policy/"+policy.ID)
@ -586,3 +716,410 @@ func (a *ACL) RulesTranslateToken(tokenID string) (string, error) {
return string(ruleBytes), nil
}
// RoleCreate will create a new role. It is not allowed for the role parameters
// ID field to be set as this will be generated by Consul while processing the request.
func (a *ACL) RoleCreate(role *ACLRole, q *WriteOptions) (*ACLRole, *WriteMeta, error) {
if role.ID != "" {
return nil, nil, fmt.Errorf("Cannot specify an ID in Role Creation")
}
r := a.c.newRequest("PUT", "/v1/acl/role")
r.setWriteOptions(q)
r.obj = role
rtt, resp, err := requireOK(a.c.doRequest(r))
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
wm := &WriteMeta{RequestTime: rtt}
var out ACLRole
if err := decodeBody(resp, &out); err != nil {
return nil, nil, err
}
return &out, wm, nil
}
// RoleUpdate updates a role. The ID field of the role parameter must be set to an
// existing role ID
func (a *ACL) RoleUpdate(role *ACLRole, q *WriteOptions) (*ACLRole, *WriteMeta, error) {
if role.ID == "" {
return nil, nil, fmt.Errorf("Must specify an ID in Role Update")
}
r := a.c.newRequest("PUT", "/v1/acl/role/"+role.ID)
r.setWriteOptions(q)
r.obj = role
rtt, resp, err := requireOK(a.c.doRequest(r))
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
wm := &WriteMeta{RequestTime: rtt}
var out ACLRole
if err := decodeBody(resp, &out); err != nil {
return nil, nil, err
}
return &out, wm, nil
}
// RoleDelete deletes a role given its ID.
func (a *ACL) RoleDelete(roleID string, q *WriteOptions) (*WriteMeta, error) {
r := a.c.newRequest("DELETE", "/v1/acl/role/"+roleID)
r.setWriteOptions(q)
rtt, resp, err := requireOK(a.c.doRequest(r))
if err != nil {
return nil, err
}
resp.Body.Close()
wm := &WriteMeta{RequestTime: rtt}
return wm, nil
}
// RoleRead retrieves the role details (by ID). Returns nil if not found.
func (a *ACL) RoleRead(roleID string, q *QueryOptions) (*ACLRole, *QueryMeta, error) {
r := a.c.newRequest("GET", "/v1/acl/role/"+roleID)
r.setQueryOptions(q)
found, rtt, resp, err := requireNotFoundOrOK(a.c.doRequest(r))
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
qm := &QueryMeta{}
parseQueryMeta(resp, qm)
qm.RequestTime = rtt
if !found {
return nil, qm, nil
}
var out ACLRole
if err := decodeBody(resp, &out); err != nil {
return nil, nil, err
}
return &out, qm, nil
}
// RoleReadByName retrieves the role details (by name). Returns nil if not found.
func (a *ACL) RoleReadByName(roleName string, q *QueryOptions) (*ACLRole, *QueryMeta, error) {
r := a.c.newRequest("GET", "/v1/acl/role/name/"+url.QueryEscape(roleName))
r.setQueryOptions(q)
found, rtt, resp, err := requireNotFoundOrOK(a.c.doRequest(r))
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
qm := &QueryMeta{}
parseQueryMeta(resp, qm)
qm.RequestTime = rtt
if !found {
return nil, qm, nil
}
var out ACLRole
if err := decodeBody(resp, &out); err != nil {
return nil, nil, err
}
return &out, qm, nil
}
// RoleList retrieves a listing of all roles. The listing does not include some
// metadata for the role as those should be retrieved by subsequent calls to
// RoleRead.
func (a *ACL) RoleList(q *QueryOptions) ([]*ACLRole, *QueryMeta, error) {
r := a.c.newRequest("GET", "/v1/acl/roles")
r.setQueryOptions(q)
rtt, resp, err := requireOK(a.c.doRequest(r))
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
qm := &QueryMeta{}
parseQueryMeta(resp, qm)
qm.RequestTime = rtt
var entries []*ACLRole
if err := decodeBody(resp, &entries); err != nil {
return nil, nil, err
}
return entries, qm, nil
}
// AuthMethodCreate will create a new auth method.
func (a *ACL) AuthMethodCreate(method *ACLAuthMethod, q *WriteOptions) (*ACLAuthMethod, *WriteMeta, error) {
if method.Name == "" {
return nil, nil, fmt.Errorf("Must specify a Name in Auth Method Creation")
}
r := a.c.newRequest("PUT", "/v1/acl/auth-method")
r.setWriteOptions(q)
r.obj = method
rtt, resp, err := requireOK(a.c.doRequest(r))
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
wm := &WriteMeta{RequestTime: rtt}
var out ACLAuthMethod
if err := decodeBody(resp, &out); err != nil {
return nil, nil, err
}
return &out, wm, nil
}
// AuthMethodUpdate updates an auth method.
func (a *ACL) AuthMethodUpdate(method *ACLAuthMethod, q *WriteOptions) (*ACLAuthMethod, *WriteMeta, error) {
if method.Name == "" {
return nil, nil, fmt.Errorf("Must specify a Name in Auth Method Update")
}
r := a.c.newRequest("PUT", "/v1/acl/auth-method/"+url.QueryEscape(method.Name))
r.setWriteOptions(q)
r.obj = method
rtt, resp, err := requireOK(a.c.doRequest(r))
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
wm := &WriteMeta{RequestTime: rtt}
var out ACLAuthMethod
if err := decodeBody(resp, &out); err != nil {
return nil, nil, err
}
return &out, wm, nil
}
// AuthMethodDelete deletes an auth method given its Name.
func (a *ACL) AuthMethodDelete(methodName string, q *WriteOptions) (*WriteMeta, error) {
if methodName == "" {
return nil, fmt.Errorf("Must specify a Name in Auth Method Delete")
}
r := a.c.newRequest("DELETE", "/v1/acl/auth-method/"+url.QueryEscape(methodName))
r.setWriteOptions(q)
rtt, resp, err := requireOK(a.c.doRequest(r))
if err != nil {
return nil, err
}
resp.Body.Close()
wm := &WriteMeta{RequestTime: rtt}
return wm, nil
}
// AuthMethodRead retrieves the auth method. Returns nil if not found.
func (a *ACL) AuthMethodRead(methodName string, q *QueryOptions) (*ACLAuthMethod, *QueryMeta, error) {
if methodName == "" {
return nil, nil, fmt.Errorf("Must specify a Name in Auth Method Read")
}
r := a.c.newRequest("GET", "/v1/acl/auth-method/"+url.QueryEscape(methodName))
r.setQueryOptions(q)
found, rtt, resp, err := requireNotFoundOrOK(a.c.doRequest(r))
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
qm := &QueryMeta{}
parseQueryMeta(resp, qm)
qm.RequestTime = rtt
if !found {
return nil, qm, nil
}
var out ACLAuthMethod
if err := decodeBody(resp, &out); err != nil {
return nil, nil, err
}
return &out, qm, nil
}
// AuthMethodList retrieves a listing of all auth methods. The listing does not
// include some metadata for the auth method as those should be retrieved by
// subsequent calls to AuthMethodRead.
func (a *ACL) AuthMethodList(q *QueryOptions) ([]*ACLAuthMethodListEntry, *QueryMeta, error) {
r := a.c.newRequest("GET", "/v1/acl/auth-methods")
r.setQueryOptions(q)
rtt, resp, err := requireOK(a.c.doRequest(r))
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
qm := &QueryMeta{}
parseQueryMeta(resp, qm)
qm.RequestTime = rtt
var entries []*ACLAuthMethodListEntry
if err := decodeBody(resp, &entries); err != nil {
return nil, nil, err
}
return entries, qm, nil
}
// BindingRuleCreate will create a new binding rule. It is not allowed for the
// binding rule parameter's ID field to be set as this will be generated by
// Consul while processing the request.
func (a *ACL) BindingRuleCreate(rule *ACLBindingRule, q *WriteOptions) (*ACLBindingRule, *WriteMeta, error) {
if rule.ID != "" {
return nil, nil, fmt.Errorf("Cannot specify an ID in Binding Rule Creation")
}
r := a.c.newRequest("PUT", "/v1/acl/binding-rule")
r.setWriteOptions(q)
r.obj = rule
rtt, resp, err := requireOK(a.c.doRequest(r))
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
wm := &WriteMeta{RequestTime: rtt}
var out ACLBindingRule
if err := decodeBody(resp, &out); err != nil {
return nil, nil, err
}
return &out, wm, nil
}
// BindingRuleUpdate updates a binding rule. The ID field of the role binding
// rule parameter must be set to an existing binding rule ID.
func (a *ACL) BindingRuleUpdate(rule *ACLBindingRule, q *WriteOptions) (*ACLBindingRule, *WriteMeta, error) {
if rule.ID == "" {
return nil, nil, fmt.Errorf("Must specify an ID in Binding Rule Update")
}
r := a.c.newRequest("PUT", "/v1/acl/binding-rule/"+rule.ID)
r.setWriteOptions(q)
r.obj = rule
rtt, resp, err := requireOK(a.c.doRequest(r))
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
wm := &WriteMeta{RequestTime: rtt}
var out ACLBindingRule
if err := decodeBody(resp, &out); err != nil {
return nil, nil, err
}
return &out, wm, nil
}
// BindingRuleDelete deletes a binding rule given its ID.
func (a *ACL) BindingRuleDelete(bindingRuleID string, q *WriteOptions) (*WriteMeta, error) {
r := a.c.newRequest("DELETE", "/v1/acl/binding-rule/"+bindingRuleID)
r.setWriteOptions(q)
rtt, resp, err := requireOK(a.c.doRequest(r))
if err != nil {
return nil, err
}
resp.Body.Close()
wm := &WriteMeta{RequestTime: rtt}
return wm, nil
}
// BindingRuleRead retrieves the binding rule details. Returns nil if not found.
func (a *ACL) BindingRuleRead(bindingRuleID string, q *QueryOptions) (*ACLBindingRule, *QueryMeta, error) {
r := a.c.newRequest("GET", "/v1/acl/binding-rule/"+bindingRuleID)
r.setQueryOptions(q)
found, rtt, resp, err := requireNotFoundOrOK(a.c.doRequest(r))
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
qm := &QueryMeta{}
parseQueryMeta(resp, qm)
qm.RequestTime = rtt
if !found {
return nil, qm, nil
}
var out ACLBindingRule
if err := decodeBody(resp, &out); err != nil {
return nil, nil, err
}
return &out, qm, nil
}
// BindingRuleList retrieves a listing of all binding rules.
func (a *ACL) BindingRuleList(methodName string, q *QueryOptions) ([]*ACLBindingRule, *QueryMeta, error) {
r := a.c.newRequest("GET", "/v1/acl/binding-rules")
if methodName != "" {
r.params.Set("authmethod", methodName)
}
r.setQueryOptions(q)
rtt, resp, err := requireOK(a.c.doRequest(r))
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
qm := &QueryMeta{}
parseQueryMeta(resp, qm)
qm.RequestTime = rtt
var entries []*ACLBindingRule
if err := decodeBody(resp, &entries); err != nil {
return nil, nil, err
}
return entries, qm, nil
}
// Login is used to exchange auth method credentials for a newly-minted Consul Token.
func (a *ACL) Login(auth *ACLLoginParams, q *WriteOptions) (*ACLToken, *WriteMeta, error) {
r := a.c.newRequest("POST", "/v1/acl/login")
r.setWriteOptions(q)
r.obj = auth
rtt, resp, err := requireOK(a.c.doRequest(r))
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
wm := &WriteMeta{RequestTime: rtt}
var out ACLToken
if err := decodeBody(resp, &out); err != nil {
return nil, nil, err
}
return &out, wm, nil
}
// Logout is used to destroy a Consul Token created via Login().
func (a *ACL) Logout(q *WriteOptions) (*WriteMeta, error) {
r := a.c.newRequest("POST", "/v1/acl/logout")
r.setWriteOptions(q)
rtt, resp, err := requireOK(a.c.doRequest(r))
if err != nil {
return nil, err
}
resp.Body.Close()
wm := &WriteMeta{RequestTime: rtt}
return wm, nil
}

View File

@ -30,6 +30,10 @@ const (
// the HTTP token.
HTTPTokenEnvName = "CONSUL_HTTP_TOKEN"
// HTTPTokenFileEnvName defines an environment variable name which sets
// the HTTP token file.
HTTPTokenFileEnvName = "CONSUL_HTTP_TOKEN_FILE"
// HTTPAuthEnvName defines an environment variable name which sets
// the HTTP authentication header.
HTTPAuthEnvName = "CONSUL_HTTP_AUTH"
@ -280,6 +284,10 @@ type Config struct {
// which overrides the agent's default token.
Token string
// TokenFile is a file containing the current token to use for this client.
// If provided it is read once at startup and never again.
TokenFile string
TLSConfig TLSConfig
}
@ -343,6 +351,10 @@ func defaultConfig(transportFn func() *http.Transport) *Config {
config.Address = addr
}
if tokenFile := os.Getenv(HTTPTokenFileEnvName); tokenFile != "" {
config.TokenFile = tokenFile
}
if token := os.Getenv(HTTPTokenEnvName); token != "" {
config.Token = token
}
@ -449,6 +461,7 @@ func (c *Config) GenerateEnv() []string {
env = append(env,
fmt.Sprintf("%s=%s", HTTPAddrEnvName, c.Address),
fmt.Sprintf("%s=%s", HTTPTokenEnvName, c.Token),
fmt.Sprintf("%s=%s", HTTPTokenFileEnvName, c.TokenFile),
fmt.Sprintf("%s=%t", HTTPSSLEnvName, c.Scheme == "https"),
fmt.Sprintf("%s=%s", HTTPCAFile, c.TLSConfig.CAFile),
fmt.Sprintf("%s=%s", HTTPCAPath, c.TLSConfig.CAPath),
@ -541,6 +554,19 @@ func NewClient(config *Config) (*Client, error) {
config.Address = parts[1]
}
// If the TokenFile is set, always use that, even if a Token is configured.
// This is because when TokenFile is set it is read into the Token field.
// We want any derived clients to have to re-read the token file.
if config.TokenFile != "" {
data, err := ioutil.ReadFile(config.TokenFile)
if err != nil {
return nil, fmt.Errorf("Error loading token file: %s", err)
}
if token := strings.TrimSpace(string(data)); token != "" {
config.Token = token
}
}
if config.Token == "" {
config.Token = defConfig.Token
}
@ -820,6 +846,8 @@ func (c *Client) write(endpoint string, in, out interface{}, q *WriteOptions) (*
}
// parseQueryMeta is used to help parse query meta-data
//
// TODO(rb): bug? the error from this function is never handled
func parseQueryMeta(resp *http.Response, q *QueryMeta) error {
header := resp.Header
@ -897,10 +925,7 @@ func requireOK(d time.Duration, resp *http.Response, e error) (time.Duration, *h
return d, nil, e
}
if resp.StatusCode != 200 {
var buf bytes.Buffer
io.Copy(&buf, resp.Body)
resp.Body.Close()
return d, nil, fmt.Errorf("Unexpected response code: %d (%s)", resp.StatusCode, buf.Bytes())
return d, nil, generateUnexpectedResponseCodeError(resp)
}
return d, resp, nil
}
@ -912,3 +937,30 @@ func (req *request) filterQuery(filter string) {
req.params.Set("filter", filter)
}
// generateUnexpectedResponseCodeError consumes the rest of the body, closes
// the body stream and generates an error indicating the status code was
// unexpected.
func generateUnexpectedResponseCodeError(resp *http.Response) error {
var buf bytes.Buffer
io.Copy(&buf, resp.Body)
resp.Body.Close()
return fmt.Errorf("Unexpected response code: %d (%s)", resp.StatusCode, buf.Bytes())
}
func requireNotFoundOrOK(d time.Duration, resp *http.Response, e error) (bool, time.Duration, *http.Response, error) {
if e != nil {
if resp != nil {
resp.Body.Close()
}
return false, d, nil, e
}
switch resp.StatusCode {
case 200:
return true, d, resp, nil
case 404:
return false, d, resp, nil
default:
return false, d, nil, generateUnexpectedResponseCodeError(resp)
}
}

View File

@ -875,9 +875,10 @@ func TestAPI_GenerateEnv(t *testing.T) {
t.Parallel()
c := &Config{
Address: "127.0.0.1:8500",
Token: "test",
Scheme: "http",
Address: "127.0.0.1:8500",
Token: "test",
TokenFile: "test.file",
Scheme: "http",
TLSConfig: TLSConfig{
CAFile: "",
CAPath: "",
@ -891,6 +892,7 @@ func TestAPI_GenerateEnv(t *testing.T) {
expected := []string{
"CONSUL_HTTP_ADDR=127.0.0.1:8500",
"CONSUL_HTTP_TOKEN=test",
"CONSUL_HTTP_TOKEN_FILE=test.file",
"CONSUL_HTTP_SSL=false",
"CONSUL_CACERT=",
"CONSUL_CAPATH=",
@ -908,9 +910,10 @@ func TestAPI_GenerateEnvHTTPS(t *testing.T) {
t.Parallel()
c := &Config{
Address: "127.0.0.1:8500",
Token: "test",
Scheme: "https",
Address: "127.0.0.1:8500",
Token: "test",
TokenFile: "test.file",
Scheme: "https",
TLSConfig: TLSConfig{
CAFile: "/var/consul/ca.crt",
CAPath: "/var/consul/ca.dir",
@ -928,6 +931,7 @@ func TestAPI_GenerateEnvHTTPS(t *testing.T) {
expected := []string{
"CONSUL_HTTP_ADDR=127.0.0.1:8500",
"CONSUL_HTTP_TOKEN=test",
"CONSUL_HTTP_TOKEN_FILE=test.file",
"CONSUL_HTTP_SSL=true",
"CONSUL_CACERT=/var/consul/ca.crt",
"CONSUL_CAPATH=/var/consul/ca.dir",

View File

@ -1,6 +1,7 @@
package acl
import (
"encoding/json"
"fmt"
"strings"
@ -10,19 +11,40 @@ import (
)
func PrintToken(token *api.ACLToken, ui cli.Ui, showMeta bool) {
ui.Info(fmt.Sprintf("AccessorID: %s", token.AccessorID))
ui.Info(fmt.Sprintf("SecretID: %s", token.SecretID))
ui.Info(fmt.Sprintf("Description: %s", token.Description))
ui.Info(fmt.Sprintf("Local: %t", token.Local))
ui.Info(fmt.Sprintf("Create Time: %v", token.CreateTime))
if showMeta {
ui.Info(fmt.Sprintf("Hash: %x", token.Hash))
ui.Info(fmt.Sprintf("Create Index: %d", token.CreateIndex))
ui.Info(fmt.Sprintf("Modify Index: %d", token.ModifyIndex))
ui.Info(fmt.Sprintf("AccessorID: %s", token.AccessorID))
ui.Info(fmt.Sprintf("SecretID: %s", token.SecretID))
ui.Info(fmt.Sprintf("Description: %s", token.Description))
ui.Info(fmt.Sprintf("Local: %t", token.Local))
ui.Info(fmt.Sprintf("Create Time: %v", token.CreateTime))
if token.ExpirationTime != nil && !token.ExpirationTime.IsZero() {
ui.Info(fmt.Sprintf("Expiration Time: %v", *token.ExpirationTime))
}
ui.Info(fmt.Sprintf("Policies:"))
for _, policy := range token.Policies {
ui.Info(fmt.Sprintf(" %s - %s", policy.ID, policy.Name))
if showMeta {
ui.Info(fmt.Sprintf("Hash: %x", token.Hash))
ui.Info(fmt.Sprintf("Create Index: %d", token.CreateIndex))
ui.Info(fmt.Sprintf("Modify Index: %d", token.ModifyIndex))
}
if len(token.Policies) > 0 {
ui.Info(fmt.Sprintf("Policies:"))
for _, policy := range token.Policies {
ui.Info(fmt.Sprintf(" %s - %s", policy.ID, policy.Name))
}
}
if len(token.Roles) > 0 {
ui.Info(fmt.Sprintf("Roles:"))
for _, role := range token.Roles {
ui.Info(fmt.Sprintf(" %s - %s", role.ID, role.Name))
}
}
if len(token.ServiceIdentities) > 0 {
ui.Info(fmt.Sprintf("Service Identities:"))
for _, svcid := range token.ServiceIdentities {
if len(svcid.Datacenters) > 0 {
ui.Info(fmt.Sprintf(" %s (Datacenters: %s)", svcid.ServiceName, strings.Join(svcid.Datacenters, ", ")))
} else {
ui.Info(fmt.Sprintf(" %s (Datacenters: all)", svcid.ServiceName))
}
}
}
if token.Rules != "" {
ui.Info(fmt.Sprintf("Rules:"))
@ -31,19 +53,40 @@ func PrintToken(token *api.ACLToken, ui cli.Ui, showMeta bool) {
}
func PrintTokenListEntry(token *api.ACLTokenListEntry, ui cli.Ui, showMeta bool) {
ui.Info(fmt.Sprintf("AccessorID: %s", token.AccessorID))
ui.Info(fmt.Sprintf("Description: %s", token.Description))
ui.Info(fmt.Sprintf("Local: %t", token.Local))
ui.Info(fmt.Sprintf("Create Time: %v", token.CreateTime))
ui.Info(fmt.Sprintf("Legacy: %t", token.Legacy))
if showMeta {
ui.Info(fmt.Sprintf("Hash: %x", token.Hash))
ui.Info(fmt.Sprintf("Create Index: %d", token.CreateIndex))
ui.Info(fmt.Sprintf("Modify Index: %d", token.ModifyIndex))
ui.Info(fmt.Sprintf("AccessorID: %s", token.AccessorID))
ui.Info(fmt.Sprintf("Description: %s", token.Description))
ui.Info(fmt.Sprintf("Local: %t", token.Local))
ui.Info(fmt.Sprintf("Create Time: %v", token.CreateTime))
if token.ExpirationTime != nil && !token.ExpirationTime.IsZero() {
ui.Info(fmt.Sprintf("Expiration Time: %v", *token.ExpirationTime))
}
ui.Info(fmt.Sprintf("Policies:"))
for _, policy := range token.Policies {
ui.Info(fmt.Sprintf(" %s - %s", policy.ID, policy.Name))
ui.Info(fmt.Sprintf("Legacy: %t", token.Legacy))
if showMeta {
ui.Info(fmt.Sprintf("Hash: %x", token.Hash))
ui.Info(fmt.Sprintf("Create Index: %d", token.CreateIndex))
ui.Info(fmt.Sprintf("Modify Index: %d", token.ModifyIndex))
}
if len(token.Policies) > 0 {
ui.Info(fmt.Sprintf("Policies:"))
for _, policy := range token.Policies {
ui.Info(fmt.Sprintf(" %s - %s", policy.ID, policy.Name))
}
}
if len(token.Roles) > 0 {
ui.Info(fmt.Sprintf("Roles:"))
for _, role := range token.Roles {
ui.Info(fmt.Sprintf(" %s - %s", role.ID, role.Name))
}
}
if len(token.ServiceIdentities) > 0 {
ui.Info(fmt.Sprintf("Service Identities:"))
for _, svcid := range token.ServiceIdentities {
if len(svcid.Datacenters) > 0 {
ui.Info(fmt.Sprintf(" %s (Datacenters: %s)", svcid.ServiceName, strings.Join(svcid.Datacenters, ", ")))
} else {
ui.Info(fmt.Sprintf(" %s (Datacenters: all)", svcid.ServiceName))
}
}
}
}
@ -73,6 +116,112 @@ func PrintPolicyListEntry(policy *api.ACLPolicyListEntry, ui cli.Ui, showMeta bo
}
}
func PrintRole(role *api.ACLRole, ui cli.Ui, showMeta bool) {
ui.Info(fmt.Sprintf("ID: %s", role.ID))
ui.Info(fmt.Sprintf("Name: %s", role.Name))
ui.Info(fmt.Sprintf("Description: %s", role.Description))
if showMeta {
ui.Info(fmt.Sprintf("Hash: %x", role.Hash))
ui.Info(fmt.Sprintf("Create Index: %d", role.CreateIndex))
ui.Info(fmt.Sprintf("Modify Index: %d", role.ModifyIndex))
}
if len(role.Policies) > 0 {
ui.Info(fmt.Sprintf("Policies:"))
for _, policy := range role.Policies {
ui.Info(fmt.Sprintf(" %s - %s", policy.ID, policy.Name))
}
}
if len(role.ServiceIdentities) > 0 {
ui.Info(fmt.Sprintf("Service Identities:"))
for _, svcid := range role.ServiceIdentities {
if len(svcid.Datacenters) > 0 {
ui.Info(fmt.Sprintf(" %s (Datacenters: %s)", svcid.ServiceName, strings.Join(svcid.Datacenters, ", ")))
} else {
ui.Info(fmt.Sprintf(" %s (Datacenters: all)", svcid.ServiceName))
}
}
}
}
func PrintRoleListEntry(role *api.ACLRole, ui cli.Ui, showMeta bool) {
ui.Info(fmt.Sprintf("%s:", role.Name))
ui.Info(fmt.Sprintf(" ID: %s", role.ID))
ui.Info(fmt.Sprintf(" Description: %s", role.Description))
if showMeta {
ui.Info(fmt.Sprintf(" Hash: %x", role.Hash))
ui.Info(fmt.Sprintf(" Create Index: %d", role.CreateIndex))
ui.Info(fmt.Sprintf(" Modify Index: %d", role.ModifyIndex))
}
if len(role.Policies) > 0 {
ui.Info(fmt.Sprintf(" Policies:"))
for _, policy := range role.Policies {
ui.Info(fmt.Sprintf(" %s - %s", policy.ID, policy.Name))
}
}
if len(role.ServiceIdentities) > 0 {
ui.Info(fmt.Sprintf(" Service Identities:"))
for _, svcid := range role.ServiceIdentities {
if len(svcid.Datacenters) > 0 {
ui.Info(fmt.Sprintf(" %s (Datacenters: %s)", svcid.ServiceName, strings.Join(svcid.Datacenters, ", ")))
} else {
ui.Info(fmt.Sprintf(" %s (Datacenters: all)", svcid.ServiceName))
}
}
}
}
func PrintAuthMethod(method *api.ACLAuthMethod, ui cli.Ui, showMeta bool) {
ui.Info(fmt.Sprintf("Name: %s", method.Name))
ui.Info(fmt.Sprintf("Type: %s", method.Type))
ui.Info(fmt.Sprintf("Description: %s", method.Description))
if showMeta {
ui.Info(fmt.Sprintf("Create Index: %d", method.CreateIndex))
ui.Info(fmt.Sprintf("Modify Index: %d", method.ModifyIndex))
}
ui.Info(fmt.Sprintf("Config:"))
output, err := json.MarshalIndent(method.Config, "", " ")
if err != nil {
ui.Error(fmt.Sprintf("Error formatting auth method configuration: %s", err))
}
ui.Output(string(output))
}
func PrintAuthMethodListEntry(method *api.ACLAuthMethodListEntry, ui cli.Ui, showMeta bool) {
ui.Info(fmt.Sprintf("%s:", method.Name))
ui.Info(fmt.Sprintf(" Type: %s", method.Type))
ui.Info(fmt.Sprintf(" Description: %s", method.Description))
if showMeta {
ui.Info(fmt.Sprintf(" Create Index: %d", method.CreateIndex))
ui.Info(fmt.Sprintf(" Modify Index: %d", method.ModifyIndex))
}
}
func PrintBindingRule(rule *api.ACLBindingRule, ui cli.Ui, showMeta bool) {
ui.Info(fmt.Sprintf("ID: %s", rule.ID))
ui.Info(fmt.Sprintf("AuthMethod: %s", rule.AuthMethod))
ui.Info(fmt.Sprintf("Description: %s", rule.Description))
ui.Info(fmt.Sprintf("BindType: %s", rule.BindType))
ui.Info(fmt.Sprintf("BindName: %s", rule.BindName))
ui.Info(fmt.Sprintf("Selector: %s", rule.Selector))
if showMeta {
ui.Info(fmt.Sprintf("Create Index: %d", rule.CreateIndex))
ui.Info(fmt.Sprintf("Modify Index: %d", rule.ModifyIndex))
}
}
func PrintBindingRuleListEntry(rule *api.ACLBindingRule, ui cli.Ui, showMeta bool) {
ui.Info(fmt.Sprintf("%s:", rule.ID))
ui.Info(fmt.Sprintf(" AuthMethod: %s", rule.AuthMethod))
ui.Info(fmt.Sprintf(" Description: %s", rule.Description))
ui.Info(fmt.Sprintf(" BindType: %s", rule.BindType))
ui.Info(fmt.Sprintf(" BindName: %s", rule.BindName))
ui.Info(fmt.Sprintf(" Selector: %s", rule.Selector))
if showMeta {
ui.Info(fmt.Sprintf(" Create Index: %d", rule.CreateIndex))
ui.Info(fmt.Sprintf(" Modify Index: %d", rule.ModifyIndex))
}
}
func GetTokenIDFromPartial(client *api.Client, partialID string) (string, error) {
if partialID == "anonymous" {
return structs.ACLTokenAnonymousID, nil
@ -185,3 +334,123 @@ func GetRulesFromLegacyToken(client *api.Client, tokenID string, isSecret bool)
return token.Rules, nil
}
func GetRoleIDFromPartial(client *api.Client, partialID string) (string, error) {
// the full UUID string was given
if len(partialID) == 36 {
return partialID, nil
}
roles, _, err := client.ACL().RoleList(nil)
if err != nil {
return "", err
}
roleID := ""
for _, role := range roles {
if strings.HasPrefix(role.ID, partialID) {
if roleID != "" {
return "", fmt.Errorf("Partial role ID is not unique")
}
roleID = role.ID
}
}
if roleID == "" {
return "", fmt.Errorf("No such role ID with prefix: %s", partialID)
}
return roleID, nil
}
func GetRoleIDByName(client *api.Client, name string) (string, error) {
if name == "" {
return "", fmt.Errorf("No name specified")
}
roles, _, err := client.ACL().RoleList(nil)
if err != nil {
return "", err
}
for _, role := range roles {
if role.Name == name {
return role.ID, nil
}
}
return "", fmt.Errorf("No such role with name %s", name)
}
func GetBindingRuleIDFromPartial(client *api.Client, partialID string) (string, error) {
// the full UUID string was given
if len(partialID) == 36 {
return partialID, nil
}
rules, _, err := client.ACL().BindingRuleList("", nil)
if err != nil {
return "", err
}
ruleID := ""
for _, rule := range rules {
if strings.HasPrefix(rule.ID, partialID) {
if ruleID != "" {
return "", fmt.Errorf("Partial rule ID is not unique")
}
ruleID = rule.ID
}
}
if ruleID == "" {
return "", fmt.Errorf("No such rule ID with prefix: %s", partialID)
}
return ruleID, nil
}
func ExtractServiceIdentities(serviceIdents []string) ([]*api.ACLServiceIdentity, error) {
var out []*api.ACLServiceIdentity
for _, svcidRaw := range serviceIdents {
parts := strings.Split(svcidRaw, ":")
switch len(parts) {
case 2:
out = append(out, &api.ACLServiceIdentity{
ServiceName: parts[0],
Datacenters: strings.Split(parts[1], ","),
})
case 1:
out = append(out, &api.ACLServiceIdentity{
ServiceName: parts[0],
})
default:
return nil, fmt.Errorf("Malformed -service-identity argument: %q", svcidRaw)
}
}
return out, nil
}
// TestKubernetesJWT_A is a valid service account jwt extracted from a minikube setup.
//
// {
// "iss": "kubernetes/serviceaccount",
// "kubernetes.io/serviceaccount/namespace": "default",
// "kubernetes.io/serviceaccount/secret.name": "admin-token-qlz42",
// "kubernetes.io/serviceaccount/service-account.name": "admin",
// "kubernetes.io/serviceaccount/service-account.uid": "738bc251-6532-11e9-b67f-48e6c8b8ecb5",
// "sub": "system:serviceaccount:default:admin"
// }
const TestKubernetesJWT_A = "eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImFkbWluLXRva2VuLXFsejQyIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImFkbWluIiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQudWlkIjoiNzM4YmMyNTEtNjUzMi0xMWU5LWI2N2YtNDhlNmM4YjhlY2I1Iiwic3ViIjoic3lzdGVtOnNlcnZpY2VhY2NvdW50OmRlZmF1bHQ6YWRtaW4ifQ.ixMlnWrAG7NVuTTKu8cdcYfM7gweS3jlKaEsIBNGOVEjPE7rtXtgMkAwjQTdYR08_0QBjkgzy5fQC5ZNyglSwONJ-bPaXGvhoH1cTnRi1dz9H_63CfqOCvQP1sbdkMeRxNTGVAyWZT76rXoCUIfHP4LY2I8aab0KN9FTIcgZRF0XPTtT70UwGIrSmRpxW38zjiy2ymWL01cc5VWGhJqVysmWmYk3wNp0h5N57H_MOrz4apQR4pKaamzskzjLxO55gpbmZFC76qWuUdexAR7DT2fpbHLOw90atN_NlLMY-VrXyW3-Ei5EhYaVreMB9PSpKwkrA4jULITohV-sxpa1LA"
// TestKubernetesJWT_B is a valid service account jwt extracted from a minikube setup.
//
// {
// "iss": "kubernetes/serviceaccount",
// "kubernetes.io/serviceaccount/namespace": "default",
// "kubernetes.io/serviceaccount/secret.name": "demo-token-kmb9n",
// "kubernetes.io/serviceaccount/service-account.name": "demo",
// "kubernetes.io/serviceaccount/service-account.uid": "76091af4-4b56-11e9-ac4b-708b11801cbe",
// "sub": "system:serviceaccount:default:demo"
// }
const TestKubernetesJWT_B = "eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6ImRlbW8tdG9rZW4ta21iOW4iLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoiZGVtbyIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6Ijc2MDkxYWY0LTRiNTYtMTFlOS1hYzRiLTcwOGIxMTgwMWNiZSIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OmRlbW8ifQ.ZiAHjijBAOsKdum0Aix6lgtkLkGo9_Tu87dWQ5Zfwnn3r2FejEWDAnftTft1MqqnMzivZ9Wyyki5ZjQRmTAtnMPJuHC-iivqY4Wh4S6QWCJ1SivBv5tMZR79t5t8mE7R1-OHwst46spru1pps9wt9jsA04d3LpV0eeKYgdPTVaQKklxTm397kIMUugA6yINIBQ3Rh8eQqBgNwEmL4iqyYubzHLVkGkoP9MJikFI05vfRiHtYr-piXz6JFDzXMQj9rW6xtMmrBSn79ChbyvC5nz-Nj2rJPnHsb_0rDUbmXY5PpnMhBpdSH-CbZ4j8jsiib6DtaGJhVZeEQ1GjsFAZwQ"

View File

@ -0,0 +1,64 @@
package authmethod
import (
"github.com/hashicorp/consul/command/flags"
"github.com/mitchellh/cli"
)
func New() *cmd {
return &cmd{}
}
type cmd struct{}
func (c *cmd) Run(args []string) int {
return cli.RunResultHelp
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return flags.Usage(help, nil)
}
const synopsis = "Manage Consul's ACL Auth Methods"
const help = `
Usage: consul acl auth-method <subcommand> [options] [args]
This command has subcommands for managing Consul's ACL Auth Methods.
Here are some simple examples, and more detailed examples are available in
the subcommands or the documentation.
Create a new auth method:
$ consul acl auth-method create -type "kubernetes" \
-name "my-k8s" \
-description "This is an example kube auth method" \
-kubernetes-host "https://apiserver.example.com:8443" \
-kubernetes-ca-file /path/to/kube.ca.crt \
-kubernetes-service-account-jwt "JWT_CONTENTS"
List all auth methods:
$ consul acl auth-method list
Update all editable fields of the auth method:
$ consul acl auth-method update -name "my-k8s" \
-description "new description" \
-kubernetes-host "https://new-apiserver.example.com:8443" \
-kubernetes-ca-file /path/to/new-kube.ca.crt \
-kubernetes-service-account-jwt "NEW_JWT_CONTENTS"
Read an auth method:
$ consul acl auth-method read -name my-k8s
Delete an auth method:
$ consul acl auth-method delete -name my-k8s
For more examples, ask for subcommand help or view the documentation.
`

View File

@ -0,0 +1,186 @@
package authmethodcreate
import (
"flag"
"fmt"
"io"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/acl"
"github.com/hashicorp/consul/command/flags"
"github.com/hashicorp/consul/command/helpers"
"github.com/mitchellh/cli"
)
func New(ui cli.Ui) *cmd {
c := &cmd{UI: ui}
c.init()
return c
}
type cmd struct {
UI cli.Ui
flags *flag.FlagSet
http *flags.HTTPFlags
help string
authMethodType string
name string
description string
k8sHost string
k8sCACert string
k8sServiceAccountJWT string
showMeta bool
testStdin io.Reader
}
func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.flags.BoolVar(
&c.showMeta,
"meta",
false,
"Indicates that auth method metadata such "+
"as the content hash and raft indices should be shown for each entry.",
)
c.flags.StringVar(
&c.authMethodType,
"type",
"",
"The new auth method's type. This flag is required.",
)
c.flags.StringVar(
&c.name,
"name",
"",
"The new auth method's name. This flag is required.",
)
c.flags.StringVar(
&c.description,
"description",
"",
"A description of the auth method.",
)
c.flags.StringVar(
&c.k8sHost,
"kubernetes-host",
"",
"Address of the Kubernetes API server. This flag is required for type=kubernetes.",
)
c.flags.StringVar(
&c.k8sCACert,
"kubernetes-ca-cert",
"",
"PEM encoded CA cert for use by the TLS client used to talk with the "+
"Kubernetes API. May be prefixed with '@' to indicate that the "+
"value is a file path to load the cert from. "+
"This flag is required for type=kubernetes.",
)
c.flags.StringVar(
&c.k8sServiceAccountJWT,
"kubernetes-service-account-jwt",
"",
"A kubernetes service account JWT used to access the TokenReview API to "+
"validate other JWTs during login. "+
"This flag is required for type=kubernetes.",
)
c.http = &flags.HTTPFlags{}
flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.ServerFlags())
c.help = flags.Usage(help, c.flags)
}
func (c *cmd) Run(args []string) int {
if err := c.flags.Parse(args); err != nil {
return 1
}
if c.authMethodType == "" {
c.UI.Error(fmt.Sprintf("Missing required '-type' flag"))
c.UI.Error(c.Help())
return 1
} else if c.name == "" {
c.UI.Error(fmt.Sprintf("Missing required '-name' flag"))
c.UI.Error(c.Help())
return 1
}
client, err := c.http.APIClient()
if err != nil {
c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
newAuthMethod := &api.ACLAuthMethod{
Type: c.authMethodType,
Name: c.name,
Description: c.description,
}
if c.authMethodType == "kubernetes" {
if c.k8sHost == "" {
c.UI.Error(fmt.Sprintf("Missing required '-kubernetes-host' flag"))
return 1
} else if c.k8sCACert == "" {
c.UI.Error(fmt.Sprintf("Missing required '-kubernetes-ca-cert' flag"))
return 1
} else if c.k8sServiceAccountJWT == "" {
c.UI.Error(fmt.Sprintf("Missing required '-kubernetes-service-account-jwt' flag"))
return 1
}
c.k8sCACert, err = helpers.LoadDataSource(c.k8sCACert, c.testStdin)
if err != nil {
c.UI.Error(fmt.Sprintf("Invalid '-kubernetes-ca-cert' value: %v", err))
return 1
} else if c.k8sCACert == "" {
c.UI.Error(fmt.Sprintf("Kubernetes CA Cert is empty"))
return 1
}
newAuthMethod.Config = map[string]interface{}{
"Host": c.k8sHost,
"CACert": c.k8sCACert,
"ServiceAccountJWT": c.k8sServiceAccountJWT,
}
}
method, _, err := client.ACL().AuthMethodCreate(newAuthMethod, nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to create new auth method: %v", err))
return 1
}
acl.PrintAuthMethod(method, c.UI, c.showMeta)
return 0
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return flags.Usage(c.help, nil)
}
const synopsis = "Create an ACL Auth Method"
const help = `
Usage: consul acl auth-method create -name NAME -type TYPE [options]
Create a new auth method:
$ consul acl auth-method create -type "kubernetes" \
-name "my-k8s" \
-description "This is an example kube method" \
-kubernetes-host "https://apiserver.example.com:8443" \
-kubernetes-ca-file /path/to/kube.ca.crt \
-kubernetes-service-account-jwt "JWT_CONTENTS"
`

View File

@ -0,0 +1,226 @@
package authmethodcreate
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/command/acl"
"github.com/hashicorp/consul/logger"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/testrpc"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
// activate testing auth method
_ "github.com/hashicorp/consul/agent/consul/authmethod/testauth"
)
func TestAuthMethodCreateCommand_noTabs(t *testing.T) {
t.Parallel()
if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') {
t.Fatal("help has tabs")
}
}
func TestAuthMethodCreateCommand(t *testing.T) {
t.Parallel()
testDir := testutil.TempDir(t, "acl")
defer os.RemoveAll(testDir)
a := agent.NewTestAgent(t, t.Name(), `
primary_datacenter = "dc1"
acl {
enabled = true
tokens {
master = "root"
}
}`)
a.Agent.LogWriter = logger.NewLogWriter(512)
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
t.Run("type required", func(t *testing.T) {
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Missing required '-type' flag")
})
t.Run("name required", func(t *testing.T) {
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-type=testing",
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Missing required '-name' flag")
})
t.Run("invalid type", func(t *testing.T) {
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-type=invalid",
"-name=my-method",
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Invalid Auth Method: Type should be one of")
})
t.Run("create testing", func(t *testing.T) {
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-type=testing",
"-name=test",
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
})
}
func TestAuthMethodCreateCommand_k8s(t *testing.T) {
t.Parallel()
testDir := testutil.TempDir(t, "acl")
defer os.RemoveAll(testDir)
a := agent.NewTestAgent(t, t.Name(), `
primary_datacenter = "dc1"
acl {
enabled = true
tokens {
master = "root"
}
}`)
a.Agent.LogWriter = logger.NewLogWriter(512)
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
t.Run("k8s host required", func(t *testing.T) {
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-type=kubernetes",
"-name=k8s",
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Missing required '-kubernetes-host' flag")
})
t.Run("k8s ca cert required", func(t *testing.T) {
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-type=kubernetes",
"-name=k8s",
"-kubernetes-host=https://foo.internal:8443",
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Missing required '-kubernetes-ca-cert' flag")
})
ca := connect.TestCA(t, nil)
t.Run("k8s jwt required", func(t *testing.T) {
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-type=kubernetes",
"-name=k8s",
"-kubernetes-host=https://foo.internal:8443",
"-kubernetes-ca-cert", ca.RootCert,
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Missing required '-kubernetes-service-account-jwt' flag")
})
t.Run("create k8s", func(t *testing.T) {
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-type=kubernetes",
"-name=k8s",
"-kubernetes-host", "https://foo.internal:8443",
"-kubernetes-ca-cert", ca.RootCert,
"-kubernetes-service-account-jwt", acl.TestKubernetesJWT_A,
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
})
caFile := filepath.Join(testDir, "ca.crt")
require.NoError(t, ioutil.WriteFile(caFile, []byte(ca.RootCert), 0600))
t.Run("create k8s with cert file", func(t *testing.T) {
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-type=kubernetes",
"-name=k8s",
"-kubernetes-host", "https://foo.internal:8443",
"-kubernetes-ca-cert", "@" + caFile,
"-kubernetes-service-account-jwt", acl.TestKubernetesJWT_A,
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
})
}

View File

@ -0,0 +1,82 @@
package authmethoddelete
import (
"flag"
"fmt"
"github.com/hashicorp/consul/command/flags"
"github.com/mitchellh/cli"
)
func New(ui cli.Ui) *cmd {
c := &cmd{UI: ui}
c.init()
return c
}
type cmd struct {
UI cli.Ui
flags *flag.FlagSet
http *flags.HTTPFlags
help string
name string
}
func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.flags.StringVar(
&c.name,
"name",
"",
"The name of the auth method to delete.",
)
c.http = &flags.HTTPFlags{}
flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.ServerFlags())
c.help = flags.Usage(help, c.flags)
}
func (c *cmd) Run(args []string) int {
if err := c.flags.Parse(args); err != nil {
return 1
}
if c.name == "" {
c.UI.Error(fmt.Sprintf("Must specify the -name parameter"))
return 1
}
client, err := c.http.APIClient()
if err != nil {
c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
if _, err := client.ACL().AuthMethodDelete(c.name, nil); err != nil {
c.UI.Error(fmt.Sprintf("Error deleting auth method %q: %v", c.name, err))
return 1
}
c.UI.Info(fmt.Sprintf("Auth method %q deleted successfully", c.name))
return 0
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return flags.Usage(c.help, nil)
}
const synopsis = "Delete an ACL Auth Method"
const help = `
Usage: consul acl auth-method delete -name NAME [options]
Delete an auth method:
$ consul acl auth-method delete -name "my-auth-method"
`

View File

@ -0,0 +1,131 @@
package authmethoddelete
import (
"fmt"
"os"
"strings"
"testing"
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/logger"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/testrpc"
"github.com/hashicorp/go-uuid"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
// activate testing auth method
_ "github.com/hashicorp/consul/agent/consul/authmethod/testauth"
)
func TestAuthMethodDeleteCommand_noTabs(t *testing.T) {
t.Parallel()
if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') {
t.Fatal("help has tabs")
}
}
func TestAuthMethodDeleteCommand(t *testing.T) {
t.Parallel()
testDir := testutil.TempDir(t, "acl")
defer os.RemoveAll(testDir)
a := agent.NewTestAgent(t, t.Name(), `
primary_datacenter = "dc1"
acl {
enabled = true
tokens {
master = "root"
}
}`)
a.Agent.LogWriter = logger.NewLogWriter(512)
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
client := a.Client()
t.Run("name required", func(t *testing.T) {
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
}
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Must specify the -name parameter")
})
t.Run("delete notfound", func(t *testing.T) {
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-name=notfound",
}
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
output := ui.OutputWriter.String()
require.Contains(t, output, fmt.Sprintf("deleted successfully"))
require.Contains(t, output, "notfound")
})
createAuthMethod := func(t *testing.T) string {
id, err := uuid.GenerateUUID()
require.NoError(t, err)
methodName := "test-" + id
_, _, err = client.ACL().AuthMethodCreate(
&api.ACLAuthMethod{
Name: methodName,
Type: "testing",
Description: "test",
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
return methodName
}
t.Run("delete works", func(t *testing.T) {
name := createAuthMethod(t)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-name=" + name,
}
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
output := ui.OutputWriter.String()
require.Contains(t, output, fmt.Sprintf("deleted successfully"))
require.Contains(t, output, name)
method, _, err := client.ACL().AuthMethodRead(
name,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.Nil(t, method)
})
}

View File

@ -0,0 +1,83 @@
package authmethodlist
import (
"flag"
"fmt"
"github.com/hashicorp/consul/command/acl"
"github.com/hashicorp/consul/command/flags"
"github.com/mitchellh/cli"
)
func New(ui cli.Ui) *cmd {
c := &cmd{UI: ui}
c.init()
return c
}
type cmd struct {
UI cli.Ui
flags *flag.FlagSet
http *flags.HTTPFlags
help string
showMeta bool
}
func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.flags.BoolVar(
&c.showMeta,
"meta",
false,
"Indicates that auth method metadata such "+
"as the content hash and raft indices should be shown for each entry.",
)
c.http = &flags.HTTPFlags{}
flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.ServerFlags())
c.help = flags.Usage(help, c.flags)
}
func (c *cmd) Run(args []string) int {
if err := c.flags.Parse(args); err != nil {
return 1
}
client, err := c.http.APIClient()
if err != nil {
c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
methods, _, err := client.ACL().AuthMethodList(nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to retrieve the auth method list: %v", err))
return 1
}
for _, method := range methods {
acl.PrintAuthMethodListEntry(method, c.UI, c.showMeta)
}
return 0
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return flags.Usage(c.help, nil)
}
const synopsis = "Lists ACL Auth Methods"
const help = `
Usage: consul acl auth-method list [options]
List all auth methods:
$ consul acl auth-method list
`

View File

@ -0,0 +1,109 @@
package authmethodlist
import (
"os"
"strings"
"testing"
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/logger"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/testrpc"
"github.com/hashicorp/go-uuid"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
// activate testing auth method
_ "github.com/hashicorp/consul/agent/consul/authmethod/testauth"
)
func TestAuthMethodListCommand_noTabs(t *testing.T) {
t.Parallel()
if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') {
t.Fatal("help has tabs")
}
}
func TestAuthMethodListCommand(t *testing.T) {
t.Parallel()
testDir := testutil.TempDir(t, "acl")
defer os.RemoveAll(testDir)
a := agent.NewTestAgent(t, t.Name(), `
primary_datacenter = "dc1"
acl {
enabled = true
tokens {
master = "root"
}
}`)
a.Agent.LogWriter = logger.NewLogWriter(512)
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
t.Run("found none", func(t *testing.T) {
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
}
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
require.Empty(t, ui.OutputWriter.String())
})
client := a.Client()
createAuthMethod := func(t *testing.T) string {
id, err := uuid.GenerateUUID()
require.NoError(t, err)
methodName := "test-" + id
_, _, err = client.ACL().AuthMethodCreate(
&api.ACLAuthMethod{
Name: methodName,
Type: "testing",
Description: "test",
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
return methodName
}
var methodNames []string
for i := 0; i < 5; i++ {
methodName := createAuthMethod(t)
methodNames = append(methodNames, methodName)
}
t.Run("found some", func(t *testing.T) {
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
}
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
output := ui.OutputWriter.String()
for _, methodName := range methodNames {
require.Contains(t, output, methodName)
}
})
}

View File

@ -0,0 +1,96 @@
package authmethodread
import (
"flag"
"fmt"
"github.com/hashicorp/consul/command/acl"
"github.com/hashicorp/consul/command/flags"
"github.com/mitchellh/cli"
)
func New(ui cli.Ui) *cmd {
c := &cmd{UI: ui}
c.init()
return c
}
type cmd struct {
UI cli.Ui
flags *flag.FlagSet
http *flags.HTTPFlags
help string
name string
showMeta bool
}
func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.flags.BoolVar(
&c.showMeta,
"meta",
false,
"Indicates that auth method metadata such "+
"as the content hash and raft indices should be shown for each entry.",
)
c.flags.StringVar(
&c.name,
"name",
"",
"The name of the auth method to read.",
)
c.http = &flags.HTTPFlags{}
flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.ServerFlags())
c.help = flags.Usage(help, c.flags)
}
func (c *cmd) Run(args []string) int {
if err := c.flags.Parse(args); err != nil {
return 1
}
if c.name == "" {
c.UI.Error(fmt.Sprintf("Must specify the -name parameter"))
return 1
}
client, err := c.http.APIClient()
if err != nil {
c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
method, _, err := client.ACL().AuthMethodRead(c.name, nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Error reading auth method %q: %v", c.name, err))
return 1
} else if method == nil {
c.UI.Error(fmt.Sprintf("Auth method not found with name %q", c.name))
return 1
}
acl.PrintAuthMethod(method, c.UI, c.showMeta)
return 0
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return flags.Usage(c.help, nil)
}
const synopsis = "Read an ACL Auth Method"
const help = `
Usage: consul acl auth-method read -name NAME [options]
Read an auth method:
$ consul acl auth-method read -name my-auth-method
`

View File

@ -0,0 +1,118 @@
package authmethodread
import (
"os"
"strings"
"testing"
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/logger"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/testrpc"
"github.com/hashicorp/go-uuid"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
// activate testing auth method
_ "github.com/hashicorp/consul/agent/consul/authmethod/testauth"
)
func TestAuthMethodReadCommand_noTabs(t *testing.T) {
t.Parallel()
if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') {
t.Fatal("help has tabs")
}
}
func TestAuthMethodReadCommand(t *testing.T) {
t.Parallel()
testDir := testutil.TempDir(t, "acl")
defer os.RemoveAll(testDir)
a := agent.NewTestAgent(t, t.Name(), `
primary_datacenter = "dc1"
acl {
enabled = true
tokens {
master = "root"
}
}`)
a.Agent.LogWriter = logger.NewLogWriter(512)
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
client := a.Client()
t.Run("name required", func(t *testing.T) {
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
}
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Must specify the -name parameter")
})
t.Run("not found", func(t *testing.T) {
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-name=notfound",
}
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Auth method not found with name")
})
createAuthMethod := func(t *testing.T) string {
id, err := uuid.GenerateUUID()
require.NoError(t, err)
methodName := "test-" + id
_, _, err = client.ACL().AuthMethodCreate(
&api.ACLAuthMethod{
Name: methodName,
Type: "testing",
Description: "test",
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
return methodName
}
t.Run("read by name", func(t *testing.T) {
name := createAuthMethod(t)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-name=" + name,
}
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
output := ui.OutputWriter.String()
require.Contains(t, output, name)
})
}

View File

@ -0,0 +1,220 @@
package authmethodupdate
import (
"flag"
"fmt"
"io"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/acl"
"github.com/hashicorp/consul/command/flags"
"github.com/hashicorp/consul/command/helpers"
"github.com/mitchellh/cli"
)
func New(ui cli.Ui) *cmd {
c := &cmd{UI: ui}
c.init()
return c
}
type cmd struct {
UI cli.Ui
flags *flag.FlagSet
http *flags.HTTPFlags
help string
name string
description string
k8sHost string
k8sCACert string
k8sServiceAccountJWT string
noMerge bool
showMeta bool
testStdin io.Reader
}
func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.flags.BoolVar(
&c.showMeta,
"meta",
false,
"Indicates that auth method metadata such "+
"as the content hash and raft indices should be shown for each entry.",
)
c.flags.StringVar(
&c.name,
"name",
"",
"The auth method name.",
)
c.flags.StringVar(
&c.description,
"description",
"",
"A description of the auth method.",
)
c.flags.StringVar(
&c.k8sHost,
"kubernetes-host",
"",
"Address of the Kubernetes API server. This flag is required for type=kubernetes.",
)
c.flags.StringVar(
&c.k8sCACert,
"kubernetes-ca-cert",
"",
"PEM encoded CA cert for use by the TLS client used to talk with the "+
"Kubernetes API. May be prefixed with '@' to indicate that the "+
"value is a file path to load the cert from. "+
"This flag is required for type=kubernetes.",
)
c.flags.StringVar(
&c.k8sServiceAccountJWT,
"kubernetes-service-account-jwt",
"",
"A kubernetes service account JWT used to access the TokenReview API to "+
"validate other JWTs during login. "+
"This flag is required for type=kubernetes.",
)
c.flags.BoolVar(&c.noMerge, "no-merge", false, "Do not merge the current auth method "+
"information with what is provided to the command. Instead overwrite all fields "+
"with the exception of the name which is immutable.")
c.http = &flags.HTTPFlags{}
flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.ServerFlags())
c.help = flags.Usage(help, c.flags)
}
func (c *cmd) Run(args []string) int {
if err := c.flags.Parse(args); err != nil {
return 1
}
if c.name == "" {
c.UI.Error(fmt.Sprintf("Cannot update an auth method without specifying the -name parameter"))
return 1
}
client, err := c.http.APIClient()
if err != nil {
c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
// Regardless of merge, we need to fetch the prior immutable fields first.
currentAuthMethod, _, err := client.ACL().AuthMethodRead(c.name, nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Error when retrieving current auth method: %v", err))
return 1
} else if currentAuthMethod == nil {
c.UI.Error(fmt.Sprintf("Auth method not found with name %q", c.name))
return 1
}
if c.k8sCACert != "" {
c.k8sCACert, err = helpers.LoadDataSource(c.k8sCACert, c.testStdin)
if err != nil {
c.UI.Error(fmt.Sprintf("Invalid '-kubernetes-ca-cert' value: %v", err))
return 1
} else if c.k8sCACert == "" {
c.UI.Error(fmt.Sprintf("Kubernetes CA Cert is empty"))
return 1
}
}
var method *api.ACLAuthMethod
if c.noMerge {
method = &api.ACLAuthMethod{
Name: currentAuthMethod.Name,
Type: currentAuthMethod.Type,
Description: c.description,
}
if currentAuthMethod.Type == "kubernetes" {
if c.k8sHost == "" {
c.UI.Error(fmt.Sprintf("Missing required '-kubernetes-host' flag"))
return 1
} else if c.k8sCACert == "" {
c.UI.Error(fmt.Sprintf("Missing required '-kubernetes-ca-cert' flag"))
return 1
} else if c.k8sServiceAccountJWT == "" {
c.UI.Error(fmt.Sprintf("Missing required '-kubernetes-service-account-jwt' flag"))
return 1
}
method.Config = map[string]interface{}{
"Host": c.k8sHost,
"CACert": c.k8sCACert,
"ServiceAccountJWT": c.k8sServiceAccountJWT,
}
}
} else {
methodCopy := *currentAuthMethod
method = &methodCopy
if c.description != "" {
method.Description = c.description
}
if method.Config == nil {
method.Config = make(map[string]interface{})
}
if currentAuthMethod.Type == "kubernetes" {
if c.k8sHost != "" {
method.Config["Host"] = c.k8sHost
}
if c.k8sCACert != "" {
method.Config["CACert"] = c.k8sCACert
}
if c.k8sServiceAccountJWT != "" {
method.Config["ServiceAccountJWT"] = c.k8sServiceAccountJWT
}
}
}
method, _, err = client.ACL().AuthMethodUpdate(method, nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Error updating auth method %q: %v", c.name, err))
return 1
}
c.UI.Info(fmt.Sprintf("Auth method updated successfully"))
acl.PrintAuthMethod(method, c.UI, c.showMeta)
return 0
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return flags.Usage(c.help, nil)
}
const synopsis = "Update an ACL Auth Method"
const help = `
Usage: consul acl auth-method update -name NAME [options]
Updates an auth method. By default it will merge the auth method
information with its current state so that you do not have to provide all
parameters. This behavior can be disabled by passing -no-merge.
Update all editable fields of the auth method:
$ consul acl auth-method update -name "my-k8s" \
-description "new description" \
-kubernetes-host "https://new-apiserver.example.com:8443" \
-kubernetes-ca-file /path/to/new-kube.ca.crt \
-kubernetes-service-account-jwt "NEW_JWT_CONTENTS"
`

View File

@ -0,0 +1,647 @@
package authmethodupdate
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/agent/connect"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/acl"
"github.com/hashicorp/consul/logger"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/testrpc"
"github.com/hashicorp/go-uuid"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
// activate testing auth method
_ "github.com/hashicorp/consul/agent/consul/authmethod/testauth"
)
func TestAuthMethodUpdateCommand_noTabs(t *testing.T) {
t.Parallel()
if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') {
t.Fatal("help has tabs")
}
}
func TestAuthMethodUpdateCommand(t *testing.T) {
t.Parallel()
testDir := testutil.TempDir(t, "acl")
defer os.RemoveAll(testDir)
a := agent.NewTestAgent(t, t.Name(), `
primary_datacenter = "dc1"
acl {
enabled = true
tokens {
master = "root"
}
}`)
a.Agent.LogWriter = logger.NewLogWriter(512)
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
client := a.Client()
t.Run("update without name", func(t *testing.T) {
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Cannot update an auth method without specifying the -name parameter")
})
t.Run("update nonexistent method", func(t *testing.T) {
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-name=test",
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Auth method not found with name")
})
createAuthMethod := func(t *testing.T) string {
id, err := uuid.GenerateUUID()
require.NoError(t, err)
methodName := "test-" + id
_, _, err = client.ACL().AuthMethodCreate(
&api.ACLAuthMethod{
Name: methodName,
Type: "testing",
Description: "test",
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
return methodName
}
t.Run("update all fields", func(t *testing.T) {
name := createAuthMethod(t)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-name=" + name,
"-description", "updated description",
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
method, _, err := client.ACL().AuthMethodRead(
name,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, method)
require.Equal(t, "updated description", method.Description)
})
}
func TestAuthMethodUpdateCommand_noMerge(t *testing.T) {
t.Parallel()
testDir := testutil.TempDir(t, "acl")
defer os.RemoveAll(testDir)
a := agent.NewTestAgent(t, t.Name(), `
primary_datacenter = "dc1"
acl {
enabled = true
tokens {
master = "root"
}
}`)
a.Agent.LogWriter = logger.NewLogWriter(512)
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
client := a.Client()
t.Run("update without name", func(t *testing.T) {
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-no-merge",
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Cannot update an auth method without specifying the -name parameter")
})
t.Run("update nonexistent method", func(t *testing.T) {
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-no-merge",
"-name=test",
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Auth method not found with name")
})
createAuthMethod := func(t *testing.T) string {
id, err := uuid.GenerateUUID()
require.NoError(t, err)
methodName := "test-" + id
_, _, err = client.ACL().AuthMethodCreate(
&api.ACLAuthMethod{
Name: methodName,
Type: "testing",
Description: "test",
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
return methodName
}
t.Run("update all fields", func(t *testing.T) {
name := createAuthMethod(t)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-no-merge",
"-name=" + name,
"-description", "updated description",
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
method, _, err := client.ACL().AuthMethodRead(
name,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, method)
require.Equal(t, "updated description", method.Description)
})
}
func TestAuthMethodUpdateCommand_k8s(t *testing.T) {
t.Parallel()
testDir := testutil.TempDir(t, "acl")
defer os.RemoveAll(testDir)
a := agent.NewTestAgent(t, t.Name(), `
primary_datacenter = "dc1"
acl {
enabled = true
tokens {
master = "root"
}
}`)
a.Agent.LogWriter = logger.NewLogWriter(512)
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
client := a.Client()
ca := connect.TestCA(t, nil)
ca2 := connect.TestCA(t, nil)
createAuthMethod := func(t *testing.T) string {
id, err := uuid.GenerateUUID()
require.NoError(t, err)
methodName := "k8s-" + id
_, _, err = client.ACL().AuthMethodCreate(
&api.ACLAuthMethod{
Name: methodName,
Type: "kubernetes",
Description: "test",
Config: map[string]interface{}{
"Host": "https://foo.internal:8443",
"CACert": ca.RootCert,
"ServiceAccountJWT": acl.TestKubernetesJWT_A,
},
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
return methodName
}
t.Run("update all fields", func(t *testing.T) {
name := createAuthMethod(t)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-name=" + name,
"-description", "updated description",
"-kubernetes-host", "https://foo-new.internal:8443",
"-kubernetes-ca-cert", ca2.RootCert,
"-kubernetes-service-account-jwt", acl.TestKubernetesJWT_B,
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
method, _, err := client.ACL().AuthMethodRead(
name,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, method)
require.Equal(t, "updated description", method.Description)
config, err := api.ParseKubernetesAuthMethodConfig(method.Config)
require.NoError(t, err)
require.Equal(t, "https://foo-new.internal:8443", config.Host)
require.Equal(t, ca2.RootCert, config.CACert)
require.Equal(t, acl.TestKubernetesJWT_B, config.ServiceAccountJWT)
})
ca2File := filepath.Join(testDir, "ca2.crt")
require.NoError(t, ioutil.WriteFile(ca2File, []byte(ca2.RootCert), 0600))
t.Run("update all fields with cert file", func(t *testing.T) {
name := createAuthMethod(t)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-name=" + name,
"-description", "updated description",
"-kubernetes-host", "https://foo-new.internal:8443",
"-kubernetes-ca-cert", "@" + ca2File,
"-kubernetes-service-account-jwt", acl.TestKubernetesJWT_B,
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
method, _, err := client.ACL().AuthMethodRead(
name,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, method)
require.Equal(t, "updated description", method.Description)
config, err := api.ParseKubernetesAuthMethodConfig(method.Config)
require.NoError(t, err)
require.Equal(t, "https://foo-new.internal:8443", config.Host)
require.Equal(t, ca2.RootCert, config.CACert)
require.Equal(t, acl.TestKubernetesJWT_B, config.ServiceAccountJWT)
})
t.Run("update all fields but k8s host", func(t *testing.T) {
name := createAuthMethod(t)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-name=" + name,
"-description", "updated description",
"-kubernetes-ca-cert", ca2.RootCert,
"-kubernetes-service-account-jwt", acl.TestKubernetesJWT_B,
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
method, _, err := client.ACL().AuthMethodRead(
name,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, method)
require.Equal(t, "updated description", method.Description)
config, err := api.ParseKubernetesAuthMethodConfig(method.Config)
require.NoError(t, err)
require.Equal(t, "https://foo.internal:8443", config.Host)
require.Equal(t, ca2.RootCert, config.CACert)
require.Equal(t, acl.TestKubernetesJWT_B, config.ServiceAccountJWT)
})
t.Run("update all fields but k8s ca cert", func(t *testing.T) {
name := createAuthMethod(t)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-name=" + name,
"-description", "updated description",
"-kubernetes-host", "https://foo-new.internal:8443",
"-kubernetes-service-account-jwt", acl.TestKubernetesJWT_B,
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
method, _, err := client.ACL().AuthMethodRead(
name,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, method)
require.Equal(t, "updated description", method.Description)
config, err := api.ParseKubernetesAuthMethodConfig(method.Config)
require.NoError(t, err)
require.Equal(t, "https://foo-new.internal:8443", config.Host)
require.Equal(t, ca.RootCert, config.CACert)
require.Equal(t, acl.TestKubernetesJWT_B, config.ServiceAccountJWT)
})
t.Run("update all fields but k8s jwt", func(t *testing.T) {
name := createAuthMethod(t)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-name=" + name,
"-description", "updated description",
"-kubernetes-host", "https://foo-new.internal:8443",
"-kubernetes-ca-cert", ca2.RootCert,
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
method, _, err := client.ACL().AuthMethodRead(
name,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, method)
require.Equal(t, "updated description", method.Description)
config, err := api.ParseKubernetesAuthMethodConfig(method.Config)
require.NoError(t, err)
require.Equal(t, "https://foo-new.internal:8443", config.Host)
require.Equal(t, ca2.RootCert, config.CACert)
require.Equal(t, acl.TestKubernetesJWT_A, config.ServiceAccountJWT)
})
}
func TestAuthMethodUpdateCommand_k8s_noMerge(t *testing.T) {
t.Parallel()
testDir := testutil.TempDir(t, "acl")
defer os.RemoveAll(testDir)
a := agent.NewTestAgent(t, t.Name(), `
primary_datacenter = "dc1"
acl {
enabled = true
tokens {
master = "root"
}
}`)
a.Agent.LogWriter = logger.NewLogWriter(512)
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
client := a.Client()
ca := connect.TestCA(t, nil)
ca2 := connect.TestCA(t, nil)
createAuthMethod := func(t *testing.T) string {
id, err := uuid.GenerateUUID()
require.NoError(t, err)
methodName := "k8s-" + id
_, _, err = client.ACL().AuthMethodCreate(
&api.ACLAuthMethod{
Name: methodName,
Type: "kubernetes",
Description: "test",
Config: map[string]interface{}{
"Host": "https://foo.internal:8443",
"CACert": ca.RootCert,
"ServiceAccountJWT": acl.TestKubernetesJWT_A,
},
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
return methodName
}
t.Run("update missing k8s host", func(t *testing.T) {
name := createAuthMethod(t)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-no-merge",
"-name=" + name,
"-description", "updated description",
"-kubernetes-ca-cert", ca2.RootCert,
"-kubernetes-service-account-jwt", acl.TestKubernetesJWT_B,
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Missing required '-kubernetes-host' flag")
})
t.Run("update missing k8s ca cert", func(t *testing.T) {
name := createAuthMethod(t)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-no-merge",
"-name=" + name,
"-description", "updated description",
"-kubernetes-host", "https://foo-new.internal:8443",
"-kubernetes-service-account-jwt", acl.TestKubernetesJWT_B,
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Missing required '-kubernetes-ca-cert' flag")
})
t.Run("update missing k8s jwt", func(t *testing.T) {
name := createAuthMethod(t)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-no-merge",
"-name=" + name,
"-description", "updated description",
"-kubernetes-host", "https://foo-new.internal:8443",
"-kubernetes-ca-cert", ca2.RootCert,
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Missing required '-kubernetes-service-account-jwt' flag")
})
t.Run("update all fields", func(t *testing.T) {
name := createAuthMethod(t)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-no-merge",
"-name=" + name,
"-description", "updated description",
"-kubernetes-host", "https://foo-new.internal:8443",
"-kubernetes-ca-cert", ca2.RootCert,
"-kubernetes-service-account-jwt", acl.TestKubernetesJWT_B,
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
method, _, err := client.ACL().AuthMethodRead(
name,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, method)
require.Equal(t, "updated description", method.Description)
config, err := api.ParseKubernetesAuthMethodConfig(method.Config)
require.NoError(t, err)
require.Equal(t, "https://foo-new.internal:8443", config.Host)
require.Equal(t, ca2.RootCert, config.CACert)
require.Equal(t, acl.TestKubernetesJWT_B, config.ServiceAccountJWT)
})
ca2File := filepath.Join(testDir, "ca2.crt")
require.NoError(t, ioutil.WriteFile(ca2File, []byte(ca2.RootCert), 0600))
t.Run("update all fields with cert file", func(t *testing.T) {
name := createAuthMethod(t)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-no-merge",
"-name=" + name,
"-description", "updated description",
"-kubernetes-host", "https://foo-new.internal:8443",
"-kubernetes-ca-cert", "@" + ca2File,
"-kubernetes-service-account-jwt", acl.TestKubernetesJWT_B,
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
method, _, err := client.ACL().AuthMethodRead(
name,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, method)
require.Equal(t, "updated description", method.Description)
config, err := api.ParseKubernetesAuthMethodConfig(method.Config)
require.NoError(t, err)
require.Equal(t, "https://foo-new.internal:8443", config.Host)
require.Equal(t, ca2.RootCert, config.CACert)
require.Equal(t, acl.TestKubernetesJWT_B, config.ServiceAccountJWT)
})
}

View File

@ -0,0 +1,60 @@
package bindingrule
import (
"github.com/hashicorp/consul/command/flags"
"github.com/mitchellh/cli"
)
func New() *cmd {
return &cmd{}
}
type cmd struct{}
func (c *cmd) Run(args []string) int {
return cli.RunResultHelp
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return flags.Usage(help, nil)
}
const synopsis = "Manage Consul's ACL Binding Rules"
const help = `
Usage: consul acl binding-rule <subcommand> [options] [args]
This command has subcommands for managing Consul's ACL Binding Rules. Here
are some simple examples, and more detailed examples are available in the
subcommands or the documentation.
Create a new binding rule:
$ consul acl binding-rule create \
-method=minikube \
-bind-type=service \
-bind-name='k8s-${serviceaccount.name}' \
-selector='serviceaccount.namespace==default and serviceaccount.name==web'
List all binding rules:
$ consul acl binding-rule list
Update a binding rule:
$ consul acl binding-rule update -id=43cb72df-9c6f-4315-ac8a-01a9d98155ef \
-bind-name='k8s-${serviceaccount.name}'
Read a binding rule:
$ consul acl binding-rule read -id fdabbcb5-9de5-4b1a-961f-77214ae88cba
Delete a binding rule:
$ consul acl binding-rule delete -id b6b856da-5193-4e78-845a-7d61ca8371ba
For more examples, ask for subcommand help or view the documentation.
`

View File

@ -0,0 +1,148 @@
package bindingrulecreate
import (
"flag"
"fmt"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/acl"
"github.com/hashicorp/consul/command/flags"
"github.com/mitchellh/cli"
)
func New(ui cli.Ui) *cmd {
c := &cmd{UI: ui}
c.init()
return c
}
type cmd struct {
UI cli.Ui
flags *flag.FlagSet
http *flags.HTTPFlags
help string
authMethodName string
description string
selector string
bindType string
bindName string
showMeta bool
}
func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.flags.BoolVar(
&c.showMeta,
"meta",
false,
"Indicates that binding rule metadata such "+
"as the content hash and raft indices should be shown for each entry.",
)
c.flags.StringVar(
&c.authMethodName,
"method",
"",
"The auth method's name for which this binding rule applies. "+
"This flag is required.",
)
c.flags.StringVar(
&c.description,
"description",
"",
"A description of the binding rule.",
)
c.flags.StringVar(
&c.selector,
"selector",
"",
"Selector is an expression that matches against verified identity "+
"attributes returned from the auth method during login.",
)
c.flags.StringVar(
&c.bindType,
"bind-type",
string(api.BindingRuleBindTypeService),
"Type of binding to perform (\"service\" or \"role\").",
)
c.flags.StringVar(
&c.bindName,
"bind-name",
"",
"Name to bind on match. Can use ${var} interpolation. "+
"This flag is required.",
)
c.http = &flags.HTTPFlags{}
flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.ServerFlags())
c.help = flags.Usage(help, c.flags)
}
func (c *cmd) Run(args []string) int {
if err := c.flags.Parse(args); err != nil {
return 1
}
if c.authMethodName == "" {
c.UI.Error(fmt.Sprintf("Missing required '-method' flag"))
c.UI.Error(c.Help())
return 1
} else if c.bindType == "" {
c.UI.Error(fmt.Sprintf("Missing required '-bind-type' flag"))
c.UI.Error(c.Help())
return 1
} else if c.bindName == "" {
c.UI.Error(fmt.Sprintf("Missing required '-bind-name' flag"))
c.UI.Error(c.Help())
return 1
}
newRule := &api.ACLBindingRule{
Description: c.description,
AuthMethod: c.authMethodName,
BindType: api.BindingRuleBindType(c.bindType),
BindName: c.bindName,
Selector: c.selector,
}
client, err := c.http.APIClient()
if err != nil {
c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
rule, _, err := client.ACL().BindingRuleCreate(newRule, nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to create new binding rule: %v", err))
return 1
}
acl.PrintBindingRule(rule, c.UI, c.showMeta)
return 0
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return flags.Usage(c.help, nil)
}
const synopsis = "Create an ACL Binding Rule"
const help = `
Usage: consul acl binding-rule create [options]
Create a new binding rule:
$ consul acl binding-rule create \
-method=minikube \
-bind-type=service \
-bind-name='k8s-${serviceaccount.name}' \
-selector='serviceaccount.namespace==default and serviceaccount.name==web'
`

View File

@ -0,0 +1,178 @@
package bindingrulecreate
import (
"os"
"strings"
"testing"
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/logger"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/testrpc"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
// activate testing auth method
_ "github.com/hashicorp/consul/agent/consul/authmethod/testauth"
)
func TestBindingRuleCreateCommand_noTabs(t *testing.T) {
t.Parallel()
if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') {
t.Fatal("help has tabs")
}
}
func TestBindingRuleCreateCommand(t *testing.T) {
t.Parallel()
testDir := testutil.TempDir(t, "acl")
defer os.RemoveAll(testDir)
a := agent.NewTestAgent(t, t.Name(), `
primary_datacenter = "dc1"
acl {
enabled = true
tokens {
master = "root"
}
}`)
a.Agent.LogWriter = logger.NewLogWriter(512)
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
client := a.Client()
// create an auth method in advance
{
_, _, err := client.ACL().AuthMethodCreate(
&api.ACLAuthMethod{
Name: "test",
Type: "testing",
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
}
t.Run("method is required", func(t *testing.T) {
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Missing required '-method' flag")
})
t.Run("bind type required", func(t *testing.T) {
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-method=test",
"-bind-type=",
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Missing required '-bind-type' flag")
})
t.Run("bind name required", func(t *testing.T) {
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-method=test",
"-bind-type=service",
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Missing required '-bind-name' flag")
})
t.Run("must use roughly valid selector", func(t *testing.T) {
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-method=test",
"-bind-type=service",
"-bind-name=demo",
"-selector", "foo",
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Selector is invalid")
})
t.Run("create it with no selector", func(t *testing.T) {
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-method=test",
"-bind-type=service",
"-bind-name=demo",
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
})
t.Run("create it with a match selector", func(t *testing.T) {
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-method=test",
"-bind-type=service",
"-bind-name=demo",
"-selector", "serviceaccount.namespace==default and serviceaccount.name==vault",
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
})
t.Run("create it with type role", func(t *testing.T) {
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-method=test",
"-bind-type=role",
"-bind-name=demo",
"-selector", "serviceaccount.namespace==default and serviceaccount.name==vault",
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
})
}

View File

@ -0,0 +1,97 @@
package bindingruledelete
import (
"flag"
"fmt"
"github.com/hashicorp/consul/command/acl"
"github.com/hashicorp/consul/command/flags"
"github.com/mitchellh/cli"
)
func New(ui cli.Ui) *cmd {
c := &cmd{UI: ui}
c.init()
return c
}
type cmd struct {
UI cli.Ui
flags *flag.FlagSet
http *flags.HTTPFlags
help string
ruleID string
}
func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.flags.StringVar(
&c.ruleID,
"id",
"",
"The ID of the binding rule to delete. "+
"It may be specified as a unique ID prefix but will error if the prefix "+
"matches multiple binding rule IDs",
)
c.http = &flags.HTTPFlags{}
flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.ServerFlags())
c.help = flags.Usage(help, c.flags)
}
func (c *cmd) Run(args []string) int {
if err := c.flags.Parse(args); err != nil {
return 1
}
if c.ruleID == "" {
c.UI.Error(fmt.Sprintf("Must specify the -id parameter"))
return 1
}
client, err := c.http.APIClient()
if err != nil {
c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
ruleID, err := acl.GetBindingRuleIDFromPartial(client, c.ruleID)
if err != nil {
c.UI.Error(fmt.Sprintf("Error determining binding rule ID: %v", err))
return 1
}
if _, err := client.ACL().BindingRuleDelete(ruleID, nil); err != nil {
c.UI.Error(fmt.Sprintf("Error deleting binding rule %q: %v", ruleID, err))
return 1
}
c.UI.Info(fmt.Sprintf("Binding rule %q deleted successfully", ruleID))
return 0
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return flags.Usage(c.help, nil)
}
const synopsis = "Delete an ACL Binding Rule"
const help = `
Usage: consul acl binding-rule delete -id ID [options]
Deletes an ACL binding rule by providing the ID or a unique ID prefix.
Delete by prefix:
$ consul acl binding-rule delete -id b6b85
Delete by full ID:
$ consul acl binding-rule delete -id b6b856da-5193-4e78-845a-7d61ca8371ba
`

View File

@ -0,0 +1,187 @@
package bindingruledelete
import (
"fmt"
"os"
"strings"
"testing"
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/logger"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/testrpc"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
// activate testing auth method
_ "github.com/hashicorp/consul/agent/consul/authmethod/testauth"
)
func TestBindingRuleDeleteCommand_noTabs(t *testing.T) {
t.Parallel()
if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') {
t.Fatal("help has tabs")
}
}
func TestBindingRuleDeleteCommand(t *testing.T) {
t.Parallel()
testDir := testutil.TempDir(t, "acl")
defer os.RemoveAll(testDir)
a := agent.NewTestAgent(t, t.Name(), `
primary_datacenter = "dc1"
acl {
enabled = true
tokens {
master = "root"
}
}`)
a.Agent.LogWriter = logger.NewLogWriter(512)
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
client := a.Client()
// create an auth method in advance
{
_, _, err := client.ACL().AuthMethodCreate(
&api.ACLAuthMethod{
Name: "test",
Type: "testing",
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
}
createRule := func(t *testing.T) string {
rule, _, err := client.ACL().BindingRuleCreate(
&api.ACLBindingRule{
AuthMethod: "test",
Description: "test rule",
BindType: api.BindingRuleBindTypeService,
BindName: "test-${serviceaccount.name}",
Selector: "serviceaccount.namespace==default",
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
return rule.ID
}
createDupe := func(t *testing.T) string {
for {
// Check for 1-char duplicates.
rules, _, err := client.ACL().BindingRuleList(
"test",
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
m := make(map[byte]struct{})
for _, rule := range rules {
c := rule.ID[0]
if _, ok := m[c]; ok {
return string(c)
}
m[c] = struct{}{}
}
_ = createRule(t)
}
}
t.Run("id required", func(t *testing.T) {
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
}
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Must specify the -id parameter")
})
t.Run("delete works", func(t *testing.T) {
id := createRule(t)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-id", id,
}
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
output := ui.OutputWriter.String()
require.Contains(t, output, fmt.Sprintf("deleted successfully"))
require.Contains(t, output, id)
rule, _, err := client.ACL().BindingRuleRead(
id,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.Nil(t, rule)
})
t.Run("delete works via prefixes", func(t *testing.T) {
id := createRule(t)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-id", id[0:5],
}
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
output := ui.OutputWriter.String()
require.Contains(t, output, fmt.Sprintf("deleted successfully"))
require.Contains(t, output, id)
rule, _, err := client.ACL().BindingRuleRead(
id,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.Nil(t, rule)
})
t.Run("delete fails when prefix matches more than one rule", func(t *testing.T) {
prefix := createDupe(t)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-id=" + prefix,
}
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Error determining binding rule ID")
})
}

View File

@ -0,0 +1,98 @@
package bindingrulelist
import (
"flag"
"fmt"
"github.com/hashicorp/consul/command/acl"
"github.com/hashicorp/consul/command/flags"
"github.com/mitchellh/cli"
)
func New(ui cli.Ui) *cmd {
c := &cmd{UI: ui}
c.init()
return c
}
type cmd struct {
UI cli.Ui
flags *flag.FlagSet
http *flags.HTTPFlags
help string
authMethodName string
showMeta bool
}
func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.flags.BoolVar(
&c.showMeta,
"meta",
false,
"Indicates that binding rule metadata such "+
"as the content hash and raft indices should be shown for each entry.",
)
c.flags.StringVar(
&c.authMethodName,
"method",
"",
"Only show rules linked to the auth method with the given name.",
)
c.http = &flags.HTTPFlags{}
flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.ServerFlags())
c.help = flags.Usage(help, c.flags)
}
func (c *cmd) Run(args []string) int {
if err := c.flags.Parse(args); err != nil {
return 1
}
client, err := c.http.APIClient()
if err != nil {
c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
rules, _, err := client.ACL().BindingRuleList(c.authMethodName, nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to retrieve the binding rule list: %v", err))
return 1
}
for _, rule := range rules {
acl.PrintBindingRuleListEntry(rule, c.UI, c.showMeta)
}
return 0
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return flags.Usage(c.help, nil)
}
const synopsis = "Lists ACL Binding Rules"
const help = `
Usage: consul acl binding-rule list [options]
Lists all the ACL binding rules.
Show all:
$ consul acl binding-rule list
Show all for a specific auth method:
$ consul acl binding-rule list -method="my-method"
`

View File

@ -0,0 +1,167 @@
package bindingrulelist
import (
"fmt"
"os"
"strings"
"testing"
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/logger"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/testrpc"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
// activate testing auth method
_ "github.com/hashicorp/consul/agent/consul/authmethod/testauth"
)
func TestBindingRuleListCommand_noTabs(t *testing.T) {
t.Parallel()
if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') {
t.Fatal("help has tabs")
}
}
func TestBindingRuleListCommand(t *testing.T) {
t.Parallel()
testDir := testutil.TempDir(t, "acl")
defer os.RemoveAll(testDir)
a := agent.NewTestAgent(t, t.Name(), `
primary_datacenter = "dc1"
acl {
enabled = true
tokens {
master = "root"
}
}`)
a.Agent.LogWriter = logger.NewLogWriter(512)
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
client := a.Client()
{
_, _, err := client.ACL().AuthMethodCreate(
&api.ACLAuthMethod{
Name: "test-1",
Type: "testing",
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
_, _, err = client.ACL().AuthMethodCreate(
&api.ACLAuthMethod{
Name: "test-2",
Type: "testing",
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
}
createRule := func(t *testing.T, methodName, description string) string {
rule, _, err := client.ACL().BindingRuleCreate(
&api.ACLBindingRule{
AuthMethod: methodName,
Description: description,
BindType: api.BindingRuleBindTypeService,
BindName: "test-${serviceaccount.name}",
Selector: "serviceaccount.namespace==default",
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
return rule.ID
}
var ruleIDs []string
for i := 0; i < 10; i++ {
name := fmt.Sprintf("test-rule-%d", i)
var methodName string
if i%2 == 0 {
methodName = "test-1"
} else {
methodName = "test-2"
}
id := createRule(t, methodName, name)
ruleIDs = append(ruleIDs, id)
}
t.Run("normal", func(t *testing.T) {
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
output := ui.OutputWriter.String()
for i, v := range ruleIDs {
require.Contains(t, output, fmt.Sprintf("test-rule-%d", i))
require.Contains(t, output, v)
}
})
t.Run("filter by method 1", func(t *testing.T) {
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-method=test-1",
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
output := ui.OutputWriter.String()
for i, v := range ruleIDs {
if i%2 == 0 {
require.Contains(t, output, fmt.Sprintf("test-rule-%d", i))
require.Contains(t, output, v)
}
}
})
t.Run("filter by method 2", func(t *testing.T) {
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-method=test-2",
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
output := ui.OutputWriter.String()
for i, v := range ruleIDs {
if i%2 == 1 {
require.Contains(t, output, fmt.Sprintf("test-rule-%d", i))
require.Contains(t, output, v)
}
}
})
}

View File

@ -0,0 +1,108 @@
package bindingruleread
import (
"flag"
"fmt"
"github.com/hashicorp/consul/command/acl"
"github.com/hashicorp/consul/command/flags"
"github.com/mitchellh/cli"
)
func New(ui cli.Ui) *cmd {
c := &cmd{UI: ui}
c.init()
return c
}
type cmd struct {
UI cli.Ui
flags *flag.FlagSet
http *flags.HTTPFlags
help string
ruleID string
showMeta bool
}
func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.flags.BoolVar(
&c.showMeta,
"meta",
false,
"Indicates that binding rule metadata such "+
"as the content hash and raft indices should be shown for each entry.",
)
c.flags.StringVar(
&c.ruleID,
"id",
"",
"The ID of the binding rule to read. "+
"It may be specified as a unique ID prefix but will error if the prefix "+
"matches multiple binding rule IDs",
)
c.http = &flags.HTTPFlags{}
flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.ServerFlags())
c.help = flags.Usage(help, c.flags)
}
func (c *cmd) Run(args []string) int {
if err := c.flags.Parse(args); err != nil {
return 1
}
if c.ruleID == "" {
c.UI.Error(fmt.Sprintf("Must specify the -id parameter."))
return 1
}
client, err := c.http.APIClient()
if err != nil {
c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
ruleID, err := acl.GetBindingRuleIDFromPartial(client, c.ruleID)
if err != nil {
c.UI.Error(fmt.Sprintf("Error determining binding rule ID: %v", err))
return 1
}
rule, _, err := client.ACL().BindingRuleRead(ruleID, nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Error reading binding rule %q: %v", ruleID, err))
return 1
} else if rule == nil {
c.UI.Error(fmt.Sprintf("Binding rule not found with ID %q", ruleID))
return 1
}
acl.PrintBindingRule(rule, c.UI, c.showMeta)
return 0
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return flags.Usage(c.help, nil)
}
const synopsis = "Read an ACL Binding Rule"
const help = `
Usage: consul acl binding-rule read -id ID [options]
This command will retrieve and print out the details of a single binding
rule.
Read a binding rule:
$ consul acl binding-rule read -id fdabbcb5-9de5-4b1a-961f-77214ae88cba
`

View File

@ -0,0 +1,152 @@
package bindingruleread
import (
"fmt"
"os"
"strings"
"testing"
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/logger"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/testrpc"
"github.com/hashicorp/go-uuid"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
// activate testing auth method
_ "github.com/hashicorp/consul/agent/consul/authmethod/testauth"
)
func TestBindingRuleReadCommand_noTabs(t *testing.T) {
t.Parallel()
if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') {
t.Fatal("help has tabs")
}
}
func TestBindingRuleReadCommand(t *testing.T) {
t.Parallel()
testDir := testutil.TempDir(t, "acl")
defer os.RemoveAll(testDir)
a := agent.NewTestAgent(t, t.Name(), `
primary_datacenter = "dc1"
acl {
enabled = true
tokens {
master = "root"
}
}`)
a.Agent.LogWriter = logger.NewLogWriter(512)
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
client := a.Client()
// create an auth method in advance
{
_, _, err := client.ACL().AuthMethodCreate(
&api.ACLAuthMethod{
Name: "test",
Type: "testing",
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
}
createRule := func(t *testing.T) string {
rule, _, err := client.ACL().BindingRuleCreate(
&api.ACLBindingRule{
AuthMethod: "test",
Description: "test rule",
BindType: api.BindingRuleBindTypeService,
BindName: "test-${serviceaccount.name}",
Selector: "serviceaccount.namespace==default",
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
return rule.ID
}
t.Run("id required", func(t *testing.T) {
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
}
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Must specify the -id parameter")
})
t.Run("read by id not found", func(t *testing.T) {
fakeID, err := uuid.GenerateUUID()
require.NoError(t, err)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-id=" + fakeID,
}
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Binding rule not found with ID")
})
t.Run("read by id", func(t *testing.T) {
id := createRule(t)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-id=" + id,
}
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
output := ui.OutputWriter.String()
require.Contains(t, output, fmt.Sprintf("test rule"))
require.Contains(t, output, id)
})
t.Run("read by id prefix", func(t *testing.T) {
id := createRule(t)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-id=" + id[0:5],
}
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
output := ui.OutputWriter.String()
require.Contains(t, output, fmt.Sprintf("test rule"))
require.Contains(t, output, id)
})
}

View File

@ -0,0 +1,212 @@
package bindingruleupdate
import (
"flag"
"fmt"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/acl"
"github.com/hashicorp/consul/command/flags"
"github.com/mitchellh/cli"
)
func New(ui cli.Ui) *cmd {
c := &cmd{UI: ui}
c.init()
return c
}
type cmd struct {
UI cli.Ui
flags *flag.FlagSet
http *flags.HTTPFlags
help string
ruleID string
description string
selector string
bindType string
bindName string
noMerge bool
showMeta bool
}
func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.flags.BoolVar(
&c.showMeta,
"meta",
false,
"Indicates that binding rule metadata such "+
"as the content hash and raft indices should be shown for each entry.",
)
c.flags.StringVar(
&c.ruleID,
"id",
"",
"The ID of the binding rule to update. "+
"It may be specified as a unique ID prefix but will error if the prefix "+
"matches multiple binding rule IDs",
)
c.flags.StringVar(
&c.description,
"description",
"",
"A description of the binding rule.",
)
c.flags.StringVar(
&c.selector,
"selector",
"",
"Selector is an expression that matches against verified identity "+
"attributes returned from the auth method during login.",
)
c.flags.StringVar(
&c.bindType,
"bind-type",
string(api.BindingRuleBindTypeService),
"Type of binding to perform (\"service\" or \"role\").",
)
c.flags.StringVar(
&c.bindName,
"bind-name",
"",
"Name to bind on match. Can use ${var} interpolation. "+
"This flag is required.",
)
c.flags.BoolVar(
&c.noMerge,
"no-merge",
false,
"Do not merge the current binding rule "+
"information with what is provided to the command. Instead overwrite all fields "+
"with the exception of the binding rule ID which is immutable.",
)
c.http = &flags.HTTPFlags{}
flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.ServerFlags())
c.help = flags.Usage(help, c.flags)
}
func (c *cmd) Run(args []string) int {
if err := c.flags.Parse(args); err != nil {
return 1
}
if c.ruleID == "" {
c.UI.Error(fmt.Sprintf("Cannot update a binding rule without specifying the -id parameter"))
return 1
}
client, err := c.http.APIClient()
if err != nil {
c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
ruleID, err := acl.GetBindingRuleIDFromPartial(client, c.ruleID)
if err != nil {
c.UI.Error(fmt.Sprintf("Error determining binding rule ID: %v", err))
return 1
}
// Read the current binding rule in both cases so we can fail better if not found.
currentRule, _, err := client.ACL().BindingRuleRead(ruleID, nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Error when retrieving current binding rule: %v", err))
return 1
} else if currentRule == nil {
c.UI.Error(fmt.Sprintf("Binding rule not found with ID %q", ruleID))
return 1
}
var rule *api.ACLBindingRule
if c.noMerge {
if c.bindType == "" {
c.UI.Error(fmt.Sprintf("Missing required '-bind-type' flag"))
c.UI.Error(c.Help())
return 1
} else if c.bindName == "" {
c.UI.Error(fmt.Sprintf("Missing required '-bind-name' flag"))
c.UI.Error(c.Help())
return 1
}
rule = &api.ACLBindingRule{
ID: ruleID,
AuthMethod: currentRule.AuthMethod, // immutable
Description: c.description,
BindType: api.BindingRuleBindType(c.bindType),
BindName: c.bindName,
Selector: c.selector,
}
} else {
rule = currentRule
if c.description != "" {
rule.Description = c.description
}
if c.bindType != "" {
rule.BindType = api.BindingRuleBindType(c.bindType)
}
if c.bindName != "" {
rule.BindName = c.bindName
}
if isFlagSet(c.flags, "selector") {
rule.Selector = c.selector // empty is valid
}
}
rule, _, err = client.ACL().BindingRuleUpdate(rule, nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Error updating binding rule %q: %v", ruleID, err))
return 1
}
c.UI.Info(fmt.Sprintf("Binding rule updated successfully"))
acl.PrintBindingRule(rule, c.UI, c.showMeta)
return 0
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return flags.Usage(c.help, nil)
}
func isFlagSet(flags *flag.FlagSet, name string) bool {
found := false
flags.Visit(func(f *flag.Flag) {
if f.Name == name {
found = true
}
})
return found
}
const synopsis = "Update an ACL Binding Rule"
const help = `
Usage: consul acl binding-rule update -id ID [options]
Updates a binding rule. By default it will merge the binding rule
information with its current state so that you do not have to provide all
parameters. This behavior can be disabled by passing -no-merge.
Update all editable fields of the binding rule:
$ consul acl binding-rule update \
-id=43cb72df-9c6f-4315-ac8a-01a9d98155ef \
-description="new description" \
-bind-type=role \
-bind-name='k8s-${serviceaccount.name}' \
-selector='serviceaccount.namespace==default and serviceaccount.name==web'
`

View File

@ -0,0 +1,768 @@
package bindingruleupdate
import (
"os"
"strings"
"testing"
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/logger"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/testrpc"
uuid "github.com/hashicorp/go-uuid"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
// activate testing auth method
_ "github.com/hashicorp/consul/agent/consul/authmethod/testauth"
)
func TestBindingRuleUpdateCommand_noTabs(t *testing.T) {
t.Parallel()
if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') {
t.Fatal("help has tabs")
}
}
func TestBindingRuleUpdateCommand(t *testing.T) {
t.Parallel()
testDir := testutil.TempDir(t, "acl")
defer os.RemoveAll(testDir)
a := agent.NewTestAgent(t, t.Name(), `
primary_datacenter = "dc1"
acl {
enabled = true
tokens {
master = "root"
}
}`)
a.Agent.LogWriter = logger.NewLogWriter(512)
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
client := a.Client()
// create an auth method in advance
{
_, _, err := client.ACL().AuthMethodCreate(
&api.ACLAuthMethod{
Name: "test",
Type: "testing",
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
}
deleteRules := func(t *testing.T) {
rules, _, err := client.ACL().BindingRuleList(
"test",
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
for _, rule := range rules {
_, err := client.ACL().BindingRuleDelete(
rule.ID,
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
}
}
t.Run("rule id required", func(t *testing.T) {
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Cannot update a binding rule without specifying the -id parameter")
})
t.Run("rule id partial matches nothing", func(t *testing.T) {
fakeID, err := uuid.GenerateUUID()
require.NoError(t, err)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-id=" + fakeID[0:5],
"-token=root",
"-description=test rule edited",
}
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Error determining binding rule ID")
})
t.Run("rule id exact match doesn't exist", func(t *testing.T) {
fakeID, err := uuid.GenerateUUID()
require.NoError(t, err)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-id=" + fakeID,
"-token=root",
"-description=test rule edited",
}
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Binding rule not found with ID")
})
createRule := func(t *testing.T) string {
rule, _, err := client.ACL().BindingRuleCreate(
&api.ACLBindingRule{
AuthMethod: "test",
Description: "test rule",
BindType: api.BindingRuleBindTypeService,
BindName: "test-${serviceaccount.name}",
Selector: "serviceaccount.namespace==default",
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
return rule.ID
}
createDupe := func(t *testing.T) string {
for {
// Check for 1-char duplicates.
rules, _, err := client.ACL().BindingRuleList(
"test",
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
m := make(map[byte]struct{})
for _, rule := range rules {
c := rule.ID[0]
if _, ok := m[c]; ok {
return string(c)
}
m[c] = struct{}{}
}
_ = createRule(t)
}
}
t.Run("rule id partial matches multiple", func(t *testing.T) {
prefix := createDupe(t)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-id=" + prefix,
"-token=root",
"-description=test rule edited",
}
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Error determining binding rule ID")
})
t.Run("must use roughly valid selector", func(t *testing.T) {
id := createRule(t)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-id", id,
"-selector", "foo",
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Selector is invalid")
})
t.Run("update all fields", func(t *testing.T) {
id := createRule(t)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-id", id,
"-description=test rule edited",
"-bind-type", "role",
"-bind-name=role-updated",
"-selector=serviceaccount.namespace==alt and serviceaccount.name==demo",
}
code := cmd.Run(args)
require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
rule, _, err := client.ACL().BindingRuleRead(
id,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, rule)
require.Equal(t, "test rule edited", rule.Description)
require.Equal(t, "role-updated", rule.BindName)
require.Equal(t, api.BindingRuleBindTypeRole, rule.BindType)
require.Equal(t, "serviceaccount.namespace==alt and serviceaccount.name==demo", rule.Selector)
})
t.Run("update all fields - partial", func(t *testing.T) {
deleteRules(t) // reset since we created a bunch that might be dupes
id := createRule(t)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-id", id[0:5],
"-description=test rule edited",
"-bind-type", "role",
"-bind-name=role-updated",
"-selector=serviceaccount.namespace==alt and serviceaccount.name==demo",
}
code := cmd.Run(args)
require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
rule, _, err := client.ACL().BindingRuleRead(
id,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, rule)
require.Equal(t, "test rule edited", rule.Description)
require.Equal(t, "role-updated", rule.BindName)
require.Equal(t, api.BindingRuleBindTypeRole, rule.BindType)
require.Equal(t, "serviceaccount.namespace==alt and serviceaccount.name==demo", rule.Selector)
})
t.Run("update all fields but description", func(t *testing.T) {
id := createRule(t)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-id", id,
"-bind-type", "role",
"-bind-name=role-updated",
"-selector=serviceaccount.namespace==alt and serviceaccount.name==demo",
}
code := cmd.Run(args)
require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
rule, _, err := client.ACL().BindingRuleRead(
id,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, rule)
require.Equal(t, "test rule", rule.Description)
require.Equal(t, api.BindingRuleBindTypeRole, rule.BindType)
require.Equal(t, "role-updated", rule.BindName)
require.Equal(t, "serviceaccount.namespace==alt and serviceaccount.name==demo", rule.Selector)
})
t.Run("update all fields but bind name", func(t *testing.T) {
id := createRule(t)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-id", id,
"-description=test rule edited",
"-bind-type", "role",
"-selector=serviceaccount.namespace==alt and serviceaccount.name==demo",
}
code := cmd.Run(args)
require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
rule, _, err := client.ACL().BindingRuleRead(
id,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, rule)
require.Equal(t, "test rule edited", rule.Description)
require.Equal(t, api.BindingRuleBindTypeRole, rule.BindType)
require.Equal(t, "test-${serviceaccount.name}", rule.BindName)
require.Equal(t, "serviceaccount.namespace==alt and serviceaccount.name==demo", rule.Selector)
})
t.Run("update all fields but must exist", func(t *testing.T) {
id := createRule(t)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-id", id,
"-description=test rule edited",
"-bind-name=role-updated",
"-selector=serviceaccount.namespace==alt and serviceaccount.name==demo",
}
code := cmd.Run(args)
require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
rule, _, err := client.ACL().BindingRuleRead(
id,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, rule)
require.Equal(t, "test rule edited", rule.Description)
require.Equal(t, api.BindingRuleBindTypeService, rule.BindType)
require.Equal(t, "role-updated", rule.BindName)
require.Equal(t, "serviceaccount.namespace==alt and serviceaccount.name==demo", rule.Selector)
})
t.Run("update all fields but selector", func(t *testing.T) {
id := createRule(t)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-id", id,
"-description=test rule edited",
"-bind-type", "role",
"-bind-name=role-updated",
}
code := cmd.Run(args)
require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
rule, _, err := client.ACL().BindingRuleRead(
id,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, rule)
require.Equal(t, "test rule edited", rule.Description)
require.Equal(t, api.BindingRuleBindTypeRole, rule.BindType)
require.Equal(t, "role-updated", rule.BindName)
require.Equal(t, "serviceaccount.namespace==default", rule.Selector)
})
t.Run("update all fields clear selector", func(t *testing.T) {
id := createRule(t)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-id", id,
"-description=test rule edited",
"-bind-type", "role",
"-bind-name=role-updated",
"-selector=",
}
code := cmd.Run(args)
require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
rule, _, err := client.ACL().BindingRuleRead(
id,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, rule)
require.Equal(t, "test rule edited", rule.Description)
require.Equal(t, api.BindingRuleBindTypeRole, rule.BindType)
require.Equal(t, "role-updated", rule.BindName)
require.Empty(t, rule.Selector)
})
}
func TestBindingRuleUpdateCommand_noMerge(t *testing.T) {
t.Parallel()
testDir := testutil.TempDir(t, "acl")
defer os.RemoveAll(testDir)
a := agent.NewTestAgent(t, t.Name(), `
primary_datacenter = "dc1"
acl {
enabled = true
tokens {
master = "root"
}
}`)
a.Agent.LogWriter = logger.NewLogWriter(512)
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
client := a.Client()
// create an auth method in advance
{
_, _, err := client.ACL().AuthMethodCreate(
&api.ACLAuthMethod{
Name: "test",
Type: "testing",
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
}
deleteRules := func(t *testing.T) {
rules, _, err := client.ACL().BindingRuleList(
"test",
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
for _, rule := range rules {
_, err := client.ACL().BindingRuleDelete(
rule.ID,
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
}
}
t.Run("rule id required", func(t *testing.T) {
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-no-merge",
}
ui := cli.NewMockUi()
cmd := New(ui)
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Cannot update a binding rule without specifying the -id parameter")
})
t.Run("rule id partial matches nothing", func(t *testing.T) {
fakeID, err := uuid.GenerateUUID()
require.NoError(t, err)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-id=" + fakeID[0:5],
"-token=root",
"-no-merge",
"-description=test rule edited",
}
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Error determining binding rule ID")
})
t.Run("rule id exact match doesn't exist", func(t *testing.T) {
fakeID, err := uuid.GenerateUUID()
require.NoError(t, err)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-id=" + fakeID,
"-token=root",
"-no-merge",
"-description=test rule edited",
}
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Binding rule not found with ID")
})
createRule := func(t *testing.T) string {
rule, _, err := client.ACL().BindingRuleCreate(
&api.ACLBindingRule{
AuthMethod: "test",
Description: "test rule",
BindType: api.BindingRuleBindTypeRole,
BindName: "test-${serviceaccount.name}",
Selector: "serviceaccount.namespace==default",
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
return rule.ID
}
createDupe := func(t *testing.T) string {
for {
// Check for 1-char duplicates.
rules, _, err := client.ACL().BindingRuleList(
"test",
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
m := make(map[byte]struct{})
for _, rule := range rules {
c := rule.ID[0]
if _, ok := m[c]; ok {
return string(c)
}
m[c] = struct{}{}
}
_ = createRule(t)
}
}
t.Run("rule id partial matches multiple", func(t *testing.T) {
prefix := createDupe(t)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-id=" + prefix,
"-token=root",
"-no-merge",
"-description=test rule edited",
}
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Error determining binding rule ID")
})
t.Run("must use roughly valid selector", func(t *testing.T) {
id := createRule(t)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-no-merge",
"-id", id,
"-description=test rule edited",
"-bind-type", "service",
"-bind-name=role-updated",
"-selector", "foo",
}
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Selector is invalid")
})
t.Run("update all fields", func(t *testing.T) {
id := createRule(t)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-no-merge",
"-id", id,
"-description=test rule edited",
"-bind-type", "service",
"-bind-name=role-updated",
"-selector=serviceaccount.namespace==alt and serviceaccount.name==demo",
}
code := cmd.Run(args)
require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
rule, _, err := client.ACL().BindingRuleRead(
id,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, rule)
require.Equal(t, "test rule edited", rule.Description)
require.Equal(t, api.BindingRuleBindTypeService, rule.BindType)
require.Equal(t, "role-updated", rule.BindName)
require.Equal(t, "serviceaccount.namespace==alt and serviceaccount.name==demo", rule.Selector)
})
t.Run("update all fields - partial", func(t *testing.T) {
deleteRules(t) // reset since we created a bunch that might be dupes
id := createRule(t)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-no-merge",
"-id", id[0:5],
"-description=test rule edited",
"-bind-type", "service",
"-bind-name=role-updated",
"-selector=serviceaccount.namespace==alt and serviceaccount.name==demo",
}
code := cmd.Run(args)
require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
rule, _, err := client.ACL().BindingRuleRead(
id,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, rule)
require.Equal(t, "test rule edited", rule.Description)
require.Equal(t, api.BindingRuleBindTypeService, rule.BindType)
require.Equal(t, "role-updated", rule.BindName)
require.Equal(t, "serviceaccount.namespace==alt and serviceaccount.name==demo", rule.Selector)
})
t.Run("update all fields but description", func(t *testing.T) {
id := createRule(t)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-no-merge",
"-id", id,
"-bind-type", "service",
"-bind-name=role-updated",
"-selector=serviceaccount.namespace==alt and serviceaccount.name==demo",
}
code := cmd.Run(args)
require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
rule, _, err := client.ACL().BindingRuleRead(
id,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, rule)
require.Empty(t, rule.Description)
require.Equal(t, api.BindingRuleBindTypeService, rule.BindType)
require.Equal(t, "role-updated", rule.BindName)
require.Equal(t, "serviceaccount.namespace==alt and serviceaccount.name==demo", rule.Selector)
})
t.Run("missing bind name", func(t *testing.T) {
id := createRule(t)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-no-merge",
"-id=" + id,
"-description=test rule edited",
"-bind-type", "role",
"-selector=serviceaccount.namespace==alt and serviceaccount.name==demo",
}
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Missing required '-bind-name' flag")
})
t.Run("update all fields but selector", func(t *testing.T) {
id := createRule(t)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-no-merge",
"-id", id,
"-description=test rule edited",
"-bind-type", "service",
"-bind-name=role-updated",
}
code := cmd.Run(args)
require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
rule, _, err := client.ACL().BindingRuleRead(
id,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, rule)
require.Equal(t, "test rule edited", rule.Description)
require.Equal(t, api.BindingRuleBindTypeService, rule.BindType)
require.Equal(t, "role-updated", rule.BindName)
require.Empty(t, rule.Selector)
})
}

View File

@ -0,0 +1,134 @@
package rolecreate
import (
"flag"
"fmt"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/acl"
aclhelpers "github.com/hashicorp/consul/command/acl"
"github.com/hashicorp/consul/command/flags"
"github.com/mitchellh/cli"
)
func New(ui cli.Ui) *cmd {
c := &cmd{UI: ui}
c.init()
return c
}
type cmd struct {
UI cli.Ui
flags *flag.FlagSet
http *flags.HTTPFlags
help string
name string
description string
policyIDs []string
policyNames []string
serviceIdents []string
showMeta bool
}
func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.flags.BoolVar(&c.showMeta, "meta", false, "Indicates that role metadata such "+
"as the content hash and raft indices should be shown for each entry")
c.flags.StringVar(&c.name, "name", "", "The new role's name. This flag is required.")
c.flags.StringVar(&c.description, "description", "", "A description of the role")
c.flags.Var((*flags.AppendSliceValue)(&c.policyIDs), "policy-id", "ID of a "+
"policy to use for this role. May be specified multiple times")
c.flags.Var((*flags.AppendSliceValue)(&c.policyNames), "policy-name", "Name of a "+
"policy to use for this role. May be specified multiple times")
c.flags.Var((*flags.AppendSliceValue)(&c.serviceIdents), "service-identity", "Name of a "+
"service identity to use for this role. May be specified multiple times. Format is "+
"the SERVICENAME or SERVICENAME:DATACENTER1,DATACENTER2,...")
c.http = &flags.HTTPFlags{}
flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.ServerFlags())
c.help = flags.Usage(help, c.flags)
}
func (c *cmd) Run(args []string) int {
if err := c.flags.Parse(args); err != nil {
return 1
}
if c.name == "" {
c.UI.Error(fmt.Sprintf("Missing require '-name' flag"))
c.UI.Error(c.Help())
return 1
}
if len(c.policyNames) == 0 && len(c.policyIDs) == 0 && len(c.serviceIdents) == 0 {
c.UI.Error(fmt.Sprintf("Cannot create a role without specifying -policy-name, -policy-id, or -service-identity at least once"))
return 1
}
client, err := c.http.APIClient()
if err != nil {
c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
newRole := &api.ACLRole{
Name: c.name,
Description: c.description,
}
for _, policyName := range c.policyNames {
// We could resolve names to IDs here but there isn't any reason why its would be better
// than allowing the agent to do it.
newRole.Policies = append(newRole.Policies, &api.ACLRolePolicyLink{Name: policyName})
}
for _, policyID := range c.policyIDs {
policyID, err := acl.GetPolicyIDFromPartial(client, policyID)
if err != nil {
c.UI.Error(fmt.Sprintf("Error resolving policy ID %s: %v", policyID, err))
return 1
}
newRole.Policies = append(newRole.Policies, &api.ACLRolePolicyLink{ID: policyID})
}
parsedServiceIdents, err := acl.ExtractServiceIdentities(c.serviceIdents)
if err != nil {
c.UI.Error(err.Error())
return 1
}
newRole.ServiceIdentities = parsedServiceIdents
role, _, err := client.ACL().RoleCreate(newRole, nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to create new role: %v", err))
return 1
}
aclhelpers.PrintRole(role, c.UI, c.showMeta)
return 0
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return flags.Usage(c.help, nil)
}
const synopsis = "Create an ACL Role"
const help = `
Usage: consul acl role create -name NAME [options]
Create a new role:
$ consul acl role create -name "new-role" \
-description "This is an example role" \
-policy-id b52fc3de-5 \
-policy-name "acl-replication" \
-service-identity "web" \
-service-identity "db:east,west"
`

View File

@ -0,0 +1,116 @@
package rolecreate
import (
"os"
"strings"
"testing"
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/logger"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/testrpc"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
)
func TestRoleCreateCommand_noTabs(t *testing.T) {
t.Parallel()
if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') {
t.Fatal("help has tabs")
}
}
func TestRoleCreateCommand(t *testing.T) {
t.Parallel()
testDir := testutil.TempDir(t, "acl")
defer os.RemoveAll(testDir)
a := agent.NewTestAgent(t, t.Name(), `
primary_datacenter = "dc1"
acl {
enabled = true
tokens {
master = "root"
}
}`)
a.Agent.LogWriter = logger.NewLogWriter(512)
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
ui := cli.NewMockUi()
cmd := New(ui)
// Create a policy
client := a.Client()
policy, _, err := client.ACL().PolicyCreate(
&api.ACLPolicy{Name: "test-policy"},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
// create with policy by name
{
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-name=role-with-policy-by-name",
"-description=test-role",
"-policy-name=" + policy.Name,
}
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
}
// create with policy by id
{
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-name=role-with-policy-by-id",
"-description=test-role",
"-policy-id=" + policy.ID,
}
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
}
// create with service identity
{
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-name=role-with-service-identity",
"-description=test-role",
"-service-identity=web",
}
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
}
// create with service identity scoped to 2 DCs
{
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-name=role-with-service-identity-in-2-dcs",
"-description=test-role",
"-service-identity=db:abc,xyz",
}
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
}
}

View File

@ -0,0 +1,98 @@
package roledelete
import (
"flag"
"fmt"
"github.com/hashicorp/consul/command/acl"
"github.com/hashicorp/consul/command/flags"
"github.com/mitchellh/cli"
)
func New(ui cli.Ui) *cmd {
c := &cmd{UI: ui}
c.init()
return c
}
type cmd struct {
UI cli.Ui
flags *flag.FlagSet
http *flags.HTTPFlags
help string
roleID string
roleName string
}
func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.flags.StringVar(&c.roleID, "id", "", "The ID of the role to delete. "+
"It may be specified as a unique ID prefix but will error if the prefix "+
"matches multiple role IDs")
c.flags.StringVar(&c.roleName, "name", "", "The name of the role to delete.")
c.http = &flags.HTTPFlags{}
flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.ServerFlags())
c.help = flags.Usage(help, c.flags)
}
func (c *cmd) Run(args []string) int {
if err := c.flags.Parse(args); err != nil {
return 1
}
if c.roleID == "" && c.roleName == "" {
c.UI.Error(fmt.Sprintf("Must specify the -id or -name parameters"))
return 1
}
client, err := c.http.APIClient()
if err != nil {
c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
var roleID string
if c.roleID != "" {
roleID, err = acl.GetRoleIDFromPartial(client, c.roleID)
} else {
roleID, err = acl.GetRoleIDByName(client, c.roleName)
}
if err != nil {
c.UI.Error(fmt.Sprintf("Error determining role ID: %v", err))
return 1
}
if _, err := client.ACL().RoleDelete(roleID, nil); err != nil {
c.UI.Error(fmt.Sprintf("Error deleting role %q: %v", roleID, err))
return 1
}
c.UI.Info(fmt.Sprintf("Role %q deleted successfully", roleID))
return 0
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return flags.Usage(c.help, nil)
}
const synopsis = "Delete an ACL Role"
const help = `
Usage: consul acl role delete [options] -id ROLE
Deletes an ACL role by providing the ID or a unique ID prefix.
Delete by prefix:
$ consul acl role delete -id b6b85
Delete by full ID:
$ consul acl role delete -id b6b856da-5193-4e78-845a-7d61ca8371ba
`

View File

@ -0,0 +1,141 @@
package roledelete
import (
"fmt"
"os"
"strings"
"testing"
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/logger"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/testrpc"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
)
func TestRoleDeleteCommand_noTabs(t *testing.T) {
t.Parallel()
if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') {
t.Fatal("help has tabs")
}
}
func TestRoleDeleteCommand(t *testing.T) {
t.Parallel()
testDir := testutil.TempDir(t, "acl")
defer os.RemoveAll(testDir)
a := agent.NewTestAgent(t, t.Name(), `
primary_datacenter = "dc1"
acl {
enabled = true
tokens {
master = "root"
}
}`)
a.Agent.LogWriter = logger.NewLogWriter(512)
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
client := a.Client()
t.Run("id or name required", func(t *testing.T) {
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
}
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Must specify the -id or -name parameters")
})
t.Run("delete works", func(t *testing.T) {
// Create a role
role, _, err := client.ACL().RoleCreate(
&api.ACLRole{
Name: "test-role-for-id-delete",
ServiceIdentities: []*api.ACLServiceIdentity{
&api.ACLServiceIdentity{
ServiceName: "fake",
},
},
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-id=" + role.ID,
}
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
output := ui.OutputWriter.String()
require.Contains(t, output, fmt.Sprintf("deleted successfully"))
require.Contains(t, output, role.ID)
role, _, err = client.ACL().RoleRead(
role.ID,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.Nil(t, role)
})
t.Run("delete works via prefixes", func(t *testing.T) {
// Create a role
role, _, err := client.ACL().RoleCreate(
&api.ACLRole{
Name: "test-role-for-id-prefix-delete",
ServiceIdentities: []*api.ACLServiceIdentity{
&api.ACLServiceIdentity{
ServiceName: "fake",
},
},
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-id=" + role.ID[0:5],
}
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
output := ui.OutputWriter.String()
require.Contains(t, output, fmt.Sprintf("deleted successfully"))
require.Contains(t, output, role.ID)
role, _, err = client.ACL().RoleRead(
role.ID,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.Nil(t, role)
})
}

View File

@ -0,0 +1,79 @@
package rolelist
import (
"flag"
"fmt"
"github.com/hashicorp/consul/command/acl"
"github.com/hashicorp/consul/command/flags"
"github.com/mitchellh/cli"
)
func New(ui cli.Ui) *cmd {
c := &cmd{UI: ui}
c.init()
return c
}
type cmd struct {
UI cli.Ui
flags *flag.FlagSet
http *flags.HTTPFlags
help string
showMeta bool
}
func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.flags.BoolVar(&c.showMeta, "meta", false, "Indicates that policy metadata such "+
"as the content hash and raft indices should be shown for each entry")
c.http = &flags.HTTPFlags{}
flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.ServerFlags())
c.help = flags.Usage(help, c.flags)
}
func (c *cmd) Run(args []string) int {
if err := c.flags.Parse(args); err != nil {
return 1
}
client, err := c.http.APIClient()
if err != nil {
c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
roles, _, err := client.ACL().RoleList(nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to retrieve the role list: %v", err))
return 1
}
for _, role := range roles {
acl.PrintRoleListEntry(role, c.UI, c.showMeta)
}
return 0
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return flags.Usage(c.help, nil)
}
const synopsis = "Lists ACL Roles"
const help = `
Usage: consul acl role list [options]
Lists all the ACL roles.
Example:
$ consul acl role list
`

View File

@ -0,0 +1,83 @@
package rolelist
import (
"fmt"
"os"
"strings"
"testing"
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/logger"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/testrpc"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
)
func TestRoleListCommand_noTabs(t *testing.T) {
t.Parallel()
if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') {
t.Fatal("help has tabs")
}
}
func TestRoleListCommand(t *testing.T) {
t.Parallel()
require := require.New(t)
testDir := testutil.TempDir(t, "acl")
defer os.RemoveAll(testDir)
a := agent.NewTestAgent(t, t.Name(), `
primary_datacenter = "dc1"
acl {
enabled = true
tokens {
master = "root"
}
}`)
a.Agent.LogWriter = logger.NewLogWriter(512)
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
ui := cli.NewMockUi()
cmd := New(ui)
var roleIDs []string
// Create a couple roles to list
client := a.Client()
svcids := []*api.ACLServiceIdentity{
&api.ACLServiceIdentity{ServiceName: "fake"},
}
for i := 0; i < 5; i++ {
name := fmt.Sprintf("test-role-%d", i)
role, _, err := client.ACL().RoleCreate(
&api.ACLRole{Name: name, ServiceIdentities: svcids},
&api.WriteOptions{Token: "root"},
)
roleIDs = append(roleIDs, role.ID)
require.NoError(err)
}
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
}
code := cmd.Run(args)
require.Equal(code, 0)
require.Empty(ui.ErrorWriter.String())
output := ui.OutputWriter.String()
for i, v := range roleIDs {
require.Contains(output, fmt.Sprintf("test-role-%d", i))
require.Contains(output, v)
}
}

View File

@ -0,0 +1,115 @@
package roleread
import (
"flag"
"fmt"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/acl"
"github.com/hashicorp/consul/command/flags"
"github.com/mitchellh/cli"
)
func New(ui cli.Ui) *cmd {
c := &cmd{UI: ui}
c.init()
return c
}
type cmd struct {
UI cli.Ui
flags *flag.FlagSet
http *flags.HTTPFlags
help string
roleID string
roleName string
showMeta bool
}
func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.flags.BoolVar(&c.showMeta, "meta", false, "Indicates that role metadata such "+
"as the content hash and raft indices should be shown for each entry")
c.flags.StringVar(&c.roleID, "id", "", "The ID of the role to read. "+
"It may be specified as a unique ID prefix but will error if the prefix "+
"matches multiple policy IDs")
c.flags.StringVar(&c.roleName, "name", "", "The name of the role to read.")
c.http = &flags.HTTPFlags{}
flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.ServerFlags())
c.help = flags.Usage(help, c.flags)
}
func (c *cmd) Run(args []string) int {
if err := c.flags.Parse(args); err != nil {
return 1
}
if c.roleID == "" && c.roleName == "" {
c.UI.Error(fmt.Sprintf("Must specify either the -id or -name parameters"))
return 1
}
client, err := c.http.APIClient()
if err != nil {
c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
var role *api.ACLRole
if c.roleID != "" {
roleID, err := acl.GetRoleIDFromPartial(client, c.roleID)
if err != nil {
c.UI.Error(fmt.Sprintf("Error determining role ID: %v", err))
return 1
}
role, _, err = client.ACL().RoleRead(roleID, nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Error reading role %q: %v", roleID, err))
return 1
} else if role == nil {
c.UI.Error(fmt.Sprintf("Role not found with ID %q", roleID))
return 1
}
} else {
role, _, err = client.ACL().RoleReadByName(c.roleName, nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Error reading role %q: %v", c.roleName, err))
return 1
} else if role == nil {
c.UI.Error(fmt.Sprintf("Role not found with name %q", c.roleName))
return 1
}
}
acl.PrintRole(role, c.UI, c.showMeta)
return 0
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return flags.Usage(c.help, nil)
}
const synopsis = "Read an ACL Role"
const help = `
Usage: consul acl role read [options] ROLE
This command will retrieve and print out the details
of a single role.
Read:
$ consul acl role read -id fdabbcb5-9de5-4b1a-961f-77214ae88cba
Read by name:
$ consul acl role read -name my-policy
`

View File

@ -0,0 +1,194 @@
package roleread
import (
"fmt"
"os"
"strings"
"testing"
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/logger"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/testrpc"
"github.com/hashicorp/go-uuid"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
)
func TestRoleReadCommand_noTabs(t *testing.T) {
t.Parallel()
if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') {
t.Fatal("help has tabs")
}
}
func TestRoleReadCommand(t *testing.T) {
t.Parallel()
testDir := testutil.TempDir(t, "acl")
defer os.RemoveAll(testDir)
a := agent.NewTestAgent(t, t.Name(), `
primary_datacenter = "dc1"
acl {
enabled = true
tokens {
master = "root"
}
}`)
a.Agent.LogWriter = logger.NewLogWriter(512)
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
client := a.Client()
t.Run("id or name required", func(t *testing.T) {
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
}
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Must specify either the -id or -name parameters")
})
t.Run("read by id not found", func(t *testing.T) {
fakeID, err := uuid.GenerateUUID()
require.NoError(t, err)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-id=" + fakeID,
}
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Role not found with ID")
})
t.Run("read by name not found", func(t *testing.T) {
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-name=blah",
}
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Role not found with name")
})
t.Run("read by id", func(t *testing.T) {
// create a role
role, _, err := client.ACL().RoleCreate(
&api.ACLRole{
Name: "test-role-by-id",
ServiceIdentities: []*api.ACLServiceIdentity{
&api.ACLServiceIdentity{
ServiceName: "fake",
},
},
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-id=" + role.ID,
}
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
output := ui.OutputWriter.String()
require.Contains(t, output, fmt.Sprintf("test-role"))
require.Contains(t, output, role.ID)
})
t.Run("read by id prefix", func(t *testing.T) {
// create a role
role, _, err := client.ACL().RoleCreate(
&api.ACLRole{
Name: "test-role-by-id-prefix",
ServiceIdentities: []*api.ACLServiceIdentity{
&api.ACLServiceIdentity{
ServiceName: "fake",
},
},
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-id=" + role.ID[0:5],
}
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
output := ui.OutputWriter.String()
require.Contains(t, output, fmt.Sprintf("test-role"))
require.Contains(t, output, role.ID)
})
t.Run("read by name", func(t *testing.T) {
// create a role
role, _, err := client.ACL().RoleCreate(
&api.ACLRole{
Name: "test-role-by-name",
ServiceIdentities: []*api.ACLServiceIdentity{
&api.ACLServiceIdentity{
ServiceName: "fake",
},
},
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-name=" + role.Name,
}
code := cmd.Run(args)
require.Equal(t, code, 0)
require.Empty(t, ui.ErrorWriter.String())
output := ui.OutputWriter.String()
require.Contains(t, output, fmt.Sprintf("test-role"))
require.Contains(t, output, role.ID)
})
}

56
command/acl/role/role.go Normal file
View File

@ -0,0 +1,56 @@
package role
import (
"github.com/hashicorp/consul/command/flags"
"github.com/mitchellh/cli"
)
func New() *cmd {
return &cmd{}
}
type cmd struct{}
func (c *cmd) Run(args []string) int {
return cli.RunResultHelp
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return flags.Usage(help, nil)
}
const synopsis = "Manage Consul's ACL Roles"
const help = `
Usage: consul acl role <subcommand> [options] [args]
This command has subcommands for managing Consul's ACL Roles.
Here are some simple examples, and more detailed examples are available
in the subcommands or the documentation.
Create a new ACL Role:
$ consul acl role create -name "new-role" \
-description "This is an example role" \
-policy-id 06acc965
List all roles:
$ consul acl role list
Update a role:
$ consul acl role update -name "other-role" -datacenter "dc1"
Read a role:
$ consul acl role read -id 0479e93e-091c-4475-9b06-79a004765c24
Delete a role
$ consul acl role delete -name "my-role"
For more examples, ask for subcommand help or view the documentation.
`

View File

@ -0,0 +1,225 @@
package roleupdate
import (
"flag"
"fmt"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/acl"
"github.com/hashicorp/consul/command/flags"
"github.com/mitchellh/cli"
)
func New(ui cli.Ui) *cmd {
c := &cmd{UI: ui}
c.init()
return c
}
type cmd struct {
UI cli.Ui
flags *flag.FlagSet
http *flags.HTTPFlags
help string
roleID string
name string
description string
policyIDs []string
policyNames []string
serviceIdents []string
noMerge bool
showMeta bool
}
func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.flags.BoolVar(&c.showMeta, "meta", false, "Indicates that role metadata such "+
"as the content hash and raft indices should be shown for each entry")
c.flags.StringVar(&c.roleID, "id", "", "The ID of the role to update. "+
"It may be specified as a unique ID prefix but will error if the prefix "+
"matches multiple role IDs")
c.flags.StringVar(&c.name, "name", "", "The role name.")
c.flags.StringVar(&c.description, "description", "", "A description of the role")
c.flags.Var((*flags.AppendSliceValue)(&c.policyIDs), "policy-id", "ID of a "+
"policy to use for this role. May be specified multiple times")
c.flags.Var((*flags.AppendSliceValue)(&c.policyNames), "policy-name", "Name of a "+
"policy to use for this role. May be specified multiple times")
c.flags.Var((*flags.AppendSliceValue)(&c.serviceIdents), "service-identity", "Name of a "+
"service identity to use for this role. May be specified multiple times. Format is "+
"the SERVICENAME or SERVICENAME:DATACENTER1,DATACENTER2,...")
c.flags.BoolVar(&c.noMerge, "no-merge", false, "Do not merge the current role "+
"information with what is provided to the command. Instead overwrite all fields "+
"with the exception of the role ID which is immutable.")
c.http = &flags.HTTPFlags{}
flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.ServerFlags())
c.help = flags.Usage(help, c.flags)
}
func (c *cmd) Run(args []string) int {
if err := c.flags.Parse(args); err != nil {
return 1
}
if c.roleID == "" {
c.UI.Error(fmt.Sprintf("Cannot update a role without specifying the -id parameter"))
return 1
}
client, err := c.http.APIClient()
if err != nil {
c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
roleID, err := acl.GetRoleIDFromPartial(client, c.roleID)
if err != nil {
c.UI.Error(fmt.Sprintf("Error determining role ID: %v", err))
return 1
}
parsedServiceIdents, err := acl.ExtractServiceIdentities(c.serviceIdents)
if err != nil {
c.UI.Error(err.Error())
return 1
}
// Read the current role in both cases so we can fail better if not found.
currentRole, _, err := client.ACL().RoleRead(roleID, nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Error when retrieving current role: %v", err))
return 1
} else if currentRole == nil {
c.UI.Error(fmt.Sprintf("Role not found with ID %q", roleID))
return 1
}
var role *api.ACLRole
if c.noMerge {
role = &api.ACLRole{
ID: c.roleID,
Name: c.name,
Description: c.description,
ServiceIdentities: parsedServiceIdents,
}
for _, policyName := range c.policyNames {
// We could resolve names to IDs here but there isn't any reason
// why its would be better than allowing the agent to do it.
role.Policies = append(role.Policies, &api.ACLRolePolicyLink{Name: policyName})
}
for _, policyID := range c.policyIDs {
policyID, err := acl.GetPolicyIDFromPartial(client, policyID)
if err != nil {
c.UI.Error(fmt.Sprintf("Error resolving policy ID %s: %v", policyID, err))
return 1
}
role.Policies = append(role.Policies, &api.ACLRolePolicyLink{ID: policyID})
}
} else {
role = currentRole
if c.name != "" {
role.Name = c.name
}
if c.description != "" {
role.Description = c.description
}
for _, policyName := range c.policyNames {
found := false
for _, link := range role.Policies {
if link.Name == policyName {
found = true
break
}
}
if !found {
// We could resolve names to IDs here but there isn't any
// reason why its would be better than allowing the agent to do
// it.
role.Policies = append(role.Policies, &api.ACLRolePolicyLink{Name: policyName})
}
}
for _, policyID := range c.policyIDs {
policyID, err := acl.GetPolicyIDFromPartial(client, policyID)
if err != nil {
c.UI.Error(fmt.Sprintf("Error resolving policy ID %s: %v", policyID, err))
return 1
}
found := false
for _, link := range role.Policies {
if link.ID == policyID {
found = true
break
}
}
if !found {
role.Policies = append(role.Policies, &api.ACLRolePolicyLink{ID: policyID})
}
}
for _, svcid := range parsedServiceIdents {
found := -1
for i, link := range role.ServiceIdentities {
if link.ServiceName == svcid.ServiceName {
found = i
break
}
}
if found != -1 {
role.ServiceIdentities[found] = svcid
} else {
role.ServiceIdentities = append(role.ServiceIdentities, svcid)
}
}
}
role, _, err = client.ACL().RoleUpdate(role, nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Error updating role %q: %v", roleID, err))
return 1
}
c.UI.Info(fmt.Sprintf("Role updated successfully"))
acl.PrintRole(role, c.UI, c.showMeta)
return 0
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return flags.Usage(c.help, nil)
}
const synopsis = "Update an ACL Role"
const help = `
Usage: consul acl role update [options]
Updates a role. By default it will merge the role information with its
current state so that you do not have to provide all parameters. This
behavior can be disabled by passing -no-merge.
Rename the Role:
$ consul acl role update -id abcd -name "better-name"
Update all editable fields of the role:
$ consul acl role update -id abcd \
-name "better-name" \
-description "replication" \
-policy-name "token-replication" \
-service-identity "web"
`

View File

@ -0,0 +1,398 @@
package roleupdate
import (
"os"
"strings"
"testing"
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/logger"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/testrpc"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
uuid "github.com/hashicorp/go-uuid"
)
func TestRoleUpdateCommand_noTabs(t *testing.T) {
t.Parallel()
if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') {
t.Fatal("help has tabs")
}
}
func TestRoleUpdateCommand(t *testing.T) {
t.Parallel()
testDir := testutil.TempDir(t, "acl")
defer os.RemoveAll(testDir)
a := agent.NewTestAgent(t, t.Name(), `
primary_datacenter = "dc1"
acl {
enabled = true
tokens {
master = "root"
}
}`)
a.Agent.LogWriter = logger.NewLogWriter(512)
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
client := a.Client()
// Create 2 policies
policy1, _, err := client.ACL().PolicyCreate(
&api.ACLPolicy{Name: "test-policy1"},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
policy2, _, err := client.ACL().PolicyCreate(
&api.ACLPolicy{Name: "test-policy2"},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
// create a role
role, _, err := client.ACL().RoleCreate(
&api.ACLRole{
Name: "test-role",
ServiceIdentities: []*api.ACLServiceIdentity{
&api.ACLServiceIdentity{
ServiceName: "fake",
},
},
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
t.Run("update a role that does not exist", func(t *testing.T) {
fakeID, err := uuid.GenerateUUID()
require.NoError(t, err)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-id=" + fakeID,
"-token=root",
"-policy-name=" + policy1.Name,
"-description=test role edited",
}
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Role not found with ID")
})
t.Run("update with policy by name", func(t *testing.T) {
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-id=" + role.ID,
"-token=root",
"-policy-name=" + policy1.Name,
"-description=test role edited",
}
code := cmd.Run(args)
require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
role, _, err := client.ACL().RoleRead(
role.ID,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, role)
require.Equal(t, "test role edited", role.Description)
require.Len(t, role.Policies, 1)
require.Len(t, role.ServiceIdentities, 1)
})
t.Run("update with policy by id", func(t *testing.T) {
// also update with no description shouldn't delete the current
// description
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-id=" + role.ID,
"-token=root",
"-policy-id=" + policy2.ID,
}
code := cmd.Run(args)
require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
role, _, err := client.ACL().RoleRead(
role.ID,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, role)
require.Equal(t, "test role edited", role.Description)
require.Len(t, role.Policies, 2)
require.Len(t, role.ServiceIdentities, 1)
})
t.Run("update with service identity", func(t *testing.T) {
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-id=" + role.ID,
"-token=root",
"-service-identity=web",
}
code := cmd.Run(args)
require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
role, _, err := client.ACL().RoleRead(
role.ID,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, role)
require.Equal(t, "test role edited", role.Description)
require.Len(t, role.Policies, 2)
require.Len(t, role.ServiceIdentities, 2)
})
t.Run("update with service identity scoped to 2 DCs", func(t *testing.T) {
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-id=" + role.ID,
"-token=root",
"-service-identity=db:abc,xyz",
}
code := cmd.Run(args)
require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
role, _, err := client.ACL().RoleRead(
role.ID,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, role)
require.Equal(t, "test role edited", role.Description)
require.Len(t, role.Policies, 2)
require.Len(t, role.ServiceIdentities, 3)
})
}
func TestRoleUpdateCommand_noMerge(t *testing.T) {
t.Parallel()
testDir := testutil.TempDir(t, "acl")
defer os.RemoveAll(testDir)
a := agent.NewTestAgent(t, t.Name(), `
primary_datacenter = "dc1"
acl {
enabled = true
tokens {
master = "root"
}
}`)
a.Agent.LogWriter = logger.NewLogWriter(512)
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
client := a.Client()
// Create 3 policies
policy1, _, err := client.ACL().PolicyCreate(
&api.ACLPolicy{Name: "test-policy1"},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
policy2, _, err := client.ACL().PolicyCreate(
&api.ACLPolicy{Name: "test-policy2"},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
policy3, _, err := client.ACL().PolicyCreate(
&api.ACLPolicy{Name: "test-policy3"},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
// create a role
createRole := func(t *testing.T) *api.ACLRole {
roleUnq, err := uuid.GenerateUUID()
require.NoError(t, err)
role, _, err := client.ACL().RoleCreate(
&api.ACLRole{
Name: "test-role-" + roleUnq,
Description: "original description",
ServiceIdentities: []*api.ACLServiceIdentity{
&api.ACLServiceIdentity{
ServiceName: "fake",
},
},
Policies: []*api.ACLRolePolicyLink{
&api.ACLRolePolicyLink{
ID: policy3.ID,
},
},
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
return role
}
t.Run("update a role that does not exist", func(t *testing.T) {
fakeID, err := uuid.GenerateUUID()
require.NoError(t, err)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-id=" + fakeID,
"-token=root",
"-policy-name=" + policy1.Name,
"-no-merge",
"-description=test role edited",
}
code := cmd.Run(args)
require.Equal(t, code, 1)
require.Contains(t, ui.ErrorWriter.String(), "Role not found with ID")
})
t.Run("update with policy by name", func(t *testing.T) {
role := createRole(t)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-id=" + role.ID,
"-name=" + role.Name,
"-token=root",
"-no-merge",
"-policy-name=" + policy1.Name,
}
code := cmd.Run(args)
require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
role, _, err := client.ACL().RoleRead(
role.ID,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, role)
require.Equal(t, "", role.Description)
require.Len(t, role.Policies, 1)
require.Len(t, role.ServiceIdentities, 0)
})
t.Run("update with policy by id", func(t *testing.T) {
role := createRole(t)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-id=" + role.ID,
"-name=" + role.Name,
"-token=root",
"-no-merge",
"-policy-id=" + policy2.ID,
}
code := cmd.Run(args)
require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
role, _, err := client.ACL().RoleRead(
role.ID,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, role)
require.Equal(t, "", role.Description)
require.Len(t, role.Policies, 1)
require.Len(t, role.ServiceIdentities, 0)
})
t.Run("update with service identity", func(t *testing.T) {
role := createRole(t)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-id=" + role.ID,
"-name=" + role.Name,
"-token=root",
"-no-merge",
"-service-identity=web",
}
code := cmd.Run(args)
require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
role, _, err := client.ACL().RoleRead(
role.ID,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, role)
require.Equal(t, "", role.Description)
require.Len(t, role.Policies, 0)
require.Len(t, role.ServiceIdentities, 1)
})
t.Run("update with service identity scoped to 2 DCs", func(t *testing.T) {
role := createRole(t)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-id=" + role.ID,
"-name=" + role.Name,
"-token=root",
"-no-merge",
"-service-identity=db:abc,xyz",
}
code := cmd.Run(args)
require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
role, _, err := client.ACL().RoleRead(
role.ID,
&api.QueryOptions{Token: "root"},
)
require.NoError(t, err)
require.NotNil(t, role)
require.Equal(t, "", role.Description)
require.Len(t, role.Policies, 0)
require.Len(t, role.ServiceIdentities, 1)
})
}

View File

@ -19,11 +19,11 @@ import (
func parseCloneOutput(t *testing.T, output string) *api.ACLToken {
// This will only work for non-legacy tokens
re := regexp.MustCompile("Token cloned successfully.\n" +
"AccessorID: ([a-zA-Z0-9\\-]{36})\n" +
"SecretID: ([a-zA-Z0-9\\-]{36})\n" +
"Description: ([^\n]*)\n" +
"Local: (true|false)\n" +
"Create Time: ([^\n]+)\n" +
"AccessorID: ([a-zA-Z0-9\\-]{36})\n" +
"SecretID: ([a-zA-Z0-9\\-]{36})\n" +
"Description: ([^\n]*)\n" +
"Local: (true|false)\n" +
"Create Time: ([^\n]+)\n" +
"Policies:\n" +
"( [a-zA-Z0-9\\-]{36} - [^\n]+\n)*")

View File

@ -3,6 +3,7 @@ package tokencreate
import (
"flag"
"fmt"
"time"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/acl"
@ -22,11 +23,15 @@ type cmd struct {
http *flags.HTTPFlags
help string
policyIDs []string
policyNames []string
description string
local bool
showMeta bool
policyIDs []string
policyNames []string
roleIDs []string
roleNames []string
serviceIdents []string
expirationTTL time.Duration
description string
local bool
showMeta bool
}
func (c *cmd) init() {
@ -39,6 +44,15 @@ func (c *cmd) init() {
"policy to use for this token. May be specified multiple times")
c.flags.Var((*flags.AppendSliceValue)(&c.policyNames), "policy-name", "Name of a "+
"policy to use for this token. May be specified multiple times")
c.flags.Var((*flags.AppendSliceValue)(&c.roleIDs), "role-id", "ID of a "+
"role to use for this token. May be specified multiple times")
c.flags.Var((*flags.AppendSliceValue)(&c.roleNames), "role-name", "Name of a "+
"role to use for this token. May be specified multiple times")
c.flags.Var((*flags.AppendSliceValue)(&c.serviceIdents), "service-identity", "Name of a "+
"service identity to use for this token. May be specified multiple times. Format is "+
"the SERVICENAME or SERVICENAME:DATACENTER1,DATACENTER2,...")
c.flags.DurationVar(&c.expirationTTL, "expires-ttl", 0, "Duration of time this "+
"token should be valid for")
c.http = &flags.HTTPFlags{}
flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.ServerFlags())
@ -50,8 +64,10 @@ func (c *cmd) Run(args []string) int {
return 1
}
if len(c.policyNames) == 0 && len(c.policyIDs) == 0 {
c.UI.Error(fmt.Sprintf("Cannot create a token without specifying -policy-name or -policy-id at least once"))
if len(c.policyNames) == 0 && len(c.policyIDs) == 0 &&
len(c.roleNames) == 0 && len(c.roleIDs) == 0 &&
len(c.serviceIdents) == 0 {
c.UI.Error(fmt.Sprintf("Cannot create a token without specifying -policy-name, -policy-id, -role-name, -role-id, or -service-identity at least once"))
return 1
}
@ -65,6 +81,16 @@ func (c *cmd) Run(args []string) int {
Description: c.description,
Local: c.local,
}
if c.expirationTTL > 0 {
newToken.ExpirationTTL = c.expirationTTL
}
parsedServiceIdents, err := acl.ExtractServiceIdentities(c.serviceIdents)
if err != nil {
c.UI.Error(err.Error())
return 1
}
newToken.ServiceIdentities = parsedServiceIdents
for _, policyName := range c.policyNames {
// We could resolve names to IDs here but there isn't any reason why its would be better
@ -81,6 +107,21 @@ func (c *cmd) Run(args []string) int {
newToken.Policies = append(newToken.Policies, &api.ACLTokenPolicyLink{ID: policyID})
}
for _, roleName := range c.roleNames {
// We could resolve names to IDs here but there isn't any reason why its would be better
// than allowing the agent to do it.
newToken.Roles = append(newToken.Roles, &api.ACLTokenRoleLink{Name: roleName})
}
for _, roleID := range c.roleIDs {
roleID, err := acl.GetRoleIDFromPartial(client, roleID)
if err != nil {
c.UI.Error(fmt.Sprintf("Error resolving role ID %s: %v", roleID, err))
return 1
}
newToken.Roles = append(newToken.Roles, &api.ACLTokenRoleLink{ID: roleID})
}
token, _, err := client.ACL().TokenCreate(newToken, nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to create new token: %v", err))
@ -109,7 +150,11 @@ Usage: consul acl token create [options]
Create a new token:
$ consul acl token create -description "Replication token"
-policy-id b52fc3de-5
-policy-name "acl-replication"
$ consul acl token create -description "Replication token" \
-policy-id b52fc3de-5 \
-policy-name "acl-replication" \
-role-id c630d4ef-6 \
-role-name "db-updater" \
-service-identity "web" \
-service-identity "db:east,west"
`

View File

@ -22,13 +22,18 @@ type cmd struct {
http *flags.HTTPFlags
help string
tokenID string
policyIDs []string
policyNames []string
description string
mergePolicies bool
showMeta bool
upgradeLegacy bool
tokenID string
policyIDs []string
policyNames []string
roleIDs []string
roleNames []string
serviceIdents []string
description string
mergePolicies bool
mergeRoles bool
mergeServiceIdents bool
showMeta bool
upgradeLegacy bool
}
func (c *cmd) init() {
@ -37,6 +42,10 @@ func (c *cmd) init() {
"as the content hash and raft indices should be shown for each entry")
c.flags.BoolVar(&c.mergePolicies, "merge-policies", false, "Merge the new policies "+
"with the existing policies")
c.flags.BoolVar(&c.mergeRoles, "merge-roles", false, "Merge the new roles "+
"with the existing roles")
c.flags.BoolVar(&c.mergeServiceIdents, "merge-service-identities", false, "Merge the new service identities "+
"with the existing service identities")
c.flags.StringVar(&c.tokenID, "id", "", "The Accessor ID of the token to read. "+
"It may be specified as a unique ID prefix but will error if the prefix "+
"matches multiple token Accessor IDs")
@ -45,6 +54,13 @@ func (c *cmd) init() {
"policy to use for this token. May be specified multiple times")
c.flags.Var((*flags.AppendSliceValue)(&c.policyNames), "policy-name", "Name of a "+
"policy to use for this token. May be specified multiple times")
c.flags.Var((*flags.AppendSliceValue)(&c.roleIDs), "role-id", "ID of a "+
"role to use for this token. May be specified multiple times")
c.flags.Var((*flags.AppendSliceValue)(&c.roleNames), "role-name", "Name of a "+
"role to use for this token. May be specified multiple times")
c.flags.Var((*flags.AppendSliceValue)(&c.serviceIdents), "service-identity", "Name of a "+
"service identity to use for this token. May be specified multiple times. Format is "+
"the SERVICENAME or SERVICENAME:DATACENTER1,DATACENTER2,...")
c.flags.BoolVar(&c.upgradeLegacy, "upgrade-legacy", false, "Add new polices "+
"to a legacy token replacing all existing rules. This will cause the legacy "+
"token to behave exactly like a new token but keep the same Secret.\n"+
@ -107,6 +123,12 @@ func (c *cmd) Run(args []string) int {
token.Description = c.description
}
parsedServiceIdents, err := acl.ExtractServiceIdentities(c.serviceIdents)
if err != nil {
c.UI.Error(err.Error())
return 1
}
if c.mergePolicies {
for _, policyName := range c.policyNames {
found := false
@ -162,6 +184,81 @@ func (c *cmd) Run(args []string) int {
}
}
if c.mergeRoles {
for _, roleName := range c.roleNames {
found := false
for _, link := range token.Roles {
if link.Name == roleName {
found = true
break
}
}
if !found {
// We could resolve names to IDs here but there isn't any reason why its would be better
// than allowing the agent to do it.
token.Roles = append(token.Roles, &api.ACLTokenRoleLink{Name: roleName})
}
}
for _, roleID := range c.roleIDs {
roleID, err := acl.GetRoleIDFromPartial(client, roleID)
if err != nil {
c.UI.Error(fmt.Sprintf("Error resolving role ID %s: %v", roleID, err))
return 1
}
found := false
for _, link := range token.Roles {
if link.ID == roleID {
found = true
break
}
}
if !found {
token.Roles = append(token.Roles, &api.ACLTokenRoleLink{Name: roleID})
}
}
} else {
token.Roles = nil
for _, roleName := range c.roleNames {
// We could resolve names to IDs here but there isn't any reason why its would be better
// than allowing the agent to do it.
token.Roles = append(token.Roles, &api.ACLTokenRoleLink{Name: roleName})
}
for _, roleID := range c.roleIDs {
roleID, err := acl.GetRoleIDFromPartial(client, roleID)
if err != nil {
c.UI.Error(fmt.Sprintf("Error resolving role ID %s: %v", roleID, err))
return 1
}
token.Roles = append(token.Roles, &api.ACLTokenRoleLink{ID: roleID})
}
}
if c.mergeServiceIdents {
for _, svcid := range parsedServiceIdents {
found := -1
for i, link := range token.ServiceIdentities {
if link.ServiceName == svcid.ServiceName {
found = i
break
}
}
if found != -1 {
token.ServiceIdentities[found] = svcid
} else {
token.ServiceIdentities = append(token.ServiceIdentities, svcid)
}
}
} else {
token.ServiceIdentities = parsedServiceIdents
}
token, _, err = client.ACL().TokenUpdate(token, nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to update token %s: %v", tokenID, err))
@ -192,7 +289,10 @@ Usage: consul acl token update [options]
$ consul acl token update -id abcd -description "replication" -merge-policies
Update all editable fields of the token:
Update all editable fields of the token:
$ consul acl token update -id abcd -description "replication" -policy-name "token-replication"
$ consul acl token update -id abcd \
-description "replication" \
-policy-name "token-replication" \
-role-name "db-updater"
`

View File

@ -3,6 +3,18 @@ package command
import (
"github.com/hashicorp/consul/command/acl"
aclagent "github.com/hashicorp/consul/command/acl/agenttokens"
aclam "github.com/hashicorp/consul/command/acl/authmethod"
aclamcreate "github.com/hashicorp/consul/command/acl/authmethod/create"
aclamdelete "github.com/hashicorp/consul/command/acl/authmethod/delete"
aclamlist "github.com/hashicorp/consul/command/acl/authmethod/list"
aclamread "github.com/hashicorp/consul/command/acl/authmethod/read"
aclamupdate "github.com/hashicorp/consul/command/acl/authmethod/update"
aclbr "github.com/hashicorp/consul/command/acl/bindingrule"
aclbrcreate "github.com/hashicorp/consul/command/acl/bindingrule/create"
aclbrdelete "github.com/hashicorp/consul/command/acl/bindingrule/delete"
aclbrlist "github.com/hashicorp/consul/command/acl/bindingrule/list"
aclbrread "github.com/hashicorp/consul/command/acl/bindingrule/read"
aclbrupdate "github.com/hashicorp/consul/command/acl/bindingrule/update"
aclbootstrap "github.com/hashicorp/consul/command/acl/bootstrap"
aclpolicy "github.com/hashicorp/consul/command/acl/policy"
aclpcreate "github.com/hashicorp/consul/command/acl/policy/create"
@ -10,6 +22,12 @@ import (
aclplist "github.com/hashicorp/consul/command/acl/policy/list"
aclpread "github.com/hashicorp/consul/command/acl/policy/read"
aclpupdate "github.com/hashicorp/consul/command/acl/policy/update"
aclrole "github.com/hashicorp/consul/command/acl/role"
aclrcreate "github.com/hashicorp/consul/command/acl/role/create"
aclrdelete "github.com/hashicorp/consul/command/acl/role/delete"
aclrlist "github.com/hashicorp/consul/command/acl/role/list"
aclrread "github.com/hashicorp/consul/command/acl/role/read"
aclrupdate "github.com/hashicorp/consul/command/acl/role/update"
aclrules "github.com/hashicorp/consul/command/acl/rules"
acltoken "github.com/hashicorp/consul/command/acl/token"
acltclone "github.com/hashicorp/consul/command/acl/token/clone"
@ -51,6 +69,8 @@ import (
kvput "github.com/hashicorp/consul/command/kv/put"
"github.com/hashicorp/consul/command/leave"
"github.com/hashicorp/consul/command/lock"
login "github.com/hashicorp/consul/command/login"
logout "github.com/hashicorp/consul/command/logout"
"github.com/hashicorp/consul/command/maint"
"github.com/hashicorp/consul/command/members"
"github.com/hashicorp/consul/command/monitor"
@ -106,6 +126,24 @@ func init() {
Register("acl token read", func(ui cli.Ui) (cli.Command, error) { return acltread.New(ui), nil })
Register("acl token update", func(ui cli.Ui) (cli.Command, error) { return acltupdate.New(ui), nil })
Register("acl token delete", func(ui cli.Ui) (cli.Command, error) { return acltdelete.New(ui), nil })
Register("acl role", func(cli.Ui) (cli.Command, error) { return aclrole.New(), nil })
Register("acl role create", func(ui cli.Ui) (cli.Command, error) { return aclrcreate.New(ui), nil })
Register("acl role list", func(ui cli.Ui) (cli.Command, error) { return aclrlist.New(ui), nil })
Register("acl role read", func(ui cli.Ui) (cli.Command, error) { return aclrread.New(ui), nil })
Register("acl role update", func(ui cli.Ui) (cli.Command, error) { return aclrupdate.New(ui), nil })
Register("acl role delete", func(ui cli.Ui) (cli.Command, error) { return aclrdelete.New(ui), nil })
Register("acl auth-method", func(cli.Ui) (cli.Command, error) { return aclam.New(), nil })
Register("acl auth-method create", func(ui cli.Ui) (cli.Command, error) { return aclamcreate.New(ui), nil })
Register("acl auth-method list", func(ui cli.Ui) (cli.Command, error) { return aclamlist.New(ui), nil })
Register("acl auth-method read", func(ui cli.Ui) (cli.Command, error) { return aclamread.New(ui), nil })
Register("acl auth-method update", func(ui cli.Ui) (cli.Command, error) { return aclamupdate.New(ui), nil })
Register("acl auth-method delete", func(ui cli.Ui) (cli.Command, error) { return aclamdelete.New(ui), nil })
Register("acl binding-rule", func(cli.Ui) (cli.Command, error) { return aclbr.New(), nil })
Register("acl binding-rule create", func(ui cli.Ui) (cli.Command, error) { return aclbrcreate.New(ui), nil })
Register("acl binding-rule list", func(ui cli.Ui) (cli.Command, error) { return aclbrlist.New(ui), nil })
Register("acl binding-rule read", func(ui cli.Ui) (cli.Command, error) { return aclbrread.New(ui), nil })
Register("acl binding-rule update", func(ui cli.Ui) (cli.Command, error) { return aclbrupdate.New(ui), nil })
Register("acl binding-rule delete", func(ui cli.Ui) (cli.Command, error) { return aclbrdelete.New(ui), nil })
Register("agent", func(ui cli.Ui) (cli.Command, error) {
return agent.New(ui, rev, ver, verPre, verHuman, make(chan struct{})), nil
})
@ -141,6 +179,8 @@ func init() {
Register("kv put", func(ui cli.Ui) (cli.Command, error) { return kvput.New(ui), nil })
Register("leave", func(ui cli.Ui) (cli.Command, error) { return leave.New(ui), nil })
Register("lock", func(ui cli.Ui) (cli.Command, error) { return lock.New(ui), nil })
Register("login", func(ui cli.Ui) (cli.Command, error) { return login.New(ui), nil })
Register("logout", func(ui cli.Ui) (cli.Command, error) { return logout.New(ui), nil })
Register("maint", func(ui cli.Ui) (cli.Command, error) { return maint.New(ui), nil })
Register("members", func(ui cli.Ui) (cli.Command, error) { return members.New(ui), nil })
Register("monitor", func(ui cli.Ui) (cli.Command, error) { return monitor.New(ui, MakeShutdownCh()), nil })

View File

@ -104,7 +104,7 @@ func (c *cmd) Run(args []string) int {
// enabled.
c.grpcAddr = "localhost:8502"
}
if c.http.Token() == "" {
if c.http.Token() == "" && c.http.TokenFile() == "" {
// Extra check needed since CONSUL_HTTP_TOKEN has not been consulted yet but
// calling SetToken with empty will force that to override the
if proxyToken := os.Getenv(proxyAgent.EnvProxyToken); proxyToken != "" {

View File

@ -129,7 +129,7 @@ func (c *cmd) Run(args []string) int {
if c.sidecarFor == "" {
c.sidecarFor = os.Getenv(proxyAgent.EnvSidecarFor)
}
if c.http.Token() == "" {
if c.http.Token() == "" && c.http.TokenFile() == "" {
c.http.SetToken(os.Getenv(proxyAgent.EnvProxyToken))
}

View File

@ -2,6 +2,8 @@ package flags
import (
"flag"
"io/ioutil"
"strings"
"github.com/hashicorp/consul/api"
)
@ -10,6 +12,7 @@ type HTTPFlags struct {
// client api flags
address StringValue
token StringValue
tokenFile StringValue
caFile StringValue
caPath StringValue
certFile StringValue
@ -33,6 +36,10 @@ func (f *HTTPFlags) ClientFlags() *flag.FlagSet {
"ACL token to use in the request. This can also be specified via the "+
"CONSUL_HTTP_TOKEN environment variable. If unspecified, the query will "+
"default to the token of the Consul agent at the HTTP address.")
fs.Var(&f.tokenFile, "token-file",
"File containing the ACL token to use in the request instead of one specified "+
"via the -token argument or CONSUL_HTTP_TOKEN environment variable. "+
"This can also be specified via the CONSUL_HTTP_TOKEN_FILE environment variable.")
fs.Var(&f.caFile, "ca-file",
"Path to a CA file to use for TLS when communicating with Consul. This "+
"can also be specified via the CONSUL_CACERT environment variable.")
@ -88,6 +95,28 @@ func (f *HTTPFlags) SetToken(v string) error {
return f.token.Set(v)
}
func (f *HTTPFlags) TokenFile() string {
return f.tokenFile.String()
}
func (f *HTTPFlags) SetTokenFile(v string) error {
return f.tokenFile.Set(v)
}
func (f *HTTPFlags) ReadTokenFile() (string, error) {
tokenFile := f.tokenFile.String()
if tokenFile == "" {
return "", nil
}
data, err := ioutil.ReadFile(tokenFile)
if err != nil {
return "", err
}
return strings.TrimSpace(string(data)), nil
}
func (f *HTTPFlags) APIClient() (*api.Client, error) {
c := api.DefaultConfig()
@ -99,6 +128,7 @@ func (f *HTTPFlags) APIClient() (*api.Client, error) {
func (f *HTTPFlags) MergeOntoConfig(c *api.Config) {
f.address.Merge(&c.Address)
f.token.Merge(&c.Token)
f.tokenFile.Merge(&c.TokenFile)
f.caFile.Merge(&c.TLSConfig.CAFile)
f.caPath.Merge(&c.TLSConfig.CAPath)
f.certFile.Merge(&c.TLSConfig.CertFile)

148
command/login/login.go Normal file
View File

@ -0,0 +1,148 @@
package login
import (
"flag"
"fmt"
"io/ioutil"
"strings"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/flags"
"github.com/hashicorp/consul/lib/file"
"github.com/mitchellh/cli"
)
func New(ui cli.Ui) *cmd {
c := &cmd{UI: ui}
c.init()
return c
}
type cmd struct {
UI cli.Ui
flags *flag.FlagSet
http *flags.HTTPFlags
help string
shutdownCh <-chan struct{}
bearerToken string
// flags
authMethodName string
bearerTokenFile string
tokenSinkFile string
meta map[string]string
}
func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.flags.StringVar(&c.authMethodName, "method", "",
"Name of the auth method to login to.")
c.flags.StringVar(&c.bearerTokenFile, "bearer-token-file", "",
"Path to a file containing a secret bearer token to use with this auth method.")
c.flags.StringVar(&c.tokenSinkFile, "token-sink-file", "",
"The most recent token's SecretID is kept up to date in this file.")
c.flags.Var((*flags.FlagMapValue)(&c.meta), "meta",
"Metadata to set on the token, formatted as key=value. This flag "+
"may be specified multiple times to set multiple meta fields.")
c.http = &flags.HTTPFlags{}
flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.ServerFlags())
c.help = flags.Usage(help, c.flags)
}
func (c *cmd) Run(args []string) int {
if err := c.flags.Parse(args); err != nil {
return 1
}
if len(c.flags.Args()) > 0 {
c.UI.Error(fmt.Sprintf("Should have no non-flag arguments."))
return 1
}
if c.authMethodName == "" {
c.UI.Error(fmt.Sprintf("Missing required '-method' flag"))
return 1
}
if c.tokenSinkFile == "" {
c.UI.Error(fmt.Sprintf("Missing required '-token-sink-file' flag"))
return 1
}
if c.bearerTokenFile == "" {
c.UI.Error(fmt.Sprintf("Missing required '-bearer-token-file' flag"))
return 1
}
data, err := ioutil.ReadFile(c.bearerTokenFile)
if err != nil {
c.UI.Error(err.Error())
return 1
}
c.bearerToken = strings.TrimSpace(string(data))
if c.bearerToken == "" {
c.UI.Error(fmt.Sprintf("No bearer token found in %s", c.bearerTokenFile))
return 1
}
// Ensure that we don't try to use a token when performing a login
// operation.
c.http.SetToken("")
c.http.SetTokenFile("")
client, err := c.http.APIClient()
if err != nil {
c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
// Do the login.
req := &api.ACLLoginParams{
AuthMethod: c.authMethodName,
BearerToken: c.bearerToken,
Meta: c.meta,
}
tok, _, err := client.ACL().Login(req, nil)
if err != nil {
c.UI.Error(fmt.Sprintf("Error logging in: %s", err))
return 1
}
if err := c.writeToSink(tok); err != nil {
c.UI.Error(fmt.Sprintf("Error writing token to file sink: %s", err))
return 1
}
return 0
}
func (c *cmd) writeToSink(tok *api.ACLToken) error {
payload := []byte(tok.SecretID)
return file.WriteAtomicWithPerms(c.tokenSinkFile, payload, 0600)
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return flags.Usage(c.help, nil)
}
const synopsis = "Login to Consul using an Auth Method"
const help = `
Usage: consul login [options]
The login command will exchange the provided third party credentials with the
requested auth method for a newly minted Consul ACL Token. The companion
command 'consul logout' should be used to destroy any tokens created this way
to avoid a resource leak.
`

321
command/login/login_test.go Normal file
View File

@ -0,0 +1,321 @@
package login
import (
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/agent/consul/authmethod/kubeauth"
"github.com/hashicorp/consul/agent/consul/authmethod/testauth"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/acl"
"github.com/hashicorp/consul/logger"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/testrpc"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
)
func TestLoginCommand_noTabs(t *testing.T) {
t.Parallel()
if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') {
t.Fatal("help has tabs")
}
}
func TestLoginCommand(t *testing.T) {
t.Parallel()
testDir := testutil.TempDir(t, "acl")
defer os.RemoveAll(testDir)
a := agent.NewTestAgent(t, t.Name(), `
primary_datacenter = "dc1"
acl {
enabled = true
tokens {
master = "root"
}
}`)
a.Agent.LogWriter = logger.NewLogWriter(512)
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
client := a.Client()
t.Run("method is required", func(t *testing.T) {
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
}
code := cmd.Run(args)
require.Equal(t, code, 1, "err: %s", ui.ErrorWriter.String())
require.Contains(t, ui.ErrorWriter.String(), "Missing required '-method' flag")
})
tokenSinkFile := filepath.Join(testDir, "test.token")
t.Run("token-sink-file is required", func(t *testing.T) {
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-method=test",
}
code := cmd.Run(args)
require.Equal(t, code, 1, "err: %s", ui.ErrorWriter.String())
require.Contains(t, ui.ErrorWriter.String(), "Missing required '-token-sink-file' flag")
})
t.Run("bearer-token-file is required", func(t *testing.T) {
defer os.Remove(tokenSinkFile)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-method=test",
"-token-sink-file", tokenSinkFile,
}
code := cmd.Run(args)
require.Equal(t, code, 1, "err: %s", ui.ErrorWriter.String())
require.Contains(t, ui.ErrorWriter.String(), "Missing required '-bearer-token-file' flag")
})
bearerTokenFile := filepath.Join(testDir, "bearer.token")
t.Run("bearer-token-file is empty", func(t *testing.T) {
defer os.Remove(tokenSinkFile)
require.NoError(t, ioutil.WriteFile(bearerTokenFile, []byte(""), 0600))
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-method=test",
"-token-sink-file", tokenSinkFile,
"-bearer-token-file", bearerTokenFile,
}
code := cmd.Run(args)
require.Equal(t, code, 1, "err: %s", ui.ErrorWriter.String())
require.Contains(t, ui.ErrorWriter.String(), "No bearer token found in")
})
require.NoError(t, ioutil.WriteFile(bearerTokenFile, []byte("demo-token"), 0600))
t.Run("try login with no method configured", func(t *testing.T) {
defer os.Remove(tokenSinkFile)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-method=test",
"-token-sink-file", tokenSinkFile,
"-bearer-token-file", bearerTokenFile,
}
code := cmd.Run(args)
require.Equal(t, code, 1, "err: %s", ui.ErrorWriter.String())
require.Contains(t, ui.ErrorWriter.String(), "403 (ACL not found)")
})
testSessionID := testauth.StartSession()
defer testauth.ResetSession(testSessionID)
testauth.InstallSessionToken(
testSessionID,
"demo-token",
"default", "demo", "76091af4-4b56-11e9-ac4b-708b11801cbe",
)
{
_, _, err := client.ACL().AuthMethodCreate(
&api.ACLAuthMethod{
Name: "test",
Type: "testing",
Config: map[string]interface{}{
"SessionID": testSessionID,
},
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
}
t.Run("try login with method configured but no binding rules", func(t *testing.T) {
defer os.Remove(tokenSinkFile)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-method=test",
"-token-sink-file", tokenSinkFile,
"-bearer-token-file", bearerTokenFile,
}
code := cmd.Run(args)
require.Equal(t, 1, code, "err: %s", ui.ErrorWriter.String())
require.Contains(t, ui.ErrorWriter.String(), "403 (Permission denied)")
})
{
_, _, err := client.ACL().BindingRuleCreate(&api.ACLBindingRule{
AuthMethod: "test",
BindType: api.BindingRuleBindTypeService,
BindName: "${serviceaccount.name}",
Selector: "serviceaccount.namespace==default",
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
}
t.Run("try login with method configured and binding rules", func(t *testing.T) {
defer os.Remove(tokenSinkFile)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-method=test",
"-token-sink-file", tokenSinkFile,
"-bearer-token-file", bearerTokenFile,
}
code := cmd.Run(args)
require.Equal(t, 0, code, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
require.Empty(t, ui.OutputWriter.String())
raw, err := ioutil.ReadFile(tokenSinkFile)
require.NoError(t, err)
token := strings.TrimSpace(string(raw))
require.Len(t, token, 36, "must be a valid uid: %s", token)
})
}
func TestLoginCommand_k8s(t *testing.T) {
t.Parallel()
testDir := testutil.TempDir(t, "acl")
defer os.RemoveAll(testDir)
a := agent.NewTestAgent(t, t.Name(), `
primary_datacenter = "dc1"
acl {
enabled = true
tokens {
master = "root"
}
}`)
a.Agent.LogWriter = logger.NewLogWriter(512)
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
client := a.Client()
tokenSinkFile := filepath.Join(testDir, "test.token")
bearerTokenFile := filepath.Join(testDir, "bearer.token")
// the "B" jwt will be the one being reviewed
require.NoError(t, ioutil.WriteFile(bearerTokenFile, []byte(acl.TestKubernetesJWT_B), 0600))
// spin up a fake api server
testSrv := kubeauth.StartTestAPIServer(t)
defer testSrv.Stop()
testSrv.AuthorizeJWT(acl.TestKubernetesJWT_A)
testSrv.SetAllowedServiceAccount(
"default",
"demo",
"76091af4-4b56-11e9-ac4b-708b11801cbe",
"",
acl.TestKubernetesJWT_B,
)
{
_, _, err := client.ACL().AuthMethodCreate(
&api.ACLAuthMethod{
Name: "k8s",
Type: "kubernetes",
Config: map[string]interface{}{
"Host": testSrv.Addr(),
"CACert": testSrv.CACert(),
// the "A" jwt will be the one with token review privs
"ServiceAccountJWT": acl.TestKubernetesJWT_A,
},
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
}
{
_, _, err := client.ACL().BindingRuleCreate(&api.ACLBindingRule{
AuthMethod: "k8s",
BindType: api.BindingRuleBindTypeService,
BindName: "${serviceaccount.name}",
Selector: "serviceaccount.namespace==default",
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
}
t.Run("try login with method configured and binding rules", func(t *testing.T) {
defer os.Remove(tokenSinkFile)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=root",
"-method=k8s",
"-token-sink-file", tokenSinkFile,
"-bearer-token-file", bearerTokenFile,
}
code := cmd.Run(args)
require.Equal(t, 0, code, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
require.Empty(t, ui.OutputWriter.String())
raw, err := ioutil.ReadFile(tokenSinkFile)
require.NoError(t, err)
token := strings.TrimSpace(string(raw))
require.Len(t, token, 36, "must be a valid uid: %s", token)
})
}

70
command/logout/logout.go Normal file
View File

@ -0,0 +1,70 @@
package logout
import (
"flag"
"fmt"
"github.com/hashicorp/consul/command/flags"
"github.com/mitchellh/cli"
)
func New(ui cli.Ui) *cmd {
c := &cmd{UI: ui}
c.init()
return c
}
type cmd struct {
UI cli.Ui
flags *flag.FlagSet
http *flags.HTTPFlags
help string
}
func (c *cmd) init() {
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
c.http = &flags.HTTPFlags{}
flags.Merge(c.flags, c.http.ClientFlags())
flags.Merge(c.flags, c.http.ServerFlags())
c.help = flags.Usage(help, c.flags)
}
func (c *cmd) Run(args []string) int {
if err := c.flags.Parse(args); err != nil {
return 1
}
if len(c.flags.Args()) > 0 {
c.UI.Error(fmt.Sprintf("Should have no non-flag arguments."))
return 1
}
client, err := c.http.APIClient()
if err != nil {
c.UI.Error(fmt.Sprintf("Error connecting to Consul agent: %s", err))
return 1
}
if _, err := client.ACL().Logout(nil); err != nil {
c.UI.Error(fmt.Sprintf("Error destroying token: %v", err))
return 1
}
return 0
}
func (c *cmd) Synopsis() string {
return synopsis
}
func (c *cmd) Help() string {
return flags.Usage(c.help, nil)
}
const synopsis = "Destroy a Consul Token created with Login"
const help = `
Usage: consul logout [options]
The logout command will destroy the provided token if it was created from
'consul login'.
`

View File

@ -0,0 +1,299 @@
package logout
import (
"os"
"strings"
"testing"
"github.com/hashicorp/consul/agent"
"github.com/hashicorp/consul/agent/consul/authmethod/kubeauth"
"github.com/hashicorp/consul/agent/consul/authmethod/testauth"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/command/acl"
"github.com/hashicorp/consul/logger"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/testrpc"
"github.com/hashicorp/go-uuid"
"github.com/mitchellh/cli"
"github.com/stretchr/testify/require"
)
func TestLogout_noTabs(t *testing.T) {
t.Parallel()
if strings.ContainsRune(New(cli.NewMockUi()).Help(), '\t') {
t.Fatal("help has tabs")
}
}
func TestLogoutCommand(t *testing.T) {
t.Parallel()
testDir := testutil.TempDir(t, "acl")
defer os.RemoveAll(testDir)
a := agent.NewTestAgent(t, t.Name(), `
primary_datacenter = "dc1"
acl {
enabled = true
tokens {
master = "root"
}
}`)
a.Agent.LogWriter = logger.NewLogWriter(512)
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
client := a.Client()
t.Run("no token specified", func(t *testing.T) {
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
}
code := cmd.Run(args)
require.Equal(t, code, 1, "err: %s", ui.ErrorWriter.String())
require.Contains(t, ui.ErrorWriter.String(), "403 (ACL not found)")
})
t.Run("logout of deleted token", func(t *testing.T) {
fakeID, err := uuid.GenerateUUID()
require.NoError(t, err)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=" + fakeID,
}
code := cmd.Run(args)
require.Equal(t, code, 1, "err: %s", ui.ErrorWriter.String())
require.Contains(t, ui.ErrorWriter.String(), "403 (ACL not found)")
})
plainToken, _, err := client.ACL().TokenCreate(
&api.ACLToken{Description: "test"},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
t.Run("logout of ordinary token", func(t *testing.T) {
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=" + plainToken.SecretID,
}
code := cmd.Run(args)
require.Equal(t, code, 1, "err: %s", ui.ErrorWriter.String())
require.Contains(t, ui.ErrorWriter.String(), "403 (Permission denied)")
})
testSessionID := testauth.StartSession()
defer testauth.ResetSession(testSessionID)
testauth.InstallSessionToken(
testSessionID,
"demo-token",
"default", "demo", "76091af4-4b56-11e9-ac4b-708b11801cbe",
)
{
_, _, err := client.ACL().AuthMethodCreate(
&api.ACLAuthMethod{
Name: "test",
Type: "testing",
Config: map[string]interface{}{
"SessionID": testSessionID,
},
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
}
{
_, _, err := client.ACL().BindingRuleCreate(&api.ACLBindingRule{
AuthMethod: "test",
BindType: api.BindingRuleBindTypeService,
BindName: "${serviceaccount.name}",
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
}
var loginTokenSecret string
{
tok, _, err := client.ACL().Login(&api.ACLLoginParams{
AuthMethod: "test",
BearerToken: "demo-token",
}, nil)
require.NoError(t, err)
loginTokenSecret = tok.SecretID
}
t.Run("logout of login token", func(t *testing.T) {
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=" + loginTokenSecret,
}
code := cmd.Run(args)
require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
})
}
func TestLogoutCommand_k8s(t *testing.T) {
t.Parallel()
testDir := testutil.TempDir(t, "acl")
defer os.RemoveAll(testDir)
a := agent.NewTestAgent(t, t.Name(), `
primary_datacenter = "dc1"
acl {
enabled = true
tokens {
master = "root"
}
}`)
a.Agent.LogWriter = logger.NewLogWriter(512)
defer a.Shutdown()
testrpc.WaitForLeader(t, a.RPC, "dc1")
client := a.Client()
t.Run("no token specified", func(t *testing.T) {
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
}
code := cmd.Run(args)
require.Equal(t, code, 1, "err: %s", ui.ErrorWriter.String())
require.Contains(t, ui.ErrorWriter.String(), "403 (ACL not found)")
})
t.Run("logout of deleted token", func(t *testing.T) {
fakeID, err := uuid.GenerateUUID()
require.NoError(t, err)
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=" + fakeID,
}
code := cmd.Run(args)
require.Equal(t, code, 1, "err: %s", ui.ErrorWriter.String())
require.Contains(t, ui.ErrorWriter.String(), "403 (ACL not found)")
})
plainToken, _, err := client.ACL().TokenCreate(
&api.ACLToken{Description: "test"},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
t.Run("logout of ordinary token", func(t *testing.T) {
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=" + plainToken.SecretID,
}
code := cmd.Run(args)
require.Equal(t, code, 1, "err: %s", ui.ErrorWriter.String())
require.Contains(t, ui.ErrorWriter.String(), "403 (Permission denied)")
})
// go to the trouble of creating a login token
// require.NoError(t, ioutil.WriteFile(bearerTokenFile, []byte(acl.TestKubernetesJWT_B), 0600))
// spin up a fake api server
testSrv := kubeauth.StartTestAPIServer(t)
defer testSrv.Stop()
testSrv.AuthorizeJWT(acl.TestKubernetesJWT_A)
testSrv.SetAllowedServiceAccount(
"default",
"demo",
"76091af4-4b56-11e9-ac4b-708b11801cbe",
"",
acl.TestKubernetesJWT_B,
)
{
_, _, err := client.ACL().AuthMethodCreate(
&api.ACLAuthMethod{
Name: "k8s",
Type: "kubernetes",
Config: map[string]interface{}{
"Host": testSrv.Addr(),
"CACert": testSrv.CACert(),
// the "A" jwt will be the one with token review privs
"ServiceAccountJWT": acl.TestKubernetesJWT_A,
},
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
}
{
_, _, err := client.ACL().BindingRuleCreate(&api.ACLBindingRule{
AuthMethod: "k8s",
BindType: api.BindingRuleBindTypeService,
BindName: "${serviceaccount.name}",
},
&api.WriteOptions{Token: "root"},
)
require.NoError(t, err)
}
var loginTokenSecret string
{
tok, _, err := client.ACL().Login(&api.ACLLoginParams{
AuthMethod: "k8s",
BearerToken: acl.TestKubernetesJWT_B,
}, nil)
require.NoError(t, err)
loginTokenSecret = tok.SecretID
}
t.Run("logout of login token", func(t *testing.T) {
ui := cli.NewMockUi()
cmd := New(ui)
args := []string{
"-http-addr=" + a.HTTPAddr(),
"-token=" + loginTokenSecret,
}
code := cmd.Run(args)
require.Equal(t, code, 0, "err: %s", ui.ErrorWriter.String())
require.Empty(t, ui.ErrorWriter.String())
})
}

View File

@ -87,6 +87,14 @@ func (c *cmd) Run(args []string) int {
return 1
}
token := c.http.Token()
if tokenFromFile, err := c.http.ReadTokenFile(); err != nil {
c.UI.Error(fmt.Sprintf("Error loading token file: %s", err))
return 1
} else if tokenFromFile != "" {
token = tokenFromFile
}
// Compile the watch parameters
params := make(map[string]interface{})
if c.watchType != "" {
@ -95,8 +103,8 @@ func (c *cmd) Run(args []string) int {
if c.http.Datacenter() != "" {
params["datacenter"] = c.http.Datacenter()
}
if c.http.Token() != "" {
params["token"] = c.http.Token()
if token != "" {
params["token"] = token
}
if c.key != "" {
params["key"] = c.key

6
go.mod
View File

@ -123,7 +123,9 @@ require (
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect
gopkg.in/mgo.v2 v2.0.0-20160818020120-3f83fa500528 // indirect
gopkg.in/ory-am/dockertest.v3 v3.3.4 // indirect
gopkg.in/square/go-jose.v2 v2.3.1
gotest.tools v2.2.0+incompatible // indirect
k8s.io/api v0.0.0-20190118113203-912cbe2bfef3 // indirect
k8s.io/apimachinery v0.0.0-20180904193909-def12e63c512 // indirect
k8s.io/api v0.0.0-20190325185214-7544f9db76f6
k8s.io/apimachinery v0.0.0-20190223001710-c182ff3b9841
k8s.io/client-go v8.0.0+incompatible
)

10
go.sum
View File

@ -383,6 +383,8 @@ gopkg.in/mgo.v2 v2.0.0-20160818020120-3f83fa500528 h1:/saqWwm73dLmuzbNhe92F0QsZ/
gopkg.in/mgo.v2 v2.0.0-20160818020120-3f83fa500528/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA=
gopkg.in/ory-am/dockertest.v3 v3.3.4 h1:oen8RiwxVNxtQ1pRoV4e4jqh6UjNsOuIZ1NXns6jdcw=
gopkg.in/ory-am/dockertest.v3 v3.3.4/go.mod h1:s9mmoLkaGeAh97qygnNj4xWkiN7e1SKekYC6CovU+ek=
gopkg.in/square/go-jose.v2 v2.3.1 h1:SK5KegNXmKmqE342YYN2qPHEnUYeoMiXXl1poUlI+o4=
gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
@ -391,10 +393,10 @@ gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/api v0.0.0-20180806132203-61b11ee65332/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA=
k8s.io/api v0.0.0-20190118113203-912cbe2bfef3 h1:lV0+KGoNkvZOt4zGT4H83hQrzWMt/US/LSz4z4+BQS4=
k8s.io/api v0.0.0-20190118113203-912cbe2bfef3/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA=
k8s.io/api v0.0.0-20190325185214-7544f9db76f6 h1:9MWtbqhwTyDvF4cS1qAhxDb9Mi8taXiAu+5nEacl7gY=
k8s.io/api v0.0.0-20190325185214-7544f9db76f6/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA=
k8s.io/apimachinery v0.0.0-20180821005732-488889b0007f/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
k8s.io/apimachinery v0.0.0-20180904193909-def12e63c512 h1:/Z1m/6oEN6hE2SzWP4BHW2yATeUrBRr+1GxNf1Ny58Y=
k8s.io/apimachinery v0.0.0-20180904193909-def12e63c512/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
k8s.io/apimachinery v0.0.0-20190223001710-c182ff3b9841 h1:Q4RZrHNtlC/mSdC1sTrcZ5RchC/9vxLVj57pWiCBKv4=
k8s.io/apimachinery v0.0.0-20190223001710-c182ff3b9841/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
k8s.io/client-go v8.0.0+incompatible h1:tTI4hRmb1DRMl4fG6Vclfdi6nTM82oIrTT7HfitmxC4=
k8s.io/client-go v8.0.0+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s=

77
vendor/golang.org/x/crypto/pbkdf2/pbkdf2.go generated vendored Normal file
View File

@ -0,0 +1,77 @@
// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
Package pbkdf2 implements the key derivation function PBKDF2 as defined in RFC
2898 / PKCS #5 v2.0.
A key derivation function is useful when encrypting data based on a password
or any other not-fully-random data. It uses a pseudorandom function to derive
a secure encryption key based on the password.
While v2.0 of the standard defines only one pseudorandom function to use,
HMAC-SHA1, the drafted v2.1 specification allows use of all five FIPS Approved
Hash Functions SHA-1, SHA-224, SHA-256, SHA-384 and SHA-512 for HMAC. To
choose, you can pass the `New` functions from the different SHA packages to
pbkdf2.Key.
*/
package pbkdf2 // import "golang.org/x/crypto/pbkdf2"
import (
"crypto/hmac"
"hash"
)
// Key derives a key from the password, salt and iteration count, returning a
// []byte of length keylen that can be used as cryptographic key. The key is
// derived based on the method described as PBKDF2 with the HMAC variant using
// the supplied hash function.
//
// For example, to use a HMAC-SHA-1 based PBKDF2 key derivation function, you
// can get a derived key for e.g. AES-256 (which needs a 32-byte key) by
// doing:
//
// dk := pbkdf2.Key([]byte("some password"), salt, 4096, 32, sha1.New)
//
// Remember to get a good random salt. At least 8 bytes is recommended by the
// RFC.
//
// Using a higher iteration count will increase the cost of an exhaustive
// search but will also make derivation proportionally slower.
func Key(password, salt []byte, iter, keyLen int, h func() hash.Hash) []byte {
prf := hmac.New(h, password)
hashLen := prf.Size()
numBlocks := (keyLen + hashLen - 1) / hashLen
var buf [4]byte
dk := make([]byte, 0, numBlocks*hashLen)
U := make([]byte, hashLen)
for block := 1; block <= numBlocks; block++ {
// N.B.: || means concatenation, ^ means XOR
// for each block T_i = U_1 ^ U_2 ^ ... ^ U_iter
// U_1 = PRF(password, salt || uint(i))
prf.Reset()
prf.Write(salt)
buf[0] = byte(block >> 24)
buf[1] = byte(block >> 16)
buf[2] = byte(block >> 8)
buf[3] = byte(block)
prf.Write(buf[:4])
dk = prf.Sum(dk)
T := dk[len(dk)-hashLen:]
copy(U, T)
// U_n = PRF(password, U_(n-1))
for n := 2; n <= iter; n++ {
prf.Reset()
prf.Write(U)
U = U[:0]
U = prf.Sum(U)
for x := range U {
T[x] ^= U[x]
}
}
}
return dk[:keyLen]
}

1
vendor/gopkg.in/square/go-jose.v2/.gitcookies.sh.enc generated vendored Normal file
View File

@ -0,0 +1 @@
'|Ę&{tÄU|gGę(ěŹCy=+¨śňcű:u:/pś#~žü["±4¤!­nŮAŞDK<Šuf˙hĹażÂ:şü¸ˇ´B/ŁŘ¤ą¤ň_<C588>hÎŰSăT*wĚxĽŻťą-ç|ťŕŔÓ<C594>ŃÄäóĚ㣗A$$â6ŁÁâG)8nĎpűĆˡ3ĚšśoďĎB­]xÝ“Ó2l§G•|qRŢŻ ö2 5R–Ó×Ç$´ń˝YčˇŢÝ™lË«yAI"ŰŚ<C5B0>®íĂ»ąĽkÄ|Kĺţ[9ĆâŇĺ=°ú˙źń|@S•3 ó#ćťx?ľV„,ľSĆÝőśwPíogŇ6&V6 ©D.dBŠ 7

Some files were not shown because too many files have changed in this diff Show More