15107: Get additional email addresses from Google account.
authorTom Clegg <tclegg@veritasgenetics.com>
Thu, 7 Nov 2019 19:55:38 +0000 (14:55 -0500)
committerTom Clegg <tclegg@veritasgenetics.com>
Tue, 12 Nov 2019 15:20:20 +0000 (10:20 -0500)
Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg@veritasgenetics.com>

lib/config/config.default.yml
lib/config/generated_config.go
lib/controller/localdb/login.go
lib/controller/localdb/login_test.go
lib/controller/rpc/conn.go
sdk/go/arvados/login.go
services/api/app/controllers/database_controller.rb
vendor/vendor.json

index fee8503df812492357ebf813240fc8c8abda4596..0cf7b5e6932517d6d425083068a39d7ce086426a 100644 (file)
@@ -498,9 +498,11 @@ Clusters:
 
       # (Experimental) Authenticate with Google, bypassing the
       # SSO-provider gateway service. Use the Google Cloud console to
-      # generate the Client ID and secret (APIs and Services >
-      # Credentials > Create credentials > OAuth client ID > Web
-      # application) and add your controller's /login URL (e.g.,
+      # enable the People API (APIs and Services > Enable APIs and
+      # services > Google People API > Enable), generate a Client ID
+      # and secret (APIs and Services > Credentials > Create
+      # credentials > OAuth client ID > Web application) and add your
+      # controller's /login URL (e.g.,
       # "https://zzzzz.example.com/login") as an authorized redirect
       # URL.
       #
index 42beb066344545f6faa2e9a1c30c6dc8b744578e..4dc00cf62845db0248dd9cb2e409692c15a0f44f 100644 (file)
@@ -504,9 +504,11 @@ Clusters:
 
       # (Experimental) Authenticate with Google, bypassing the
       # SSO-provider gateway service. Use the Google Cloud console to
-      # generate the Client ID and secret (APIs and Services >
-      # Credentials > Create credentials > OAuth client ID > Web
-      # application) and add your controller's /login URL (e.g.,
+      # enable the People API (APIs and Services > Enable APIs and
+      # services > Google People API > Enable), generate a Client ID
+      # and secret (APIs and Services > Credentials > Create
+      # credentials > OAuth client ID > Web application) and add your
+      # controller's /login URL (e.g.,
       # "https://zzzzz.example.com/login") as an authorized redirect
       # URL.
       #
