diff --git a/README.md b/README.md index 704dddf..d9e140b 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,8 @@ The following configuration options are available: | LDAP_USER | user | ldap | company\\\\ldap_wireguard | The bind user. | | LDAP_PASSWORD | pass | ldap | SuperSecret | The bind password. | | LDAP_LOGIN_FILTER | loginFilter | ldap | (&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2)) | {{login_identifier}} will be replaced with the login email address. | -| LDAP_SYNC_FILTER | syncFilter | ldap | (&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*)) | The filter string for the LDAP synchronization service. | +| LDAP_SYNC_FILTER | syncFilter | ldap | (&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*)) | The filter string for the LDAP synchronization service. Users matching this filter will be synchronized with the WireGuard Portal database. | +| LDAP_SYNC_GROUP_FILTER | syncGroupFilter | ldap | (&(objectClass=group)) | The filter string for the LDAP groups. The groups are used to recursively check for admin group member ship of users. | | LDAP_ADMIN_GROUP | adminGroup | ldap | CN=WireGuardAdmins,OU=_O_IT,DC=COMPANY,DC=LOCAL | Users in this group are marked as administrators. | | LDAP_ATTR_EMAIL | attrEmail | ldap | mail | User email attribute. | | LDAP_ATTR_FIRSTNAME | attrFirstname | ldap | givenName | User firstname attribute. | diff --git a/internal/ldap/ldap.go b/internal/ldap/ldap.go index 17ca6c6..cad3e76 100644 --- a/internal/ldap/ldap.go +++ b/internal/ldap/ldap.go @@ -8,11 +8,11 @@ import ( "github.com/pkg/errors" ) -type ObjectType int64 +type ObjectType int const ( - Users ObjectType = 1 - Groups ObjectType = 2 + Users ObjectType = iota + Groups ) type RawLdapData struct { @@ -86,7 +86,8 @@ func FindAllObjects(cfg *Config, objType ObjectType) ([]RawLdapData, error) { var searchRequest *ldap.SearchRequest var attrs []string - if objType == Users { + switch objType { + case Users: // Search all users attrs = []string{"dn", cfg.EmailAttribute, cfg.EmailAttribute, cfg.FirstNameAttribute, cfg.LastNameAttribute, cfg.PhoneAttribute, cfg.GroupMemberAttribute} @@ -95,7 +96,7 @@ func FindAllObjects(cfg *Config, objType ObjectType) ([]RawLdapData, error) { ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, cfg.SyncFilter, attrs, nil, ) - } else if objType == Groups { + case Groups: // Search all groups attrs = []string{"dn", cfg.GroupMemberAttribute} searchRequest = ldap.NewSearchRequest( @@ -103,6 +104,8 @@ func FindAllObjects(cfg *Config, objType ObjectType) ([]RawLdapData, error) { ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, cfg.SyncGroupFilter, attrs, nil, ) + default: + panic("invalid object type") } sr, err := client.Search(searchRequest) diff --git a/internal/server/ldapsync.go b/internal/server/ldapsync.go index 098a7ff..a5f126b 100644 --- a/internal/server/ldapsync.go +++ b/internal/server/ldapsync.go @@ -5,10 +5,9 @@ import ( "time" gldap "github.com/go-ldap/ldap/v3" - "github.com/h44z/wg-portal/internal/wireguard" - "github.com/h44z/wg-portal/internal/ldap" "github.com/h44z/wg-portal/internal/users" + "github.com/h44z/wg-portal/internal/wireguard" "github.com/sirupsen/logrus" "gorm.io/gorm" ) @@ -21,12 +20,17 @@ func (s *Server) SyncLdapWithUserDatabase() { // Main work here logrus.Trace("syncing ldap users to database...") ldapUsers, err := ldap.FindAllObjects(&s.config.LDAP, ldap.Users) - ldapGroups, errGroups := ldap.FindAllObjects(&s.config.LDAP, ldap.Groups) - if err != nil && errGroups != nil { + if err != nil { logrus.Errorf("failed to fetch users from ldap: %v", err) continue } - logrus.Tracef("found %d users in ldap", len(ldapUsers)) + ldapGroups, err := ldap.FindAllObjects(&s.config.LDAP, ldap.Groups) + if err != nil { + logrus.Errorf("failed to fetch groups from ldap: %v", err) + continue + } + + logrus.Tracef("found %d users and %d groups in ldap", len(ldapUsers), len(ldapGroups)) // Update existing LDAP users s.updateLdapUsers(ldapUsers, ldapGroups) @@ -34,6 +38,8 @@ func (s *Server) SyncLdapWithUserDatabase() { // Disable missing LDAP users s.disableMissingLdapUsers(ldapUsers) + logrus.Trace("synchronized ldap users to database") + // Select blocks until one of the cases happens select { case <-time.After(1 * time.Minute): @@ -47,35 +53,62 @@ func (s *Server) SyncLdapWithUserDatabase() { logrus.Info("ldap user synchronization stopped") } -func (s Server) userIsInAdminGroup(ldapData *ldap.RawLdapData, ldapGroupData []ldap.RawLdapData, layer int) bool { +func (s Server) userIsInAdminGroup(userData *ldap.RawLdapData, groupTreeData []ldap.RawLdapData) bool { if s.config.LDAP.EveryoneAdmin { return true } if s.config.LDAP.AdminLdapGroup_ == nil { return false } - //fmt.Printf("%+v\n", ldapData.Attributes) - var prefix string - for i := 0; i < layer; i++ { - prefix += "+" - } - logrus.Tracef("%s Group layer: %d\n", prefix, layer) - for _, group := range ldapData.RawAttributes[s.config.LDAP.GroupMemberAttribute] { - logrus.Tracef("%s%s\n", prefix, string(group)) - var dn, _ = gldap.ParseDN(string(group)) - if s.config.LDAP.AdminLdapGroup_.Equal(dn) { - logrus.Tracef("%sFOUND: %s\n", prefix, string(group)) + + for _, userGroup := range userData.RawAttributes[s.config.LDAP.GroupMemberAttribute] { + var userGroupDn, _ = gldap.ParseDN(string(userGroup)) + if s.dnIsAdminGroup(userGroupDn, groupTreeData) { return true } - for _, group2 := range ldapGroupData { - if group2.DN == string(group) { - logrus.Tracef("%sChecking nested: %s\n", prefix, group2.DN) - isAdmin := s.userIsInAdminGroup(&group2, ldapGroupData, layer+1) - if isAdmin { - return true - } + } + return false +} + +// dnIsAdminGroup checks if the given DN is equal to the admin group, or if it is included in a groupTree that has the +// admin group as parent/root. +// +// WGPortal-Admin (L0) +// +// \_ IT-Admin (L1) +// |_ Alice (L2) +// |_ Bob (L2) +// \_ Eve (L2) +// \_ External-Company (L1) +// |_ External-Admin (L2) +// |_ Sam (L3) +// \_ Steve (L3) +// +// All DNs in the example above are member of the admin group. +func (s Server) dnIsAdminGroup(dn *gldap.DN, groupTreeData []ldap.RawLdapData) bool { + if s.config.LDAP.AdminLdapGroup_ == nil { + return false + } + + if s.config.LDAP.AdminLdapGroup_.Equal(dn) { + return true + } + + // Recursively check the whole group tree + for _, group := range groupTreeData { + var groupDn, _ = gldap.ParseDN(group.DN) + if !dn.Equal(groupDn) { + continue + } + + for _, parentGroupDn := range group.RawAttributes[s.config.LDAP.GroupMemberAttribute] { + var parentDn, _ = gldap.ParseDN(string(parentGroupDn)) + if s.dnIsAdminGroup(parentDn, groupTreeData) { + return true } } + + break } return false } @@ -101,7 +134,7 @@ func (s Server) userChangedInLdap(user *users.User, ldapData *ldap.RawLdapData, return true } - if user.IsAdmin != s.userIsInAdminGroup(ldapData, ldapGroupData, 0) { + if user.IsAdmin != s.userIsInAdminGroup(ldapData, ldapGroupData) { return true } @@ -176,7 +209,7 @@ func (s *Server) updateLdapUsers(ldapUsers []ldap.RawLdapData, ldapGroups []ldap user.Lastname = ldapUsers[i].Attributes[s.config.LDAP.LastNameAttribute] user.Email = ldapUsers[i].Attributes[s.config.LDAP.EmailAttribute] user.Phone = ldapUsers[i].Attributes[s.config.LDAP.PhoneAttribute] - user.IsAdmin = s.userIsInAdminGroup(&ldapUsers[i], ldapGroups, 0) + user.IsAdmin = s.userIsInAdminGroup(&ldapUsers[i], ldapGroups) user.Source = users.UserSourceLdap user.DeletedAt = gorm.DeletedAt{} // Not deleted