1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
16 "git.arvados.org/arvados.git/lib/controller/rpc"
17 "git.arvados.org/arvados.git/sdk/go/arvados"
18 "git.arvados.org/arvados.git/sdk/go/ctxlog"
19 "git.arvados.org/arvados.git/sdk/go/httpserver"
20 "github.com/go-ldap/ldap"
23 type ldapLoginController struct {
24 Cluster *arvados.Cluster
28 func (ctrl *ldapLoginController) Logout(ctx context.Context, opts arvados.LogoutOptions) (arvados.LogoutResponse, error) {
29 return logout(ctx, ctrl.Cluster, opts)
32 func (ctrl *ldapLoginController) Login(ctx context.Context, opts arvados.LoginOptions) (arvados.LoginResponse, error) {
33 return arvados.LoginResponse{}, errors.New("interactive login is not available")
36 func (ctrl *ldapLoginController) UserAuthenticate(ctx context.Context, opts arvados.UserAuthenticateOptions) (arvados.APIClientAuthorization, error) {
37 log := ctxlog.FromContext(ctx)
38 conf := ctrl.Cluster.Login.LDAP
39 errFailed := httpserver.ErrorWithStatus(fmt.Errorf("LDAP: Authentication failure (with username %q and password)", opts.Username), http.StatusUnauthorized)
41 if conf.SearchAttribute == "" {
42 return arvados.APIClientAuthorization{}, errors.New("config error: SearchAttribute is blank")
44 if opts.Password == "" {
45 log.WithField("username", opts.Username).Error("refusing to authenticate with empty password")
46 return arvados.APIClientAuthorization{}, errFailed
49 log = log.WithField("URL", conf.URL.String())
52 if conf.URL.Scheme == "ldaps" {
53 // ldap.DialURL does not currently allow us to control
54 // tls.Config, so we need to figure out the port
55 // ourselves and call DialTLS.
56 host, port, err := net.SplitHostPort(conf.URL.Host)
58 // Assume error means no port given
60 port = ldap.DefaultLdapsPort
62 l, err = ldap.DialTLS("tcp", net.JoinHostPort(host, port), &tls.Config{
64 MinVersion: uint16(conf.MinTLSVersion),
67 l, err = ldap.DialURL(conf.URL.String())
70 log.WithError(err).Error("ldap connection failed")
71 return arvados.APIClientAuthorization{}, err
76 var tlsconfig tls.Config
77 tlsconfig.MinVersion = uint16(conf.MinTLSVersion)
79 tlsconfig.InsecureSkipVerify = true
81 if host, _, err := net.SplitHostPort(conf.URL.Host); err != nil {
82 // Assume SplitHostPort error means
83 // port was not specified
84 tlsconfig.ServerName = conf.URL.Host
86 tlsconfig.ServerName = host
89 err = l.StartTLS(&tlsconfig)
91 log.WithError(err).Error("ldap starttls failed")
92 return arvados.APIClientAuthorization{}, err
96 username := opts.Username
97 if at := strings.Index(username, "@"); at >= 0 {
98 if conf.StripDomain == "*" || strings.ToLower(conf.StripDomain) == strings.ToLower(username[at+1:]) {
99 username = username[:at]
102 if conf.AppendDomain != "" && !strings.Contains(username, "@") {
103 username = username + "@" + conf.AppendDomain
106 if conf.SearchBindUser != "" {
107 err = l.Bind(conf.SearchBindUser, conf.SearchBindPassword)
109 log.WithError(err).WithField("user", conf.SearchBindUser).Error("ldap authentication failed")
110 return arvados.APIClientAuthorization{}, err
114 search := fmt.Sprintf("(%s=%s)", ldap.EscapeFilter(conf.SearchAttribute), ldap.EscapeFilter(username))
115 if conf.SearchFilters != "" {
116 search = fmt.Sprintf("(&%s%s)", conf.SearchFilters, search)
118 log = log.WithField("search", search)
119 req := ldap.NewSearchRequest(
121 ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false,
123 []string{"DN", "givenName", "SN", conf.EmailAttribute, conf.UsernameAttribute},
125 resp, err := l.Search(req)
126 if ldap.IsErrorWithCode(err, ldap.LDAPResultNoResultsReturned) ||
127 ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) ||
128 (err == nil && len(resp.Entries) == 0) {
129 log.WithError(err).Info("ldap lookup returned no results")
130 return arvados.APIClientAuthorization{}, errFailed
131 } else if err != nil {
132 log.WithError(err).Error("ldap lookup failed")
133 return arvados.APIClientAuthorization{}, err
135 userdn := resp.Entries[0].DN
137 log.Warn("refusing to authenticate with empty dn")
138 return arvados.APIClientAuthorization{}, errFailed
140 log = log.WithField("DN", userdn)
142 attrs := map[string]string{}
143 for _, attr := range resp.Entries[0].Attributes {
144 if attr == nil || len(attr.Values) == 0 {
147 attrs[strings.ToLower(attr.Name)] = attr.Values[0]
149 log.WithField("attrs", attrs).Debug("ldap search succeeded")
151 // Now that we have the DN, try authenticating.
152 err = l.Bind(userdn, opts.Password)
154 log.WithError(err).Info("ldap user authentication failed")
155 return arvados.APIClientAuthorization{}, errFailed
157 log.Debug("ldap authentication succeeded")
159 email := attrs[strings.ToLower(conf.EmailAttribute)]
161 log.Errorf("ldap returned no email address in %q attribute", conf.EmailAttribute)
162 return arvados.APIClientAuthorization{}, errors.New("authentication succeeded but ldap returned no email address")
165 return ctrl.Parent.CreateAPIClientAuthorization(ctx, ctrl.Cluster.SystemRootToken, rpc.UserSessionAuthInfo{
167 FirstName: attrs["givenname"],
168 LastName: attrs["sn"],
169 Username: attrs[strings.ToLower(conf.UsernameAttribute)],