Skip to content

Commit b89dec9

Browse files
authored
Merge pull request cesanta#228 from mrueg/ldap
MAP LDAP account attributes to labels such as groups
2 parents 1e13883 + 1bc7597 commit b89dec9

File tree

2 files changed

+109
-25
lines changed

2 files changed

+109
-25
lines changed

auth_server/authn/ldap_auth.go

Lines changed: 93 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package authn
1818

1919
import (
20-
"bytes"
2120
"crypto/tls"
2221
"crypto/x509"
2322
"fmt"
@@ -28,17 +27,21 @@ import (
2827
"github.com/cesanta/glog"
2928
)
3029

30+
type LabelMap struct {
31+
Attribute string `yaml:"attribute,omitempty"`
32+
ParseCN bool `yaml:"parse_cn,omitempty"`
33+
}
34+
3135
type LDAPAuthConfig struct {
32-
Addr string `yaml:"addr,omitempty"`
33-
TLS string `yaml:"tls,omitempty"`
34-
InsecureTLSSkipVerify bool `yaml:"insecure_tls_skip_verify,omitempty"`
35-
CACertificate string `yaml:"ca_certificate,omitempty"`
36-
Base string `yaml:"base,omitempty"`
37-
Filter string `yaml:"filter,omitempty"`
38-
BindDN string `yaml:"bind_dn,omitempty"`
39-
BindPasswordFile string `yaml:"bind_password_file,omitempty"`
40-
GroupBaseDN string `yaml:"group_base_dn,omitempty"`
41-
GroupFilter string `yaml:"group_filter,omitempty"`
36+
Addr string `yaml:"addr,omitempty"`
37+
TLS string `yaml:"tls,omitempty"`
38+
InsecureTLSSkipVerify bool `yaml:"insecure_tls_skip_verify,omitempty"`
39+
CACertificate string `yaml:"ca_certificate,omitempty"`
40+
Base string `yaml:"base,omitempty"`
41+
Filter string `yaml:"filter,omitempty"`
42+
BindDN string `yaml:"bind_dn,omitempty"`
43+
BindPasswordFile string `yaml:"bind_password_file,omitempty"`
44+
LabelMaps map[string]LabelMap `yaml:"labels,omitempty"`
4245
}
4346

4447
type LDAPAuth struct {
@@ -73,13 +76,20 @@ func (la *LDAPAuth) Authenticate(account string, password PasswordString) (bool,
7376
account = la.escapeAccountInput(account)
7477

7578
filter := la.getFilter(account)
76-
accountEntryDN, uSearchErr := la.ldapSearch(l, &la.config.Base, &filter, &[]string{})
79+
80+
labelAttributes, labelsConfigErr := la.getLabelAttributes()
81+
if labelsConfigErr != nil {
82+
return false, nil, labelsConfigErr
83+
}
84+
85+
accountEntryDN, entryAttrMap, uSearchErr := la.ldapSearch(l, &la.config.Base, &filter, &labelAttributes)
7786
if uSearchErr != nil {
7887
return false, nil, uSearchErr
7988
}
8089
if accountEntryDN == "" {
8190
return false, nil, NoMatch // User does not exist
8291
}
92+
8393
// Bind as the user to verify their password
8494
if len(accountEntryDN) > 0 {
8595
err := l.Bind(accountEntryDN, string(password))
@@ -95,7 +105,13 @@ func (la *LDAPAuth) Authenticate(account string, password PasswordString) (bool,
95105
return false, nil, bindErr
96106
}
97107

98-
return true, nil, nil
108+
// Extract labels from the attribute values
109+
labels, labelsExtractErr := la.getLabelsFromMap(entryAttrMap)
110+
if labelsExtractErr != nil {
111+
return false, nil, labelsExtractErr
112+
}
113+
114+
return true, labels, nil
99115
}
100116

101117
func (la *LDAPAuth) bindReadOnlyUser(l *ldap.Conn) error {
@@ -185,9 +201,9 @@ func (la *LDAPAuth) getFilter(account string) string {
185201

186202
//ldap search and return required attributes' value from searched entries
187203
//default return entry's DN value if you leave attrs array empty
188-
func (la *LDAPAuth) ldapSearch(l *ldap.Conn, baseDN *string, filter *string, attrs *[]string) (string, error) {
204+
func (la *LDAPAuth) ldapSearch(l *ldap.Conn, baseDN *string, filter *string, attrs *[]string) (string, map[string][]string, error) {
189205
if l == nil {
190-
return "", fmt.Errorf("No ldap connection!")
206+
return "", nil, fmt.Errorf("No ldap connection!")
191207
}
192208
glog.V(2).Infof("Searching...basedDN:%s, filter:%s", *baseDN, *filter)
193209
searchRequest := ldap.NewSearchRequest(
@@ -198,30 +214,82 @@ func (la *LDAPAuth) ldapSearch(l *ldap.Conn, baseDN *string, filter *string, att
198214
nil)
199215
sr, err := l.Search(searchRequest)
200216
if err != nil {
201-
return "", err
217+
return "", nil, err
202218
}
203219

204220
if len(sr.Entries) == 0 {
205-
return "", nil // User does not exist
221+
return "", nil, nil // User does not exist
206222
} else if len(sr.Entries) > 1 {
207-
return "", fmt.Errorf("Too many entries returned.")
223+
return "", nil, fmt.Errorf("Too many entries returned.")
208224
}
209225

210-
var buffer bytes.Buffer
226+
attributes := make(map[string][]string)
227+
var entryDn string
211228
for _, entry := range sr.Entries {
229+
entryDn = entry.DN
212230
if len(*attrs) == 0 {
213-
glog.V(2).Infof("Entry DN = %s", entry.DN)
214-
buffer.WriteString(entry.DN)
231+
glog.V(2).Infof("Entry DN = %s", entryDn)
215232
} else {
216233
for _, attr := range *attrs {
217-
values := strings.Join(entry.GetAttributeValues(attr), " ")
218-
glog.V(2).Infof("Entry %s = %s", attr, values)
219-
buffer.WriteString(values)
234+
values := entry.GetAttributeValues(attr)
235+
glog.V(2).Infof("Entry %s = %s", attr, strings.Join(values, "\n"))
236+
attributes[attr] = values
237+
}
238+
}
239+
}
240+
241+
return entryDn, attributes, nil
242+
}
243+
244+
func (la *LDAPAuth) getLabelAttributes() ([]string, error) {
245+
labelAttributes := make([]string, len(la.config.LabelMaps))
246+
i := 0
247+
for key, mapping := range la.config.LabelMaps {
248+
if mapping.Attribute == "" {
249+
return nil, fmt.Errorf("Label %s is missing 'attribute' to map from", key)
250+
}
251+
labelAttributes[i] = mapping.Attribute
252+
i++
253+
}
254+
return labelAttributes, nil
255+
}
256+
257+
func (la *LDAPAuth) getLabelsFromMap(attrMap map[string][]string) (map[string][]string, error) {
258+
labels := make(map[string][]string)
259+
for key, mapping := range la.config.LabelMaps {
260+
if mapping.Attribute == "" {
261+
return nil, fmt.Errorf("Label %s is missing 'attribute' to map from", key)
262+
}
263+
264+
mappingValues := attrMap[mapping.Attribute]
265+
if mappingValues != nil {
266+
if mapping.ParseCN {
267+
// shorten attribute to its common name
268+
for i, value := range mappingValues {
269+
cn := la.getCNFromDN(value)
270+
mappingValues[i] = cn
271+
}
272+
}
273+
labels[key] = mappingValues
274+
}
275+
}
276+
return labels, nil
277+
}
278+
279+
func (la *LDAPAuth) getCNFromDN(dn string) string {
280+
parsedDN, err := ldap.ParseDN(dn)
281+
if err != nil || len(parsedDN.RDNs) > 0 {
282+
for _, rdn := range parsedDN.RDNs {
283+
for _, rdnAttr := range rdn.Attributes {
284+
if rdnAttr.Type == "CN" {
285+
return rdnAttr.Value
286+
}
220287
}
221288
}
222289
}
223290

224-
return buffer.String(), nil
291+
// else try using raw DN
292+
return dn
225293
}
226294

227295
func (la *LDAPAuth) Stop() {

examples/reference.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,16 @@ ldap_auth:
143143
# User query settings. ${account} is expanded from auth request
144144
base: o=example.com
145145
filter: (&(uid=${account})(objectClass=person))
146+
# Labels can be mapped from LDAP attributes
147+
labels:
148+
# Add the user's title to a label called title
149+
title:
150+
attribute: title
151+
# Add the user's memberOf values to a label called groups
152+
groups:
153+
attribute: memberOf
154+
# Special handling to simplify the values to just the common name
155+
parse_cn: true
146156

147157
mongo_auth:
148158
# Essentially all options are described here: https://godoc.org/gopkg.in/mgo.v2#DialInfo
@@ -256,6 +266,12 @@ acl:
256266
- match: {name: "${labels:project}-{labels:tier}/*"}
257267
actions: ["push", "pull"]
258268
comment: "Users can push to a project-tier/* that they are assigned to"
269+
- match: {labels: {"title": "Developer"}}
270+
actions: ["*"]
271+
comment: "If you call yourself a developer you can do anything (this ACL is an example for LDAP labels as defined above)"
272+
- match: {labels: {"groups": "Admin"}}
273+
actions: ["push"]
274+
comment: "If you are part of the admin group you can push. (this ACL is an example for LDAP labels as defined above)"
259275
# Access is denied by default.
260276

261277
# (optional) Define to query ACL from a MongoDB server.

0 commit comments

Comments
 (0)