From 20cea53c48260a0cec3d588c2af520b27433b8c1 Mon Sep 17 00:00:00 2001 From: Tom Clegg Date: Tue, 18 Aug 2020 16:37:58 -0400 Subject: [PATCH] 16314: Support "Test" authentication type. Arvados-DCO-1.1-Signed-off-by: Tom Clegg --- go.sum | 2 + lib/config/config.default.yml | 10 ++ lib/config/generated_config.go | 10 ++ lib/controller/localdb/login.go | 15 +-- lib/controller/localdb/login_testuser.go | 45 +++++++++ lib/controller/localdb/login_testuser_test.go | 94 +++++++++++++++++++ sdk/go/arvados/config.go | 9 ++ 7 files changed, 179 insertions(+), 6 deletions(-) create mode 100644 lib/controller/localdb/login_testuser.go create mode 100644 lib/controller/localdb/login_testuser_test.go diff --git a/go.sum b/go.sum index 2565964e7d..ac5c03fc83 100644 --- a/go.sum +++ b/go.sum @@ -78,6 +78,7 @@ github.com/go-ldap/ldap v3.0.3+incompatible h1:HTeSZO8hWMS1Rgb2Ziku6b8a7qRIZZMHj github.com/go-ldap/ldap v3.0.3+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo= @@ -226,6 +227,7 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowK golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= diff --git a/lib/config/config.default.yml b/lib/config/config.default.yml index 270d4045b5..cac0ac61d7 100644 --- a/lib/config/config.default.yml +++ b/lib/config/config.default.yml @@ -686,6 +686,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) diff --git a/lib/config/generated_config.go b/lib/config/generated_config.go index 0241673aa5..e3bd13fd26 100644 --- a/lib/config/generated_config.go +++ b/lib/config/generated_config.go @@ -692,6 +692,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) diff --git a/lib/controller/localdb/login.go b/lib/controller/localdb/login.go index ee1ea56924..34da4933af 100644 --- a/lib/controller/localdb/login.go +++ b/lib/controller/localdb/login.go @@ -33,8 +33,9 @@ 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 wantGoogle && !wantOpenIDConnect && !wantSSO && !wantPAM && !wantLDAP && !wantTest: return &oidcLoginController{ Cluster: cluster, RailsProxy: railsProxy, @@ -45,7 +46,7 @@ func chooseLoginController(cluster *arvados.Cluster, railsProxy *railsProxy) log EmailClaim: "email", EmailVerifiedClaim: "email_verified", } - case !wantGoogle && wantOpenIDConnect && !wantSSO && !wantPAM && !wantLDAP: + case !wantGoogle && wantOpenIDConnect && !wantSSO && !wantPAM && !wantLDAP && !wantTest: return &oidcLoginController{ Cluster: cluster, RailsProxy: railsProxy, @@ -56,15 +57,17 @@ 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 !wantGoogle && !wantOpenIDConnect && wantSSO && !wantPAM && !wantLDAP && !wantTest: return &ssoLoginController{railsProxy} - case !wantGoogle && !wantOpenIDConnect && !wantSSO && wantPAM && !wantLDAP: + case !wantGoogle && !wantOpenIDConnect && !wantSSO && wantPAM && !wantLDAP && !wantTest: return &pamLoginController{Cluster: cluster, RailsProxy: railsProxy} - case !wantGoogle && !wantOpenIDConnect && !wantSSO && !wantPAM && wantLDAP: + case !wantGoogle && !wantOpenIDConnect && !wantSSO && !wantPAM && wantLDAP && !wantTest: return &ldapLoginController{Cluster: cluster, RailsProxy: railsProxy} + case !wantGoogle && !wantOpenIDConnect && !wantSSO && !wantPAM && !wantLDAP && 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("configuration problem: exactly one of Login.Google, Login.OpenIDConnect, Login.SSO, Login.PAM, Login.LDAP, and Login.Test must be enabled"), } } } diff --git a/lib/controller/localdb/login_testuser.go b/lib/controller/localdb/login_testuser.go new file mode 100644 index 0000000000..5a3d803b89 --- /dev/null +++ b/lib/controller/localdb/login_testuser.go @@ -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 index 0000000000..d2d651e205 --- /dev/null +++ b/lib/controller/localdb/login_testuser_test.go @@ -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.*`) + } + } +} diff --git a/sdk/go/arvados/config.go b/sdk/go/arvados/config.go index c21addbba9..e123671d4a 100644 --- a/sdk/go/arvados/config.go +++ b/sdk/go/arvados/config.go @@ -176,6 +176,10 @@ type Cluster struct { ProviderAppID string ProviderAppSecret string } + Test struct { + Enable bool + Users map[string]TestUser + } LoginCluster string RemoteTokenRefresh Duration } @@ -329,6 +333,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 -- 2.30.2