Merge branch '16314-testuserdb'
authorTom Clegg <tom@tomclegg.ca>
Fri, 21 Aug 2020 20:39:34 +0000 (16:39 -0400)
committerTom Clegg <tom@tomclegg.ca>
Fri, 21 Aug 2020 20:39:34 +0000 (16:39 -0400)
refs #16314

Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom@tomclegg.ca>

lib/config/config.default.yml
lib/config/export.go
lib/config/generated_config.go
lib/controller/localdb/login.go
lib/controller/localdb/login_testuser.go [new file with mode: 0644]
lib/controller/localdb/login_testuser_test.go [new file with mode: 0644]
sdk/go/arvados/config.go

index a1b471bd229e7f27b0cbd90bf79919f0d7123992..8b6c1ce56ba20704411a3bac792c9b17c29e99bd 100644 (file)
@@ -689,6 +689,16 @@ Clusters:
         ProviderAppID: ""
         ProviderAppSecret: ""
 
+      Test:
+        # Authenticate users listed here in the config file. This
+        # feature is intended to be used in test environments, and
+        # should not be used in production.
+        Enable: false
+        Users:
+          SAMPLE:
+            email: alice@example.com
+            password: xyzzy
+
       # 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 RemoteClusters with Proxy: true)
index f15a2996197804c24face6be71fc4f97afb558f4..5cdc3dae6a11b3dd5464743aee7213a2bd5d21d6 100644 (file)
@@ -170,6 +170,9 @@ var whitelist = map[string]bool{
        "Login.SSO.Enable":                             true,
        "Login.SSO.ProviderAppID":                      false,
        "Login.SSO.ProviderAppSecret":                  false,
+       "Login.Test":                                   true,
+       "Login.Test.Enable":                            true,
+       "Login.Test.Users":                             false,
        "Mail":                                         true,
        "Mail.EmailFrom":                               false,
        "Mail.IssueReporterEmailFrom":                  false,
index 8e42eb350516d172cec46c99fc0c163dcaa4fb46..74879d23999a68ab1fb0406f13c5d067955711e8 100644 (file)
@@ -695,6 +695,16 @@ Clusters:
         ProviderAppID: ""
         ProviderAppSecret: ""
 
+      Test:
+        # Authenticate users listed here in the config file. This
+        # feature is intended to be used in test environments, and
+        # should not be used in production.
+        Enable: false
+        Users:
+          SAMPLE:
+            email: alice@example.com
+            password: xyzzy
+
       # 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 RemoteClusters with Proxy: true)
index ee1ea56924c5700d25e43262347d1045d534ca5c..12674148426133fa97f1ef786b38cf6803b1c376 100644 (file)
@@ -33,8 +33,13 @@ func chooseLoginController(cluster *arvados.Cluster, railsProxy *railsProxy) log
        wantSSO := cluster.Login.SSO.Enable
        wantPAM := cluster.Login.PAM.Enable
        wantLDAP := cluster.Login.LDAP.Enable
+       wantTest := cluster.Login.Test.Enable
        switch {
-       case wantGoogle && !wantOpenIDConnect && !wantSSO && !wantPAM && !wantLDAP:
+       case 1 != countTrue(wantGoogle, wantOpenIDConnect, wantSSO, wantPAM, wantLDAP, wantTest):
+               return errorLoginController{
+                       error: errors.New("configuration problem: exactly one of Login.Google, Login.OpenIDConnect, Login.SSO, Login.PAM, Login.LDAP, and Login.Test must be enabled"),
+               }
+       case wantGoogle:
                return &oidcLoginController{
                        Cluster:            cluster,
                        RailsProxy:         railsProxy,
@@ -45,7 +50,7 @@ func chooseLoginController(cluster *arvados.Cluster, railsProxy *railsProxy) log
                        EmailClaim:         "email",
                        EmailVerifiedClaim: "email_verified",
                }
-       case !wantGoogle && wantOpenIDConnect && !wantSSO && !wantPAM && !wantLDAP:
+       case wantOpenIDConnect:
                return &oidcLoginController{
                        Cluster:            cluster,
                        RailsProxy:         railsProxy,
@@ -56,17 +61,29 @@ func chooseLoginController(cluster *arvados.Cluster, railsProxy *railsProxy) log
                        EmailVerifiedClaim: cluster.Login.OpenIDConnect.EmailVerifiedClaim,
                        UsernameClaim:      cluster.Login.OpenIDConnect.UsernameClaim,
                }
-       case !wantGoogle && !wantOpenIDConnect && wantSSO && !wantPAM && !wantLDAP:
+       case wantSSO:
                return &ssoLoginController{railsProxy}
-       case !wantGoogle && !wantOpenIDConnect && !wantSSO && wantPAM && !wantLDAP:
+       case wantPAM:
                return &pamLoginController{Cluster: cluster, RailsProxy: railsProxy}
-       case !wantGoogle && !wantOpenIDConnect && !wantSSO && !wantPAM && wantLDAP:
+       case wantLDAP:
                return &ldapLoginController{Cluster: cluster, RailsProxy: railsProxy}
+       case wantTest:
+               return &testLoginController{Cluster: cluster, RailsProxy: railsProxy}
        default:
                return errorLoginController{
-                       error: errors.New("configuration problem: exactly one of Login.Google, Login.OpenIDConnect, Login.SSO, Login.PAM, and Login.LDAP must be enabled"),
+                       error: errors.New("BUG: missing case in login controller setup switch"),
+               }
+       }
+}
+
+func countTrue(vals ...bool) int {
+       n := 0
+       for _, val := range vals {
+               if val {
+                       n++
                }
        }
+       return n
 }
 
 // Login and Logout are passed through to the wrapped railsProxy;
