forked from hashicorp/vault
-
Notifications
You must be signed in to change notification settings - Fork 0
/
path_config.go
312 lines (273 loc) · 8 KB
/
path_config.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
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
package ldap
import (
"crypto/tls"
"crypto/x509"
"fmt"
"net"
"net/url"
"strings"
"github.com/go-ldap/ldap"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func pathConfig(b *backend) *framework.Path {
return &framework.Path{
Pattern: `config`,
Fields: map[string]*framework.FieldSchema{
"url": &framework.FieldSchema{
Type: framework.TypeString,
Description: "ldap URL to connect to (default: ldap://127.0.0.1)",
},
"userdn": &framework.FieldSchema{
Type: framework.TypeString,
Description: "LDAP domain to use for users (eg: ou=People,dc=example,dc=org)",
},
"binddn": &framework.FieldSchema{
Type: framework.TypeString,
Description: "LDAP DN for searching for the user DN (optional)",
},
"bindpass": &framework.FieldSchema{
Type: framework.TypeString,
Description: "LDAP password for searching for the user DN (optional)",
},
"groupdn": &framework.FieldSchema{
Type: framework.TypeString,
Description: "LDAP domain to use for groups (eg: ou=Groups,dc=example,dc=org)",
},
"upndomain": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Enables userPrincipalDomain login with [username]@UPNDomain (optional)",
},
"userattr": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Attribute used for users (default: cn)",
},
"certificate": &framework.FieldSchema{
Type: framework.TypeString,
Description: "CA certificate to use when verifying LDAP server certificate, must be x509 PEM encoded (optional)",
},
"discoverdn": &framework.FieldSchema{
Type: framework.TypeBool,
Description: "Use anonymous bind to discover the bind DN of a user (optional)",
},
"insecure_tls": &framework.FieldSchema{
Type: framework.TypeBool,
Description: "Skip LDAP server SSL Certificate verification - VERY insecure (optional)",
},
"starttls": &framework.FieldSchema{
Type: framework.TypeBool,
Description: "Issue a StartTLS command after establishing unencrypted connection (optional)",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.pathConfigRead,
logical.UpdateOperation: b.pathConfigWrite,
},
HelpSynopsis: pathConfigHelpSyn,
HelpDescription: pathConfigHelpDesc,
}
}
func (b *backend) Config(req *logical.Request) (*ConfigEntry, error) {
entry, err := req.Storage.Get("config")
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}
var result ConfigEntry
result.SetDefaults()
if err := entry.DecodeJSON(&result); err != nil {
return nil, err
}
return &result, nil
}
func (b *backend) pathConfigRead(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
cfg, err := b.Config(req)
if err != nil {
return nil, err
}
if cfg == nil {
return nil, nil
}
return &logical.Response{
Data: map[string]interface{}{
"url": cfg.Url,
"userdn": cfg.UserDN,
"groupdn": cfg.GroupDN,
"upndomain": cfg.UPNDomain,
"userattr": cfg.UserAttr,
"certificate": cfg.Certificate,
"insecure_tls": cfg.InsecureTLS,
"starttls": cfg.StartTLS,
"binddn": cfg.BindDN,
"bindpass": cfg.BindPassword,
"discoverdn": cfg.DiscoverDN,
},
}, nil
}
func (b *backend) pathConfigWrite(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
cfg := &ConfigEntry{}
url := d.Get("url").(string)
if url != "" {
cfg.Url = strings.ToLower(url)
}
userattr := d.Get("userattr").(string)
if userattr != "" {
cfg.UserAttr = strings.ToLower(userattr)
}
userdn := d.Get("userdn").(string)
if userdn != "" {
cfg.UserDN = userdn
}
groupdn := d.Get("groupdn").(string)
if groupdn != "" {
cfg.GroupDN = groupdn
}
upndomain := d.Get("upndomain").(string)
if groupdn != "" {
cfg.UPNDomain = upndomain
}
certificate := d.Get("certificate").(string)
if certificate != "" {
cfg.Certificate = certificate
}
insecureTLS := d.Get("insecure_tls").(bool)
if insecureTLS {
cfg.InsecureTLS = insecureTLS
}
startTLS := d.Get("starttls").(bool)
if startTLS {
cfg.StartTLS = startTLS
}
bindDN := d.Get("binddn").(string)
if bindDN != "" {
cfg.BindDN = bindDN
}
bindPass := d.Get("bindpass").(string)
if bindPass != "" {
cfg.BindPassword = bindPass
}
discoverDN := d.Get("discoverdn").(bool)
if discoverDN {
cfg.DiscoverDN = discoverDN
}
// Try to connect to the LDAP server, to validate the URL configuration
// We can also check the URL at this stage, as anything else would probably
// require authentication.
conn, cerr := cfg.DialLDAP()
if cerr != nil {
return logical.ErrorResponse(cerr.Error()), nil
}
conn.Close()
entry, err := logical.StorageEntryJSON("config", cfg)
if err != nil {
return nil, err
}
if err := req.Storage.Put(entry); err != nil {
return nil, err
}
return nil, nil
}
type ConfigEntry struct {
Url string
UserDN string
GroupDN string
UPNDomain string
UserAttr string
Certificate string
InsecureTLS bool
StartTLS bool
BindDN string
BindPassword string
DiscoverDN bool
}
func (c *ConfigEntry) GetTLSConfig(host string) (*tls.Config, error) {
tlsConfig := &tls.Config{
ServerName: host,
}
if c.InsecureTLS {
tlsConfig.InsecureSkipVerify = true
}
if c.Certificate != "" {
caPool := x509.NewCertPool()
ok := caPool.AppendCertsFromPEM([]byte(c.Certificate))
if !ok {
return nil, fmt.Errorf("could not append CA certificate")
}
tlsConfig.RootCAs = caPool
}
return tlsConfig, nil
}
func (c *ConfigEntry) DialLDAP() (*ldap.Conn, error) {
u, err := url.Parse(c.Url)
if err != nil {
return nil, err
}
host, port, err := net.SplitHostPort(u.Host)
if err != nil {
host = u.Host
}
var conn *ldap.Conn
switch u.Scheme {
case "ldap":
if port == "" {
port = "389"
}
conn, err = ldap.Dial("tcp", host+":"+port)
if c.StartTLS {
tlsConfig, err := c.GetTLSConfig(host)
if err != nil {
break
}
err = conn.StartTLS(tlsConfig)
}
case "ldaps":
if port == "" {
port = "636"
}
tlsConfig, err := c.GetTLSConfig(host)
if err != nil {
break
}
conn, err = ldap.DialTLS("tcp", host+":"+port, tlsConfig)
default:
return nil, fmt.Errorf("invalid LDAP scheme")
}
if err != nil {
return nil, fmt.Errorf("cannot connect to LDAP: %v", err)
}
return conn, nil
}
func (c *ConfigEntry) SetDefaults() {
c.Url = "ldap://127.0.0.1"
c.UserAttr = "cn"
}
const pathConfigHelpSyn = `
Configure the LDAP server to connect to, along with its options.
`
const pathConfigHelpDesc = `
This endpoint allows you to configure the LDAP server to connect to and its
configuration options.
The LDAP URL can use either the "ldap://" or "ldaps://" schema. In the former
case, an unencrypted connection will be made with a default port of 389, unless
the "starttls" parameter is set to true, in which case TLS will be used. In the
latter case, a SSL connection will be established with a default port of 636.
## A NOTE ON ESCAPING
It is up to the administrator to provide properly escaped DNs. This includes
the user DN, bind DN for search, and so on.
The only DN escaping performed by this backend is on usernames given at login
time when they are inserted into the final bind DN, and uses escaping rules
defined in RFC 4514.
Additionally, Active Directory has escaping rules that differ slightly from the
RFC; in particular it requires escaping of '#' regardless of position in the DN
(the RFC only requires it to be escaped when it is the first character), and
'=', which the RFC indicates can be escaped with a backslash, but does not
contain in its set of required escapes. If you are using Active Directory and
these appear in your usernames, please ensure that they are escaped, in
addition to being properly escaped in your configured DNs.
For reference, see https://www.ietf.org/rfc/rfc4514.txt and
http://social.technet.microsoft.com/wiki/contents/articles/5312.active-directory-characters-to-escape.aspx
`