diff --git a/nomad/operator_endpoint.go b/nomad/operator_endpoint.go index 576962aa3..a45339c07 100644 --- a/nomad/operator_endpoint.go +++ b/nomad/operator_endpoint.go @@ -20,6 +20,13 @@ func (op *Operator) RaftGetConfiguration(args *structs.GenericRequest, reply *st return err } + // Check management permissions + if aclObj, err := op.srv.ResolveToken(args.SecretID); err != nil { + return err + } else if aclObj != nil && !aclObj.IsManagement() { + return structs.ErrPermissionDenied + } + // We can't fetch the leader and the configuration atomically with // the current Raft API. future := op.srv.raft.GetConfiguration() @@ -69,6 +76,13 @@ func (op *Operator) RaftRemovePeerByAddress(args *structs.RaftPeerByAddressReque return err } + // Check management permissions + if aclObj, err := op.srv.ResolveToken(args.SecretID); err != nil { + return err + } else if aclObj != nil && !aclObj.IsManagement() { + return structs.ErrPermissionDenied + } + // Since this is an operation designed for humans to use, we will return // an error if the supplied address isn't among the peers since it's // likely they screwed up. diff --git a/nomad/operator_endpoint_test.go b/nomad/operator_endpoint_test.go index db1ba9397..a72c75f90 100644 --- a/nomad/operator_endpoint_test.go +++ b/nomad/operator_endpoint_test.go @@ -7,9 +7,12 @@ import ( "testing" "github.com/hashicorp/net-rpc-msgpackrpc" + "github.com/hashicorp/nomad/acl" + "github.com/hashicorp/nomad/nomad/mock" "github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/testutil" "github.com/hashicorp/raft" + "github.com/stretchr/testify/assert" ) func TestOperator_RaftGetConfiguration(t *testing.T) { @@ -54,6 +57,68 @@ func TestOperator_RaftGetConfiguration(t *testing.T) { } } +func TestOperator_RaftGetConfiguration_ACL(t *testing.T) { + t.Parallel() + s1, root := testACLServer(t, nil) + defer s1.Shutdown() + codec := rpcClient(t, s1) + testutil.WaitForLeader(t, s1.RPC) + assert := assert.New(t) + state := s1.fsm.State() + + // Create ACL token + invalidToken := mock.CreatePolicyAndToken(t, state, 1001, "test-invalid", mock.NodePolicy(acl.PolicyWrite)) + + arg := structs.GenericRequest{ + QueryOptions: structs.QueryOptions{ + Region: s1.config.Region, + }, + } + + // Try with no token and expect permission denied + { + var reply structs.RaftConfigurationResponse + err := msgpackrpc.CallWithCodec(codec, "Operator.RaftGetConfiguration", &arg, &reply) + assert.NotNil(err) + assert.Equal(err.Error(), structs.ErrPermissionDenied.Error()) + } + + // Try with an invalid token and expect permission denied + { + arg.SecretID = invalidToken.SecretID + var reply structs.RaftConfigurationResponse + err := msgpackrpc.CallWithCodec(codec, "Operator.RaftGetConfiguration", &arg, &reply) + assert.NotNil(err) + assert.Equal(err.Error(), structs.ErrPermissionDenied.Error()) + } + + // Use management token + { + arg.SecretID = root.SecretID + var reply structs.RaftConfigurationResponse + assert.Nil(msgpackrpc.CallWithCodec(codec, "Operator.RaftGetConfiguration", &arg, &reply)) + + future := s1.raft.GetConfiguration() + assert.Nil(future.Error()) + assert.Len(future.Configuration().Servers, 1) + + me := future.Configuration().Servers[0] + expected := structs.RaftConfigurationResponse{ + Servers: []*structs.RaftServer{ + { + ID: me.ID, + Node: fmt.Sprintf("%v.%v", s1.config.NodeName, s1.config.Region), + Address: me.Address, + Leader: true, + Voter: true, + }, + }, + Index: future.Index(), + } + assert.Equal(expected, reply) + } +} + func TestOperator_RaftRemovePeerByAddress(t *testing.T) { t.Parallel() s1 := testServer(t, nil) @@ -109,3 +174,51 @@ func TestOperator_RaftRemovePeerByAddress(t *testing.T) { } } } + +func TestOperator_RaftRemovePeerByAddress_ACL(t *testing.T) { + t.Parallel() + s1, root := testACLServer(t, nil) + defer s1.Shutdown() + codec := rpcClient(t, s1) + testutil.WaitForLeader(t, s1.RPC) + assert := assert.New(t) + state := s1.fsm.State() + + // Create ACL token + invalidToken := mock.CreatePolicyAndToken(t, state, 1001, "test-invalid", mock.NodePolicy(acl.PolicyWrite)) + + arg := structs.RaftPeerByAddressRequest{ + Address: raft.ServerAddress(fmt.Sprintf("127.0.0.1:%d", getPort())), + } + arg.Region = s1.config.Region + + // Add peer manually to Raft. + { + future := s1.raft.AddPeer(arg.Address) + assert.Nil(future.Error()) + } + + var reply struct{} + + // Try with no token and expect permission denied + { + err := msgpackrpc.CallWithCodec(codec, "Operator.RaftRemovePeerByAddress", &arg, &reply) + assert.NotNil(err) + assert.Equal(err.Error(), structs.ErrPermissionDenied.Error()) + } + + // Try with an invalid token and expect permission denied + { + arg.SecretID = invalidToken.SecretID + err := msgpackrpc.CallWithCodec(codec, "Operator.RaftRemovePeerByAddress", &arg, &reply) + assert.NotNil(err) + assert.Equal(err.Error(), structs.ErrPermissionDenied.Error()) + } + + // Try with a management token + { + arg.SecretID = root.SecretID + err := msgpackrpc.CallWithCodec(codec, "Operator.RaftRemovePeerByAddress", &arg, &reply) + assert.Nil(err) + } +} diff --git a/website/source/api/operator.html.md b/website/source/api/operator.html.md index 7676e064e..22aed2584 100644 --- a/website/source/api/operator.html.md +++ b/website/source/api/operator.html.md @@ -34,7 +34,7 @@ The table below shows this endpoint's support for | Blocking Queries | ACL Required | | ---------------- | ------------ | -| `NO` | `none` | +| `NO` | `management` | ### Parameters @@ -104,7 +104,7 @@ The table below shows this endpoint's support for | Blocking Queries | ACL Required | | ---------------- | ------------ | -| `NO` | `none` | +| `NO` | `management` | ### Parameters