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  }