diff --git a/internal/ldap/config.go b/internal/ldap/config.go index 726ec0a..8334a07 100644 --- a/internal/ldap/config.go +++ b/internal/ldap/config.go @@ -27,6 +27,7 @@ type Config struct { LoginFilter string `yaml:"loginFilter" envconfig:"LDAP_LOGIN_FILTER"` // {{login_identifier}} gets replaced with the login email address SyncFilter string `yaml:"syncFilter" envconfig:"LDAP_SYNC_FILTER"` + SyncGroupFilter string `yaml:"syncGroupFilter" envconfig:"LDAP_SYNC_GROUP_FILTER"` AdminLdapGroup string `yaml:"adminGroup" envconfig:"LDAP_ADMIN_GROUP"` // Members of this group receive admin rights in WG-Portal AdminLdapGroup_ *gldap.DN `yaml:"-"` EveryoneAdmin bool `yaml:"everyoneAdmin" envconfig:"LDAP_EVERYONE_ADMIN"` diff --git a/internal/ldap/ldap.go b/internal/ldap/ldap.go index feec115..17ca6c6 100644 --- a/internal/ldap/ldap.go +++ b/internal/ldap/ldap.go @@ -8,6 +8,13 @@ import ( "github.com/pkg/errors" ) +type ObjectType int64 + +const ( + Users ObjectType = 1 + Groups ObjectType = 2 +) + type RawLdapData struct { DN string Attributes map[string]string @@ -69,21 +76,34 @@ func Close(conn *ldap.Conn) { } } -func FindAllUsers(cfg *Config) ([]RawLdapData, error) { +func FindAllObjects(cfg *Config, objType ObjectType) ([]RawLdapData, error) { client, err := Open(cfg) if err != nil { return nil, errors.WithMessage(err, "failed to open ldap connection") } defer Close(client) - // Search all users - attrs := []string{"dn", cfg.EmailAttribute, cfg.EmailAttribute, cfg.FirstNameAttribute, cfg.LastNameAttribute, - cfg.PhoneAttribute, cfg.GroupMemberAttribute} - searchRequest := ldap.NewSearchRequest( - cfg.BaseDN, - ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, - cfg.SyncFilter, attrs, nil, - ) + var searchRequest *ldap.SearchRequest + var attrs []string + + if objType == Users { + // Search all users + attrs = []string{"dn", cfg.EmailAttribute, cfg.EmailAttribute, cfg.FirstNameAttribute, cfg.LastNameAttribute, + cfg.PhoneAttribute, cfg.GroupMemberAttribute} + searchRequest = ldap.NewSearchRequest( + cfg.BaseDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + cfg.SyncFilter, attrs, nil, + ) + } else if objType == Groups { + // Search all groups + attrs = []string{"dn", cfg.GroupMemberAttribute} + searchRequest = ldap.NewSearchRequest( + cfg.BaseDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + cfg.SyncGroupFilter, attrs, nil, + ) + } sr, err := client.Search(searchRequest) if err != nil { diff --git a/internal/server/configuration.go b/internal/server/configuration.go index 4368283..17bcc6a 100644 --- a/internal/server/configuration.go +++ b/internal/server/configuration.go @@ -114,6 +114,7 @@ func NewConfig() *Config { cfg.LDAP.AdminLdapGroup = "CN=WireGuardAdmins,OU=_O_IT,DC=COMPANY,DC=LOCAL" cfg.LDAP.LoginFilter = "(&(objectClass=organizationalPerson)(mail={{login_identifier}})(!userAccountControl:1.2.840.113556.1.4.803:=2))" cfg.LDAP.SyncFilter = "(&(objectClass=organizationalPerson)(!userAccountControl:1.2.840.113556.1.4.803:=2)(mail=*))" + cfg.LDAP.SyncGroupFilter = "(&(objectClass=group))" cfg.WG.DeviceNames = []string{"wg0"} cfg.WG.DefaultDeviceName = "wg0" diff --git a/internal/server/ldapsync.go b/internal/server/ldapsync.go index bad4cab..098a7ff 100644 --- a/internal/server/ldapsync.go +++ b/internal/server/ldapsync.go @@ -4,20 +4,36 @@ import ( "strings" "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/sirupsen/logrus" "gorm.io/gorm" - - gldap "github.com/go-ldap/ldap/v3" ) func (s *Server) SyncLdapWithUserDatabase() { logrus.Info("starting ldap user synchronization...") + running := true for running { + // 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 { + logrus.Errorf("failed to fetch users from ldap: %v", err) + continue + } + logrus.Tracef("found %d users in ldap", len(ldapUsers)) + + // Update existing LDAP users + s.updateLdapUsers(ldapUsers, ldapGroups) + + // Disable missing LDAP users + s.disableMissingLdapUsers(ldapUsers) + // Select blocks until one of the cases happens select { case <-time.After(1 * time.Minute): @@ -27,42 +43,44 @@ func (s *Server) SyncLdapWithUserDatabase() { running = false continue } - - // Main work here - logrus.Trace("syncing ldap users to database...") - ldapUsers, err := ldap.FindAllUsers(&s.config.LDAP) - if err != nil { - logrus.Errorf("failed to fetch users from ldap: %v", err) - continue - } - logrus.Tracef("found %d users in ldap", len(ldapUsers)) - - // Update existing LDAP users - s.updateLdapUsers(ldapUsers) - - // Disable missing LDAP users - s.disableMissingLdapUsers(ldapUsers) } logrus.Info("ldap user synchronization stopped") } -func (s Server) userIsInAdminGroup(ldapData *ldap.RawLdapData) bool { +func (s Server) userIsInAdminGroup(ldapData *ldap.RawLdapData, ldapGroupData []ldap.RawLdapData, layer int) 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)) 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 } -func (s Server) userChangedInLdap(user *users.User, ldapData *ldap.RawLdapData) bool { +func (s Server) userChangedInLdap(user *users.User, ldapData *ldap.RawLdapData, ldapGroupData []ldap.RawLdapData) bool { if user.Firstname != ldapData.Attributes[s.config.LDAP.FirstNameAttribute] { return true } @@ -83,7 +101,7 @@ func (s Server) userChangedInLdap(user *users.User, ldapData *ldap.RawLdapData) return true } - if user.IsAdmin != s.userIsInAdminGroup(ldapData) { + if user.IsAdmin != s.userIsInAdminGroup(ldapData, ldapGroupData, 0) { return true } @@ -126,7 +144,7 @@ func (s *Server) disableMissingLdapUsers(ldapUsers []ldap.RawLdapData) { } } -func (s *Server) updateLdapUsers(ldapUsers []ldap.RawLdapData) { +func (s *Server) updateLdapUsers(ldapUsers []ldap.RawLdapData, ldapGroups []ldap.RawLdapData) { for i := range ldapUsers { if ldapUsers[i].Attributes[s.config.LDAP.EmailAttribute] == "" { logrus.Tracef("skipping sync of %s, empty email attribute", ldapUsers[i].DN) @@ -152,13 +170,13 @@ func (s *Server) updateLdapUsers(ldapUsers []ldap.RawLdapData) { } // Sync attributes from ldap - if s.userChangedInLdap(user, &ldapUsers[i]) { + if s.userChangedInLdap(user, &ldapUsers[i], ldapGroups) { logrus.Debugf("updating ldap user %s", user.Email) user.Firstname = ldapUsers[i].Attributes[s.config.LDAP.FirstNameAttribute] 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]) + user.IsAdmin = s.userIsInAdminGroup(&ldapUsers[i], ldapGroups, 0) user.Source = users.UserSourceLdap user.DeletedAt = gorm.DeletedAt{} // Not deleted