ACL: add ACL binding rule RPC and HTTP API handlers. (#15529)

This change add the RPC ACL binding rule handlers. These handlers
are responsible for the creation, updating, reading, and deletion
of binding rules.

The write handlers are feature gated so that they can only be used
when all federated servers are running the required version.

The HTTP API handlers and API SDK have also been added where
required. This allows the endpoints to be called from the API by users
and clients.
This commit is contained in:
James Rasell 2022-12-15 09:18:55 +01:00 committed by GitHub
parent 13f207ea78
commit 95c9ffa505
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 1456 additions and 2 deletions

View File

@ -211,6 +211,10 @@ var (
// errMissingACLAuthMethodName is the generic error to use when a call is
// missing the required ACL auth-method name parameter.
errMissingACLAuthMethodName = errors.New("missing ACL auth-method name")
// errMissingACLBindingRuleID is the generic error to use when a call is
// missing the required ACL binding rule ID parameter.
errMissingACLBindingRuleID = errors.New("missing ACL binding rule ID")
)
// ACLRoles is used to query the ACL Role endpoints.
@ -369,6 +373,75 @@ func (a *ACLAuthMethods) Get(authMethodName string, q *QueryOptions) (*ACLAuthMe
return &resp, qm, nil
}
// ACLBindingRules is used to query the ACL auth-methods endpoints.
type ACLBindingRules struct {
client *Client
}
// ACLBindingRules returns a new handle on the ACL auth-methods API client.
func (c *Client) ACLBindingRules() *ACLBindingRules {
return &ACLBindingRules{client: c}
}
// List is used to detail all the ACL binding rules currently stored within
// state.
func (a *ACLBindingRules) List(q *QueryOptions) ([]*ACLBindingRuleListStub, *QueryMeta, error) {
var resp []*ACLBindingRuleListStub
qm, err := a.client.query("/v1/acl/binding-rules", &resp, q)
if err != nil {
return nil, nil, err
}
return resp, qm, nil
}
// Create is used to create an ACL binding rule.
func (a *ACLBindingRules) Create(bindingRule *ACLBindingRule, w *WriteOptions) (*ACLBindingRule, *WriteMeta, error) {
var resp ACLBindingRule
wm, err := a.client.write("/v1/acl/binding-rule", bindingRule, &resp, w)
if err != nil {
return nil, nil, err
}
return &resp, wm, nil
}
// Update is used to update an existing ACL binding rule.
func (a *ACLBindingRules) Update(bindingRule *ACLBindingRule, w *WriteOptions) (*ACLBindingRule, *WriteMeta, error) {
if bindingRule.ID == "" {
return nil, nil, errMissingACLBindingRuleID
}
var resp ACLBindingRule
wm, err := a.client.write("/v1/acl/binding-rule/"+bindingRule.ID, bindingRule, &resp, w)
if err != nil {
return nil, nil, err
}
return &resp, wm, nil
}
// Delete is used to delete an ACL binding rule.
func (a *ACLBindingRules) Delete(bindingRuleID string, w *WriteOptions) (*WriteMeta, error) {
if bindingRuleID == "" {
return nil, errMissingACLBindingRuleID
}
wm, err := a.client.delete("/v1/acl/binding-rule/"+bindingRuleID, nil, nil, w)
if err != nil {
return nil, err
}
return wm, nil
}
// Get is used to look up an ACL binding rule.
func (a *ACLBindingRules) Get(bindingRuleID string, q *QueryOptions) (*ACLBindingRule, *QueryMeta, error) {
if bindingRuleID == "" {
return nil, nil, errMissingACLBindingRuleID
}
var resp ACLBindingRule
qm, err := a.client.query("/v1/acl/binding-rule/"+bindingRuleID, &resp, q)
if err != nil {
return nil, nil, err
}
return &resp, qm, nil
}
// ACLPolicyListStub is used to for listing ACL policies
type ACLPolicyListStub struct {
Name string
@ -647,7 +720,6 @@ type ACLAuthMethodListStub struct {
Name string
Type string
Default bool
Hash []byte
CreateIndex uint64
ModifyIndex uint64
@ -667,3 +739,80 @@ const (
// auth-method which uses the OIDC protocol.
ACLAuthMethodTypeOIDC = "OIDC"
)
// ACLBindingRule contains a direct relation to an ACLAuthMethod and represents
// a rule to apply when logging in via the named AuthMethod. This allows the
// transformation of OIDC provider claims, to Nomad based ACL concepts such as
// ACL Roles and Policies.
type ACLBindingRule struct {
// ID is an internally generated UUID for this rule and is controlled by
// Nomad.
ID string
// Description is a human-readable, operator set description that can
// provide additional context about the binding rule. This is an
// operational field.
Description string
// AuthMethod is the name of the auth method for which this rule applies
// to. This is required and the method must exist within state before the
// cluster administrator can create the rule.
AuthMethod string
// Selector is an expression that matches against verified identity
// attributes returned from the auth method during login. This is optional
// and when not set, provides a catch-all rule.
Selector string
// BindType adjusts how this binding rule is applied at login time. The
// valid values are ACLBindingRuleBindTypeRole and
// ACLBindingRuleBindTypePolicy.
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
CreateTime time.Time
ModifyTime time.Time
CreateIndex uint64
ModifyIndex uint64
}
const (
// ACLBindingRuleBindTypeRole is the ACL binding rule bind type that only
// allows the binding rule to function if a role exists at login-time. The
// role will be specified within the ACLBindingRule.BindName parameter, and
// will identify whether this is an ID or Name.
ACLBindingRuleBindTypeRole = "role"
// ACLBindingRuleBindTypePolicy is the ACL binding rule bind type that
// assigns a policy to the generate ACL token. The role will be specified
// within the ACLBindingRule.BindName parameter, and will be the policy
// name.
ACLBindingRuleBindTypePolicy = "policy"
)
// ACLBindingRuleListStub is the stub object returned when performing a listing
// of ACL binding rules.
type ACLBindingRuleListStub struct {
// ID is an internally generated UUID for this role and is controlled by
// Nomad.
ID string
// Description is a human-readable, operator set description that can
// provide additional context about the binding role. This is an
// operational field.
Description string
// AuthMethod is the name of the auth method for which this rule applies
// to. This is required and the method must exist within state before the
// cluster administrator can create the rule.
AuthMethod string
CreateIndex uint64
ModifyIndex uint64
}

View File

@ -618,7 +618,6 @@ func TestACLAuthMethods(t *testing.T) {
must.Len(t, 1, aclAuthMethodsListResp)
must.Eq(t, authMethod.Name, aclAuthMethodsListResp[0].Name)
must.True(t, aclAuthMethodsListResp[0].Default)
must.SliceNotEmpty(t, aclAuthMethodsListResp[0].Hash)
assertQueryMeta(t, queryMeta)
// Read the auth-method.
@ -655,3 +654,85 @@ func TestACLAuthMethods(t *testing.T) {
must.Len(t, 0, aclAuthMethodsListResp)
assertQueryMeta(t, queryMeta)
}
func TestACLBindingRules(t *testing.T) {
testutil.Parallel(t)
testClient, testServer, _ := makeACLClient(t, nil, nil)
defer testServer.Stop()
//
aclAuthMethod := ACLAuthMethod{
Name: "auth0",
Type: ACLAuthMethodTypeOIDC,
TokenLocality: ACLAuthMethodTokenLocalityGlobal,
MaxTokenTTL: 10 * time.Hour,
Default: true,
}
_, _, err := testClient.ACLAuthMethods().Create(&aclAuthMethod, nil)
must.NoError(t, err)
// An initial listing shouldn't return any results.
aclBindingRulesListResp, queryMeta, err := testClient.ACLBindingRules().List(nil)
must.NoError(t, err)
must.Len(t, 0, aclBindingRulesListResp)
assertQueryMeta(t, queryMeta)
// Create an ACL auth-method.
bindingRule := ACLBindingRule{
Description: "my-binding-rule",
AuthMethod: "auth0",
Selector: "nomad-engineering-team in list.groups",
BindType: "role",
BindName: "cluster-admin",
}
_, writeMeta, err := testClient.ACLBindingRules().Create(&bindingRule, nil)
must.NoError(t, err)
assertWriteMeta(t, writeMeta)
// Another listing should return one result.
aclBindingRulesListResp, queryMeta, err = testClient.ACLBindingRules().List(nil)
must.NoError(t, err)
must.Len(t, 1, aclBindingRulesListResp)
must.NotEq(t, "", aclBindingRulesListResp[0].ID)
must.Eq(t, "auth0", aclBindingRulesListResp[0].AuthMethod)
assertQueryMeta(t, queryMeta)
bindingRuleID := aclBindingRulesListResp[0].ID
// Read the binding rule.
aclBindingRuleReadResp, queryMeta, err := testClient.ACLBindingRules().Get(bindingRuleID, nil)
must.NoError(t, err)
assertQueryMeta(t, queryMeta)
must.NotNil(t, aclBindingRuleReadResp)
must.Eq(t, bindingRuleID, aclBindingRuleReadResp.ID)
must.Eq(t, bindingRule.Description, aclBindingRuleReadResp.Description)
must.Eq(t, bindingRule.AuthMethod, aclBindingRuleReadResp.AuthMethod)
must.Eq(t, bindingRule.Selector, aclBindingRuleReadResp.Selector)
must.Eq(t, bindingRule.BindType, aclBindingRuleReadResp.BindType)
must.Eq(t, bindingRule.BindName, aclBindingRuleReadResp.BindName)
// Update the binding rule description.
bindingRule.ID = bindingRuleID
bindingRule.Description = "my-binding-rule-updated"
aclBindingRuleUpdateResp, writeMeta, err := testClient.ACLBindingRules().Update(&bindingRule, nil)
must.NoError(t, err)
assertWriteMeta(t, writeMeta)
must.Eq(t, bindingRuleID, aclBindingRuleUpdateResp.ID)
must.Eq(t, bindingRule.Description, aclBindingRuleUpdateResp.Description)
must.Eq(t, bindingRule.AuthMethod, aclBindingRuleUpdateResp.AuthMethod)
must.Eq(t, bindingRule.Selector, aclBindingRuleUpdateResp.Selector)
must.Eq(t, bindingRule.BindType, aclBindingRuleUpdateResp.BindType)
must.Eq(t, bindingRule.BindName, aclBindingRuleUpdateResp.BindName)
// Delete the role.
writeMeta, err = testClient.ACLBindingRules().Delete(bindingRuleID, nil)
must.NoError(t, err)
assertWriteMeta(t, writeMeta)
// Make sure there are no ACL auth-methods now present.
aclBindingRulesListResp, queryMeta, err = testClient.ACLBindingRules().List(nil)
must.NoError(t, err)
must.Len(t, 0, aclBindingRulesListResp)
assertQueryMeta(t, queryMeta)
}

View File

@ -676,3 +676,156 @@ func (s *HTTPServer) aclAuthMethodUpsertRequest(
}
return nil, nil
}
// ACLBindingRuleListRequest performs a listing of ACL binding rules and is
// callable via the /v1/acl/binding-rules HTTP API.
func (s *HTTPServer) ACLBindingRuleListRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
// The endpoint only supports GET requests.
if req.Method != http.MethodGet {
return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod)
}
// Set up the request args and parse this to ensure the query options are
// set.
args := structs.ACLBindingRulesListRequest{}
if s.parse(resp, req, &args.Region, &args.QueryOptions) {
return nil, nil
}
// Perform the RPC request.
var reply structs.ACLBindingRulesListResponse
if err := s.agent.RPC(structs.ACLListBindingRulesRPCMethod, &args, &reply); err != nil {
return nil, err
}
setMeta(resp, &reply.QueryMeta)
if reply.ACLBindingRules == nil {
reply.ACLBindingRules = make([]*structs.ACLBindingRuleListStub, 0)
}
return reply.ACLBindingRules, nil
}
// ACLBindingRuleRequest creates a new ACL binding rule and is callable via the
// /v1/acl/binding-rule HTTP API.
func (s *HTTPServer) ACLBindingRuleRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
// // The endpoint only supports PUT or POST requests.
if !(req.Method == http.MethodPut || req.Method == http.MethodPost) {
return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod)
}
// Use the generic upsert function without setting an ID as this will be
// handled by the Nomad leader.
return s.aclBindingRuleUpsertRequest(resp, req, "")
}
// ACLBindingRuleSpecificRequest is callable via the /v1/acl/binding-rule/ HTTP
// API and handles read via both the ID, updates, and deletions.
func (s *HTTPServer) ACLBindingRuleSpecificRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) {
// Grab the suffix of the request, so we can further understand it.
reqSuffix := strings.TrimPrefix(req.URL.Path, "/v1/acl/binding-rule/")
// Ensure the binding rule ID is not an empty string which is possible if
// the caller requested "/v1/acl/role/binding-rule/".
if reqSuffix == "" {
return nil, CodedError(http.StatusBadRequest, "missing ACL binding rule ID")
}
// Identify the HTTP method which indicates which downstream function
// should be called.
switch req.Method {
case http.MethodGet:
return s.aclBindingRuleGetRequest(resp, req, reqSuffix)
case http.MethodDelete:
return s.aclBindingRuleDeleteRequest(resp, req, reqSuffix)
case http.MethodPost, http.MethodPut:
return s.aclBindingRuleUpsertRequest(resp, req, reqSuffix)
default:
return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod)
}
}
func (s *HTTPServer) aclBindingRuleGetRequest(
resp http.ResponseWriter, req *http.Request, ruleID string) (interface{}, error) {
args := structs.ACLBindingRuleRequest{
ACLBindingRuleID: ruleID,
}
if s.parse(resp, req, &args.Region, &args.QueryOptions) {
return nil, nil
}
var reply structs.ACLBindingRuleResponse
if err := s.agent.RPC(structs.ACLGetBindingRuleRPCMethod, &args, &reply); err != nil {
return nil, err
}
setMeta(resp, &reply.QueryMeta)
if reply.ACLBindingRule == nil {
return nil, CodedError(http.StatusNotFound, "ACL binding rule not found")
}
return reply.ACLBindingRule, nil
}
func (s *HTTPServer) aclBindingRuleDeleteRequest(
resp http.ResponseWriter, req *http.Request, ruleID string) (interface{}, error) {
args := structs.ACLBindingRulesDeleteRequest{
ACLBindingRuleIDs: []string{ruleID},
}
s.parseWriteRequest(req, &args.WriteRequest)
var reply structs.ACLBindingRulesDeleteResponse
if err := s.agent.RPC(structs.ACLDeleteBindingRulesRPCMethod, &args, &reply); err != nil {
return nil, err
}
setIndex(resp, reply.Index)
return nil, nil
}
// aclBindingRuleUpsertRequest handles upserting an ACL binding rule to the
// Nomad servers. It can handle both new creations, and updates to existing
// rules.
func (s *HTTPServer) aclBindingRuleUpsertRequest(
resp http.ResponseWriter, req *http.Request, ruleID string) (interface{}, error) {
// Decode the ACL binding rule.
var aclBindingRule structs.ACLBindingRule
if err := decodeBody(req, &aclBindingRule); err != nil {
return nil, CodedError(http.StatusBadRequest, err.Error())
}
// If the request path includes an ID, ensure the payload has an ID if it
// has been left empty.
if ruleID != "" && aclBindingRule.ID == "" {
aclBindingRule.ID = ruleID
}
// Ensure the request path ID matches the ACL binding rule ID that was
// decoded. Only perform this check on updates as a generic error on
// creation might be confusing to operators as there is no specific binding
// rule request path.
if ruleID != "" && ruleID != aclBindingRule.ID {
return nil, CodedError(http.StatusBadRequest, "ACL binding rule ID does not match request path")
}
args := structs.ACLBindingRulesUpsertRequest{
ACLBindingRules: []*structs.ACLBindingRule{&aclBindingRule},
}
s.parseWriteRequest(req, &args.WriteRequest)
var out structs.ACLBindingRulesUpsertResponse
if err := s.agent.RPC(structs.ACLUpsertBindingRulesRPCMethod, &args, &out); err != nil {
return nil, err
}
setIndex(resp, out.Index)
if len(out.ACLBindingRules) > 0 {
return out.ACLBindingRules[0], nil
}
return nil, nil
}

