gopkg.in/hashicorp/nomad.v0@v0.11.8/nomad/consul_test.go (about)

     1  package nomad
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"sync"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/hashicorp/nomad/command/agent/consul"
    11  	"github.com/hashicorp/nomad/helper"
    12  	"github.com/hashicorp/nomad/helper/testlog"
    13  	"github.com/hashicorp/nomad/helper/uuid"
    14  	"github.com/hashicorp/nomad/nomad/structs"
    15  	"github.com/stretchr/testify/require"
    16  	"golang.org/x/time/rate"
    17  )
    18  
    19  var _ ConsulACLsAPI = (*consulACLsAPI)(nil)
    20  var _ ConsulACLsAPI = (*mockConsulACLsAPI)(nil)
    21  
    22  type revokeRequest struct {
    23  	accessorID string
    24  	committed  bool
    25  }
    26  
    27  type mockConsulACLsAPI struct {
    28  	lock           sync.Mutex
    29  	revokeRequests []revokeRequest
    30  	stopped        bool
    31  }
    32  
    33  func (m *mockConsulACLsAPI) CheckSIPolicy(_ context.Context, _, _ string) error {
    34  	panic("not implemented yet")
    35  }
    36  
    37  func (m *mockConsulACLsAPI) CreateToken(_ context.Context, _ ServiceIdentityRequest) (*structs.SIToken, error) {
    38  	panic("not implemented yet")
    39  }
    40  
    41  func (m *mockConsulACLsAPI) ListTokens() ([]string, error) {
    42  	panic("not implemented yet")
    43  }
    44  
    45  func (m *mockConsulACLsAPI) Stop() {
    46  	m.lock.Lock()
    47  	defer m.lock.Unlock()
    48  	m.stopped = true
    49  }
    50  
    51  type mockPurgingServer struct {
    52  	purgedAccessorIDs []string
    53  	failure           error
    54  }
    55  
    56  func (mps *mockPurgingServer) purgeFunc(accessors []*structs.SITokenAccessor) error {
    57  	if mps.failure != nil {
    58  		return mps.failure
    59  	}
    60  
    61  	for _, accessor := range accessors {
    62  		mps.purgedAccessorIDs = append(mps.purgedAccessorIDs, accessor.AccessorID)
    63  	}
    64  	return nil
    65  }
    66  
    67  func (m *mockConsulACLsAPI) RevokeTokens(_ context.Context, accessors []*structs.SITokenAccessor, committed bool) bool {
    68  	return m.storeForRevocation(accessors, committed)
    69  }
    70  
    71  func (m *mockConsulACLsAPI) MarkForRevocation(accessors []*structs.SITokenAccessor) {
    72  	m.storeForRevocation(accessors, true)
    73  }
    74  
    75  func (m *mockConsulACLsAPI) storeForRevocation(accessors []*structs.SITokenAccessor, committed bool) bool {
    76  	m.lock.Lock()
    77  	defer m.lock.Unlock()
    78  
    79  	for _, accessor := range accessors {
    80  		m.revokeRequests = append(m.revokeRequests, revokeRequest{
    81  			accessorID: accessor.AccessorID,
    82  			committed:  committed,
    83  		})
    84  	}
    85  	return false
    86  }
    87  
    88  func TestConsulACLsAPI_CreateToken(t *testing.T) {
    89  	t.Parallel()
    90  
    91  	try := func(t *testing.T, expErr error) {
    92  		logger := testlog.HCLogger(t)
    93  		aclAPI := consul.NewMockACLsAPI(logger)
    94  		aclAPI.SetError(expErr)
    95  
    96  		c := NewConsulACLsAPI(aclAPI, logger, nil)
    97  
    98  		ctx := context.Background()
    99  		sii := ServiceIdentityRequest{
   100  			AllocID:   uuid.Generate(),
   101  			ClusterID: uuid.Generate(),
   102  			TaskName:  "my-task1-sidecar-proxy",
   103  			TaskKind:  structs.NewTaskKind(structs.ConnectProxyPrefix, "my-service"),
   104  		}
   105  
   106  		token, err := c.CreateToken(ctx, sii)
   107  
   108  		if expErr != nil {
   109  			require.Equal(t, expErr, err)
   110  			require.Nil(t, token)
   111  		} else {
   112  			require.NoError(t, err)
   113  			require.Equal(t, "my-task1-sidecar-proxy", token.TaskName)
   114  			require.True(t, helper.IsUUID(token.AccessorID))
   115  			require.True(t, helper.IsUUID(token.SecretID))
   116  		}
   117  	}
   118  
   119  	t.Run("create token success", func(t *testing.T) {
   120  		try(t, nil)
   121  	})
   122  
   123  	t.Run("create token error", func(t *testing.T) {
   124  		try(t, errors.New("consul broke"))
   125  	})
   126  }
   127  
   128  func TestConsulACLsAPI_RevokeTokens(t *testing.T) {
   129  	t.Parallel()
   130  
   131  	setup := func(t *testing.T, exp error) (context.Context, ConsulACLsAPI, *structs.SIToken) {
   132  		logger := testlog.HCLogger(t)
   133  		aclAPI := consul.NewMockACLsAPI(logger)
   134  
   135  		c := NewConsulACLsAPI(aclAPI, logger, nil)
   136  
   137  		ctx := context.Background()
   138  		generated, err := c.CreateToken(ctx, ServiceIdentityRequest{
   139  			ClusterID: uuid.Generate(),
   140  			AllocID:   uuid.Generate(),
   141  			TaskName:  "task1-sidecar-proxy",
   142  			TaskKind:  structs.NewTaskKind(structs.ConnectProxyPrefix, "service1"),
   143  		})
   144  		require.NoError(t, err)
   145  
   146  		// set the mock error after calling CreateToken for setting up
   147  		aclAPI.SetError(exp)
   148  
   149  		return context.Background(), c, generated
   150  	}
   151  
   152  	accessors := func(ids ...string) (result []*structs.SITokenAccessor) {
   153  		for _, id := range ids {
   154  			result = append(result, &structs.SITokenAccessor{AccessorID: id})
   155  		}
   156  		return
   157  	}
   158  
   159  	t.Run("revoke token success", func(t *testing.T) {
   160  		ctx, c, token := setup(t, nil)
   161  		retryLater := c.RevokeTokens(ctx, accessors(token.AccessorID), false)
   162  		require.False(t, retryLater)
   163  	})
   164  
   165  	t.Run("revoke token non-existent", func(t *testing.T) {
   166  		ctx, c, _ := setup(t, nil)
   167  		retryLater := c.RevokeTokens(ctx, accessors(uuid.Generate()), false)
   168  		require.False(t, retryLater)
   169  	})
   170  
   171  	t.Run("revoke token error", func(t *testing.T) {
   172  		exp := errors.New("consul broke")
   173  		ctx, c, token := setup(t, exp)
   174  		retryLater := c.RevokeTokens(ctx, accessors(token.AccessorID), false)
   175  		require.True(t, retryLater)
   176  	})
   177  }
   178  
   179  func TestConsulACLsAPI_MarkForRevocation(t *testing.T) {
   180  	t.Parallel()
   181  
   182  	logger := testlog.HCLogger(t)
   183  	aclAPI := consul.NewMockACLsAPI(logger)
   184  
   185  	c := NewConsulACLsAPI(aclAPI, logger, nil)
   186  
   187  	generated, err := c.CreateToken(context.Background(), ServiceIdentityRequest{
   188  		ClusterID: uuid.Generate(),
   189  		AllocID:   uuid.Generate(),
   190  		TaskName:  "task1-sidecar-proxy",
   191  		TaskKind:  structs.NewTaskKind(structs.ConnectProxyPrefix, "service1"),
   192  	})
   193  	require.NoError(t, err)
   194  
   195  	// set the mock error after calling CreateToken for setting up
   196  	aclAPI.SetError(nil)
   197  
   198  	accessors := []*structs.SITokenAccessor{{AccessorID: generated.AccessorID}}
   199  	c.MarkForRevocation(accessors)
   200  	require.Len(t, c.bgRetryRevocation, 1)
   201  	require.Contains(t, c.bgRetryRevocation, accessors[0])
   202  }
   203  
   204  func TestConsulACLsAPI_bgRetryRevoke(t *testing.T) {
   205  	t.Parallel()
   206  
   207  	// manually create so the bg daemon does not run, letting us explicitly
   208  	// call and test bgRetryRevoke
   209  	setup := func(t *testing.T) (*consulACLsAPI, *mockPurgingServer) {
   210  		logger := testlog.HCLogger(t)
   211  		aclAPI := consul.NewMockACLsAPI(logger)
   212  		server := new(mockPurgingServer)
   213  		shortWait := rate.Limit(1 * time.Millisecond)
   214  
   215  		return &consulACLsAPI{
   216  			aclClient: aclAPI,
   217  			purgeFunc: server.purgeFunc,
   218  			limiter:   rate.NewLimiter(shortWait, int(shortWait)),
   219  			stopC:     make(chan struct{}),
   220  			logger:    logger,
   221  		}, server
   222  	}
   223  
   224  	t.Run("retry revoke no items", func(t *testing.T) {
   225  		c, server := setup(t)
   226  		c.bgRetryRevoke()
   227  		require.Empty(t, server)
   228  	})
   229  
   230  	t.Run("retry revoke success", func(t *testing.T) {
   231  		c, server := setup(t)
   232  		accessorID := uuid.Generate()
   233  		c.bgRetryRevocation = append(c.bgRetryRevocation, &structs.SITokenAccessor{
   234  			NodeID:     uuid.Generate(),
   235  			AllocID:    uuid.Generate(),
   236  			AccessorID: accessorID,
   237  			TaskName:   "task1",
   238  		})
   239  		require.Empty(t, server.purgedAccessorIDs)
   240  		c.bgRetryRevoke()
   241  		require.Equal(t, 1, len(server.purgedAccessorIDs))
   242  		require.Equal(t, accessorID, server.purgedAccessorIDs[0])
   243  		require.Empty(t, c.bgRetryRevocation) // should be empty now
   244  	})
   245  
   246  	t.Run("retry revoke failure", func(t *testing.T) {
   247  		c, server := setup(t)
   248  		server.failure = errors.New("revocation fail")
   249  		accessorID := uuid.Generate()
   250  		c.bgRetryRevocation = append(c.bgRetryRevocation, &structs.SITokenAccessor{
   251  			NodeID:     uuid.Generate(),
   252  			AllocID:    uuid.Generate(),
   253  			AccessorID: accessorID,
   254  			TaskName:   "task1",
   255  		})
   256  		require.Empty(t, server.purgedAccessorIDs)
   257  		c.bgRetryRevoke()
   258  		require.Equal(t, 1, len(c.bgRetryRevocation)) // non-empty because purge failed
   259  		require.Equal(t, accessorID, c.bgRetryRevocation[0].AccessorID)
   260  	})
   261  }
   262  
   263  func TestConsulACLsAPI_Stop(t *testing.T) {
   264  	t.Parallel()
   265  
   266  	setup := func(t *testing.T) *consulACLsAPI {
   267  		logger := testlog.HCLogger(t)
   268  		return NewConsulACLsAPI(nil, logger, nil)
   269  	}
   270  
   271  	c := setup(t)
   272  	c.Stop()
   273  	_, err := c.CreateToken(context.Background(), ServiceIdentityRequest{
   274  		ClusterID: "",
   275  		AllocID:   "",
   276  		TaskName:  "",
   277  	})
   278  	require.Error(t, err)
   279  }
   280  
   281  func TestConsulACLsAPI_CheckSIPolicy(t *testing.T) {
   282  	t.Parallel()
   283  
   284  	try := func(t *testing.T, service, token string, expErr string) {
   285  		logger := testlog.HCLogger(t)
   286  		aclAPI := consul.NewMockACLsAPI(logger)
   287  		cAPI := NewConsulACLsAPI(aclAPI, logger, nil)
   288  
   289  		err := cAPI.CheckSIPolicy(context.Background(), service, token)
   290  		if expErr != "" {
   291  			require.EqualError(t, err, expErr)
   292  		} else {
   293  			require.NoError(t, err)
   294  		}
   295  	}
   296  
   297  	t.Run("operator has service write", func(t *testing.T) {
   298  		try(t, "service1", consul.ExampleOperatorTokenID1, "")
   299  	})
   300  
   301  	t.Run("operator has service_prefix write", func(t *testing.T) {
   302  		try(t, "foo-service1", consul.ExampleOperatorTokenID2, "")
   303  	})
   304  
   305  	t.Run("operator permissions insufficient", func(t *testing.T) {
   306  		try(t, "service1", consul.ExampleOperatorTokenID3,
   307  			"permission denied for \"service1\"",
   308  		)
   309  	})
   310  
   311  	t.Run("no token provided", func(t *testing.T) {
   312  		try(t, "service1", "", "missing consul token")
   313  	})
   314  
   315  	t.Run("nonsense token provided", func(t *testing.T) {
   316  		try(t, "service1", "f1682bde-1e71-90b1-9204-85d35467ba61",
   317  			"unable to validate operator consul token: no such token",
   318  		)
   319  	})
   320  }