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 }