-
Notifications
You must be signed in to change notification settings - Fork 3.8k
/
authentication_ldap.go
264 lines (237 loc) · 9.55 KB
/
authentication_ldap.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
// Copyright 2022 The Cockroach Authors.
//
// Licensed as a CockroachDB Enterprise file under the Cockroach Community
// License (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// https://github.com/cockroachdb/cockroach/blob/master/licenses/CCL.txt
package ldapccl
import (
"context"
"fmt"
"github.com/cockroachdb/cockroach/pkg/ccl/utilccl"
"github.com/cockroachdb/cockroach/pkg/security/username"
"github.com/cockroachdb/cockroach/pkg/server/telemetry"
"github.com/cockroachdb/cockroach/pkg/settings/cluster"
"github.com/cockroachdb/cockroach/pkg/sql/pgwire"
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/hba"
"github.com/cockroachdb/cockroach/pkg/sql/pgwire/identmap"
"github.com/cockroachdb/cockroach/pkg/util/log"
"github.com/cockroachdb/cockroach/pkg/util/syncutil"
"github.com/cockroachdb/cockroach/pkg/util/uuid"
"github.com/cockroachdb/errors"
)
const (
counterPrefix = "auth.ldap."
beginAuthCounterName = counterPrefix + "begin_auth"
loginSuccessCounterName = counterPrefix + "login_success"
enableCounterName = counterPrefix + "enable"
)
var (
beginAuthUseCounter = telemetry.GetCounterOnce(beginAuthCounterName)
loginSuccessUseCounter = telemetry.GetCounterOnce(loginSuccessCounterName)
enableUseCounter = telemetry.GetCounterOnce(enableCounterName)
)
// ldapAuthenticator is an object that is used to enable ldap connection
// validation that are used as part of the CRDB client auth flow.
//
// The implementation uses the `go-ldap/ldap/` client package and is supported
// through a number of cluster settings defined in `ldapccl/settings.go`. These
// settings specify how the ldap auth attempt should be executed and if this
// feature is enabled.
type ldapAuthenticator struct {
mu struct {
syncutil.RWMutex
// conf contains all the values that come from cluster settings.
conf ldapAuthenticatorConf
// util contains connection object required for interfacing with ldap server.
util ILDAPUtil
// enabled represents the present state of if this feature is enabled. It
// is set to true once ldap util is initialized.
enabled bool
}
// clusterUUID is used to check the validity of the enterprise license. It is set once at initialization.
clusterUUID uuid.UUID
}
// ldapAuthenticatorConf contains all the values to configure LDAP authentication. These values are copied from
// the matching cluster settings or from hba conf options for LDAP entry.
type ldapAuthenticatorConf struct {
domainCACert string
ldapServer string
ldapPort string
ldapBaseDN string
ldapBindDN string
ldapBindPassword string
ldapSearchFilter string
ldapSearchAttribute string
}
// reloadConfig locks mutex and then refreshes the values in conf from the cluster settings.
func (authenticator *ldapAuthenticator) reloadConfig(ctx context.Context, st *cluster.Settings) {
authenticator.mu.Lock()
defer authenticator.mu.Unlock()
authenticator.reloadConfigLocked(ctx, st)
}
// reloadConfig refreshes the values in conf from the cluster settings without locking the mutex.
func (authenticator *ldapAuthenticator) reloadConfigLocked(
ctx context.Context, st *cluster.Settings,
) {
conf := ldapAuthenticatorConf{
domainCACert: LDAPDomainCACertificate.Get(&st.SV),
}
authenticator.mu.conf = conf
var err error
authenticator.mu.util, err = NewLDAPUtil(ctx, authenticator.mu.conf)
if err != nil {
log.Warningf(ctx, "LDAP authentication: unable to initialize LDAP connection: %v", err)
return
}
if !authenticator.mu.enabled {
telemetry.Inc(enableUseCounter)
}
authenticator.mu.enabled = true
log.Infof(ctx, "initialized LDAP authenticator")
}
// setLDAPConfigOptions extracts hba conf parameters required for connecting and
// querying LDAP server from hba conf entry and sets them for LDAP authenticator.
func (authenticator *ldapAuthenticator) setLDAPConfigOptions(entry *hba.Entry) error {
conf := ldapAuthenticatorConf{
domainCACert: authenticator.mu.conf.domainCACert,
}
for _, opt := range entry.Options {
switch opt[0] {
case "ldapserver":
conf.ldapServer = opt[1]
case "ldapport":
conf.ldapPort = opt[1]
case "ldapbasedn":
conf.ldapBaseDN = opt[1]
case "ldapbinddn":
conf.ldapBindDN = opt[1]
case "ldapbindpasswd":
conf.ldapBindPassword = opt[1]
case "ldapsearchfilter":
conf.ldapSearchFilter = opt[1]
case "ldapsearchattribute":
conf.ldapSearchAttribute = opt[1]
default:
return errors.Newf("invalid LDAP option provided in hba conf")
}
}
authenticator.mu.conf = conf
return nil
}
// validateLDAPOptions checks the ldap authenticator config values for validity.
func (authenticator *ldapAuthenticator) validateLDAPOptions() error {
if authenticator.mu.conf.ldapServer == "" {
return errors.Newf("ldap server not provided in hba conf")
}
if authenticator.mu.conf.ldapPort == "" {
return errors.Newf("ldap port not provided in hba conf")
}
if authenticator.mu.conf.ldapBaseDN == "" {
return errors.Newf("ldap base DN not provided in hba conf")
}
if authenticator.mu.conf.ldapBindDN == "" {
return errors.Newf("ldap bind DN not provided in hba conf")
}
if authenticator.mu.conf.ldapBindPassword == "" {
return errors.Newf("ldap bind password not provided in hba conf")
}
if authenticator.mu.conf.ldapSearchFilter == "" {
return errors.Newf("ldap search filter not provided in hba conf")
}
if authenticator.mu.conf.ldapSearchAttribute == "" {
return errors.Newf("ldap search attribute not provided in hba conf")
}
return nil
}
// ValidateLDAPLogin validates an attempt to bind to an LDAP server.
// In particular, it checks that:
// * LDAP authentication is enabled after settings were reloaded.
// * the auth attempt is not for a reserved user.
// * the hba conf entry options could be parsed to obtain ldap server params.
// * all ldap server params are valid.
// * the cluster has an enterprise license.
// * LDAPs connection can be established with configured server.
// * configured bind DN and password can be used to search for the sql user DN on ldap server.
// * the obtained user DN could be used to bind with the password from sql connection string.
// It returns authError (which is the error sql clients will see in case of
// failures) and detailedError (which is the internal error from ldap clients
// that might contain sensitive information we do not want to send to sql
// clients but still want to log it). We do not want to send any information
// back to client which was not provided by the client.
func (authenticator *ldapAuthenticator) ValidateLDAPLogin(
ctx context.Context,
st *cluster.Settings,
user username.SQLUsername,
ldapPwd string,
entry *hba.Entry,
_ *identmap.Conf,
) (detailedErrorMsg string, authError error) {
authenticator.mu.Lock()
defer authenticator.mu.Unlock()
if !authenticator.mu.enabled {
return "", errors.Newf("LDAP authentication: not enabled")
}
telemetry.Inc(beginAuthUseCounter)
if user.IsRootUser() || user.IsReserved() {
return "", errors.WithDetailf(
errors.Newf("LDAP authentication: invalid identity"),
"cannot use LDAP auth to login to a reserved user %s", user.Normalized())
}
if err := authenticator.setLDAPConfigOptions(entry); err != nil {
return fmt.Sprintf("error when fetching hba conf options for LDAP: %v", err),
errors.Newf("LDAP authentication: unable to fetch hba conf options")
}
if err := authenticator.validateLDAPOptions(); err != nil {
return fmt.Sprintf("error validation authenticator options for LDAP: %v", err),
errors.Newf("LDAP authentication: unable to validate authenticator options")
}
if err := utilccl.CheckEnterpriseEnabled(st, "LDAP authentication"); err != nil {
return "", err
}
// Establish a LDAPs connection with the set LDAP server and port
err := authenticator.mu.util.LDAPSConn(ctx, authenticator.mu.conf)
if err != nil {
return fmt.Sprintf("error when trying to create LDAP connection: %v", err),
errors.Newf("LDAP authentication: unable to establish LDAP connection")
}
// Fetch the ldap server Distinguished Name using sql username as search value
// for ldap search attribute
userDN, err := authenticator.mu.util.Search(ctx, authenticator.mu.conf, user.Normalized())
if err != nil {
return fmt.Sprintf("error when searching for user in LDAP server: %v", err),
errors.WithDetailf(
errors.Newf("LDAP authentication: unable to find LDAP user distinguished name"),
"cannot find provided user %s on LDAP server", user.Normalized())
}
// Bind as the user to verify their password
err = authenticator.mu.util.Bind(ctx, userDN, ldapPwd)
if err != nil {
return fmt.Sprintf("error when biding as user %s with DN(%s) in LDAP server: %v", user.Normalized(), userDN, err),
errors.WithDetailf(
errors.Newf("LDAP authentication: unable to bind as LDAP user"),
"credentials invalid for LDAP server user %s", user.Normalized())
}
telemetry.Inc(loginSuccessUseCounter)
return "", nil
}
// ConfigureLDAPAuth initializes and returns a ldapAuthenticator. It also sets up listeners so
// that the ldapAuthenticator's config is updated when the cluster settings values change.
var ConfigureLDAPAuth = func(
serverCtx context.Context,
ambientCtx log.AmbientContext,
st *cluster.Settings,
clusterUUID uuid.UUID,
) pgwire.LDAPVerifier {
authenticator := ldapAuthenticator{}
authenticator.clusterUUID = clusterUUID
authenticator.reloadConfig(serverCtx, st)
LDAPDomainCACertificate.SetOnChange(&st.SV, func(ctx context.Context) {
authenticator.reloadConfig(ambientCtx.AnnotateCtx(ctx), st)
})
return &authenticator
}
func init() {
pgwire.ConfigureLDAPAuth = ConfigureLDAPAuth
}