github.com/grailbio/base@v0.0.11/cmd/grail-access/cmd_test.go (about)

     1  package main_test
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"net"
     9  	"net/http"
    10  	"os"
    11  	"os/exec"
    12  	"path"
    13  	"strings"
    14  	"testing"
    15  	"time"
    16  
    17  	ticketServerUtil "github.com/grailbio/base/cmd/ticket-server/testutil"
    18  	"github.com/grailbio/base/security/identity"
    19  	"github.com/grailbio/testutil"
    20  	_ "github.com/grailbio/v23/factories/grail"
    21  	"github.com/stretchr/testify/assert"
    22  	v23 "v.io/v23"
    23  	"v.io/v23/context"
    24  	"v.io/v23/naming"
    25  	"v.io/v23/rpc"
    26  	"v.io/v23/security"
    27  	"v.io/x/ref"
    28  	libsecurity "v.io/x/ref/lib/security"
    29  )
    30  
    31  func TestCmd(t *testing.T) {
    32  	ctx, v23CleanUp := v23.Init()
    33  	defer v23CleanUp()
    34  	assert.NoError(t, ref.EnvClearCredentials())
    35  	exe := testutil.GoExecutable(t, "//go/src/github.com/grailbio/base/cmd/grail-access/grail-access")
    36  
    37  	// Preserve the test environment's PATH. On Darwin, Vanadium's agentlib uses `ioreg` from the
    38  	// path in the process of locking [1] and loading [2] the principal when there's no agent.
    39  	// [1] https://github.com/vanadium/core/blob/694a147f5dfd7ebc2d2e5a4fb3c4fe448c7a377c/x/ref/services/agent/internal/lockutil/version1_darwin.go#L21
    40  	// [2] https://github.com/vanadium/core/blob/694a147f5dfd7ebc2d2e5a4fb3c4fe448c7a377c/x/ref/services/agent/agentlib/principal.go#L57
    41  	pathEnv := "PATH=" + os.Getenv("PATH")
    42  
    43  	t.Run("help", func(t *testing.T) {
    44  		cmd := exec.Command(exe, "-help")
    45  		cmd.Env = []string{pathEnv}
    46  		stdout, stderr := ticketServerUtil.RunAndCapture(t, cmd)
    47  		assert.NotEmpty(t, stdout)
    48  		assert.Empty(t, stderr)
    49  	})
    50  
    51  	// TODO(josh): Test with v23agentd on the path, too.
    52  	t.Run("dump_existing_principal", func(t *testing.T) {
    53  		homeDir, cleanUp := testutil.TempDir(t, "", "")
    54  		defer cleanUp()
    55  		principalDir := path.Join(homeDir, ".v23")
    56  		principal, err := libsecurity.CreatePersistentPrincipal(principalDir, nil)
    57  		assert.NoError(t, err)
    58  		decoyPrincipalDir := path.Join(homeDir, "decoy_principal_dir")
    59  		// Create a principal in the decoyPrincipalDir, as -dir still requires
    60  		// a valid principal at $V23_CREDENTIALS.
    61  		// TODO: Consider removing -dir flag, as this is surprising behavior.
    62  		_, err = libsecurity.CreatePersistentPrincipal(decoyPrincipalDir, nil)
    63  		assert.NoError(t, err)
    64  
    65  		const blessingName = "grail-access-test-blessing-ln7z94"
    66  		blessings, err := principal.BlessSelf(blessingName)
    67  		assert.NoError(t, err)
    68  		assert.NoError(t, principal.BlessingStore().SetDefault(blessings))
    69  
    70  		t.Run("flag_dir", func(t *testing.T) {
    71  			cmd := exec.Command(exe, "-dump", "-dir", principalDir)
    72  			// Set $V23_CREDENTIALS to test that -dir takes priority.
    73  			cmd.Env = []string{pathEnv, "V23_CREDENTIALS=" + decoyPrincipalDir}
    74  			stdout, stderr := ticketServerUtil.RunAndCapture(t, cmd)
    75  			assert.Contains(t, stdout, blessingName)
    76  			assert.Empty(t, stderr)
    77  		})
    78  
    79  		t.Run("env_home", func(t *testing.T) {
    80  			cmd := exec.Command(exe, "-dump")
    81  			cmd.Env = []string{pathEnv, "HOME=" + homeDir}
    82  			stdout, stderr := ticketServerUtil.RunAndCapture(t, cmd)
    83  			assert.Contains(t, stdout, blessingName)
    84  			assert.Empty(t, stderr)
    85  		})
    86  	})
    87  
    88  	t.Run("do_not_refresh/existing", func(t *testing.T) {
    89  		principalDir, cleanUp := testutil.TempDir(t, "", "")
    90  		defer cleanUp()
    91  		principal, err := libsecurity.CreatePersistentPrincipal(principalDir, nil)
    92  		assert.NoError(t, err)
    93  
    94  		const blessingName = "grail-access-test-blessing-nuz823"
    95  		doNotRefreshDuration := time.Hour
    96  		expirationTime := time.Now().Add(2 * doNotRefreshDuration)
    97  		expiryCaveat, err := security.NewExpiryCaveat(expirationTime)
    98  		assert.NoError(t, err)
    99  		blessings, err := principal.BlessSelf(blessingName, expiryCaveat)
   100  		assert.NoError(t, err)
   101  		assert.NoError(t, principal.BlessingStore().SetDefault(blessings))
   102  
   103  		cmd := exec.Command(exe,
   104  			"-dir", principalDir,
   105  			"-do-not-refresh-duration", doNotRefreshDuration.String())
   106  		cmd.Env = []string{pathEnv}
   107  		stdout, stderr := ticketServerUtil.RunAndCapture(t, cmd)
   108  		assert.Contains(t, stdout, blessingName)
   109  		assert.Empty(t, stderr)
   110  	})
   111  
   112  	t.Run("fake_v23_servers", func(t *testing.T) {
   113  
   114  		t.Run("ec2", func(t *testing.T) {
   115  			const (
   116  				wantDoc                 = "grailaccesstesttoken92lsl83"
   117  				serverBlessingName      = "grail-access-test-blessing-laul37"
   118  				clientBlessingExtension = "ec2-test"
   119  				wantClientBlessing      = serverBlessingName + ":" + clientBlessingExtension
   120  			)
   121  
   122  			// Run fake ticket server: accepts EC2 instance identity document, returns blessings.
   123  			var blesserEndpoint naming.Endpoint
   124  			ctx, blesserEndpoint = ticketServerUtil.RunBlesserServer(ctx, t,
   125  				identity.Ec2BlesserServer(fakeBlesser(
   126  					func(gotDoc string, recipient security.PublicKey) security.Blessings {
   127  						assert.Equal(t, wantDoc, gotDoc)
   128  						p := v23.GetPrincipal(ctx)
   129  						caveat, err := security.NewExpiryCaveat(time.Now().Add(24 * time.Hour))
   130  						assert.NoError(t, err)
   131  						localBlessings, err := p.BlessSelf(serverBlessingName)
   132  						assert.NoError(t, err)
   133  						b, err := p.Bless(recipient, localBlessings, clientBlessingExtension, caveat)
   134  						assert.NoError(t, err)
   135  						return b
   136  					}),
   137  				),
   138  			)
   139  
   140  			// Run fake EC2 instance identity server.
   141  			listener, err := net.Listen("tcp", "localhost:")
   142  			assert.NoError(t, err)
   143  			defer func() { assert.NoError(t, listener.Close()) }()
   144  			go http.Serve( // nolint: errcheck
   145  				listener,
   146  				http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
   147  					_, httpErr := w.Write([]byte(wantDoc))
   148  					assert.NoError(t, httpErr)
   149  				}),
   150  			)
   151  
   152  			// Run grail-access to create a principal and bless it with the EC2 flow.
   153  			principalDir, principalCleanUp := testutil.TempDir(t, "", "")
   154  			defer principalCleanUp()
   155  			cmd := exec.Command(exe,
   156  				"-dir", principalDir,
   157  				"-ec2",
   158  				"-blesser", fmt.Sprintf("/%s", blesserEndpoint.Address),
   159  				"-ec2-instance-identity-url", fmt.Sprintf("http://%s/", listener.Addr().String()))
   160  			cmd.Env = []string{pathEnv}
   161  			stdout, _ := ticketServerUtil.RunAndCapture(t, cmd)
   162  			assert.Contains(t, stdout, wantClientBlessing)
   163  
   164  			// Make sure we got the right blessing.
   165  			principal, err := libsecurity.LoadPersistentPrincipal(principalDir, nil)
   166  			assert.NoError(t, err)
   167  			defaultBlessing, _ := principal.BlessingStore().Default()
   168  			assert.Contains(t, defaultBlessing.String(), wantClientBlessing)
   169  		})
   170  
   171  		t.Run("google", func(t *testing.T) {
   172  			const (
   173  				wantToken               = "grailaccesstesttokensjo289d"
   174  				serverBlessingName      = "grail-access-test-blessing-s8j9dk"
   175  				clientBlessingExtension = "google-test"
   176  				wantClientBlessing      = serverBlessingName + ":" + clientBlessingExtension
   177  			)
   178  
   179  			// Run fake ticket server: accepts Google ID token, returns blessings.
   180  			var blesserEndpoint naming.Endpoint
   181  			ctx, blesserEndpoint = ticketServerUtil.RunBlesserServer(ctx, t,
   182  				identity.GoogleBlesserServer(fakeBlesser(
   183  					func(gotToken string, recipient security.PublicKey) security.Blessings {
   184  						assert.Equal(t, wantToken, gotToken)
   185  						p := v23.GetPrincipal(ctx)
   186  						caveat, err := security.NewExpiryCaveat(time.Now().Add(24 * time.Hour))
   187  						assert.NoError(t, err)
   188  						localBlessings, err := p.BlessSelf(serverBlessingName)
   189  						assert.NoError(t, err)
   190  						b, err := p.Bless(recipient, localBlessings, clientBlessingExtension, caveat)
   191  						assert.NoError(t, err)
   192  						return b
   193  					}),
   194  				),
   195  			)
   196  
   197  			// Run fake oauth server.
   198  			listener, err := net.Listen("tcp", "localhost:")
   199  			assert.NoError(t, err)
   200  			defer func() { assert.NoError(t, listener.Close()) }()
   201  			go http.Serve( // nolint: errcheck
   202  				listener,
   203  				http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
   204  					if req.URL.Path != "/token" {
   205  						assert.FailNowf(t, "fake oauth server: unexpected request: %s", req.URL.Path)
   206  					}
   207  					w.Header().Set("Content-Type", "application/json")
   208  					assert.NoError(t, json.NewEncoder(w).Encode(
   209  						map[string]interface{}{
   210  							"access_token": "testtoken",
   211  							"expires_in":   3600,
   212  							"id_token":     wantToken,
   213  							"scope":        "https://www.googleapis.com/auth/userinfo.email",
   214  						},
   215  					))
   216  				}),
   217  			)
   218  
   219  			// Run grail-access to create a principal and bless it with the EC2 flow.
   220  			principalDir, principalCleanUp := testutil.TempDir(t, "", "")
   221  			defer principalCleanUp()
   222  			cmd := exec.Command(exe,
   223  				"-dir", principalDir,
   224  				"-browser=false",
   225  				"-blesser", fmt.Sprintf("/%s", blesserEndpoint.Address),
   226  				"-google-oauth2-url", fmt.Sprintf("http://%s", listener.Addr().String()))
   227  			cmd.Env = []string{pathEnv}
   228  			cmd.Stdin = bytes.NewReader([]byte("testcode"))
   229  			stdout, _ := ticketServerUtil.RunAndCapture(t, cmd)
   230  			assert.Contains(t, stdout, wantClientBlessing)
   231  
   232  			// Make sure we got the right blessing.
   233  			principal, err := libsecurity.LoadPersistentPrincipal(principalDir, nil)
   234  			assert.NoError(t, err)
   235  			defaultBlessing, _ := principal.BlessingStore().Default()
   236  			assert.Contains(t, defaultBlessing.String(), wantClientBlessing)
   237  		})
   238  
   239  		t.Run("k8s", func(t *testing.T) {
   240  			const (
   241  				wantCaCrt               = "caCrt"
   242  				wantNamespace           = "namespace"
   243  				wantToken               = "token"
   244  				wantRegion              = "us-west-2"
   245  				serverBlessingName      = "grail-access-test-blessing-abc123"
   246  				clientBlessingExtension = "k8s-test"
   247  				wantClientBlessing      = serverBlessingName + ":" + clientBlessingExtension
   248  			)
   249  
   250  			// Run fake ticket server: accepts (caCrt, namespace, token), returns blessings.
   251  			var blesserEndpoint naming.Endpoint
   252  			ctx, blesserEndpoint = ticketServerUtil.RunBlesserServer(ctx, t,
   253  				identity.K8sBlesserServer(fakeK8sBlesser(
   254  					func(gotCaCrt string, gotNamespace string, gotToken string, gotRegion string, recipient security.PublicKey) security.Blessings {
   255  						assert.Equal(t, gotCaCrt, wantCaCrt)
   256  						assert.Equal(t, gotNamespace, wantNamespace)
   257  						assert.Equal(t, gotToken, wantToken)
   258  						assert.Equal(t, gotRegion, wantRegion)
   259  						p := v23.GetPrincipal(ctx)
   260  						caveat, err := security.NewExpiryCaveat(time.Now().Add(24 * time.Hour))
   261  						assert.NoError(t, err)
   262  						localBlessings, err := p.BlessSelf(serverBlessingName)
   263  						assert.NoError(t, err)
   264  						b, err := p.Bless(recipient, localBlessings, clientBlessingExtension, caveat)
   265  						assert.NoError(t, err)
   266  						return b
   267  					}),
   268  				),
   269  			)
   270  
   271  			// Create caCrt, namespace, and token files
   272  			tmpDir, cleanUp := testutil.TempDir(t, "", "")
   273  			defer cleanUp()
   274  
   275  			assert.NoError(t, ioutil.WriteFile(path.Join(tmpDir, "caCrt"), []byte(wantCaCrt), 0644))
   276  			assert.NoError(t, ioutil.WriteFile(path.Join(tmpDir, "namespace"), []byte(wantNamespace), 0644))
   277  			assert.NoError(t, ioutil.WriteFile(path.Join(tmpDir, "token"), []byte(wantToken), 0644))
   278  
   279  			// Run grail-access to create a principal and bless it with the k8s flow.
   280  			principalDir, principalCleanUp := testutil.TempDir(t, "", "")
   281  			defer principalCleanUp()
   282  			cmd := exec.Command(exe,
   283  				"-dir", principalDir,
   284  				"-blesser", fmt.Sprintf("/%s", blesserEndpoint.Address),
   285  				"-k8s",
   286  				"-ca-crt", path.Join(tmpDir, "caCrt"),
   287  				"-namespace", path.Join(tmpDir, "namespace"),
   288  				"-token", path.Join(tmpDir, "token"),
   289  			)
   290  			cmd.Env = []string{pathEnv}
   291  			stdout, _ := ticketServerUtil.RunAndCapture(t, cmd)
   292  			assert.Contains(t, stdout, wantClientBlessing)
   293  
   294  			// Make sure we got the right blessing.
   295  			principal, err := libsecurity.LoadPersistentPrincipal(principalDir, nil)
   296  			assert.NoError(t, err)
   297  			defaultBlessing, _ := principal.BlessingStore().Default()
   298  			assert.Contains(t, defaultBlessing.String(), wantClientBlessing)
   299  		})
   300  
   301  		// If any of ca.crt, namespace, or token files are missing, an error should be thrown.
   302  		t.Run("k8s_missing_file_should_fail", func(t *testing.T) {
   303  			// Run grail-access to create a principal and bless it with the k8s flow.
   304  			principalDir, principalCleanUp := testutil.TempDir(t, "", "")
   305  			defer principalCleanUp()
   306  			cmd := exec.Command(exe,
   307  				"-dir", principalDir,
   308  				"-k8s",
   309  			)
   310  			cmd.Env = []string{pathEnv}
   311  			var stderrBuf strings.Builder
   312  			cmd.Stderr = &stderrBuf
   313  			err := cmd.Run()
   314  			assert.Error(t, err)
   315  			wantStderr := "no such file or directory"
   316  			assert.True(t, strings.Contains(stderrBuf.String(), wantStderr))
   317  		})
   318  
   319  	})
   320  }
   321  
   322  type fakeBlesser func(arg string, recipientKey security.PublicKey) security.Blessings
   323  
   324  func (f fakeBlesser) BlessEc2(_ *context.T, call rpc.ServerCall, s string) (security.Blessings, error) {
   325  	return f(s, call.Security().RemoteBlessings().PublicKey()), nil
   326  }
   327  
   328  func (f fakeBlesser) BlessGoogle(_ *context.T, call rpc.ServerCall, s string) (security.Blessings, error) {
   329  	return f(s, call.Security().RemoteBlessings().PublicKey()), nil
   330  }
   331  
   332  type fakeK8sBlesser func(arg1, arg2, arg3, arg4 string, recipientKey security.PublicKey) security.Blessings
   333  
   334  func (f fakeK8sBlesser) BlessK8s(_ *context.T, call rpc.ServerCall, s1, s2, s3, s4 string) (security.Blessings, error) {
   335  	return f(s1, s2, s3, s4, call.Security().RemoteBlessings().PublicKey()), nil
   336  }