// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package acl import ( "context" "fmt" "testing" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "github.com/hashicorp/go-hclog" "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/consul/auth" "github.com/hashicorp/consul/agent/grpc-external/testutils" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/proto-public/pbacl" ) func TestServer_Logout_Success(t *testing.T) { secretID := generateID(t) tokenWriter := NewMockTokenWriter(t) tokenWriter.On("Delete", secretID, true).Return(nil) server := NewServer(Config{ ACLsEnabled: true, InPrimaryDatacenter: true, ForwardRPC: noopForwardRPC, LocalTokensEnabled: noopLocalTokensEnabled, Logger: hclog.NewNullLogger(), NewTokenWriter: func() TokenWriter { return tokenWriter }, }) _, err := server.Logout(context.Background(), &pbacl.LogoutRequest{ Token: secretID, }) require.NoError(t, err) } func TestServer_Logout_EmptyToken(t *testing.T) { server := NewServer(Config{ ACLsEnabled: true, Logger: hclog.NewNullLogger(), }) _, err := server.Logout(context.Background(), &pbacl.LogoutRequest{ Token: "", }) require.Error(t, err) require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String()) require.Contains(t, err.Error(), "token is required") } func TestServer_Logout_ACLsDisabled(t *testing.T) { server := NewServer(Config{ ACLsEnabled: false, Logger: hclog.NewNullLogger(), ValidateEnterpriseRequest: noopValidateEnterpriseRequest, ForwardRPC: noopForwardRPC, LocalTokensEnabled: noopLocalTokensEnabled, }) _, err := server.Logout(context.Background(), &pbacl.LogoutRequest{ Token: generateID(t), }) require.Error(t, err) require.Equal(t, codes.FailedPrecondition.String(), status.Code(err).String()) } func TestServer_Logout_LocalTokensDisabled(t *testing.T) { server := NewServer(Config{ ACLsEnabled: true, Logger: hclog.NewNullLogger(), ForwardRPC: noopForwardRPC, LocalTokensEnabled: func() bool { return false }, }) _, err := server.Logout(context.Background(), &pbacl.LogoutRequest{ Token: generateID(t), }) require.Error(t, err) require.Equal(t, codes.FailedPrecondition.String(), status.Code(err).String()) require.Contains(t, err.Error(), "token replication is required") } func TestServer_Logout_NoSuchToken(t *testing.T) { tokenWriter := NewMockTokenWriter(t) tokenWriter.On("Delete", mock.Anything, true).Return(acl.ErrNotFound) server := NewServer(Config{ ACLsEnabled: true, Logger: hclog.NewNullLogger(), ForwardRPC: noopForwardRPC, LocalTokensEnabled: noopLocalTokensEnabled, NewTokenWriter: func() TokenWriter { return tokenWriter }, }) _, err := server.Logout(context.Background(), &pbacl.LogoutRequest{ Token: generateID(t), }) require.NoError(t, err) } func TestServer_Logout_PermissionDenied(t *testing.T) { tokenWriter := NewMockTokenWriter(t) tokenWriter.On("Delete", mock.Anything, true).Return(acl.ErrPermissionDenied) server := NewServer(Config{ ACLsEnabled: true, InPrimaryDatacenter: true, ForwardRPC: noopForwardRPC, LocalTokensEnabled: noopLocalTokensEnabled, Logger: hclog.NewNullLogger(), NewTokenWriter: func() TokenWriter { return tokenWriter }, }) _, err := server.Logout(context.Background(), &pbacl.LogoutRequest{ Token: generateID(t), }) require.Error(t, err) require.Equal(t, codes.PermissionDenied.String(), status.Code(err).String()) } func TestServer_Logout_RPCForwarding(t *testing.T) { tokenWriter := NewMockTokenWriter(t) tokenWriter.On("Delete", mock.Anything, true).Return(nil) dc1 := NewServer(Config{ ACLsEnabled: true, Logger: hclog.NewNullLogger(), NewTokenWriter: func() TokenWriter { return tokenWriter }, ForwardRPC: noopForwardRPC, LocalTokensEnabled: func() bool { return true }, }) dc1Conn, err := grpc.Dial( testutils.RunTestServer(t, dc1).String(), //nolint:staticcheck grpc.WithInsecure(), ) require.NoError(t, err) dc2 := NewServer(Config{ ACLsEnabled: true, Logger: hclog.NewNullLogger(), ForwardRPC: func(rpcInfo structs.RPCInfo, fn func(*grpc.ClientConn) error) (bool, error) { return true, fn(dc1Conn) }, }) _, err = dc2.Logout(context.Background(), &pbacl.LogoutRequest{ Token: generateID(t), }) require.NoError(t, err) } func TestServer_Logout_GlobalWritesForwardedToPrimaryDC(t *testing.T) { tokenWriter := NewMockTokenWriter(t) tokenWriter.On("Delete", mock.Anything, true).Return(nil) // This test checks that requests to delete global tokens are forwared to the // primary datacenter by: // // 1. Setting up 2 servers (1 in the primary DC, 1 in the secondary). // 2. Making a logout request to the secondary DC. // 3. Mocking TokenWriter.Delete to return ErrCannotWriteGlobalToken in the // secondary DC. // 4. Checking that the primary DC server's TokenWriter receives a call to // Delete. // 5. Capturing the forwarded request's Datacenter in the primary DC server's // ForwardRPC (to check that we overwrote the user-supplied Datacenter // field to prevent infinite forwarding loops!) var forwardedRequestDatacenter string primary := NewServer(Config{ ACLsEnabled: true, InPrimaryDatacenter: true, LocalTokensEnabled: noopLocalTokensEnabled, Logger: hclog.NewNullLogger(), NewTokenWriter: func() TokenWriter { return tokenWriter }, ForwardRPC: func(info structs.RPCInfo, _ func(*grpc.ClientConn) error) (bool, error) { forwardedRequestDatacenter = info.RequestDatacenter() return false, nil }, }) primaryConn, err := grpc.Dial( testutils.RunTestServer(t, primary).String(), //nolint:staticcheck grpc.WithInsecure(), ) require.NoError(t, err) secondary := NewServer(Config{ ACLsEnabled: true, InPrimaryDatacenter: false, LocalTokensEnabled: noopLocalTokensEnabled, Logger: hclog.NewNullLogger(), PrimaryDatacenter: "primary", ForwardRPC: func(info structs.RPCInfo, fn func(*grpc.ClientConn) error) (bool, error) { dc := info.RequestDatacenter() switch dc { case "secondary": return false, nil case "primary": return true, fn(primaryConn) default: return false, fmt.Errorf("unexpected target datacenter: %s", dc) } }, NewTokenWriter: func() TokenWriter { tokenWriter := NewMockTokenWriter(t) tokenWriter.On("Delete", mock.Anything, true).Return(auth.ErrCannotWriteGlobalToken) return tokenWriter }, }) _, err = secondary.Logout(context.Background(), &pbacl.LogoutRequest{ Token: generateID(t), Datacenter: "secondary", }) require.NoError(t, err) require.Equal(t, "primary", forwardedRequestDatacenter) }