View File

@ -1297,3 +1297,328 @@ func TestHTTPServer_ACLAuthMethodSpecificRequest(t *testing.T) {
})
}
}
func TestHTTPServer_ACLBindingRuleListRequest(t *testing.T) {
ci.Parallel(t)
testCases := []struct {
name string
testFn func(srv *TestAgent)
}{
{
name: "no auth token set",
testFn: func(srv *TestAgent) {
// Build the HTTP request.
req, err := http.NewRequest(http.MethodGet, "/v1/acl/binding-rules", nil)
must.NoError(t, err)
respW := httptest.NewRecorder()
// Send the HTTP request.
obj, err := srv.Server.ACLBindingRuleListRequest(respW, req)
must.EqError(t, err, "Permission denied")
must.Nil(t, obj)
},
},
{
name: "invalid method",
testFn: func(srv *TestAgent) {
// Build the HTTP request.
req, err := http.NewRequest(http.MethodConnect, "/v1/acl/binding-rules", nil)
must.NoError(t, err)
respW := httptest.NewRecorder()
// Ensure we have a token set.
setToken(req, srv.RootToken)
// Send the HTTP request.
obj, err := srv.Server.ACLBindingRuleListRequest(respW, req)
must.EqError(t, err, "Invalid method")
must.Nil(t, obj)
},
},
{
name: "no binding rules in state",
testFn: func(srv *TestAgent) {
// Build the HTTP request.
req, err := http.NewRequest(http.MethodGet, "/v1/acl/binding-rules", nil)
must.NoError(t, err)
respW := httptest.NewRecorder()
// Ensure we have a token set.
setToken(req, srv.RootToken)
// Send the HTTP request.
obj, err := srv.Server.ACLBindingRuleListRequest(respW, req)
must.NoError(t, err)
must.Len(t, 0, obj.([]*structs.ACLBindingRuleListStub))
},
},
{
name: "binding rules in state",
testFn: func(srv *TestAgent) {
// Upsert two binding rules into state.
must.NoError(t, srv.server.State().UpsertACLBindingRules(
10, []*structs.ACLBindingRule{mock.ACLBindingRule(), mock.ACLBindingRule()}, true))
// Build the HTTP request.
req, err := http.NewRequest(http.MethodGet, "/v1/acl/binding-rules", nil)
must.NoError(t, err)
respW := httptest.NewRecorder()
// Ensure we have a token set.
setToken(req, srv.RootToken)
// Send the HTTP request.
obj, err := srv.Server.ACLBindingRuleListRequest(respW, req)
must.NoError(t, err)
must.Len(t, 2, obj.([]*structs.ACLBindingRuleListStub))
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
httpACLTest(t, nil, tc.testFn)
})
}
}
func TestHTTPServer_ACLBindingRuleRequest(t *testing.T) {
ci.Parallel(t)
testCases := []struct {
name string
testFn func(srv *TestAgent)
}{
{
name: "no auth token set",
testFn: func(srv *TestAgent) {
// Create a mock binding rule to use in the request body.
mockACLBindingRule := mock.ACLBindingRule()
mockACLBindingRule.ID = ""
// Build the HTTP request.
req, err := http.NewRequest(http.MethodPut, "/v1/acl/binding-rule", encodeReq(mockACLBindingRule))
must.NoError(t, err)
respW := httptest.NewRecorder()
// Send the HTTP request.
obj, err := srv.Server.ACLBindingRuleRequest(respW, req)
must.Error(t, err)
must.StrContains(t, err.Error(), "Permission denied")
must.Nil(t, obj)
},
},
{
name: "invalid method",
testFn: func(srv *TestAgent) {
// Build the HTTP request.
req, err := http.NewRequest(http.MethodConnect, "/v1/acl/binding-rule", nil)
must.NoError(t, err)
respW := httptest.NewRecorder()
// Ensure we have a token set.
setToken(req, srv.RootToken)
// Send the HTTP request.
obj, err := srv.Server.ACLBindingRuleRequest(respW, req)
must.Error(t, err)
must.StrContains(t, err.Error(), "Invalid method")
must.Nil(t, obj)
},
},
{
name: "successful upsert",
testFn: func(srv *TestAgent) {
// Upsert the auth method that the binding rule will associate
// with.
mockACLAuthMethod := mock.ACLAuthMethod()
must.NoError(t, srv.server.State().UpsertACLAuthMethods(
10, []*structs.ACLAuthMethod{mockACLAuthMethod}))
// Create a mock binding rule to use in the request body.
mockACLBindingRule := mock.ACLBindingRule()
mockACLBindingRule.AuthMethod = mockACLAuthMethod.Name
mockACLBindingRule.ID = ""
// Build the HTTP request.
req, err := http.NewRequest(http.MethodPut, "/v1/acl/binding-rule", encodeReq(mockACLBindingRule))
must.NoError(t, err)
respW := httptest.NewRecorder()
// Ensure we have a token set.
setToken(req, srv.RootToken)
// Send the HTTP request.
obj, err := srv.Server.ACLBindingRuleRequest(respW, req)
must.NoError(t, err)
result := obj.(*structs.ACLBindingRule)
must.Eq(t, mockACLBindingRule.Selector, result.Selector)
must.Eq(t, mockACLBindingRule.AuthMethod, result.AuthMethod)
must.Eq(t, mockACLBindingRule.Description, result.Description)
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
httpACLTest(t, nil, tc.testFn)
})
}
}
func TestHTTPServer_ACLBindingRuleSpecificRequest(t *testing.T) {
ci.Parallel(t)
testCases := []struct {
name string
testFn func(srv *TestAgent)
}{
{
name: "missing binding rule ID",
testFn: func(srv *TestAgent) {
// Build the HTTP request.
req, err := http.NewRequest(http.MethodGet, "/v1/acl/binding-rule/", nil)
must.NoError(t, err)
respW := httptest.NewRecorder()
// Send the HTTP request.
obj, err := srv.Server.ACLBindingRuleSpecificRequest(respW, req)
must.EqError(t, err, "missing ACL binding rule ID")
must.Nil(t, obj)
},
},
{
name: "incorrect method",
testFn: func(srv *TestAgent) {
// Build the HTTP request.
req, err := http.NewRequest(http.MethodConnect, "/v1/acl/binding-rule/foobar", nil)
must.NoError(t, err)
respW := httptest.NewRecorder()
// Send the HTTP request.
obj, err := srv.Server.ACLBindingRuleSpecificRequest(respW, req)
must.EqError(t, err, "Invalid method")
must.Nil(t, obj)
},
},
{
name: "get binding rule",
testFn: func(srv *TestAgent) {
// Upsert a binding rule into state.
aclBindingRules := []*structs.ACLBindingRule{mock.ACLBindingRule()}
must.NoError(t, srv.server.State().UpsertACLBindingRules(10, aclBindingRules, true))
url := "/v1/acl/binding-rule/" + aclBindingRules[0].ID
// Build the HTTP request.
req, err := http.NewRequest(http.MethodGet, url, nil)
must.NoError(t, err)
respW := httptest.NewRecorder()
// Ensure we have a token set.
setToken(req, srv.RootToken)
// Send the HTTP request.
obj, err := srv.Server.ACLBindingRuleSpecificRequest(respW, req)
must.NoError(t, err)
result := obj.(*structs.ACLBindingRule)
must.Eq(t, aclBindingRules[0].ID, result.ID)
must.Eq(t, aclBindingRules[0].Selector, result.Selector)
must.Eq(t, aclBindingRules[0].AuthMethod, result.AuthMethod)
must.Eq(t, aclBindingRules[0].Description, result.Description)
},
},
{
name: "get, update, and delete binding rule",
testFn: func(srv *TestAgent) {
// Upsert the auth method that the binding rule will associate
// with.
mockACLAuthMethod := mock.ACLAuthMethod()
must.NoError(t, srv.server.State().UpsertACLAuthMethods(
10, []*structs.ACLAuthMethod{mockACLAuthMethod}))
// Upsert a binding rule into state.
mockedAclBindingRule := mock.ACLBindingRule()
mockedAclBindingRule.AuthMethod = mockACLAuthMethod.Name
must.NoError(t, srv.server.State().UpsertACLBindingRules(
10, []*structs.ACLBindingRule{mockedAclBindingRule}, true))
url := "/v1/acl/binding-rule/" + mockedAclBindingRule.ID
// Build the HTTP request to read the binding rule.
req, err := http.NewRequest(http.MethodGet, url, nil)
must.NoError(t, err)
respW := httptest.NewRecorder()
// Ensure we have a token set.
setToken(req, srv.RootToken)
// Send the HTTP request.
obj, err := srv.Server.ACLBindingRuleSpecificRequest(respW, req)
must.NoError(t, err)
result := obj.(*structs.ACLBindingRule)
must.Eq(t, mockedAclBindingRule.ID, result.ID)
must.Eq(t, mockedAclBindingRule.Selector, result.Selector)
must.Eq(t, mockedAclBindingRule.AuthMethod, result.AuthMethod)
must.Eq(t, mockedAclBindingRule.Description, result.Description)
// Update the binding rule and make the request via the HTTP
// API.
result.Description = "new description"
req, err = http.NewRequest(http.MethodPost, url, encodeReq(result))
must.NoError(t, err)
respW = httptest.NewRecorder()
// Ensure we have a token set.
setToken(req, srv.RootToken)
// Send the HTTP request.
_, err = srv.Server.ACLBindingRuleSpecificRequest(respW, req)
must.NoError(t, err)
// Delete the ACL binding rule.
req, err = http.NewRequest(http.MethodDelete, url, nil)
must.NoError(t, err)
respW = httptest.NewRecorder()
// Ensure we have a token set.
setToken(req, srv.RootToken)
// Send the HTTP request.
obj, err = srv.Server.ACLBindingRuleSpecificRequest(respW, req)
must.NoError(t, err)
must.Nil(t, obj)
// Ensure the ACL binding rule is no longer stored within
// state.
aclBindingRule, err := srv.server.State().GetACLBindingRule(nil, mockedAclBindingRule.ID)
must.NoError(t, err)
must.Nil(t, aclBindingRule)
},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
cb := func(c *Config) { c.NomadConfig.ACLTokenMaxExpirationTTL = 3600 * time.Hour }
httpACLTest(t, cb, tc.testFn)
})
}
}

