github.com/pachyderm/pachyderm@v1.13.4/src/server/auth/cmds/cmds_test.go (about) 1 package cmds 2 3 // Tests to add: 4 // basic login test 5 // login with no auth service deployed 6 7 import ( 8 "bytes" 9 "encoding/base64" 10 "fmt" 11 "io/ioutil" 12 "os" 13 "strings" 14 "sync" 15 "testing" 16 "time" 17 18 "github.com/pachyderm/pachyderm/src/client/pkg/errors" 19 "github.com/pachyderm/pachyderm/src/client/pkg/require" 20 "github.com/pachyderm/pachyderm/src/server/pkg/backoff" 21 tu "github.com/pachyderm/pachyderm/src/server/pkg/testutil" 22 ) 23 24 var activateMut sync.Mutex 25 26 // activateEnterprise checks if Pachyderm Enterprise is active and if not, 27 // activates it 28 func activateEnterprise(t *testing.T) { 29 cmd := tu.Cmd("pachctl", "enterprise", "get-state") 30 out, err := cmd.Output() 31 require.NoError(t, err) 32 if !strings.Contains(string(out), "ACTIVE") { 33 // Enterprise not active in the cluster. Activate it. 34 cmd := tu.Cmd("pachctl", "enterprise", "activate") 35 cmd.Stdin = strings.NewReader(fmt.Sprintf("%s\n", tu.GetTestEnterpriseCode(t))) 36 require.NoError(t, cmd.Run()) 37 } 38 } 39 40 func activateAuth(t *testing.T) { 41 t.Helper() 42 activateMut.Lock() 43 defer activateMut.Unlock() 44 activateEnterprise(t) 45 // TODO(msteffen): Make sure client & server have the same version 46 // Logout (to clear any expired tokens) and activate Pachyderm auth 47 require.NoError(t, tu.Cmd("pachctl", "auth", "logout").Run()) 48 cmd := tu.Cmd("pachctl", "auth", "activate") 49 cmd.Stdin = strings.NewReader("admin\n") 50 require.NoError(t, cmd.Run()) 51 } 52 53 func deactivateAuth(t *testing.T) { 54 t.Helper() 55 activateMut.Lock() 56 defer activateMut.Unlock() 57 58 // Check if Pachyderm Auth is active -- if so, deactivate it 59 if err := tu.BashCmd("echo admin | pachctl auth login").Run(); err == nil { 60 require.NoError(t, tu.BashCmd("yes | pachctl auth deactivate").Run()) 61 } 62 63 // Wait for auth to finish deactivating 64 time.Sleep(time.Second) 65 backoff.Retry(func() error { 66 cmd := tu.Cmd("pachctl", "auth", "login") 67 cmd.Stdin = strings.NewReader("admin\n") 68 cmd.Stdout, cmd.Stderr = ioutil.Discard, ioutil.Discard 69 if cmd.Run() != nil { 70 return nil // cmd errored -- auth is deactivated 71 } 72 return errors.New("auth not deactivated yet") 73 }, backoff.RetryEvery(time.Second)) 74 } 75 76 func TestAuthBasic(t *testing.T) { 77 if testing.Short() { 78 t.Skip("Skipping integration tests in short mode") 79 } 80 activateAuth(t) 81 defer deactivateAuth(t) 82 require.NoError(t, tu.BashCmd(` 83 echo "{{.alice}}" | pachctl auth login 84 pachctl create repo {{.repo}} 85 pachctl list repo \ 86 | match {{.repo}} 87 pachctl inspect repo {{.repo}} 88 `, 89 "alice", tu.UniqueString("alice"), 90 "repo", tu.UniqueString("TestAuthBasic-repo"), 91 ).Run()) 92 } 93 94 func TestWhoAmI(t *testing.T) { 95 if testing.Short() { 96 t.Skip("Skipping integration tests in short mode") 97 } 98 activateAuth(t) 99 defer deactivateAuth(t) 100 require.NoError(t, tu.BashCmd(` 101 echo "{{.alice}}" | pachctl auth login 102 pachctl auth whoami | match {{.alice}} 103 `, 104 "alice", tu.UniqueString("alice"), 105 ).Run()) 106 } 107 108 func TestCheckGetSet(t *testing.T) { 109 if testing.Short() { 110 t.Skip("Skipping integration tests in short mode") 111 } 112 activateAuth(t) 113 defer deactivateAuth(t) 114 // Test both forms of the 'pachctl auth get' command, as well as 'pachctl auth check' 115 require.NoError(t, tu.BashCmd(` 116 echo "{{.alice}}" | pachctl auth login 117 pachctl create repo {{.repo}} 118 pachctl auth check owner {{.repo}} 119 pachctl auth get {{.repo}} \ 120 | match {{.alice}} 121 pachctl auth get {{.bob}} {{.repo}} \ 122 | match NONE 123 `, 124 "alice", tu.UniqueString("alice"), 125 "bob", tu.UniqueString("bob"), 126 "repo", tu.UniqueString("TestGet-repo"), 127 ).Run()) 128 129 // Test 'pachctl auth set' 130 require.NoError(t, tu.BashCmd(` 131 echo "{{.alice}}" | pachctl auth login 132 pachctl create repo {{.repo}} 133 pachctl auth set {{.bob}} reader {{.repo}} 134 pachctl auth get {{.bob}} {{.repo}} \ 135 | match READER 136 `, 137 "alice", tu.UniqueString("alice"), 138 "bob", tu.UniqueString("bob"), 139 "repo", tu.UniqueString("TestGet-repo"), 140 ).Run()) 141 } 142 143 func TestAdmins(t *testing.T) { 144 if testing.Short() { 145 t.Skip("Skipping integration tests in short mode") 146 } 147 activateAuth(t) 148 defer deactivateAuth(t) 149 150 // Modify the list of admins to replace 'admin' with 'admin2' 151 require.NoError(t, tu.BashCmd("echo admin | pachctl auth login").Run()) 152 require.NoError(t, tu.BashCmd(` 153 pachctl auth list-admins \ 154 | match "admin" 155 pachctl auth modify-admins --add admin2 156 pachctl auth list-admins \ 157 | match "admin2" 158 pachctl auth modify-admins --remove admin 159 160 # as 'admin' is a substr of 'admin2', use '^admin$' regex... 161 pachctl auth list-admins \ 162 | match -v "^github:admin$" \ 163 | match "^github:admin2$" 164 `).Run()) 165 166 // Now 'admin2' is the only admin. Login as admin2, and swap 'admin' back in 167 // (so that deactivateAuth() runs), and call 'list-admin' (to make sure it 168 // works for non-admins) 169 require.NoError(t, tu.BashCmd("echo admin2 | pachctl auth login").Run()) 170 require.NoError(t, tu.BashCmd(` 171 pachctl auth modify-admins --add admin --remove admin2 172 `).Run()) 173 require.NoError(t, backoff.Retry(func() error { 174 return tu.BashCmd(`pachctl auth list-admins \ 175 | match -v "admin2" \ 176 | match "admin" 177 `).Run() 178 }, backoff.NewTestingBackOff())) 179 180 } 181 182 func TestGetAndUseAuthToken(t *testing.T) { 183 if testing.Short() { 184 t.Skip("Skipping integration tests in short mode") 185 } 186 activateAuth(t) 187 defer deactivateAuth(t) 188 189 // Test both get-auth-token and use-auth-token; make sure that they work 190 // together with -q 191 require.NoError(t, tu.BashCmd("echo admin | pachctl auth login").Run()) 192 require.NoError(t, tu.BashCmd(` 193 pachctl auth get-auth-token -q robot:marvin \ 194 | pachctl auth use-auth-token 195 pachctl auth whoami \ 196 | match 'robot:marvin' 197 `).Run()) 198 } 199 200 func TestActivateAsRobotUser(t *testing.T) { 201 if testing.Short() { 202 t.Skip("Skipping integration tests in short mode") 203 } 204 // We need a custom 'activate' command, so reproduce 'activateAuth' minus the 205 // actual call 206 defer deactivateAuth(t) // unwind "activate" command before deactivating 207 activateMut.Lock() 208 defer activateMut.Unlock() 209 activateEnterprise(t) 210 // Logout (to clear any expired tokens) and activate Pachyderm auth 211 require.NoError(t, tu.BashCmd(` 212 pachctl auth logout 213 pachctl auth activate --initial-admin=robot:hal9000 214 pachctl auth whoami \ 215 | match 'robot:hal9000' 216 `).Run()) 217 218 // Make "admin" a cluster admins, so that deactivateAuth works 219 require.NoError(t, 220 tu.Cmd("pachctl", "auth", "modify-admins", "--add=admin").Run()) 221 } 222 223 func TestActivateMismatchedUsernames(t *testing.T) { 224 if testing.Short() { 225 t.Skip("Skipping integration tests in short mode") 226 } 227 // We need a custom 'activate' command, to reproduce 'activateAuth' minus the 228 // actual call 229 activateMut.Lock() 230 defer activateMut.Unlock() 231 activateEnterprise(t) 232 // Logout (to clear any expired tokens) and activate Pachyderm auth 233 activate := tu.BashCmd(` 234 pachctl auth logout 235 echo alice | pachctl auth activate --initial-admin=bob 236 `) 237 var errorMsg bytes.Buffer 238 activate.Stderr = &errorMsg 239 require.YesError(t, activate.Run()) 240 require.Matches(t, "github:alice", errorMsg.String()) 241 require.Matches(t, "github:bob", errorMsg.String()) 242 } 243 244 func TestConfig(t *testing.T) { 245 if os.Getenv("RUN_BAD_TESTS") == "" { 246 t.Skip("Skipping because RUN_BAD_TESTS was empty") 247 } 248 if testing.Short() { 249 t.Skip("Skipping integration tests in short mode") 250 } 251 activateAuth(t) 252 defer deactivateAuth(t) 253 254 idpMetadata := base64.StdEncoding.EncodeToString([]byte(`<EntityDescriptor 255 xmlns="urn:oasis:names:tc:SAML:2.0:metadata" 256 validUntil="` + time.Now().Format(time.RFC3339) + `" 257 entityID="metadata"> 258 <SPSSODescriptor 259 xmlns="urn:oasis:names:tc:SAML:2.0:metadata" 260 validUntil="` + time.Now().Format(time.RFC3339) + `" 261 protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol" 262 AuthnRequestsSigned="false" 263 WantAssertionsSigned="true"> 264 <AssertionConsumerService 265 Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" 266 Location="acs" 267 index="1"> 268 </AssertionConsumerService> 269 </SPSSODescriptor> 270 </EntityDescriptor>`)) 271 require.NoError(t, tu.BashCmd(` 272 echo "admin" | pachctl auth login 273 pachctl auth set-config <<EOF 274 { 275 "live_config_version": 1, 276 "id_providers": [{ 277 "name": "github", 278 "description": "oauth-based authentication with github.com", 279 "github":{} 280 }, 281 { 282 "name": "idp", 283 "description": "fake ID provider for testing", 284 "saml": { 285 "metadata_xml": "`+idpMetadata+`" 286 } 287 }], 288 "saml_svc_options": { 289 "acs_url": "http://www.example.com", 290 "metadata_url": "http://www.example.com" 291 } 292 } 293 EOF 294 pachctl auth get-config \ 295 | match '"live_config_version": 2,' \ 296 | match '"saml_svc_options": {' \ 297 | match '"acs_url": "http://www.example.com",' \ 298 | match '"metadata_url": "http://www.example.com"' \ 299 | match '}' 300 `).Run()) 301 } 302 303 // TestGetAuthTokenNoSubject tests that 'pachctl get-auth-token' infers the 304 // subject from the currently logged-in user if none is specified on the command 305 // line 306 func TestGetAuthTokenNoSubject(t *testing.T) { 307 if testing.Short() { 308 t.Skip("Skipping integration tests in short mode") 309 } 310 activateAuth(t) 311 defer deactivateAuth(t) 312 require.NoError(t, tu.BashCmd(` 313 echo "{{.alice}}" | pachctl auth login 314 pachctl auth get-auth-token -q | pachctl auth use-auth-token 315 pachctl auth whoami | match {{.alice}} 316 `, 317 "alice", tu.UniqueString("alice"), 318 ).Run()) 319 } 320 321 // TestGetAuthTokenTTL tests that the --ttl argument to 'pachctl get-auth-token' 322 // correctly limits the lifetime of the returned token 323 func TestGetAuthTokenTTL(t *testing.T) { 324 if testing.Short() { 325 t.Skip("Skipping integration tests in short mode") 326 } 327 activateAuth(t) 328 defer deactivateAuth(t) 329 330 alice := tu.UniqueString("alice") 331 require.NoError(t, tu.BashCmd(`echo "{{.alice}}" | pachctl auth login `, 332 "alice", alice, 333 ).Run()) 334 335 var tokenBuf bytes.Buffer 336 tokenCmd := tu.BashCmd(`pachctl auth get-auth-token --ttl=5s -q`) 337 tokenCmd.Stdout = &tokenBuf 338 require.NoError(t, tokenCmd.Run()) 339 token := strings.TrimSpace(tokenBuf.String()) 340 341 time.Sleep(6 * time.Second) 342 var errMsg bytes.Buffer 343 login := tu.BashCmd(` 344 echo {{.token}} | pachctl auth use-auth-token 345 pachctl auth whoami 346 `, "token", token) 347 login.Stderr = &errMsg 348 require.YesError(t, login.Run()) 349 require.Matches(t, "try logging in", errMsg.String()) 350 } 351 352 // TestGetOneTimePasswordNoSubject tests that 'pachctl get-otp' infers the 353 // subject from the currently logged-in user if none is specified on the command 354 // line 355 func TestGetOneTimePasswordNoSubject(t *testing.T) { 356 if testing.Short() { 357 t.Skip("Skipping integration tests in short mode") 358 } 359 activateAuth(t) 360 defer deactivateAuth(t) 361 require.NoError(t, tu.BashCmd(` 362 echo "{{.alice}}" | pachctl auth login 363 otp="$(pachctl auth get-otp)" 364 echo "${otp}" | pachctl auth login --one-time-password 365 pachctl auth whoami | match {{.alice}} 366 `, 367 "alice", tu.UniqueString("alice"), 368 ).Run()) 369 } 370 371 // TestGetOneTimePasswordTTL tests that the --ttl argument to 'pachctl get-otp' 372 // correctly limits the lifetime of the returned token 373 func TestGetOneTimePasswordTTL(t *testing.T) { 374 if testing.Short() { 375 t.Skip("Skipping integration tests in short mode") 376 } 377 activateAuth(t) 378 defer deactivateAuth(t) 379 380 alice := tu.UniqueString("alice") 381 require.NoError(t, tu.BashCmd(`echo "{{.alice}}" | pachctl auth login`, 382 "alice", alice, 383 ).Run()) 384 385 var otpBuf bytes.Buffer 386 otpCmd := tu.BashCmd(`pachctl auth get-otp --ttl=5s`) 387 otpCmd.Stdout = &otpBuf 388 require.NoError(t, otpCmd.Run()) 389 otp := strings.TrimSpace(otpBuf.String()) 390 391 // wait for OTP to expire 392 time.Sleep(6 * time.Second) 393 var errMsg bytes.Buffer 394 login := tu.BashCmd(` 395 echo {{.otp}} | pachctl auth login --one-time-password 396 `, "otp", otp) 397 login.Stderr = &errMsg 398 require.YesError(t, login.Run()) 399 require.Matches(t, "otp is invalid or has expired", errMsg.String()) 400 } 401 402 func TestYAMLConfig(t *testing.T) { 403 if testing.Short() { 404 t.Skip("Skipping integration tests in short mode") 405 } 406 activateAuth(t) 407 defer deactivateAuth(t) 408 409 require.NoError(t, tu.BashCmd(` 410 echo "admin" | pachctl auth login 411 pachctl auth get-config -o yaml \ 412 | match 'live_config_version: 1' \ 413 | match 'id_providers:' \ 414 | match 'name: GitHub' \ 415 | match 'description: oauth-based authentication with github.com' \ 416 | match 'github: {}' 417 `).Run()) 418 } 419 420 func TestMain(m *testing.M) { 421 // Preemptively deactivate Pachyderm auth (to avoid errors in early tests) 422 if err := tu.BashCmd("echo 'admin' | pachctl auth login &>/dev/null").Run(); err == nil { 423 if err := tu.BashCmd("yes | pachctl auth deactivate").Run(); err != nil { 424 panic(err.Error()) 425 } 426 } 427 time.Sleep(time.Second) 428 backoff.Retry(func() error { 429 cmd := tu.Cmd("pachctl", "auth", "login") 430 cmd.Stdin = strings.NewReader("admin\n") 431 cmd.Stdout, cmd.Stderr = ioutil.Discard, ioutil.Discard 432 if cmd.Run() != nil { 433 return nil // cmd errored -- auth is deactivated 434 } 435 return errors.New("auth not deactivated yet") 436 }, backoff.RetryEvery(time.Second)) 437 os.Exit(m.Run()) 438 }