[ARVADOS] created: 1.3.0-2542-g576340981

Git user git at public.arvados.org
Thu May 7 04:39:05 UTC 2020


        at  5763409818cd2ab68c0f59b6a97d0c3df090907f (commit)


commit 5763409818cd2ab68c0f59b6a97d0c3df090907f
Author: Tom Clegg <tom at tomclegg.ca>
Date:   Thu May 7 00:38:23 2020 -0400

    15881: Add LDAP authentication option.
    
    Arvados-DCO-1.1-Signed-off-by: Tom Clegg <tom at tomclegg.ca>

diff --git a/go.mod b/go.mod
index 34b7e0779..482c6971d 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 03b2f77b6..a92b3c11a 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 d4870919e..a06526fd6 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 ded03fc30..323043fbe 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 42707396d..e5ec035c6 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 ae5984999..8cba3b6fa 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 000000000..44e42ac40
--- /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 000000000..54454a190
--- /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 b8f281bc2..61b1e0e88 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" <<EOF
     Login:
       PAM: true
       # Without this magic PAMDefaultEmailDomain, inserted users would
       # prevent subsequent database/reset from working (see
       # database_controller.rb).
       PAMDefaultEmailDomain: example.com
-    SystemLogs:
-      LogLevel: debug
 EOF
+        ;;
+    ldap)
+        setup_pam_ldap=""
+        cat >>"${tmpdir}/zzzzz.yml" <<EOF
+    Login:
+      LDAP:
+        Enable: true
+        URL: ${ldapurl}
+        StartTLS: false
+        SearchBase: dc=example,dc=org
+        SearchBindUser: cn=admin,dc=example,dc=org
+        SearchBindPassword: admin
+EOF
+            ;;
+esac
+
+cat >&2 "${tmpdir}/zzzzz.yml"
 
 cat >"${tmpdir}/pam_ldap.conf" <<EOF
 base dc=example,dc=org
@@ -113,12 +144,12 @@ cn: bar
 gidNumber: 11111
 description: "Example group 'bar'"
 
-dn: uid=foo,dc=example,dc=org
-uid: foo
-cn: foo
+dn: uid=foo-bar,dc=example,dc=org
+uid: foo-bar
+cn: "Foo Bar"
 givenName: Foo
 sn: Bar
-mail: foobar at example.org
+mail: foo-bar-baz at example.com
 objectClass: inetOrgPerson
 objectClass: posixAccount
 objectClass: top
@@ -130,11 +161,11 @@ shadowLastChange: 10701
 loginShell: /bin/bash
 uidNumber: 11111
 gidNumber: 11111
-homeDirectory: /home/foo
+homeDirectory: /home/foo-bar
 userPassword: ${passwordhash}
 EOF
 
-echo >&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 at example.com"'
+else
+    # PAMDefaultEmailDomain
+    check_contains "${resp}" '"email":"foo-bar at example.com"'
+fi
+
 cleanup
diff --git a/lib/controller/localdb/login_pam.go b/lib/controller/localdb/login_pam.go
index 01dfc1379..538e3118e 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 8a02b2c38..000000000
--- 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 38de6b8ea..817f5b7a6 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 85f32772b..200260bce 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

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list