mirror of https://github.com/hashicorp/consul
Merge pull request #5617 from hashicorp/f-acl-ux
Secure ACL Introduction for Kubernetespull/5656/head
commit
c6722fc43d
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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.
|
||||
//
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
@ -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
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
@ -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"
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"`
|
||||
}
|
|
@ -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"
|
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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"`
|
||||
}
|
|
@ -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-
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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
|
@ -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")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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, "")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 (
|
||||
|
|
575
api/acl.go
575
api/acl.go
|
@ -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
|
||||
}
|
||||
|
|
60
api/api.go
60
api/api.go
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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.
|
||||
`
|
|
@ -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"
|
||||
`
|
|
@ -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())
|
||||
})
|
||||
}
|
|
@ -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"
|
||||
`
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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
|
||||
`
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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
|
||||
`
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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"
|
||||
`
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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.
|
||||
`
|
|
@ -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'
|
||||
`
|
|
@ -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())
|
||||
})
|
||||
}
|
|
@ -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
|
||||
`
|
|
@ -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")
|
||||
})
|
||||
}
|
|
@ -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"
|
||||
`
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
|
@ -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
|
||||
`
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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'
|
||||
`
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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"
|
||||
`
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
`
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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
|
||||
`
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
||||
`
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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.
|
||||
`
|
|
@ -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"
|
||||
`
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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)*")
|
||||
|
||||
|
|
|
@ -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"
|
||||
`
|
||||
|
|
|
@ -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"
|
||||
`
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -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 != "" {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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.
|
||||
`
|
|
@ -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)
|
||||
})
|
||||
}
|
|
@ -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'.
|
||||
`
|
|
@ -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())
|
||||
})
|
||||
}
|
|
@ -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
6
go.mod
|
@ -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
10
go.sum
|
@ -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=
|
||||
|
|
|
@ -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]
|
||||
}
|
|
@ -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ďĎvŽB–3ż]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
Loading…
Reference in New Issue