Merge branch 'patch-1' of https://github.com/mr-c/arvados into mr-c-patch-1
[arvados.git] / lib / controller / localdb / login_ldap.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package localdb
6
7 import (
8         "context"
9         "crypto/tls"
10         "errors"
11         "fmt"
12         "net"
13         "net/http"
14         "strings"
15
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"
21 )
22
23 type ldapLoginController struct {
24         Cluster    *arvados.Cluster
25         RailsProxy *railsProxy
26 }
27
28 func (ctrl *ldapLoginController) Logout(ctx context.Context, opts arvados.LogoutOptions) (arvados.LogoutResponse, error) {
29         return noopLogout(ctrl.Cluster, opts)
30 }
31
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")
34 }
35
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)
40
41         if conf.SearchAttribute == "" {
42                 return arvados.APIClientAuthorization{}, errors.New("config error: SearchAttribute is blank")
43         }
44         if opts.Password == "" {
45                 log.WithField("username", opts.Username).Error("refusing to authenticate with empty password")
46                 return arvados.APIClientAuthorization{}, errFailed
47         }
48
49         log = log.WithField("URL", conf.URL.String())
50         l, err := ldap.DialURL(conf.URL.String())
51         if err != nil {
52                 log.WithError(err).Error("ldap connection failed")
53                 return arvados.APIClientAuthorization{}, err
54         }
55         defer l.Close()
56
57         if conf.StartTLS {
58                 var tlsconfig tls.Config
59                 if conf.InsecureTLS {
60                         tlsconfig.InsecureSkipVerify = true
61                 } else {
62                         if host, _, err := net.SplitHostPort(conf.URL.Host); err != nil {
63                                 // Assume SplitHostPort error means
64                                 // port was not specified
65                                 tlsconfig.ServerName = conf.URL.Host
66                         } else {
67                                 tlsconfig.ServerName = host
68                         }
69                 }
70                 err = l.StartTLS(&tlsconfig)
71                 if err != nil {
72                         log.WithError(err).Error("ldap starttls failed")
73                         return arvados.APIClientAuthorization{}, err
74                 }
75         }
76
77         username := opts.Username
78         if at := strings.Index(username, "@"); at >= 0 {
79                 if conf.StripDomain == "*" || strings.ToLower(conf.StripDomain) == strings.ToLower(username[at+1:]) {
80                         username = username[:at]
81                 }
82         }
83         if conf.AppendDomain != "" && !strings.Contains(username, "@") {
84                 username = username + "@" + conf.AppendDomain
85         }
86
87         if conf.SearchBindUser != "" {
88                 err = l.Bind(conf.SearchBindUser, conf.SearchBindPassword)
89                 if err != nil {
90                         log.WithError(err).WithField("user", conf.SearchBindUser).Error("ldap authentication failed")
91                         return arvados.APIClientAuthorization{}, err
92                 }
93         }
94
95         search := fmt.Sprintf("(%s=%s)", ldap.EscapeFilter(conf.SearchAttribute), ldap.EscapeFilter(username))
96         if conf.SearchFilters != "" {
97                 search = fmt.Sprintf("(&%s%s)", conf.SearchFilters, search)
98         }
99         log = log.WithField("search", search)
100         req := ldap.NewSearchRequest(
101                 conf.SearchBase,
102                 ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false,
103                 search,
104                 []string{"DN", "givenName", "SN", conf.EmailAttribute, conf.UsernameAttribute},
105                 nil)
106         resp, err := l.Search(req)
107         if ldap.IsErrorWithCode(err, ldap.LDAPResultNoResultsReturned) ||
108                 ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) ||
109                 (err == nil && len(resp.Entries) == 0) {
110                 log.WithError(err).Info("ldap lookup returned no results")
111                 return arvados.APIClientAuthorization{}, errFailed
112         } else if err != nil {
113                 log.WithError(err).Error("ldap lookup failed")
114                 return arvados.APIClientAuthorization{}, err
115         }
116         userdn := resp.Entries[0].DN
117         if userdn == "" {
118                 log.Warn("refusing to authenticate with empty dn")
119                 return arvados.APIClientAuthorization{}, errFailed
120         }
121         log = log.WithField("DN", userdn)
122
123         attrs := map[string]string{}
124         for _, attr := range resp.Entries[0].Attributes {
125                 if attr == nil || len(attr.Values) == 0 {
126                         continue
127                 }
128                 attrs[strings.ToLower(attr.Name)] = attr.Values[0]
129         }
130         log.WithField("attrs", attrs).Debug("ldap search succeeded")
131
132         // Now that we have the DN, try authenticating.
133         err = l.Bind(userdn, opts.Password)
134         if err != nil {
135                 log.WithError(err).Info("ldap user authentication failed")
136                 return arvados.APIClientAuthorization{}, errFailed
137         }
138         log.Debug("ldap authentication succeeded")
139
140         email := attrs[strings.ToLower(conf.EmailAttribute)]
141         if email == "" {
142                 log.Errorf("ldap returned no email address in %q attribute", conf.EmailAttribute)
143                 return arvados.APIClientAuthorization{}, errors.New("authentication succeeded but ldap returned no email address")
144         }
145
146         return createAPIClientAuthorization(ctx, ctrl.RailsProxy, ctrl.Cluster.SystemRootToken, rpc.UserSessionAuthInfo{
147                 Email:     email,
148                 FirstName: attrs["givenname"],
149                 LastName:  attrs["sn"],
150                 Username:  attrs[strings.ToLower(conf.UsernameAttribute)],
151         })
152 }