diff --git a/lib/controller/localdb/login_testuser.go b/lib/controller/localdb/login_testuser.go
new file mode 100644 (file)
index 0000000..5a3d803
--- /dev/null
@@ -0,0 +1,45 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package localdb
+
+import (
+       "context"
+       "errors"
+       "fmt"
+
+       "git.arvados.org/arvados.git/lib/controller/rpc"
+       "git.arvados.org/arvados.git/sdk/go/arvados"
+       "git.arvados.org/arvados.git/sdk/go/ctxlog"
+       "github.com/sirupsen/logrus"
+)
+
+type testLoginController struct {
+       Cluster    *arvados.Cluster
+       RailsProxy *railsProxy
+}
+
+func (ctrl *testLoginController) Logout(ctx context.Context, opts arvados.LogoutOptions) (arvados.LogoutResponse, error) {
+       return noopLogout(ctrl.Cluster, opts)
+}
+
+func (ctrl *testLoginController) Login(ctx context.Context, opts arvados.LoginOptions) (arvados.LoginResponse, error) {
+       return arvados.LoginResponse{}, errors.New("interactive login is not available")
+}
+
+func (ctrl *testLoginController) UserAuthenticate(ctx context.Context, opts arvados.UserAuthenticateOptions) (arvados.APIClientAuthorization, error) {
+       for username, user := range ctrl.Cluster.Login.Test.Users {
+               if (opts.Username == username || opts.Username == user.Email) && opts.Password == user.Password {
+                       ctxlog.FromContext(ctx).WithFields(logrus.Fields{
+                               "username": username,
+                               "email":    user.Email,
+                       }).Debug("test authentication succeeded")
+                       return createAPIClientAuthorization(ctx, ctrl.RailsProxy, ctrl.Cluster.SystemRootToken, rpc.UserSessionAuthInfo{
+                               Username: username,
+                               Email:    user.Email,
+                       })
+               }
+       }
+       return arvados.APIClientAuthorization{}, fmt.Errorf("authentication failed for user %q with password len=%d", opts.Username, len(opts.Password))
+}
diff --git a/lib/controller/localdb/login_testuser_test.go b/lib/controller/localdb/login_testuser_test.go
new file mode 100644 (file)
index 0000000..d2d651e
--- /dev/null
@@ -0,0 +1,94 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+package localdb
+
+import (
+       "context"
+
+       "git.arvados.org/arvados.git/lib/config"
+       "git.arvados.org/arvados.git/lib/controller/rpc"
+       "git.arvados.org/arvados.git/lib/ctrlctx"
+       "git.arvados.org/arvados.git/sdk/go/arvados"
+       "git.arvados.org/arvados.git/sdk/go/arvadostest"
+       "git.arvados.org/arvados.git/sdk/go/ctxlog"
+       "github.com/jmoiron/sqlx"
+       check "gopkg.in/check.v1"
+)
+
+var _ = check.Suite(&TestUserSuite{})
+
+type TestUserSuite struct {
+       cluster  *arvados.Cluster
+       ctrl     *testLoginController
+       railsSpy *arvadostest.Proxy
+       db       *sqlx.DB
+
+       // transaction context
+       ctx      context.Context
+       rollback func() error
+}
+
+func (s *TestUserSuite) SetUpSuite(c *check.C) {
+       cfg, err := config.NewLoader(nil, ctxlog.TestLogger(c)).Load()
+       c.Assert(err, check.IsNil)
+       s.cluster, err = cfg.GetCluster("")
+       c.Assert(err, check.IsNil)
+       s.cluster.Login.Test.Enable = true
+       s.cluster.Login.Test.Users = map[string]arvados.TestUser{
+               "valid": {Email: "valid@example.com", Password: "v@l1d"},
+       }
+       s.railsSpy = arvadostest.NewProxy(c, s.cluster.Services.RailsAPI)
+       s.ctrl = &testLoginController{
+               Cluster:    s.cluster,
+               RailsProxy: rpc.NewConn(s.cluster.ClusterID, s.railsSpy.URL, true, rpc.PassthroughTokenProvider),
+       }
+       s.db = arvadostest.DB(c, s.cluster)
+}
+
+func (s *TestUserSuite) SetUpTest(c *check.C) {
+       tx, err := s.db.Beginx()
+       c.Assert(err, check.IsNil)
+       s.ctx = ctrlctx.NewWithTransaction(context.Background(), tx)
+       s.rollback = tx.Rollback
+}
+
+func (s *TestUserSuite) TearDownTest(c *check.C) {
+       if s.rollback != nil {
+               s.rollback()
+       }
+}
+
+func (s *TestUserSuite) TestLogin(c *check.C) {
+       for _, trial := range []struct {
+               success  bool
+               username string
+               password string
+       }{
+               {false, "foo", "bar"},
+               {false, "", ""},
+               {false, "valid", ""},
+               {false, "", "v@l1d"},
+               {true, "valid", "v@l1d"},
+               {true, "valid@example.com", "v@l1d"},
+       } {
+               c.Logf("=== %#v", trial)
+               resp, err := s.ctrl.UserAuthenticate(s.ctx, arvados.UserAuthenticateOptions{
+                       Username: trial.username,
+                       Password: trial.password,
+               })
+               if trial.success {
+                       c.Check(err, check.IsNil)
+                       c.Check(resp.APIToken, check.Not(check.Equals), "")
+                       c.Check(resp.UUID, check.Matches, `zzzzz-gj3su-.*`)
+                       c.Check(resp.Scopes, check.DeepEquals, []string{"all"})
+
+                       authinfo := getCallbackAuthInfo(c, s.railsSpy)
+                       c.Check(authinfo.Email, check.Equals, "valid@example.com")
+                       c.Check(authinfo.AlternateEmails, check.DeepEquals, []string(nil))
+               } else {
+                       c.Check(err, check.ErrorMatches, `authentication failed.*`)
+               }
+       }
+}
index 41c20c8db2ee71cf4c4a024e7d1d73b72878a098..c87f880e5e56365e4bbec06c211a72cee9e0ee7c 100644 (file)
@@ -177,6 +177,10 @@ type Cluster struct {
                        ProviderAppID     string
                        ProviderAppSecret string
                }
+               Test struct {
+                       Enable bool
+                       Users  map[string]TestUser
+               }
                LoginCluster       string
                RemoteTokenRefresh Duration
        }
@@ -330,6 +334,11 @@ type Service struct {
        ExternalURL  URL
 }
 
+type TestUser struct {
+       Email    string
+       Password string
+}
+
 // URL is a url.URL that is also usable as a JSON key/value.
 type URL url.URL