278 lines
7.9 KiB
Go
278 lines
7.9 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
|
|
}
|
|
|
|
// only use the first 15 characters of the uuid and remove all slashes
|
|
return u.String(), nil
|
|
}
|
|
|
|
// 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 objectGUID, 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{
|
|
objectGUID: objectGUID,
|
|
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 objectGUID, 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"),
|
|
objectGUID: objectGUID,
|
|
mail: entry.GetAttributeValue("mail"),
|
|
dn: entry.DN,
|
|
cn: entry.GetAttributeValue("cn"),
|
|
msSFU30NisDomain: entry.GetAttributeValue("msSFU30NisDomain"),
|
|
memberOf: entry.GetAttributeValues("memberOf"),
|
|
REALM: "LDAP",
|
|
})
|
|
if err != nil {
|
|
errors = append(errors, err)
|
|
} else {
|
|
syncedCount++
|
|
}
|
|
}
|
|
|
|
var removedCount int
|
|
|
|
// remove old users
|
|
// step1: get a timeStamp ten minutes ago
|
|
timeStamp := time.Now().Add(time.Minute * -10)
|
|
|
|
// step2: get all users that have not been synced since that timeStamp
|
|
records, err := app.Dao().FindRecordsByFilter(
|
|
ldapUsersTableName,
|
|
"updated < {:timeStamp} && REALM = 'LDAP'", "", 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
|
|
}
|