package ldapSync import ( "encoding/json" "fmt" "git.stuve.uni-ulm.de/stuve-it/stuve-it-backend/logger" "github.com/go-ldap/ldap/v3" "github.com/google/uuid" "github.com/pocketbase/dbx" "github.com/pocketbase/pocketbase" "github.com/pocketbase/pocketbase/forms" "github.com/pocketbase/pocketbase/models" "os" "time" ) type SyncResult struct { Found int Synced int Removed int Errors []error } // getObjectGUID This function gets the objectGUID from an ldap entry // // since the objectGUID is a binary value, it is returned as a string (uuid) func getObjectGUID(entry *ldap.Entry) (string, error) { // get the objectGUID var bytes = entry.GetRawAttributeValue("objectGUID") // convert to uuid u, err := uuid.FromBytes(bytes) if err != nil { return "", err } return u.String()[:15], nil // only use the first 15 characters of the uuid } // upsertLDAPGroup This function creates / updates a record in the ldap groups table // // this function expects that the table ldapGroups already exists // // old groups are removed from the database. a group is considered old if it has not been synced for 1 day func syncLdapGroups(app *pocketbase.PocketBase, ldapClient *ldap.Conn) SyncResult { var syncedCount int var errors []error // Create a search request for groups groupsSearchRequest := ldap.NewSearchRequest( os.Getenv("LDAP_GROUP_BASE_DN"), // The base dn to search ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 30, false, os.Getenv("LDAP_GROUP_FILTER"), // The filter to apply []string{"description", "dn", "cn", "msSFU30NisDomain", "memberOf", "objectGUID"}, nil, ) groupsFoundInLdap, err := ldapClient.Search(groupsSearchRequest) if err != nil { return SyncResult{ Errors: []error{fmt.Errorf("unable to search in ldap for groups - %s", err)}, } } for _, entry := range groupsFoundInLdap.Entries { var id, e = getObjectGUID(entry) if e != nil { errors = append(errors, fmt.Errorf("unable to get objectGUID for group with dn: %s - %s", entry.DN, e)) } err := upsertLDAPGroup(app, &LDAPGroup{ id: id, description: entry.GetAttributeValue("description"), dn: entry.DN, cn: entry.GetAttributeValue("cn"), msSFU30NisDomain: entry.GetAttributeValue("msSFU30NisDomain"), memberOf: entry.GetAttributeValues("memberOf"), }) if err != nil { errors = append(errors, err) } else { syncedCount++ } } var removedCount int // remove old groups // step1: get a timeStamp one day ago timeStamp := time.Now().AddDate(0, 0, -1) // step2: get all groups that have not been synced since that timeStamp records, err := app.Dao().FindRecordsByFilter( ldapGroupsTableName, "updated < {:timeStamp}", "", 0, 0, dbx.Params{"timeStamp": timeStamp}, ) if err != nil { errors = append(errors, fmt.Errorf("unable to get old ldap groups from db - %s", err)) return SyncResult{ Found: len(groupsFoundInLdap.Entries), Synced: syncedCount, Errors: errors, } } // step3: delete all groups that have not been synced since that timeStamp for _, record := range records { err := app.Dao().DeleteRecord(record) if err != nil { errors = append(errors, fmt.Errorf("unable to remove old group with dn: %s - %s", record.Get("dn"), err)) } else { removedCount++ } } return SyncResult{ Found: len(groupsFoundInLdap.Entries), Synced: syncedCount, Removed: removedCount, Errors: errors, } } // syncLdapUsers This function syncs the ldap users with the database // // this function expects that the tables ldapUsers and ldapGroups already exist // // old users are removed from the database. a user is considered old if it has not been synced for 1 day func syncLdapUsers(app *pocketbase.PocketBase, ldapClient *ldap.Conn) SyncResult { var syncedCount int var errors []error // Create a search request for users userSearchRequest := ldap.NewSearchRequest( os.Getenv("LDAP_BASE_DN"), // The base dn to search ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 30, false, os.Getenv("LDAP_USER_FILTER"), // The filter to apply []string{"givenName", "sn", "accountExpires", "mail", "dn", "cn", "msSFU30NisDomain", "memberOf", "objectGUID"}, nil, ) usersFoundInLdap, err := ldapClient.Search(userSearchRequest) if err != nil { return SyncResult{ Errors: []error{fmt.Errorf("unable to search in ldap for users - %s", err)}, } } for _, entry := range usersFoundInLdap.Entries { var id, e = getObjectGUID(entry) if e != nil { errors = append(errors, fmt.Errorf("unable to get objectGUID for user with dn: %s - %s", entry.DN, e)) } err := upsertLDAPUser(app, &LDAPUser{ givenName: entry.GetAttributeValue("givenName"), sn: entry.GetAttributeValue("sn"), accountExpires: entry.GetAttributeValue("accountExpires"), id: id, mail: entry.GetAttributeValue("mail"), dn: entry.DN, cn: entry.GetAttributeValue("cn"), msSFU30NisDomain: entry.GetAttributeValue("msSFU30NisDomain"), memberOf: entry.GetAttributeValues("memberOf"), }) if err != nil { errors = append(errors, err) } else { syncedCount++ } } var removedCount int // remove old users // step1: get a timeStamp one day ago timeStamp := time.Now().AddDate(0, 0, -1) // step2: get all users that have not been synced since that timeStamp records, err := app.Dao().FindRecordsByFilter( ldapUsersTableName, "updated < {:timeStamp}", "", 0, 0, dbx.Params{"timeStamp": timeStamp}, ) if err != nil { return SyncResult{ Found: len(usersFoundInLdap.Entries), Synced: syncedCount, Errors: []error{fmt.Errorf("unable to get old ldap users from db - %s", err)}, } } // step3: delete all users that have not been synced since that timeStamp for _, record := range records { err := app.Dao().DeleteRecord(record) if err != nil { errors = append(errors, fmt.Errorf("unable to remove old user with dn: %s - %s", record.Get("dn"), err)) } else { removedCount++ } } return SyncResult{ Found: len(usersFoundInLdap.Entries), Synced: syncedCount, Removed: removedCount, Errors: errors, } } // syncLdap This function syncs the ldap groups and users with the database func syncLdap(app *pocketbase.PocketBase) { // Connect to the LDAP server conn, err := ldap.DialURL(os.Getenv("LDAP_URL")) if err != nil { logger.LogErrorF("unable to connect to ldap server - %s", err) return } defer conn.Close() // Bind to the server err = conn.Bind(os.Getenv("LDAP_BIND_DN"), os.Getenv("LDAP_BIND_PASSWORD")) if err != nil { logger.LogErrorF("unable to bind to ldap server - %s", err) return } // sync groups groupSyncResult := syncLdapGroups(app, conn) // sync users userSyncResult := syncLdapUsers(app, conn) // store sync log in database collection, err := app.Dao().FindCollectionByNameOrId(ldapSyncLogsTableName) if err != nil { logger.LogErrorF("unable to find % table: %s", ldapSyncLogsTableName, err) return } record := models.NewRecord(collection) form := forms.NewRecordUpsert(app, record) form.LoadData(map[string]any{ "usersFound": userSyncResult.Found, "usersSynced": userSyncResult.Synced, "usersRemoved": userSyncResult.Removed, "userSyncErrors": errorsToJSON(userSyncResult.Errors), "groupsFound": groupSyncResult.Found, "groupsSynced": groupSyncResult.Synced, "groupsRemoved": groupSyncResult.Removed, "groupSyncErrors": errorsToJSON(groupSyncResult.Errors), }) if err := form.Submit(); err != nil { logger.LogErrorF("unable to store ldap sync log: %s", err) return } } // errorsToJSON converts an array of errors to a json string func errorsToJSON(errors []error) string { var jsonErrors []string for _, err := range errors { jsonErrors = append(jsonErrors, err.Error()) } jsonString, _ := json.Marshal(jsonErrors) s := string(jsonString) return s }