cleanup recursive ldap group sync

This commit is contained in:
Christoph Haas 2022-12-27 13:25:28 +01:00
parent f2afd4a21c
commit 53a6602a64
3 changed files with 69 additions and 32 deletions

View File

@ -175,7 +175,8 @@ The following configuration options are available:
| LDAP_USER | user | ldap | company\\\\ldap_wireguard | The bind user. | | LDAP_USER | user | ldap | company\\\\ldap_wireguard | The bind user. |
| LDAP_PASSWORD | pass | ldap | SuperSecret | The bind password. | | 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_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_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_EMAIL | attrEmail | ldap | mail | User email attribute. |
| LDAP_ATTR_FIRSTNAME | attrFirstname | ldap | givenName | User firstname attribute. | | LDAP_ATTR_FIRSTNAME | attrFirstname | ldap | givenName | User firstname attribute. |

View File

@ -8,11 +8,11 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
type ObjectType int64 type ObjectType int
const ( const (
Users ObjectType = 1 Users ObjectType = iota
Groups ObjectType = 2 Groups
) )
type RawLdapData struct { type RawLdapData struct {
@ -86,7 +86,8 @@ func FindAllObjects(cfg *Config, objType ObjectType) ([]RawLdapData, error) {
var searchRequest *ldap.SearchRequest var searchRequest *ldap.SearchRequest
var attrs []string var attrs []string
if objType == Users { switch objType {
case Users:
// Search all users // Search all users
attrs = []string{"dn", cfg.EmailAttribute, cfg.EmailAttribute, cfg.FirstNameAttribute, cfg.LastNameAttribute, attrs = []string{"dn", cfg.EmailAttribute, cfg.EmailAttribute, cfg.FirstNameAttribute, cfg.LastNameAttribute,
cfg.PhoneAttribute, cfg.GroupMemberAttribute} cfg.PhoneAttribute, cfg.GroupMemberAttribute}
@ -95,7 +96,7 @@ func FindAllObjects(cfg *Config, objType ObjectType) ([]RawLdapData, error) {
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
cfg.SyncFilter, attrs, nil, cfg.SyncFilter, attrs, nil,
) )
} else if objType == Groups { case Groups:
// Search all groups // Search all groups
attrs = []string{"dn", cfg.GroupMemberAttribute} attrs = []string{"dn", cfg.GroupMemberAttribute}
searchRequest = ldap.NewSearchRequest( searchRequest = ldap.NewSearchRequest(
@ -103,6 +104,8 @@ func FindAllObjects(cfg *Config, objType ObjectType) ([]RawLdapData, error) {
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
cfg.SyncGroupFilter, attrs, nil, cfg.SyncGroupFilter, attrs, nil,
) )
default:
panic("invalid object type")
} }
sr, err := client.Search(searchRequest) sr, err := client.Search(searchRequest)

View File

@ -5,10 +5,9 @@ import (
"time" "time"
gldap "github.com/go-ldap/ldap/v3" 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/ldap"
"github.com/h44z/wg-portal/internal/users" "github.com/h44z/wg-portal/internal/users"
"github.com/h44z/wg-portal/internal/wireguard"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"gorm.io/gorm" "gorm.io/gorm"
) )
@ -21,12 +20,17 @@ func (s *Server) SyncLdapWithUserDatabase() {
// Main work here // Main work here
logrus.Trace("syncing ldap users to database...") logrus.Trace("syncing ldap users to database...")
ldapUsers, err := ldap.FindAllObjects(&s.config.LDAP, ldap.Users) ldapUsers, err := ldap.FindAllObjects(&s.config.LDAP, ldap.Users)
ldapGroups, errGroups := ldap.FindAllObjects(&s.config.LDAP, ldap.Groups) if err != nil {
if err != nil && errGroups != nil {
logrus.Errorf("failed to fetch users from ldap: %v", err) logrus.Errorf("failed to fetch users from ldap: %v", err)
continue 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 // Update existing LDAP users
s.updateLdapUsers(ldapUsers, ldapGroups) s.updateLdapUsers(ldapUsers, ldapGroups)
@ -34,6 +38,8 @@ func (s *Server) SyncLdapWithUserDatabase() {
// Disable missing LDAP users // Disable missing LDAP users
s.disableMissingLdapUsers(ldapUsers) s.disableMissingLdapUsers(ldapUsers)
logrus.Trace("synchronized ldap users to database")
// Select blocks until one of the cases happens // Select blocks until one of the cases happens
select { select {
case <-time.After(1 * time.Minute): case <-time.After(1 * time.Minute):
@ -47,35 +53,62 @@ func (s *Server) SyncLdapWithUserDatabase() {
logrus.Info("ldap user synchronization stopped") 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 { if s.config.LDAP.EveryoneAdmin {
return true return true
} }
if s.config.LDAP.AdminLdapGroup_ == nil { if s.config.LDAP.AdminLdapGroup_ == nil {
return false return false
} }
//fmt.Printf("%+v\n", ldapData.Attributes)
var prefix string for _, userGroup := range userData.RawAttributes[s.config.LDAP.GroupMemberAttribute] {
for i := 0; i < layer; i++ { var userGroupDn, _ = gldap.ParseDN(string(userGroup))
prefix += "+" if s.dnIsAdminGroup(userGroupDn, groupTreeData) {
return true
} }
logrus.Tracef("%s Group layer: %d\n", prefix, layer) }
for _, group := range ldapData.RawAttributes[s.config.LDAP.GroupMemberAttribute] { return false
logrus.Tracef("%s%s\n", prefix, string(group)) }
var dn, _ = gldap.ParseDN(string(group))
// 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) { if s.config.LDAP.AdminLdapGroup_.Equal(dn) {
logrus.Tracef("%sFOUND: %s\n", prefix, string(group))
return true return true
} }
for _, group2 := range ldapGroupData {
if group2.DN == string(group) { // Recursively check the whole group tree
logrus.Tracef("%sChecking nested: %s\n", prefix, group2.DN) for _, group := range groupTreeData {
isAdmin := s.userIsInAdminGroup(&group2, ldapGroupData, layer+1) var groupDn, _ = gldap.ParseDN(group.DN)
if isAdmin { 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 return true
} }
} }
}
break
} }
return false return false
} }
@ -101,7 +134,7 @@ func (s Server) userChangedInLdap(user *users.User, ldapData *ldap.RawLdapData,
return true return true
} }
if user.IsAdmin != s.userIsInAdminGroup(ldapData, ldapGroupData, 0) { if user.IsAdmin != s.userIsInAdminGroup(ldapData, ldapGroupData) {
return true 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.Lastname = ldapUsers[i].Attributes[s.config.LDAP.LastNameAttribute]
user.Email = ldapUsers[i].Attributes[s.config.LDAP.EmailAttribute] user.Email = ldapUsers[i].Attributes[s.config.LDAP.EmailAttribute]
user.Phone = ldapUsers[i].Attributes[s.config.LDAP.PhoneAttribute] 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.Source = users.UserSourceLdap
user.DeletedAt = gorm.DeletedAt{} // Not deleted user.DeletedAt = gorm.DeletedAt{} // Not deleted