mirror of
https://github.com/kemko/nomad.git
synced 2026-01-04 01:15:43 +03:00
Merge pull request #14057 from hashicorp/f-gh-13120-acl-role-rpc-endpoints
ACL Role: add RPC, HTTP API, and API SDK functionality.
This commit is contained in:
130
api/acl.go
130
api/acl.go
@@ -1,6 +1,7 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
@@ -202,6 +203,96 @@ func (a *ACLTokens) ExchangeOneTimeToken(secret string, q *WriteOptions) (*ACLTo
|
||||
return resp.Token, wm, nil
|
||||
}
|
||||
|
||||
var (
|
||||
// errMissingACLRoleID is the generic errors to use when a call is missing
|
||||
// the required ACL Role ID parameter.
|
||||
errMissingACLRoleID = errors.New("missing ACL role ID")
|
||||
)
|
||||
|
||||
// ACLRoles is used to query the ACL Role endpoints.
|
||||
type ACLRoles struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// ACLRoles returns a new handle on the ACL roles API client.
|
||||
func (c *Client) ACLRoles() *ACLRoles {
|
||||
return &ACLRoles{client: c}
|
||||
}
|
||||
|
||||
// List is used to detail all the ACL roles currently stored within state.
|
||||
func (a *ACLRoles) List(q *QueryOptions) ([]*ACLRole, *QueryMeta, error) {
|
||||
var resp []*ACLRole
|
||||
qm, err := a.client.query("/v1/acl/roles", &resp, q)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return resp, qm, nil
|
||||
}
|
||||
|
||||
// Create is used to create an ACL role.
|
||||
func (a *ACLRoles) Create(role *ACLRole, w *WriteOptions) (*ACLRole, *WriteMeta, error) {
|
||||
if role.ID != "" {
|
||||
return nil, nil, errors.New("cannot specify ACL role ID")
|
||||
}
|
||||
var resp ACLRole
|
||||
wm, err := a.client.write("/v1/acl/role", role, &resp, w)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return &resp, wm, nil
|
||||
}
|
||||
|
||||
// Update is used to update an existing ACL role.
|
||||
func (a *ACLRoles) Update(role *ACLRole, w *WriteOptions) (*ACLRole, *WriteMeta, error) {
|
||||
if role.ID == "" {
|
||||
return nil, nil, errMissingACLRoleID
|
||||
}
|
||||
var resp ACLRole
|
||||
wm, err := a.client.write("/v1/acl/role/"+role.ID, role, &resp, w)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return &resp, wm, nil
|
||||
}
|
||||
|
||||
// Delete is used to delete an ACL role.
|
||||
func (a *ACLRoles) Delete(roleID string, w *WriteOptions) (*WriteMeta, error) {
|
||||
if roleID == "" {
|
||||
return nil, errMissingACLRoleID
|
||||
}
|
||||
wm, err := a.client.delete("/v1/acl/role/"+roleID, nil, nil, w)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wm, nil
|
||||
}
|
||||
|
||||
// Get is used to look up an ACL role.
|
||||
func (a *ACLRoles) Get(roleID string, q *QueryOptions) (*ACLRole, *QueryMeta, error) {
|
||||
if roleID == "" {
|
||||
return nil, nil, errMissingACLRoleID
|
||||
}
|
||||
var resp ACLRole
|
||||
qm, err := a.client.query("/v1/acl/role/"+roleID, &resp, q)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return &resp, qm, nil
|
||||
}
|
||||
|
||||
// GetByName is used to look up an ACL role using its name.
|
||||
func (a *ACLRoles) GetByName(roleName string, q *QueryOptions) (*ACLRole, *QueryMeta, error) {
|
||||
if roleName == "" {
|
||||
return nil, nil, errors.New("missing ACL role name")
|
||||
}
|
||||
var resp ACLRole
|
||||
qm, err := a.client.query("/v1/acl/role/name/"+roleName, &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
|
||||
@@ -285,3 +376,42 @@ type OneTimeTokenExchangeResponse struct {
|
||||
type BootstrapRequest struct {
|
||||
BootstrapSecret string
|
||||
}
|
||||
|
||||
// ACLRole is an abstraction for the ACL system which allows the grouping of
|
||||
// ACL policies into a single object. ACL tokens can be created and linked to
|
||||
// a role; the token then inherits all the permissions granted by the policies.
|
||||
type ACLRole struct {
|
||||
|
||||
// ID is an internally generated UUID for this role and is controlled by
|
||||
// Nomad. It can be used after role creation to update the existing role.
|
||||
ID string
|
||||
|
||||
// Name is unique across the entire set of federated clusters and is
|
||||
// supplied by the operator on role creation. The name can be modified by
|
||||
// updating the role and including the Nomad generated ID. This update will
|
||||
// not affect tokens created and linked to this role. This is a required
|
||||
// field.
|
||||
Name string
|
||||
|
||||
// Description is a human-readable, operator set description that can
|
||||
// provide additional context about the role. This is an optional field.
|
||||
Description string
|
||||
|
||||
// Policies is an array of ACL policy links. Although currently policies
|
||||
// can only be linked using their name, in the future we will want to add
|
||||
// IDs also and thus allow operators to specify either a name, an ID, or
|
||||
// both. At least one entry is required.
|
||||
Policies []*ACLRolePolicyLink
|
||||
|
||||
CreateIndex uint64
|
||||
ModifyIndex uint64
|
||||
}
|
||||
|
||||
// ACLRolePolicyLink is used to link a policy to an ACL role. We use a struct
|
||||
// rather than a list of strings as in the future we will want to add IDs to
|
||||
// policies and then link via these.
|
||||
type ACLRolePolicyLink struct {
|
||||
|
||||
// Name is the ACLPolicy.Name value which will be linked to the ACL role.
|
||||
Name string
|
||||
}
|
||||
|
||||
@@ -346,3 +346,77 @@ func TestACLTokens_BootstrapValidToken(t *testing.T) {
|
||||
assertWriteMeta(t, wm)
|
||||
assert.Equal(t, bootkn, out.SecretID)
|
||||
}
|
||||
|
||||
func TestACLRoles(t *testing.T) {
|
||||
testutil.Parallel(t)
|
||||
|
||||
testClient, testServer, _ := makeACLClient(t, nil, nil)
|
||||
defer testServer.Stop()
|
||||
|
||||
// An initial listing shouldn't return any results.
|
||||
aclRoleListResp, queryMeta, err := testClient.ACLRoles().List(nil)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, aclRoleListResp)
|
||||
assertQueryMeta(t, queryMeta)
|
||||
|
||||
// Create an ACL policy that can be referenced within the ACL role.
|
||||
aclPolicy := ACLPolicy{
|
||||
Name: "acl-role-api-test",
|
||||
Rules: `namespace "default" {
|
||||
policy = "read"
|
||||
}
|
||||
`,
|
||||
}
|
||||
writeMeta, err := testClient.ACLPolicies().Upsert(&aclPolicy, nil)
|
||||
require.NoError(t, err)
|
||||
assertWriteMeta(t, writeMeta)
|
||||
|
||||
// Create an ACL role referencing the previously created policy.
|
||||
role := ACLRole{
|
||||
Name: "acl-role-api-test",
|
||||
Policies: []*ACLRolePolicyLink{{Name: aclPolicy.Name}},
|
||||
}
|
||||
aclRoleCreateResp, writeMeta, err := testClient.ACLRoles().Create(&role, nil)
|
||||
require.NoError(t, err)
|
||||
assertWriteMeta(t, writeMeta)
|
||||
require.NotEmpty(t, aclRoleCreateResp.ID)
|
||||
require.Equal(t, role.Name, aclRoleCreateResp.Name)
|
||||
|
||||
// Another listing should return one result.
|
||||
aclRoleListResp, queryMeta, err = testClient.ACLRoles().List(nil)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, aclRoleListResp, 1)
|
||||
assertQueryMeta(t, queryMeta)
|
||||
|
||||
// Read the role using its ID.
|
||||
aclRoleReadResp, queryMeta, err := testClient.ACLRoles().Get(aclRoleCreateResp.ID, nil)
|
||||
require.NoError(t, err)
|
||||
assertQueryMeta(t, queryMeta)
|
||||
require.Equal(t, aclRoleCreateResp, aclRoleReadResp)
|
||||
|
||||
// Read the role using its name.
|
||||
aclRoleReadResp, queryMeta, err = testClient.ACLRoles().GetByName(aclRoleCreateResp.Name, nil)
|
||||
require.NoError(t, err)
|
||||
assertQueryMeta(t, queryMeta)
|
||||
require.Equal(t, aclRoleCreateResp, aclRoleReadResp)
|
||||
|
||||
// Update the role name.
|
||||
role.Name = "acl-role-api-test-badger-badger-badger"
|
||||
role.ID = aclRoleCreateResp.ID
|
||||
aclRoleUpdateResp, writeMeta, err := testClient.ACLRoles().Update(&role, nil)
|
||||
require.NoError(t, err)
|
||||
assertWriteMeta(t, writeMeta)
|
||||
require.Equal(t, role.Name, aclRoleUpdateResp.Name)
|
||||
require.Equal(t, role.ID, aclRoleUpdateResp.ID)
|
||||
|
||||
// Delete the role.
|
||||
writeMeta, err = testClient.ACLRoles().Delete(aclRoleCreateResp.ID, nil)
|
||||
require.NoError(t, err)
|
||||
assertWriteMeta(t, writeMeta)
|
||||
|
||||
// Make sure there are no ACL roles now present.
|
||||
aclRoleListResp, queryMeta, err = testClient.ACLRoles().List(nil)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, aclRoleListResp)
|
||||
assertQueryMeta(t, queryMeta)
|
||||
}
|
||||
|
||||
@@ -322,3 +322,206 @@ func (s *HTTPServer) ExchangeOneTimeToken(resp http.ResponseWriter, req *http.Re
|
||||
setIndex(resp, out.Index)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ACLRoleListRequest performs a listing of ACL roles and is callable via the
|
||||
// /v1/acl/roles HTTP API.
|
||||
func (s *HTTPServer) ACLRoleListRequest(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.ACLRolesListRequest{}
|
||||
|
||||
if s.parse(resp, req, &args.Region, &args.QueryOptions) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Perform the RPC request.
|
||||
var reply structs.ACLRolesListResponse
|
||||
if err := s.agent.RPC(structs.ACLListRolesRPCMethod, &args, &reply); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
setMeta(resp, &reply.QueryMeta)
|
||||
|
||||
if reply.ACLRoles == nil {
|
||||
reply.ACLRoles = make([]*structs.ACLRole, 0)
|
||||
}
|
||||
return reply.ACLRoles, nil
|
||||
}
|
||||
|
||||
// ACLRoleRequest creates a new ACL role and is callable via the
|
||||
// /v1/acl/role HTTP API.
|
||||
func (s *HTTPServer) ACLRoleRequest(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.aclRoleUpsertRequest(resp, req, "")
|
||||
}
|
||||
|
||||
// ACLRoleSpecificRequest is callable via the /v1/acl/role/ HTTP API and
|
||||
// handles read via both the role name and ID, updates, and deletions.
|
||||
func (s *HTTPServer) ACLRoleSpecificRequest(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/role/")
|
||||
|
||||
// Split the request suffix in order to identify whether this is a lookup
|
||||
// of a service, or whether this includes a service and service identifier.
|
||||
suffixParts := strings.Split(reqSuffix, "/")
|
||||
|
||||
switch len(suffixParts) {
|
||||
case 1:
|
||||
// Ensure the role ID is not an empty string which is possible if the
|
||||
// caller requested "/v1/acl/role/"
|
||||
if suffixParts[0] == "" {
|
||||
return nil, CodedError(http.StatusBadRequest, "missing ACL role ID")
|
||||
}
|
||||
return s.aclRoleRequest(resp, req, suffixParts[0])
|
||||
case 2:
|
||||
// This endpoint only supports GET.
|
||||
if req.Method != http.MethodGet {
|
||||
return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod)
|
||||
}
|
||||
|
||||
// Ensure that the path is correct, otherwise the call could use
|
||||
// "/v1/acl/role/foobar/role-name" and successfully pass through here.
|
||||
if suffixParts[0] != "name" {
|
||||
return nil, CodedError(http.StatusBadRequest, "invalid URI")
|
||||
}
|
||||
|
||||
// Ensure the role name is not an empty string which is possible if the
|
||||
// caller requested "/v1/acl/role/name/"
|
||||
if suffixParts[1] == "" {
|
||||
return nil, CodedError(http.StatusBadRequest, "missing ACL role name")
|
||||
}
|
||||
|
||||
return s.aclRoleGetByNameRequest(resp, req, suffixParts[1])
|
||||
|
||||
default:
|
||||
return nil, CodedError(http.StatusBadRequest, "invalid URI")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *HTTPServer) aclRoleRequest(
|
||||
resp http.ResponseWriter, req *http.Request, roleID string) (interface{}, error) {
|
||||
|
||||
// Identify the method which indicates which downstream function should be
|
||||
// called.
|
||||
switch req.Method {
|
||||
case http.MethodGet:
|
||||
return s.aclRoleGetByIDRequest(resp, req, roleID)
|
||||
case http.MethodDelete:
|
||||
return s.aclRoleDeleteRequest(resp, req, roleID)
|
||||
case http.MethodPost, http.MethodPut:
|
||||
return s.aclRoleUpsertRequest(resp, req, roleID)
|
||||
default:
|
||||
return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *HTTPServer) aclRoleGetByIDRequest(
|
||||
resp http.ResponseWriter, req *http.Request, roleID string) (interface{}, error) {
|
||||
|
||||
args := structs.ACLRoleByIDRequest{
|
||||
RoleID: roleID,
|
||||
}
|
||||
if s.parse(resp, req, &args.Region, &args.QueryOptions) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var reply structs.ACLRoleByIDResponse
|
||||
if err := s.agent.RPC(structs.ACLGetRoleByIDRPCMethod, &args, &reply); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setMeta(resp, &reply.QueryMeta)
|
||||
|
||||
if reply.ACLRole == nil {
|
||||
return nil, CodedError(http.StatusNotFound, "ACL role not found")
|
||||
}
|
||||
return reply.ACLRole, nil
|
||||
}
|
||||
|
||||
func (s *HTTPServer) aclRoleDeleteRequest(
|
||||
resp http.ResponseWriter, req *http.Request, roleID string) (interface{}, error) {
|
||||
|
||||
args := structs.ACLRolesDeleteByIDRequest{
|
||||
ACLRoleIDs: []string{roleID},
|
||||
}
|
||||
s.parseWriteRequest(req, &args.WriteRequest)
|
||||
|
||||
var reply structs.ACLRolesDeleteByIDResponse
|
||||
if err := s.agent.RPC(structs.ACLDeleteRolesByIDRPCMethod, &args, &reply); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setIndex(resp, reply.Index)
|
||||
return nil, nil
|
||||
|
||||
}
|
||||
|
||||
// aclRoleUpsertRequest handles upserting an ACL to the Nomad servers. It can
|
||||
// handle both new creations, and updates to existing roles.
|
||||
func (s *HTTPServer) aclRoleUpsertRequest(
|
||||
resp http.ResponseWriter, req *http.Request, roleID string) (interface{}, error) {
|
||||
|
||||
// Decode the ACL role.
|
||||
var aclRole structs.ACLRole
|
||||
if err := decodeBody(req, &aclRole); err != nil {
|
||||
return nil, CodedError(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
// Ensure the request path ID matches the ACL role 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 role request path.
|
||||
if roleID != "" && roleID != aclRole.ID {
|
||||
return nil, CodedError(http.StatusBadRequest, "ACL role ID does not match request path")
|
||||
}
|
||||
|
||||
args := structs.ACLRolesUpsertRequest{
|
||||
ACLRoles: []*structs.ACLRole{&aclRole},
|
||||
}
|
||||
s.parseWriteRequest(req, &args.WriteRequest)
|
||||
|
||||
var out structs.ACLRolesUpsertResponse
|
||||
if err := s.agent.RPC(structs.ACLUpsertRolesRPCMethod, &args, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setIndex(resp, out.Index)
|
||||
|
||||
if len(out.ACLRoles) > 0 {
|
||||
return out.ACLRoles[0], nil
|
||||
}
|
||||
return nil, nil
|
||||
|
||||
}
|
||||
|
||||
func (s *HTTPServer) aclRoleGetByNameRequest(
|
||||
resp http.ResponseWriter, req *http.Request, roleName string) (interface{}, error) {
|
||||
|
||||
args := structs.ACLRoleByNameRequest{
|
||||
RoleName: roleName,
|
||||
}
|
||||
if s.parse(resp, req, &args.Region, &args.QueryOptions) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var reply structs.ACLRoleByNameResponse
|
||||
if err := s.agent.RPC(structs.ACLGetRoleByNameRPCMethod, &args, &reply); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
setMeta(resp, &reply.QueryMeta)
|
||||
|
||||
if reply.ACLRole == nil {
|
||||
return nil, CodedError(http.StatusNotFound, "ACL role not found")
|
||||
}
|
||||
return reply.ACLRole, nil
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/helper/uuid"
|
||||
"github.com/hashicorp/nomad/nomad/mock"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -558,3 +560,438 @@ func TestHTTP_OneTimeToken(t *testing.T) {
|
||||
require.EqualError(t, err, structs.ErrPermissionDenied.Error())
|
||||
})
|
||||
}
|
||||
|
||||
func TestHTTPServer_ACLRoleListRequest(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/roles", nil)
|
||||
require.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := srv.Server.ACLRoleListRequest(respW, req)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "Permission denied")
|
||||
require.Nil(t, obj)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid method",
|
||||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(http.MethodConnect, "/v1/acl/roles", nil)
|
||||
require.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Ensure we have a token set.
|
||||
setToken(req, srv.RootToken)
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := srv.Server.ACLRoleListRequest(respW, req)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "Invalid method")
|
||||
require.Nil(t, obj)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no roles in state",
|
||||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(http.MethodGet, "/v1/acl/roles", nil)
|
||||
require.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Ensure we have a token set.
|
||||
setToken(req, srv.RootToken)
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := srv.Server.ACLRoleListRequest(respW, req)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, obj.([]*structs.ACLRole))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "roles in state",
|
||||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Create the policies our ACL roles wants to link to.
|
||||
policy1 := mock.ACLPolicy()
|
||||
policy1.Name = "mocked-test-policy-1"
|
||||
policy2 := mock.ACLPolicy()
|
||||
policy2.Name = "mocked-test-policy-2"
|
||||
|
||||
require.NoError(t, srv.server.State().UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
||||
|
||||
// Create two ACL roles and put these directly into state.
|
||||
aclRoles := []*structs.ACLRole{mock.ACLRole(), mock.ACLRole()}
|
||||
require.NoError(t, srv.server.State().UpsertACLRoles(structs.MsgTypeTestSetup, 20, aclRoles))
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(http.MethodGet, "/v1/acl/roles", nil)
|
||||
require.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Ensure we have a token set.
|
||||
setToken(req, srv.RootToken)
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := srv.Server.ACLRoleListRequest(respW, req)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, obj.([]*structs.ACLRole), 2)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "roles in state using prefix",
|
||||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Create the policies our ACL roles wants to link to.
|
||||
policy1 := mock.ACLPolicy()
|
||||
policy1.Name = "mocked-test-policy-1"
|
||||
policy2 := mock.ACLPolicy()
|
||||
policy2.Name = "mocked-test-policy-2"
|
||||
|
||||
require.NoError(t, srv.server.State().UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
||||
|
||||
// Create two ACL roles and put these directly into state, one
|
||||
// using a custom prefix.
|
||||
aclRoles := []*structs.ACLRole{mock.ACLRole(), mock.ACLRole()}
|
||||
aclRoles[1].ID = "badger-badger-badger-" + uuid.Generate()
|
||||
require.NoError(t, srv.server.State().UpsertACLRoles(structs.MsgTypeTestSetup, 20, aclRoles))
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(http.MethodGet, "/v1/acl/roles?prefix=badger-badger-badger", nil)
|
||||
require.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Ensure we have a token set.
|
||||
setToken(req, srv.RootToken)
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := srv.Server.ACLRoleListRequest(respW, req)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, obj.([]*structs.ACLRole), 1)
|
||||
require.Contains(t, obj.([]*structs.ACLRole)[0].ID, "badger-badger-badger")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
httpACLTest(t, nil, tc.testFn)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPServer_ACLRoleRequest(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 role to use in the request body.
|
||||
mockACLRole := mock.ACLRole()
|
||||
mockACLRole.ID = ""
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(http.MethodPut, "/v1/acl/role", encodeReq(mockACLRole))
|
||||
require.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := srv.Server.ACLRoleRequest(respW, req)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "Permission denied")
|
||||
require.Nil(t, obj)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid method",
|
||||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(http.MethodConnect, "/v1/acl/role", nil)
|
||||
require.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Ensure we have a token set.
|
||||
setToken(req, srv.RootToken)
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := srv.Server.ACLRoleRequest(respW, req)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "Invalid method")
|
||||
require.Nil(t, obj)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "successful upsert",
|
||||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Create the policies our ACL roles wants to link to.
|
||||
policy1 := mock.ACLPolicy()
|
||||
policy1.Name = "mocked-test-policy-1"
|
||||
policy2 := mock.ACLPolicy()
|
||||
policy2.Name = "mocked-test-policy-2"
|
||||
|
||||
require.NoError(t, srv.server.State().UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
||||
|
||||
// Create a mock role to use in the request body.
|
||||
mockACLRole := mock.ACLRole()
|
||||
mockACLRole.ID = ""
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(http.MethodPut, "/v1/acl/role", encodeReq(mockACLRole))
|
||||
require.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Ensure we have a token set.
|
||||
setToken(req, srv.RootToken)
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := srv.Server.ACLRoleRequest(respW, req)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, obj)
|
||||
require.Equal(t, obj.(*structs.ACLRole).Hash, mockACLRole.Hash)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
httpACLTest(t, nil, tc.testFn)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPServer_ACLRoleSpecificRequest(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
testFn func(srv *TestAgent)
|
||||
}{
|
||||
{
|
||||
name: "invalid URI",
|
||||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(http.MethodGet, "/v1/acl/role/name/this/is/will/not/work", nil)
|
||||
require.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := srv.Server.ACLRoleSpecificRequest(respW, req)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "invalid URI")
|
||||
require.Nil(t, obj)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid role name lookalike URI",
|
||||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(http.MethodGet, "/v1/acl/role/foobar/rolename", nil)
|
||||
require.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := srv.Server.ACLRoleSpecificRequest(respW, req)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "invalid URI")
|
||||
require.Nil(t, obj)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing role name",
|
||||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(http.MethodGet, "/v1/acl/role/name/", nil)
|
||||
require.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := srv.Server.ACLRoleSpecificRequest(respW, req)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "missing ACL role name")
|
||||
require.Nil(t, obj)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing role ID",
|
||||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(http.MethodGet, "/v1/acl/role/", nil)
|
||||
require.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := srv.Server.ACLRoleSpecificRequest(respW, req)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "missing ACL role ID")
|
||||
require.Nil(t, obj)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "role name incorrect method",
|
||||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(http.MethodConnect, "/v1/acl/role/name/foobar", nil)
|
||||
require.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := srv.Server.ACLRoleSpecificRequest(respW, req)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "Invalid method")
|
||||
require.Nil(t, obj)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "role ID incorrect method",
|
||||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(http.MethodConnect, "/v1/acl/role/foobar", nil)
|
||||
require.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := srv.Server.ACLRoleSpecificRequest(respW, req)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "Invalid method")
|
||||
require.Nil(t, obj)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get role by name",
|
||||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Create the policies our ACL roles wants to link to.
|
||||
policy1 := mock.ACLPolicy()
|
||||
policy1.Name = "mocked-test-policy-1"
|
||||
policy2 := mock.ACLPolicy()
|
||||
policy2.Name = "mocked-test-policy-2"
|
||||
|
||||
require.NoError(t, srv.server.State().UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
||||
|
||||
// Create a mock role and put directly into state.
|
||||
mockACLRole := mock.ACLRole()
|
||||
require.NoError(t, srv.server.State().UpsertACLRoles(
|
||||
structs.MsgTypeTestSetup, 20, []*structs.ACLRole{mockACLRole}))
|
||||
|
||||
url := fmt.Sprintf("/v1/acl/role/name/%s", mockACLRole.Name)
|
||||
|
||||
// Build the HTTP request.
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
require.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Ensure we have a token set.
|
||||
setToken(req, srv.RootToken)
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := srv.Server.ACLRoleSpecificRequest(respW, req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, obj.(*structs.ACLRole).Hash, mockACLRole.Hash)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get, update, and delete role by ID",
|
||||
testFn: func(srv *TestAgent) {
|
||||
|
||||
// Create the policies our ACL roles wants to link to.
|
||||
policy1 := mock.ACLPolicy()
|
||||
policy1.Name = "mocked-test-policy-1"
|
||||
policy2 := mock.ACLPolicy()
|
||||
policy2.Name = "mocked-test-policy-2"
|
||||
|
||||
require.NoError(t, srv.server.State().UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
||||
|
||||
// Create a mock role and put directly into state.
|
||||
mockACLRole := mock.ACLRole()
|
||||
require.NoError(t, srv.server.State().UpsertACLRoles(
|
||||
structs.MsgTypeTestSetup, 20, []*structs.ACLRole{mockACLRole}))
|
||||
|
||||
url := fmt.Sprintf("/v1/acl/role/%s", mockACLRole.ID)
|
||||
|
||||
// Build the HTTP request to read the role using its ID.
|
||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
||||
require.NoError(t, err)
|
||||
respW := httptest.NewRecorder()
|
||||
|
||||
// Ensure we have a token set.
|
||||
setToken(req, srv.RootToken)
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err := srv.Server.ACLRoleSpecificRequest(respW, req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, obj.(*structs.ACLRole).Hash, mockACLRole.Hash)
|
||||
|
||||
// Update the role policy list and make the request via the
|
||||
// HTTP API.
|
||||
mockACLRole.Policies = []*structs.ACLRolePolicyLink{{Name: "mocked-test-policy-1"}}
|
||||
|
||||
req, err = http.NewRequest(http.MethodPost, url, encodeReq(mockACLRole))
|
||||
require.NoError(t, err)
|
||||
respW = httptest.NewRecorder()
|
||||
|
||||
// Ensure we have a token set.
|
||||
setToken(req, srv.RootToken)
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err = srv.Server.ACLRoleSpecificRequest(respW, req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, obj.(*structs.ACLRole).Policies, mockACLRole.Policies)
|
||||
|
||||
// Delete the ACL role using its ID.
|
||||
req, err = http.NewRequest(http.MethodDelete, url, nil)
|
||||
require.NoError(t, err)
|
||||
respW = httptest.NewRecorder()
|
||||
|
||||
// Ensure we have a token set.
|
||||
setToken(req, srv.RootToken)
|
||||
|
||||
// Send the HTTP request.
|
||||
obj, err = srv.Server.ACLRoleSpecificRequest(respW, req)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, obj)
|
||||
|
||||
// Ensure the ACL role is no longer stored within state.
|
||||
aclRole, err := srv.server.State().GetACLRoleByID(nil, mockACLRole.ID)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, aclRole)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
httpACLTest(t, nil, tc.testFn)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -381,6 +381,11 @@ func (s HTTPServer) registerHandlers(enableDebug bool) {
|
||||
s.mux.HandleFunc("/v1/acl/token", s.wrap(s.ACLTokenSpecificRequest))
|
||||
s.mux.HandleFunc("/v1/acl/token/", s.wrap(s.ACLTokenSpecificRequest))
|
||||
|
||||
// Register our ACL role handlers.
|
||||
s.mux.HandleFunc("/v1/acl/roles", s.wrap(s.ACLRoleListRequest))
|
||||
s.mux.HandleFunc("/v1/acl/role", s.wrap(s.ACLRoleRequest))
|
||||
s.mux.HandleFunc("/v1/acl/role/", s.wrap(s.ACLRoleSpecificRequest))
|
||||
|
||||
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)))
|
||||
|
||||
@@ -526,7 +526,7 @@ func (a *ACL) UpsertTokens(args *structs.ACLTokenUpsertRequest, reply *structs.A
|
||||
if token.AccessorID != "" {
|
||||
out, err := stateSnapshot.ACLTokenByAccessorID(nil, token.AccessorID)
|
||||
if err != nil {
|
||||
return structs.NewErrRPCCodedf(http.StatusBadRequest, "token lookup failed: %v", err)
|
||||
return structs.NewErrRPCCodedf(http.StatusInternalServerError, "token lookup failed: %v", err)
|
||||
}
|
||||
if out == nil {
|
||||
return structs.NewErrRPCCodedf(http.StatusBadRequest, "cannot find token %s", token.AccessorID)
|
||||
@@ -1028,3 +1028,341 @@ func (a *ACL) ExpireOneTimeTokens(args *structs.OneTimeTokenExpireRequest, reply
|
||||
reply.Index = index
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpsertRoles creates or updates ACL roles held within Nomad.
|
||||
func (a *ACL) UpsertRoles(
|
||||
args *structs.ACLRolesUpsertRequest,
|
||||
reply *structs.ACLRolesUpsertResponse) error {
|
||||
|
||||
// Only allow operators to upsert ACL roles when ACLs are enabled.
|
||||
if !a.srv.config.ACLEnabled {
|
||||
return aclDisabled
|
||||
}
|
||||
|
||||
// This endpoint always forwards to the authoritative region as ACL roles
|
||||
// are global.
|
||||
args.Region = a.srv.config.AuthoritativeRegion
|
||||
|
||||
if done, err := a.srv.forward(structs.ACLUpsertRolesRPCMethod, args, args, reply); done {
|
||||
return err
|
||||
}
|
||||
defer metrics.MeasureSince([]string{"nomad", "acl", "upsert_roles"}, time.Now())
|
||||
|
||||
// Only tokens with management level permissions can create ACL roles.
|
||||
if acl, err := a.srv.ResolveToken(args.AuthToken); err != nil {
|
||||
return err
|
||||
} else if acl == nil || !acl.IsManagement() {
|
||||
return structs.ErrPermissionDenied
|
||||
}
|
||||
|
||||
// Snapshot the state so we can perform lookups against the ID and policy
|
||||
// links if needed. Do it here, so we only need to do this once no matter
|
||||
// how many roles we are upserting.
|
||||
stateSnapshot, err := a.srv.State().Snapshot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Validate each role.
|
||||
for idx, role := range args.ACLRoles {
|
||||
|
||||
// Perform all the static validation of the ACL role object. Use the
|
||||
// array index as we cannot be sure the error was caused by a missing
|
||||
// name.
|
||||
if err := role.Validate(); err != nil {
|
||||
return structs.NewErrRPCCodedf(http.StatusBadRequest, "role %d invalid: %v", idx, err)
|
||||
}
|
||||
|
||||
policyNames := make(map[string]struct{})
|
||||
var policiesLinks []*structs.ACLRolePolicyLink
|
||||
|
||||
// We need to deduplicate the ACL policy links within this role as well
|
||||
// as ensure the policies exist within state.
|
||||
for _, policyLink := range role.Policies {
|
||||
|
||||
// Perform a state look up for the policy. An error or not being
|
||||
// able to find the policy is terminal. We can include the name in
|
||||
// the error message as it has previously been validated.
|
||||
existing, err := stateSnapshot.ACLPolicyByName(nil, policyLink.Name)
|
||||
if err != nil {
|
||||
return structs.NewErrRPCCodedf(http.StatusInternalServerError, "policy lookup failed: %v", err)
|
||||
}
|
||||
if existing == nil {
|
||||
return structs.NewErrRPCCodedf(http.StatusBadRequest, "cannot find policy %s", policyLink.Name)
|
||||
}
|
||||
|
||||
// If the policy name is not found within our map, this means we
|
||||
// have not seen it previously. We need to add this to our
|
||||
// deduplicated array and also mark the policy name as seen, so we
|
||||
// skip any future policies of the same name.
|
||||
if _, ok := policyNames[policyLink.Name]; !ok {
|
||||
policiesLinks = append(policiesLinks, policyLink)
|
||||
policyNames[policyLink.Name] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
// Stored the potentially updated policy links within our role.
|
||||
role.Policies = policiesLinks
|
||||
|
||||
// If the caller has passed a role ID, this call is considered an
|
||||
// update to an existing role. We should therefore ensure it is found
|
||||
// within state.
|
||||
if role.ID != "" {
|
||||
out, err := stateSnapshot.GetACLRoleByID(nil, role.ID)
|
||||
if err != nil {
|
||||
return structs.NewErrRPCCodedf(http.StatusBadRequest, "role lookup failed: %v", err)
|
||||
}
|
||||
if out == nil {
|
||||
return structs.NewErrRPCCodedf(http.StatusBadRequest, "cannot find role %s", role.ID)
|
||||
}
|
||||
}
|
||||
|
||||
role.Canonicalize()
|
||||
role.SetHash()
|
||||
}
|
||||
|
||||
// Update via Raft.
|
||||
out, index, err := a.srv.raftApply(structs.ACLRolesUpsertRequestType, 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 times.
|
||||
stateSnapshot, err = a.srv.State().Snapshot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, role := range args.ACLRoles {
|
||||
lookupACLRole, err := stateSnapshot.GetACLRoleByName(nil, role.Name)
|
||||
if err != nil {
|
||||
return structs.NewErrRPCCodedf(400, "ACL role lookup failed: %v", err)
|
||||
}
|
||||
reply.ACLRoles = append(reply.ACLRoles, lookupACLRole)
|
||||
}
|
||||
|
||||
// Update the index. There is no need to floor this as we are writing to
|
||||
// state and therefore will get a non-zero index response.
|
||||
reply.Index = index
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteRolesByID is used to batch delete ACL roles using the ID as the
|
||||
// deletion key.
|
||||
func (a *ACL) DeleteRolesByID(
|
||||
args *structs.ACLRolesDeleteByIDRequest,
|
||||
reply *structs.ACLRolesDeleteByIDResponse) error {
|
||||
|
||||
// Only allow operators to delete ACL roles when ACLs are enabled.
|
||||
if !a.srv.config.ACLEnabled {
|
||||
return aclDisabled
|
||||
}
|
||||
|
||||
// This endpoint always forwards to the authoritative region as ACL roles
|
||||
// are global.
|
||||
args.Region = a.srv.config.AuthoritativeRegion
|
||||
|
||||
if done, err := a.srv.forward(structs.ACLDeleteRolesByIDRPCMethod, args, args, reply); done {
|
||||
return err
|
||||
}
|
||||
defer metrics.MeasureSince([]string{"nomad", "acl", "delete_roles"}, time.Now())
|
||||
|
||||
// Only tokens with management level permissions can create ACL roles.
|
||||
if acl, err := a.srv.ResolveToken(args.AuthToken); err != nil {
|
||||
return err
|
||||
} else if acl == nil || !acl.IsManagement() {
|
||||
return structs.ErrPermissionDenied
|
||||
}
|
||||
|
||||
// Update via Raft.
|
||||
out, index, err := a.srv.raftApply(structs.ACLRolesDeleteByIDRequestType, 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. There is no need to floor this as we are writing to
|
||||
// state and therefore will get a non-zero index response.
|
||||
reply.Index = index
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListRoles is used to list ACL roles within state. If not prefix is supplied,
|
||||
// all ACL roles are listed, otherwise a prefix search is performed on the ACL
|
||||
// role name.
|
||||
func (a *ACL) ListRoles(
|
||||
args *structs.ACLRolesListRequest,
|
||||
reply *structs.ACLRolesListResponse) error {
|
||||
|
||||
// Only allow operators to list ACL roles when ACLs are enabled.
|
||||
if !a.srv.config.ACLEnabled {
|
||||
return aclDisabled
|
||||
}
|
||||
|
||||
if done, err := a.srv.forward(structs.ACLListRolesRPCMethod, args, args, reply); done {
|
||||
return err
|
||||
}
|
||||
defer metrics.MeasureSince([]string{"nomad", "acl", "list_roles"}, time.Now())
|
||||
|
||||
// TODO (jrasell) allow callers to list role associated to their token.
|
||||
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 {
|
||||
|
||||
var (
|
||||
err error
|
||||
iter memdb.ResultIterator
|
||||
)
|
||||
|
||||
// If the operator supplied a prefix, perform a prefix search.
|
||||
// Otherwise, list all ACL roles in state.
|
||||
switch args.QueryOptions.Prefix {
|
||||
case "":
|
||||
iter, err = stateStore.GetACLRoles(ws)
|
||||
default:
|
||||
iter, err = stateStore.GetACLRoleByIDPrefix(ws, args.QueryOptions.Prefix)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Iterate all the results and add these to our reply object. There
|
||||
// is no stub object for an ACL role and the hash is needed by the
|
||||
// replication process.
|
||||
for raw := iter.Next(); raw != nil; raw = iter.Next() {
|
||||
reply.ACLRoles = append(reply.ACLRoles, raw.(*structs.ACLRole))
|
||||
}
|
||||
|
||||
// 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.TableACLRoles, &reply.QueryMeta)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetRoleByID is used to look up an individual ACL role using its ID.
|
||||
func (a *ACL) GetRoleByID(
|
||||
args *structs.ACLRoleByIDRequest,
|
||||
reply *structs.ACLRoleByIDResponse) error {
|
||||
|
||||
// Only allow operators to read an ACL role when ACLs are enabled.
|
||||
if !a.srv.config.ACLEnabled {
|
||||
return aclDisabled
|
||||
}
|
||||
|
||||
if done, err := a.srv.forward(structs.ACLGetRoleByIDRPCMethod, args, args, reply); done {
|
||||
return err
|
||||
}
|
||||
defer metrics.MeasureSince([]string{"nomad", "acl", "get_role_id"}, time.Now())
|
||||
|
||||
// TODO (jrasell) allow callers to detail a role associated to their token.
|
||||
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.GetACLRoleByID(ws, args.RoleID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the index correctly depending on whether the ACL role was
|
||||
// found.
|
||||
switch out {
|
||||
case nil:
|
||||
index, err := stateStore.Index(state.TableACLRoles)
|
||||
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
|
||||
// role on the reply and exit successfully.
|
||||
reply.ACLRole = out
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// GetRoleByName is used to look up an individual ACL role using its name.
|
||||
func (a *ACL) GetRoleByName(
|
||||
args *structs.ACLRoleByNameRequest,
|
||||
reply *structs.ACLRoleByNameResponse) error {
|
||||
|
||||
// Only allow operators to read an ACL role when ACLs are enabled.
|
||||
if !a.srv.config.ACLEnabled {
|
||||
return aclDisabled
|
||||
}
|
||||
|
||||
if done, err := a.srv.forward(structs.ACLGetRoleByNameRPCMethod, args, args, reply); done {
|
||||
return err
|
||||
}
|
||||
defer metrics.MeasureSince([]string{"nomad", "acl", "get_role_name"}, time.Now())
|
||||
|
||||
// TODO (jrasell) allow callers to detail a role associated to their token.
|
||||
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.GetACLRoleByName(ws, args.RoleName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the index correctly depending on whether the ACL role was
|
||||
// found.
|
||||
switch out {
|
||||
case nil:
|
||||
index, err := stateStore.Index(state.TableACLRoles)
|
||||
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
|
||||
// role on the reply and exit successfully.
|
||||
reply.ACLRole = out
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-memdb"
|
||||
msgpackrpc "github.com/hashicorp/net-rpc-msgpackrpc"
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/helper/uuid"
|
||||
@@ -1776,3 +1777,389 @@ func TestACLEndpoint_OneTimeToken(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, ott)
|
||||
}
|
||||
|
||||
func TestACL_UpsertRoles(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 role and remove the ID so this looks like a creation.
|
||||
aclRole1 := mock.ACLRole()
|
||||
aclRole1.ID = ""
|
||||
|
||||
// Attempt to upsert this role without setting an ACL token. This should
|
||||
// fail.
|
||||
aclRoleReq1 := &structs.ACLRolesUpsertRequest{
|
||||
ACLRoles: []*structs.ACLRole{aclRole1},
|
||||
WriteRequest: structs.WriteRequest{
|
||||
Region: "global",
|
||||
},
|
||||
}
|
||||
var aclRoleResp1 structs.ACLRolesUpsertResponse
|
||||
err := msgpackrpc.CallWithCodec(codec, structs.ACLUpsertRolesRPCMethod, aclRoleReq1, &aclRoleResp1)
|
||||
require.ErrorContains(t, err, "Permission denied")
|
||||
|
||||
// Attempt to upsert this role again, this time setting the ACL root token.
|
||||
// This should fail because the linked policies do not exist within state.
|
||||
aclRoleReq2 := &structs.ACLRolesUpsertRequest{
|
||||
ACLRoles: []*structs.ACLRole{aclRole1},
|
||||
WriteRequest: structs.WriteRequest{
|
||||
Region: DefaultRegion,
|
||||
AuthToken: aclRootToken.SecretID,
|
||||
},
|
||||
}
|
||||
var aclRoleResp2 structs.ACLRolesUpsertResponse
|
||||
err = msgpackrpc.CallWithCodec(codec, structs.ACLUpsertRolesRPCMethod, aclRoleReq2, &aclRoleResp2)
|
||||
require.ErrorContains(t, err, "cannot find policy")
|
||||
|
||||
// Create the policies our ACL roles wants to link to.
|
||||
policy1 := mock.ACLPolicy()
|
||||
policy1.Name = "mocked-test-policy-1"
|
||||
policy2 := mock.ACLPolicy()
|
||||
policy2.Name = "mocked-test-policy-2"
|
||||
|
||||
require.NoError(t, testServer.fsm.State().UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
||||
|
||||
// Try the upsert a third time, which should succeed.
|
||||
aclRoleReq3 := &structs.ACLRolesUpsertRequest{
|
||||
ACLRoles: []*structs.ACLRole{aclRole1},
|
||||
WriteRequest: structs.WriteRequest{
|
||||
Region: DefaultRegion,
|
||||
AuthToken: aclRootToken.SecretID,
|
||||
},
|
||||
}
|
||||
var aclRoleResp3 structs.ACLRolesUpsertResponse
|
||||
err = msgpackrpc.CallWithCodec(codec, structs.ACLUpsertRolesRPCMethod, aclRoleReq3, &aclRoleResp3)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, aclRoleResp3.ACLRoles, 1)
|
||||
require.True(t, aclRole1.Equals(aclRoleResp3.ACLRoles[0]))
|
||||
|
||||
// Perform an update of the ACL role by removing a policy and changing the
|
||||
// name.
|
||||
aclRole1Copy := aclRole1.Copy()
|
||||
aclRole1Copy.Name = "updated-role-name"
|
||||
aclRole1Copy.Policies = append(aclRole1Copy.Policies[:1], aclRole1Copy.Policies[1+1:]...)
|
||||
aclRole1Copy.SetHash()
|
||||
|
||||
aclRoleReq4 := &structs.ACLRolesUpsertRequest{
|
||||
ACLRoles: []*structs.ACLRole{aclRole1Copy},
|
||||
WriteRequest: structs.WriteRequest{
|
||||
Region: DefaultRegion,
|
||||
AuthToken: aclRootToken.SecretID,
|
||||
},
|
||||
}
|
||||
var aclRoleResp4 structs.ACLRolesUpsertResponse
|
||||
err = msgpackrpc.CallWithCodec(codec, structs.ACLUpsertRolesRPCMethod, aclRoleReq4, &aclRoleResp4)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, aclRoleResp4.ACLRoles, 1)
|
||||
require.True(t, aclRole1Copy.Equals(aclRoleResp4.ACLRoles[0]))
|
||||
require.Greater(t, aclRoleResp4.ACLRoles[0].ModifyIndex, aclRoleResp3.ACLRoles[0].ModifyIndex)
|
||||
|
||||
// Create another ACL role that will fail validation. Attempting to upsert
|
||||
// this ensures the handler is triggering the validation function.
|
||||
aclRole2 := mock.ACLRole()
|
||||
aclRole2.Policies = nil
|
||||
|
||||
aclRoleReq5 := &structs.ACLRolesUpsertRequest{
|
||||
ACLRoles: []*structs.ACLRole{aclRole2},
|
||||
WriteRequest: structs.WriteRequest{
|
||||
Region: DefaultRegion,
|
||||
AuthToken: aclRootToken.SecretID,
|
||||
},
|
||||
}
|
||||
var aclRoleResp5 structs.ACLRolesUpsertResponse
|
||||
err = msgpackrpc.CallWithCodec(codec, structs.ACLUpsertRolesRPCMethod, aclRoleReq5, &aclRoleResp5)
|
||||
require.Error(t, err)
|
||||
require.NotContains(t, err, "Permission denied")
|
||||
}
|
||||
|
||||
func TestACL_DeleteRolesByID(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
testServer, aclRootToken, testServerCleanupFn := TestACLServer(t, nil)
|
||||
defer testServerCleanupFn()
|
||||
codec := rpcClient(t, testServer)
|
||||
testutil.WaitForLeader(t, testServer.RPC)
|
||||
|
||||
// Create the policies our ACL roles wants to link to.
|
||||
policy1 := mock.ACLPolicy()
|
||||
policy1.Name = "mocked-test-policy-1"
|
||||
policy2 := mock.ACLPolicy()
|
||||
policy2.Name = "mocked-test-policy-2"
|
||||
|
||||
require.NoError(t, testServer.fsm.State().UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
||||
|
||||
// Create two ACL roles and put these directly into state.
|
||||
aclRoles := []*structs.ACLRole{mock.ACLRole(), mock.ACLRole()}
|
||||
require.NoError(t, testServer.State().UpsertACLRoles(structs.MsgTypeTestSetup, 10, aclRoles))
|
||||
|
||||
// Attempt to delete an ACL role without setting an auth token. This should
|
||||
// fail.
|
||||
aclRoleReq1 := &structs.ACLRolesDeleteByIDRequest{
|
||||
ACLRoleIDs: []string{aclRoles[0].ID},
|
||||
WriteRequest: structs.WriteRequest{
|
||||
Region: DefaultRegion,
|
||||
},
|
||||
}
|
||||
var aclRoleResp1 structs.ACLRolesDeleteByIDResponse
|
||||
err := msgpackrpc.CallWithCodec(codec, structs.ACLDeleteRolesByIDRPCMethod, aclRoleReq1, &aclRoleResp1)
|
||||
require.ErrorContains(t, err, "Permission denied")
|
||||
|
||||
// Attempt to delete an ACL role now using a valid management token which
|
||||
// should succeed.
|
||||
aclRoleReq2 := &structs.ACLRolesDeleteByIDRequest{
|
||||
ACLRoleIDs: []string{aclRoles[0].ID},
|
||||
WriteRequest: structs.WriteRequest{
|
||||
Region: DefaultRegion,
|
||||
AuthToken: aclRootToken.SecretID,
|
||||
},
|
||||
}
|
||||
var aclRoleResp2 structs.ACLRolesDeleteByIDResponse
|
||||
err = msgpackrpc.CallWithCodec(codec, structs.ACLDeleteRolesByIDRPCMethod, aclRoleReq2, &aclRoleResp2)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Ensure the deleted role is not found within state and that the other is.
|
||||
ws := memdb.NewWatchSet()
|
||||
iter, err := testServer.State().GetACLRoles(ws)
|
||||
require.NoError(t, err)
|
||||
|
||||
var aclRolesLookup []*structs.ACLRole
|
||||
for raw := iter.Next(); raw != nil; raw = iter.Next() {
|
||||
aclRolesLookup = append(aclRolesLookup, raw.(*structs.ACLRole))
|
||||
}
|
||||
|
||||
require.Len(t, aclRolesLookup, 1)
|
||||
require.True(t, aclRolesLookup[0].Equals(aclRoles[1]))
|
||||
|
||||
// Try to delete the previously deleted ACL role, this should fail.
|
||||
aclRoleReq3 := &structs.ACLRolesDeleteByIDRequest{
|
||||
ACLRoleIDs: []string{aclRoles[0].ID},
|
||||
WriteRequest: structs.WriteRequest{
|
||||
Region: DefaultRegion,
|
||||
AuthToken: aclRootToken.SecretID,
|
||||
},
|
||||
}
|
||||
var aclRoleResp3 structs.ACLRolesDeleteByIDResponse
|
||||
err = msgpackrpc.CallWithCodec(codec, structs.ACLDeleteRolesByIDRPCMethod, aclRoleReq3, &aclRoleResp3)
|
||||
require.ErrorContains(t, err, "ACL role not found")
|
||||
}
|
||||
|
||||
func TestACL_ListRoles(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
testServer, aclRootToken, testServerCleanupFn := TestACLServer(t, nil)
|
||||
defer testServerCleanupFn()
|
||||
codec := rpcClient(t, testServer)
|
||||
testutil.WaitForLeader(t, testServer.RPC)
|
||||
|
||||
// Create the policies our ACL roles wants to link to.
|
||||
policy1 := mock.ACLPolicy()
|
||||
policy1.Name = "mocked-test-policy-1"
|
||||
policy2 := mock.ACLPolicy()
|
||||
policy2.Name = "mocked-test-policy-2"
|
||||
|
||||
require.NoError(t, testServer.fsm.State().UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
||||
|
||||
// Create two ACL roles with a known prefix and put these directly into
|
||||
// state.
|
||||
aclRoles := []*structs.ACLRole{mock.ACLRole(), mock.ACLRole()}
|
||||
aclRoles[0].ID = "prefix-" + uuid.Generate()
|
||||
aclRoles[1].ID = "prefix-" + uuid.Generate()
|
||||
require.NoError(t, testServer.State().UpsertACLRoles(structs.MsgTypeTestSetup, 10, aclRoles))
|
||||
|
||||
// Try listing roles without a valid ACL token.
|
||||
aclRoleReq1 := &structs.ACLRolesListRequest{
|
||||
QueryOptions: structs.QueryOptions{
|
||||
Region: DefaultRegion,
|
||||
},
|
||||
}
|
||||
var aclRoleResp1 structs.ACLRolesListResponse
|
||||
err := msgpackrpc.CallWithCodec(codec, structs.ACLListRolesRPCMethod, aclRoleReq1, &aclRoleResp1)
|
||||
require.ErrorContains(t, err, "Permission denied")
|
||||
|
||||
// Try listing roles with a valid ACL token.
|
||||
aclRoleReq2 := &structs.ACLRolesListRequest{
|
||||
QueryOptions: structs.QueryOptions{
|
||||
Region: DefaultRegion,
|
||||
AuthToken: aclRootToken.SecretID,
|
||||
},
|
||||
}
|
||||
var aclRoleResp2 structs.ACLRolesListResponse
|
||||
err = msgpackrpc.CallWithCodec(codec, structs.ACLListRolesRPCMethod, aclRoleReq2, &aclRoleResp2)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, aclRoleResp2.ACLRoles, 2)
|
||||
|
||||
// Try listing roles with a valid ACL token using a prefix that doesn't
|
||||
// match anything.
|
||||
aclRoleReq3 := &structs.ACLRolesListRequest{
|
||||
QueryOptions: structs.QueryOptions{
|
||||
Region: DefaultRegion,
|
||||
AuthToken: aclRootToken.SecretID,
|
||||
Prefix: "please",
|
||||
},
|
||||
}
|
||||
var aclRoleResp3 structs.ACLRolesListResponse
|
||||
err = msgpackrpc.CallWithCodec(codec, structs.ACLListRolesRPCMethod, aclRoleReq3, &aclRoleResp3)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, aclRoleResp3.ACLRoles, 0)
|
||||
|
||||
// Try listing roles with a valid ACL token using a prefix that matches two
|
||||
// entries.
|
||||
aclRoleReq4 := &structs.ACLRolesListRequest{
|
||||
QueryOptions: structs.QueryOptions{
|
||||
Region: DefaultRegion,
|
||||
AuthToken: aclRootToken.SecretID,
|
||||
Prefix: "prefix-",
|
||||
},
|
||||
}
|
||||
var aclRoleResp4 structs.ACLRolesListResponse
|
||||
err = msgpackrpc.CallWithCodec(codec, structs.ACLListRolesRPCMethod, aclRoleReq4, &aclRoleResp4)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, aclRoleResp4.ACLRoles, 2)
|
||||
}
|
||||
|
||||
func TestACL_GetRoleByID(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
testServer, aclRootToken, testServerCleanupFn := TestACLServer(t, nil)
|
||||
defer testServerCleanupFn()
|
||||
codec := rpcClient(t, testServer)
|
||||
testutil.WaitForLeader(t, testServer.RPC)
|
||||
|
||||
// Create the policies our ACL roles wants to link to.
|
||||
policy1 := mock.ACLPolicy()
|
||||
policy1.Name = "mocked-test-policy-1"
|
||||
policy2 := mock.ACLPolicy()
|
||||
policy2.Name = "mocked-test-policy-2"
|
||||
|
||||
require.NoError(t, testServer.fsm.State().UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
||||
|
||||
// Create two ACL roles and put these directly into state.
|
||||
aclRoles := []*structs.ACLRole{mock.ACLRole(), mock.ACLRole()}
|
||||
require.NoError(t, testServer.State().UpsertACLRoles(structs.MsgTypeTestSetup, 10, aclRoles))
|
||||
|
||||
// Try reading a role without setting a correct auth token.
|
||||
aclRoleReq1 := &structs.ACLRoleByIDRequest{
|
||||
QueryOptions: structs.QueryOptions{
|
||||
Region: DefaultRegion,
|
||||
},
|
||||
}
|
||||
var aclRoleResp1 structs.ACLRoleByIDResponse
|
||||
err := msgpackrpc.CallWithCodec(codec, structs.ACLGetRoleByIDRPCMethod, aclRoleReq1, &aclRoleResp1)
|
||||
require.ErrorContains(t, err, "Permission denied")
|
||||
|
||||
// Try reading a role that doesn't exist.
|
||||
aclRoleReq2 := &structs.ACLRoleByIDRequest{
|
||||
RoleID: "nope",
|
||||
QueryOptions: structs.QueryOptions{
|
||||
Region: DefaultRegion,
|
||||
AuthToken: aclRootToken.SecretID,
|
||||
},
|
||||
}
|
||||
var aclRoleResp2 structs.ACLRoleByIDResponse
|
||||
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetRoleByIDRPCMethod, aclRoleReq2, &aclRoleResp2)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, aclRoleResp2.ACLRole)
|
||||
|
||||
// Read both our available ACL roles using a valid auth token.
|
||||
aclRoleReq3 := &structs.ACLRoleByIDRequest{
|
||||
RoleID: aclRoles[0].ID,
|
||||
QueryOptions: structs.QueryOptions{
|
||||
Region: DefaultRegion,
|
||||
AuthToken: aclRootToken.SecretID,
|
||||
},
|
||||
}
|
||||
var aclRoleResp3 structs.ACLRoleByIDResponse
|
||||
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetRoleByIDRPCMethod, aclRoleReq3, &aclRoleResp3)
|
||||
require.NoError(t, err)
|
||||
require.True(t, aclRoleResp3.ACLRole.Equals(aclRoles[0]))
|
||||
|
||||
aclRoleReq4 := &structs.ACLRoleByIDRequest{
|
||||
RoleID: aclRoles[1].ID,
|
||||
QueryOptions: structs.QueryOptions{
|
||||
Region: DefaultRegion,
|
||||
AuthToken: aclRootToken.SecretID,
|
||||
},
|
||||
}
|
||||
var aclRoleResp4 structs.ACLRoleByIDResponse
|
||||
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetRoleByIDRPCMethod, aclRoleReq4, &aclRoleResp4)
|
||||
require.NoError(t, err)
|
||||
require.True(t, aclRoleResp4.ACLRole.Equals(aclRoles[1]))
|
||||
}
|
||||
|
||||
func TestACL_GetRoleByName(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
|
||||
testServer, aclRootToken, testServerCleanupFn := TestACLServer(t, nil)
|
||||
defer testServerCleanupFn()
|
||||
codec := rpcClient(t, testServer)
|
||||
testutil.WaitForLeader(t, testServer.RPC)
|
||||
|
||||
// Create the policies our ACL roles wants to link to.
|
||||
policy1 := mock.ACLPolicy()
|
||||
policy1.Name = "mocked-test-policy-1"
|
||||
policy2 := mock.ACLPolicy()
|
||||
policy2.Name = "mocked-test-policy-2"
|
||||
|
||||
require.NoError(t, testServer.fsm.State().UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
||||
|
||||
// Create two ACL roles and put these directly into state.
|
||||
aclRoles := []*structs.ACLRole{mock.ACLRole(), mock.ACLRole()}
|
||||
require.NoError(t, testServer.State().UpsertACLRoles(structs.MsgTypeTestSetup, 10, aclRoles))
|
||||
|
||||
// Try reading a role without setting a correct auth token.
|
||||
aclRoleReq1 := &structs.ACLRoleByNameRequest{
|
||||
QueryOptions: structs.QueryOptions{
|
||||
Region: DefaultRegion,
|
||||
},
|
||||
}
|
||||
var aclRoleResp1 structs.ACLRoleByNameResponse
|
||||
err := msgpackrpc.CallWithCodec(codec, structs.ACLGetRoleByNameRPCMethod, aclRoleReq1, &aclRoleResp1)
|
||||
require.ErrorContains(t, err, "Permission denied")
|
||||
|
||||
// Try reading a role that doesn't exist.
|
||||
aclRoleReq2 := &structs.ACLRoleByNameRequest{
|
||||
RoleName: "nope",
|
||||
QueryOptions: structs.QueryOptions{
|
||||
Region: DefaultRegion,
|
||||
AuthToken: aclRootToken.SecretID,
|
||||
},
|
||||
}
|
||||
var aclRoleResp2 structs.ACLRoleByNameResponse
|
||||
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetRoleByNameRPCMethod, aclRoleReq2, &aclRoleResp2)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, aclRoleResp2.ACLRole)
|
||||
|
||||
// Read both our available ACL roles using a valid auth token.
|
||||
aclRoleReq3 := &structs.ACLRoleByNameRequest{
|
||||
RoleName: aclRoles[0].Name,
|
||||
QueryOptions: structs.QueryOptions{
|
||||
Region: DefaultRegion,
|
||||
AuthToken: aclRootToken.SecretID,
|
||||
},
|
||||
}
|
||||
var aclRoleResp3 structs.ACLRoleByNameResponse
|
||||
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetRoleByNameRPCMethod, aclRoleReq3, &aclRoleResp3)
|
||||
require.NoError(t, err)
|
||||
require.True(t, aclRoleResp3.ACLRole.Equals(aclRoles[0]))
|
||||
|
||||
aclRoleReq4 := &structs.ACLRoleByNameRequest{
|
||||
RoleName: aclRoles[1].Name,
|
||||
QueryOptions: structs.QueryOptions{
|
||||
Region: DefaultRegion,
|
||||
AuthToken: aclRootToken.SecretID,
|
||||
},
|
||||
}
|
||||
var aclRoleResp4 structs.ACLRoleByNameResponse
|
||||
err = msgpackrpc.CallWithCodec(codec, structs.ACLGetRoleByNameRPCMethod, aclRoleReq4, &aclRoleResp4)
|
||||
require.NoError(t, err)
|
||||
require.True(t, aclRoleResp4.ACLRole.Equals(aclRoles[1]))
|
||||
}
|
||||
|
||||
@@ -120,13 +120,6 @@ func (s *StateStore) upsertACLRoleTxn(
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// ValidateACLRolePolicyLinks ensures all ACL policies linked to from the ACL
|
||||
// role exist within state.
|
||||
func (s *StateStore) ValidateACLRolePolicyLinks(role *structs.ACLRole) error {
|
||||
txn := s.db.ReadTxn()
|
||||
return s.validateACLRolePolicyLinksTxn(txn, role)
|
||||
}
|
||||
|
||||
// validateACLRolePolicyLinksTxn is the same as ValidateACLRolePolicyLinks but
|
||||
// allows callers to pass their own transaction.
|
||||
func (s *StateStore) validateACLRolePolicyLinksTxn(txn *txn, role *structs.ACLRole) error {
|
||||
@@ -228,3 +221,17 @@ func (s *StateStore) GetACLRoleByName(ws memdb.WatchSet, roleName string) (*stru
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// GetACLRoleByIDPrefix is used to lookup ACL policies using a prefix to match
|
||||
// on the ID.
|
||||
func (s *StateStore) GetACLRoleByIDPrefix(ws memdb.WatchSet, idPrefix string) (memdb.ResultIterator, error) {
|
||||
txn := s.db.ReadTxn()
|
||||
|
||||
iter, err := txn.Get(TableACLRoles, indexID+"_prefix", idPrefix)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ACL role lookup failed: %v", err)
|
||||
}
|
||||
ws.Add(iter.WatchCh())
|
||||
|
||||
return iter, nil
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/hashicorp/go-memdb"
|
||||
"github.com/hashicorp/nomad/ci"
|
||||
"github.com/hashicorp/nomad/helper/pointer"
|
||||
"github.com/hashicorp/nomad/helper/uuid"
|
||||
"github.com/hashicorp/nomad/nomad/mock"
|
||||
"github.com/hashicorp/nomad/nomad/structs"
|
||||
"github.com/shoenig/test/must"
|
||||
@@ -443,3 +444,47 @@ func TestStateStore_GetACLRoleByName(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, mockedACLRoles[1], aclRole)
|
||||
}
|
||||
|
||||
func TestStateStore_GetACLRoleByIDPrefix(t *testing.T) {
|
||||
ci.Parallel(t)
|
||||
testState := testStateStore(t)
|
||||
|
||||
// Create the policies our ACL roles wants to link to.
|
||||
policy1 := mock.ACLPolicy()
|
||||
policy1.Name = "mocked-test-policy-1"
|
||||
policy2 := mock.ACLPolicy()
|
||||
policy2.Name = "mocked-test-policy-2"
|
||||
|
||||
require.NoError(t, testState.UpsertACLPolicies(
|
||||
structs.MsgTypeTestSetup, 10, []*structs.ACLPolicy{policy1, policy2}))
|
||||
|
||||
// Generate a some mocked ACL roles for testing and upsert these straight
|
||||
// into state. Set the ID to something with a prefix we know so it is easy
|
||||
// to test.
|
||||
mockedACLRoles := []*structs.ACLRole{mock.ACLRole(), mock.ACLRole()}
|
||||
mockedACLRoles[0].ID = "test-prefix-" + uuid.Generate()
|
||||
mockedACLRoles[1].ID = "test-prefix-" + uuid.Generate()
|
||||
require.NoError(t, testState.UpsertACLRoles(structs.MsgTypeTestSetup, 10, mockedACLRoles))
|
||||
|
||||
ws := memdb.NewWatchSet()
|
||||
|
||||
// Try using a prefix that doesn't match any entries.
|
||||
iter, err := testState.GetACLRoleByIDPrefix(ws, "nope")
|
||||
require.NoError(t, err)
|
||||
|
||||
var aclRoles []*structs.ACLRole
|
||||
for raw := iter.Next(); raw != nil; raw = iter.Next() {
|
||||
aclRoles = append(aclRoles, raw.(*structs.ACLRole))
|
||||
}
|
||||
require.Len(t, aclRoles, 0)
|
||||
|
||||
// Use a prefix which should match two entries in state.
|
||||
iter, err = testState.GetACLRoleByIDPrefix(ws, "test-prefix-")
|
||||
require.NoError(t, err)
|
||||
|
||||
aclRoles = []*structs.ACLRole{}
|
||||
for raw := iter.Next(); raw != nil; raw = iter.Next() {
|
||||
aclRoles = append(aclRoles, raw.(*structs.ACLRole))
|
||||
}
|
||||
require.Len(t, aclRoles, 2)
|
||||
}
|
||||
|
||||
@@ -35,6 +35,40 @@ const (
|
||||
// Args: ACLTokenDeleteRequest
|
||||
// Reply: GenericResponse
|
||||
ACLDeleteTokensRPCMethod = "ACL.DeleteTokens"
|
||||
|
||||
// ACLUpsertRolesRPCMethod is the RPC method for batch creating or
|
||||
// modifying ACL roles.
|
||||
//
|
||||
// Args: ACLRolesUpsertRequest
|
||||
// Reply: ACLRolesUpsertResponse
|
||||
ACLUpsertRolesRPCMethod = "ACL.UpsertRoles"
|
||||
|
||||
// ACLDeleteRolesByIDRPCMethod the RPC method for batch deleting ACL
|
||||
// roles by their ID.
|
||||
//
|
||||
// Args: ACLRolesDeleteByIDRequest
|
||||
// Reply: ACLRolesDeleteByIDResponse
|
||||
ACLDeleteRolesByIDRPCMethod = "ACL.DeleteRolesByID"
|
||||
|
||||
// ACLListRolesRPCMethod is the RPC method for listing ACL roles.
|
||||
//
|
||||
// Args: ACLRolesListRequest
|
||||
// Reply: ACLRolesListResponse
|
||||
ACLListRolesRPCMethod = "ACL.ListRoles"
|
||||
|
||||
// ACLGetRoleByIDRPCMethod is the RPC method for detailing an individual
|
||||
// ACL role using its ID.
|
||||
//
|
||||
// Args: ACLRoleByIDRequest
|
||||
// Reply: ACLRoleByIDResponse
|
||||
ACLGetRoleByIDRPCMethod = "ACL.GetRoleByID"
|
||||
|
||||
// ACLGetRoleByNameRPCMethod is the RPC method for detailing an individual
|
||||
// ACL role using its name.
|
||||
//
|
||||
// Args: ACLRoleByNameRequest
|
||||
// Reply: ACLRoleByNameResponse
|
||||
ACLGetRoleByNameRPCMethod = "ACL.GetRoleByName"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -266,6 +300,15 @@ func (a *ACLRole) Validate() error {
|
||||
return mErr.ErrorOrNil()
|
||||
}
|
||||
|
||||
// Canonicalize performs basic canonicalization on the ACL role object. It is
|
||||
// important for callers to understand certain fields such as ID are set if it
|
||||
// is empty, so copies should be taken if needed before calling this function.
|
||||
func (a *ACLRole) Canonicalize() {
|
||||
if a.ID == "" {
|
||||
a.ID = uuid.Generate()
|
||||
}
|
||||
}
|
||||
|
||||
// Equals performs an equality check on the two service registrations. It
|
||||
// handles nil objects.
|
||||
func (a *ACLRole) Equals(o *ACLRole) bool {
|
||||
@@ -307,6 +350,7 @@ type ACLRolesUpsertRequest struct {
|
||||
// ACLRolesUpsertResponse is the response object when one or more ACL roles
|
||||
// have been successfully upserted into state.
|
||||
type ACLRolesUpsertResponse struct {
|
||||
ACLRoles []*ACLRole
|
||||
WriteMeta
|
||||
}
|
||||
|
||||
@@ -322,3 +366,43 @@ type ACLRolesDeleteByIDRequest struct {
|
||||
type ACLRolesDeleteByIDResponse struct {
|
||||
WriteMeta
|
||||
}
|
||||
|
||||
// ACLRolesListRequest is the request object when performing ACL role listings.
|
||||
type ACLRolesListRequest struct {
|
||||
QueryOptions
|
||||
}
|
||||
|
||||
// ACLRolesListResponse is the response object when performing ACL role
|
||||
// listings.
|
||||
type ACLRolesListResponse struct {
|
||||
ACLRoles []*ACLRole
|
||||
QueryMeta
|
||||
}
|
||||
|
||||
// ACLRoleByIDRequest is the request object to perform a lookup of an ACL
|
||||
// role using a specific ID.
|
||||
type ACLRoleByIDRequest struct {
|
||||
RoleID string
|
||||
QueryOptions
|
||||
}
|
||||
|
||||
// ACLRoleByIDResponse is the response object when performing a lookup of an
|
||||
// ACL role matching a specific ID.
|
||||
type ACLRoleByIDResponse struct {
|
||||
ACLRole *ACLRole
|
||||
QueryMeta
|
||||
}
|
||||
|
||||
// ACLRoleByNameRequest is the request object to perform a lookup of an ACL
|
||||
// role using a specific name.
|
||||
type ACLRoleByNameRequest struct {
|
||||
RoleName string
|
||||
QueryOptions
|
||||
}
|
||||
|
||||
// ACLRoleByNameResponse is the response object when performing a lookup of an
|
||||
// ACL role matching a specific name.
|
||||
type ACLRoleByNameResponse struct {
|
||||
ACLRole *ACLRole
|
||||
QueryMeta
|
||||
}
|
||||
|
||||
@@ -418,6 +418,34 @@ func TestACLRole_Validate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestACLRole_Canonicalize(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
inputACLRole *ACLRole
|
||||
}{
|
||||
{
|
||||
name: "no ID set",
|
||||
inputACLRole: &ACLRole{},
|
||||
},
|
||||
{
|
||||
name: "id set",
|
||||
inputACLRole: &ACLRole{ID: "some-random-uuid"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
existing := tc.inputACLRole.Copy()
|
||||
tc.inputACLRole.Canonicalize()
|
||||
if existing.ID == "" {
|
||||
require.NotEmpty(t, tc.inputACLRole.ID)
|
||||
} else {
|
||||
require.Equal(t, existing.ID, tc.inputACLRole.ID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestACLRole_Equals(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
@@ -638,3 +666,18 @@ func Test_ACLRolesDeleteByIDRequest(t *testing.T) {
|
||||
req := ACLRolesDeleteByIDRequest{}
|
||||
require.False(t, req.IsRead())
|
||||
}
|
||||
|
||||
func Test_ACLRolesListRequest(t *testing.T) {
|
||||
req := ACLRolesListRequest{}
|
||||
require.True(t, req.IsRead())
|
||||
}
|
||||
|
||||
func Test_ACLRoleByIDRequest(t *testing.T) {
|
||||
req := ACLRoleByIDRequest{}
|
||||
require.True(t, req.IsRead())
|
||||
}
|
||||
|
||||
func Test_ACLRoleByNameRequest(t *testing.T) {
|
||||
req := ACLRoleByNameRequest{}
|
||||
require.True(t, req.IsRead())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user