github.com/hernad/nomad@v1.6.112/e2e/connect/acls.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package connect
     5  
     6  import (
     7  	"os"
     8  	"regexp"
     9  	"testing"
    10  	"time"
    11  
    12  	consulapi "github.com/hashicorp/consul/api"
    13  	uuidparse "github.com/hashicorp/go-uuid"
    14  	nomadapi "github.com/hernad/nomad/api"
    15  	"github.com/hernad/nomad/e2e/e2eutil"
    16  	"github.com/hernad/nomad/e2e/framework"
    17  	"github.com/hernad/nomad/helper/uuid"
    18  	"github.com/hernad/nomad/jobspec"
    19  	"github.com/stretchr/testify/require"
    20  )
    21  
    22  type ConnectACLsE2ETest struct {
    23  	framework.TC
    24  
    25  	// used to store the root token so we can reset the client back to
    26  	// it as needed
    27  	consulManagementToken string
    28  
    29  	// things to cleanup after each test case
    30  	jobIDs          []string
    31  	consulPolicyIDs []string
    32  	consulTokenIDs  []string
    33  }
    34  
    35  func (tc *ConnectACLsE2ETest) BeforeAll(f *framework.F) {
    36  	// Wait for Nomad to be ready before doing anything.
    37  	e2eutil.WaitForLeader(f.T(), tc.Nomad())
    38  	e2eutil.WaitForNodesReady(f.T(), tc.Nomad(), 2)
    39  
    40  	// Validate the consul root token exists, otherwise tests are just
    41  	// going to be a train wreck.
    42  	tc.consulManagementToken = os.Getenv(envConsulToken)
    43  
    44  	_, err := uuidparse.ParseUUID(tc.consulManagementToken)
    45  	f.NoError(err, "CONSUL_HTTP_TOKEN not set")
    46  
    47  	// ensure SI tokens from previous test cases were removed
    48  	f.Eventually(func() bool {
    49  		siTokens := tc.countSITokens(f.T())
    50  		f.T().Log("cleanup: checking for remaining SI tokens:", siTokens)
    51  		return len(siTokens) == 0
    52  	}, 2*time.Minute, 2*time.Second, "SI tokens did not get removed")
    53  }
    54  
    55  // AfterEach does cleanup of Consul ACL objects that were created during each
    56  // test case. Each test case may assume it is starting from a "fresh" state -
    57  // as if the consul ACL bootstrap process had just taken place.
    58  func (tc *ConnectACLsE2ETest) AfterEach(f *framework.F) {
    59  	if os.Getenv("NOMAD_TEST_SKIPCLEANUP") == "1" {
    60  		return
    61  	}
    62  
    63  	t := f.T()
    64  
    65  	// cleanup jobs
    66  	for _, id := range tc.jobIDs {
    67  		t.Log("cleanup: deregister nomad job id:", id)
    68  		_, _, err := tc.Nomad().Jobs().Deregister(id, true, nil)
    69  		f.NoError(err)
    70  	}
    71  
    72  	// cleanup consul tokens
    73  	for _, id := range tc.consulTokenIDs {
    74  		t.Log("cleanup: delete consul token id:", id)
    75  		_, err := tc.Consul().ACL().TokenDelete(id, &consulapi.WriteOptions{Token: tc.consulManagementToken})
    76  		f.NoError(err)
    77  	}
    78  
    79  	// cleanup consul policies
    80  	for _, id := range tc.consulPolicyIDs {
    81  		t.Log("cleanup: delete consul policy id:", id)
    82  		_, err := tc.Consul().ACL().PolicyDelete(id, &consulapi.WriteOptions{Token: tc.consulManagementToken})
    83  		f.NoError(err)
    84  	}
    85  
    86  	// do garbage collection
    87  	err := tc.Nomad().System().GarbageCollect()
    88  	f.NoError(err)
    89  
    90  	// assert there are no leftover SI tokens, which may take a minute to be
    91  	// cleaned up
    92  	f.Eventually(func() bool {
    93  		siTokens := tc.countSITokens(t)
    94  		t.Log("cleanup: checking for remaining SI tokens:", siTokens)
    95  		return len(siTokens) == 0
    96  	}, 2*time.Minute, 2*time.Second, "SI tokens did not get removed")
    97  
    98  	tc.jobIDs = []string{}
    99  	tc.consulTokenIDs = []string{}
   100  	tc.consulPolicyIDs = []string{}
   101  }
   102  
   103  // todo(shoenig): follow up refactor with e2eutil.ConsulPolicy
   104  type consulPolicy struct {
   105  	Name  string // e.g. nomad-operator
   106  	Rules string // e.g. service "" { policy="write" }
   107  }
   108  
   109  // todo(shoenig): follow up refactor with e2eutil.ConsulPolicy
   110  func (tc *ConnectACLsE2ETest) createConsulPolicy(p consulPolicy, f *framework.F) string {
   111  	result, _, err := tc.Consul().ACL().PolicyCreate(&consulapi.ACLPolicy{
   112  		Name:        p.Name,
   113  		Description: "test policy " + p.Name,
   114  		Rules:       p.Rules,
   115  	}, &consulapi.WriteOptions{Token: tc.consulManagementToken})
   116  	f.NoError(err, "failed to create consul policy")
   117  	tc.consulPolicyIDs = append(tc.consulPolicyIDs, result.ID)
   118  	return result.ID
   119  }
   120  
   121  // todo(shoenig): follow up refactor with e2eutil.ConsulPolicy
   122  func (tc *ConnectACLsE2ETest) createOperatorToken(policyID string, f *framework.F) string {
   123  	token, _, err := tc.Consul().ACL().TokenCreate(&consulapi.ACLToken{
   124  		Description: "operator token",
   125  		Policies:    []*consulapi.ACLTokenPolicyLink{{ID: policyID}},
   126  	}, &consulapi.WriteOptions{Token: tc.consulManagementToken})
   127  	f.NoError(err, "failed to create operator token")
   128  	tc.consulTokenIDs = append(tc.consulTokenIDs, token.AccessorID)
   129  	return token.SecretID
   130  }
   131  
   132  func (tc *ConnectACLsE2ETest) TestConnectACLsRegisterMasterToken(f *framework.F) {
   133  	t := f.T()
   134  
   135  	t.Log("test register Connect job w/ ACLs enabled w/ master token")
   136  
   137  	jobID := "connect" + uuid.Generate()[0:8]
   138  	tc.jobIDs = append(tc.jobIDs, jobID)
   139  
   140  	jobAPI := tc.Nomad().Jobs()
   141  
   142  	job, err := jobspec.ParseFile(demoConnectJob)
   143  	f.NoError(err)
   144  
   145  	// Set the job file to use the consul master token.
   146  	// One should never do this in practice, but, it should work.
   147  	// https://www.consul.io/docs/acl/acl-system.html#builtin-tokens
   148  	job.ConsulToken = &tc.consulManagementToken
   149  	job.ID = &jobID
   150  
   151  	// Avoid using Register here, because that would actually create and run the
   152  	// Job which runs the task, creates the SI token, which all needs to be
   153  	// given time to settle and cleaned up. That is all covered in the big slow
   154  	// test at the bottom.
   155  	resp, _, err := jobAPI.Plan(job, false, nil)
   156  	f.NoError(err)
   157  	f.NotNil(resp)
   158  }
   159  
   160  func (tc *ConnectACLsE2ETest) TestConnectACLsRegisterMissingOperatorToken(f *framework.F) {
   161  	t := f.T()
   162  
   163  	t.Skip("we don't have consul.allow_unauthenticated=false set because it would required updating every E2E test to pass a Consul token")
   164  
   165  	t.Log("test register Connect job w/ ACLs enabled w/o operator token")
   166  
   167  	jobID := "connect" + uuid.Short()
   168  	tc.jobIDs = append(tc.jobIDs, jobID) // need to clean up if the test fails
   169  
   170  	job, err := jobspec.ParseFile(demoConnectJob)
   171  	f.NoError(err)
   172  	jobAPI := tc.Nomad().Jobs()
   173  
   174  	// Explicitly show the ConsulToken is not set
   175  	job.ConsulToken = nil
   176  	job.ID = &jobID
   177  
   178  	_, _, err = jobAPI.Register(job, nil)
   179  	f.Error(err)
   180  
   181  	t.Log("job correctly rejected, with error:", err)
   182  }
   183  
   184  func (tc *ConnectACLsE2ETest) TestConnectACLsRegisterFakeOperatorToken(f *framework.F) {
   185  	t := f.T()
   186  
   187  	t.Skip("we don't have consul.allow_unauthenticated=false set because it would required updating every E2E test to pass a Consul token")
   188  
   189  	t.Log("test register Connect job w/ ACLs enabled w/ operator token")
   190  
   191  	policyID := tc.createConsulPolicy(consulPolicy{
   192  		Name:  "nomad-operator-policy-" + uuid.Short(),
   193  		Rules: `service "count-api" { policy = "write" } service "count-dashboard" { policy = "write" }`,
   194  	}, f)
   195  	t.Log("created operator policy:", policyID)
   196  
   197  	// generate a fake consul token token
   198  	fakeToken := uuid.Generate()
   199  
   200  	jobID := "connect" + uuid.Short()
   201  	tc.jobIDs = append(tc.jobIDs, jobID) // need to clean up if the test fails
   202  
   203  	job := tc.parseJobSpecFile(t, demoConnectJob)
   204  
   205  	jobAPI := tc.Nomad().Jobs()
   206  
   207  	// deliberately set the fake Consul token
   208  	job.ConsulToken = &fakeToken
   209  	job.ID = &jobID
   210  
   211  	// should fail, because the token is fake
   212  	_, _, err := jobAPI.Register(job, nil)
   213  	f.Error(err)
   214  	t.Log("job correctly rejected, with error:", err)
   215  }
   216  
   217  func (tc *ConnectACLsE2ETest) TestConnectACLsConnectDemo(f *framework.F) {
   218  	t := f.T()
   219  
   220  	t.Log("test register Connect job w/ ACLs enabled w/ operator token")
   221  
   222  	// === Setup ACL policy and mint Operator token ===
   223  
   224  	// create a policy allowing writes of services "count-api" and "count-dashboard"
   225  	policyID := tc.createConsulPolicy(consulPolicy{
   226  		Name:  "nomad-operator-policy-" + uuid.Short(),
   227  		Rules: `service "count-api" { policy = "write" } service "count-dashboard" { policy = "write" }`,
   228  	}, f)
   229  	t.Log("created operator policy:", policyID)
   230  
   231  	// create a Consul "operator token" blessed with the above policy
   232  	operatorToken := tc.createOperatorToken(policyID, f)
   233  	t.Log("created operator token:", operatorToken)
   234  
   235  	jobID := connectJobID()
   236  	tc.jobIDs = append(tc.jobIDs, jobID)
   237  
   238  	allocs := e2eutil.RegisterAndWaitForAllocs(t, tc.Nomad(), demoConnectJob, jobID, operatorToken)
   239  	f.Equal(2, len(allocs), "expected 2 allocs for connect demo", allocs)
   240  	allocIDs := e2eutil.AllocIDsFromAllocationListStubs(allocs)
   241  	f.Equal(2, len(allocIDs), "expected 2 allocIDs for connect demo", allocIDs)
   242  	e2eutil.WaitForAllocsRunning(t, tc.Nomad(), allocIDs)
   243  
   244  	// === Check Consul SI tokens were generated for sidecars ===
   245  	foundSITokens := tc.countSITokens(t)
   246  	f.Equal(2, len(foundSITokens), "expected 2 SI tokens total: %v", foundSITokens)
   247  	f.Equal(1, foundSITokens["connect-proxy-count-api"], "expected 1 SI token for connect-proxy-count-api: %v", foundSITokens)
   248  	f.Equal(1, foundSITokens["connect-proxy-count-dashboard"], "expected 1 SI token for connect-proxy-count-dashboard: %v", foundSITokens)
   249  
   250  	t.Log("connect legacy job with ACLs enable finished")
   251  }
   252  
   253  func (tc *ConnectACLsE2ETest) TestConnectACLsConnectNativeDemo(f *framework.F) {
   254  	t := f.T()
   255  
   256  	t.Log("test register Connect job w/ ACLs enabled w/ operator token")
   257  
   258  	// === Setup ACL policy and mint Operator token ===
   259  
   260  	// create a policy allowing writes of services "uuid-fe" and "uuid-api"
   261  	policyID := tc.createConsulPolicy(consulPolicy{
   262  		Name:  "nomad-operator-policy-" + uuid.Short(),
   263  		Rules: `service "uuid-fe" { policy = "write" } service "uuid-api" { policy = "write" }`,
   264  	}, f)
   265  	t.Log("created operator policy:", policyID)
   266  
   267  	// create a Consul "operator token" blessed with the above policy
   268  	operatorToken := tc.createOperatorToken(policyID, f)
   269  	t.Log("created operator token:", operatorToken)
   270  
   271  	jobID := connectJobID()
   272  	tc.jobIDs = append(tc.jobIDs, jobID)
   273  
   274  	allocs := e2eutil.RegisterAndWaitForAllocs(t, tc.Nomad(), demoConnectNativeJob, jobID, operatorToken)
   275  	allocIDs := e2eutil.AllocIDsFromAllocationListStubs(allocs)
   276  	e2eutil.WaitForAllocsRunning(t, tc.Nomad(), allocIDs)
   277  
   278  	// === Check Consul SI tokens were generated for native tasks ===
   279  	foundSITokens := tc.countSITokens(t)
   280  	f.Equal(2, len(foundSITokens), "expected 2 SI tokens total: %v", foundSITokens)
   281  	f.Equal(1, foundSITokens["frontend"], "expected 1 SI token for frontend: %v", foundSITokens)
   282  	f.Equal(1, foundSITokens["generate"], "expected 1 SI token for generate: %v", foundSITokens)
   283  
   284  	t.Log("connect native job with ACLs enabled finished")
   285  }
   286  
   287  func (tc *ConnectACLsE2ETest) TestConnectACLsConnectIngressGatewayDemo(f *framework.F) {
   288  	t := f.T()
   289  
   290  	t.Log("test register Connect Ingress Gateway job w/ ACLs enabled")
   291  
   292  	// setup ACL policy and mint operator token
   293  
   294  	policyID := tc.createConsulPolicy(consulPolicy{
   295  		Name:  "nomad-operator-policy-" + uuid.Short(),
   296  		Rules: `service "my-ingress-service" { policy = "write" } service "uuid-api" { policy = "write" }`,
   297  	}, f)
   298  	operatorToken := tc.createOperatorToken(policyID, f)
   299  	t.Log("created operator token:", operatorToken)
   300  
   301  	jobID := connectJobID()
   302  	tc.jobIDs = append(tc.jobIDs, jobID)
   303  
   304  	allocs := e2eutil.RegisterAndWaitForAllocs(t, tc.Nomad(), demoConnectIngressGateway, jobID, operatorToken)
   305  	allocIDs := e2eutil.AllocIDsFromAllocationListStubs(allocs)
   306  	e2eutil.WaitForAllocsRunning(t, tc.Nomad(), allocIDs)
   307  
   308  	foundSITokens := tc.countSITokens(t)
   309  	f.Equal(2, len(foundSITokens), "expected 2 SI tokens total: %v", foundSITokens)
   310  	f.Equal(1, foundSITokens["connect-ingress-my-ingress-service"], "expected 1 SI token for connect-ingress-my-ingress-service: %v", foundSITokens)
   311  	f.Equal(1, foundSITokens["generate"], "expected 1 SI token for generate: %v", foundSITokens)
   312  
   313  	t.Log("connect ingress gateway job with ACLs enabled finished")
   314  }
   315  
   316  func (tc *ConnectACLsE2ETest) TestConnectACLsConnectTerminatingGatewayDemo(f *framework.F) {
   317  	t := f.T()
   318  
   319  	t.Log("test register Connect Terminating Gateway job w/ ACLs enabled")
   320  
   321  	// setup ACL policy and mint operator token
   322  
   323  	policyID := tc.createConsulPolicy(consulPolicy{
   324  		Name:  "nomad-operator-policy-" + uuid.Short(),
   325  		Rules: `service "api-gateway" { policy = "write" } service "count-dashboard" { policy = "write" }`,
   326  	}, f)
   327  	operatorToken := tc.createOperatorToken(policyID, f)
   328  	t.Log("created operator token:", operatorToken)
   329  
   330  	jobID := connectJobID()
   331  	tc.jobIDs = append(tc.jobIDs, jobID)
   332  
   333  	allocs := e2eutil.RegisterAndWaitForAllocs(t, tc.Nomad(), demoConnectTerminatingGateway, jobID, operatorToken)
   334  	allocIDs := e2eutil.AllocIDsFromAllocationListStubs(allocs)
   335  	e2eutil.WaitForAllocsRunning(t, tc.Nomad(), allocIDs)
   336  
   337  	foundSITokens := tc.countSITokens(t)
   338  	f.Equal(2, len(foundSITokens), "expected 2 SI tokens total: %v", foundSITokens)
   339  	f.Equal(1, foundSITokens["connect-terminating-api-gateway"], "expected 1 SI token for connect-terminating-api-gateway: %v", foundSITokens)
   340  	f.Equal(1, foundSITokens["connect-proxy-count-dashboard"], "expected 1 SI token for count-dashboard: %v", foundSITokens)
   341  
   342  	t.Log("connect terminating gateway job with ACLs enabled finished")
   343  }
   344  
   345  var (
   346  	siTokenRe = regexp.MustCompile(`_nomad_si \[[\w-]{36}] \[[\w-]{36}] \[([\S]+)]`)
   347  )
   348  
   349  func (tc *ConnectACLsE2ETest) serviceofSIToken(description string) string {
   350  	if m := siTokenRe.FindStringSubmatch(description); len(m) == 2 {
   351  		return m[1]
   352  	}
   353  	return ""
   354  }
   355  
   356  func (tc *ConnectACLsE2ETest) countSITokens(t *testing.T) map[string]int {
   357  	aclAPI := tc.Consul().ACL()
   358  	tokens, _, err := aclAPI.TokenList(&consulapi.QueryOptions{
   359  		Token: tc.consulManagementToken,
   360  	})
   361  	require.NoError(t, err)
   362  
   363  	// count the number of SI tokens matching each service name
   364  	foundSITokens := make(map[string]int)
   365  	for _, token := range tokens {
   366  		if service := tc.serviceofSIToken(token.Description); service != "" {
   367  			foundSITokens[service]++
   368  		}
   369  	}
   370  
   371  	return foundSITokens
   372  }
   373  
   374  func (tc *ConnectACLsE2ETest) parseJobSpecFile(t *testing.T, filename string) *nomadapi.Job {
   375  	job, err := jobspec.ParseFile(filename)
   376  	require.NoError(t, err)
   377  	return job
   378  }