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 }