From 20f6a212f3239df59ce1ff6687fafc6c3fc7063f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABl=20Franke?= Date: Fri, 27 Jan 2023 13:55:00 +0100 Subject: [PATCH] Add service endpoints. --- services/graph/pkg/identity/backend.go | 7 + services/graph/pkg/identity/err_education.go | 15 ++ .../pkg/identity/ldap_education_class.go | 4 +- .../pkg/identity/ldap_education_school.go | 2 +- .../pkg/identity/mocks/education_backend.go | 51 ++++++ .../graph/pkg/service/v0/educationschools.go | 170 ++++++++++++++++++ services/graph/pkg/service/v0/service.go | 5 + 7 files changed, 251 insertions(+), 3 deletions(-) diff --git a/services/graph/pkg/identity/backend.go b/services/graph/pkg/identity/backend.go index 440909f3b..d8c9d877d 100644 --- a/services/graph/pkg/identity/backend.go +++ b/services/graph/pkg/identity/backend.go @@ -60,6 +60,13 @@ type EducationBackend interface { // RemoveUserFromEducationSchool removes a single member (by ID) from a school RemoveUserFromEducationSchool(ctx context.Context, schoolID string, memberID string) error + // GetEducationSchoolClasses lists all classes in a chool + GetEducationSchoolClasses(ctx context.Context, schoolNumberOrID string) ([]*libregraph.EducationClass, error) + // AddClassesToEducationSchool adds new classes (referenced by a slice of IDs) to supplied school in the identity backend. + AddClassesToEducationSchool(ctx context.Context, schoolNumberOrID string, memberIDs []string) error + // RemoveClassFromEducationSchool removes a class from a school. + RemoveClassFromEducationSchool(ctx context.Context, schoolNumberOrID string, memberID string) error + // GetEducationClasses lists all classes GetEducationClasses(ctx context.Context, queryParam url.Values) ([]*libregraph.EducationClass, error) // GetEducationClasses reads a given class by id diff --git a/services/graph/pkg/identity/err_education.go b/services/graph/pkg/identity/err_education.go index b18080d51..323e01d79 100644 --- a/services/graph/pkg/identity/err_education.go +++ b/services/graph/pkg/identity/err_education.go @@ -40,6 +40,21 @@ func (i *ErrEducationBackend) GetEducationSchoolUsers(ctx context.Context, id st return nil, errNotImplemented } +// GetEducationSchoolClasses implements the EducationBackend interface for the ErrEducationBackend backend. +func (i *ErrEducationBackend) GetEducationSchoolClasses(ctx context.Context, schoolNumberOrID string) ([]*libregraph.EducationClass, error) { + return nil, errNotImplemented +} + +// AddClassesToEducationSchool implements the EducationBackend interface for the ErrEducationBackend backend. +func (i *ErrEducationBackend) AddClassesToEducationSchool(ctx context.Context, schoolNumberOrID string, memberIDs []string) error { + return errNotImplemented +} + +// RemoveClassFromEducationSchool implements the EducationBackend interface for the ErrEducationBackend backend. +func (i *ErrEducationBackend) RemoveClassFromEducationSchool(ctx context.Context, schoolNumberOrID string, memberID string) error { + return errNotImplemented +} + // AddUsersToEducationSchool adds new members (reference by a slice of IDs) to supplied school in the identity backend. func (i *ErrEducationBackend) AddUsersToEducationSchool(ctx context.Context, schoolID string, memberID []string) error { return errNotImplemented diff --git a/services/graph/pkg/identity/ldap_education_class.go b/services/graph/pkg/identity/ldap_education_class.go index 0d2d96afa..e3f2d2386 100644 --- a/services/graph/pkg/identity/ldap_education_class.go +++ b/services/graph/pkg/identity/ldap_education_class.go @@ -344,8 +344,8 @@ func (i *LDAP) getEducationClassLDAPDN(class libregraph.EducationClass) string { func (i *LDAP) getEducationClassByID(nameOrID string, requestMembers bool) (*ldap.Entry, error) { return i.getEducationObjectByNameOrID( nameOrID, - i.groupAttributeMap.name, - i.groupAttributeMap.id, + i.userAttributeMap.id, + i.educationConfig.classAttributeMap.externalID, i.groupFilter, i.educationConfig.classObjectClass, i.groupBaseDN, diff --git a/services/graph/pkg/identity/ldap_education_school.go b/services/graph/pkg/identity/ldap_education_school.go index 48f89a593..52804e560 100644 --- a/services/graph/pkg/identity/ldap_education_school.go +++ b/services/graph/pkg/identity/ldap_education_school.go @@ -354,7 +354,7 @@ func (i *LDAP) GetEducationSchoolUsers(ctx context.Context, schoolNumberOrID str logger.Debug().Str("backend", "ldap").Msg("GetEducationSchoolUsers") entries, err := i.getEducationSchoolEntries( - schoolNumberOrID, i.userFilter, i.educationConfig.userObjectClass, i.userBaseDN, i.userScope, i.getUserAttrTypes(), logger, + schoolNumberOrID, i.userFilter, i.educationConfig.userObjectClass, i.userBaseDN, i.userScope, i.getEducationUserAttrTypes(), logger, ) if err != nil { return nil, err diff --git a/services/graph/pkg/identity/mocks/education_backend.go b/services/graph/pkg/identity/mocks/education_backend.go index cb8278116..dd6af905e 100644 --- a/services/graph/pkg/identity/mocks/education_backend.go +++ b/services/graph/pkg/identity/mocks/education_backend.go @@ -17,6 +17,20 @@ type EducationBackend struct { mock.Mock } +// AddClassesToEducationSchool provides a mock function with given fields: ctx, schoolNumberOrID, memberIDs +func (_m *EducationBackend) AddClassesToEducationSchool(ctx context.Context, schoolNumberOrID string, memberIDs []string) error { + ret := _m.Called(ctx, schoolNumberOrID, memberIDs) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, []string) error); ok { + r0 = rf(ctx, schoolNumberOrID, memberIDs) + } 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) @@ -234,6 +248,29 @@ func (_m *EducationBackend) GetEducationSchool(ctx context.Context, nameOrID str return r0, r1 } +// GetEducationSchoolClasses provides a mock function with given fields: ctx, schoolNumberOrID +func (_m *EducationBackend) GetEducationSchoolClasses(ctx context.Context, schoolNumberOrID string) ([]*libregraph.EducationClass, error) { + ret := _m.Called(ctx, schoolNumberOrID) + + var r0 []*libregraph.EducationClass + if rf, ok := ret.Get(0).(func(context.Context, string) []*libregraph.EducationClass); ok { + r0 = rf(ctx, schoolNumberOrID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*libregraph.EducationClass) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, schoolNumberOrID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetEducationSchoolUsers provides a mock function with given fields: ctx, id func (_m *EducationBackend) GetEducationSchoolUsers(ctx context.Context, id string) ([]*libregraph.EducationUser, error) { ret := _m.Called(ctx, id) @@ -326,6 +363,20 @@ func (_m *EducationBackend) GetEducationUsers(ctx context.Context, queryParam ur return r0, r1 } +// RemoveClassFromEducationSchool provides a mock function with given fields: ctx, schoolNumberOrID, memberID +func (_m *EducationBackend) RemoveClassFromEducationSchool(ctx context.Context, schoolNumberOrID string, memberID string) error { + ret := _m.Called(ctx, schoolNumberOrID, memberID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, schoolNumberOrID, memberID) + } 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) diff --git a/services/graph/pkg/service/v0/educationschools.go b/services/graph/pkg/service/v0/educationschools.go index 8ac3df947..acb4be015 100644 --- a/services/graph/pkg/service/v0/educationschools.go +++ b/services/graph/pkg/service/v0/educationschools.go @@ -423,6 +423,176 @@ func (g Graph) DeleteEducationSchoolUser(w http.ResponseWriter, r *http.Request) render.NoContent(w, r) } +// GetEducationSchoolUsers implements the Service interface. +func (g Graph) GetEducationSchoolClasses(w http.ResponseWriter, r *http.Request) { + logger := g.logger.SubloggerWithRequestID(r.Context()) + logger.Info().Msg("calling get school classes") + schoolID := chi.URLParam(r, "schoolID") + schoolID, err := url.PathUnescape(schoolID) + if err != nil { + logger.Debug().Str("id", schoolID).Msg("could not get school users: unescaping school id failed") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping school id failed") + return + } + + if schoolID == "" { + logger.Debug().Msg("could not get school users: missing school id") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing school id") + return + } + + logger.Debug().Str("id", schoolID).Msg("calling get school classes on backend") + classes, err := g.identityEducationBackend.GetEducationSchoolClasses(r.Context(), schoolID) + if err != nil { + logger.Debug().Err(err).Msg("could not get school classes: 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, classes) +} + +// PostEducationSchoolUser implements the Service interface. +func (g Graph) PostEducationSchoolClass(w http.ResponseWriter, r *http.Request) { + logger := g.logger.SubloggerWithRequestID(r.Context()) + logger.Info().Msg("Calling post school class") + + schoolID := chi.URLParam(r, "schoolID") + schoolID, err := url.PathUnescape(schoolID) + if err != nil { + logger.Debug(). + Err(err). + Str("id", schoolID). + Msg("could not add class to school: unescaping school id failed") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping school id failed") + return + } + + if schoolID == "" { + logger.Debug().Msg("could not add school class: missing school id") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing school 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 school class: 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 school class: @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 school class: 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", "school" and "organizational Contact" + // we restrict this to users for now. Might add Schools as members later + if memberType != "classes" { + logger.Debug().Str("type", memberType).Msg("could not add school class: Only classes are allowed as school members") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "Only classes are allowed as school members") + return + } + + logger.Debug().Str("memberType", memberType).Str("id", id).Msg("calling add class on backend") + err = g.identityEducationBackend.AddClassesToEducationSchool(r.Context(), schoolID, []string{id}) + + if err != nil { + logger.Debug().Err(err).Msg("could not add school class: 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 + e := events.SchoolMemberAdded{SchoolID: schoolID, UserID: id} + if currentUser, ok := ctxpkg.ContextGetUser(r.Context()); ok { + e.Executant = currentUser.GetId() + } + g.publishEvent(e) + */ + + render.Status(r, http.StatusNoContent) + render.NoContent(w, r) +} + +// DeleteEducationSchoolUser implements the Service interface. +func (g Graph) DeleteEducationSchoolClass(w http.ResponseWriter, r *http.Request) { + logger := g.logger.SubloggerWithRequestID(r.Context()) + logger.Info().Msg("calling delete school class") + + schoolID := chi.URLParam(r, "schoolID") + schoolID, err := url.PathUnescape(schoolID) + if err != nil { + logger.Debug().Err(err).Str("id", schoolID).Msg("could not delete school class: unescaping school id failed") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping school id failed") + return + } + + if schoolID == "" { + logger.Debug().Msg("could not delete school class: missing school id") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing school id") + return + } + + classID := chi.URLParam(r, "classID") + classID, err = url.PathUnescape(classID) + if err != nil { + logger.Debug().Err(err).Str("id", classID).Msg("could not delete school class: unescaping class id failed") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping class id failed") + return + } + + if classID == "" { + logger.Debug().Msg("could not delete school class: missing class id") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing class id") + return + } + logger.Debug().Str("schoolID", schoolID).Str("userID", classID).Msg("calling delete class on backend") + err = g.identityEducationBackend.RemoveClassFromEducationSchool(r.Context(), schoolID, classID) + + if err != nil { + logger.Debug().Err(err).Msg("could not delete school class: 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 + e := events.SchoolMemberRemoved{SchoolID: schoolID, UserID: userID} + if currentUser, ok := ctxpkg.ContextGetUser(r.Context()); ok { + e.Executant = currentUser.GetId() + } + g.publishEvent(e) + */ + + render.Status(r, http.StatusNoContent) + render.NoContent(w, r) +} + func sortEducationSchools(req *godata.GoDataRequest, schools []*libregraph.EducationSchool) ([]*libregraph.EducationSchool, error) { if req.Query.OrderBy == nil || len(req.Query.OrderBy.OrderByItems) != 1 { return schools, nil diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go index bfc287b4f..9ebb5000e 100644 --- a/services/graph/pkg/service/v0/service.go +++ b/services/graph/pkg/service/v0/service.go @@ -241,6 +241,11 @@ func NewService(opts ...Option) (Graph, error) { r.Post("/$ref", svc.PostEducationSchoolUser) r.Delete("/{userID}/$ref", svc.DeleteEducationSchoolUser) }) + r.Route("/classes", func(r chi.Router) { + r.Get("/", svc.GetEducationSchoolClasses) + r.Post("/$ref", svc.PostEducationSchoolClass) + r.Delete("/{classID}/$ref", svc.DeleteEducationSchoolClass) + }) }) }) r.Route("/users", func(r chi.Router) {