diff --git a/README.md b/README.md index c4bc8bf..ce63875 100644 --- a/README.md +++ b/README.md @@ -55,8 +55,6 @@ To Do * Add TLS options (skip checks / specify CA) for the Graylog API. * Read object ownership using `grn_permissions` to preserve privileges on users' own objects -* Read group member records from the LDAP server and extract their username - from an attribute. * Support granting ownership on objects * Cleaner CLI * Use goroutines ? Maybe. diff --git a/graylog-groups.yml.example b/graylog-groups.yml.example index 9f56e76..5c7d549 100644 --- a/graylog-groups.yml.example +++ b/graylog-groups.yml.example @@ -37,6 +37,13 @@ ldap: - uniqueMember - 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: diff --git a/main.go b/main.go index faf6e95..36d8f6d 100644 --- a/main.go +++ b/main.go @@ -27,12 +27,13 @@ type ( Host string Port uint16 Tls string - TlsNoVerify bool `yaml:"tls_skip_verify"` - TlsAllowCnOnly bool `yaml:"tls_allow_cn_only"` - CaChain string + TlsNoVerify bool `yaml:"tls_skip_verify"` + TlsAllowCnOnly bool `yaml:"tls_allow_cn_only"` + CaChain string `yaml:"cachain"` BindUser string `yaml:"bind_user"` BindPassword string `yaml:"bind_password"` MemberFields []string `yaml:"member_fields"` + UsernameAttr string `yaml:"username_attribute"` } // Graylog server configuration @@ -194,22 +195,6 @@ func getGraylogUsers(configuration GraylogConfig) (users []GraylogUser) { 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 func getLdapConnection(cfg LdapConfig) (conn *ldap.Conn) { tlsConfig := &tls.Config{ @@ -248,26 +233,79 @@ func getLdapConnection(cfg LdapConfig) (conn *ldap.Conn) { return } -// Read the list of members from a LDAP group -func getGroupMembers(group string, conn *ldap.Conn, fields []string) (members []string) { - req := ldap.NewSearchRequest(group, ldap.ScopeBaseObject, ldap.NeverDerefAliases, 1, 0, false, "(objectClass=*)", fields, nil) +// Run a LDAP query to obtain a single object. +func executeQuery(conn *ldap.Conn, dn string, attrs []string) (bool, *ldap.Entry) { + req := ldap.NewSearchRequest( + dn, + ldap.ScopeBaseObject, ldap.NeverDerefAliases, 1, 0, false, + "(objectClass=*)", attrs, nil) res, err := conn.Search(req) if err != nil { - log.Fatalf("LDAP search for %s: %v", group, err) - } - - for _, entry := range res.Entries { - for _, attr := range fields { - values := entry.GetAttributeValues(attr) - if len(values) == 0 { - continue - } - members = make([]string, len(values)) - for i, value := range values { - members[i] = usernameFromMember(value) - } - break + 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] +} + +// Read a username from a LDAP record based on a DN. +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) + if len(values) == 0 { + continue + } + for _, value := range values { + ok, name := usernameFromMember(value, conn, config) + if ok { + members = append(members, name) + } + } + break } return } @@ -286,7 +324,7 @@ func readLdapGroups(configuration Configuration) (groups GroupMembers) { groups = make(GroupMembers) for group := range configuration.Mapping { - groups[group] = getGroupMembers(group, conn, configuration.Ldap.MemberFields) + groups[group] = getGroupMembers(group, conn, configuration.Ldap) } return }