From 5763409818cd2ab68c0f59b6a97d0c3df090907f Mon Sep 17 00:00:00 2001 From: Tom Clegg Date: Thu, 7 May 2020 00:38:23 -0400 Subject: [PATCH] 15881: Add LDAP authentication option. Arvados-DCO-1.1-Signed-off-by: Tom Clegg --- go.mod | 2 + go.sum | 4 + lib/config/config.default.yml | 57 +++++++ lib/config/export.go | 14 ++ lib/config/generated_config.go | 57 +++++++ lib/controller/localdb/login.go | 34 +++- lib/controller/localdb/login_ldap.go | 150 ++++++++++++++++++ .../localdb/login_ldap_docker_test.go | 46 ++++++ ...cker_test.sh => login_ldap_docker_test.sh} | 87 ++++++++-- lib/controller/localdb/login_pam.go | 29 +--- .../localdb/login_pam_docker_test.go | 23 --- sdk/go/arvados/config.go | 15 ++ .../controllers/user_sessions_controller.rb | 2 + 13 files changed, 456 insertions(+), 64 deletions(-) create mode 100644 lib/controller/localdb/login_ldap.go create mode 100644 lib/controller/localdb/login_ldap_docker_test.go rename lib/controller/localdb/{login_pam_docker_test.sh => login_ldap_docker_test.sh} (66%) delete mode 100644 lib/controller/localdb/login_pam_docker_test.go diff --git a/go.mod b/go.mod index 34b7e07790..482c6971d3 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/fsnotify/fsnotify v1.4.9 github.com/ghodss/yaml v1.0.0 github.com/gliderlabs/ssh v0.2.2 // indirect + github.com/go-ldap/ldap v3.0.3+incompatible github.com/gogo/protobuf v1.1.1 github.com/gorilla/context v1.1.1 // indirect github.com/gorilla/mux v1.6.1-0.20180107155708-5bbbb5b2b572 @@ -57,6 +58,7 @@ require ( golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 golang.org/x/sys v0.0.0-20191105231009-c1f44814a5cd google.golang.org/api v0.13.0 + gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d // indirect gopkg.in/check.v1 v1.0.0-20161208181325-20d25e280405 gopkg.in/square/go-jose.v2 v2.3.1 gopkg.in/src-d/go-billy.v4 v4.0.1 diff --git a/go.sum b/go.sum index 03b2f77b6d..a92b3c11a4 100644 --- a/go.sum +++ b/go.sum @@ -64,6 +64,8 @@ github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-ldap/ldap v3.0.3+incompatible h1:HTeSZO8hWMS1Rgb2Ziku6b8a7qRIZZMHjsvuZyatzwk= +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-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -249,6 +251,8 @@ google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZi google.golang.org/grpc v1.20.1 h1:Hz2g2wirWK7H0qIIhGIqRGTuMwTE8HEKFnDZZ7lm9NU= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM= +gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20161208181325-20d25e280405 h1:829vOVxxusYHC+IqBtkX5mbKtsY9fheQiQn0MZRVLfQ= gopkg.in/check.v1 v1.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/lib/config/config.default.yml b/lib/config/config.default.yml index d4870919ea..a06526fd67 100644 --- a/lib/config/config.default.yml +++ b/lib/config/config.default.yml @@ -573,6 +573,63 @@ Clusters: # accounts. PAMDefaultEmailDomain: "" + LDAP: + # Use an LDAP service to authenticate users. + Enable: false + + # Server URL, like "ldap://ldapserver.example.com:389". + URL: "ldap://ldap:389" + + # Use StartTLS upon connecting to the server. + StartTLS: true + + # Skip TLS certificate name verification. + InsecureTLS: false + + # Strip the @domain part if a user supplies an email-style + # username with this domain. If "*", strip any user-provided + # domain. If "", never strip the domain part. Example: + # "example.com" + StripDomain: "" + + # If, after applying StripDomain, the username contains no "@" + # character, append this domain to form an email-style + # username. Example: "example.com" + AppendDomain: "" + + # The LDAP attribute to filter on when looking up a username + # (after applying StripDomain and AppendDomain). + SearchAttribute: uid + + # Bind with this username (DN or UPN) and password when + # looking up the user record. + # + # Example user: "cn=admin,dc=example,dc=com" + SearchBindUser: "" + SearchBindPassword: "" + + # Directory base for username lookup. Example: + # "ou=Users,dc=example,dc=com" + SearchBase: "" + + # Additional filters for username lookup. Special characters + # in assertion values must be escaped (see RFC4515). Example: + # "(objectClass=person)" + SearchFilters: "" + + # LDAP attribute to use as the user's email address. + # + # Important: This must not be an attribute whose value can be + # edited in the directory by the users themselves. Otherwise, + # users can take over other users' Arvados accounts trivially + # (email address is the primary key for Arvados accounts.) + EmailAttribute: mail + + # LDAP attribute to use as the preferred Arvados username. If + # no value is found (or this config is empty) the username + # originally supplied by the user will be used. + UsernameAttribute: uid + # 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/export.go b/lib/config/export.go index ded03fc303..323043fbe7 100644 --- a/lib/config/export.go +++ b/lib/config/export.go @@ -139,6 +139,20 @@ var whitelist = map[string]bool{ "Login.PAMDefaultEmailDomain": false, "Login.ProviderAppID": false, "Login.ProviderAppSecret": false, + "Login.LDAP": true, + "Login.LDAP.AppendDomain": false, + "Login.LDAP.EmailAttribute": false, + "Login.LDAP.Enable": true, + "Login.LDAP.InsecureTLS": false, + "Login.LDAP.SearchAttribute": false, + "Login.LDAP.SearchBase": false, + "Login.LDAP.SearchBindUser": false, + "Login.LDAP.SearchBindPassword": false, + "Login.LDAP.SearchFilters": false, + "Login.LDAP.StartTLS": false, + "Login.LDAP.StripDomain": false, + "Login.LDAP.URL": false, + "Login.LDAP.UsernameAttribute": false, "Login.LoginCluster": true, "Login.RemoteTokenRefresh": true, "Mail": true, diff --git a/lib/config/generated_config.go b/lib/config/generated_config.go index 42707396dd..e5ec035c67 100644 --- a/lib/config/generated_config.go +++ b/lib/config/generated_config.go @@ -579,6 +579,63 @@ Clusters: # accounts. PAMDefaultEmailDomain: "" + LDAP: + # Use an LDAP service to authenticate users. + Enable: false + + # Server URL, like "ldap://ldapserver.example.com:389". + URL: "ldap://ldap:389" + + # Use StartTLS upon connecting to the server. + StartTLS: true + + # Skip TLS certificate name verification. + InsecureTLS: false + + # Strip the @domain part if a user supplies an email-style + # username with this domain. If "*", strip any user-provided + # domain. If "", never strip the domain part. Example: + # "example.com" + StripDomain: "" + + # If, after applying StripDomain, the username contains no "@" + # character, append this domain to form an email-style + # username. Example: "example.com" + AppendDomain: "" + + # The LDAP attribute to filter on when looking up a username + # (after applying StripDomain and AppendDomain). + SearchAttribute: uid + + # Bind with this username (DN or UPN) and password when + # looking up the user record. + # + # Example user: "cn=admin,dc=example,dc=com" + SearchBindUser: "" + SearchBindPassword: "" + + # Directory base for username lookup. Example: + # "ou=Users,dc=example,dc=com" + SearchBase: "" + + # Additional filters for username lookup. Special characters + # in assertion values must be escaped (see RFC4515). Example: + # "(objectClass=person)" + SearchFilters: "" + + # LDAP attribute to use as the user's email address. + # + # Important: This must not be an attribute whose value can be + # edited in the directory by the users themselves. Otherwise, + # users can take over other users' Arvados accounts trivially + # (email address is the primary key for Arvados accounts.) + EmailAttribute: mail + + # LDAP attribute to use as the preferred Arvados username. If + # no value is found (or this config is empty) the username + # originally supplied by the user will be used. + UsernameAttribute: uid + # 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 ae59849993..8cba3b6fa1 100644 --- a/lib/controller/localdb/login.go +++ b/lib/controller/localdb/login.go @@ -8,8 +8,11 @@ import ( "context" "errors" "net/http" + "net/url" + "git.arvados.org/arvados.git/lib/controller/rpc" "git.arvados.org/arvados.git/sdk/go/arvados" + "git.arvados.org/arvados.git/sdk/go/auth" "git.arvados.org/arvados.git/sdk/go/httpserver" ) @@ -23,16 +26,19 @@ func chooseLoginController(cluster *arvados.Cluster, railsProxy *railsProxy) log wantGoogle := cluster.Login.GoogleClientID != "" wantSSO := cluster.Login.ProviderAppID != "" wantPAM := cluster.Login.PAM + wantLDAP := cluster.Login.LDAP.Enable switch { - case wantGoogle && !wantSSO && !wantPAM: + case wantGoogle && !wantSSO && !wantPAM && !wantLDAP: return &googleLoginController{Cluster: cluster, RailsProxy: railsProxy} - case !wantGoogle && wantSSO && !wantPAM: + case !wantGoogle && wantSSO && !wantPAM && !wantLDAP: return &ssoLoginController{railsProxy} - case !wantGoogle && !wantSSO && wantPAM: + case !wantGoogle && !wantSSO && wantPAM && !wantLDAP: return &pamLoginController{Cluster: cluster, RailsProxy: railsProxy} + case !wantGoogle && !wantSSO && !wantPAM && wantLDAP: + return &ldapLoginController{Cluster: cluster, RailsProxy: railsProxy} default: return errorLoginController{ - error: errors.New("configuration problem: exactly one of Login.GoogleClientID, Login.ProviderAppID, or Login.PAM must be configured"), + error: errors.New("configuration problem: exactly one of Login.GoogleClientID, Login.ProviderAppID, Login.PAM, or Login.LDAP.Enable must be configured"), } } } @@ -68,3 +74,23 @@ func noopLogout(cluster *arvados.Cluster, opts arvados.LogoutOptions) (arvados.L } return arvados.LogoutResponse{RedirectLocation: target}, nil } + +func createAPIClientAuthorization(ctx context.Context, conn *rpc.Conn, rootToken string, authinfo rpc.UserSessionAuthInfo) (arvados.APIClientAuthorization, error) { + ctxRoot := auth.NewContext(ctx, &auth.Credentials{Tokens: []string{rootToken}}) + resp, err := conn.UserSessionCreate(ctxRoot, rpc.UserSessionCreateOptions{ + // Send a fake ReturnTo value instead of the caller's + // opts.ReturnTo. We won't follow the resulting + // redirect target anyway. + ReturnTo: ",https://none.invalid", + AuthInfo: authinfo, + }) + if err != nil { + return arvados.APIClientAuthorization{}, err + } + target, err := url.Parse(resp.RedirectLocation) + if err != nil { + return arvados.APIClientAuthorization{}, err + } + token := target.Query().Get("api_token") + return conn.APIClientAuthorizationCurrent(auth.NewContext(ctx, auth.NewCredentials(token)), arvados.GetOptions{}) +} diff --git a/lib/controller/localdb/login_ldap.go b/lib/controller/localdb/login_ldap.go new file mode 100644 index 0000000000..44e42ac405 --- /dev/null +++ b/lib/controller/localdb/login_ldap.go @@ -0,0 +1,150 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +package localdb + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "net" + "net/http" + "strings" + + "git.arvados.org/arvados.git/lib/controller/rpc" + "git.arvados.org/arvados.git/sdk/go/arvados" + "git.arvados.org/arvados.git/sdk/go/ctxlog" + "git.arvados.org/arvados.git/sdk/go/httpserver" + "github.com/go-ldap/ldap" +) + +type ldapLoginController struct { + Cluster *arvados.Cluster + RailsProxy *railsProxy +} + +func (ctrl *ldapLoginController) Logout(ctx context.Context, opts arvados.LogoutOptions) (arvados.LogoutResponse, error) { + return noopLogout(ctrl.Cluster, opts) +} + +func (ctrl *ldapLoginController) Login(ctx context.Context, opts arvados.LoginOptions) (arvados.LoginResponse, error) { + return arvados.LoginResponse{}, errors.New("interactive login is not available") +} + +func (ctrl *ldapLoginController) UserAuthenticate(ctx context.Context, opts arvados.UserAuthenticateOptions) (arvados.APIClientAuthorization, error) { + log := ctxlog.FromContext(ctx) + conf := ctrl.Cluster.Login.LDAP + errFailed := httpserver.ErrorWithStatus(fmt.Errorf("LDAP: Authentication failure (with username %q and password)", opts.Username), http.StatusUnauthorized) + + if opts.Password == "" { + log.WithField("username", opts.Username).Error("refusing to authenticate with empty password") + return arvados.APIClientAuthorization{}, errFailed + } + + log = log.WithField("URL", conf.URL.String()) + l, err := ldap.DialURL(conf.URL.String()) + if err != nil { + log.WithError(err).Error("ldap connection failed") + return arvados.APIClientAuthorization{}, err + } + defer l.Close() + + if conf.StartTLS { + var tlsconfig tls.Config + if conf.InsecureTLS { + tlsconfig.InsecureSkipVerify = true + } else { + if host, _, err := net.SplitHostPort(conf.URL.Host); err != nil { + // Assume SplitHostPort error means + // port was not specified + tlsconfig.ServerName = conf.URL.Host + } else { + tlsconfig.ServerName = host + } + } + err = l.StartTLS(&tlsconfig) + if err != nil { + log.WithError(err).Error("ldap starttls failed") + return arvados.APIClientAuthorization{}, err + } + } + + username := opts.Username + if at := strings.Index(username, "@"); at >= 0 { + if conf.StripDomain == "*" || strings.ToLower(conf.StripDomain) == strings.ToLower(username[at+1:]) { + username = username[:at] + } + } + if conf.AppendDomain != "" && !strings.Contains(username, "@") { + username = username + "@" + conf.AppendDomain + } + + if conf.SearchBindUser != "" { + err = l.Bind(conf.SearchBindUser, conf.SearchBindPassword) + if err != nil { + log.WithError(err).WithField("user", conf.SearchBindUser).Error("ldap authentication failed") + return arvados.APIClientAuthorization{}, err + } + } + + if conf.SearchAttribute == "" { + return arvados.APIClientAuthorization{}, errors.New("config error: must provide SearchAttribute") + } + + search := fmt.Sprintf("(&%s(%s=%s))", conf.SearchFilters, ldap.EscapeFilter(conf.SearchAttribute), ldap.EscapeFilter(username)) + log = log.WithField("search", search) + req := ldap.NewSearchRequest( + conf.SearchBase, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false, + search, + []string{"DN", "givenName", "SN", conf.EmailAttribute, conf.UsernameAttribute}, + nil) + resp, err := l.Search(req) + if ldap.IsErrorWithCode(err, ldap.LDAPResultNoResultsReturned) || + ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) || + (err == nil && len(resp.Entries) == 0) { + log.WithError(err).Debug("ldap lookup returned no results") + return arvados.APIClientAuthorization{}, errFailed + } else if err != nil { + log.WithError(err).Error("ldap lookup failed") + return arvados.APIClientAuthorization{}, err + } + userdn := resp.Entries[0].DN + if userdn == "" { + log.Warn("refusing to authenticate with empty dn") + return arvados.APIClientAuthorization{}, errFailed + } + log = log.WithField("DN", userdn) + + attrs := map[string]string{} + for _, attr := range resp.Entries[0].Attributes { + if attr == nil || len(attr.Values) == 0 { + continue + } + attrs[strings.ToLower(attr.Name)] = attr.Values[0] + } + log.WithField("attrs", attrs).Debug("ldap search succeeded") + + // Now that we have the DN, try authenticating. + err = l.Bind(userdn, opts.Password) + if err != nil { + log.WithError(err).Warn("ldap user authentication failed") + return arvados.APIClientAuthorization{}, errFailed + } + log.Debug("ldap authentication succeeded") + + email := attrs[strings.ToLower(conf.EmailAttribute)] + if email == "" { + log.Errorf("ldap returned no email address in %q attribute", conf.EmailAttribute) + return arvados.APIClientAuthorization{}, errors.New("authentication succeeded but ldap returned no email address") + } + + return createAPIClientAuthorization(ctx, ctrl.RailsProxy, ctrl.Cluster.SystemRootToken, rpc.UserSessionAuthInfo{ + Email: email, + FirstName: attrs["givenname"], + LastName: attrs["sn"], + Username: attrs[strings.ToLower(conf.UsernameAttribute)], + }) +} diff --git a/lib/controller/localdb/login_ldap_docker_test.go b/lib/controller/localdb/login_ldap_docker_test.go new file mode 100644 index 0000000000..54454a190f --- /dev/null +++ b/lib/controller/localdb/login_ldap_docker_test.go @@ -0,0 +1,46 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +// Skip this slow test unless invoked as "go test -tags docker". +// +build docker + +package localdb + +import ( + "os" + "os/exec" + + "git.arvados.org/arvados.git/sdk/go/arvados" + "git.arvados.org/arvados.git/sdk/go/arvadostest" + check "gopkg.in/check.v1" +) + +var _ = check.Suite(&LDAPSuite{}) + +type LDAPSuite struct{} + +func (s *LDAPSuite) 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 *LDAPSuite) TestLoginLDAPViaPAM(c *check.C) { + cmd := exec.Command("bash", "login_ldap_docker_test.sh") + cmd.Stdout = os.Stderr + cmd.Stderr = os.Stderr + cmd.Env = append(os.Environ(), "config_method=pam") + err := cmd.Run() + c.Check(err, check.IsNil) +} + +func (s *LDAPSuite) TestLoginLDAPBuiltin(c *check.C) { + cmd := exec.Command("bash", "login_ldap_docker_test.sh") + cmd.Stdout = os.Stderr + cmd.Stderr = os.Stderr + cmd.Env = append(os.Environ(), "config_method=ldap") + err := cmd.Run() + c.Check(err, check.IsNil) +} diff --git a/lib/controller/localdb/login_pam_docker_test.sh b/lib/controller/localdb/login_ldap_docker_test.sh similarity index 66% rename from lib/controller/localdb/login_pam_docker_test.sh rename to lib/controller/localdb/login_ldap_docker_test.sh index b8f281bc2e..61b1e0e884 100755 --- a/lib/controller/localdb/login_pam_docker_test.sh +++ b/lib/controller/localdb/login_ldap_docker_test.sh @@ -2,9 +2,9 @@ # This script demonstrates using LDAP for Arvados user authentication. # -# It configures pam_ldap(5) and arvados controller in a docker -# container, with pam_ldap configured to authenticate against an -# OpenLDAP server in a second docker container. +# It configures arvados controller in a docker container, optionally +# with pam_ldap(5) configured to authenticate against an OpenLDAP +# server in a second docker container. # # After adding a "foo" user entry, it uses curl to check that the # Arvados controller's login endpoint accepts the "foo" account @@ -24,6 +24,15 @@ if [[ -n ${ARVADOS_DEBUG} ]]; then set -x fi +case "${config_method}" in + pam | ldap) + ;; + *) + echo >&2 "\$config_method env var must be 'pam' or 'ldap'" + exit 1 + ;; +esac + hostname="$(hostname)" tmpdir="$(mktemp -d)" cleanup() { @@ -86,15 +95,37 @@ Clusters: ExternalURL: http://0.0.0.0:9999/ InternalURLs: "http://0.0.0.0:9999/": {} + SystemLogs: + LogLevel: debug +EOF +case "${config_method}" in + pam) + setup_pam_ldap="apt update && DEBIAN_FRONTEND=noninteractive apt install -y ldap-utils libpam-ldap && pam-auth-update --package /usr/share/pam-configs/ldap" + cat >>"${tmpdir}/zzzzz.yml" <>"${tmpdir}/zzzzz.yml" <&2 "${tmpdir}/zzzzz.yml" cat >"${tmpdir}/pam_ldap.conf" <&2 "Adding example user entry user=foo pass=secret (retrying until server comes up)" +echo >&2 "Adding example user entry user=foo-bar pass=secret (retrying until server comes up)" docker run --rm --entrypoint= \ -v "${tmpdir}/add_example_user.ldif":/add_example_user.ldif:ro \ osixia/openldap:1.3.0 \ @@ -152,7 +183,7 @@ docker run --detach --rm --name=${ctrlctr} \ -v "${tmpdir}/zzzzz.yml":/etc/arvados/config.yml:ro \ -v $(realpath "${PWD}/../../.."):/arvados:ro \ debian:10 \ - bash -c "apt update && DEBIAN_FRONTEND=noninteractive apt install -y ldap-utils libpam-ldap && pam-auth-update --package /usr/share/pam-configs/ldap && arvados-server controller" + bash -c "${setup_pam_ldap:-true} && arvados-server controller" docker logs --follow ${ctrlctr} 2>$debug >$debug & ctrlhostport=$(docker port ${ctrlctr} 9999/tcp) @@ -178,16 +209,42 @@ check_contains() { fi } +set +x + echo >&2 "Testing authentication failure" -resp="$(curl -s --include -d username=foo -d password=nosecret "http://${ctrlhostport}/arvados/v1/users/authenticate" | tee $debug)" +resp="$(set -x; curl -s --include -d username=foo-bar -d password=nosecret "http://${ctrlhostport}/arvados/v1/users/authenticate" | tee $debug)" check_contains "${resp}" "HTTP/1.1 401" -check_contains "${resp}" '{"errors":["PAM: Authentication failure (with username \"foo\" and password)"]}' +if [[ "${config_method}" = ldap ]]; then + check_contains "${resp}" '{"errors":["LDAP: Authentication failure (with username \"foo-bar\" and password)"]}' +else + check_contains "${resp}" '{"errors":["PAM: Authentication failure (with username \"foo-bar\" and password)"]}' +fi echo >&2 "Testing authentication success" -resp="$(curl -s --include -d username=foo -d password=secret "http://${ctrlhostport}/arvados/v1/users/authenticate" | tee $debug)" +resp="$(set -x; curl -s --include -d username=foo-bar -d password=secret "http://${ctrlhostport}/arvados/v1/users/authenticate" | tee $debug)" check_contains "${resp}" "HTTP/1.1 200" check_contains "${resp}" '"api_token":"' check_contains "${resp}" '"scopes":["all"]' check_contains "${resp}" '"uuid":"zzzzz-gj3su-' +secret="${resp##*api_token\":\"}" +secret="${secret%%\"*}" +uuid="${resp##*uuid\":\"}" +uuid="${uuid%%\"*}" +token="v2/$uuid/$secret" +echo >&2 "New token is ${token}" + +resp="$(set -x; curl -s --include -H "Authorization: Bearer ${token}" "http://${ctrlhostport}/arvados/v1/users/current" | tee $debug)" +check_contains "${resp}" "HTTP/1.1 200" +if [[ "${config_method}" = ldap ]]; then + # user fields come from LDAP attributes + check_contains "${resp}" '"first_name":"Foo"' + check_contains "${resp}" '"last_name":"Bar"' + check_contains "${resp}" '"username":"foobar"' # "-" removed by rails api + check_contains "${resp}" '"email":"foo-bar-baz@example.com"' +else + # PAMDefaultEmailDomain + check_contains "${resp}" '"email":"foo-bar@example.com"' +fi + cleanup diff --git a/lib/controller/localdb/login_pam.go b/lib/controller/localdb/login_pam.go index 01dfc1379d..538e3118ed 100644 --- a/lib/controller/localdb/login_pam.go +++ b/lib/controller/localdb/login_pam.go @@ -9,12 +9,10 @@ import ( "errors" "fmt" "net/http" - "net/url" "strings" "git.arvados.org/arvados.git/lib/controller/rpc" "git.arvados.org/arvados.git/sdk/go/arvados" - "git.arvados.org/arvados.git/sdk/go/auth" "git.arvados.org/arvados.git/sdk/go/ctxlog" "git.arvados.org/arvados.git/sdk/go/httpserver" "github.com/msteinert/pam" @@ -85,25 +83,12 @@ func (ctrl *pamLoginController) UserAuthenticate(ctx context.Context, opts arvad if domain := ctrl.Cluster.Login.PAMDefaultEmailDomain; domain != "" && !strings.Contains(email, "@") { email = email + "@" + domain } - ctxlog.FromContext(ctx).WithFields(logrus.Fields{"user": user, "email": email}).Debug("pam authentication succeeded") - ctxRoot := auth.NewContext(ctx, &auth.Credentials{Tokens: []string{ctrl.Cluster.SystemRootToken}}) - resp, err := ctrl.RailsProxy.UserSessionCreate(ctxRoot, rpc.UserSessionCreateOptions{ - // Send a fake ReturnTo value instead of the caller's - // opts.ReturnTo. We won't follow the resulting - // redirect target anyway. - ReturnTo: ",https://none.invalid", - AuthInfo: rpc.UserSessionAuthInfo{ - Username: user, - Email: email, - }, + ctxlog.FromContext(ctx).WithFields(logrus.Fields{ + "user": user, + "email": email, + }).Debug("pam authentication succeeded") + return createAPIClientAuthorization(ctx, ctrl.RailsProxy, ctrl.Cluster.SystemRootToken, rpc.UserSessionAuthInfo{ + Username: user, + Email: email, }) - if err != nil { - return arvados.APIClientAuthorization{}, err - } - target, err := url.Parse(resp.RedirectLocation) - if err != nil { - return arvados.APIClientAuthorization{}, err - } - token := target.Query().Get("api_token") - return ctrl.RailsProxy.APIClientAuthorizationCurrent(auth.NewContext(ctx, auth.NewCredentials(token)), arvados.GetOptions{}) } diff --git a/lib/controller/localdb/login_pam_docker_test.go b/lib/controller/localdb/login_pam_docker_test.go deleted file mode 100644 index 8a02b2c382..0000000000 --- a/lib/controller/localdb/login_pam_docker_test.go +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -// Skip this slow test unless invoked as "go test -tags docker". -// +build docker - -package localdb - -import ( - "os" - "os/exec" - - check "gopkg.in/check.v1" -) - -func (s *PamSuite) TestLoginLDAPViaPAM(c *check.C) { - cmd := exec.Command("bash", "login_pam_docker_test.sh") - cmd.Stdout = os.Stderr - cmd.Stderr = os.Stderr - err := cmd.Run() - c.Check(err, check.IsNil) -} diff --git a/sdk/go/arvados/config.go b/sdk/go/arvados/config.go index 38de6b8ea4..817f5b7a69 100644 --- a/sdk/go/arvados/config.go +++ b/sdk/go/arvados/config.go @@ -135,6 +135,21 @@ type Cluster struct { Repositories string } Login struct { + LDAP struct { + Enable bool + URL URL + StartTLS bool + InsecureTLS bool + StripDomain string + AppendDomain string + SearchAttribute string + SearchBindUser string + SearchBindPassword string + SearchBase string + SearchFilters string + EmailAttribute string + UsernameAttribute string + } GoogleClientID string GoogleClientSecret string GoogleAlternateEmailAddresses bool diff --git a/services/api/app/controllers/user_sessions_controller.rb b/services/api/app/controllers/user_sessions_controller.rb index 85f32772b1..200260bce2 100644 --- a/services/api/app/controllers/user_sessions_controller.rb +++ b/services/api/app/controllers/user_sessions_controller.rb @@ -30,6 +30,8 @@ class UserSessionsController < ApplicationController authinfo = request.env['omniauth.auth']['info'].with_indifferent_access end + Rails.logger.warn "authinfo was #{authinfo.inspect}" + begin user = User.register(authinfo) rescue => e -- 2.30.2