Skip to content

Commit

Permalink
Merge pull request #5534 from owncloud/excds/feature/5411_support_add…
Browse files Browse the repository at this point in the history
…ing_and_removing_of_teachers_for_classes

graph: Add support for listing/adding/removing teachers to a class
  • Loading branch information
Daniel Swärd authored Feb 13, 2023
2 parents 2c98d32 + 2338515 commit e853b98
Show file tree
Hide file tree
Showing 14 changed files with 590 additions and 73 deletions.
7 changes: 7 additions & 0 deletions services/graph/pkg/identity/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,13 @@ type EducationBackend interface {
GetEducationUser(ctx context.Context, nameOrID string) (*libregraph.EducationUser, error)
// GetEducationUsers lists all education users
GetEducationUsers(ctx context.Context) ([]*libregraph.EducationUser, error)

// GetEducationClassTeachers returns the EducationUser teachers for an EducationClass
GetEducationClassTeachers(ctx context.Context, classID string) ([]*libregraph.EducationUser, error)
// AddTeacherToEducationclass adds a teacher (by ID) to class in the identity backend.
AddTeacherToEducationClass(ctx context.Context, classID string, teacherID string) error
// RemoveTeacherFromEducationClass removes teacher (by ID) from a class
RemoveTeacherFromEducationClass(ctx context.Context, classID string, teacherID string) error
}

