Initial QSfera import
This commit is contained in:
@@ -0,0 +1,812 @@
|
||||
package identity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
"github.com/google/uuid"
|
||||
libregraph "github.com/opencloud-eu/libre-graph-api-go"
|
||||
"github.com/qsfera/server/pkg/log"
|
||||
"github.com/qsfera/server/services/graph/pkg/config"
|
||||
"github.com/qsfera/server/services/graph/pkg/errorcode"
|
||||
)
|
||||
|
||||
type educationConfig struct {
|
||||
schoolBaseDN string
|
||||
schoolFilter string
|
||||
schoolObjectClass string
|
||||
schoolScope int
|
||||
// memberOfSchoolAttribute defines the AttributeType on the user/group objects
|
||||
// which contains the school Id to which the user/group is assigned
|
||||
memberOfSchoolAttribute string
|
||||
schoolAttributeMap schoolAttributeMap
|
||||
|
||||
userObjectClass string
|
||||
userAttributeMap educationUserAttributeMap
|
||||
|
||||
classObjectClass string
|
||||
classAttributeMap educationClassAttributeMap
|
||||
}
|
||||
|
||||
type schoolAttributeMap struct {
|
||||
displayName string
|
||||
schoolNumber string
|
||||
externalId string
|
||||
id string
|
||||
terminationDate string
|
||||
}
|
||||
|
||||
type schoolUpdateOperation uint8
|
||||
|
||||
const (
|
||||
tooManyValues schoolUpdateOperation = iota
|
||||
schoolUnchanged
|
||||
schoolRenamed
|
||||
schoolPropertiesUpdated
|
||||
)
|
||||
|
||||
var (
|
||||
errNotSet = errors.New("attribute not set")
|
||||
errSchoolNameExists = errorcode.New(errorcode.NameAlreadyExists, "A school with that name is already present")
|
||||
errSchoolNumberExists = errorcode.New(errorcode.NameAlreadyExists, "A school with that number is already present")
|
||||
)
|
||||
|
||||
func defaultEducationConfig() educationConfig {
|
||||
return educationConfig{
|
||||
schoolObjectClass: "qsferaEducationSchool",
|
||||
schoolScope: ldap.ScopeWholeSubtree,
|
||||
memberOfSchoolAttribute: "qsferaMemberOfSchool",
|
||||
schoolAttributeMap: newSchoolAttributeMap(),
|
||||
|
||||
userObjectClass: "qsferaEducationUser",
|
||||
userAttributeMap: newEducationUserAttributeMap(),
|
||||
|
||||
classObjectClass: "qsferaEducationClass",
|
||||
classAttributeMap: newEducationClassAttributeMap(),
|
||||
}
|
||||
}
|
||||
|
||||
func newEducationConfig(config config.LDAP) (educationConfig, error) {
|
||||
if config.EducationResourcesEnabled {
|
||||
var err error
|
||||
eduCfg := defaultEducationConfig()
|
||||
eduCfg.schoolBaseDN = config.EducationConfig.SchoolBaseDN
|
||||
if config.EducationConfig.SchoolSearchScope != "" {
|
||||
if eduCfg.schoolScope, err = stringToScope(config.EducationConfig.SchoolSearchScope); err != nil {
|
||||
return educationConfig{}, fmt.Errorf("error configuring school search scope: %w", err)
|
||||
}
|
||||
}
|
||||
if config.EducationConfig.SchoolFilter != "" {
|
||||
eduCfg.schoolFilter = config.EducationConfig.SchoolFilter
|
||||
}
|
||||
if config.EducationConfig.SchoolObjectClass != "" {
|
||||
eduCfg.schoolObjectClass = config.EducationConfig.SchoolObjectClass
|
||||
}
|
||||
|
||||
// Attribute mapping config
|
||||
if config.EducationConfig.SchoolNameAttribute != "" {
|
||||
eduCfg.schoolAttributeMap.displayName = config.EducationConfig.SchoolNameAttribute
|
||||
}
|
||||
if config.EducationConfig.SchoolNumberAttribute != "" {
|
||||
eduCfg.schoolAttributeMap.schoolNumber = config.EducationConfig.SchoolNumberAttribute
|
||||
}
|
||||
if config.EducationConfig.SchoolIDAttribute != "" {
|
||||
eduCfg.schoolAttributeMap.id = config.EducationConfig.SchoolIDAttribute
|
||||
}
|
||||
|
||||
return eduCfg, nil
|
||||
}
|
||||
return educationConfig{}, nil
|
||||
}
|
||||
|
||||
func newSchoolAttributeMap() schoolAttributeMap {
|
||||
return schoolAttributeMap{
|
||||
displayName: "ou",
|
||||
schoolNumber: "qsferaEducationSchoolNumber",
|
||||
externalId: "qsferaEducationExternalId",
|
||||
id: "qsferaUUID",
|
||||
terminationDate: "qsferaEducationSchoolTerminationTimestamp",
|
||||
}
|
||||
}
|
||||
|
||||
// CreateEducationSchool creates the supplied school in the identity backend.
|
||||
func (i *LDAP) CreateEducationSchool(ctx context.Context, school libregraph.EducationSchool) (*libregraph.EducationSchool, error) {
|
||||
logger := i.logger.SubloggerWithRequestID(ctx)
|
||||
logger.Debug().Str("backend", "ldap").Msg("CreateEducationSchool")
|
||||
if !i.writeEnabled {
|
||||
return nil, ErrReadOnly
|
||||
}
|
||||
|
||||
// Check that the school number is not already used
|
||||
if school.HasSchoolNumber() {
|
||||
_, err := i.getSchoolByNumber(school.GetSchoolNumber())
|
||||
switch {
|
||||
case err == nil:
|
||||
logger.Debug().Err(errSchoolNumberExists).Str("schoolNumber", school.GetSchoolNumber()).Msg("duplicate school number")
|
||||
return nil, errSchoolNumberExists
|
||||
case errors.Is(err, ErrNotFound):
|
||||
break
|
||||
default:
|
||||
logger.Error().Err(err).Str("schoolNumber", school.GetSchoolNumber()).Msg("error looking up school by number")
|
||||
return nil, errorcode.New(errorcode.GeneralException, "error looking up school by number")
|
||||
}
|
||||
}
|
||||
|
||||
attributeTypeAndValue := ldap.AttributeTypeAndValue{
|
||||
Type: i.educationConfig.schoolAttributeMap.displayName,
|
||||
Value: school.GetDisplayName(),
|
||||
}
|
||||
|
||||
dn := fmt.Sprintf("%s,%s",
|
||||
attributeTypeAndValue.String(),
|
||||
i.educationConfig.schoolBaseDN,
|
||||
)
|
||||
ar := ldap.NewAddRequest(dn, nil)
|
||||
ar.Attribute(i.educationConfig.schoolAttributeMap.displayName, []string{school.GetDisplayName()})
|
||||
if school.HasSchoolNumber() {
|
||||
ar.Attribute(i.educationConfig.schoolAttributeMap.schoolNumber, []string{school.GetSchoolNumber()})
|
||||
}
|
||||
if school.HasExternalId() {
|
||||
ar.Attribute(i.educationConfig.schoolAttributeMap.externalId, []string{school.GetExternalId()})
|
||||
}
|
||||
if !i.useServerUUID {
|
||||
ar.Attribute(i.educationConfig.schoolAttributeMap.id, []string{uuid.NewString()})
|
||||
}
|
||||
objectClasses := []string{"organizationalUnit", i.educationConfig.schoolObjectClass, "top"}
|
||||
ar.Attribute("objectClass", objectClasses)
|
||||
|
||||
if err := i.conn.Add(ar); err != nil {
|
||||
var lerr *ldap.Error
|
||||
logger.Debug().Err(err).Msg("error adding school")
|
||||
if errors.As(err, &lerr) {
|
||||
if lerr.ResultCode == ldap.LDAPResultEntryAlreadyExists {
|
||||
err = errSchoolNameExists
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read back school from LDAP to get the generated UUID
|
||||
e, err := i.getSchoolByDN(ar.DN)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return i.createSchoolModelFromLDAP(e), nil
|
||||
}
|
||||
|
||||
// UpdateEducationSchoolOperation contains the logic for which update operation to apply to a school
|
||||
func (i *LDAP) updateEducationSchoolOperation(
|
||||
schoolUpdate libregraph.EducationSchool,
|
||||
currentSchool libregraph.EducationSchool,
|
||||
) schoolUpdateOperation {
|
||||
|
||||
providedDisplayName, displayNameIsSet := schoolUpdate.GetDisplayNameOk()
|
||||
if displayNameIsSet {
|
||||
if *providedDisplayName == "" || *providedDisplayName == currentSchool.GetDisplayName() {
|
||||
// The school name hasn't changed
|
||||
displayNameIsSet = false
|
||||
}
|
||||
}
|
||||
|
||||
var propertiesUpdated bool
|
||||
|
||||
switch {
|
||||
case schoolUpdate.HasSchoolNumber():
|
||||
if schoolUpdate.GetSchoolNumber() != "" && schoolUpdate.GetSchoolNumber() != currentSchool.GetSchoolNumber() {
|
||||
propertiesUpdated = true
|
||||
}
|
||||
case schoolUpdate.HasTerminationDate():
|
||||
if schoolUpdate.GetTerminationDate() != currentSchool.GetTerminationDate() {
|
||||
propertiesUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
if propertiesUpdated && displayNameIsSet {
|
||||
return tooManyValues
|
||||
}
|
||||
|
||||
if displayNameIsSet {
|
||||
return schoolRenamed
|
||||
}
|
||||
|
||||
if propertiesUpdated {
|
||||
return schoolPropertiesUpdated
|
||||
}
|
||||
|
||||
return schoolUnchanged
|
||||
}
|
||||
|
||||
// updateDisplayName updates the school OU in the identity backend
|
||||
func (i *LDAP) updateDisplayName(ctx context.Context, dn string, providedDisplayName string) error {
|
||||
logger := i.logger.SubloggerWithRequestID(ctx)
|
||||
attributeTypeAndValue := ldap.AttributeTypeAndValue{
|
||||
Type: i.educationConfig.schoolAttributeMap.displayName,
|
||||
Value: providedDisplayName,
|
||||
}
|
||||
|
||||
mrdn := ldap.NewModifyDNRequest(dn, attributeTypeAndValue.String(), true, "")
|
||||
i.logger.Debug().Str("backend", "ldap").
|
||||
Str("dn", mrdn.DN).
|
||||
Str("newrdn", mrdn.NewRDN).
|
||||
Msg("updateDisplayName")
|
||||
|
||||
if err := i.conn.ModifyDN(mrdn); err != nil {
|
||||
var lerr *ldap.Error
|
||||
logger.Debug().Err(err).Msg("error updating school name")
|
||||
if errors.As(err, &lerr) {
|
||||
if lerr.ResultCode == ldap.LDAPResultEntryAlreadyExists {
|
||||
err = errSchoolNameExists
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateSchoolProperties updates the properties (other that displayName) of a school.
|
||||
// It checks if a school number is already taken, before updating the school number
|
||||
func (i *LDAP) updateSchoolProperties(ctx context.Context, dn string, currentSchool, updatedSchool libregraph.EducationSchool) error {
|
||||
logger := i.logger.SubloggerWithRequestID(ctx)
|
||||
|
||||
mr := ldap.NewModifyRequest(dn, nil)
|
||||
if updatedSchoolNumber, ok := updatedSchool.GetSchoolNumberOk(); ok {
|
||||
if *updatedSchoolNumber != "" && currentSchool.GetSchoolNumber() != *updatedSchoolNumber {
|
||||
_, err := i.getSchoolByNumber(*updatedSchoolNumber)
|
||||
if err == nil {
|
||||
return errSchoolNumberExists
|
||||
}
|
||||
mr.Replace(i.educationConfig.schoolAttributeMap.schoolNumber, []string{*updatedSchoolNumber})
|
||||
}
|
||||
}
|
||||
|
||||
if updatedTerminationDate, ok := updatedSchool.GetTerminationDateOk(); ok {
|
||||
if updatedTerminationDate == nil && currentSchool.HasTerminationDate() {
|
||||
// Delete the termination date
|
||||
mr.Delete(i.educationConfig.schoolAttributeMap.terminationDate, []string{})
|
||||
}
|
||||
if updatedTerminationDate != nil && *updatedTerminationDate != currentSchool.GetTerminationDate() {
|
||||
ldapDateTime := updatedTerminationDate.UTC().Format(ldapDateFormat)
|
||||
mr.Replace(i.educationConfig.schoolAttributeMap.terminationDate, []string{ldapDateTime})
|
||||
}
|
||||
}
|
||||
|
||||
if err := i.conn.Modify(mr); err != nil {
|
||||
logger.Debug().Err(err).Msg("error updating school number")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateEducationSchool updates the supplied school in the identity backend
|
||||
func (i *LDAP) UpdateEducationSchool(ctx context.Context, numberOrID string, school libregraph.EducationSchool) (*libregraph.EducationSchool, error) {
|
||||
logger := i.logger.SubloggerWithRequestID(ctx)
|
||||
logger.Debug().Str("backend", "ldap").Msg("UpdateEducationSchool")
|
||||
if !i.writeEnabled {
|
||||
return nil, ErrReadOnly
|
||||
}
|
||||
|
||||
e, err := i.getSchoolByNumberOrID(numberOrID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
currentSchool := i.createSchoolModelFromLDAP(e)
|
||||
switch i.updateEducationSchoolOperation(school, *currentSchool) {
|
||||
case tooManyValues:
|
||||
return nil, fmt.Errorf("school name and school number cannot be updated in the same request")
|
||||
case schoolUnchanged:
|
||||
logger.Debug().Str("backend", "ldap").Msg("UpdateEducationSchool: Nothing changed")
|
||||
return currentSchool, nil
|
||||
case schoolRenamed:
|
||||
if err := i.updateDisplayName(ctx, e.DN, school.GetDisplayName()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case schoolPropertiesUpdated:
|
||||
if err := i.updateSchoolProperties(ctx, e.DN, *currentSchool, school); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Read back school from LDAP
|
||||
e, err = i.getSchoolByNumberOrID(i.getID(e))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return i.createSchoolModelFromLDAP(e), nil
|
||||
}
|
||||
|
||||
// DeleteEducationSchool deletes a given school, identified by id
|
||||
func (i *LDAP) DeleteEducationSchool(ctx context.Context, id string) error {
|
||||
logger := i.logger.SubloggerWithRequestID(ctx)
|
||||
logger.Debug().Str("backend", "ldap").Msg("DeleteEducationSchool")
|
||||
if !i.writeEnabled {
|
||||
return ErrReadOnly
|
||||
}
|
||||
e, err := i.getSchoolByNumberOrID(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dr := ldap.DelRequest{DN: e.DN}
|
||||
if err = i.conn.Del(&dr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO update any users that are member of this school
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetEducationSchool implements the EducationBackend interface for the LDAP backend.
|
||||
func (i *LDAP) GetEducationSchool(ctx context.Context, numberOrID string) (*libregraph.EducationSchool, error) {
|
||||
logger := i.logger.SubloggerWithRequestID(ctx)
|
||||
logger.Debug().Str("backend", "ldap").Msg("GetEducationSchool")
|
||||
e, err := i.getSchoolByNumberOrID(numberOrID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return i.createSchoolModelFromLDAP(e), nil
|
||||
}
|
||||
|
||||
// GetEducationSchools implements the EducationBackend interface for the LDAP backend.
|
||||
func (i *LDAP) GetEducationSchools(ctx context.Context) ([]*libregraph.EducationSchool, error) {
|
||||
filter := fmt.Sprintf("(objectClass=%s)", i.educationConfig.schoolObjectClass)
|
||||
if i.educationConfig.schoolFilter != "" {
|
||||
filter = fmt.Sprintf("(&%s%s)", i.educationConfig.schoolFilter, filter)
|
||||
}
|
||||
return i.searchEducationSchools(ctx, filter)
|
||||
}
|
||||
|
||||
// FilterEducationSchoolsByAttribute implements the EducationBackend interface for the LDAP backend.
|
||||
func (i *LDAP) FilterEducationSchoolsByAttribute(ctx context.Context, attr, value string) ([]*libregraph.EducationSchool, error) {
|
||||
logger := i.logger.SubloggerWithRequestID(ctx).With().Str("func", "FilterEducationSchoolsByAttribute").Logger()
|
||||
logger.Debug().Str("backend", "ldap").Str("attribute", attr).Str("value", value).Msg("")
|
||||
|
||||
var ldapAttr string
|
||||
switch attr {
|
||||
case "externalId":
|
||||
ldapAttr = i.educationConfig.schoolAttributeMap.externalId
|
||||
default:
|
||||
return nil, errorcode.New(errorcode.InvalidRequest, fmt.Sprintf("filtering by attribute '%s' is not supported", attr))
|
||||
}
|
||||
filter := fmt.Sprintf("(&%s(objectClass=%s)(%s=%s))",
|
||||
i.educationConfig.schoolFilter,
|
||||
i.educationConfig.schoolObjectClass,
|
||||
ldap.EscapeFilter(ldapAttr),
|
||||
ldap.EscapeFilter(value),
|
||||
)
|
||||
return i.searchEducationSchools(ctx, filter)
|
||||
}
|
||||
|
||||
// searchEducationSchools builds and executes an LDAP search for education schools and converts the results to EducationSchool models.
|
||||
func (i *LDAP) searchEducationSchools(ctx context.Context, filter string) ([]*libregraph.EducationSchool, error) {
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
i.educationConfig.schoolBaseDN,
|
||||
i.educationConfig.schoolScope,
|
||||
ldap.NeverDerefAliases, 0, 0, false,
|
||||
filter,
|
||||
i.getEducationSchoolAttrTypes(),
|
||||
nil,
|
||||
)
|
||||
logger := i.logger.SubloggerWithRequestID(ctx)
|
||||
logger.Debug().Str("backend", "ldap").
|
||||
Str("base", searchRequest.BaseDN).
|
||||
Str("filter", searchRequest.Filter).
|
||||
Int("scope", searchRequest.Scope).
|
||||
Int("sizelimit", searchRequest.SizeLimit).
|
||||
Interface("attributes", searchRequest.Attributes).
|
||||
Msg("searchEducationSchools")
|
||||
|
||||
res, err := i.conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
return nil, errorcode.New(errorcode.ItemNotFound, err.Error())
|
||||
}
|
||||
|
||||
schools := make([]*libregraph.EducationSchool, 0, len(res.Entries))
|
||||
for _, e := range res.Entries {
|
||||
school := i.createSchoolModelFromLDAP(e)
|
||||
// Skip invalid LDAP entries
|
||||
if school == nil {
|
||||
continue
|
||||
}
|
||||
schools = append(schools, school)
|
||||
}
|
||||
return schools, nil
|
||||
}
|
||||
|
||||
// GetEducationSchoolUsers implements the EducationBackend interface for the LDAP backend.
|
||||
func (i *LDAP) GetEducationSchoolUsers(ctx context.Context, schoolNumberOrID string) ([]*libregraph.EducationUser, error) {
|
||||
logger := i.logger.SubloggerWithRequestID(ctx)
|
||||
logger.Debug().Str("backend", "ldap").Msg("GetEducationSchoolUsers")
|
||||
|
||||
entries, err := i.getEducationSchoolEntries(
|
||||
schoolNumberOrID, i.userFilter, i.educationConfig.userObjectClass, i.userBaseDN, i.userScope, i.getEducationUserAttrTypes(), logger,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
users := make([]*libregraph.EducationUser, 0, len(entries))
|
||||
|
||||
for _, e := range entries {
|
||||
u := i.createEducationUserModelFromLDAP(e)
|
||||
// Skip invalid LDAP users
|
||||
if u == nil {
|
||||
continue
|
||||
}
|
||||
users = append(users, u)
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// AddUsersToEducationSchool adds new members (reference by a slice of IDs) to supplied school in the identity backend.
|
||||
func (i *LDAP) AddUsersToEducationSchool(ctx context.Context, schoolNumberOrID string, memberIDs []string) error {
|
||||
logger := i.logger.SubloggerWithRequestID(ctx)
|
||||
logger.Debug().Str("backend", "ldap").Msg("AddUsersToEducationSchool")
|
||||
|
||||
schoolEntry, err := i.getSchoolByNumberOrID(schoolNumberOrID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if schoolEntry == nil {
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
schoolID := schoolEntry.GetEqualFoldAttributeValue(i.educationConfig.schoolAttributeMap.id)
|
||||
|
||||
userEntries := make([]*ldap.Entry, 0, len(memberIDs))
|
||||
for _, memberID := range memberIDs {
|
||||
user, err := i.getEducationUserByNameOrID(memberID)
|
||||
if err != nil {
|
||||
i.logger.Warn().Str("userid", memberID).Msg("User does not exist")
|
||||
return errorcode.New(errorcode.ItemNotFound, fmt.Sprintf("user '%s' not found", memberID))
|
||||
}
|
||||
userEntries = append(userEntries, user)
|
||||
}
|
||||
|
||||
for _, userEntry := range userEntries {
|
||||
if err := i.addEntryToSchool(userEntry, schoolID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addEntryToSchool adds the schoolID to the entry's memberOfSchool attribute if not already present.
|
||||
func (i *LDAP) addEntryToSchool(entry *ldap.Entry, schoolID string) error {
|
||||
currentSchools := entry.GetEqualFoldAttributeValues(i.educationConfig.memberOfSchoolAttribute)
|
||||
if slices.Contains(currentSchools, schoolID) {
|
||||
return nil
|
||||
}
|
||||
mr := ldap.ModifyRequest{DN: entry.DN}
|
||||
mr.Add(i.educationConfig.memberOfSchoolAttribute, []string{schoolID})
|
||||
return i.conn.Modify(&mr)
|
||||
}
|
||||
|
||||
// RemoveUserFromEducationSchool removes a single member (by ID) from a school
|
||||
func (i *LDAP) RemoveUserFromEducationSchool(ctx context.Context, schoolNumberOrID string, memberID string) error {
|
||||
logger := i.logger.SubloggerWithRequestID(ctx)
|
||||
logger.Debug().Str("backend", "ldap").Msg("RemoveUserFromEducationSchool")
|
||||
|
||||
schoolEntry, err := i.getSchoolByNumberOrID(schoolNumberOrID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if schoolEntry == nil {
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
schoolID := schoolEntry.GetEqualFoldAttributeValue(i.educationConfig.schoolAttributeMap.id)
|
||||
user, err := i.getEducationUserByNameOrID(memberID)
|
||||
if err != nil {
|
||||
i.logger.Warn().Str("userid", memberID).Msg("User does not exist")
|
||||
return err
|
||||
}
|
||||
currentSchools := user.GetEqualFoldAttributeValues(i.educationConfig.memberOfSchoolAttribute)
|
||||
for _, currentSchool := range currentSchools {
|
||||
if currentSchool == schoolID {
|
||||
mr := ldap.ModifyRequest{DN: user.DN}
|
||||
mr.Delete(i.educationConfig.memberOfSchoolAttribute, []string{schoolID})
|
||||
if err := i.conn.Modify(&mr); err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetEducationSchoolClasses implements the EducationBackend interface for the LDAP backend.
|
||||
func (i *LDAP) GetEducationSchoolClasses(ctx context.Context, schoolNumberOrID string) ([]*libregraph.EducationClass, error) {
|
||||
logger := i.logger.SubloggerWithRequestID(ctx)
|
||||
logger.Debug().Str("backend", "ldap").Msg("GetEducationSchoolClasses")
|
||||
|
||||
entries, err := i.getEducationSchoolEntries(
|
||||
schoolNumberOrID, i.groupFilter, i.educationConfig.classObjectClass, i.groupBaseDN, i.groupScope, i.getEducationClassAttrTypes(false), logger,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
classes := make([]*libregraph.EducationClass, 0, len(entries))
|
||||
|
||||
for _, e := range entries {
|
||||
class := i.createEducationClassModelFromLDAP(e)
|
||||
// Skip invalid LDAP classes
|
||||
if class == nil {
|
||||
continue
|
||||
}
|
||||
classes = append(classes, class)
|
||||
}
|
||||
return classes, nil
|
||||
}
|
||||
|
||||
func (i *LDAP) getEducationSchoolEntries(
|
||||
schoolNumberOrID, filter, objectClass, baseDN string,
|
||||
scope int,
|
||||
attributes []string,
|
||||
logger log.Logger,
|
||||
) ([]*ldap.Entry, error) {
|
||||
schoolEntry, err := i.getSchoolByNumberOrID(schoolNumberOrID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if schoolEntry == nil {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
schoolID := schoolEntry.GetEqualFoldAttributeValue(i.educationConfig.schoolAttributeMap.id)
|
||||
schoolID = ldap.EscapeFilter(schoolID)
|
||||
idFilter := fmt.Sprintf("(%s=%s)", i.educationConfig.memberOfSchoolAttribute, schoolID)
|
||||
searchFilter := fmt.Sprintf("(&%s(objectClass=%s)%s)", filter, objectClass, idFilter)
|
||||
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
baseDN,
|
||||
scope,
|
||||
ldap.NeverDerefAliases, 0, 0, false,
|
||||
searchFilter,
|
||||
attributes,
|
||||
nil,
|
||||
)
|
||||
logger.Debug().Str("backend", "ldap").
|
||||
Str("base", searchRequest.BaseDN).
|
||||
Str("filter", searchRequest.Filter).
|
||||
Int("scope", searchRequest.Scope).
|
||||
Int("sizelimit", searchRequest.SizeLimit).
|
||||
Interface("attributes", searchRequest.Attributes).
|
||||
Msg("GetEducationClasses")
|
||||
res, err := i.conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
return nil, errorcode.New(errorcode.ItemNotFound, err.Error())
|
||||
}
|
||||
return res.Entries, nil
|
||||
}
|
||||
|
||||
// AddClassesToEducationSchool adds new members (reference by a slice of IDs) to supplied school in the identity backend.
|
||||
func (i *LDAP) AddClassesToEducationSchool(ctx context.Context, schoolNumberOrID string, memberIDs []string) error {
|
||||
logger := i.logger.SubloggerWithRequestID(ctx)
|
||||
logger.Debug().Str("backend", "ldap").Msg("AddClassesToEducationSchool")
|
||||
|
||||
schoolEntry, err := i.getSchoolByNumberOrID(schoolNumberOrID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if schoolEntry == nil {
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
schoolID := schoolEntry.GetEqualFoldAttributeValue(i.educationConfig.schoolAttributeMap.id)
|
||||
|
||||
classEntries := make([]*ldap.Entry, 0, len(memberIDs))
|
||||
for _, memberID := range memberIDs {
|
||||
class, err := i.getEducationClassByID(memberID, false)
|
||||
if err != nil {
|
||||
i.logger.Warn().Str("userid", memberID).Msg("Class does not exist")
|
||||
return err
|
||||
}
|
||||
classEntries = append(classEntries, class)
|
||||
}
|
||||
|
||||
for _, classEntry := range classEntries {
|
||||
if err := i.addEntryToSchool(classEntry, schoolID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveClassFromEducationSchool removes a single member (by ID) from a school
|
||||
func (i *LDAP) RemoveClassFromEducationSchool(ctx context.Context, schoolNumberOrID string, memberID string) error {
|
||||
logger := i.logger.SubloggerWithRequestID(ctx)
|
||||
logger.Debug().Str("backend", "ldap").Msg("RemoveClassFromEducationSchool")
|
||||
|
||||
schoolEntry, err := i.getSchoolByNumberOrID(schoolNumberOrID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if schoolEntry == nil {
|
||||
return ErrNotFound
|
||||
}
|
||||
|
||||
schoolID := schoolEntry.GetEqualFoldAttributeValue(i.educationConfig.schoolAttributeMap.id)
|
||||
class, err := i.getEducationClassByID(memberID, false)
|
||||
if err != nil {
|
||||
i.logger.Warn().Str("userid", memberID).Msg("Class does not exist")
|
||||
return err
|
||||
}
|
||||
currentSchools := class.GetEqualFoldAttributeValues(i.educationConfig.memberOfSchoolAttribute)
|
||||
for _, currentSchool := range currentSchools {
|
||||
if currentSchool == schoolID {
|
||||
mr := ldap.ModifyRequest{DN: class.DN}
|
||||
mr.Delete(i.educationConfig.memberOfSchoolAttribute, []string{schoolID})
|
||||
if err := i.conn.Modify(&mr); err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *LDAP) getSchoolByDN(dn string) (*ldap.Entry, error) {
|
||||
filter := fmt.Sprintf("(objectClass=%s)", i.educationConfig.schoolObjectClass)
|
||||
|
||||
if i.educationConfig.schoolFilter != "" {
|
||||
filter = fmt.Sprintf("(&%s(%s))", filter, i.educationConfig.schoolFilter)
|
||||
}
|
||||
return i.getEntryByDN(dn, i.getEducationSchoolAttrTypes(), filter)
|
||||
}
|
||||
|
||||
func (i *LDAP) getSchoolByNumberOrID(numberOrID string) (*ldap.Entry, error) {
|
||||
numberOrID = ldap.EscapeFilter(numberOrID)
|
||||
filter := fmt.Sprintf(
|
||||
"(|(%s=%s)(%s=%s))",
|
||||
i.educationConfig.schoolAttributeMap.id,
|
||||
numberOrID,
|
||||
i.educationConfig.schoolAttributeMap.schoolNumber,
|
||||
numberOrID,
|
||||
)
|
||||
return i.getSchoolByFilter(filter)
|
||||
}
|
||||
|
||||
func (i *LDAP) getSchoolByNumber(schoolNumber string) (*ldap.Entry, error) {
|
||||
schoolNumber = ldap.EscapeFilter(schoolNumber)
|
||||
filter := fmt.Sprintf(
|
||||
"(%s=%s)",
|
||||
i.educationConfig.schoolAttributeMap.schoolNumber,
|
||||
schoolNumber,
|
||||
)
|
||||
return i.getSchoolByFilter(filter)
|
||||
}
|
||||
|
||||
func (i *LDAP) getSchoolByFilter(filter string) (*ldap.Entry, error) {
|
||||
filter = fmt.Sprintf("(&%s(objectClass=%s)%s)",
|
||||
i.educationConfig.schoolFilter,
|
||||
i.educationConfig.schoolObjectClass,
|
||||
filter,
|
||||
)
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
i.educationConfig.schoolBaseDN,
|
||||
i.educationConfig.schoolScope,
|
||||
ldap.NeverDerefAliases, 1, 0, false,
|
||||
filter,
|
||||
i.getEducationSchoolAttrTypes(),
|
||||
nil,
|
||||
)
|
||||
i.logger.Debug().Str("backend", "ldap").
|
||||
Str("base", searchRequest.BaseDN).
|
||||
Str("filter", searchRequest.Filter).
|
||||
Int("scope", searchRequest.Scope).
|
||||
Int("sizelimit", searchRequest.SizeLimit).
|
||||
Interface("attributes", searchRequest.Attributes).
|
||||
Msg("getSchoolByFilter")
|
||||
res, err := i.conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
var errmsg string
|
||||
if lerr, ok := err.(*ldap.Error); ok {
|
||||
if lerr.ResultCode == ldap.LDAPResultSizeLimitExceeded {
|
||||
errmsg = fmt.Sprintf("too many results searching for school '%s'", filter)
|
||||
i.logger.Debug().Str("backend", "ldap").Err(lerr).
|
||||
Str("schoolfilter", filter).Msg("too many results searching for school")
|
||||
}
|
||||
}
|
||||
return nil, errorcode.New(errorcode.ItemNotFound, errmsg)
|
||||
}
|
||||
if len(res.Entries) == 0 {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
|
||||
return res.Entries[0], nil
|
||||
}
|
||||
|
||||
func (i *LDAP) createSchoolModelFromLDAP(e *ldap.Entry) *libregraph.EducationSchool {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
displayName := i.getDisplayName(e)
|
||||
id := i.getID(e)
|
||||
schoolNumber := i.getSchoolNumber(e)
|
||||
|
||||
externalId := i.getExternalId(e)
|
||||
|
||||
t, err := i.getTerminationDate(e)
|
||||
if err != nil && !errors.Is(err, errNotSet) {
|
||||
i.logger.Error().Err(err).Str("dn", e.DN).Msg("Error reading termination date for LDAP entry")
|
||||
}
|
||||
|
||||
if id == "" || displayName == "" {
|
||||
i.logger.Warn().Str("dn", e.DN).Str("id", id).Str("displayName", displayName).Msg("Invalid School. Missing required attribute")
|
||||
return nil
|
||||
}
|
||||
|
||||
school := libregraph.NewEducationSchool()
|
||||
school.SetDisplayName(displayName)
|
||||
school.SetId(id)
|
||||
if schoolNumber != "" {
|
||||
school.SetSchoolNumber(schoolNumber)
|
||||
}
|
||||
if externalId != "" {
|
||||
school.SetExternalId(externalId)
|
||||
}
|
||||
if t != nil {
|
||||
school.SetTerminationDate(*t)
|
||||
}
|
||||
return school
|
||||
}
|
||||
|
||||
func (i *LDAP) getSchoolNumber(e *ldap.Entry) string {
|
||||
schoolNumber := e.GetEqualFoldAttributeValue(i.educationConfig.schoolAttributeMap.schoolNumber)
|
||||
return schoolNumber
|
||||
}
|
||||
|
||||
func (i *LDAP) getExternalId(e *ldap.Entry) string {
|
||||
externalId := e.GetEqualFoldAttributeValue(i.educationConfig.schoolAttributeMap.externalId)
|
||||
return externalId
|
||||
}
|
||||
|
||||
func (i *LDAP) getID(e *ldap.Entry) string {
|
||||
id := e.GetEqualFoldAttributeValue(i.educationConfig.schoolAttributeMap.id)
|
||||
return id
|
||||
}
|
||||
|
||||
func (i *LDAP) getDisplayName(e *ldap.Entry) string {
|
||||
displayName := e.GetEqualFoldAttributeValue(i.educationConfig.schoolAttributeMap.displayName)
|
||||
return displayName
|
||||
}
|
||||
|
||||
func (i *LDAP) getTerminationDate(e *ldap.Entry) (*time.Time, error) {
|
||||
dateString := e.GetEqualFoldAttributeValue(i.educationConfig.schoolAttributeMap.terminationDate)
|
||||
if dateString == "" {
|
||||
return nil, errNotSet
|
||||
}
|
||||
t, err := time.Parse(ldapDateFormat, dateString)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error parsing LDAP date: '%s': %w", dateString, err)
|
||||
return nil, err
|
||||
}
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
func (i *LDAP) getEducationSchoolAttrTypes() []string {
|
||||
return []string{
|
||||
i.educationConfig.schoolAttributeMap.displayName,
|
||||
i.educationConfig.schoolAttributeMap.id,
|
||||
i.educationConfig.schoolAttributeMap.externalId,
|
||||
i.educationConfig.schoolAttributeMap.schoolNumber,
|
||||
i.educationConfig.schoolAttributeMap.terminationDate,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user