index 8b83c385719227eefc757f13904f7fe06f0fe575..ddd342699a47d792d6ad131dec86657b46fba15e 100644 (file)
@@ -21,14 +21,18 @@ import (
        "git.curoverse.com/arvados.git/lib/controller/rpc"
        "git.curoverse.com/arvados.git/sdk/go/arvados"
        "git.curoverse.com/arvados.git/sdk/go/auth"
+       "git.curoverse.com/arvados.git/sdk/go/ctxlog"
        "github.com/coreos/go-oidc"
        "golang.org/x/oauth2"
+       "google.golang.org/api/option"
+       "google.golang.org/api/people/v1"
 )
 
 type googleLoginController struct {
-       issuer   string // override OIDC issuer URL (normally https://accounts.google.com) for testing
-       provider *oidc.Provider
-       mu       sync.Mutex
+       issuer            string // override OIDC issuer URL (normally https://accounts.google.com) for testing
+       peopleAPIBasePath string // override Google People API base URL (normally set by google pkg to https://people.googleapis.com/)
+       provider          *oidc.Provider
+       mu                sync.Mutex
 }
 
 func (ctrl *googleLoginController) getProvider() (*oidc.Provider, error) {
@@ -106,34 +110,101 @@ func (ctrl *googleLoginController) Login(ctx context.Context, cluster *arvados.C
                if err != nil {
                        return ctrl.loginError(fmt.Errorf("error verifying ID token: %s", err))
                }
-               var claims struct {
-                       Name     string `json:"name"`
-                       Email    string `json:"email"`
-                       Verified bool   `json:"email_verified"`
+               authinfo, err := ctrl.getAuthInfo(ctx, conf, oauth2Token, idToken)
+               if err != nil {
+                       return ctrl.loginError(err)
                }
-               if err := idToken.Claims(&claims); err != nil {
-                       return ctrl.loginError(fmt.Errorf("error extracting claims from ID token: %s", err))
+               ctxRoot := auth.NewContext(ctx, &auth.Credentials{Tokens: []string{cluster.SystemRootToken}})
+               return railsproxy.UserSessionCreate(ctxRoot, rpc.UserSessionCreateOptions{
+                       ReturnTo: state.Remote + "," + state.ReturnTo,
+                       AuthInfo: *authinfo,
+               })
+       }
+}
+
+// Use a person's token to get all of their email addresses, with the
+// primary address at index 0. The provided defaultAddr is always
+// included in the returned slice, and is used as the primary if the
+// Google API does not indicate one.
+func (ctrl *googleLoginController) getAuthInfo(ctx context.Context, conf *oauth2.Config, token *oauth2.Token, idToken *oidc.IDToken) (*rpc.UserSessionAuthInfo, error) {
+       var ret rpc.UserSessionAuthInfo
+       defer ctxlog.FromContext(ctx).Infof("ret: %#v", &ret) // debug
+
+       var claims struct {
+               Name     string `json:"name"`
+               Email    string `json:"email"`
+               Verified bool   `json:"email_verified"`
+       }
+       if err := idToken.Claims(&claims); err != nil {
+               return nil, fmt.Errorf("error extracting claims from ID token: %s", err)
+       } else if claims.Verified {
+               // Fall back to this info if the People API call
+               // (below) doesn't return a primary && verified email.
+               if names := strings.Fields(strings.TrimSpace(claims.Name)); len(names) > 1 {
+                       ret.FirstName = strings.Join(names[0:len(names)-1], " ")
+                       ret.LastName = names[len(names)-1]
+               } else {
+                       ret.FirstName = names[0]
                }
-               if !claims.Verified {
-                       return ctrl.loginError(errors.New("cannot authenticate using an unverified email address"))
+               ret.Email = claims.Email
+       }
+
+       svc, err := people.NewService(ctx, option.WithTokenSource(conf.TokenSource(ctx, token)), option.WithScopes(people.UserEmailsReadScope))
+       if err != nil {
+               return nil, fmt.Errorf("error setting up People API: %s", err)
+       }
+       if p := ctrl.peopleAPIBasePath; p != "" {
+               // Override normal API endpoint (for testing)
+               svc.BasePath = p
+       }
+       person, err := people.NewPeopleService(svc).Get("people/me").Fields("emailAddresses,names").Do()
+       if err != nil {
+               if strings.Contains(err.Error(), "Error 403") && strings.Contains(err.Error(), "accessNotConfigured") && ret.Email != "" {
+                       // Fall back on the primary email from the OAuth2 token.
+                       ctxlog.FromContext(ctx).WithError(err).WithField("email", ret.Email).Warn("cannot look up alternate email addresses because People API is not enabled")
+                       return &ret, nil
+               } else {
+                       // Unexpected error, or no email to fall back on.
+                       return nil, fmt.Errorf("error getting profile info from People API: %s", err)
                }
+       }
+
+       ctxlog.FromContext(ctx).Infof("people/me response: %#v", person) // debug
 
-               firstname, lastname := strings.TrimSpace(claims.Name), ""
-               if names := strings.Fields(firstname); len(names) > 1 {
-                       firstname = strings.Join(names[0:len(names)-1], " ")
-                       lastname = names[len(names)-1]
+       // The given/family names returned by the People API and
+       // flagged as "primary" (if any) take precedence over the
+       // split-by-whitespace result from above.
+       for _, name := range person.Names {
+               if name.Metadata != nil && name.Metadata.Primary {
+                       ret.FirstName = name.GivenName
+                       ret.LastName = name.FamilyName
+                       break
                }
+       }
 
-               ctxRoot := auth.NewContext(ctx, &auth.Credentials{Tokens: []string{cluster.SystemRootToken}})
-               return railsproxy.UserSessionCreate(ctxRoot, rpc.UserSessionCreateOptions{
-                       ReturnTo: state.Remote + "," + state.ReturnTo,
-                       AuthInfo: map[string]interface{}{
-                               "email":      claims.Email,
-                               "first_name": firstname,
-                               "last_name":  lastname,
-                       },
-               })
+       altEmails := map[string]bool{}
+       if ret.Email != "" {
+               altEmails[ret.Email] = true
+       }
+       for _, ea := range person.EmailAddresses {
+               if ea.Metadata == nil || !ea.Metadata.Verified {
+                       ctxlog.FromContext(ctx).WithField("address", ea.Value).Debug("skipping unverified email address")
+                       continue
+               }
+               altEmails[ea.Value] = true
+               if ea.Metadata.Primary || ret.Email == "" {
+                       ret.Email = ea.Value
+               }
+       }
+       if len(altEmails) == 0 {
+               return nil, errors.New("cannot log in without a verified email address")
+       }
+       for ae := range altEmails {
+               if ae != ret.Email {
+                       ret.AlternateEmails = append(ret.AlternateEmails, ae)
+               }
        }
+       return &ret, nil
 }
 
 func (ctrl *googleLoginController) loginError(sendError error) (resp arvados.LoginResponse, err error) {
index 362e258409aa4221ecbabe10d504671f55f8b9a7..e36571ef168ad1ae52b2386cf0371e72279176ce 100644 (file)
@@ -14,6 +14,7 @@ import (
        "net/http"
        "net/http/httptest"
        "net/url"
+       "sort"
        "strings"
        "testing"
        "time"
@@ -36,12 +37,14 @@ func Test(t *testing.T) {
 var _ = check.Suite(&LoginSuite{})
 
 type LoginSuite struct {
-       cluster    *arvados.Cluster
-       ctx        context.Context
-       localdb    *Conn
-       railsSpy   *arvadostest.Proxy
-       fakeIssuer *httptest.Server
-       issuerKey  *rsa.PrivateKey
+       cluster               *arvados.Cluster
+       ctx                   context.Context
+       localdb               *Conn
+       railsSpy              *arvadostest.Proxy
+       fakeIssuer            *httptest.Server
+       fakePeopleAPI         *httptest.Server
+       fakePeopleAPIResponse map[string]interface{}
+       issuerKey             *rsa.PrivateKey
 
        // expected token request
        validCode string
@@ -51,6 +54,13 @@ type LoginSuite struct {
        authName          string
 }
 
+func (s *LoginSuite) TearDownSuite(c *check.C) {
+       // Undo any changes/additions to the user database so they
+       // don't affect subsequent tests.
+       arvadostest.ResetEnv()
+       c.Check(arvados.NewClientFromEnv().RequestAndDecode(nil, "POST", "database/reset", nil, nil), check.IsNil)
+}
+
 func (s *LoginSuite) SetUpTest(c *check.C) {
        var err error
        s.issuerKey, err = rsa.GenerateKey(rand.Reader, 2048)
@@ -115,6 +125,24 @@ func (s *LoginSuite) SetUpTest(c *check.C) {
                        w.WriteHeader(http.StatusNotFound)
                }
        }))
+       s.validCode = fmt.Sprintf("abcdefgh-%d", time.Now().Unix())
+
+       s.fakePeopleAPI = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+               req.ParseForm()
+               c.Logf("fakePeopleAPI: got req: %s %s %s", req.Method, req.URL, req.Form)
+               w.Header().Set("Content-Type", "application/json")
+               switch req.URL.Path {
+               case "/v1/people/me":
+                       if f := req.Form.Get("fields"); f != "emailAddresses,names" {
+                               w.WriteHeader(http.StatusBadRequest)
+                               break
+                       }
+                       json.NewEncoder(w).Encode(s.fakePeopleAPIResponse)
+               default:
+                       w.WriteHeader(http.StatusNotFound)
+               }
+       }))
+       s.fakePeopleAPIResponse = map[string]interface{}{}
 
        cfg, err := config.NewLoader(nil, ctxlog.TestLogger(c)).Load()
        s.cluster, err = cfg.GetCluster("")
@@ -124,6 +152,7 @@ func (s *LoginSuite) SetUpTest(c *check.C) {
 
        s.localdb = NewConn(s.cluster)
        s.localdb.googleLoginController.issuer = s.fakeIssuer.URL
+       s.localdb.googleLoginController.peopleAPIBasePath = s.fakePeopleAPI.URL
 
        s.railsSpy = arvadostest.NewProxy(c, s.cluster.Services.RailsAPI)
        s.localdb.railsProxy = rpc.NewConn(s.cluster.ClusterID, s.railsSpy.URL, true, rpc.PassthroughTokenProvider)
@@ -133,14 +162,14 @@ func (s *LoginSuite) TearDownTest(c *check.C) {
        s.railsSpy.Close()
 }
 
-func (s *LoginSuite) TestGoogleLoginStart_Bogus(c *check.C) {
+func (s *LoginSuite) TestGoogleLogin_Start_Bogus(c *check.C) {
        resp, err := s.localdb.Login(context.Background(), arvados.LoginOptions{})
        c.Check(err, check.IsNil)
        c.Check(resp.RedirectLocation, check.Equals, "")
        c.Check(resp.HTML.String(), check.Matches, `.*missing return_to parameter.*`)
 }
 
-func (s *LoginSuite) TestGoogleLoginStart(c *check.C) {
+func (s *LoginSuite) TestGoogleLogin_Start(c *check.C) {
        for _, remote := range []string{"", "zzzzz"} {
                resp, err := s.localdb.Login(context.Background(), arvados.LoginOptions{Remote: remote, ReturnTo: "https://app.example.com/foo?bar"})
                c.Check(err, check.IsNil)
@@ -158,70 +187,48 @@ func (s *LoginSuite) TestGoogleLoginStart(c *check.C) {
        }
 }
 
-func (s *LoginSuite) TestGoogleLoginSuccess(c *check.C) {
-       // Initiate login, but instead of following the redirect to
-       // the provider, just grab state from the redirect URL.
-       resp, err := s.localdb.Login(context.Background(), arvados.LoginOptions{ReturnTo: "https://app.example.com/foo?bar"})
-       c.Check(err, check.IsNil)
-       target, err := url.Parse(resp.RedirectLocation)
-       c.Check(err, check.IsNil)
-       state := target.Query().Get("state")
-       c.Check(state, check.Not(check.Equals), "")
-
-       // Prime the fake issuer with a valid code.
-       s.validCode = fmt.Sprintf("abcdefgh-%d", time.Now().Unix())
-
-       // Callback with invalid code.
-       resp, err = s.localdb.Login(context.Background(), arvados.LoginOptions{
+func (s *LoginSuite) TestGoogleLogin_InvalidCode(c *check.C) {
+       state := s.startLogin(c)
+       resp, err := s.localdb.Login(context.Background(), arvados.LoginOptions{
                Code:  "first-try-a-bogus-code",
                State: state,
        })
        c.Check(err, check.IsNil)
        c.Check(resp.RedirectLocation, check.Equals, "")
        c.Check(resp.HTML.String(), check.Matches, `(?ms).*error in OAuth2 exchange.*cannot fetch token.*`)
+}
 
-       // Callback with invalid state.
-       resp, err = s.localdb.Login(context.Background(), arvados.LoginOptions{
+func (s *LoginSuite) TestGoogleLogin_InvalidState(c *check.C) {
+       s.startLogin(c)
+       resp, err := s.localdb.Login(context.Background(), arvados.LoginOptions{
                Code:  s.validCode,
                State: "bogus-state",
        })
        c.Check(err, check.IsNil)
        c.Check(resp.RedirectLocation, check.Equals, "")
        c.Check(resp.HTML.String(), check.Matches, `(?ms).*invalid OAuth2 state.*`)
+}
 
-       // Callback with valid code and state.
-       resp, err = s.localdb.Login(context.Background(), arvados.LoginOptions{
+func (s *LoginSuite) TestGoogleLogin_Success(c *check.C) {
+       state := s.startLogin(c)
+       resp, err := s.localdb.Login(context.Background(), arvados.LoginOptions{
                Code:  s.validCode,
                State: state,
        })
        c.Check(err, check.IsNil)
        c.Check(resp.HTML.String(), check.Equals, "")
-       c.Check(resp.RedirectLocation, check.Not(check.Equals), "")
-       target, err = url.Parse(resp.RedirectLocation)
+       target, err := url.Parse(resp.RedirectLocation)
        c.Check(err, check.IsNil)
        c.Check(target.Host, check.Equals, "app.example.com")
        c.Check(target.Path, check.Equals, "/foo")
        token := target.Query().Get("api_token")
        c.Check(token, check.Matches, `v2/zzzzz-gj3su-.{15}/.{32,50}`)
 
-       foundCallback := false
-       for _, dump := range s.railsSpy.RequestDumps {
-               c.Logf("spied request: %q", dump)
-               split := bytes.Split(dump, []byte("\r\n\r\n"))
-               c.Assert(split, check.HasLen, 2)
-               hdr, body := string(split[0]), string(split[1])
-               if strings.Contains(hdr, "POST /auth/controller/callback") {
-                       vs, err := url.ParseQuery(body)
-                       var authinfo map[string]interface{}
-                       c.Check(json.Unmarshal([]byte(vs.Get("auth_info")), &authinfo), check.IsNil)
-                       c.Check(err, check.IsNil)
-                       c.Check(authinfo["first_name"], check.Equals, "Fake User")
-                       c.Check(authinfo["last_name"], check.Equals, "Name")
-                       c.Check(authinfo["email"], check.Equals, "active-user@arvados.local")
-                       foundCallback = true
-               }
-       }
-       c.Check(foundCallback, check.Equals, true)
+       authinfo := s.getCallbackAuthInfo(c)
+       c.Check(authinfo.FirstName, check.Equals, "Fake User")
+       c.Check(authinfo.LastName, check.Equals, "Name")
+       c.Check(authinfo.Email, check.Equals, "active-user@arvados.local")
+       c.Check(authinfo.AlternateEmails, check.HasLen, 0)
 
        // Try using the returned Arvados token.
        c.Logf("trying an API call with new token %q", token)
@@ -241,6 +248,157 @@ func (s *LoginSuite) TestGoogleLoginSuccess(c *check.C) {
        c.Check(err, check.ErrorMatches, `.*401 Unauthorized: Not logged in.*`)
 }
 
+func (s *LoginSuite) TestGoogleLogin_RealName(c *check.C) {
+       s.authEmail = "joe.smith@primary.example.com"
+       s.fakePeopleAPIResponse = map[string]interface{}{
+               "names": []map[string]interface{}{
+                       {
+                               "metadata":   map[string]interface{}{"primary": false},
+                               "givenName":  "Joe",
+                               "familyName": "Smith",
+                       },
+                       {
+                               "metadata":   map[string]interface{}{"primary": true},
+                               "givenName":  "Joseph",
+                               "familyName": "Psmith",
+                       },
+               },
+       }
+       state := s.startLogin(c)
+       s.localdb.Login(context.Background(), arvados.LoginOptions{
+               Code:  s.validCode,
+               State: state,
+       })
+
+       authinfo := s.getCallbackAuthInfo(c)
+       c.Check(authinfo.FirstName, check.Equals, "Joseph")
+       c.Check(authinfo.LastName, check.Equals, "Psmith")
+}
+
+func (s *LoginSuite) TestGoogleLogin_OIDCRealName(c *check.C) {
+       s.authName = "Joe P. Smith"
+       s.authEmail = "joe.smith@primary.example.com"
+       state := s.startLogin(c)
+       s.localdb.Login(context.Background(), arvados.LoginOptions{
+               Code:  s.validCode,
+               State: state,
+       })
+
+       authinfo := s.getCallbackAuthInfo(c)
+       c.Check(authinfo.FirstName, check.Equals, "Joe P.")
+       c.Check(authinfo.LastName, check.Equals, "Smith")
+}
+
+// People API returns some additional email addresses.
+func (s *LoginSuite) TestGoogleLogin_AlternateEmailAddresses(c *check.C) {
+       s.authEmail = "joe.smith@primary.example.com"
+       s.fakePeopleAPIResponse = map[string]interface{}{
+               "emailAddresses": []map[string]interface{}{
+                       {
+                               "metadata": map[string]interface{}{"verified": true},
+                               "value":    "joe.smith@work.example.com",
+                       },
+                       {
+                               "value": "joe.smith@unverified.example.com", // unverified, so this one will be ignored
+                       },
+                       {
+                               "metadata": map[string]interface{}{"verified": true},
+                               "value":    "joe.smith@home.example.com",
+                       },
+               },
+       }
+       state := s.startLogin(c)
+       s.localdb.Login(context.Background(), arvados.LoginOptions{
+               Code:  s.validCode,
+               State: state,
+       })
+
+       authinfo := s.getCallbackAuthInfo(c)
+       c.Check(authinfo.Email, check.Equals, "joe.smith@primary.example.com")
+       c.Check(authinfo.AlternateEmails, check.DeepEquals, []string{"joe.smith@home.example.com", "joe.smith@work.example.com"})
+}
+
+// Primary address is not the one initially returned by oidc.
+func (s *LoginSuite) TestGoogleLogin_AlternateEmailAddresses_Primary(c *check.C) {
+       s.authEmail = "joe.smith@alternate.example.com"
+       s.fakePeopleAPIResponse = map[string]interface{}{
+               "emailAddresses": []map[string]interface{}{
+                       {
+                               "metadata": map[string]interface{}{"verified": true, "primary": true},
+                               "value":    "joe.smith@primary.example.com",
+                       },
+                       {
+                               "metadata": map[string]interface{}{"verified": true},
+                               "value":    "joe.smith@alternate.example.com",
+                       },
+               },
+       }
+       state := s.startLogin(c)
+       s.localdb.Login(context.Background(), arvados.LoginOptions{
+               Code:  s.validCode,
+               State: state,
+       })
+       authinfo := s.getCallbackAuthInfo(c)
+       c.Check(authinfo.Email, check.Equals, "joe.smith@primary.example.com")
+       c.Check(authinfo.AlternateEmails, check.DeepEquals, []string{"joe.smith@alternate.example.com"})
+}
+
+func (s *LoginSuite) TestGoogleLogin_NoPrimaryEmailAddress(c *check.C) {
+       s.authEmail = "joe.smith@unverified.example.com"
+       s.authEmailVerified = false
+       s.fakePeopleAPIResponse = map[string]interface{}{
+               "emailAddresses": []map[string]interface{}{
+                       {
+                               "metadata": map[string]interface{}{"verified": true},
+                               "value":    "joe.smith@work.example.com",
+                       },
+                       {
+                               "metadata": map[string]interface{}{"verified": true},
+                               "value":    "joe.smith@home.example.com",
+                       },
+               },
+       }
+       state := s.startLogin(c)
+       s.localdb.Login(context.Background(), arvados.LoginOptions{
+               Code:  s.validCode,
+               State: state,
+       })
+
+       authinfo := s.getCallbackAuthInfo(c)
+       c.Check(authinfo.Email, check.Equals, "joe.smith@work.example.com") // first verified email in People response
+       c.Check(authinfo.AlternateEmails, check.DeepEquals, []string{"joe.smith@home.example.com"})
+}
+
+func (s *LoginSuite) getCallbackAuthInfo(c *check.C) (authinfo rpc.UserSessionAuthInfo) {
+       for _, dump := range s.railsSpy.RequestDumps {
+               c.Logf("spied request: %q", dump)
+               split := bytes.Split(dump, []byte("\r\n\r\n"))
+               c.Assert(split, check.HasLen, 2)
+               hdr, body := string(split[0]), string(split[1])
+               if strings.Contains(hdr, "POST /auth/controller/callback") {
+                       vs, err := url.ParseQuery(body)
+                       c.Check(json.Unmarshal([]byte(vs.Get("auth_info")), &authinfo), check.IsNil)
+                       c.Check(err, check.IsNil)
+                       sort.Strings(authinfo.AlternateEmails)
+                       return
+               }
+       }
+       c.Error("callback not found")
+       return
+}
+
+func (s *LoginSuite) startLogin(c *check.C) (state string) {
+       // Initiate login, but instead of following the redirect to
+       // the provider, just grab state from the redirect URL.
+       resp, err := s.localdb.Login(context.Background(), arvados.LoginOptions{ReturnTo: "https://app.example.com/foo?bar"})
+       c.Check(err, check.IsNil)
+       target, err := url.Parse(resp.RedirectLocation)
+       c.Check(err, check.IsNil)
+       state = target.Query().Get("state")
+       c.Check(state, check.Not(check.Equals), "")
+       return
+}
+
 func (s *LoginSuite) fakeToken(c *check.C, payload []byte) string {
        signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.RS256, Key: s.issuerKey}, nil)
        if err != nil {
index cb23c7fad1ea99dce585f3626b66bf9130b82069..7d7cb486f4f742d57411751254cf7b8dd6ab22ad 100644 (file)
@@ -315,9 +315,16 @@ func (conn *Conn) APIClientAuthorizationCurrent(ctx context.Context, options arv
        return resp, err
 }
 
+type UserSessionAuthInfo struct {
+       Email           string   `json:"email"`
+       AlternateEmails []string `json:"alternate_emails"`
+       FirstName       string   `json:"first_name"`
+       LastName        string   `json:"last_name"`
+}
+
 type UserSessionCreateOptions struct {
-       AuthInfo map[string]interface{} `json:"auth_info"`
-       ReturnTo string                 `json:"return_to"`
+       AuthInfo UserSessionAuthInfo `json:"auth_info"`
+       ReturnTo string              `json:"return_to"`
 }
 
 func (conn *Conn) UserSessionCreate(ctx context.Context, options UserSessionCreateOptions) (arvados.LoginResponse, error) {
index 8c515468cc18d34c2413f917f175657faacea863..7107ac57ab76c029ccc28764762ad4b08de42bbe 100644 (file)
@@ -15,10 +15,12 @@ type LoginResponse struct {
 }
 
 func (resp LoginResponse) ServeHTTP(w http.ResponseWriter, req *http.Request) {
+       w.Header().Set("Cache-Control", "no-store")
        if resp.RedirectLocation != "" {
                w.Header().Set("Location", resp.RedirectLocation)
                w.WriteHeader(http.StatusFound)
        } else {
+               w.Header().Set("Content-Type", "text/html")
                w.Write(resp.HTML.Bytes())
        }
 }
index b618a321e5d7578665141df4876a98a5bf872d59..d6045a5dcbf35a3c786bb6db5105d49e9636cc39 100644 (file)
@@ -14,7 +14,7 @@ class DatabaseController < ApplicationController
     # use @example.com email addresses when creating user records, so
     # we can tell they're not valuable.
     user_uuids = User.
-      where('email is null or email not like ?', '%@example.com').
+      where('email is null or (email not like ? and email not like ?)', '%@example.com', '%.example.com').
       collect(&:uuid)
     fixture_uuids =
       YAML::load_file(File.expand_path('../../../test/fixtures/users.yml',
index b449e2f12bc8394e0c7e92d00c84100cf8c18ec5..7585a8c9e1fd0f7253aa246f3cd41141f619ae22 100644 (file)
@@ -2,6 +2,12 @@
        "comment": "",
        "ignore": "test appengine",
        "package": [
+               {
+                       "checksumSHA1": "AH7jcN7pvaPDU6UjHdpT081DDGk=",
+                       "path": "cloud.google.com/go/compute/metadata",
+                       "revision": "f07fddce3276603951ac45f50f743de632956bef",
+                       "revisionTime": "2018-05-30T18:12:30Z"
+               },
                {
                        "checksumSHA1": "jfYWZyRWLMfG0J5K7G2K8a9AKfs=",
                        "origin": "github.com/curoverse/goamz/aws",
                        "revisionTime": "2018-05-09T16:24:41Z"
                },
                {
-                       "checksumSHA1": "yqF125xVSkmfLpIVGrLlfE05IUk=",
+                       "checksumSHA1": "LHNzQwau1zPeFPPG5zbNf8AgUOQ=",
+                       "path": "github.com/golang/groupcache/lru",
+                       "revision": "611e8accdfc92c4187d399e95ce826046d4c8d73",
+                       "revisionTime": "2019-10-27T21:21:12Z"
+               },
+               {
+                       "checksumSHA1": "Q3FteGbNvRRUMJqbYbmrcBd2DMo=",
                        "path": "github.com/golang/protobuf/proto",
-                       "revision": "1e59b77b52bf8e4b449a57e6f79f21226d571845",
-                       "revisionTime": "2017-11-13T18:07:20Z"
+                       "revision": "ed6926b37a637426117ccab59282c3839528a700",
+                       "revisionTime": "2019-10-22T19:55:53Z"
+               },
+               {
+                       "checksumSHA1": "aEiR2m3NGaMGTbUW5P+w5gKFyc8=",
+                       "path": "github.com/golang/protobuf/ptypes",
+                       "revision": "ed6926b37a637426117ccab59282c3839528a700",
+                       "revisionTime": "2019-10-22T19:55:53Z"
+               },
+               {
+                       "checksumSHA1": "2/Xg4L9IVGQRJB8zCELZx7/Z4HU=",
+                       "path": "github.com/golang/protobuf/ptypes/any",
+                       "revision": "ed6926b37a637426117ccab59282c3839528a700",
+                       "revisionTime": "2019-10-22T19:55:53Z"
+               },
+               {
+                       "checksumSHA1": "RE9rLveNHapyMKQC8p10tbkUE9w=",
+                       "path": "github.com/golang/protobuf/ptypes/duration",
+                       "revision": "ed6926b37a637426117ccab59282c3839528a700",
+                       "revisionTime": "2019-10-22T19:55:53Z"
+               },
+               {
+                       "checksumSHA1": "seEwY2xETpK9yHJ9+bHqkLZ0VMU=",
+                       "path": "github.com/golang/protobuf/ptypes/timestamp",
+                       "revision": "ed6926b37a637426117ccab59282c3839528a700",
+                       "revisionTime": "2019-10-22T19:55:53Z"
+               },
+               {
+                       "checksumSHA1": "xN4Xr7jzSvXl7DOOqzbWihUbfuA=",
+                       "path": "github.com/google/go-cmp/cmp",
+                       "revision": "776445f29feeb6041579ae3df3c5615aba0fa128",
+                       "revisionTime": "2019-11-05T00:03:44Z"
+               },
+               {
+                       "checksumSHA1": "FUnTgtE5i3f8asIvicGkJSFlrts=",
+                       "path": "github.com/google/go-cmp/cmp/internal/diff",
+                       "revision": "776445f29feeb6041579ae3df3c5615aba0fa128",
+                       "revisionTime": "2019-11-05T00:03:44Z"
+               },
+               {
+                       "checksumSHA1": "nR8EJ8i8lqxxmtLPnXI7WlYANiE=",
+                       "path": "github.com/google/go-cmp/cmp/internal/flags",
+                       "revision": "776445f29feeb6041579ae3df3c5615aba0fa128",
+                       "revisionTime": "2019-11-05T00:03:44Z"
+               },
+               {
+                       "checksumSHA1": "0pcLJsUQUaBdPXM5LuL9uFeuETs=",
+                       "path": "github.com/google/go-cmp/cmp/internal/function",
+                       "revision": "776445f29feeb6041579ae3df3c5615aba0fa128",
+                       "revisionTime": "2019-11-05T00:03:44Z"
+               },
+               {
+                       "checksumSHA1": "zQxhgAvWmYtrTZjxRenQQYiDX50=",
+                       "path": "github.com/google/go-cmp/cmp/internal/value",
+                       "revision": "776445f29feeb6041579ae3df3c5615aba0fa128",
+                       "revisionTime": "2019-11-05T00:03:44Z"
+               },
+               {
+                       "checksumSHA1": "NqlcmlYFsLm2R1iJY0zynpIWNhg=",
+                       "path": "github.com/googleapis/gax-go/v2",
+                       "revision": "b443e5a67ec8eeac76f5f384004931878cab24b3",
+                       "revisionTime": "2019-10-18T15:11:19Z"
                },
                {
                        "checksumSHA1": "iIUYZyoanCQQTUaWsu8b+iOSPt4=",
                        "revision": "ba9c9e33906f58169366275e3450db66139a31a9",
                        "revisionTime": "2015-12-15T15:34:51Z"
                },
+               {
+                       "checksumSHA1": "Ijg5Yx2tIE09R698JrJrlDJuH6U=",
+                       "path": "go.opencensus.io",
+                       "revision": "3b5a343282fe4b4fccdb0f24cbd1d7169d20858a",
+                       "revisionTime": "2019-10-15T19:20:41Z"
+               },
+               {
+                       "checksumSHA1": "qJGsfghV4/lQ6Rhq/EaVqQPJ0s4=",
+                       "path": "go.opencensus.io/internal",
+                       "revision": "3b5a343282fe4b4fccdb0f24cbd1d7169d20858a",
+                       "revisionTime": "2019-10-15T19:20:41Z"
+               },
+               {
+                       "checksumSHA1": "Dw3rpna1DwTa7TCzijInKcU49g4=",
+                       "path": "go.opencensus.io/internal/tagencoding",
+                       "revision": "3b5a343282fe4b4fccdb0f24cbd1d7169d20858a",
+                       "revisionTime": "2019-10-15T19:20:41Z"
+               },
+               {
+                       "checksumSHA1": "r6fbtPwxK4/TYUOWc7y0hXdAG4Q=",
+                       "path": "go.opencensus.io/metric/metricdata",
+                       "revision": "3b5a343282fe4b4fccdb0f24cbd1d7169d20858a",
+                       "revisionTime": "2019-10-15T19:20:41Z"
+               },
+               {
+                       "checksumSHA1": "kWj13srwY1SH5KgFecPhEfHnzVc=",
+                       "path": "go.opencensus.io/metric/metricproducer",
+                       "revision": "3b5a343282fe4b4fccdb0f24cbd1d7169d20858a",
+                       "revisionTime": "2019-10-15T19:20:41Z"
+               },
+               {
+                       "checksumSHA1": "PxZNj+yFM4Ru4Pu2jEatlCpmqFU=",
+                       "path": "go.opencensus.io/plugin/ocgrpc",
+                       "revision": "3b5a343282fe4b4fccdb0f24cbd1d7169d20858a",
+                       "revisionTime": "2019-10-15T19:20:41Z"
+               },
+               {
+                       "checksumSHA1": "h/Q73rMyitTcsqw1Fy8C74F31hk=",
+                       "path": "go.opencensus.io/plugin/ochttp",
+                       "revision": "3b5a343282fe4b4fccdb0f24cbd1d7169d20858a",
+                       "revisionTime": "2019-10-15T19:20:41Z"
+               },
+               {
+                       "checksumSHA1": "UZhIoErIy1tKLmVT/5huwlp6KFQ=",
+                       "path": "go.opencensus.io/plugin/ochttp/propagation/b3",
+                       "revision": "3b5a343282fe4b4fccdb0f24cbd1d7169d20858a",
+                       "revisionTime": "2019-10-15T19:20:41Z"
+               },
+               {
+                       "checksumSHA1": "q+y8X+5nDONIlJlxfkv+OtA18ds=",
+                       "path": "go.opencensus.io/resource",
+                       "revision": "3b5a343282fe4b4fccdb0f24cbd1d7169d20858a",
+                       "revisionTime": "2019-10-15T19:20:41Z"
+               },
+               {
+                       "checksumSHA1": "EbYHMjHqN1YfUNgwf97qS/Z4uP8=",
+                       "path": "go.opencensus.io/stats",
+                       "revision": "3b5a343282fe4b4fccdb0f24cbd1d7169d20858a",
+                       "revisionTime": "2019-10-15T19:20:41Z"
+               },
+               {
+                       "checksumSHA1": "oIo4NRi6AVCfcwVfHzCXAsoZsdI=",
+                       "path": "go.opencensus.io/stats/internal",
+                       "revision": "3b5a343282fe4b4fccdb0f24cbd1d7169d20858a",
+                       "revisionTime": "2019-10-15T19:20:41Z"
+               },
+               {
+                       "checksumSHA1": "6LKTLjjNUw74vuJik17FFLMHOoY=",
+                       "path": "go.opencensus.io/stats/view",
+                       "revision": "3b5a343282fe4b4fccdb0f24cbd1d7169d20858a",
+                       "revisionTime": "2019-10-15T19:20:41Z"
+               },
+               {
+                       "checksumSHA1": "rx4HvicGhFI5wv55qVaRAMsHZ7g=",
+                       "path": "go.opencensus.io/tag",
+                       "revision": "3b5a343282fe4b4fccdb0f24cbd1d7169d20858a",
+                       "revisionTime": "2019-10-15T19:20:41Z"
+               },
+               {
+                       "checksumSHA1": "Qehn8Uz+e5KgZW8gPXK4snQNfiU=",
+                       "path": "go.opencensus.io/trace",
+                       "revision": "3b5a343282fe4b4fccdb0f24cbd1d7169d20858a",
+                       "revisionTime": "2019-10-15T19:20:41Z"
+               },
+               {
+                       "checksumSHA1": "JkvEb8oMEFjic5K/03Tyr5Lok+w=",
+                       "path": "go.opencensus.io/trace/internal",
+                       "revision": "3b5a343282fe4b4fccdb0f24cbd1d7169d20858a",
+                       "revisionTime": "2019-10-15T19:20:41Z"
+               },
+               {
+                       "checksumSHA1": "FHJParRi8f1GHO7Cx+lk3bMWBq0=",
+                       "path": "go.opencensus.io/trace/propagation",
+                       "revision": "3b5a343282fe4b4fccdb0f24cbd1d7169d20858a",
+                       "revisionTime": "2019-10-15T19:20:41Z"
+               },
+               {
+                       "checksumSHA1": "UHbxxaMqpEPsubh8kPwzSlyEwqI=",
+                       "path": "go.opencensus.io/trace/tracestate",
+                       "revision": "3b5a343282fe4b4fccdb0f24cbd1d7169d20858a",
+                       "revisionTime": "2019-10-15T19:20:41Z"
+               },
                {
                        "checksumSHA1": "TT1rac6kpQp2vz24m5yDGUNQ/QQ=",
                        "path": "golang.org/x/crypto/cast5",
                        "revision": "434ec0c7fe3742c984919a691b2018a6e9694425",
                        "revisionTime": "2017-09-25T09:26:47Z"
                },
+               {
+                       "checksumSHA1": "pCY4YtdNKVBYRbNvODjx8hj0hIs=",
+                       "path": "golang.org/x/net/http/httpguts",
+                       "revision": "60506f45cf65977eb3a9c6e30f995f54a721c271",
+                       "revisionTime": "2019-06-03T08:53:59Z"
+               },
+               {
+                       "checksumSHA1": "Fjq5E3MoRRoXn+VkHZ8nziuw3Vk=",
+                       "path": "golang.org/x/net/http2",
+                       "revision": "60506f45cf65977eb3a9c6e30f995f54a721c271",
+                       "revisionTime": "2019-06-03T08:53:59Z"
+               },
+               {
+                       "checksumSHA1": "VJwSx33rjMC7O6K2O50Jw6o1vw4=",
+                       "path": "golang.org/x/net/http2/hpack",
+                       "revision": "60506f45cf65977eb3a9c6e30f995f54a721c271",
+                       "revisionTime": "2019-06-03T08:53:59Z"
+               },
+               {
+                       "checksumSHA1": "vL6l4FZWitsxht0uqA/GpDNkNNc=",
+                       "path": "golang.org/x/net/idna",
+                       "revision": "60506f45cf65977eb3a9c6e30f995f54a721c271",
+                       "revisionTime": "2019-06-03T08:53:59Z"
+               },
+               {
+                       "checksumSHA1": "UxahDzW2v4mf/+aFxruuupaoIwo=",
+                       "path": "golang.org/x/net/internal/timeseries",
+                       "revision": "60506f45cf65977eb3a9c6e30f995f54a721c271",
+                       "revisionTime": "2019-06-03T08:53:59Z"
+               },
                {
                        "checksumSHA1": "r9l4r3H6FOLQ0c2JaoXpopFjpnw=",
                        "path": "golang.org/x/net/proxy",
                        "revision": "434ec0c7fe3742c984919a691b2018a6e9694425",
                        "revisionTime": "2017-09-25T09:26:47Z"
                },
+               {
+                       "checksumSHA1": "HvmG9LfStMLF+hIC7xR4SxegMis=",
+                       "path": "golang.org/x/net/trace",
+                       "revision": "60506f45cf65977eb3a9c6e30f995f54a721c271",
+                       "revisionTime": "2019-06-03T08:53:59Z"
+               },
                {
                        "checksumSHA1": "TBlnCuZUOzJHLu5DNY7XEj8TvbU=",
                        "path": "golang.org/x/net/webdav",
                        "revision": "ec22f46f877b4505e0117eeaab541714644fdd28",
                        "revisionTime": "2018-05-28T20:23:04Z"
                },
+               {
+                       "checksumSHA1": "z7mSaGccufg15ki2YPd+M5PlsUc=",
+                       "path": "golang.org/x/oauth2/google",
+                       "revision": "ec22f46f877b4505e0117eeaab541714644fdd28",
+                       "revisionTime": "2018-05-28T20:23:04Z"
+               },
                {
                        "checksumSHA1": "fddd1btmbXxnlMKHUZewlVlSaEQ=",
                        "path": "golang.org/x/oauth2/internal",
                        "revisionTime": "2018-05-28T20:23:04Z"
                },
                {
-                       "checksumSHA1": "znPq37/LZ4pJh7B4Lbu0ZuoMhNk=",
+                       "checksumSHA1": "huVltYnXdRFDJLgp/ZP9IALzG7g=",
+                       "path": "golang.org/x/oauth2/jws",
+                       "revision": "ec22f46f877b4505e0117eeaab541714644fdd28",
+                       "revisionTime": "2018-05-28T20:23:04Z"
+               },
+               {
+                       "checksumSHA1": "QPndO4ODVdEBILRhJ6869UDAoHc=",
+                       "path": "golang.org/x/oauth2/jwt",
+                       "revision": "ec22f46f877b4505e0117eeaab541714644fdd28",
+                       "revisionTime": "2018-05-28T20:23:04Z"
+               },
+               {
+                       "checksumSHA1": "cvrBKcl7QwkGktQiWFoQj1SGb94=",
                        "origin": "github.com/docker/docker/vendor/golang.org/x/sys/unix",
                        "path": "golang.org/x/sys/unix",
-                       "revision": "94b8a116fbf1cd90e68d8f5361b520d326a66f9b",
-                       "revisionTime": "2018-01-09T01:38:17Z"
+                       "revision": "c36460c437c8c515c543dd31afcbb5c2a9f5dd48",
+                       "revisionTime": "2019-11-05T21:04:14Z"
                },
                {
-                       "checksumSHA1": "8BcMOi8XTSigDtV2npDc8vMrS60=",
+                       "checksumSHA1": "+1FhB9xHOPgEPl5uaAiaegod/R0=",
                        "origin": "github.com/docker/docker/vendor/golang.org/x/sys/windows",
                        "path": "golang.org/x/sys/windows",
-                       "revision": "94b8a116fbf1cd90e68d8f5361b520d326a66f9b",
-                       "revisionTime": "2018-01-09T01:38:17Z"
+                       "revision": "c36460c437c8c515c543dd31afcbb5c2a9f5dd48",
+                       "revisionTime": "2019-11-05T21:04:14Z"
+               },
+               {
+                       "checksumSHA1": "CbpjEkkOeh0fdM/V8xKDdI0AA88=",
+                       "path": "golang.org/x/text/secure/bidirule",
+                       "revision": "7922cc490dd5a7dbaa7fd5d6196b49db59ac042f",
+                       "revisionTime": "2018-04-05T08:39:28Z"
                },
                {
                        "checksumSHA1": "ziMb9+ANGRJSSIuxYdRbA+cDRBQ=",
                        "revision": "e19ae1496984b1c655b8044a65c0300a3c878dd3",
                        "revisionTime": "2017-12-24T20:31:28Z"
                },
+               {
+                       "checksumSHA1": "w8kDfZ1Ug+qAcVU0v8obbu3aDOY=",
+                       "path": "golang.org/x/text/unicode/bidi",
+                       "revision": "7922cc490dd5a7dbaa7fd5d6196b49db59ac042f",
+                       "revisionTime": "2018-04-05T08:39:28Z"
+               },
                {
                        "checksumSHA1": "BCNYmf4Ek93G4lk5x3ucNi/lTwA=",
                        "path": "golang.org/x/text/unicode/norm",
                        "revision": "e19ae1496984b1c655b8044a65c0300a3c878dd3",
                        "revisionTime": "2017-12-24T20:31:28Z"
                },
+               {
+                       "checksumSHA1": "RIKH6cQNe0mczH5HxseRIpEYidE=",
+                       "path": "google.golang.org/api/gensupport",
+                       "revision": "de943baf05a022a8f921b544b7827bacaba1aed5",
+                       "revisionTime": "2016-10-20T18:20:02Z"
+               },
+               {
+                       "checksumSHA1": "LxVdu+BwMh3wiugATYeipYXwJIw=",
+                       "path": "google.golang.org/api/googleapi",
+                       "revision": "473217c7f590f56568f04c71c91866d794beb596",
+                       "revisionTime": "2019-11-04T23:03:48Z"
+               },
+               {
+                       "checksumSHA1": "1K0JxrUfDqAB3MyRiU1LKjfHyf4=",
+                       "path": "google.golang.org/api/googleapi/internal/uritemplates",
+                       "revision": "de943baf05a022a8f921b544b7827bacaba1aed5",
+                       "revisionTime": "2016-10-20T18:20:02Z"
+               },
+               {
+                       "checksumSHA1": "8cjsXKNgewlFLlMnJ3N83abOQfA=",
+                       "path": "google.golang.org/api/googleapi/transport",
+                       "revision": "473217c7f590f56568f04c71c91866d794beb596",
+                       "revisionTime": "2019-11-04T23:03:48Z"
+               },
+               {
+                       "checksumSHA1": "FGskZ2MgVCROzzlbrdYPnISEgu0=",
+                       "path": "google.golang.org/api/internal",
+                       "revision": "473217c7f590f56568f04c71c91866d794beb596",
+                       "revisionTime": "2019-11-04T23:03:48Z"
+               },
+               {
+                       "checksumSHA1": "I4Oe5Q+AuaxmN3duL38r2evqGKk=",
+                       "path": "google.golang.org/api/internal/gensupport",
+                       "revision": "473217c7f590f56568f04c71c91866d794beb596",
+                       "revisionTime": "2019-11-04T23:03:48Z"
+               },
+               {
+                       "checksumSHA1": "nN+zggDyWr8HPYzwltMkzJJr1Jc=",
+                       "path": "google.golang.org/api/internal/third_party/uritemplates",
+                       "revision": "473217c7f590f56568f04c71c91866d794beb596",
+                       "revisionTime": "2019-11-04T23:03:48Z"
+               },
+               {
+                       "checksumSHA1": "+ogQsnuO518OACDBVThVxjAoDO8=",
+                       "path": "google.golang.org/api/option",
+                       "revision": "473217c7f590f56568f04c71c91866d794beb596",
+                       "revisionTime": "2019-11-04T23:03:48Z"
+               },
+               {
+                       "checksumSHA1": "lDj30SHq8JTOxBhzjZSPoidYU4U=",
+                       "path": "google.golang.org/api/people/v1",
+                       "revision": "473217c7f590f56568f04c71c91866d794beb596",
+                       "revisionTime": "2019-11-04T23:03:48Z"
+               },
+               {
+                       "checksumSHA1": "Hz0CoIHu+fpeNxQhCtvdJL5KgUg=",
+                       "path": "google.golang.org/api/transport",
+                       "revision": "473217c7f590f56568f04c71c91866d794beb596",
+                       "revisionTime": "2019-11-04T23:03:48Z"
+               },
+               {
+                       "checksumSHA1": "5jHkcf/bO2VsFBm1fdMMQfp3gVY=",
+                       "path": "google.golang.org/api/transport/grpc",
+                       "revision": "473217c7f590f56568f04c71c91866d794beb596",
+                       "revisionTime": "2019-11-04T23:03:48Z"
+               },
+               {
+                       "checksumSHA1": "q628X+HuQrsONyEGovbgsNVWyKo=",
+                       "path": "google.golang.org/api/transport/http",
+                       "revision": "473217c7f590f56568f04c71c91866d794beb596",
+                       "revisionTime": "2019-11-04T23:03:48Z"
+               },
+               {
+                       "checksumSHA1": "DeoMDYdl1dFTGDPYxn346S8kvz4=",
+                       "path": "google.golang.org/api/transport/http/internal/propagation",
+                       "revision": "473217c7f590f56568f04c71c91866d794beb596",
+                       "revisionTime": "2019-11-04T23:03:48Z"
+               },
+               {
+                       "checksumSHA1": "HGXXkbBydog8zRyrzyX2b8OcrUc=",
+                       "path": "google.golang.org/appengine",
+                       "revision": "16bce7d3dc4e458f2f6f56a1349cbbfcdc8a8fdf",
+                       "revisionTime": "2019-10-16T20:46:03Z"
+               },
+               {
+                       "checksumSHA1": "uuHlQoXvEZ9E51No2iwxh1jmd9w=",
+                       "path": "google.golang.org/appengine/internal",
+                       "revision": "16bce7d3dc4e458f2f6f56a1349cbbfcdc8a8fdf",
+                       "revisionTime": "2019-10-16T20:46:03Z"
+               },
+               {
+                       "checksumSHA1": "GyzSDzUj78G9nyNhmlFGg5IufHc=",
+                       "path": "google.golang.org/appengine/internal/app_identity",
+                       "revision": "16bce7d3dc4e458f2f6f56a1349cbbfcdc8a8fdf",
+                       "revisionTime": "2019-10-16T20:46:03Z"
+               },
+               {
+                       "checksumSHA1": "5PakGXEgSbyFptkhGO8MnGf7uH0=",
+                       "path": "google.golang.org/appengine/internal/base",
+                       "revision": "16bce7d3dc4e458f2f6f56a1349cbbfcdc8a8fdf",
+                       "revisionTime": "2019-10-16T20:46:03Z"
+               },
+               {
+                       "checksumSHA1": "3DZ+Ah5hFQb1/nh1+li2VE+kkfk=",
+                       "path": "google.golang.org/appengine/internal/datastore",
+                       "revision": "16bce7d3dc4e458f2f6f56a1349cbbfcdc8a8fdf",
+                       "revisionTime": "2019-10-16T20:46:03Z"
+               },
+               {
+                       "checksumSHA1": "HJQ4JM9YWfwIe4vmAgXC7J/1T3E=",
+                       "path": "google.golang.org/appengine/internal/log",
+                       "revision": "16bce7d3dc4e458f2f6f56a1349cbbfcdc8a8fdf",
+                       "revisionTime": "2019-10-16T20:46:03Z"
+               },
+               {
+                       "checksumSHA1": "rPcVt7Td1StpB6Z9DiShhu753PM=",
+                       "path": "google.golang.org/appengine/internal/modules",
+                       "revision": "16bce7d3dc4e458f2f6f56a1349cbbfcdc8a8fdf",
+                       "revisionTime": "2019-10-16T20:46:03Z"
+               },
+               {
+                       "checksumSHA1": "hApgRLSl7w9XG2waJxdH/o0A398=",
+                       "path": "google.golang.org/appengine/internal/remote_api",
+                       "revision": "16bce7d3dc4e458f2f6f56a1349cbbfcdc8a8fdf",
+                       "revisionTime": "2019-10-16T20:46:03Z"
+               },
+               {
+                       "checksumSHA1": "dU5fToNngC22+3DsebkdYv+T3jE=",
+                       "path": "google.golang.org/genproto/googleapis/rpc/status",
+                       "revision": "919d9bdd9fe6f1a5dd95ce5d5e4cdb8fd3c516d0",
+                       "revisionTime": "2019-10-28T17:36:16Z"
+               },
+               {
+                       "checksumSHA1": "X6j/RZqsMEAqbMTzutcTUE8ae88=",
+                       "path": "google.golang.org/grpc",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "8KrSbWYdhP+hwdJd45wv+hn4Aw0=",
+                       "path": "google.golang.org/grpc/backoff",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "nflETQgLBqUZkh8zIxoYVXQaq+4=",
+                       "path": "google.golang.org/grpc/balancer",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "ZLXafW099RJJQXAtUIExAhjeMFI=",
+                       "path": "google.golang.org/grpc/balancer/base",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "cDvwK2ubxN2/O27pRjKMWEcXUqA=",
+                       "path": "google.golang.org/grpc/balancer/grpclb",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "0vfNsMgaZFc7sKe8S8pnVCNIfsg=",
+                       "path": "google.golang.org/grpc/balancer/grpclb/grpc_lb_v1",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "cE7mFcyGz0F+EnlTZrzLkhprH/4=",
+                       "path": "google.golang.org/grpc/balancer/roundrobin",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "YyTUFAVju8wgb1s/3azC2CeSbfY=",
+                       "path": "google.golang.org/grpc/binarylog/grpc_binarylog_v1",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "y2MH/S0g7vnJorDX+hRZNu7qc+c=",
+                       "path": "google.golang.org/grpc/channelz",
+                       "revision": "590da37e2dfb4705d8ebd9574ce4cb75295d9674",
+                       "revisionTime": "2018-05-29T21:11:52Z"
+               },
+               {
+                       "checksumSHA1": "e0xLHThZgMNcuR7aFuY+pzuQVVU=",
+                       "path": "google.golang.org/grpc/codes",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "UgxkVy6e/BMqXrmS21WmcHtdcd4=",
+                       "path": "google.golang.org/grpc/connectivity",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "5UJiuwNblPiTlsNITE1qNmzPhOw=",
+                       "path": "google.golang.org/grpc/credentials",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "DAGMJ469uZMtSAJxCvb4dciT7Lc=",
+                       "path": "google.golang.org/grpc/credentials/alts",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "oz4z2akZwVszSKy03amrf6P9P5o=",
+                       "path": "google.golang.org/grpc/credentials/alts/internal",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "PTVv5w1hd88sHf2TJbctBasS4ck=",
+                       "path": "google.golang.org/grpc/credentials/alts/internal/authinfo",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "/s6U8ulRJiogFjFygs450dOeIoI=",
+                       "path": "google.golang.org/grpc/credentials/alts/internal/conn",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "l9QCc3IT7X19lLCdmA9CrdTv/4w=",
+                       "path": "google.golang.org/grpc/credentials/alts/internal/handshaker",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "vnI/oN6L9r8gyUIfxrepOyRyt1M=",
+                       "path": "google.golang.org/grpc/credentials/alts/internal/handshaker/service",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "0xSsV5vKH+LsNBq48neyAqJQE5s=",
+                       "path": "google.golang.org/grpc/credentials/alts/internal/proto/grpc_gcp",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "uqTU997XGQ/YxgsFj6Vnwuie4GQ=",
+                       "path": "google.golang.org/grpc/credentials/google",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "hj4XY8K4TjmMZwErpAWaSKFrk2c=",
+                       "path": "google.golang.org/grpc/credentials/internal",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "sFnZthdQsbhUK8DM374dTO521z0=",
+                       "path": "google.golang.org/grpc/credentials/oauth",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "P4QQAmAm6l8rAeOfk6Ljp0qka0k=",
+                       "path": "google.golang.org/grpc/encoding",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "LKKkn7EYA+Do9Qwb2/SUKLFNxoo=",
+                       "path": "google.golang.org/grpc/encoding/proto",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "Qwx9pMTkn1USjW3ZEbbo/mdl4fU=",
+                       "path": "google.golang.org/grpc/grpclb",
+                       "revision": "590da37e2dfb4705d8ebd9574ce4cb75295d9674",
+                       "revisionTime": "2018-05-29T21:11:52Z"
+               },
+               {
+                       "checksumSHA1": "n+8rAQxWcf9LPJat2UHq2uVzH20=",
+                       "path": "google.golang.org/grpc/grpclb/grpc_lb_v1/messages",
+                       "revision": "590da37e2dfb4705d8ebd9574ce4cb75295d9674",
+                       "revisionTime": "2018-05-29T21:11:52Z"
+               },
+               {
+                       "checksumSHA1": "ekrstGhOIsZVKjUih7aWcLEISTQ=",
+                       "path": "google.golang.org/grpc/grpclog",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "6Zx3ZzU/okf+1KAsS6kLsgwWNVQ=",
+                       "path": "google.golang.org/grpc/internal",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "o9H97P0b9GU7912BOEitXnQT2bw=",
+                       "path": "google.golang.org/grpc/internal/backoff",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "k4ITR7VpzDbbf0tRqI6p9xsmPug=",
+                       "path": "google.golang.org/grpc/internal/balancerload",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "J8ebTUPPKc0yf+ER3wJBhPHCht4=",
+                       "path": "google.golang.org/grpc/internal/binarylog",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "iup/lNMZ3GB5wmda8sp14Rrt2QY=",
+                       "path": "google.golang.org/grpc/internal/buffer",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "YtqLJH9Ht2sD5EqAOSqbARDUeXw=",
+                       "path": "google.golang.org/grpc/internal/channelz",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "5dFUCEaPjKwza9kwKqgljp8ckU4=",
+                       "path": "google.golang.org/grpc/internal/envconfig",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "70gndc/uHwyAl3D45zqp7vyHWlo=",
+                       "path": "google.golang.org/grpc/internal/grpcrand",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "psHSfNyU2y9L9zRK+s41e7ScTf4=",
+                       "path": "google.golang.org/grpc/internal/grpcsync",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "q+fLA+VV0jadkfNoeTO7WT7359o=",
+                       "path": "google.golang.org/grpc/internal/resolver/dns",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "hqNexYeP/V1a66ZWiDeFBptNjwY=",
+                       "path": "google.golang.org/grpc/internal/resolver/passthrough",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "wTCshPVAgkVAk+4nvDj5Yj6AFp4=",
+                       "path": "google.golang.org/grpc/internal/syscall",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "ryOd/62SbRr4NN65f4mRauOhEVI=",
+                       "path": "google.golang.org/grpc/internal/transport",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "cDYDzrrgfj9Y45GDWcXXCrRofp0=",
+                       "path": "google.golang.org/grpc/keepalive",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "0OoJw+Wc7+1Ox5nBbwjgqWW8Xpw=",
+                       "path": "google.golang.org/grpc/metadata",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "bk9IupgyMXhqsOBR/dp7ZmRjVEE=",
+                       "path": "google.golang.org/grpc/naming",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "ltPJN8UyzvWN0H0BvkP2AREujgQ=",
+                       "path": "google.golang.org/grpc/peer",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "+uvdsd+Wki49BMFvpwsqnCwPx2w=",
+                       "path": "google.golang.org/grpc/resolver",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "hCvY7ChoHLFFhZcx/iX0uEpQJKU=",
+                       "path": "google.golang.org/grpc/resolver/dns",
+                       "revision": "590da37e2dfb4705d8ebd9574ce4cb75295d9674",
+                       "revisionTime": "2018-05-29T21:11:52Z"
+               },
+               {
+                       "checksumSHA1": "zs9M4xE8Lyg4wvuYvR00XoBxmuw=",
+                       "path": "google.golang.org/grpc/resolver/passthrough",
+                       "revision": "590da37e2dfb4705d8ebd9574ce4cb75295d9674",
+                       "revisionTime": "2018-05-29T21:11:52Z"
+               },
+               {
+                       "checksumSHA1": "S7duOGyPoeGhK3EOhKNyxa/KHtk=",
+                       "path": "google.golang.org/grpc/serviceconfig",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "3ZPGj/HdfLTiK7I3xPdnqELnHdk=",
+                       "path": "google.golang.org/grpc/stats",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "pF8iy9/Pmnt2a8sEAtYtOLQtdHE=",
+                       "path": "google.golang.org/grpc/status",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "HGXDrPBB90iBU4NJ7C1N8MJRkI0=",
+                       "path": "google.golang.org/grpc/tap",
+                       "revision": "6dac0204800b039598a8e1561f66861a6492d833",
+                       "revisionTime": "2019-11-05T19:11:34Z"
+               },
+               {
+                       "checksumSHA1": "W++POptYDWQXnNmDqcw6hg8rgxQ=",
+                       "path": "google.golang.org/grpc/transport",
+                       "revision": "590da37e2dfb4705d8ebd9574ce4cb75295d9674",
+                       "revisionTime": "2018-05-29T21:11:52Z"
+               },
                {
                        "checksumSHA1": "CEFTYXtWmgSh+3Ik1NmDaJcz4E0=",
                        "path": "gopkg.in/check.v1",