initial schools API

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
This commit is contained in:
Jörn Friedrich Dreyer
2022-12-07 15:49:57 +00:00
committed by Ralf Haferkamp
parent 26e494ec8b
commit 9844f5f8ce
14 changed files with 1365 additions and 46 deletions

View File

@@ -31,6 +31,7 @@ ci-go-generate: $(MOCKERY) # CI runs ci-node-generate automatically before this
$(MOCKERY) --dir pkg/service/v0 --case underscore --name Permissions
$(MOCKERY) --dir pkg/service/v0 --case underscore --name RoleService
$(MOCKERY) --dir pkg/identity --output pkg/identity/mocks --case underscore --name Backend
$(MOCKERY) --dir pkg/identity --output pkg/identity/mocks --case underscore --name EducationBackend
$(MOCKERY) --srcpkg github.com/go-ldap/ldap/v3 --case underscore --filename ldapclient.go --name Client

View File

@@ -8,6 +8,7 @@ import (
libregraph "github.com/owncloud/libre-graph-api-go"
)
// Backend defines the Interface for an IdentityBackend implementation
type Backend interface {
// CreateUser creates a given user in the identity backend.
CreateUser(ctx context.Context, user libregraph.User) (*libregraph.User, error)
@@ -31,6 +32,23 @@ type Backend interface {
RemoveMemberFromGroup(ctx context.Context, groupID string, memberID string) error
}
// EducationBackend defines the Interface for an EducationBackend implementation
type EducationBackend interface {
// CreateSchool creates the supplied school in the identity backend.
CreateSchool(ctx context.Context, group libregraph.EducationSchool) (*libregraph.EducationSchool, error)
// DeleteSchool deletes a given school, identified by id
DeleteSchool(ctx context.Context, id string) error
// GetSchool reads a given school by id
GetSchool(ctx context.Context, nameOrID string, queryParam url.Values) (*libregraph.EducationSchool, error)
// GetSchools lists all schools
GetSchools(ctx context.Context, queryParam url.Values) ([]*libregraph.EducationSchool, error)
GetSchoolMembers(ctx context.Context, id string) ([]*libregraph.User, error)
// AddMembersToSchool adds new members (reference by a slice of IDs) to supplied school in the identity backend.
AddMembersToSchool(ctx context.Context, schoolID string, memberID []string) error
// RemoveMemberFromSchool removes a single member (by ID) from a school
RemoveMemberFromSchool(ctx context.Context, schoolID string, memberID string) error
}
func CreateUserModelFromCS3(u *cs3.User) *libregraph.User {
if u.Id == nil {
u.Id = &cs3.UserId{}

View File

@@ -0,0 +1,65 @@
package identity
import (
"context"
"net/url"
libregraph "github.com/owncloud/libre-graph-api-go"
)
type schoolConfig struct {
schoolBaseDN string
schoolFilter string
schoolObjectClass string
schoolScope int
schoolAttributeMap schoolAttributeMap
}
type schoolAttributeMap struct {
displayname string
schoolNumber string
id string
}
func newSchoolAttributeMap() schoolAttributeMap {
return schoolAttributeMap{
displayname: "ou",
schoolNumber: "ocEducationSchoolNumber",
id: "owncloudUUID",
}
}
// CreateSchool creates the supplied school in the identity backend.
func (i *LDAP) CreateSchool(ctx context.Context, school libregraph.EducationSchool) (*libregraph.EducationSchool, error) {
return nil, errNotImplemented
}
// DeleteSchool deletes a given school, identified by id
func (i *LDAP) DeleteSchool(ctx context.Context, id string) error {
return errNotImplemented
}
// GetSchool implements the EducationBackend interface for the LDAP backend.
func (i *LDAP) GetSchool(ctx context.Context, nameOrID string, queryParam url.Values) (*libregraph.EducationSchool, error) {
return nil, errNotImplemented
}
// GetSchools implements the EducationBackend interface for the LDAP backend.
func (i *LDAP) GetSchools(ctx context.Context, queryParam url.Values) ([]*libregraph.EducationSchool, error) {
return nil, errNotImplemented
}
// GetSchoolMembers implements the EducationBackend interface for the LDAP backend.
func (i *LDAP) GetSchoolMembers(ctx context.Context, id string) ([]*libregraph.User, error) {
return nil, errNotImplemented
}
// AddMembersToSchool adds new members (reference by a slice of IDs) to supplied school in the identity backend.
func (i *LDAP) AddMembersToSchool(ctx context.Context, schoolID string, memberID []string) error {
return errNotImplemented
}
// RemoveMemberFromSchool removes a single member (by ID) from a school
func (i *LDAP) RemoveMemberFromSchool(ctx context.Context, schoolID string, memberID string) error {
return errNotImplemented
}

View File

@@ -0,0 +1,167 @@
// Code generated by mockery v2.14.1. DO NOT EDIT.
package mocks
import (
context "context"
libregraph "github.com/owncloud/libre-graph-api-go"
mock "github.com/stretchr/testify/mock"
url "net/url"
)
// EducationBackend is an autogenerated mock type for the EducationBackend type
type EducationBackend struct {
mock.Mock
}
// AddMembersToSchool provides a mock function with given fields: ctx, schoolID, memberID
func (_m *EducationBackend) AddMembersToSchool(ctx context.Context, schoolID string, memberID []string) error {
ret := _m.Called(ctx, schoolID, memberID)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, []string) error); ok {
r0 = rf(ctx, schoolID, memberID)
} else {
r0 = ret.Error(0)
}
return r0
}
// CreateSchool provides a mock function with given fields: ctx, group
func (_m *EducationBackend) CreateSchool(ctx context.Context, group libregraph.EducationSchool) (*libregraph.EducationSchool, error) {
ret := _m.Called(ctx, group)
var r0 *libregraph.EducationSchool
if rf, ok := ret.Get(0).(func(context.Context, libregraph.EducationSchool) *libregraph.EducationSchool); ok {
r0 = rf(ctx, group)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*libregraph.EducationSchool)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, libregraph.EducationSchool) error); ok {
r1 = rf(ctx, group)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// DeleteSchool provides a mock function with given fields: ctx, id
func (_m *EducationBackend) DeleteSchool(ctx context.Context, id string) error {
ret := _m.Called(ctx, id)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
r0 = rf(ctx, id)
} else {
r0 = ret.Error(0)
}
return r0
}
// GetSchool provides a mock function with given fields: ctx, nameOrID, queryParam
func (_m *EducationBackend) GetSchool(ctx context.Context, nameOrID string, queryParam url.Values) (*libregraph.EducationSchool, error) {
ret := _m.Called(ctx, nameOrID, queryParam)
var r0 *libregraph.EducationSchool
if rf, ok := ret.Get(0).(func(context.Context, string, url.Values) *libregraph.EducationSchool); ok {
r0 = rf(ctx, nameOrID, queryParam)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*libregraph.EducationSchool)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string, url.Values) error); ok {
r1 = rf(ctx, nameOrID, queryParam)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetSchoolMembers provides a mock function with given fields: ctx, id
func (_m *EducationBackend) GetSchoolMembers(ctx context.Context, id string) ([]*libregraph.User, error) {
ret := _m.Called(ctx, id)
var r0 []*libregraph.User
if rf, ok := ret.Get(0).(func(context.Context, string) []*libregraph.User); ok {
r0 = rf(ctx, id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*libregraph.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = rf(ctx, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetSchools provides a mock function with given fields: ctx, queryParam
func (_m *EducationBackend) GetSchools(ctx context.Context, queryParam url.Values) ([]*libregraph.EducationSchool, error) {
ret := _m.Called(ctx, queryParam)
var r0 []*libregraph.EducationSchool
if rf, ok := ret.Get(0).(func(context.Context, url.Values) []*libregraph.EducationSchool); ok {
r0 = rf(ctx, queryParam)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*libregraph.EducationSchool)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, url.Values) error); ok {
r1 = rf(ctx, queryParam)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// RemoveMemberFromSchool provides a mock function with given fields: ctx, schoolID, memberID
func (_m *EducationBackend) RemoveMemberFromSchool(ctx context.Context, schoolID string, memberID string) error {
ret := _m.Called(ctx, schoolID, memberID)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok {
r0 = rf(ctx, schoolID, memberID)
} else {
r0 = ret.Error(0)
}
return r0
}
type mockConstructorTestingTNewEducationBackend interface {
mock.TestingT
Cleanup(func())
}
// NewEducationBackend creates a new instance of EducationBackend. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
func NewEducationBackend(t mockConstructorTestingTNewEducationBackend) *EducationBackend {
mock := &EducationBackend{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@@ -2,9 +2,11 @@ package svc
import (
"context"
"errors"
"net/http"
"net/url"
"path"
"strings"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
@@ -90,16 +92,17 @@ type RoleService interface {
// Graph defines implements the business logic for Service.
type Graph struct {
config *config.Config
mux *chi.Mux
logger *log.Logger
identityBackend identity.Backend
gatewayClient GatewayClient
roleService RoleService
permissionsService Permissions
spacePropertiesCache *ttlcache.Cache
eventsPublisher events.Publisher
searchService searchsvc.SearchProviderService
config *config.Config
mux *chi.Mux
logger *log.Logger
identityBackend identity.Backend
identityEducationBackend identity.EducationBackend
gatewayClient GatewayClient
roleService RoleService
permissionsService Permissions
spacePropertiesCache *ttlcache.Cache
eventsPublisher events.Publisher
searchService searchsvc.SearchProviderService
}
// ServeHTTP implements the Service interface.
@@ -107,7 +110,7 @@ func (g Graph) ServeHTTP(w http.ResponseWriter, r *http.Request) {
g.mux.ServeHTTP(w, r)
}
// GetClient returns a gateway client to talk to reva
// GetGatewayClient returns a gateway client to talk to reva
func (g Graph) GetGatewayClient() GatewayClient {
return g.gatewayClient
}
@@ -131,6 +134,7 @@ func (g Graph) getWebDavBaseURL() (*url.URL, error) {
return webDavBaseURL, nil
}
// ListResponse is used for proper marshalling of Graph list responses
type ListResponse struct {
Value interface{} `json:"value,omitempty"`
}
@@ -139,3 +143,18 @@ const (
ReadmeSpecialFolderName = "readme"
SpaceImageSpecialFolderName = "image"
)
// TODO might be different for /education/users vs /users
func (g Graph) parseMemberRef(ref string) (string, string, error) {
memberURL, err := url.ParseRequestURI(ref)
if err != nil {
return "", "", err
}
segments := strings.Split(memberURL.Path, "/")
if len(segments) < 2 {
return "", "", errors.New("invalid member reference")
}
id := segments[len(segments)-1]
memberType := segments[len(segments)-2]
return memberType, id, nil
}

View File

@@ -20,6 +20,7 @@ import (
)
const memberRefsLimit = 20
const memberTypeUsers = "users"
// GetGroups implements the Service interface.
func (g Graph) GetGroups(w http.ResponseWriter, r *http.Request) {
@@ -145,7 +146,7 @@ func (g Graph) PatchGroup(w http.ResponseWriter, r *http.Request) {
logger.Debug().Str("membertype", memberType).Str("memberid", id).Msg("add group member")
// The MS Graph spec allows "directoryObject", "user", "group" and "organizational Contact"
// we restrict this to users for now. Might add Groups as members later
if memberType != "users" {
if memberType != memberTypeUsers {
logger.Debug().
Str("type", memberType).
Msg("could not change group: could not add member, only user type is allowed")
@@ -324,7 +325,7 @@ func (g Graph) PostGroupMember(w http.ResponseWriter, r *http.Request) {
}
// The MS Graph spec allows "directoryObject", "user", "group" and "organizational Contact"
// we restrict this to users for now. Might add Groups as members later
if memberType != "users" {
if memberType != memberTypeUsers {
logger.Debug().Str("type", memberType).Msg("could not add group member: Only users are allowed as group members")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "Only users are allowed as group members")
return
@@ -401,20 +402,6 @@ func (g Graph) DeleteGroupMember(w http.ResponseWriter, r *http.Request) {
render.NoContent(w, r)
}
func (g Graph) parseMemberRef(ref string) (string, string, error) {
memberURL, err := url.ParseRequestURI(ref)
if err != nil {
return "", "", err
}
segments := strings.Split(memberURL.Path, "/")
if len(segments) < 2 {
return "", "", errors.New("invalid member reference")
}
id := segments[len(segments)-1]
memberType := segments[len(segments)-2]
return memberType, id, nil
}
func sortGroups(req *godata.GoDataRequest, groups []*libregraph.Group) ([]*libregraph.Group, error) {
var sorter sort.Interface
if req.Query.OrderBy == nil || len(req.Query.OrderBy.OrderByItems) != 1 {

View File

@@ -99,6 +99,46 @@ func (i instrument) DeleteGroupMember(w http.ResponseWriter, r *http.Request) {
i.next.DeleteGroupMember(w, r)
}
// GetSchools implements the Service interface.
func (i instrument) GetSchools(w http.ResponseWriter, r *http.Request) {
i.next.GetSchools(w, r)
}
// GetSchool implements the Service interface.
func (i instrument) GetSchool(w http.ResponseWriter, r *http.Request) {
i.next.GetSchool(w, r)
}
// PostSchool implements the Service interface.
func (i instrument) PostSchool(w http.ResponseWriter, r *http.Request) {
i.next.PostSchool(w, r)
}
// PatchSchool implements the Service interface.
func (i instrument) PatchSchool(w http.ResponseWriter, r *http.Request) {
i.next.PatchSchool(w, r)
}
// DeleteSchool implements the Service interface.
func (i instrument) DeleteSchool(w http.ResponseWriter, r *http.Request) {
i.next.DeleteSchool(w, r)
}
// GetSchoolMembers implements the Service interface.
func (i instrument) GetSchoolMembers(w http.ResponseWriter, r *http.Request) {
i.next.GetSchoolMembers(w, r)
}
// PostSchoolMember implements the Service interface.
func (i instrument) PostSchoolMember(w http.ResponseWriter, r *http.Request) {
i.next.PostSchoolMember(w, r)
}
// DeleteSchoolMember implements the Service interface.
func (i instrument) DeleteSchoolMember(w http.ResponseWriter, r *http.Request) {
i.next.DeleteSchoolMember(w, r)
}
// GetDrives implements the Service interface.
func (i instrument) GetDrives(w http.ResponseWriter, r *http.Request) {
i.next.GetDrives(w, r)

View File

@@ -99,6 +99,46 @@ func (l logging) DeleteGroupMember(w http.ResponseWriter, r *http.Request) {
l.next.DeleteGroupMember(w, r)
}
// GetSchools implements the Service interface.
func (l logging) GetSchools(w http.ResponseWriter, r *http.Request) {
l.next.GetSchools(w, r)
}
// GetSchool implements the Service interface.
func (l logging) GetSchool(w http.ResponseWriter, r *http.Request) {
l.next.GetSchool(w, r)
}
// PostSchool implements the Service interface.
func (l logging) PostSchool(w http.ResponseWriter, r *http.Request) {
l.next.PostSchool(w, r)
}
// PatchSchool implements the Service interface.
func (l logging) PatchSchool(w http.ResponseWriter, r *http.Request) {
l.next.PatchSchool(w, r)
}
// DeleteSchool implements the Service interface.
func (l logging) DeleteSchool(w http.ResponseWriter, r *http.Request) {
l.next.DeleteSchool(w, r)
}
// GetSchoolMembers implements the Service interface.
func (l logging) GetSchoolMembers(w http.ResponseWriter, r *http.Request) {
l.next.GetSchoolMembers(w, r)
}
// PostSchoolMember implements the Service interface.
func (l logging) PostSchoolMember(w http.ResponseWriter, r *http.Request) {
l.next.PostSchoolMember(w, r)
}
// DeleteSchoolMember implements the Service interface.
func (l logging) DeleteSchoolMember(w http.ResponseWriter, r *http.Request) {
l.next.DeleteSchoolMember(w, r)
}
// GetDrives implements the Service interface.
func (l logging) GetDrives(w http.ResponseWriter, r *http.Request) {
l.next.GetDrives(w, r)

View File

@@ -17,17 +17,18 @@ type Option func(o *Options)
// Options defines the available options for this package.
type Options struct {
Logger log.Logger
Config *config.Config
Middleware []func(http.Handler) http.Handler
RequireAdminMiddleware func(http.Handler) http.Handler
GatewayClient GatewayClient
IdentityBackend identity.Backend
RoleService RoleService
PermissionService Permissions
RoleManager *roles.Manager
EventsPublisher events.Publisher
SearchService searchsvc.SearchProviderService
Logger log.Logger
Config *config.Config
Middleware []func(http.Handler) http.Handler
RequireAdminMiddleware func(http.Handler) http.Handler
GatewayClient GatewayClient
IdentityBackend identity.Backend
IdentityEducationBackend identity.EducationBackend
RoleService RoleService
PermissionService Permissions
RoleManager *roles.Manager
EventsPublisher events.Publisher
SearchService searchsvc.SearchProviderService
}
// newOptions initializes the available default options.
@@ -83,6 +84,13 @@ func WithIdentityBackend(val identity.Backend) Option {
}
}
// WithIdentityEducationBackend provides a function to set the IdentityEducationBackend option.
func WithIdentityEducationBackend(val identity.EducationBackend) Option {
return func(o *Options) {
o.IdentityEducationBackend = val
}
}
// WithRoleService provides a function to set the RoleService option.
func WithRoleService(val RoleService) Option {
return func(o *Options) {

View File

@@ -101,3 +101,21 @@ type groupsByDisplayName struct {
func (g groupsByDisplayName) Less(i, j int) bool {
return strings.ToLower(g.groupSlice[i].GetDisplayName()) < strings.ToLower(g.groupSlice[j].GetDisplayName())
}
type schoolSlice []*libregraph.EducationSchool
// Len is the number of elements in the collection.
func (d schoolSlice) Len() int { return len(d) }
// Swap swaps the elements with indexes i and j.
func (d schoolSlice) Swap(i, j int) { d[i], d[j] = d[j], d[i] }
type schoolsByDisplayName struct {
schoolSlice
}
// Less reports whether the element with index i
// must sort before the element with index j.
func (g schoolsByDisplayName) Less(i, j int) bool {
return strings.ToLower(g.schoolSlice[i].GetDisplayName()) < strings.ToLower(g.schoolSlice[j].GetDisplayName())
}

View File

@@ -0,0 +1,397 @@
package svc
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"sort"
"strings"
"github.com/CiscoM31/godata"
libregraph "github.com/owncloud/libre-graph-api-go"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/events"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
)
// GetSchools implements the Service interface.
func (g Graph) GetSchools(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Interface("query", r.URL.Query()).Msg("calling get schools")
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 schools: query error")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
return
}
schools, err := g.identityEducationBackend.GetSchools(r.Context(), r.URL.Query())
if err != nil {
logger.Debug().Err(err).Msg("could not get schools: 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
}
schools, err = sortSchools(odataReq, schools)
if err != nil {
logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("cannot get schools: could not sort schools according to query")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
return
}
render.Status(r, http.StatusOK)
render.JSON(w, r, &ListResponse{Value: schools})
}
// PostSchool implements the Service interface.
func (g Graph) PostSchool(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Msg("calling post school")
school := libregraph.NewEducationSchool()
err := json.NewDecoder(r.Body).Decode(school)
if err != nil {
logger.Debug().Err(err).Interface("body", r.Body).Msg("could not create school: invalid request body")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, fmt.Sprintf("invalid request body: %s", err.Error()))
return
}
if _, ok := school.GetDisplayNameOk(); !ok {
logger.Debug().Err(err).Interface("school", school).Msg("could not create school: 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 := school.GetIdOk(); ok {
logger.Debug().Msg("could not create school: id is a read-only attribute")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "school id is a read-only attribute")
return
}
if school, err = g.identityEducationBackend.CreateSchool(r.Context(), *school); err != nil {
logger.Debug().Interface("school", school).Msg("could not create school: backend error")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
return
}
if school != nil && school.Id != nil {
e := events.SchoolCreated{SchoolID: *school.Id}
if currentUser, ok := ctxpkg.ContextGetUser(r.Context()); ok {
e.Executant = currentUser.GetId()
}
g.publishEvent(e)
}
render.Status(r, http.StatusCreated)
render.JSON(w, r, school)
}
// PatchSchool implements the Service interface.
func (g Graph) PatchSchool(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Msg("calling patch school")
schoolID := chi.URLParam(r, "schoolID")
schoolID, err := url.PathUnescape(schoolID)
if err != nil {
logger.Debug().Str("id", schoolID).Msg("could not change school: unescaping school id failed")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping school id failed")
return
}
if schoolID == "" {
logger.Debug().Msg("could not change school: missing school id")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing school id")
return
}
changes := libregraph.NewEducationSchool()
err = json.NewDecoder(r.Body).Decode(changes)
if err != nil {
logger.Debug().Err(err).Interface("body", r.Body).Msg("could not change school: invalid request body")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, fmt.Sprintf("invalid request body: %s", err.Error()))
return
}
e := events.SchoolFeatureChanged{SchoolID: schoolID}
if currentUser, ok := ctxpkg.ContextGetUser(r.Context()); ok {
e.Executant = currentUser.GetId()
}
g.publishEvent(e)
render.Status(r, http.StatusNoContent)
render.NoContent(w, r)
}
// GetSchool implements the Service interface.
func (g Graph) GetSchool(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Msg("calling get school")
schoolID := chi.URLParam(r, "schoolID")
schoolID, err := url.PathUnescape(schoolID)
if err != nil {
logger.Debug().Str("id", schoolID).Msg("could not get school: unescaping school id failed")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping school id failed")
}
if schoolID == "" {
logger.Debug().Msg("could not get school: missing school id")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing school id")
return
}
logger.Debug().
Str("id", schoolID).
Interface("query", r.URL.Query()).
Msg("calling get school on backend")
school, err := g.identityEducationBackend.GetSchool(r.Context(), schoolID, r.URL.Query())
if err != nil {
logger.Debug().Err(err).Msg("could not get school: 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())
}
}
render.Status(r, http.StatusOK)
render.JSON(w, r, school)
}
// DeleteSchool implements the Service interface.
func (g Graph) DeleteSchool(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Msg("calling delete school")
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: 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: missing school id")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing school id")
return
}
logger.Debug().Str("id", schoolID).Msg("calling delete school on backend")
err = g.identityEducationBackend.DeleteSchool(r.Context(), schoolID)
if err != nil {
logger.Debug().Err(err).Msg("could not delete school: 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
}
e := events.SchoolDeleted{SchoolID: schoolID}
if currentUser, ok := ctxpkg.ContextGetUser(r.Context()); ok {
e.Executant = currentUser.GetId()
}
g.publishEvent(e)
render.Status(r, http.StatusNoContent)
render.NoContent(w, r)
}
// GetSchoolMembers implements the Service interface.
func (g Graph) GetSchoolMembers(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Msg("calling get school members")
schoolID := chi.URLParam(r, "schoolID")
schoolID, err := url.PathUnescape(schoolID)
if err != nil {
logger.Debug().Str("id", schoolID).Msg("could not get school members: 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 members: missing school id")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing school id")
return
}
logger.Debug().Str("id", schoolID).Msg("calling get school members on backend")
members, err := g.identityEducationBackend.GetSchoolMembers(r.Context(), schoolID)
if err != nil {
logger.Debug().Err(err).Msg("could not get school members: 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, members)
}
// PostSchoolMember implements the Service interface.
func (g Graph) PostSchoolMember(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Msg("Calling post school member")
schoolID := chi.URLParam(r, "schoolID")
schoolID, err := url.PathUnescape(schoolID)
if err != nil {
logger.Debug().
Err(err).
Str("id", schoolID).
Msg("could not add member 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 member: 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 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 school 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 school 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", "school" and "organizational Contact"
// we restrict this to users for now. Might add Schools as members later
if memberType != "users" {
logger.Debug().Str("type", memberType).Msg("could not add school member: Only users are allowed as school members")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "Only users are allowed as school members")
return
}
logger.Debug().Str("memberType", memberType).Str("id", id).Msg("calling add member on backend")
err = g.identityEducationBackend.AddMembersToSchool(r.Context(), schoolID, []string{id})
if err != nil {
logger.Debug().Err(err).Msg("could not add school member: 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
}
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)
}
// DeleteSchoolMember implements the Service interface.
func (g Graph) DeleteSchoolMember(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Msg("calling delete school member")
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 member: 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 member: missing school id")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing school id")
return
}
memberID := chi.URLParam(r, "memberID")
memberID, err = url.PathUnescape(memberID)
if err != nil {
logger.Debug().Err(err).Str("id", memberID).Msg("could not delete school 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 school member: missing member id")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing member id")
return
}
logger.Debug().Str("schoolID", schoolID).Str("memberID", memberID).Msg("calling delete member on backend")
err = g.identityEducationBackend.RemoveMemberFromSchool(r.Context(), schoolID, memberID)
if err != nil {
logger.Debug().Err(err).Msg("could not delete school member: 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
}
e := events.SchoolMemberRemoved{SchoolID: schoolID, UserID: memberID}
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 sortSchools(req *godata.GoDataRequest, schools []*libregraph.EducationSchool) ([]*libregraph.EducationSchool, error) {
var sorter sort.Interface
if req.Query.OrderBy == nil || len(req.Query.OrderBy.OrderByItems) != 1 {
return schools, nil
}
switch req.Query.OrderBy.OrderByItems[0].Field.Value {
case "displayName":
sorter = schoolsByDisplayName{schools}
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 == "desc" {
sorter = sort.Reverse(sorter)
}
sort.Sort(sorter)
return schools, nil
}

View File

@@ -0,0 +1,465 @@
package svc_test
import (
"bytes"
"context"
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"net/http/httptest"
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/go-chi/chi/v5"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/test-go/testify/mock"
libregraph "github.com/owncloud/libre-graph-api-go"
ogrpc "github.com/owncloud/ocis/v2/ocis-pkg/service/grpc"
"github.com/owncloud/ocis/v2/ocis-pkg/shared"
"github.com/owncloud/ocis/v2/services/graph/mocks"
"github.com/owncloud/ocis/v2/services/graph/pkg/config"
"github.com/owncloud/ocis/v2/services/graph/pkg/config/defaults"
identitymocks "github.com/owncloud/ocis/v2/services/graph/pkg/identity/mocks"
service "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
)
type schoolList struct {
Value []*libregraph.EducationSchool
}
var _ = Describe("Schools", func() {
var (
svc service.Service
ctx context.Context
cfg *config.Config
gatewayClient *mocks.GatewayClient
eventsPublisher mocks.Publisher
identityEducationBackend *identitymocks.EducationBackend
rr *httptest.ResponseRecorder
newSchool *libregraph.EducationSchool
currentUser = &userv1beta1.User{
Id: &userv1beta1.UserId{
OpaqueId: "user",
},
}
)
BeforeEach(func() {
eventsPublisher.On("Publish", mock.Anything, mock.Anything, mock.Anything).Return(nil)
identityEducationBackend = &identitymocks.EducationBackend{}
gatewayClient = &mocks.GatewayClient{}
newSchool = libregraph.NewEducationSchool()
newSchool.SetId("school1")
rr = httptest.NewRecorder()
ctx = context.Background()
cfg = defaults.FullDefaultConfig()
cfg.Identity.LDAP.CACert = "" // skip the startup checks, we don't use LDAP at all in this tests
cfg.TokenManager.JWTSecret = "loremipsum"
cfg.Commons = &shared.Commons{}
cfg.GRPCClientTLS = &shared.GRPCClientTLS{}
_ = ogrpc.Configure(ogrpc.GetClientOptions(cfg.GRPCClientTLS)...)
svc = service.NewService(
service.Config(cfg),
service.WithGatewayClient(gatewayClient),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityEducationBackend(identityEducationBackend),
)
})
Describe("GetSchools", func() {
It("handles invalid ODATA parameters", func() {
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/schools?§foo=bar", nil)
svc.GetSchools(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("handles invalid sorting queries", func() {
identityEducationBackend.On("GetSchools", ctx, mock.Anything).Return([]*libregraph.EducationSchool{newSchool}, nil)
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/schools?$orderby=invalid", nil)
svc.GetSchools(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
data, err := ioutil.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
odataerr := libregraph.OdataError{}
err = json.Unmarshal(data, &odataerr)
Expect(err).ToNot(HaveOccurred())
Expect(odataerr.Error.Code).To(Equal("invalidRequest"))
})
It("handles unknown backend errors", func() {
identityEducationBackend.On("GetSchools", ctx, mock.Anything).Return(nil, errors.New("failed"))
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/schools", nil)
svc.GetSchools(rr, r)
Expect(rr.Code).To(Equal(http.StatusInternalServerError))
data, err := ioutil.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
odataerr := libregraph.OdataError{}
err = json.Unmarshal(data, &odataerr)
Expect(err).ToNot(HaveOccurred())
Expect(odataerr.Error.Code).To(Equal("generalException"))
})
It("handles backend errors", func() {
identityEducationBackend.On("GetSchools", ctx, mock.Anything).Return(nil, errorcode.New(errorcode.AccessDenied, "access denied"))
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/schools", nil)
svc.GetSchools(rr, r)
Expect(rr.Code).To(Equal(http.StatusInternalServerError))
data, err := ioutil.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
odataerr := libregraph.OdataError{}
err = json.Unmarshal(data, &odataerr)
Expect(err).ToNot(HaveOccurred())
Expect(odataerr.Error.Code).To(Equal("accessDenied"))
})
It("renders an empty list of schools", func() {
identityEducationBackend.On("GetSchools", ctx, mock.Anything).Return([]*libregraph.EducationSchool{}, nil)
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/schools", nil)
svc.GetSchools(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
data, err := ioutil.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
res := service.ListResponse{}
err = json.Unmarshal(data, &res)
Expect(err).ToNot(HaveOccurred())
Expect(res.Value).To(Equal([]interface{}{}))
})
It("renders a list of schools", func() {
identityEducationBackend.On("GetSchools", ctx, mock.Anything).Return([]*libregraph.EducationSchool{newSchool}, nil)
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/schools", nil)
svc.GetSchools(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
data, err := ioutil.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
res := schoolList{}
err = json.Unmarshal(data, &res)
Expect(err).ToNot(HaveOccurred())
Expect(len(res.Value)).To(Equal(1))
Expect(res.Value[0].GetId()).To(Equal("school1"))
})
})
Describe("GetSchool", func() {
It("handles missing or empty school id", func() {
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/schools", nil)
svc.GetSchool(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
r = httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/schools", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("schoolID", "")
r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, nil), chi.RouteCtxKey, rctx))
svc.GetSchool(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
Context("with an existing school", func() {
BeforeEach(func() {
identityEducationBackend.On("GetSchool", mock.Anything, mock.Anything, mock.Anything).Return(newSchool, nil)
})
It("gets the school", func() {
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/schools/"+*newSchool.Id, nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("schoolID", *newSchool.Id)
r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, nil), chi.RouteCtxKey, rctx))
svc.GetSchool(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
})
})
})
Describe("PostSchool", func() {
It("handles invalid body", func() {
r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/schools/", bytes.NewBufferString("{invalid"))
svc.PostSchool(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("handles missing display name", func() {
newSchool = libregraph.NewEducationSchool()
newSchool.SetSchoolNumber("0034")
newSchoolJson, err := json.Marshal(newSchool)
Expect(err).ToNot(HaveOccurred())
r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/schools/", bytes.NewBuffer(newSchoolJson))
svc.PostSchool(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("handles missing school number", func() {
newSchool = libregraph.NewEducationSchool()
newSchool.SetDisplayName("New School")
newSchoolJson, err := json.Marshal(newSchool)
Expect(err).ToNot(HaveOccurred())
r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/schools/", bytes.NewBuffer(newSchoolJson))
svc.PostSchool(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("disallows school create ids", func() {
newSchool = libregraph.NewEducationSchool()
newSchool.SetId("disallowed")
newSchool.SetDisplayName("New School")
newSchool.SetSchoolNumber("0034")
newSchoolJson, err := json.Marshal(newSchool)
Expect(err).ToNot(HaveOccurred())
r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/schools/", bytes.NewBuffer(newSchoolJson))
svc.PostSchool(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("handles backend errors", func() {
newSchool = libregraph.NewEducationSchool()
newSchool.SetDisplayName("New School")
newSchool.SetSchoolNumber("0034")
newSchoolJson, err := json.Marshal(newSchool)
Expect(err).ToNot(HaveOccurred())
identityEducationBackend.On("CreateSchool", mock.Anything, mock.Anything).Return(nil, errorcode.New(errorcode.AccessDenied, "access denied"))
r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/schools/", bytes.NewBuffer(newSchoolJson))
svc.PostSchool(rr, r)
Expect(rr.Code).To(Equal(http.StatusInternalServerError))
})
It("creates the school", func() {
newSchool = libregraph.NewEducationSchool()
newSchool.SetDisplayName("New School")
newSchoolJson, err := json.Marshal(newSchool)
Expect(err).ToNot(HaveOccurred())
identityEducationBackend.On("CreateSchool", mock.Anything, mock.Anything).Return(newSchool, nil)
r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/schools/", bytes.NewBuffer(newSchoolJson))
svc.PostSchool(rr, r)
Expect(rr.Code).To(Equal(http.StatusCreated))
})
})
Describe("PatchSchool", func() {
It("handles invalid body", func() {
r := httptest.NewRequest(http.MethodPatch, "/graph/v1.0/education/schools/", bytes.NewBufferString("{invalid"))
rctx := chi.NewRouteContext()
rctx.URLParams.Add("schoolID", *newSchool.Id)
r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.PatchSchool(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("handles missing or empty school id", func() {
r := httptest.NewRequest(http.MethodPatch, "/graph/v1.0/education/schools", nil)
svc.PatchSchool(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
r = httptest.NewRequest(http.MethodPatch, "/graph/v1.0/education/schools", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("schoolID", "")
r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.PatchSchool(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("handles malformed school id", func() {
r := httptest.NewRequest(http.MethodPatch, "/graph/v1.0/education/schools", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("schoolID", "school%id")
r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.PatchSchool(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("updates the school", func() {
newSchool = libregraph.NewEducationSchool()
newSchool.SetDisplayName("New School Name")
newSchoolJson, err := json.Marshal(newSchool)
Expect(err).ToNot(HaveOccurred())
r := httptest.NewRequest(http.MethodPatch, "/graph/v1.0/education/schools/schoolid", bytes.NewBuffer(newSchoolJson))
rctx := chi.NewRouteContext()
rctx.URLParams.Add("schoolID", "school-id")
r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.PatchSchool(rr, r)
Expect(rr.Code).To(Equal(http.StatusNoContent))
})
})
Describe("DeleteSchool", func() {
Context("with an existing school", func() {
BeforeEach(func() {
identityEducationBackend.On("GetSchool", mock.Anything, mock.Anything, mock.Anything).Return(newSchool, nil)
})
})
It("deletes the school", func() {
identityEducationBackend.On("DeleteSchool", mock.Anything, mock.Anything, mock.Anything).Return(nil)
r := httptest.NewRequest(http.MethodPatch, "/graph/v1.0/education/schools", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("schoolID", *newSchool.Id)
r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.DeleteSchool(rr, r)
Expect(rr.Code).To(Equal(http.StatusNoContent))
identityEducationBackend.AssertNumberOfCalls(GinkgoT(), "DeleteSchool", 1)
eventsPublisher.AssertNumberOfCalls(GinkgoT(), "Publish", 1)
})
})
Describe("GetSchoolMembers", func() {
It("gets the list of members", func() {
user := libregraph.NewUser()
user.SetId("user")
identityEducationBackend.On("GetSchoolMembers", mock.Anything, mock.Anything, mock.Anything).Return([]*libregraph.User{user}, nil)
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/schools/{schoolID}/members", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("schoolID", *newSchool.Id)
r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.GetSchoolMembers(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
data, err := ioutil.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
var members []*libregraph.User
err = json.Unmarshal(data, &members)
Expect(err).ToNot(HaveOccurred())
Expect(len(members)).To(Equal(1))
Expect(members[0].GetId()).To(Equal("user"))
})
})
Describe("PostSchoolMembers", func() {
It("fails on invalid body", func() {
r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/schools/{schoolID}/members", bytes.NewBufferString("{invalid"))
rctx := chi.NewRouteContext()
rctx.URLParams.Add("schoolID", *newSchool.Id)
r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.PostSchoolMember(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("fails on missing member refs", func() {
member := libregraph.NewMemberReference()
data, err := json.Marshal(member)
Expect(err).ToNot(HaveOccurred())
r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/schools/{schoolID}/members", bytes.NewBuffer(data))
rctx := chi.NewRouteContext()
rctx.URLParams.Add("schoolID", *newSchool.Id)
r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.PostSchoolMember(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("fails on invalid member 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/schools/{schoolID}/members", bytes.NewBuffer(data))
rctx := chi.NewRouteContext()
rctx.URLParams.Add("schoolID", *newSchool.Id)
r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.PostSchoolMember(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("adds a new member", func() {
member := libregraph.NewMemberReference()
member.SetOdataId("/users/user")
data, err := json.Marshal(member)
Expect(err).ToNot(HaveOccurred())
identityEducationBackend.On("AddMembersToSchool", mock.Anything, mock.Anything, mock.Anything).Return(nil)
r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/schools/{schoolID}/members", bytes.NewBuffer(data))
rctx := chi.NewRouteContext()
rctx.URLParams.Add("schoolID", *newSchool.Id)
r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.PostSchoolMember(rr, r)
Expect(rr.Code).To(Equal(http.StatusNoContent))
identityEducationBackend.AssertNumberOfCalls(GinkgoT(), "AddMembersToSchool", 1)
})
})
Describe("DeleteSchoolMembers", func() {
It("handles missing or empty member id", func() {
r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/education/schools/{schoolID}/members/{memberID}/$ref", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("schoolID", *newSchool.Id)
r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.DeleteSchoolMember(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("handles missing or empty member id", func() {
r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/education/schools/{schoolID}/members/{memberID}/$ref", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("memberID", "/users/user")
r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.DeleteSchoolMember(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("deletes members", func() {
identityEducationBackend.On("RemoveMemberFromSchool", mock.Anything, mock.Anything, mock.Anything).Return(nil)
r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/education/schools/{schoolID}/members/{memberID}/$ref", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("schoolID", *newSchool.Id)
rctx.URLParams.Add("memberID", "/users/user1")
r = r.WithContext(context.WithValue(ctxpkg.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.DeleteSchoolMember(rr, r)
Expect(rr.Code).To(Equal(http.StatusNoContent))
identityEducationBackend.AssertNumberOfCalls(GinkgoT(), "RemoveMemberFromSchool", 1)
})
})
})

View File

@@ -46,6 +46,15 @@ type Service interface {
PostGroupMember(http.ResponseWriter, *http.Request)
DeleteGroupMember(http.ResponseWriter, *http.Request)
GetSchools(http.ResponseWriter, *http.Request)
GetSchool(http.ResponseWriter, *http.Request)
PostSchool(http.ResponseWriter, *http.Request)
PatchSchool(http.ResponseWriter, *http.Request)
DeleteSchool(http.ResponseWriter, *http.Request)
GetSchoolMembers(http.ResponseWriter, *http.Request)
PostSchoolMember(http.ResponseWriter, *http.Request)
DeleteSchoolMember(http.ResponseWriter, *http.Request)
GetDrives(w http.ResponseWriter, r *http.Request)
GetSingleDrive(w http.ResponseWriter, r *http.Request)
GetAllDrives(w http.ResponseWriter, r *http.Request)
@@ -67,13 +76,14 @@ func NewService(opts ...Option) Service {
m.Use(options.Middleware...)
svc := Graph{
config: options.Config,
mux: m,
logger: &options.Logger,
spacePropertiesCache: ttlcache.NewCache(),
eventsPublisher: options.EventsPublisher,
gatewayClient: options.GatewayClient,
searchService: options.SearchService,
config: options.Config,
mux: m,
logger: &options.Logger,
spacePropertiesCache: ttlcache.NewCache(),
eventsPublisher: options.EventsPublisher,
gatewayClient: options.GatewayClient,
searchService: options.SearchService,
identityEducationBackend: options.IdentityEducationBackend,
}
if options.IdentityBackend == nil {
@@ -129,10 +139,15 @@ func NewService(opts ...Option) Service {
TLSConfig: tlsConf,
},
)
if svc.identityBackend, err = identity.NewLDAPBackend(conn, options.Config.Identity.LDAP, &options.Logger); err != nil {
lb, err := identity.NewLDAPBackend(conn, options.Config.Identity.LDAP, &options.Logger)
if err != nil {
options.Logger.Error().Msgf("Error initializing LDAP Backend: '%s'", err)
return nil
}
svc.identityBackend = lb
if options.IdentityEducationBackend == nil {
svc.identityEducationBackend = lb
}
default:
options.Logger.Error().Msgf("Unknown Identity Backend: '%s'", options.Config.Identity.Backend)
return nil
@@ -215,6 +230,45 @@ func NewService(opts ...Option) Service {
r.Delete("/", svc.DeleteDrive)
})
})
r.With(requireAdmin).Route("/education", func(r chi.Router) {
r.Route("/schools", func(r chi.Router) {
r.Get("/", svc.GetSchools)
r.Post("/", svc.PostSchool)
r.Route("/{schoolID}", func(r chi.Router) {
r.Get("/", svc.GetSchool)
r.Delete("/", svc.DeleteSchool)
r.Patch("/", svc.PatchSchool)
r.Route("/members", func(r chi.Router) {
r.Get("/", svc.GetSchoolMembers)
r.Post("/$ref", svc.PostSchoolMember)
r.Delete("/{memberID}/$ref", svc.DeleteSchoolMember)
})
})
})
r.Route("/users", func(r chi.Router) {
r.Get("/", svc.GetUsers)
r.Post("/", svc.PostUser)
r.Route("/{userID}", func(r chi.Router) {
r.Get("/", svc.GetUser)
r.Delete("/", svc.DeleteUser)
r.Patch("/", svc.PatchUser)
})
})
r.Route("/classes", func(r chi.Router) {
r.Get("/", svc.GetGroups)
r.Post("/", svc.PostGroup)
r.Route("/{groupID}", func(r chi.Router) {
r.Get("/", svc.GetGroup)
r.Delete("/", svc.DeleteGroup)
r.Patch("/", svc.PatchGroup)
r.Route("/members", func(r chi.Router) {
r.Get("/", svc.GetGroupMembers)
r.Post("/$ref", svc.PostGroupMember)
r.Delete("/{memberID}/$ref", svc.DeleteGroupMember)
})
})
})
})
})
})

View File

@@ -95,6 +95,46 @@ func (t tracing) DeleteGroupMember(w http.ResponseWriter, r *http.Request) {
t.next.DeleteGroupMember(w, r)
}
// GetSchools implements the Service interface.
func (t tracing) GetSchools(w http.ResponseWriter, r *http.Request) {
t.next.GetSchools(w, r)
}
// GetSchool implements the Service interface.
func (t tracing) GetSchool(w http.ResponseWriter, r *http.Request) {
t.next.GetSchool(w, r)
}
// PostSchool implements the Service interface.
func (t tracing) PostSchool(w http.ResponseWriter, r *http.Request) {
t.next.PostSchool(w, r)
}
// PatchSchool implements the Service interface.
func (t tracing) PatchSchool(w http.ResponseWriter, r *http.Request) {
t.next.PatchSchool(w, r)
}
// DeleteSchool implements the Service interface.
func (t tracing) DeleteSchool(w http.ResponseWriter, r *http.Request) {
t.next.DeleteSchool(w, r)
}
// GetSchoolMembers implements the Service interface.
func (t tracing) GetSchoolMembers(w http.ResponseWriter, r *http.Request) {
t.next.GetSchoolMembers(w, r)
}
// PostSchoolMember implements the Service interface.
func (t tracing) PostSchoolMember(w http.ResponseWriter, r *http.Request) {
t.next.PostSchoolMember(w, r)
}
// DeleteSchoolMember implements the Service interface.
func (t tracing) DeleteSchoolMember(w http.ResponseWriter, r *http.Request) {
t.next.DeleteSchoolMember(w, r)
}
// GetDrives implements the Service interface.
func (t tracing) GetDrives(w http.ResponseWriter, r *http.Request) {
t.next.GetDrives(w, r)