diff --git a/connector/ldap/ldap.go b/connector/ldap/ldap.go index 4cb7180e..c7ce117d 100644 --- a/connector/ldap/ldap.go +++ b/connector/ldap/ldap.go @@ -11,6 +11,7 @@ import ( "net" "net/url" "os" + "regexp" "strings" "github.com/go-ldap/ldap/v3" @@ -62,6 +63,9 @@ import ( type UserMatcher struct { UserAttr string `json:"userAttr"` GroupAttr string `json:"groupAttr"` + // Work only if UserAttr is 'memberOf' and GroupAttr is dn + GroupRegexp string `json:"groupRegexp"` + groupMatcher *regexp.Regexp // Look for parent groups RecursionGroupAttr string `json:"recursionGroupAttr"` } @@ -298,6 +302,13 @@ func (c *Config) openConnector(logger *slog.Logger) (*ldapConnector, error) { // TODO(nabokihms): remove it after deleting deprecated groupSearch options c.GroupSearch.UserMatchers = userMatchers(c, logger) + for i, _ := range c.GroupSearch.UserMatchers { + c.GroupSearch.UserMatchers[i].groupMatcher, err = regexp.Compile(c.GroupSearch.UserMatchers[i].GroupRegexp) + if err != nil { + logger.Error("Regular expression compilation error", "user_attr", c.GroupSearch.UserMatchers[i].UserAttr, "group_attr", c.GroupSearch.UserMatchers[i].GroupAttr, "err", err.Error()) + c.GroupSearch.UserMatchers[i].groupMatcher, _ = regexp.Compile("") + } + } return &ldapConnector{*c, userSearchScope, groupSearchScope, tlsConfig, logger}, nil } @@ -600,20 +611,62 @@ func (c *ldapConnector) groups(ctx context.Context, user ldap.Entry) ([]string, var groupNames []string + // Initial Search + var groups []*ldap.Entry for _, matcher := range c.GroupSearch.UserMatchers { - // Initial Search - var groups []*ldap.Entry - for _, attr := range c.getAttrs(user, matcher.UserAttr) { - obtained, filter, err := c.queryGroups(ctx, matcher.GroupAttr, attr) - if err != nil { - return nil, err + // When we get groups from memberof user.s entity attribute, we may + // don.t want (Or don.t need) to perform extra search query for each group. + // Also when groupattr is dn (which implies memberof freeipa), we cannot use + // ldapsearch to retrieve group by DN (LDAP restriction) + if strings.ToLower(matcher.UserAttr) == "memberof" && + strings.ToLower(matcher.GroupAttr) == "dn" { + for _, attr := range c.getAttrs(user, matcher.UserAttr) { + // If group dn has no ends with group search base dn - ignore it. + // In FreeIPA case, it also can contain hbac, sudo rules, etc... + if !strings.HasSuffix(attr, c.GroupSearch.BaseDN) { + continue + } + + // Trim {NameAttr}= prefix and baseDN suffix to get group name. + // For NameAttr=cn and BaseDN=cn=groups,dc=example,dc=com : + // cn=groupname,cn=groups,dc=example,dc=com -> groupname + groupName := strings.TrimSuffix( + strings.TrimPrefix(attr, + fmt.Sprintf("%s=", c.GroupSearch.NameAttr)), + fmt.Sprintf(",%s", c.GroupSearch.BaseDN)) + + // Is it needed compability with GroupSearch.Filter? (r9odt) + if !matcher.groupMatcher.MatchString(groupName) { + continue + } + + // Append result to group list as ldap.Entry to process it next wihtout + // extra changes in code. + groups = append(groups, &ldap.Entry{ + DN: attr, + Attributes: []*ldap.EntryAttribute{ + { + Name: c.GroupSearch.NameAttr, + Values: []string{ + groupName, + }, + }, + }, + }) } - gotGroups := len(obtained) != 0 - if !gotGroups { - // TODO(ericchiang): Is this going to spam the logs? - c.logger.Error("ldap: groups search returned no groups", "filter", filter) + } else { + for _, attr := range c.getAttrs(user, matcher.UserAttr) { + obtained, filter, err := c.queryGroups(ctx, matcher.GroupAttr, attr) + if err != nil { + return nil, err + } + gotGroups := len(obtained) != 0 + if !gotGroups { + // TODO(ericchiang): Is this going to spam the logs? + c.logger.Error("ldap: groups search returned no groups", "filter", filter) + } + groups = append(groups, obtained...) } - groups = append(groups, obtained...) } // If RecursionGroupAttr is not set, convert direct groups into names and return