Merge pull request #5534 from owncloud/excds/feature/5411_support_adding_and_removing_of_teachers_for_classes

graph: Add support for listing/adding/removing teachers to a class
This commit is contained in:
Daniel Swärd
2023-02-13 11:48:58 +01:00
committed by GitHub
14 changed files with 590 additions and 73 deletions

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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"
@@ -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
@@ -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
}

View File

@@ -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"
@@ -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",
}
}
@@ -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
@@ -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)
@@ -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
}

View File

@@ -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 {
@@ -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 {
@@ -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 {

View File

@@ -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),
}
@@ -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),
}
@@ -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),
}

View File

@@ -37,7 +37,7 @@ func (i *LDAP) GetGroup(ctx context.Context, nameOrID string, queryParam url.Val
return nil, errorcode.New(errorcode.ItemNotFound, "not found")
}
if slices.Contains(sel, "members") || slices.Contains(exp, "members") {
members, err := i.expandLDAPGroupMembers(ctx, e)
members, err := i.expandLDAPAttributeEntries(ctx, e, i.groupAttributeMap.member)
if err != nil {
return nil, err
}
@@ -115,7 +115,7 @@ func (i *LDAP) GetGroups(ctx context.Context, queryParam url.Values) ([]*libregr
continue
}
if expandMembers {
members, err := i.expandLDAPGroupMembers(ctx, e)
members, err := i.expandLDAPAttributeEntries(ctx, e, i.groupAttributeMap.member)
if err != nil {
return nil, err
}
@@ -142,7 +142,7 @@ func (i *LDAP) GetGroupMembers(ctx context.Context, groupID string) ([]*libregra
return nil, err
}
memberEntries, err := i.expandLDAPGroupMembers(ctx, e)
memberEntries, err := i.expandLDAPAttributeEntries(ctx, e, i.groupAttributeMap.member)
result := make([]*libregraph.User, 0, len(memberEntries))
if err != nil {
return nil, err
@@ -279,7 +279,7 @@ func (i *LDAP) RemoveMemberFromGroup(ctx context.Context, groupID string, member
}
logger.Debug().Str("backend", "ldap").Str("groupdn", ge.DN).Str("member", me.DN).Msg("remove member")
if mr, err := i.removeMemberFromGroupEntry(ge, me.DN); err == nil {
if mr, err := i.removeEntryByDNAndAttributeFromEntry(ge, me.DN, i.groupAttributeMap.member); err == nil {
return i.conn.Modify(mr)
}
return nil
@@ -323,28 +323,6 @@ func (i *LDAP) groupToLDAPAttrValues(group libregraph.Group) (map[string][]strin
return attrs, nil
}
func (i *LDAP) expandLDAPGroupMembers(ctx context.Context, e *ldap.Entry) ([]*ldap.Entry, error) {
logger := i.logger.SubloggerWithRequestID(ctx)
logger.Debug().Str("backend", "ldap").Msg("expandLDAPGroupMembers")
result := []*ldap.Entry{}
for _, memberDN := range e.GetEqualFoldAttributeValues(i.groupAttributeMap.member) {
if memberDN == "" {
continue
}
logger.Debug().Str("memberDN", memberDN).Msg("lookup")
ue, err := i.getUserByDN(memberDN)
if err != nil {
// Ignore errors when reading a specific member fails, just log them and continue
logger.Debug().Err(err).Str("member", memberDN).Msg("error reading group member")
continue
}
result = append(result, ue)
}
return result, nil
}
func (i *LDAP) getLDAPGroupByID(id string, requestMembers bool) (*ldap.Entry, error) {
id = ldap.EscapeFilter(id)
filter := fmt.Sprintf("(%s=%s)", i.groupAttributeMap.id, id)
@@ -413,45 +391,6 @@ func (i *LDAP) getLDAPGroupsByFilter(filter string, requestMembers, single bool)
return res.Entries, nil
}
// removeMemberFromGroupEntry creates an LDAP Modify request (not sending it)
// that would update the supplied entry to remove the specified member from the
// group
func (i *LDAP) removeMemberFromGroupEntry(group *ldap.Entry, memberDN string) (*ldap.ModifyRequest, error) {
nOldMemberDN, err := ldapdn.ParseNormalize(memberDN)
if err != nil {
return nil, err
}
members := group.GetEqualFoldAttributeValues(i.groupAttributeMap.member)
found := false
for _, member := range members {
if member == "" {
continue
}
if nMember, err := ldapdn.ParseNormalize(member); err != nil {
// We couldn't parse the member value as a DN. Let's keep it
// as it is but log a warning
i.logger.Warn().Str("memberDN", member).Err(err).Msg("Couldn't parse DN")
continue
} else {
if nMember == nOldMemberDN {
found = true
}
}
}
if !found {
i.logger.Debug().Str("backend", "ldap").Str("groupdn", group.DN).Str("member", memberDN).
Msg("The target is not a member of the group")
return nil, ErrNotFound
}
mr := ldap.ModifyRequest{DN: group.DN}
if len(members) == 1 {
mr.Add(i.groupAttributeMap.member, []string{""})
}
mr.Delete(i.groupAttributeMap.member, []string{memberDN})
return &mr, nil
}
func (i *LDAP) getGroupByDN(dn string) (*ldap.Entry, error) {
attrs := []string{
i.groupAttributeMap.id,

View File

@@ -29,6 +29,20 @@ func (_m *EducationBackend) AddClassesToEducationSchool(ctx context.Context, sch
return r0
}
// AddTeacherToEducationClass provides a mock function with given fields: ctx, classID, teacherID
func (_m *EducationBackend) AddTeacherToEducationClass(ctx context.Context, classID string, teacherID string) error {
ret := _m.Called(ctx, classID, teacherID)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
r0 = rf(ctx, classID, teacherID)
} else {
r0 = ret.Error(0)
}
return r0
}
// AddUsersToEducationSchool provides a mock function with given fields: ctx, schoolID, memberID
func (_m *EducationBackend) AddUsersToEducationSchool(ctx context.Context, schoolID string, memberID []string) error {
ret := _m.Called(ctx, schoolID, memberID)
@@ -200,6 +214,29 @@ func (_m *EducationBackend) GetEducationClassMembers(ctx context.Context, nameOr
return r0, r1
}
// GetEducationClassTeachers provides a mock function with given fields: ctx, classID
func (_m *EducationBackend) GetEducationClassTeachers(ctx context.Context, classID string) ([]*libregraph.EducationUser, error) {
ret := _m.Called(ctx, classID)
var r0 []*libregraph.EducationUser
if rf, ok := ret.Get(0).(func(context.Context, string) []*libregraph.EducationUser); ok {
r0 = rf(ctx, classID)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*libregraph.EducationUser)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, classID)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetEducationClasses provides a mock function with given fields: ctx
func (_m *EducationBackend) GetEducationClasses(ctx context.Context) ([]*libregraph.EducationClass, error) {
ret := _m.Called(ctx)
@@ -375,6 +412,20 @@ func (_m *EducationBackend) RemoveClassFromEducationSchool(ctx context.Context,
return r0
}
// RemoveTeacherFromEducationClass provides a mock function with given fields: ctx, classID, teacherID
func (_m *EducationBackend) RemoveTeacherFromEducationClass(ctx context.Context, classID string, teacherID string) error {
ret := _m.Called(ctx, classID, teacherID)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
r0 = rf(ctx, classID, teacherID)
} else {
r0 = ret.Error(0)
}
return r0
}
// RemoveUserFromEducationSchool provides a mock function with given fields: ctx, schoolID, memberID
func (_m *EducationBackend) RemoveUserFromEducationSchool(ctx context.Context, schoolID string, memberID string) error {
ret := _m.Called(ctx, schoolID, memberID)

View File

@@ -443,6 +443,167 @@ func (g Graph) DeleteEducationClassMember(w http.ResponseWriter, r *http.Request
render.NoContent(w, r)
}
// GetEducationClassTeachers implements the Service interface.
func (g Graph) GetEducationClassTeachers(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Msg("calling get class teachers")
classID := chi.URLParam(r, "classID")
classID, err := url.PathUnescape(classID)
if err != nil {
logger.Debug().Str("id", classID).Msg("could not get class teachers: unescaping class id failed")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping class id failed")
return
}
if classID == "" {
logger.Debug().Msg("could not get class teachers: missing class id")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing class id")
return
}
logger.Debug().Str("id", classID).Msg("calling get class teachers on backend")
teachers, err := g.identityEducationBackend.GetEducationClassTeachers(r.Context(), classID)
if err != nil {
logger.Debug().Err(err).Msg("could not get class teachers: backend error")
var errcode errorcode.Error
if errors.As(err, &errcode) {
errcode.Render(w, r)
} else {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
}
return
}
render.Status(r, http.StatusOK)
render.JSON(w, r, teachers)
}
// PostEducationClassTeacher implements the Service interface.
func (g Graph) PostEducationClassTeacher(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Msg("Calling post class teacher")
classID := chi.URLParam(r, "classID")
classID, err := url.PathUnescape(classID)
if err != nil {
logger.Debug().
Err(err).
Str("id", classID).
Msg("could not add teacher to class: unescaping class id failed")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping class id failed")
return
}
if classID == "" {
logger.Debug().Msg("could not add class teacher: missing class id")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing class id")
return
}
memberRef := libregraph.NewMemberReference()
err = json.NewDecoder(r.Body).Decode(memberRef)
if err != nil {
logger.Debug().
Err(err).
Interface("body", r.Body).
Msg("could not add class teacher: invalid request body")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, fmt.Sprintf("invalid request body: %s", err.Error()))
return
}
memberRefURL, ok := memberRef.GetOdataIdOk()
if !ok {
logger.Debug().Msg("could not add class teacher: @odata.id reference is missing")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "@odata.id reference is missing")
return
}
memberType, id, err := g.parseMemberRef(*memberRefURL)
if err != nil {
logger.Debug().Err(err).Msg("could not add class teacher: error parsing @odata.id url")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "Error parsing @odata.id url")
return
}
// The MS Graph spec allows "directoryObject", "user", "class" and "organizational Contact"
// we restrict this to users for now. Might add EducationClass as members later
if memberType != memberTypeUsers {
logger.Debug().Str("type", memberType).Msg("could not add class member: Only users are allowed as class teachers")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "Only users are allowed as class teachers")
return
}
logger.Debug().Str("memberType", memberType).Str("id", id).Msg("calling add teacher on backend")
err = g.identityEducationBackend.AddTeacherToEducationClass(r.Context(), classID, id)
if err != nil {
logger.Debug().Err(err).Msg("could not add class teacher: backend error")
var errcode errorcode.Error
if errors.As(err, &errcode) {
errcode.Render(w, r)
} else {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
}
return
}
/* TODO requires reva changes
currentUser := revactx.ContextMustGetUser(r.Context())
g.publishEvent(events.EducationClassTeacherAdded{Executant: currentUser.Id, EducationClassID: classID, UserID: id})
*/
render.Status(r, http.StatusNoContent)
render.NoContent(w, r)
}
// DeleteEducationClassTeacher implements the Service interface.
func (g Graph) DeleteEducationClassTeacher(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Msg("calling delete class teacher")
classID := chi.URLParam(r, "classID")
classID, err := url.PathUnescape(classID)
if err != nil {
logger.Debug().Err(err).Str("id", classID).Msg("could not delete class teacher: unescaping class id failed")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping class id failed")
return
}
if classID == "" {
logger.Debug().Msg("could not delete class teacher: missing class id")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing class id")
return
}
teacherID := chi.URLParam(r, "teacherID")
teacherID, err = url.PathUnescape(teacherID)
if err != nil {
logger.Debug().Err(err).Str("id", teacherID).Msg("could not delete class teacher: unescaping teacher id failed")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping teacher id failed")
return
}
if teacherID == "" {
logger.Debug().Msg("could not delete class teacher: missing teacher id")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing teacher id")
return
}
logger.Debug().Str("classID", classID).Str("teacherID", teacherID).Msg("calling delete teacher on backend")
err = g.identityEducationBackend.RemoveTeacherFromEducationClass(r.Context(), classID, teacherID)
if err != nil {
logger.Debug().Err(err).Msg("could not delete class teacher: backend error")
var errcode errorcode.Error
if errors.As(err, &errcode) {
errcode.Render(w, r)
} else {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
}
return
}
/* TODO requires reva changes
currentUser := revactx.ContextMustGetUser(r.Context())
g.publishEvent(events.EducationClassTeacherRemoved{Executant: currentUser.Id, EducationClassID: classID, UserID: teacherID})
*/
render.Status(r, http.StatusNoContent)
render.NoContent(w, r)
}
func sortClasses(req *godata.GoDataRequest, classes []*libregraph.EducationClass) ([]*libregraph.EducationClass, error) {
if req.Query.OrderBy == nil || len(req.Query.OrderBy.OrderByItems) != 1 {
return classes, nil

View File

@@ -530,4 +530,119 @@ var _ = Describe("EducationClass", func() {
identityBackend.AssertNumberOfCalls(GinkgoT(), "RemoveMemberFromGroup", 1)
})
})
Describe("GetEducationClassTeachers", func() {
It("gets the list of teachers", func() {
user := libregraph.NewEducationUser()
user.SetId("user")
identityEducationBackend.On("GetEducationClassTeachers", mock.Anything, mock.Anything, mock.Anything).
Return([]*libregraph.EducationUser{user}, nil)
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/classes/{classID}/teachers", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("classID", *newClass.Id)
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.GetEducationClassTeachers(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
data, err := io.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
var teachers []*libregraph.EducationUser
err = json.Unmarshal(data, &teachers)
Expect(err).ToNot(HaveOccurred())
Expect(len(teachers)).To(Equal(1))
Expect(teachers[0].GetId()).To(Equal("user"))
})
})
Describe("PostEducationClassTeacher", func() {
It("fails on invalid body", func() {
r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/classes/{classID}/teachers", bytes.NewBufferString("{invalid"))
rctx := chi.NewRouteContext()
rctx.URLParams.Add("classID", *newClass.Id)
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.PostEducationClassTeacher(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("fails on missing teacher refs", func() {
member := libregraph.NewMemberReference()
data, err := json.Marshal(member)
Expect(err).ToNot(HaveOccurred())
r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/classes/{classID}/teachers", bytes.NewBuffer(data))
rctx := chi.NewRouteContext()
rctx.URLParams.Add("classID", *newClass.Id)
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.PostEducationClassTeacher(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("fails on invalid teacher refs", func() {
member := libregraph.NewMemberReference()
member.SetOdataId("/invalidtype/user")
data, err := json.Marshal(member)
Expect(err).ToNot(HaveOccurred())
r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/classes/{classID}/teachers", bytes.NewBuffer(data))
rctx := chi.NewRouteContext()
rctx.URLParams.Add("classID", *newClass.Id)
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.PostEducationClassTeacher(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("adds a new teacher", func() {
member := libregraph.NewMemberReference()
member.SetOdataId("/users/user")
data, err := json.Marshal(member)
Expect(err).ToNot(HaveOccurred())
identityEducationBackend.On("AddTeacherToEducationClass", mock.Anything, mock.Anything, mock.Anything).Return(nil)
r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/classes/{classID}/teachers", bytes.NewBuffer(data))
rctx := chi.NewRouteContext()
rctx.URLParams.Add("classID", *newClass.Id)
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.PostEducationClassTeacher(rr, r)
Expect(rr.Code).To(Equal(http.StatusNoContent))
identityEducationBackend.AssertNumberOfCalls(GinkgoT(), "AddTeacherToEducationClass", 1)
})
})
Describe("DeleteEducationClassTeacher", func() {
It("handles missing or empty teacher id", func() {
r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/education/classes/{classID}/teachers/{teacherID}/$ref", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("classID", *newClass.Id)
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.DeleteEducationClassTeacher(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("handles missing or empty teacher id", func() {
r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/education/classes/{classID}/teachers/{teacherID}/$ref", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("teacherID", "/users/user")
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.DeleteEducationClassTeacher(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("deletes teacher", func() {
identityEducationBackend.On("RemoveTeacherFromEducationClass", mock.Anything, mock.Anything, mock.Anything).Return(nil)
r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/education/classes/{classID}/teachers/{teacherID}/$ref", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("classID", *newClass.Id)
rctx.URLParams.Add("teacherID", "/users/user1")
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.DeleteEducationClassTeacher(rr, r)
Expect(rr.Code).To(Equal(http.StatusNoContent))
identityEducationBackend.AssertNumberOfCalls(GinkgoT(), "RemoveTeacherFromEducationClass", 1)
})
})
})

View File

@@ -293,3 +293,18 @@ func (i instrument) AssignTags(w http.ResponseWriter, r *http.Request) {
func (i instrument) UnassignTags(w http.ResponseWriter, r *http.Request) {
i.next.UnassignTags(w, r)
}
// GetEducationClassTeachers implements the Service interface.
func (i instrument) GetEducationClassTeachers(w http.ResponseWriter, r *http.Request) {
i.next.UnassignTags(w, r)
}
// PostEducationClassTeacher implements the Service interface.
func (i instrument) PostEducationClassTeacher(w http.ResponseWriter, r *http.Request) {
i.next.UnassignTags(w, r)
}
// DeleteEducationClassTeacher implements the Service interface.
func (i instrument) DeleteEducationClassTeacher(w http.ResponseWriter, r *http.Request) {
i.next.UnassignTags(w, r)
}

View File

@@ -293,3 +293,18 @@ func (l logging) AssignTags(w http.ResponseWriter, r *http.Request) {
func (l logging) UnassignTags(w http.ResponseWriter, r *http.Request) {
l.next.UnassignTags(w, r)
}
// GetEducationClassTeachers implements the Service interface.
func (l logging) GetEducationClassTeachers(w http.ResponseWriter, r *http.Request) {
l.next.UnassignTags(w, r)
}
// PostEducationClassTeacher implements the Service interface.
func (l logging) PostEducationClassTeacher(w http.ResponseWriter, r *http.Request) {
l.next.UnassignTags(w, r)
}
// DeleteEducationClassTeacher implements the Service interface.
func (l logging) DeleteEducationClassTeacher(w http.ResponseWriter, r *http.Request) {
l.next.UnassignTags(w, r)
}

View File

@@ -84,6 +84,10 @@ type Service interface {
PatchEducationUser(http.ResponseWriter, *http.Request)
DeleteEducationClassMember(w http.ResponseWriter, r *http.Request)
GetEducationClassTeachers(w http.ResponseWriter, r *http.Request)
PostEducationClassTeacher(w http.ResponseWriter, r *http.Request)
DeleteEducationClassTeacher(w http.ResponseWriter, r *http.Request)
GetDrives(w http.ResponseWriter, r *http.Request)
GetSingleDrive(w http.ResponseWriter, r *http.Request)
GetAllDrives(w http.ResponseWriter, r *http.Request)
@@ -272,6 +276,11 @@ func NewService(opts ...Option) (Graph, error) {
r.Post("/$ref", svc.PostEducationClassMember)
r.Delete("/{memberID}/$ref", svc.DeleteEducationClassMember)
})
r.Route("/teachers", func(r chi.Router) {
r.Get("/", svc.GetEducationClassTeachers)
r.Post("/$ref", svc.PostEducationClassTeacher)
r.Delete("/{teacherID}/$ref", svc.DeleteEducationClassTeacher)
})
})
})
})

View File

@@ -289,3 +289,18 @@ func (t tracing) AssignTags(w http.ResponseWriter, r *http.Request) {
func (t tracing) UnassignTags(w http.ResponseWriter, r *http.Request) {
t.next.UnassignTags(w, r)
}
// GetEducationClassTeachers implements the Service interface.
func (t tracing) GetEducationClassTeachers(w http.ResponseWriter, r *http.Request) {
t.next.UnassignTags(w, r)
}
// PostEducationClassTeacher implements the Service interface.
func (t tracing) PostEducationClassTeacher(w http.ResponseWriter, r *http.Request) {
t.next.UnassignTags(w, r)
}
// DeleteEducationClassTeacher implements the Service interface.
func (t tracing) DeleteEducationClassTeacher(w http.ResponseWriter, r *http.Request) {
t.next.UnassignTags(w, r)
}