Skip to content

Commit

Permalink
Merge pull request #12442 from hashicorp/f-sd-add-mixed-auth-read-end…
Browse files Browse the repository at this point in the history
…points

service-disco: add mixed auth to list and read RPC endpoints.
  • Loading branch information
schmichael authored Apr 4, 2022
2 parents f718c13 + e839640 commit 5de999d
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 14 deletions.
60 changes: 46 additions & 14 deletions nomad/service_registration_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,14 +164,9 @@ func (s *ServiceRegistration) List(
return s.listAllServiceRegistrations(args, reply)
}

// If ACLs are enabled, ensure the caller has the read-job namespace
// capability.
if aclObj, err := s.srv.ResolveToken(args.AuthToken); err != nil {
// Perform our mixed auth handling.
if err := s.handleMixedAuthEndpoint(args.QueryOptions, acl.NamespaceCapabilityReadJob); err != nil {
return err
} else if aclObj != nil {
if !aclObj.AllowNsOp(args.RequestNamespace(), acl.NamespaceCapabilityReadJob) {
return structs.ErrPermissionDenied
}
}

// Set up and return the blocking query.
Expand Down Expand Up @@ -377,14 +372,9 @@ func (s *ServiceRegistration) GetService(
}
defer metrics.MeasureSince([]string{"nomad", "service_registration", "get_service"}, time.Now())

// If ACLs are enabled, ensure the caller has the read-job namespace
// capability.
if aclObj, err := s.srv.ResolveToken(args.AuthToken); err != nil {
// Perform our mixed auth handling.
if err := s.handleMixedAuthEndpoint(args.QueryOptions, acl.NamespaceCapabilityReadJob); err != nil {
return err
} else if aclObj != nil {
if !aclObj.AllowNsOp(args.RequestNamespace(), acl.NamespaceCapabilityReadJob) {
return structs.ErrPermissionDenied
}
}

// Set up the blocking query.
Expand Down Expand Up @@ -415,3 +405,45 @@ func (s *ServiceRegistration) GetService(
},
})
}

// handleMixedAuthEndpoint is a helper to handle auth on RPC endpoints that can
// either be called by Nomad nodes, or by external clients.
func (s *ServiceRegistration) handleMixedAuthEndpoint(args structs.QueryOptions, cap string) error {

// Perform the initial token resolution.
aclObj, err := s.srv.ResolveToken(args.AuthToken)

switch err {
case nil:
// Perform our ACL validation. If the object is nil, this means ACLs
// are not enabled, otherwise trigger the allowed namespace function.
if aclObj != nil {
if !aclObj.AllowNsOp(args.RequestNamespace(), cap) {
return structs.ErrPermissionDenied
}
}
default:
// In the event we got any error other than notfound, consider this
// terminal.
if err != structs.ErrTokenNotFound {
return err
}

// Attempt to lookup AuthToken as a Node.SecretID and return any error
// wrapped along with the original.
node, stateErr := s.srv.fsm.State().NodeBySecretID(nil, args.AuthToken)
if stateErr != nil {
var mErr multierror.Error
mErr.Errors = append(mErr.Errors, err, stateErr)
return mErr.ErrorOrNil()
}

// At this point, we do not have a valid ACL token, nor are we being
// called, or able to confirm via the state store, by a node.
if node == nil {
return structs.ErrTokenNotFound
}
}

return nil
}
110 changes: 110 additions & 0 deletions nomad/service_registration_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,64 @@ func TestServiceRegistration_List(t *testing.T) {
},
name: "ACLs enabled with read namespace policy token",
},
{
serverFn: func(t *testing.T) (*Server, *structs.ACLToken, func()) {
return TestACLServer(t, nil)
},
testFn: func(t *testing.T, s *Server, token *structs.ACLToken) {
codec := rpcClient(t, s)
testutil.WaitForLeader(t, s.RPC)

// Create a namespace as this is needed when using an ACL like
// we do in this test.
ns := &structs.Namespace{
Name: "platform",
Description: "test namespace",
CreateIndex: 5,
ModifyIndex: 5,
}
ns.SetHash()
require.NoError(t, s.State().UpsertNamespaces(5, []*structs.Namespace{ns}))

// Generate a node.
node := mock.Node()
require.NoError(t, s.State().UpsertNode(structs.MsgTypeTestSetup, 10, node))

ws := memdb.NewWatchSet()
node, err := s.State().NodeByID(ws, node.ID)
require.NoError(t, err)
require.NotNil(t, node)

// Generate and upsert some service registrations.
services := mock.ServiceRegistrations()
require.NoError(t, s.State().UpsertServiceRegistrations(structs.MsgTypeTestSetup, 20, services))

// Test a request while setting the auth token to the node
// secret ID.
serviceRegReq := &structs.ServiceRegistrationListRequest{
QueryOptions: structs.QueryOptions{
Namespace: "platform",
Region: DefaultRegion,
AuthToken: node.SecretID,
},
}
var serviceRegResp structs.ServiceRegistrationListResponse
err = msgpackrpc.CallWithCodec(
codec, structs.ServiceRegistrationListRPCMethod, serviceRegReq, &serviceRegResp)
require.NoError(t, err)
require.ElementsMatch(t, []*structs.ServiceRegistrationListStub{
{
Namespace: "platform",
Services: []*structs.ServiceRegistrationStub{
{
ServiceName: "countdash-api",
Tags: []string{"bar"},
},
}},
}, serviceRegResp.Services)
},
name: "ACLs enabled with node secret toekn",
},
}

for _, tc := range testCases {
Expand Down Expand Up @@ -963,6 +1021,58 @@ func TestServiceRegistration_GetService(t *testing.T) {
},
name: "ACLs enabled",
},
{
serverFn: func(t *testing.T) (*Server, *structs.ACLToken, func()) {
return TestACLServer(t, nil)
},
testFn: func(t *testing.T, s *Server, token *structs.ACLToken) {
codec := rpcClient(t, s)
testutil.WaitForLeader(t, s.RPC)

// Generate mock services then upsert them individually using different indexes.
services := mock.ServiceRegistrations()

require.NoError(t, s.fsm.State().UpsertServiceRegistrations(
structs.MsgTypeTestSetup, 10, []*structs.ServiceRegistration{services[0]}))

require.NoError(t, s.fsm.State().UpsertServiceRegistrations(
structs.MsgTypeTestSetup, 20, []*structs.ServiceRegistration{services[1]}))

// Generate a node.
node := mock.Node()
require.NoError(t, s.State().UpsertNode(structs.MsgTypeTestSetup, 30, node))

ws := memdb.NewWatchSet()
node, err := s.State().NodeByID(ws, node.ID)
require.NoError(t, err)
require.NotNil(t, node)

// Test a request while setting the auth token to the node
// secret ID.
serviceRegReq := &structs.ServiceRegistrationListRequest{
QueryOptions: structs.QueryOptions{
Namespace: "platform",
Region: DefaultRegion,
AuthToken: node.SecretID,
},
}
var serviceRegResp structs.ServiceRegistrationListResponse
err = msgpackrpc.CallWithCodec(
codec, structs.ServiceRegistrationListRPCMethod, serviceRegReq, &serviceRegResp)
require.NoError(t, err)
require.ElementsMatch(t, []*structs.ServiceRegistrationListStub{
{
Namespace: "platform",
Services: []*structs.ServiceRegistrationStub{
{
ServiceName: "countdash-api",
Tags: []string{"bar"},
},
}},
}, serviceRegResp.Services)
},
name: "ACLs enabled using node secret",
},
}

for _, tc := range testCases {
Expand Down

0 comments on commit 5de999d

Please sign in to comment.