func CreateUserModelFromCS3(u *cs3.User) *libregraph.User {
Expand Down
15 changes: 15 additions & 0 deletions services/graph/pkg/identity/err_education.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,18 @@ func (i *ErrEducationBackend) GetEducationUser(ctx context.Context, nameOrID str
func (i *ErrEducationBackend) GetEducationUsers(ctx context.Context) ([]*libregraph.EducationUser, error) {
return nil, errNotImplemented
}

// GetEducationClassTeachers implements the EducationBackend interface for the ErrEducationBackend backend.
func (i *ErrEducationBackend) GetEducationClassTeachers(ctx context.Context, classID string) ([]*libregraph.EducationUser, error) {
return nil, errNotImplemented
}

// AddTeacherToEducationClass implements the EducationBackend interface for the ErrEducationBackend backend.
func (i *ErrEducationBackend) AddTeacherToEducationClass(ctx context.Context, classID string, teacherID string) error {
return errNotImplemented
}

// RemoveTeacherFromEducationClass implements the EducationBackend interface for the ErrEducationBackend backend.
func (i *ErrEducationBackend) RemoveTeacherFromEducationClass(ctx context.Context, classID string, teacherID string) error {
return errNotImplemented
}
63 changes: 62 additions & 1 deletion services/graph/pkg/identity/ldap.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/CiscoM31/godata"
"github.com/go-ldap/ldap/v3"
"github.com/gofrs/uuid"
"github.com/libregraph/idm/pkg/ldapdn"
libregraph "github.com/owncloud/libre-graph-api-go"
oldap "github.com/owncloud/ocis/v2/ocis-pkg/ldap"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
Expand Down Expand Up @@ -179,7 +180,7 @@ func (i *LDAP) DeleteUser(ctx context.Context, nameOrID string) error {
for _, group := range groupEntries {
logger.Debug().Str("group", group.DN).Str("user", e.DN).Msg("Cleaning up group membership")

if mr, err := i.removeMemberFromGroupEntry(group, e.DN); err == nil {
if mr, err := i.removeEntryByDNAndAttributeFromEntry(group, e.DN, i.groupAttributeMap.member); err == nil {
if err = i.conn.Modify(mr); err != nil {
// Errors when deleting the memberships are only logged as warnings but not returned
// to the user as we already successfully deleted the users itself
Expand Down Expand Up @@ -608,3 +609,63 @@ func stringToScope(scope string) (int, error) {
}
return s, nil
}

// removeEntryByDNAndAttributeFromEntry creates a request to remove a single member entry by attribute and DN from an ldap entry
func (i *LDAP) removeEntryByDNAndAttributeFromEntry(entry *ldap.Entry, dn string, attribute string) (*ldap.ModifyRequest, error) {
nOldDN, err := ldapdn.ParseNormalize(dn)
if err != nil {
return nil, err
}
entries := entry.GetEqualFoldAttributeValues(attribute)
found := false
for _, entry := range entries {
if entry == "" {
continue
}
if nEntry, err := ldapdn.ParseNormalize(entry); err != nil {
// We couldn't parse the entry value as a DN. Let's keep it
// as it is but log a warning
i.logger.Warn().Str("entryDN", entry).Err(err).Msg("Couldn't parse DN")
continue
} else {
if nEntry == nOldDN {
found = true
}
}
}
if !found {
i.logger.Debug().Str("backend", "ldap").Str("entry", entry.DN).Str("target", dn).
Msg("The target is not an entry in the attribute list")
return nil, ErrNotFound
}

mr := ldap.ModifyRequest{DN: entry.DN}
if len(entries) == 1 {
mr.Add(attribute, []string{""})
}
mr.Delete(attribute, []string{dn})
return &mr, nil
}

// expandLDAPAttributeEntries reads an attribute from an ldap entry and expands to users
func (i *LDAP) expandLDAPAttributeEntries(ctx context.Context, e *ldap.Entry, attribute string) ([]*ldap.Entry, error) {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("ExpandLDAPAttributeEntries")
result := []*ldap.Entry{}

for _, entryDN := range e.GetEqualFoldAttributeValues(attribute) {
if entryDN == "" {
continue
}
logger.Debug().Str("entryDN", entryDN).Msg("lookup")
ue, err := i.getUserByDN(entryDN)
if err != nil {
// Ignore errors when reading a specific entry fails, just log them and continue
logger.Debug().Err(err).Str("entry", entryDN).Msg("error reading attribute member entry")
continue
}
result = append(result, ue)
}

return result, nil
}
116 changes: 115 additions & 1 deletion services/graph/pkg/identity/ldap_education_class.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"

"github.com/go-ldap/ldap/v3"
"github.com/libregraph/idm/pkg/ldapdn"
libregraph "github.com/owncloud/libre-graph-api-go"
oldap "github.com/owncloud/ocis/v2/ocis-pkg/ldap"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
Expand All @@ -14,12 +15,14 @@ import (
type educationClassAttributeMap struct {
externalID string
classification string
teachers string
}

func newEducationClassAttributeMap() educationClassAttributeMap {
return educationClassAttributeMap{
externalID: "ocEducationExternalId",
classification: "ocEducationClassType",
teachers: "ocEducationTeacherMember",
}
}

Expand Down Expand Up @@ -225,7 +228,7 @@ func (i *LDAP) GetEducationClassMembers(ctx context.Context, id string) ([]*libr
return nil, err
}

memberEntries, err := i.expandLDAPGroupMembers(ctx, e)
memberEntries, err := i.expandLDAPAttributeEntries(ctx, e, i.groupAttributeMap.member)
result := make([]*libregraph.EducationUser, 0, len(memberEntries))
if err != nil {
return nil, err
Expand Down Expand Up @@ -283,6 +286,7 @@ func (i *LDAP) getEducationClassAttrTypes(requestMembers bool) []string {
i.educationConfig.classAttributeMap.classification,
i.educationConfig.classAttributeMap.externalID,
i.educationConfig.memberOfSchoolAttribute,
i.educationConfig.classAttributeMap.teachers,
}
if requestMembers {
attrs = append(attrs, i.groupAttributeMap.member)
Expand Down Expand Up @@ -337,3 +341,113 @@ func (i *LDAP) getEducationClassByID(nameOrID string, requestMembers bool) (*lda
i.getEducationClassAttrTypes(requestMembers),
)
}

// GetEducationClassTeachers returns the EducationUser teachers for an EducationClass
func (i *LDAP) GetEducationClassTeachers(ctx context.Context, classID string) ([]*libregraph.EducationUser, error) {
logger := i.logger.SubloggerWithRequestID(ctx)
class, err := i.getEducationClassByID(classID, false)
if err != nil {
logger.Debug().Err(err).Msg("could not get class: backend error")
return nil, err
}

teacherEntries, err := i.expandLDAPAttributeEntries(ctx, class, i.educationConfig.classAttributeMap.teachers)
result := make([]*libregraph.EducationUser, 0, len(teacherEntries))
if err != nil {
return nil, err
}
for _, teacher := range teacherEntries {
if u := i.createEducationUserModelFromLDAP(teacher); u != nil {
result = append(result, u)
}
}

return result, nil

}

// AddTeacherToEducationClass adds a teacher (by ID) to class in the identity backend.
func (i *LDAP) AddTeacherToEducationClass(ctx context.Context, classID string, teacherID string) error {
logger := i.logger.SubloggerWithRequestID(ctx)
class, err := i.getEducationClassByID(classID, false)
if err != nil {
logger.Debug().Err(err).Msg("could not get class: backend error")
return err
}

logger.Debug().Str("classDn", class.DN).Msg("got a class")
teacher, err := i.getEducationUserByNameOrID(teacherID)

if err != nil {
logger.Debug().Err(err).Msg("could not get education user: error fetching education user from backend")
return err
}

logger.Debug().Str("userDn", teacher.DN).Msg("got a user")

mr := ldap.ModifyRequest{DN: class.DN}
// Handle empty teacher list
current := class.GetEqualFoldAttributeValues(i.educationConfig.classAttributeMap.teachers)
if len(current) == 1 && current[0] == "" {
mr.Delete(i.educationConfig.classAttributeMap.teachers, []string{""})
}

// Create a Set of current teachers
currentSet := make(map[string]struct{}, len(current))
for _, currentTeacher := range current {
if currentTeacher == "" {
continue
}
nCurrentTeacher, err := ldapdn.ParseNormalize(currentTeacher)
if err != nil {
// Couldn't parse teacher value as a DN, skipping
logger.Warn().Str("teacherDN", currentTeacher).Err(err).Msg("Couldn't parse DN")
continue
}
currentSet[nCurrentTeacher] = struct{}{}
}

var newTeacherDN []string
nDN, err := ldapdn.ParseNormalize(teacher.DN)
if err != nil {
logger.Error().Str("new teacher", teacher.DN).Err(err).Msg("Couldn't parse DN")
return err
}
if _, present := currentSet[nDN]; !present {
newTeacherDN = append(newTeacherDN, teacher.DN)
} else {
logger.Debug().Str("teacherDN", teacher.DN).Msg("Member already present in group. Skipping")
}

if len(newTeacherDN) > 0 {
mr.Add(i.educationConfig.classAttributeMap.teachers, newTeacherDN)

if err := i.conn.Modify(&mr); err != nil {
return err
}
}

return nil
}

// RemoveTeacherFromEducationClass removes teacher (by ID) from a class
func (i *LDAP) RemoveTeacherFromEducationClass(ctx context.Context, classID string, teacherID string) error {
logger := i.logger.SubloggerWithRequestID(ctx)
class, err := i.getEducationClassByID(classID, false)
if err != nil {
logger.Debug().Err(err).Msg("could not get class: backend error")
return err
}

teacher, err := i.getEducationUserByNameOrID(teacherID)
if err != nil {
logger.Debug().Err(err).Msg("could not get education user: error fetching education user from backend")
return err
}

if mr, err := i.removeEntryByDNAndAttributeFromEntry(class, teacher.DN, i.educationConfig.classAttributeMap.teachers); err == nil {
return i.conn.Modify(mr)
}

return nil
}
6 changes: 3 additions & 3 deletions services/graph/pkg/identity/ldap_education_class_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ func TestGetEducationClass(t *testing.T) {
Scope: 2,
SizeLimit: 1,
Filter: tt.filter,
Attributes: []string{"cn", "entryUUID", "ocEducationClassType", "ocEducationExternalId", "ocMemberOfSchool"},
Attributes: []string{"cn", "entryUUID", "ocEducationClassType", "ocEducationExternalId", "ocMemberOfSchool", "ocEducationTeacherMember"},
Controls: []ldap.Control(nil),
}
if tt.expectedItemNotFound {
Expand Down Expand Up @@ -206,7 +206,7 @@ func TestDeleteEducationClass(t *testing.T) {
Scope: 2,
SizeLimit: 1,
Filter: tt.filter,
Attributes: []string{"cn", "entryUUID", "ocEducationClassType", "ocEducationExternalId", "ocMemberOfSchool"},
Attributes: []string{"cn", "entryUUID", "ocEducationClassType", "ocEducationExternalId", "ocMemberOfSchool", "ocEducationTeacherMember"},
Controls: []ldap.Control(nil),
}
if tt.expectedItemNotFound {
Expand Down Expand Up @@ -284,7 +284,7 @@ func TestGetEducationClassMembers(t *testing.T) {
Scope: 2,
SizeLimit: 1,
Filter: tt.filter,
Attributes: []string{"cn", "entryUUID", "ocEducationClassType", "ocEducationExternalId", "ocMemberOfSchool", "member"},
Attributes: []string{"cn", "entryUUID", "ocEducationClassType", "ocEducationExternalId", "ocMemberOfSchool", "ocEducationTeacherMember", "member"},
Controls: []ldap.Control(nil),
}
if tt.expectedItemNotFound {
Expand Down
6 changes: 3 additions & 3 deletions services/graph/pkg/identity/ldap_education_school_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ var classesBySchoolIDSearch *ldap.SearchRequest = &ldap.SearchRequest{
Scope: 2,
SizeLimit: 0,
Filter: "(&(objectClass=ocEducationClass)(ocMemberOfSchool=abcd-defg))",
Attributes: []string{"cn", "entryUUID", "ocEducationClassType", "ocEducationExternalId", "ocMemberOfSchool"},
Attributes: []string{"cn", "entryUUID", "ocEducationClassType", "ocEducationExternalId", "ocMemberOfSchool", "ocEducationTeacherMember"},
Controls: []ldap.Control(nil),
}

Expand All @@ -484,7 +484,7 @@ var classesByUUIDSearchNotFound *ldap.SearchRequest = &ldap.SearchRequest{
Scope: 2,
SizeLimit: 1,
Filter: "(&(objectClass=ocEducationClass)(|(entryUUID=does-not-exist)(ocEducationExternalId=does-not-exist)))",
Attributes: []string{"cn", "entryUUID", "ocEducationClassType", "ocEducationExternalId", "ocMemberOfSchool"},
Attributes: []string{"cn", "entryUUID", "ocEducationClassType", "ocEducationExternalId", "ocMemberOfSchool", "ocEducationTeacherMember"},
Controls: []ldap.Control(nil),
}

Expand All @@ -493,7 +493,7 @@ var classesByUUIDSearchFound *ldap.SearchRequest = &ldap.SearchRequest{
Scope: 2,
SizeLimit: 1,
Filter: "(&(objectClass=ocEducationClass)(|(entryUUID=abcd-defg)(ocEducationExternalId=abcd-defg)))",
Attributes: []string{"cn", "entryUUID", "ocEducationClassType", "ocEducationExternalId", "ocMemberOfSchool"},
Attributes: []string{"cn", "entryUUID", "ocEducationClassType", "ocEducationExternalId", "ocMemberOfSchool", "ocEducationTeacherMember"},
Controls: []ldap.Control(nil),
}

Expand Down
Loading

0 comments on commit e853b98

Please sign in to comment.