mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-09-14 23:00:31 -04:00
Introducing gowrap as a build-time tool to generate interface delegate structs from templates: * added as a 'make go-generate' target in services/graph, * added as a build-time dependency in .bingo/ Introduce an LDAP client abstraction interface to be able to wrap the go-ldap client API with metrics transparently (and possibly hooks and such in the future), in order to use delegation patterns to measure the time LDAP (client) operations take to finish, as well as to track their results (success, failure, not-found). Has two implementations that are generated using gowrap: * a go-ldap adapter implementation that directly delegates to a go-ldap connection * a time measuring and metrics collecting implementation that delegates to another LdapClient The metrics collecting one is disabled by default, can be enabled with GRAPH_LDAP_METRICS_DISABLE=false It collects durations of outbound LDAP client operations into a histogram, as well as the number of concurrent outbound LDAP operations in a gauge (via an atomic int and a gauge function, as that performs best). Add an HTTP middleware that measures how long Graph HTTP API requests take, storing taken time into a histogram along with labels for * method, * path pattern (from the chi routes), * Graph API version prefix, * Graph API resource name, * and the resulting status code. It also tracks the number of concurrent inbound Graph API HTTP requests using a gauge (also using an atomic int and a gauge function). Disabled by default, can be enabled with GRAPH_HTTP_METRICS_DISABLE=false Add Backend and EducationBackend delegate implementations that measure execution time on the level of the higher API call operations there (CreateUser, DeleteUser, ..., CreateSchool, ...), generated using gowrap. Disabled by default, can be enabled with GRAPH_IDENTITY_BACKEND_METRICS_DISABLE=false Also added a small k6 script to produce some read-only load on the Graph API, for a casual test of the metrics, as well as k6 in mise.toml. Make an internal changes to how singular LDAP entry searches work in the LDAP identity backends: * check whether searches for a singular entry returns more than one result, in which case a new error TooManyResults is returned, instead of leaving that undetected, blindly taking the first result, and potentially risking data inconsistencies Improve the loggers in identity backends by adding attributes for their request targets (Reva gateway address or LDAP URI, respectively). Also add a "backend" attribute for all Graph API logs (set to "ldap" or "cs3"), to help debug potential issues, and remove them from all the logger debug calls at the beginning of each LDAP-related function as those should really be part of the logger and set beforehand. The LDAP identity backend logger also has two new attributes to help debugging with logs: * write (bool): whether write operations are enabled * refint (bool): whether refint is enabled or not Also adds a dedicated counter metric for user password change operations. Minor campfire improvements: * add a constructor func for the CS3 backend * add a constructor func for the LDAP backend * in the LDAP identity backend, in searchLDAPEntryByFilter (used by all search/get public functions), errors that occur when performing LDAP SEARCH operations were blindly mapped to a ItemNotFound error, instead of being analyzed as it could be caused by a technical error * in the requireadmin middleware, add debug logging to explain why a request is denied * when an LDAP password change fails because the user entry was not found in LDAP, we now have a log message that tracks that
578 lines
22 KiB
Go
578 lines
22 KiB
Go
package svc
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/CiscoM31/godata"
|
|
libregraph "github.com/opencloud-eu/libre-graph-api-go"
|
|
"github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode"
|
|
revactx "github.com/opencloud-eu/reva/v2/pkg/ctx"
|
|
"github.com/opencloud-eu/reva/v2/pkg/events"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/render"
|
|
)
|
|
|
|
// GetEducationClasses implements the Service interface.
|
|
func (g Graph) GetEducationClasses(w http.ResponseWriter, r *http.Request) {
|
|
logger := g.logger.SubloggerWithRequestID(r.Context())
|
|
logger.Info().Interface("query", r.URL.Query()).Msg("calling GetEducationClasses")
|
|
sanitizedPath := strings.TrimPrefix(r.URL.Path, "/graph/v1.0/")
|
|
odataReq, err := godata.ParseRequest(r.Context(), sanitizedPath, r.URL.Query())
|
|
if err != nil {
|
|
logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg(
|
|
"could not get educationClasses: query error")
|
|
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
classes, err := g.identityEducationBackend.GetEducationClasses(r.Context())
|
|
if err != nil {
|
|
logger.Debug().Err(err).Msg("could not get classes: backend error")
|
|
errorcode.RenderError(w, r, err)
|
|
return
|
|
}
|
|
|
|
classes, err = sortClasses(odataReq, classes)
|
|
if err != nil {
|
|
logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("cannot get classes: could not sort classes according to query")
|
|
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
render.Status(r, http.StatusOK)
|
|
render.JSON(w, r, &ListResponse{Value: classes})
|
|
}
|
|
|
|
// PostEducationClass implements the Service interface.
|
|
func (g Graph) PostEducationClass(w http.ResponseWriter, r *http.Request) {
|
|
logger := g.logger.SubloggerWithRequestID(r.Context())
|
|
logger.Info().Msg("calling post EducationClass")
|
|
class := libregraph.NewEducationClassWithDefaults()
|
|
err := StrictJSONUnmarshal(r.Body, class)
|
|
if err != nil {
|
|
logger.Debug().Err(err).Interface("body", r.Body).Msg("could not create education class: invalid request body")
|
|
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, fmt.Sprintf("invalid request body: %s", err.Error()))
|
|
return
|
|
}
|
|
|
|
if _, ok := class.GetDisplayNameOk(); !ok {
|
|
logger.Debug().Err(err).Interface("class", class).Msg("could not create class: missing required attribute")
|
|
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "Missing Required Attribute")
|
|
return
|
|
}
|
|
|
|
// Disallow user-supplied IDs. It's supposed to be readonly. We're either
|
|
// generating them in the backend ourselves or rely on the Backend's
|
|
// storage (e.g. LDAP) to provide a unique ID.
|
|
if _, ok := class.GetIdOk(); ok {
|
|
logger.Debug().Msg("could not create class: id is a read-only attribute")
|
|
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "class id is a read-only attribute")
|
|
return
|
|
}
|
|
|
|
if class, err = g.identityEducationBackend.CreateEducationClass(r.Context(), *class); err != nil {
|
|
logger.Debug().Interface("class", class).Msg("could not create class: backend error")
|
|
errorcode.RenderError(w, r, err)
|
|
return
|
|
}
|
|
|
|
/* TODO requires reva changes
|
|
if class != nil && class.Id != nil {
|
|
currentUser := revactx.ContextMustGetUser(r.Context())
|
|
g.publishEvent(events.EducationClassCreated{Executant: currentUser.Id, EducationClassID: *class.Id})
|
|
}
|
|
*/
|
|
render.Status(r, http.StatusCreated)
|
|
render.JSON(w, r, class)
|
|
}
|
|
|
|
// PatchEducationClass implements the Service interface.
|
|
func (g Graph) PatchEducationClass(w http.ResponseWriter, r *http.Request) {
|
|
logger := g.logger.SubloggerWithRequestID(r.Context())
|
|
classID := chi.URLParam(r, "classID")
|
|
logger.Info().Str("classID", classID).Msg("calling patch education class")
|
|
classID, err := url.PathUnescape(classID)
|
|
if err != nil {
|
|
logger.Debug().Str("id", classID).Msg("could not change class: unescaping class id failed")
|
|
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping class id failed")
|
|
return
|
|
}
|
|
|
|
if classID == "" {
|
|
logger.Debug().Msg("could not change class: missing class id")
|
|
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing class id")
|
|
return
|
|
}
|
|
changes := libregraph.NewEducationClassWithDefaults()
|
|
err = StrictJSONUnmarshal(r.Body, changes)
|
|
if err != nil {
|
|
logger.Debug().Err(err).Interface("body", r.Body).Msg("could not change class: invalid request body")
|
|
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, fmt.Sprintf("invalid request body: %s", err.Error()))
|
|
return
|
|
}
|
|
|
|
var features []events.GroupFeature
|
|
if displayName, ok := changes.GetDisplayNameOk(); ok {
|
|
features = append(features, events.GroupFeature{Name: "displayname", Value: *displayName})
|
|
}
|
|
|
|
if externalID, ok := changes.GetExternalIdOk(); ok {
|
|
features = append(features, events.GroupFeature{Name: "externalid", Value: *externalID})
|
|
}
|
|
|
|
_, err = g.identityEducationBackend.UpdateEducationClass(r.Context(), classID, *changes)
|
|
if err != nil {
|
|
logger.Error().
|
|
Err(err).
|
|
Str("classID", classID).
|
|
Msg("could not update class")
|
|
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
if memberRefs, ok := changes.GetMembersodataBindOk(); ok {
|
|
// The spec defines a limit of 20 members maxium per Request
|
|
if len(memberRefs) > g.config.API.GroupMembersPatchLimit {
|
|
logger.Debug().
|
|
Int("number", len(memberRefs)).
|
|
Int("limit", g.config.API.GroupMembersPatchLimit).
|
|
Msg("could not add group members, exceeded members limit")
|
|
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest,
|
|
fmt.Sprintf("Request is limited to %d members", g.config.API.GroupMembersPatchLimit))
|
|
return
|
|
}
|
|
memberIDs := make([]string, 0, len(memberRefs))
|
|
for _, memberRef := range memberRefs {
|
|
memberType, id, err := g.parseMemberRef(memberRef)
|
|
if err != nil {
|
|
logger.Debug().
|
|
Str("memberref", memberRef).
|
|
Msg("could not change class: Error parsing member@odata.bind values")
|
|
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "Error parsing member@odata.bind values")
|
|
return
|
|
}
|
|
logger.Debug().Str("membertype", memberType).Str("memberid", id).Msg("add class member")
|
|
// The MS Graph spec allows "directoryObject", "user", "class" and "organizational Contact"
|
|
// we restrict this to users for now. Might add Classes as members later
|
|
if memberType != memberTypeUsers {
|
|
logger.Debug().
|
|
Str("type", memberType).
|
|
Msg("could not change class: could not add member, only user type is allowed")
|
|
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "Only user are allowed as class members")
|
|
return
|
|
}
|
|
memberIDs = append(memberIDs, id)
|
|
}
|
|
err = g.identityBackend.AddMembersToGroup(r.Context(), classID, memberIDs)
|
|
}
|
|
|
|
if err != nil {
|
|
logger.Debug().Err(err).Msg("could not change class: backend could not add members")
|
|
errorcode.RenderError(w, r, err)
|
|
return
|
|
}
|
|
|
|
if len(features) > 0 {
|
|
e := events.GroupFeatureChanged{
|
|
GroupID: classID,
|
|
Features: features,
|
|
}
|
|
|
|
if currentUser, ok := revactx.ContextGetUser(r.Context()); ok {
|
|
e.Executant = currentUser.GetId()
|
|
}
|
|
g.publishEvent(r.Context(), e)
|
|
|
|
}
|
|
|
|
render.Status(r, http.StatusNoContent) // TODO StatusNoContent when prefer=minimal is used, otherwise OK and the resource in the body
|
|
render.NoContent(w, r)
|
|
}
|
|
|
|
// GetEducationClass implements the Service interface.
|
|
func (g Graph) GetEducationClass(w http.ResponseWriter, r *http.Request) {
|
|
logger := g.logger.SubloggerWithRequestID(r.Context())
|
|
classID := chi.URLParam(r, "classID")
|
|
logger.Info().Str("classID", classID).Msg("calling get education class")
|
|
classID, err := url.PathUnescape(classID)
|
|
if err != nil {
|
|
logger.Debug().Str("id", classID).Msg("could not get class: unescaping class id failed")
|
|
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping class id failed")
|
|
}
|
|
|
|
if classID == "" {
|
|
logger.Debug().Msg("could not get class: missing class id")
|
|
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing class id")
|
|
return
|
|
}
|
|
|
|
logger.Debug().
|
|
Str("id", classID).
|
|
Interface("query", r.URL.Query()).
|
|
Msg("calling get class on backend")
|
|
class, err := g.identityEducationBackend.GetEducationClass(r.Context(), classID)
|
|
if err != nil {
|
|
logger.Debug().Err(err).Msg("could not get class: backend error")
|
|
errorcode.RenderError(w, r, err)
|
|
return
|
|
}
|
|
|
|
render.Status(r, http.StatusOK)
|
|
render.JSON(w, r, class)
|
|
}
|
|
|
|
// DeleteEducationClass implements the Service interface.
|
|
func (g Graph) DeleteEducationClass(w http.ResponseWriter, r *http.Request) {
|
|
logger := g.logger.SubloggerWithRequestID(r.Context())
|
|
classID := chi.URLParam(r, "classID")
|
|
logger.Info().Str("classID", classID).Msg("calling delete class")
|
|
classID, err := url.PathUnescape(classID)
|
|
if err != nil {
|
|
logger.Debug().Err(err).Str("id", classID).Msg("could not delete 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 class: missing class id")
|
|
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing class id")
|
|
return
|
|
}
|
|
|
|
logger.Debug().Str("id", classID).Msg("calling delete class on backend")
|
|
err = g.identityEducationBackend.DeleteEducationClass(r.Context(), classID)
|
|
|
|
if err != nil {
|
|
logger.Debug().Err(err).Msg("could not delete class: backend error")
|
|
errorcode.RenderError(w, r, err)
|
|
return
|
|
}
|
|
|
|
/* TODO requires reva changes
|
|
currentUser := revactx.ContextMustGetUser(r.Context())
|
|
g.publishEvent(events.ClassDeleted{Executant: currentUser.Id, ClassID: classID})
|
|
*/
|
|
|
|
render.Status(r, http.StatusNoContent)
|
|
render.NoContent(w, r)
|
|
}
|
|
|
|
// GetEducationClassMembers implements the Service interface.
|
|
func (g Graph) GetEducationClassMembers(w http.ResponseWriter, r *http.Request) {
|
|
logger := g.logger.SubloggerWithRequestID(r.Context())
|
|
classID := chi.URLParam(r, "classID")
|
|
logger.Info().Str("classID", classID).Msg("calling get class members")
|
|
classID, err := url.PathUnescape(classID)
|
|
if err != nil {
|
|
logger.Debug().Str("id", classID).Msg("could not get class members: 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 members: missing class id")
|
|
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing class id")
|
|
return
|
|
}
|
|
|
|
logger.Debug().Str("id", classID).Msg("calling get class members on backend")
|
|
members, err := g.identityEducationBackend.GetEducationClassMembers(r.Context(), classID)
|
|
if err != nil {
|
|
logger.Debug().Err(err).Msg("could not get class members: backend error")
|
|
errorcode.RenderError(w, r, err)
|
|
return
|
|
}
|
|
|
|
render.Status(r, http.StatusOK)
|
|
render.JSON(w, r, members)
|
|
}
|
|
|
|
// PostEducationClassMember implements the Service interface.
|
|
func (g Graph) PostEducationClassMember(w http.ResponseWriter, r *http.Request) {
|
|
logger := g.logger.SubloggerWithRequestID(r.Context())
|
|
|
|
classID := chi.URLParam(r, "classID")
|
|
logger.Info().Str("classID", classID).Msg("Calling post class member")
|
|
classID, err := url.PathUnescape(classID)
|
|
if err != nil {
|
|
logger.Debug().
|
|
Err(err).
|
|
Str("id", classID).
|
|
Msg("could not add member 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 member: missing class id")
|
|
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing class id")
|
|
return
|
|
}
|
|
memberRef := libregraph.NewMemberReference()
|
|
err = StrictJSONUnmarshal(r.Body, memberRef)
|
|
if err != nil {
|
|
logger.Debug().
|
|
Err(err).
|
|
Interface("body", r.Body).
|
|
Msg("could not add class member: 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 member: @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 member: 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 members")
|
|
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "Only users are allowed as class members")
|
|
return
|
|
}
|
|
|
|
logger.Debug().Str("memberType", memberType).Str("id", id).Msg("calling add member on backend")
|
|
err = g.identityBackend.AddMembersToGroup(r.Context(), classID, []string{id})
|
|
|
|
if err != nil {
|
|
logger.Debug().Err(err).Msg("could not add class member: backend error")
|
|
errorcode.RenderError(w, r, err)
|
|
return
|
|
}
|
|
|
|
/* TODO requires reva changes
|
|
currentUser := revactx.ContextMustGetUser(r.Context())
|
|
g.publishEvent(events.EducationClassMemberAdded{Executant: currentUser.Id, EducationClassID: classID, UserID: id})
|
|
*/
|
|
render.Status(r, http.StatusNoContent)
|
|
render.NoContent(w, r)
|
|
}
|
|
|
|
// DeleteEducationClassMember implements the Service interface.
|
|
func (g Graph) DeleteEducationClassMember(w http.ResponseWriter, r *http.Request) {
|
|
logger := g.logger.SubloggerWithRequestID(r.Context())
|
|
|
|
classID := chi.URLParam(r, "classID")
|
|
memberID := chi.URLParam(r, "memberID")
|
|
logger.Info().Str("classID", classID).Str("memberID", memberID).Msg("calling delete class member")
|
|
classID, err := url.PathUnescape(classID)
|
|
if err != nil {
|
|
logger.Debug().Err(err).Str("id", classID).Msg("could not delete class member: 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 member: missing class id")
|
|
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing class id")
|
|
return
|
|
}
|
|
|
|
memberID, err = url.PathUnescape(memberID)
|
|
if err != nil {
|
|
logger.Debug().Err(err).Str("id", memberID).Msg("could not delete class member: unescaping member id failed")
|
|
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping member id failed")
|
|
return
|
|
}
|
|
|
|
if memberID == "" {
|
|
logger.Debug().Msg("could not delete class member: missing member id")
|
|
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing member id")
|
|
return
|
|
}
|
|
logger.Debug().Str("classID", classID).Str("memberID", memberID).Msg("calling delete member on backend")
|
|
err = g.identityBackend.RemoveMemberFromGroup(r.Context(), classID, memberID)
|
|
if err != nil {
|
|
logger.Debug().Err(err).Msg("could not delete class member: backend error")
|
|
errorcode.RenderError(w, r, err)
|
|
return
|
|
}
|
|
/* TODO requires reva changes
|
|
currentUser := revactx.ContextMustGetUser(r.Context())
|
|
g.publishEvent(events.EducationClassMemberRemoved{Executant: currentUser.Id, EducationClassID: classID, UserID: memberID})
|
|
*/
|
|
render.Status(r, http.StatusNoContent)
|
|
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())
|
|
classID := chi.URLParam(r, "classID")
|
|
logger.Info().Str("classID", classID).Msg("calling get class teachers")
|
|
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")
|
|
errorcode.RenderError(w, r, err)
|
|
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())
|
|
|
|
classID := chi.URLParam(r, "classID")
|
|
logger.Info().Str("classID", classID).Msg("Calling post class teacher")
|
|
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 = StrictJSONUnmarshal(r.Body, 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")
|
|
errorcode.RenderError(w, r, err)
|
|
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())
|
|
|
|
classID := chi.URLParam(r, "classID")
|
|
teacherID := chi.URLParam(r, "teacherID")
|
|
logger.Info().Str("classID", classID).Str("teacherID", teacherID).Msg("calling delete class teacher")
|
|
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, 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")
|
|
errorcode.RenderError(w, r, err)
|
|
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
|
|
}
|
|
var less func(i, j int) bool
|
|
|
|
switch req.Query.OrderBy.OrderByItems[0].Field.Value {
|
|
case displayNameAttr:
|
|
less = func(i, j int) bool {
|
|
return strings.ToLower(classes[i].GetDisplayName()) < strings.ToLower(classes[j].GetDisplayName())
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("we do not support <%s> as a order parameter", req.Query.OrderBy.OrderByItems[0].Field.Value)
|
|
}
|
|
|
|
if req.Query.OrderBy.OrderByItems[0].Order == _sortDescending {
|
|
sort.Slice(classes, reverse(less))
|
|
} else {
|
|
sort.Slice(classes, less)
|
|
}
|
|
|
|
return classes, nil
|
|
}
|