@@ -62,6 +62,8 @@ import (
6262type UserMatcher struct {
6363 UserAttr string `json:"userAttr"`
6464 GroupAttr string `json:"groupAttr"`
65+ // Look for parent groups
66+ RecursionGroupAttr string `json:"recursionGroupAttr"`
6567}
6668
6769// Config holds configuration options for LDAP logins.
@@ -144,6 +146,8 @@ type Config struct {
144146 UserAttr string `json:"userAttr"`
145147 GroupAttr string `json:"groupAttr"`
146148
149+ RecursionGroupAttr string `json:"recursionGroupAttr"`
150+
147151 // Array of the field pairs used to match a user to a group.
148152 // See the "UserMatcher" struct for the exact field names
149153 //
@@ -197,8 +201,9 @@ func userMatchers(c *Config, logger *slog.Logger) []UserMatcher {
197201 logger .Warn (`use "groupSearch.userMatchers" option instead of "userAttr/groupAttr" fields` , "deprecated" , true )
198202 return []UserMatcher {
199203 {
200- UserAttr : c .GroupSearch .UserAttr ,
201- GroupAttr : c .GroupSearch .GroupAttr ,
204+ UserAttr : c .GroupSearch .UserAttr ,
205+ GroupAttr : c .GroupSearch .GroupAttr ,
206+ RecursionGroupAttr : c .GroupSearch .RecursionGroupAttr ,
202207 },
203208 }
204209}
@@ -591,57 +596,120 @@ func (c *ldapConnector) groups(ctx context.Context, user ldap.Entry) ([]string,
591596 return nil , nil
592597 }
593598
594- var groups []* ldap.Entry
599+ var groupNames []string
600+
595601 for _ , matcher := range c .GroupSearch .UserMatchers {
602+ // Initial Search
603+ var groups []* ldap.Entry
596604 for _ , attr := range c .getAttrs (user , matcher .UserAttr ) {
597- filter := fmt .Sprintf ("(%s=%s)" , matcher .GroupAttr , ldap .EscapeFilter (attr ))
598- if c .GroupSearch .Filter != "" {
599- filter = fmt .Sprintf ("(&%s%s)" , c .GroupSearch .Filter , filter )
605+ obtained , filter , err := c .queryGroups (ctx , matcher .GroupAttr , attr )
606+ if err != nil {
607+ return nil , err
608+ }
609+ gotGroups := len (obtained ) != 0
610+ if ! gotGroups {
611+ // TODO(ericchiang): Is this going to spam the logs?
612+ c .logger .Error ("ldap: groups search returned no groups" , "filter" , filter )
600613 }
614+ groups = append (groups , obtained ... )
615+ }
601616
602- req := & ldap.SearchRequest {
603- BaseDN : c .GroupSearch .BaseDN ,
604- Filter : filter ,
605- Scope : c .groupSearchScope ,
606- Attributes : []string {c .GroupSearch .NameAttr },
617+ // If RecursionGroupAttr is not set, convert direct groups into names and return
618+ if matcher .RecursionGroupAttr == "" {
619+ for _ , group := range groups {
620+ name := c .getAttr (* group , c .GroupSearch .NameAttr )
621+ if name == "" {
622+ return nil , fmt .Errorf (
623+ "ldap: group entity %q missing required attribute %q" ,
624+ group .DN , c .GroupSearch .NameAttr ,
625+ )
626+ }
627+ groupNames = append (groupNames , name )
607628 }
629+ continue
630+ }
631+
632+ // Recursive Search
633+ c .logger .Info ("Recursive group search enabled" , "groupAttr" , matcher .GroupAttr , "recursionAttr" , matcher .RecursionGroupAttr )
634+ for {
635+ var nextLevel []* ldap.Entry
636+ for _ , group := range groups {
637+ name := c .getAttr (* group , c .GroupSearch .NameAttr )
638+ if name == "" {
639+ return nil , fmt .Errorf ("ldap: group entity %q missing required attribute %q" ,
640+ group .DN , c .GroupSearch .NameAttr )
641+ }
642+
643+ // Prevent duplicates and circular references.
644+ duplicate := false
645+ for _ , existingName := range groupNames {
646+ if name == existingName {
647+ c .logger .Debug ("Found duplicate group" , "name" , name )
648+ duplicate = true
649+ break
650+ }
651+ }
652+ if duplicate {
653+ continue
654+ }
655+
656+ groupNames = append (groupNames , name )
608657
609- gotGroups := false
610- if err := c .do (ctx , func (conn * ldap.Conn ) error {
611- c .logger .Info ("performing ldap search" ,
612- "base_dn" , req .BaseDN , "scope" , scopeString (req .Scope ), "filter" , req .Filter )
613- resp , err := conn .Search (req )
658+ // Search for parent groups using the group's DN.
659+ parents , filter , err := c .queryGroups (ctx , matcher .RecursionGroupAttr , group .DN )
614660 if err != nil {
615- return fmt .Errorf ("ldap: search failed: %v" , err )
661+ return nil , err
662+ }
663+ if len (parents ) == 0 {
664+ c .logger .Debug ("No parent groups found" , "filter" , filter )
665+ } else {
666+ nextLevel = append (nextLevel , parents ... )
616667 }
617- gotGroups = len (resp .Entries ) != 0
618- groups = append (groups , resp .Entries ... )
619- return nil
620- }); err != nil {
621- return nil , err
622668 }
623- if ! gotGroups {
624- // TODO(ericchiang): Is this going to spam the logs?
625- c .logger .Error ("groups search returned no groups" , "filter" , filter )
669+ if len (nextLevel ) == 0 {
670+ break
626671 }
672+ groups = nextLevel
627673 }
628674 }
675+ return groupNames , nil
676+ }
629677
630- groupNames := make ([]string , 0 , len (groups ))
631- for _ , group := range groups {
632- name := c .getAttr (* group , c .GroupSearch .NameAttr )
633- if name == "" {
634- // Be obnoxious about missing attributes. If the group entry is
635- // missing its name attribute, that indicates a misconfiguration.
636- //
637- // In the future we can add configuration options to just log these errors.
638- return nil , fmt .Errorf ("ldap: group entity %q missing required attribute %q" ,
639- group .DN , c .GroupSearch .NameAttr )
640- }
678+ func (c * ldapConnector ) queryGroups (ctx context.Context , memberAttr , dn string ) ([]* ldap.Entry , string , error ) {
679+ filter := fmt .Sprintf ("(%s=%s)" , memberAttr , ldap .EscapeFilter (dn ))
680+ if c .GroupSearch .Filter != "" {
681+ filter = fmt .Sprintf ("(&%s%s)" , c .GroupSearch .Filter , filter )
682+ }
641683
642- groupNames = append (groupNames , name )
684+ req := & ldap.SearchRequest {
685+ BaseDN : c .GroupSearch .BaseDN ,
686+ Filter : filter ,
687+ Scope : c .groupSearchScope ,
688+ Attributes : []string {c .GroupSearch .NameAttr },
689+ }
690+
691+ var entries []* ldap.Entry
692+ if err := c .do (ctx , func (conn * ldap.Conn ) error {
693+ c .logger .Info (
694+ "performing ldap search" ,
695+ "base_dn" , req .BaseDN ,
696+ "scope" , scopeString (req .Scope ),
697+ "filter" , req .Filter ,
698+ )
699+ resp , err := conn .Search (req )
700+ if err != nil {
701+ if ldapErr , ok := err .(* ldap.Error ); ok && ldapErr .ResultCode == ldap .LDAPResultNoSuchObject {
702+ c .logger .Info ("LDAP search returned no groups" , "filter" , filter )
703+ return nil
704+ }
705+ return fmt .Errorf ("ldap: search failed: %v" , err )
706+ }
707+ entries = append (entries , resp .Entries ... )
708+ return nil
709+ }); err != nil {
710+ return nil , filter , err
643711 }
644- return groupNames , nil
712+ return entries , filter , nil
645713}
646714
647715func (c * ldapConnector ) Prompt () string {
0 commit comments