stuve-it-backend/ldapSync/ldapSync.go

275 lines
7.8 KiB
Go

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, 0, 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, 0, 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
}