15107: Add config option to skip alternate email addrs / People API.
authorTom Clegg <tclegg@veritasgenetics.com>
Tue, 12 Nov 2019 16:32:57 +0000 (11:32 -0500)
committerTom Clegg <tclegg@veritasgenetics.com>
Tue, 12 Nov 2019 16:32:57 +0000 (11:32 -0500)
Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tclegg@veritasgenetics.com>

lib/config/config.default.yml
lib/config/export.go
lib/config/generated_config.go
lib/controller/localdb/login.go
lib/controller/localdb/login_test.go
sdk/go/arvados/config.go

index 0cf7b5e6932517d6d425083068a39d7ce086426a..81c36b9bfbe2b00626403cfde5cbdfd2894c34f9 100644 (file)
@@ -511,6 +511,12 @@ Clusters:
       GoogleClientID: ""
       GoogleClientSecret: ""
 
+      # Allow users to log in to existing accounts using any verified
+      # email address listed by their Google account. If true, the
+      # Google People API must be enabled in order for Google login to
+      # work. If false, only the primary email address will be used.
+      GoogleAlternateEmailAddresses: true
+
       # The cluster ID to delegate the user database.  When set,
       # logins on this cluster will be redirected to the login cluster
       # (login cluster must appear in RemoteHosts with Proxy: true)
index 0dd90ff65c7d0bf2ab86bacafed780d349bc8498..7adacab4c8df392ef35f57c6ecb6d1cbe8f7d749 100644 (file)
@@ -132,6 +132,7 @@ var whitelist = map[string]bool{
        "Login":                                        true,
        "Login.GoogleClientID":                         false,
        "Login.GoogleClientSecret":                     false,
+       "Login.GoogleAlternateEmailAddresses":          false,
        "Login.ProviderAppID":                          false,
        "Login.ProviderAppSecret":                      false,
        "Login.LoginCluster":                           true,
index 4dc00cf62845db0248dd9cb2e409692c15a0f44f..68dea169f843072aa33c4f6905e6651011c57fe4 100644 (file)
@@ -517,6 +517,12 @@ Clusters:
       GoogleClientID: ""
       GoogleClientSecret: ""
 
+      # Allow users to log in to existing accounts using any verified
+      # email address listed by their Google account. If true, the
+      # Google People API must be enabled in order for Google login to
+      # work. If false, only the primary email address will be used.
+      GoogleAlternateEmailAddresses: true
+
       # The cluster ID to delegate the user database.  When set,
       # logins on this cluster will be redirected to the login cluster
       # (login cluster must appear in RemoteHosts with Proxy: true)
index ddd342699a47d792d6ad131dec86657b46fba15e..df5259a846bbac112c4ee3fa30c7dc5b7f03a60f 100644 (file)
@@ -110,7 +110,7 @@ func (ctrl *googleLoginController) Login(ctx context.Context, cluster *arvados.C
                if err != nil {
                        return ctrl.loginError(fmt.Errorf("error verifying ID token: %s", err))
                }
-               authinfo, err := ctrl.getAuthInfo(ctx, conf, oauth2Token, idToken)
+               authinfo, err := ctrl.getAuthInfo(ctx, cluster, conf, oauth2Token, idToken)
                if err != nil {
                        return ctrl.loginError(err)
                }
@@ -126,7 +126,7 @@ func (ctrl *googleLoginController) Login(ctx context.Context, cluster *arvados.C
 // 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) {
+func (ctrl *googleLoginController) getAuthInfo(ctx context.Context, cluster *arvados.Cluster, 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
 
@@ -149,6 +149,13 @@ func (ctrl *googleLoginController) getAuthInfo(ctx context.Context, conf *oauth2
                ret.Email = claims.Email
        }
 
+       if !cluster.Login.GoogleAlternateEmailAddresses {
+               if ret.Email == "" {
+                       return nil, fmt.Errorf("cannot log in with unverified email address %q", claims.Email)
+               }
+               return &ret, nil
+       }
+
        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)
@@ -159,12 +166,12 @@ func (ctrl *googleLoginController) getAuthInfo(ctx context.Context, conf *oauth2
        }
        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
+               if strings.Contains(err.Error(), "Error 403") && strings.Contains(err.Error(), "accessNotConfigured") {
+                       // Log the original API error, but display
+                       // only the "fix config" advice to the user.
+                       ctxlog.FromContext(ctx).WithError(err).WithField("email", ret.Email).Error("People API is not enabled")
+                       return nil, errors.New("configuration error: Login.GoogleAlternateEmailAddresses is true, but Google People API is not enabled")
                } else {
-                       // Unexpected error, or no email to fall back on.
                        return nil, fmt.Errorf("error getting profile info from People API: %s", err)
                }
        }
index e36571ef168ad1ae52b2386cf0371e72279176ce..e722d04d5e127df44b8197212442518906f99670 100644 (file)
@@ -209,6 +209,39 @@ func (s *LoginSuite) TestGoogleLogin_InvalidState(c *check.C) {
        c.Check(resp.HTML.String(), check.Matches, `(?ms).*invalid OAuth2 state.*`)
 }
 
+func (s *LoginSuite) setupPeopleAPIError(c *check.C) {
+       s.fakePeopleAPI = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+               w.WriteHeader(http.StatusForbidden)
+               fmt.Fprintln(w, `Error 403: accessNotConfigured`)
+       }))
+       s.localdb.googleLoginController.peopleAPIBasePath = s.fakePeopleAPI.URL
+}
+
+func (s *LoginSuite) TestGoogleLogin_PeopleAPIDisabled(c *check.C) {
+       s.cluster.Login.GoogleAlternateEmailAddresses = false
+       s.authEmail = "joe.smith@primary.example.com"
+       s.setupPeopleAPIError(c)
+       state := s.startLogin(c)
+       _, err := s.localdb.Login(context.Background(), arvados.LoginOptions{
+               Code:  s.validCode,
+               State: state,
+       })
+       c.Check(err, check.IsNil)
+       authinfo := s.getCallbackAuthInfo(c)
+       c.Check(authinfo.Email, check.Equals, "joe.smith@primary.example.com")
+}
+
+func (s *LoginSuite) TestGoogleLogin_PeopleAPIError(c *check.C) {
+       s.setupPeopleAPIError(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.RedirectLocation, check.Equals, "")
+}
+
 func (s *LoginSuite) TestGoogleLogin_Success(c *check.C) {
        state := s.startLogin(c)
        resp, err := s.localdb.Login(context.Background(), arvados.LoginOptions{
index 6ec8f345d75f430ecd4fe8ffab4bfa665f569770..805efb7db287d61a9049cf0abf69ac61236989d4 100644 (file)
@@ -132,12 +132,13 @@ type Cluster struct {
                Repositories string
        }
        Login struct {
-               GoogleClientID     string
-               GoogleClientSecret string
-               ProviderAppID      string
-               ProviderAppSecret  string
-               LoginCluster       string
-               RemoteTokenRefresh Duration
+               GoogleClientID                string
+               GoogleClientSecret            string
+               GoogleAlternateEmailAddresses bool
+               ProviderAppID                 string
+               ProviderAppSecret             string
+               LoginCluster                  string
+               RemoteTokenRefresh            Duration
        }
        Mail struct {
                MailchimpAPIKey                string