mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-02-24 14:38:56 -06:00
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:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user