View File

@ -392,6 +392,11 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) {
s.mux.HandleFunc("/v1/acl/auth-method", s.wrap(s.ACLAuthMethodRequest))
s.mux.HandleFunc("/v1/acl/auth-method/", s.wrap(s.ACLAuthMethodSpecificRequest))
// Register our ACL binding rule handlers.
s.mux.HandleFunc("/v1/acl/binding-rules", s.wrap(s.ACLBindingRuleListRequest))
s.mux.HandleFunc("/v1/acl/binding-rule", s.wrap(s.ACLBindingRuleRequest))
s.mux.HandleFunc("/v1/acl/binding-rule/", s.wrap(s.ACLBindingRuleSpecificRequest))
s.mux.Handle("/v1/client/fs/", wrapCORS(s.wrap(s.FsRequest)))
s.mux.HandleFunc("/v1/client/gc", s.wrap(s.ClientGCRequest))
s.mux.Handle("/v1/client/stats", wrapCORS(s.wrap(s.ClientStatsRequest)))

View File

@ -2005,3 +2005,327 @@ func (a *ACL) WhoAmI(args *structs.GenericRequest, reply *structs.ACLWhoAmIRespo
reply.Identity = args.GetIdentity()
return nil
}
// UpsertBindingRules creates or updates ACL binding rules held within Nomad.
func (a *ACL) UpsertBindingRules(
args *structs.ACLBindingRulesUpsertRequest, reply *structs.ACLBindingRulesUpsertResponse) error {
// Ensure ACLs are enabled, and always flow modification requests to the
// authoritative region.
if !a.srv.config.ACLEnabled {
return aclDisabled
}
args.Region = a.srv.config.AuthoritativeRegion
if done, err := a.srv.forward(structs.ACLUpsertBindingRulesRPCMethod, args, args, reply); done {
return err
}
defer metrics.MeasureSince([]string{"nomad", "acl", "upsert_binding_rules"}, time.Now())
// ACL binding rules can only be used once all servers in all federated
// regions have been upgraded to 1.5.0 or greater.
if !ServersMeetMinimumVersion(a.srv.Members(), AllRegions, minACLBindingRuleVersion, false) {
return fmt.Errorf("all servers should be running version %v or later to use ACL binding rules",
minACLBindingRuleVersion)
}
// Check management level permissions
if acl, err := a.srv.ResolveToken(args.AuthToken); err != nil {
return err
} else if acl == nil || !acl.IsManagement() {
return structs.ErrPermissionDenied
}
// Validate non-zero set of binding rules. This must be done outside the
// validate function as that uses a loop, which will be skipped if the
// length is zero.
if len(args.ACLBindingRules) == 0 {
return structs.NewErrRPCCoded(http.StatusBadRequest, "must specify as least one binding rule")
}
stateSnapshot, err := a.srv.State().Snapshot()
if err != nil {
return err
}
// Validate each binding rules and compute the hash.
for idx, bindingRule := range args.ACLBindingRules {
if err := bindingRule.Validate(); err != nil {
return structs.NewErrRPCCodedf(http.StatusBadRequest, "binding rule %d invalid: %v", idx, err)
}
// If the caller has passed a rule ID, this call is considered an
// update to an existing rule. We should therefore ensure it is found
// within state.
if bindingRule.ID != "" {
existingBindingRule, err := stateSnapshot.GetACLBindingRule(nil, bindingRule.ID)
if err != nil {
return structs.NewErrRPCCodedf(
http.StatusInternalServerError, "binding rule lookup failed: %v", err)
}
if existingBindingRule == nil {
return structs.NewErrRPCCodedf(
http.StatusBadRequest, "cannot find binding rule %s", bindingRule.ID)
}
}
// Ensure the auth method linked to exists within state.
method, err := stateSnapshot.GetACLAuthMethodByName(nil, bindingRule.AuthMethod)
if err != nil {
return err
}
if method == nil {
return structs.NewErrRPCCodedf(
http.StatusBadRequest, "ACL auth method %s not found", bindingRule.AuthMethod)
}
// All the validation has passed, we can now canonicalize the object
// with the final internal data and set the hash.
bindingRule.Canonicalize()
bindingRule.SetHash()
}
// Update via Raft.
out, index, err := a.srv.raftApply(structs.ACLBindingRulesUpsertRequestType, args)
if err != nil {
return err
}
// Check if the FSM response, which is an interface, contains an error.
if err, ok := out.(error); ok && err != nil {
return err
}
// Populate the response. We do a lookup against the state to pick up the
// proper create / modify indexes.
stateSnapshot, err = a.srv.State().Snapshot()
if err != nil {
return err
}
for _, bindingRule := range args.ACLBindingRules {
lookupBindingRule, err := stateSnapshot.GetACLBindingRule(nil, bindingRule.ID)
if err != nil {
return structs.NewErrRPCCodedf(http.StatusInternalServerError,
"ACL binding rule lookup failed: %v", err)
}
if lookupBindingRule == nil {
return structs.NewErrRPCCoded(http.StatusInternalServerError,
"ACL binding rule lookup failed: no entry found")
}
reply.ACLBindingRules = append(reply.ACLBindingRules, lookupBindingRule)
}
// Update the index
reply.Index = index
return nil
}
// DeleteBindingRules batch deletes ACL binding rules from Nomad state.
func (a *ACL) DeleteBindingRules(
args *structs.ACLBindingRulesDeleteRequest, reply *structs.ACLBindingRulesDeleteResponse) error {
// Ensure ACLs are enabled, and always flow modification requests to the
// authoritative region.
if !a.srv.config.ACLEnabled {
return aclDisabled
}
args.Region = a.srv.config.AuthoritativeRegion
if done, err := a.srv.forward(structs.ACLDeleteBindingRulesRPCMethod, args, args, reply); done {
return err
}
defer metrics.MeasureSince([]string{"nomad", "acl", "delete_binding_rules"}, time.Now())
// ACL binding rules can only be used once all servers in all federated
// regions have been upgraded to 1.5.0 or greater.
if !ServersMeetMinimumVersion(a.srv.Members(), AllRegions, minACLBindingRuleVersion, false) {
return fmt.Errorf("all servers should be running version %v or later to use ACL binding rules",
minACLBindingRuleVersion)
}
// Check management level permissions.
if acl, err := a.srv.ResolveToken(args.AuthToken); err != nil {
return err
} else if acl == nil || !acl.IsManagement() {
return structs.ErrPermissionDenied
}
// Validate non-zero set of binding rule IDs.
if len(args.ACLBindingRuleIDs) == 0 {
return structs.NewErrRPCCoded(http.StatusBadRequest, "must specify as least one binding rule")
}
// Update via Raft.
out, index, err := a.srv.raftApply(structs.ACLBindingRulesDeleteRequestType, args)
if err != nil {
return err
}
// Check if the FSM response, which is an interface, contains an error.
if err, ok := out.(error); ok && err != nil {
return err
}
// Update the index
reply.Index = index
return nil
}
// ListBindingRules returns a stub list of ACL binding rules.
func (a *ACL) ListBindingRules(
args *structs.ACLBindingRulesListRequest, reply *structs.ACLBindingRulesListResponse) error {
// Only allow operators to list ACL binding rules when ACLs are enabled.
if !a.srv.config.ACLEnabled {
return aclDisabled
}
if done, err := a.srv.forward(structs.ACLListBindingRulesRPCMethod, args, args, reply); done {
return err
}
defer metrics.MeasureSince([]string{"nomad", "acl", "list_binding_rules"}, time.Now())
// Check management level permissions.
if acl, err := a.srv.ResolveToken(args.AuthToken); err != nil {
return err
} else if acl == nil || !acl.IsManagement() {
return structs.ErrPermissionDenied
}
// Set up and return the blocking query.
return a.srv.blockingRPC(&blockingOptions{
queryOpts: &args.QueryOptions,
queryMeta: &reply.QueryMeta,
run: func(ws memdb.WatchSet, stateStore *state.StateStore) error {
// The iteration below appends directly to the reply object, so in
// order for blocking queries to work properly we must ensure the
// ACLBindingRules are reset. This allows the blocking query run
// function to work as expected.
reply.ACLBindingRules = nil
iter, err := stateStore.GetACLBindingRules(ws)
if err != nil {
return err
}
// Iterate all the results and add these to our reply object.
for raw := iter.Next(); raw != nil; raw = iter.Next() {
reply.ACLBindingRules = append(reply.ACLBindingRules, raw.(*structs.ACLBindingRule).Stub())
}
// Use the index table to populate the query meta as we have no way
// of tracking the max index on deletes.
return a.srv.setReplyQueryMeta(stateStore, state.TableACLBindingRules, &reply.QueryMeta)
},
})
}
// GetBindingRules is used to query for a set of ACL binding rules. This
// endpoint is used for replication purposes and is not exposed via the HTTP
// API.
func (a *ACL) GetBindingRules(
args *structs.ACLBindingRulesRequest, reply *structs.ACLBindingRulesResponse) error {
// This endpoint is only used by the replication process which is only
// running on ACL enabled clusters, so this check should never be
// triggered.
if !a.srv.config.ACLEnabled {
return aclDisabled
}
if done, err := a.srv.forward(structs.ACLGetBindingRulesRPCMethod, args, args, reply); done {
return err
}
defer metrics.MeasureSince([]string{"nomad", "acl", "get_rules"}, time.Now())
// Check management level permissions.
if acl, err := a.srv.ResolveToken(args.AuthToken); err != nil {
return err
} else if acl == nil || !acl.IsManagement() {
return structs.ErrPermissionDenied
}
// Set up and return the blocking query
return a.srv.blockingRPC(&blockingOptions{
queryOpts: &args.QueryOptions,
queryMeta: &reply.QueryMeta,
run: func(ws memdb.WatchSet, stateStore *state.StateStore) error {
// Instantiate the output map to the correct maximum length.
reply.ACLBindingRules = make(map[string]*structs.ACLBindingRule, len(args.ACLBindingRuleIDs))
// Look for the ACL role and add this to our mapping if we have
// found it.
for _, bindingRuleID := range args.ACLBindingRuleIDs {
out, err := stateStore.GetACLBindingRule(ws, bindingRuleID)
if err != nil {
return err
}
if out != nil {
reply.ACLBindingRules[out.ID] = out
}
}
// Use the index table to populate the query meta as we have no way
// of tracking the max index on deletes.
return a.srv.setReplyQueryMeta(stateStore, state.TableACLBindingRules, &reply.QueryMeta)
},
})
}
// GetBindingRule is used to retrieve a single ACL binding rule as defined by
// its ID.
func (a *ACL) GetBindingRule(
args *structs.ACLBindingRuleRequest, reply *structs.ACLBindingRuleResponse) error {
// Only allow operators to read an ACL binding rule when ACLs are enabled.
if !a.srv.config.ACLEnabled {
return aclDisabled
}
if done, err := a.srv.forward(structs.ACLGetBindingRuleRPCMethod, args, args, reply); done {
return err
}
defer metrics.MeasureSince([]string{"nomad", "acl", "get_binding_rule"}, time.Now())
// Check management level permissions.
if acl, err := a.srv.ResolveToken(args.AuthToken); err != nil {
return err
} else if acl == nil || !acl.IsManagement() {
return structs.ErrPermissionDenied
}
// Set up and return the blocking query.
return a.srv.blockingRPC(&blockingOptions{
queryOpts: &args.QueryOptions,
queryMeta: &reply.QueryMeta,
run: func(ws memdb.WatchSet, stateStore *state.StateStore) error {
// Perform a lookup for the ACL role.
out, err := stateStore.GetACLBindingRule(ws, args.ACLBindingRuleID)
if err != nil {
return err
}
// Set the index correctly depending on whether the ACL binding
// rule was found.
switch out {
case nil:
index, err := stateStore.Index(state.TableACLBindingRules)
if err != nil {
return err
}
reply.Index = index
default:
reply.Index = out.ModifyIndex
}
// We didn't encounter an error looking up the index; set the ACL
// binding rule on the reply and exit successfully.
reply.ACLBindingRule = out
return nil
},
})
}

View File

@ -3057,3 +3057,412 @@ func TestACLEndpoint_UpsertACLAuthMethods(t *testing.T) {
// We expect this to err since there's already a default method of the same type
must.Error(t, msgpackrpc.CallWithCodec(codec, structs.ACLUpsertAuthMethodsRPCMethod, req, &resp))
}
func TestACL_UpsertBindingRules(t *testing.T) {
ci.Parallel(t)
testServer, aclRootToken, testServerCleanupFn := TestACLServer(t, nil)
defer testServerCleanupFn()
codec := rpcClient(t, testServer)
testutil.WaitForLeader(t, testServer.RPC)
// Create a mock ACL binding rule and remove the ID so this looks like a
// creation.
aclBindingRule1 := mock.ACLBindingRule()
aclBindingRule1.ID = ""
// Attempt to upsert this binding rule without setting an ACL token. This
// should fail.
aclBindingRuleReq1 := &structs.ACLBindingRulesUpsertRequest{
ACLBindingRules: []*structs.ACLBindingRule{aclBindingRule1},
WriteRequest: structs.WriteRequest{
Region: "global",
},
}
var aclBindingRuleResp1 structs.ACLBindingRulesUpsertResponse
err := msgpackrpc.CallWithCodec(codec, structs.ACLUpsertBindingRulesRPCMethod, aclBindingRuleReq1, &aclBindingRuleResp1)
must.EqError(t, err, "Permission denied")
// Attempt to upsert this binding rule that references a auth method that
// does not exist in state.
aclBindingRuleReq2 := &structs.ACLBindingRulesUpsertRequest{
ACLBindingRules: []*structs.ACLBindingRule{aclBindingRule1},
WriteRequest: structs.WriteRequest{
Region: "global",
AuthToken: aclRootToken.SecretID,
},
}
var aclBindingRuleResp2 structs.ACLBindingRulesUpsertResponse
err = msgpackrpc.CallWithCodec(codec, structs.ACLUpsertBindingRulesRPCMethod, aclBindingRuleReq2, &aclBindingRuleResp2)
must.EqError(t, err, "RPC Error:: 400,ACL auth method auth0 not found")
// Create the policies our ACL roles wants to link to.
authMethod := mock.ACLAuthMethod()
authMethod.Name = aclBindingRule1.AuthMethod
must.NoError(t, testServer.fsm.State().UpsertACLAuthMethods(10, []*structs.ACLAuthMethod{authMethod}))
// Try the upsert a third time, which should succeed.
aclBindingRuleReq3 := &structs.ACLBindingRulesUpsertRequest{
ACLBindingRules: []*structs.ACLBindingRule{aclBindingRule1},
WriteRequest: structs.WriteRequest{
Region: "global",
AuthToken: aclRootToken.SecretID,
},
}
var aclBindingRuleResp3 structs.ACLBindingRulesUpsertResponse
err = msgpackrpc.CallWithCodec(codec, structs.ACLUpsertBindingRulesRPCMethod, aclBindingRuleReq3, &aclBindingRuleResp3)
must.NoError(t, err)
must.Len(t, 1, aclBindingRuleResp3.ACLBindingRules)
// Perform an update of the ACL binding rule by updating the description.
aclBindingRule1Copy := aclBindingRule1.Copy()
aclBindingRule1Copy.Description = "updated-description"
aclBindingRuleReq4 := &structs.ACLBindingRulesUpsertRequest{
ACLBindingRules: []*structs.ACLBindingRule{aclBindingRule1},
WriteRequest: structs.WriteRequest{
Region: "global",
AuthToken: aclRootToken.SecretID,
},
}
var aclBindingRuleResp4 structs.ACLBindingRulesUpsertResponse
err = msgpackrpc.CallWithCodec(codec, structs.ACLUpsertBindingRulesRPCMethod, aclBindingRuleReq4, &aclBindingRuleResp4)
must.NoError(t, err)
must.Len(t, 1, aclBindingRuleResp4.ACLBindingRules)
must.Greater(t, aclBindingRuleResp3.ACLBindingRules[0].ModifyIndex, aclBindingRuleResp4.ACLBindingRules[0].ModifyIndex)
// Create another ACL binding rule that will fail validation. Attempting to
// upsert this ensures the handler is triggering the validation function.
aclBindingRule2 := mock.ACLBindingRule()
aclBindingRule2.BindType = ""
aclBindingRuleReq5 := &structs.ACLBindingRulesUpsertRequest{
ACLBindingRules: []*structs.ACLBindingRule{aclBindingRule2},
WriteRequest: structs.WriteRequest{
Region: DefaultRegion,
AuthToken: aclRootToken.SecretID,
},
}
var aclBindingRuleResp5 structs.ACLBindingRulesUpsertResponse
err = msgpackrpc.CallWithCodec(codec, structs.ACLUpsertBindingRulesRPCMethod, aclBindingRuleReq5, &aclBindingRuleResp5)
must.Error(t, err)
must.StrContains(t, err.Error(), "bind type is missing")
}
func TestACL_DeleteBindingRules(t *testing.T) {
ci.Parallel(t)
testServer, aclRootToken, testServerCleanupFn := TestACLServer(t, nil)
defer testServerCleanupFn()
codec := rpcClient(t, testServer)
testutil.WaitForLeader(t, testServer.RPC)
// Create two ACL binding rules and put these directly into state.
aclBindingRules := []*structs.ACLBindingRule{mock.ACLBindingRule(), mock.ACLBindingRule()}
must.NoError(t, testServer.State().UpsertACLBindingRules(10, aclBindingRules, true))
// Attempt to delete an ACL binding rule without setting an auth token.
// This should fail.
aclBindingRuleReq1 := &structs.ACLBindingRulesDeleteRequest{
ACLBindingRuleIDs: []string{aclBindingRules[0].ID},
WriteRequest: structs.WriteRequest{
Region: DefaultRegion,
},
}
var aclBindingRuleResp1 structs.ACLBindingRulesDeleteResponse
err := msgpackrpc.CallWithCodec(codec, structs.ACLDeleteBindingRulesRPCMethod, aclBindingRuleReq1, &aclBindingRuleResp1)
must.EqError(t, err, "Permission denied")
// Attempt to delete an ACL binding rule now using a valid management token
// which should succeed.
aclBindingRuleReq2 := &structs.ACLBindingRulesDeleteRequest{
ACLBindingRuleIDs: []string{aclBindingRules[0].ID},
WriteRequest: structs.WriteRequest{
Region: DefaultRegion,
AuthToken: aclRootToken.SecretID,
},
}
var aclBindingRuleResp2 structs.ACLBindingRulesDeleteResponse
err = msgpackrpc.CallWithCodec(codec, structs.ACLDeleteBindingRulesRPCMethod, aclBindingRuleReq2, &aclBindingRuleResp2)
must.NoError(t, err)
// Ensure the deleted binding rule is not found within state and that the
// other is.
ws := memdb.NewWatchSet()
iter, err := testServer.State().GetACLBindingRules(ws)
must.NoError(t, err)
var aclBindingRulesLookup []*structs.ACLBindingRule
for raw := iter.Next(); raw != nil; raw = iter.Next() {
aclBindingRulesLookup = append(aclBindingRulesLookup, raw.(*structs.ACLBindingRule))
}
must.Len(t, 1, aclBindingRulesLookup)
must.Eq(t, aclBindingRulesLookup[0], aclBindingRules[1])
// Try to delete the previously deleted ACL binding rule, this should fail.
aclBindingRuleReq3 := &structs.ACLBindingRulesDeleteRequest{
ACLBindingRuleIDs: []string{aclBindingRules[0].ID},
WriteRequest: structs.WriteRequest{
Region: DefaultRegion,
AuthToken: aclRootToken.SecretID,
},
}
var aclBindingRuleResp3 structs.ACLBindingRulesDeleteResponse
err = msgpackrpc.CallWithCodec(codec, structs.ACLDeleteBindingRulesRPCMethod, aclBindingRuleReq3, &aclBindingRuleResp3)
must.EqError(t, err, "ACL binding rule not found")
}
func TestACL_ListBindingRules(t *testing.T) {
ci.Parallel(t)
testServer, aclRootToken, testServerCleanupFn := TestACLServer(t, nil)
defer testServerCleanupFn()
codec := rpcClient(t, testServer)
testutil.WaitForLeader(t, testServer.RPC)
// Create two ACL binding rules and put these directly into state.
aclBindingRules := []*structs.ACLBindingRule{mock.ACLBindingRule(), mock.ACLBindingRule()}
must.NoError(t, testServer.State().UpsertACLBindingRules(10, aclBindingRules, true))
// Try listing binding rules without a valid ACL token.
aclBindingRuleReq1 := &structs.ACLBindingRulesListRequest{
QueryOptions: structs.QueryOptions{
Region: DefaultRegion,
},
}
var aclBindingRuleResp1 structs.ACLBindingRulesListResponse
err := msgpackrpc.CallWithCodec(codec, structs.ACLListBindingRulesRPCMethod, aclBindingRuleReq1, &aclBindingRuleResp1)
must.EqError(t, err, "Permission denied")
// Try listing roles with a valid ACL token.
aclBindingRuleReq2 := &structs.ACLBindingRulesListRequest{
QueryOptions: structs.QueryOptions{
Region: DefaultRegion,
AuthToken: aclRootToken.SecretID,
},
}
var aclBindingRuleResp2 structs.ACLBindingRulesListResponse
err = msgpackrpc.CallWithCodec(codec, structs.ACLListBindingRulesRPCMethod, aclBindingRuleReq2, &aclBindingRuleResp2)
must.NoError(t, err)
must.Len(t, 2, aclBindingRuleResp2.ACLBindingRules)
// Now test a blocking query, where we wait for an update to the list which
// is triggered by a deletion.
type res struct {
err error
reply *structs.ACLBindingRulesListResponse
}
resultCh := make(chan *res)
go func(resultCh chan *res) {
aclBindingRuleReq3 := &structs.ACLBindingRulesListRequest{
QueryOptions: structs.QueryOptions{
Region: DefaultRegion,
AuthToken: aclRootToken.SecretID,
MinQueryIndex: aclBindingRuleResp2.Index,
MaxQueryTime: 10 * time.Second,
},
}
var aclBindingRuleResp3 structs.ACLBindingRulesListResponse
err = msgpackrpc.CallWithCodec(codec, structs.ACLListBindingRulesRPCMethod, aclBindingRuleReq3, &aclBindingRuleResp3)
resultCh <- &res{err: err, reply: &aclBindingRuleResp3}
}(resultCh)
// Delete an ACL binding rule from state which should return the blocking
// query.
must.NoError(t, testServer.fsm.State().DeleteACLBindingRules(
aclBindingRuleResp2.Index+10, []string{aclBindingRules[0].ID}))
// Wait until the test within the routine is complete.
result := <-resultCh
must.NoError(t, result.err)
must.Len(t, 1, result.reply.ACLBindingRules)
must.NotEq(t, result.reply.ACLBindingRules[0].ID, aclBindingRules[0].ID)
must.Greater(t, aclBindingRuleResp2.Index, result.reply.Index)
}
func TestACL_GetBindingRules(t *testing.T) {
ci.Parallel(t)
testServer, aclRootToken, testServerCleanupFn := TestACLServer(t, nil)
defer testServerCleanupFn()
codec := rpcClient(t, testServer)
testutil.WaitForLeader(t, testServer.RPC)
// Try reading a binding rule without setting a correct auth token.
aclBindingRuleReq1 := &structs.ACLBindingRulesRequest{
ACLBindingRuleIDs: []string{"nope"},
QueryOptions: structs.QueryOptions{
Region: DefaultRegion,
},
}
var aclBindingRuleResp1 structs.ACLBindingRulesResponse
err := msgpackrpc.CallWithCodec(codec, structs.ACLGetBindingRulesRPCMethod, aclBindingRuleReq1, &aclBindingRuleResp1)
must.EqError(t, err, "Permission denied")
must.MapEmpty(t, aclBindingRuleResp1.ACLBindingRules)
// Try reading a binding rule that doesn't exist.
aclBindingRuleReq2 := &structs.ACLBindingRulesRequest{
ACLBindingRuleIDs: []string{"nope"},
QueryOptions: structs.QueryOptions{
Region: DefaultRegion,
AuthToken: aclRootToken.SecretID,
},
}
var aclBindingRuleResp2 structs.ACLBindingRulesResponse
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetBindingRulesRPCMethod, aclBindingRuleReq2, &aclBindingRuleResp2)
must.NoError(t, err)
must.MapEmpty(t, aclBindingRuleResp1.ACLBindingRules)
// Create two ACL binding rules and put these directly into state.
aclBindingRules := []*structs.ACLBindingRule{mock.ACLBindingRule(), mock.ACLBindingRule()}
must.NoError(t, testServer.State().UpsertACLBindingRules(10, aclBindingRules, true))
// Try reading both binding rules that are within state.
aclBindingRuleReq3 := &structs.ACLBindingRulesRequest{
ACLBindingRuleIDs: []string{aclBindingRules[0].ID, aclBindingRules[1].ID},
QueryOptions: structs.QueryOptions{
Region: DefaultRegion,
AuthToken: aclRootToken.SecretID,
},
}
var aclBindingRuleResp3 structs.ACLBindingRulesResponse
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetBindingRulesRPCMethod, aclBindingRuleReq3, &aclBindingRuleResp3)
must.NoError(t, err)
must.MapLen(t, 2, aclBindingRuleResp3.ACLBindingRules)
must.MapContainsKeys(t, aclBindingRuleResp3.ACLBindingRules, []string{aclBindingRules[0].ID, aclBindingRules[1].ID})
// Now test a blocking query, where we wait for an update to the set which
// is triggered by a deletion.
type res struct {
err error
reply *structs.ACLBindingRulesResponse
}
resultCh := make(chan *res)
go func(resultCh chan *res) {
aclBindingRuleReq4 := &structs.ACLBindingRulesRequest{
ACLBindingRuleIDs: []string{aclBindingRules[0].ID, aclBindingRules[1].ID},
QueryOptions: structs.QueryOptions{
Region: DefaultRegion,
AuthToken: aclRootToken.SecretID,
MinQueryIndex: aclBindingRuleResp3.Index,
MaxQueryTime: 10 * time.Second,
},
}
var aclBindingRuleResp4 structs.ACLBindingRulesResponse
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetBindingRulesRPCMethod, aclBindingRuleReq4, &aclBindingRuleResp4)
resultCh <- &res{err: err, reply: &aclBindingRuleResp4}
}(resultCh)
// Delete an ACL role from state which should return the blocking query.
must.NoError(t, testServer.fsm.State().DeleteACLBindingRules(
aclBindingRuleResp3.Index+10, []string{aclBindingRules[0].ID}))
// Wait for the result and then test it.
result := <-resultCh
must.NoError(t, result.err)
must.MapLen(t, 1, result.reply.ACLBindingRules)
must.MapContainsKeys(t, result.reply.ACLBindingRules, []string{aclBindingRules[1].ID})
must.Greater(t, aclBindingRuleResp3.Index, result.reply.Index)
}
func TestACL_GetBindingRule(t *testing.T) {
ci.Parallel(t)
testServer, aclRootToken, testServerCleanupFn := TestACLServer(t, nil)
defer testServerCleanupFn()
codec := rpcClient(t, testServer)
testutil.WaitForLeader(t, testServer.RPC)
// Create two ACL binding rules and put these directly into state.
aclBindingRules := []*structs.ACLBindingRule{mock.ACLBindingRule(), mock.ACLBindingRule()}
must.NoError(t, testServer.State().UpsertACLBindingRules(10, aclBindingRules, true))
// Try reading a role without setting a correct auth token.
aclBindingRuleReq1 := &structs.ACLBindingRuleRequest{
QueryOptions: structs.QueryOptions{
Region: DefaultRegion,
},
}
var aclBindingRuleResp1 structs.ACLBindingRuleResponse
err := msgpackrpc.CallWithCodec(codec, structs.ACLGetBindingRuleRPCMethod, aclBindingRuleReq1, &aclBindingRuleResp1)
must.EqError(t, err, "Permission denied")
// Try reading a role that doesn't exist.
aclBindingRuleReq2 := &structs.ACLBindingRuleRequest{
ACLBindingRuleID: "nope",
QueryOptions: structs.QueryOptions{
Region: DefaultRegion,
AuthToken: aclRootToken.SecretID,
},
}
var aclBindingRuleResp2 structs.ACLBindingRuleResponse
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetBindingRuleRPCMethod, aclBindingRuleReq2, &aclBindingRuleResp2)
must.NoError(t, err)
must.Nil(t, aclBindingRuleResp2.ACLBindingRule)
// Read both our available ACL roles using a valid auth token.
aclBindingRuleReq3 := &structs.ACLBindingRuleRequest{
ACLBindingRuleID: aclBindingRules[0].ID,
QueryOptions: structs.QueryOptions{
Region: DefaultRegion,
AuthToken: aclRootToken.SecretID,
},
}
var aclBindingRuleResp3 structs.ACLBindingRuleResponse
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetBindingRuleRPCMethod, aclBindingRuleReq3, &aclBindingRuleResp3)
must.NoError(t, err)
must.Eq(t, aclBindingRules[0].ID, aclBindingRuleResp3.ACLBindingRule.ID)
aclBindingRuleReq4 := &structs.ACLBindingRuleRequest{
ACLBindingRuleID: aclBindingRules[1].ID,
QueryOptions: structs.QueryOptions{
Region: DefaultRegion,
AuthToken: aclRootToken.SecretID,
},
}
var aclBindingRuleResp4 structs.ACLBindingRuleResponse
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetBindingRuleRPCMethod, aclBindingRuleReq4, &aclBindingRuleResp4)
must.NoError(t, err)
must.Eq(t, aclBindingRules[1].ID, aclBindingRuleResp4.ACLBindingRule.ID)
// Now test a blocking query, where we wait for an update to the set which
// is triggered by an upsert.
type res struct {
err error
reply *structs.ACLBindingRuleResponse
}
resultCh := make(chan *res)
go func(resultCh chan *res) {
aclBindingRuleReq5 := &structs.ACLBindingRuleRequest{
ACLBindingRuleID: aclBindingRules[0].ID,
QueryOptions: structs.QueryOptions{
Region: DefaultRegion,
AuthToken: aclRootToken.SecretID,
MinQueryIndex: aclBindingRuleResp4.Index,
MaxQueryTime: 10 * time.Second,
},
}
var aclBindingRuleResp5 structs.ACLBindingRuleResponse
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetBindingRuleRPCMethod, aclBindingRuleReq5, &aclBindingRuleResp5)
resultCh <- &res{err: err, reply: &aclBindingRuleResp5}
}(resultCh)
// Delete an ACL role from state which should return the blocking query.
aclBindingRule1Copy := aclBindingRules[0].Copy()
aclBindingRule1Copy.Description = "updated-description"
aclBindingRule1Copy.SetHash()
must.NoError(t, testServer.fsm.State().UpsertACLBindingRules(
aclBindingRuleResp4.Index+10, []*structs.ACLBindingRule{aclBindingRule1Copy}, true))
// Wait for the result and then test it.
result := <-resultCh
must.NoError(t, result.err)
must.Eq(t, aclBindingRules[0].ID, result.reply.ACLBindingRule.ID)
must.Greater(t, aclBindingRuleResp4.Index, result.reply.Index)
}

View File

@ -62,6 +62,14 @@ var minACLRoleVersion = version.Must(version.NewVersion("1.4.0"))
// 1.5, otherwise it's hard to test the functionality
var minACLAuthMethodVersion = version.Must(version.NewVersion("1.4.3-dev"))
// minACLBindingRuleVersion is the Nomad version at which the ACL binding rules
// table was introduced. It forms the minimum version all federated servers
// must meet before the feature can be used.
//
// TODO: version constraint will be updated for every beta or rc until we reach
// 1.5, otherwise it's hard to test the functionality
var minACLBindingRuleVersion = version.Must(version.NewVersion("1.4.3-dev"))
// minNomadServiceRegistrationVersion is the Nomad version at which the service
// registrations table was introduced. It forms the minimum version all local
// servers must meet before the feature can be used.