1package ldap
2
3import (
4	"context"
5	"fmt"
6	"strings"
7
8	"github.com/hashicorp/vault/helper/mfa"
9	"github.com/hashicorp/vault/sdk/framework"
10	"github.com/hashicorp/vault/sdk/helper/ldaputil"
11	"github.com/hashicorp/vault/sdk/helper/strutil"
12	"github.com/hashicorp/vault/sdk/logical"
13)
14
15func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
16	b := Backend()
17	if err := b.Setup(ctx, conf); err != nil {
18		return nil, err
19	}
20	return b, nil
21}
22
23func Backend() *backend {
24	var b backend
25	b.Backend = &framework.Backend{
26		Help: backendHelp,
27
28		PathsSpecial: &logical.Paths{
29			Root: mfa.MFARootPaths(),
30
31			Unauthenticated: []string{
32				"login/*",
33			},
34
35			SealWrapStorage: []string{
36				"config",
37			},
38		},
39
40		Paths: append([]*framework.Path{
41			pathConfig(&b),
42			pathGroups(&b),
43			pathGroupsList(&b),
44			pathUsers(&b),
45			pathUsersList(&b),
46		},
47			mfa.MFAPaths(b.Backend, pathLogin(&b))...,
48		),
49
50		AuthRenew:   b.pathLoginRenew,
51		BackendType: logical.TypeCredential,
52	}
53
54	return &b
55}
56
57type backend struct {
58	*framework.Backend
59}
60
61func (b *backend) Login(ctx context.Context, req *logical.Request, username string, password string) ([]string, *logical.Response, []string, error) {
62
63	cfg, err := b.Config(ctx, req)
64	if err != nil {
65		return nil, nil, nil, err
66	}
67	if cfg == nil {
68		return nil, logical.ErrorResponse("ldap backend not configured"), nil, nil
69	}
70
71	if cfg.DenyNullBind && len(password) == 0 {
72		return nil, logical.ErrorResponse("password cannot be of zero length when passwordless binds are being denied"), nil, nil
73	}
74
75	ldapClient := ldaputil.Client{
76		Logger: b.Logger(),
77		LDAP:   ldaputil.NewLDAP(),
78	}
79
80	c, err := ldapClient.DialLDAP(cfg.ConfigEntry)
81	if err != nil {
82		return nil, logical.ErrorResponse(err.Error()), nil, nil
83	}
84	if c == nil {
85		return nil, logical.ErrorResponse("invalid connection returned from LDAP dial"), nil, nil
86	}
87
88	// Clean connection
89	defer c.Close()
90
91	userBindDN, err := ldapClient.GetUserBindDN(cfg.ConfigEntry, c, username)
92	if err != nil {
93		if b.Logger().IsDebug() {
94			b.Logger().Debug("error getting user bind DN", "error", err)
95		}
96		return nil, logical.ErrorResponse("ldap operation failed"), nil, nil
97	}
98
99	if b.Logger().IsDebug() {
100		b.Logger().Debug("user binddn fetched", "username", username, "binddn", userBindDN)
101	}
102
103	// Try to bind as the login user. This is where the actual authentication takes place.
104	if len(password) > 0 {
105		err = c.Bind(userBindDN, password)
106	} else {
107		err = c.UnauthenticatedBind(userBindDN)
108	}
109	if err != nil {
110		if b.Logger().IsDebug() {
111			b.Logger().Debug("ldap bind failed", "error", err)
112		}
113		return nil, logical.ErrorResponse("ldap operation failed"), nil, nil
114	}
115
116	// We re-bind to the BindDN if it's defined because we assume
117	// the BindDN should be the one to search, not the user logging in.
118	if cfg.BindDN != "" && cfg.BindPassword != "" {
119		if err := c.Bind(cfg.BindDN, cfg.BindPassword); err != nil {
120			if b.Logger().IsDebug() {
121				b.Logger().Debug("error while attempting to re-bind with the BindDN User", "error", err)
122			}
123			return nil, logical.ErrorResponse("ldap operation failed"), nil, nil
124		}
125		if b.Logger().IsDebug() {
126			b.Logger().Debug("re-bound to original binddn")
127		}
128	}
129
130	userDN, err := ldapClient.GetUserDN(cfg.ConfigEntry, c, userBindDN)
131	if err != nil {
132		return nil, logical.ErrorResponse(err.Error()), nil, nil
133	}
134
135	ldapGroups, err := ldapClient.GetLdapGroups(cfg.ConfigEntry, c, userDN, username)
136	if err != nil {
137		return nil, logical.ErrorResponse(err.Error()), nil, nil
138	}
139	if b.Logger().IsDebug() {
140		b.Logger().Debug("groups fetched from server", "num_server_groups", len(ldapGroups), "server_groups", ldapGroups)
141	}
142
143	ldapResponse := &logical.Response{
144		Data: map[string]interface{}{},
145	}
146	if len(ldapGroups) == 0 {
147		errString := fmt.Sprintf(
148			"no LDAP groups found in groupDN '%s'; only policies from locally-defined groups available",
149			cfg.GroupDN)
150		ldapResponse.AddWarning(errString)
151	}
152
153	var allGroups []string
154	canonicalUsername := username
155	cs := *cfg.CaseSensitiveNames
156	if !cs {
157		canonicalUsername = strings.ToLower(username)
158	}
159	// Import the custom added groups from ldap backend
160	user, err := b.User(ctx, req.Storage, canonicalUsername)
161	if err == nil && user != nil && user.Groups != nil {
162		if b.Logger().IsDebug() {
163			b.Logger().Debug("adding local groups", "num_local_groups", len(user.Groups), "local_groups", user.Groups)
164		}
165		allGroups = append(allGroups, user.Groups...)
166	}
167	// Merge local and LDAP groups
168	allGroups = append(allGroups, ldapGroups...)
169
170	canonicalGroups := allGroups
171	// If not case sensitive, lowercase all
172	if !cs {
173		canonicalGroups = make([]string, len(allGroups))
174		for i, v := range allGroups {
175			canonicalGroups[i] = strings.ToLower(v)
176		}
177	}
178
179	// Retrieve policies
180	var policies []string
181	for _, groupName := range canonicalGroups {
182		group, err := b.Group(ctx, req.Storage, groupName)
183		if err == nil && group != nil {
184			policies = append(policies, group.Policies...)
185		}
186	}
187	if user != nil && user.Policies != nil {
188		policies = append(policies, user.Policies...)
189	}
190	// Policies from each group may overlap
191	policies = strutil.RemoveDuplicates(policies, true)
192
193	return policies, ldapResponse, allGroups, nil
194}
195
196const backendHelp = `
197The "ldap" credential provider allows authentication querying
198a LDAP server, checking username and password, and associating groups
199to set of policies.
200
201Configuration of the server is done through the "config" and "groups"
202endpoints by a user with root access. Authentication is then done
203by supplying the two fields for "login".
204`
205