Read username from referenced LDAP record

* The `username_attribute` configuration value was added to the `ldap`
  section. When this value is set, the program will not try to extract the
  username from DNs; instead, it will look them up and extract the
  username from the referenced record, using the specified attribute.

* The program will no longer exit in error when a group listed in the
  mapping doesn't exist.
This commit is contained in:
Emmanuel BENOîT 2021-02-09 23:15:24 +01:00
parent 9bec0ad14e
commit 5c014aa951
3 changed files with 82 additions and 39 deletions

View file

@ -55,8 +55,6 @@ To Do
* Add TLS options (skip checks / specify CA) for the Graylog API. * Add TLS options (skip checks / specify CA) for the Graylog API.
* Read object ownership using `grn_permissions` to preserve privileges on users' * Read object ownership using `grn_permissions` to preserve privileges on users'
own objects own objects
* Read group member records from the LDAP server and extract their username
from an attribute.
* Support granting ownership on objects * Support granting ownership on objects
* Cleaner CLI * Cleaner CLI
* Use goroutines ? Maybe. * Use goroutines ? Maybe.

View file

@ -37,6 +37,13 @@ ldap:
- uniqueMember - uniqueMember
- memberUid - memberUid
# Username attribute. This is used when group member fields contain the '='
# ',' character, in which case the value will be considered a DN and looked up
# in the LDAP. The field specified by this configuration value will be read
# and used as the login name. If this configuration value is not set, the
# first element in the DN will be extracted and used as the username.
username_attribute: uid
# Graylog server info # Graylog server info
# -------------------- # --------------------
graylog: graylog:

94
main.go
View file

@ -29,10 +29,11 @@ type (
Tls string Tls string
TlsNoVerify bool `yaml:"tls_skip_verify"` TlsNoVerify bool `yaml:"tls_skip_verify"`
TlsAllowCnOnly bool `yaml:"tls_allow_cn_only"` TlsAllowCnOnly bool `yaml:"tls_allow_cn_only"`
CaChain string CaChain string `yaml:"cachain"`
BindUser string `yaml:"bind_user"` BindUser string `yaml:"bind_user"`
BindPassword string `yaml:"bind_password"` BindPassword string `yaml:"bind_password"`
MemberFields []string `yaml:"member_fields"` MemberFields []string `yaml:"member_fields"`
UsernameAttr string `yaml:"username_attribute"`
} }
// Graylog server configuration // Graylog server configuration
@ -194,22 +195,6 @@ func getGraylogUsers(configuration GraylogConfig) (users []GraylogUser) {
return return
} }
// Extract an username from something that may be an username or a DN.
func usernameFromMember(member string) string {
eqPos := strings.Index(member, "=")
if eqPos == -1 {
return member
}
commaPos := strings.Index(member, ",")
if commaPos == -1 {
return member[eqPos+1:]
}
if eqPos > commaPos {
log.Fatalf("couldn't extract user name from %s", member)
}
return member[eqPos+1 : commaPos]
}
// Establish a connection to the LDAP server // Establish a connection to the LDAP server
func getLdapConnection(cfg LdapConfig) (conn *ldap.Conn) { func getLdapConnection(cfg LdapConfig) (conn *ldap.Conn) {
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
@ -248,27 +233,80 @@ func getLdapConnection(cfg LdapConfig) (conn *ldap.Conn) {
return return
} }
// Read the list of members from a LDAP group // Run a LDAP query to obtain a single object.
func getGroupMembers(group string, conn *ldap.Conn, fields []string) (members []string) { func executeQuery(conn *ldap.Conn, dn string, attrs []string) (bool, *ldap.Entry) {
req := ldap.NewSearchRequest(group, ldap.ScopeBaseObject, ldap.NeverDerefAliases, 1, 0, false, "(objectClass=*)", fields, nil) req := ldap.NewSearchRequest(
dn,
ldap.ScopeBaseObject, ldap.NeverDerefAliases, 1, 0, false,
"(objectClass=*)", attrs, nil)
res, err := conn.Search(req) res, err := conn.Search(req)
if err != nil { if err != nil {
log.Fatalf("LDAP search for %s: %v", group, err) ldapError, ok := err.(*ldap.Error)
if ok && ldapError.ResultCode == ldap.LDAPResultNoSuchObject {
return false, nil
} }
log.Fatalf("LDAP search for %s: %v", dn, err)
}
if len(res.Entries) > 1 {
log.Printf("LDAP search for %s returned more than 1 record", dn)
return false, nil
}
return true, res.Entries[0]
}
for _, entry := range res.Entries { // Read a username from a LDAP record based on a DN.
for _, attr := range fields { func readUsernameFromLdap(dn string, conn *ldap.Conn, attr string) (bool, string) {
ok, res := executeQuery(conn, dn, []string{attr})
if !ok {
return false, ""
}
values := res.GetAttributeValues(attr)
if len(values) != 1 {
log.Printf("LDAP search for %s: attribute %s has %d values", dn, attr, len(values))
return false, ""
}
return true, values[0]
}
// Extract an username from something that may be an username or a DN.
func usernameFromMember(member string, conn *ldap.Conn, config LdapConfig) (bool, string) {
eqPos := strings.Index(member, "=")
if eqPos == -1 {
return true, member
}
if config.UsernameAttr != "" {
return readUsernameFromLdap(member, conn, config.UsernameAttr)
}
commaPos := strings.Index(member, ",")
if commaPos == -1 {
return true, member[eqPos+1:]
}
if eqPos > commaPos {
log.Printf("couldn't extract user name from %s", member)
return false, ""
}
return true, member[eqPos+1 : commaPos]
}
// Read the list of members from a LDAP group
func getGroupMembers(group string, conn *ldap.Conn, config LdapConfig) (members []string) {
ok, entry := executeQuery(conn, group, config.MemberFields)
if !ok {
return
}
for _, attr := range config.MemberFields {
values := entry.GetAttributeValues(attr) values := entry.GetAttributeValues(attr)
if len(values) == 0 { if len(values) == 0 {
continue continue
} }
members = make([]string, len(values)) for _, value := range values {
for i, value := range values { ok, name := usernameFromMember(value, conn, config)
members[i] = usernameFromMember(value) if ok {
members = append(members, name)
}
} }
break break
} }
}
return return
} }
@ -286,7 +324,7 @@ func readLdapGroups(configuration Configuration) (groups GroupMembers) {
groups = make(GroupMembers) groups = make(GroupMembers)
for group := range configuration.Mapping { for group := range configuration.Mapping {
groups[group] = getGroupMembers(group, conn, configuration.Ldap.MemberFields) groups[group] = getGroupMembers(group, conn, configuration.Ldap)
} }
return return
} }