handle /education/user

Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
This commit is contained in:
Jörn Friedrich Dreyer
2022-12-09 21:39:33 +00:00
committed by Ralf Haferkamp
parent 962ae09bf1
commit 57fd00d238
15 changed files with 1297 additions and 34 deletions
+10 -1
View File
@@ -42,11 +42,20 @@ type EducationBackend interface {
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)
GetSchoolMembers(ctx context.Context, id string) ([]*libregraph.EducationUser, 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
// CreateEducationUser creates a given education user in the identity backend.
CreateEducationUser(ctx context.Context, user libregraph.EducationUser) (*libregraph.EducationUser, error)
// DeleteEducationUser deletes a given educationuser, identified by username or id, from the backend
DeleteEducationUser(ctx context.Context, nameOrID string) error
// UpdateEducationUser applies changes to given education user, identified by username or id
UpdateEducationUser(ctx context.Context, nameOrID string, user libregraph.EducationUser) (*libregraph.EducationUser, error)
GetEducationUser(ctx context.Context, nameOrID string, queryParam url.Values) (*libregraph.EducationUser, error)
GetEducationUsers(ctx context.Context, queryParam url.Values) ([]*libregraph.EducationUser, error)
}
func CreateUserModelFromCS3(u *cs3.User) *libregraph.User {
@@ -0,0 +1,33 @@
package identity
import (
"context"
"net/url"
libregraph "github.com/owncloud/libre-graph-api-go"
)
// CreateEducationUser creates a given education user in the identity backend.
func (i *LDAP) CreateEducationUser(ctx context.Context, user libregraph.EducationUser) (*libregraph.EducationUser, error) {
return nil, errNotImplemented
}
// DeleteEducationUser deletes a given educationuser, identified by username or id, from the backend
func (i *LDAP) DeleteEducationUser(ctx context.Context, nameOrID string) error {
return errNotImplemented
}
// UpdateEducationUser applies changes to given education user, identified by username or id
func (i *LDAP) UpdateEducationUser(ctx context.Context, nameOrID string, user libregraph.EducationUser) (*libregraph.EducationUser, error) {
return nil, errNotImplemented
}
// GetEducationUser implements the EducationBackend interface for the LDAP backend.
func (i *LDAP) GetEducationUser(ctx context.Context, nameOrID string, queryParam url.Values) (*libregraph.EducationUser, error) {
return nil, errNotImplemented
}
// GetEducationUsers implements the EducationBackend interface for the LDAP backend.
func (i *LDAP) GetEducationUsers(ctx context.Context, queryParam url.Values) ([]*libregraph.EducationUser, error) {
return nil, errNotImplemented
}
+1 -1
View File
@@ -177,7 +177,7 @@ func (i *LDAP) GetSchools(ctx context.Context, queryParam url.Values) ([]*libreg
}
// GetSchoolMembers implements the EducationBackend interface for the LDAP backend.
func (i *LDAP) GetSchoolMembers(ctx context.Context, id string) ([]*libregraph.User, error) {
func (i *LDAP) GetSchoolMembers(ctx context.Context, id string) ([]*libregraph.EducationUser, error) {
return nil, errNotImplemented
}
@@ -31,6 +31,29 @@ func (_m *EducationBackend) AddMembersToSchool(ctx context.Context, schoolID str
return r0
}
// CreateEducationUser provides a mock function with given fields: ctx, user
func (_m *EducationBackend) CreateEducationUser(ctx context.Context, user libregraph.EducationUser) (*libregraph.EducationUser, error) {
ret := _m.Called(ctx, user)
var r0 *libregraph.EducationUser
if rf, ok := ret.Get(0).(func(context.Context, libregraph.EducationUser) *libregraph.EducationUser); ok {
r0 = rf(ctx, user)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*libregraph.EducationUser)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, libregraph.EducationUser) error); ok {
r1 = rf(ctx, user)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// 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)
@@ -54,6 +77,20 @@ func (_m *EducationBackend) CreateSchool(ctx context.Context, group libregraph.E
return r0, r1
}
// DeleteEducationUser provides a mock function with given fields: ctx, nameOrID
func (_m *EducationBackend) DeleteEducationUser(ctx context.Context, nameOrID string) error {
ret := _m.Called(ctx, nameOrID)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, string) error); ok {
r0 = rf(ctx, nameOrID)
} else {
r0 = ret.Error(0)
}
return r0
}
// 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)
@@ -68,6 +105,52 @@ func (_m *EducationBackend) DeleteSchool(ctx context.Context, id string) error {
return r0
}
// GetEducationUser provides a mock function with given fields: ctx, nameOrID, queryParam
func (_m *EducationBackend) GetEducationUser(ctx context.Context, nameOrID string, queryParam url.Values) (*libregraph.EducationUser, error) {
ret := _m.Called(ctx, nameOrID, queryParam)
var r0 *libregraph.EducationUser
if rf, ok := ret.Get(0).(func(context.Context, string, url.Values) *libregraph.EducationUser); ok {
r0 = rf(ctx, nameOrID, queryParam)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*libregraph.EducationUser)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string, url.Values) error); ok {
r1 = rf(ctx, nameOrID, queryParam)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetEducationUsers provides a mock function with given fields: ctx, queryParam
func (_m *EducationBackend) GetEducationUsers(ctx context.Context, queryParam url.Values) ([]*libregraph.EducationUser, error) {
ret := _m.Called(ctx, queryParam)
var r0 []*libregraph.EducationUser
if rf, ok := ret.Get(0).(func(context.Context, url.Values) []*libregraph.EducationUser); ok {
r0 = rf(ctx, queryParam)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*libregraph.EducationUser)
}
}
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
}
// 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)
@@ -92,15 +175,15 @@ func (_m *EducationBackend) GetSchool(ctx context.Context, nameOrID string, quer
}
// GetSchoolMembers provides a mock function with given fields: ctx, id
func (_m *EducationBackend) GetSchoolMembers(ctx context.Context, id string) ([]*libregraph.User, error) {
func (_m *EducationBackend) GetSchoolMembers(ctx context.Context, id string) ([]*libregraph.EducationUser, error) {
ret := _m.Called(ctx, id)
var r0 []*libregraph.User
if rf, ok := ret.Get(0).(func(context.Context, string) []*libregraph.User); ok {
var r0 []*libregraph.EducationUser
if rf, ok := ret.Get(0).(func(context.Context, string) []*libregraph.EducationUser); ok {
r0 = rf(ctx, id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]*libregraph.User)
r0 = ret.Get(0).([]*libregraph.EducationUser)
}
}
@@ -151,6 +234,29 @@ func (_m *EducationBackend) RemoveMemberFromSchool(ctx context.Context, schoolID
return r0
}
// UpdateEducationUser provides a mock function with given fields: ctx, nameOrID, user
func (_m *EducationBackend) UpdateEducationUser(ctx context.Context, nameOrID string, user libregraph.EducationUser) (*libregraph.EducationUser, error) {
ret := _m.Called(ctx, nameOrID, user)
var r0 *libregraph.EducationUser
if rf, ok := ret.Get(0).(func(context.Context, string, libregraph.EducationUser) *libregraph.EducationUser); ok {
r0 = rf(ctx, nameOrID, user)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*libregraph.EducationUser)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, string, libregraph.EducationUser) error); ok {
r1 = rf(ctx, nameOrID, user)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
type mockConstructorTestingTNewEducationBackend interface {
mock.TestingT
Cleanup(func())
@@ -0,0 +1,481 @@
package svc
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"sort"
"strings"
"github.com/CiscoM31/godata"
cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/events"
"github.com/cs3org/reva/v2/pkg/rgrpc/status"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/go-chi/chi/v5"
"github.com/go-chi/render"
libregraph "github.com/owncloud/libre-graph-api-go"
settings "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
settingssvc "github.com/owncloud/ocis/v2/services/settings/pkg/service/v0"
"golang.org/x/exp/slices"
)
// GetEducationUsers implements the Service interface.
func (g Graph) GetEducationUsers(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Interface("query", r.URL.Query()).Msg("calling get education users")
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 education users: query error")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
return
}
logger.Debug().Interface("query", r.URL.Query()).Msg("calling get education users on backend")
users, err := g.identityEducationBackend.GetEducationUsers(r.Context(), r.URL.Query())
if err != nil {
logger.Debug().Err(err).Interface("query", r.URL.Query()).Msg("could not get education users from backend")
var errcode errorcode.Error
if errors.As(err, &errcode) {
errcode.Render(w, r)
} else {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
}
return
}
users, err = sortEducationUsers(odataReq, users)
if err != nil {
logger.Debug().Interface("query", odataReq).Msg("error while sorting education users according to query")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
return
}
render.Status(r, http.StatusOK)
render.JSON(w, r, &ListResponse{Value: users})
}
// PostEducationUser implements the Service interface.
func (g Graph) PostEducationUser(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Interface("body", r.Body).Msg("calling create education user")
u := libregraph.NewEducationUser()
err := json.NewDecoder(r.Body).Decode(u)
if err != nil {
logger.Debug().Err(err).Interface("body", r.Body).Msg("could not create education user: invalid request body")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, fmt.Sprintf("invalid request body: %v", err.Error()))
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 := u.GetIdOk(); ok {
logger.Debug().Interface("user", u).Msg("could not create education user: id is a read-only attribute")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "education user id is a read-only attribute")
return
}
if _, ok := u.GetDisplayNameOk(); !ok {
logger.Debug().Err(err).Interface("user", u).Msg("could not create education user: missing required Attribute: 'displayName'")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing required Attribute: 'displayName'")
return
}
identities, ok := u.GetIdentitiesOk()
if !ok {
logger.Debug().Err(err).Interface("user", u).Msg("could not create education user: missing required Collection: 'identities'")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing required Attribute: 'identities'")
return
}
if len(identities) < 1 {
logger.Debug().Err(err).Interface("user", u).Msg("could not create education user: missing entry in Collection: 'identities'")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing required Collection: 'identities'")
return
}
for i, identity := range identities {
if _, ok := identity.GetIssuerOk(); !ok {
logger.Debug().Err(err).Interface("user", u).Msgf("could not create education user: missing Attribute in 'identities' Collection Entry %d: 'issuer'", i)
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, fmt.Sprintf("missing Attribute in 'identities' Collection Entry %d: 'issuer'", i))
return
}
if _, ok := identity.GetIssuerAssignedIdOk(); !ok {
logger.Debug().Err(err).Interface("user", u).Msgf("could not create education user: missing Attribute in 'identities' Collection Entry %d: 'issuerAssignedId'", i)
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, fmt.Sprintf("missing Attribute in 'identities' Collection Entry %d: 'issuerAssignedId'", i))
return
}
}
if accountName, ok := u.GetOnPremisesSamAccountNameOk(); ok {
if !isValidUsername(*accountName) {
logger.Debug().Str("username", *accountName).Msg("could not create education user: username must be at least the local part of an email")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, fmt.Sprintf("username %s must be at least the local part of an email", *u.OnPremisesSamAccountName))
return
}
} else {
logger.Debug().Interface("user", u).Msg("could not create education user: missing required Attribute: 'onPremisesSamAccountName'")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing required Attribute: 'onPremisesSamAccountName'")
return
}
if mail, ok := u.GetMailOk(); ok {
if !isValidEmail(*mail) {
logger.Debug().Str("mail", *u.Mail).Msg("could not create education user: invalid email address")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, fmt.Sprintf("%v is not a valid email address", *u.Mail))
return
}
} else {
logger.Debug().Interface("user", u).Msg("could not create education user: missing required Attribute: 'mail'")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing required Attribute: 'mail'")
return
}
if _, ok := u.GetPrimaryRoleOk(); !ok {
logger.Debug().Err(err).Interface("user", u).Msg("could not create education user: missing required Attribute: 'primaryRole'")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing required Attribute: 'primaryRole'")
return
}
logger.Debug().Interface("user", u).Msg("calling create education user on backend")
if u, err = g.identityEducationBackend.CreateEducationUser(r.Context(), *u); err != nil {
logger.Debug().Err(err).Msg("could not create education user: backend error")
var ecErr errorcode.Error
if errors.As(err, &ecErr) {
ecErr.Render(w, r)
} else {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
}
return
}
// assign roles if possible
if g.roleService != nil {
// All users get the user role by default currently.
// to all new users for now, as create Account request does not have any role field
if _, err = g.roleService.AssignRoleToUser(r.Context(), &settings.AssignRoleToUserRequest{
AccountUuid: *u.Id,
RoleId: settingssvc.BundleUUIDRoleUser,
}); err != nil {
// log as error, admin eventually needs to do something
logger.Error().Err(err).Str("id", *u.Id).Str("role", settingssvc.BundleUUIDRoleUser).Msg("could not create education user: role assignment failed")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "role assignment failed")
return
}
}
e := events.UserCreated{UserID: *u.Id}
if currentUser, ok := revactx.ContextGetUser(r.Context()); ok {
e.Executant = currentUser.GetId()
}
g.publishEvent(e)
render.Status(r, http.StatusOK)
render.JSON(w, r, u)
}
// GetEducationUser implements the Service interface.
func (g Graph) GetEducationUser(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Msg("calling get education user")
userID := chi.URLParam(r, "userID")
userID, err := url.PathUnescape(userID)
if err != nil {
logger.Debug().Err(err).Str("id", userID).Msg("could not get education user: unescaping education user id failed")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping education user id failed")
return
}
if userID == "" {
logger.Debug().Msg("could not get user: missing education user id")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing education user id")
return
}
logger.Debug().Str("id", userID).Msg("calling get education user from backend")
user, err := g.identityEducationBackend.GetEducationUser(r.Context(), userID, r.URL.Query())
if err != nil {
logger.Debug().Err(err).Msg("could not get education user: error fetching education user from backend")
var errcode errorcode.Error
if errors.As(err, &errcode) {
errcode.Render(w, r)
} else {
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error())
}
return
}
sel := strings.Split(r.URL.Query().Get("$select"), ",")
exp := strings.Split(r.URL.Query().Get("$expand"), ",")
if slices.Contains(sel, "drive") || slices.Contains(sel, "drives") || slices.Contains(exp, "drive") || slices.Contains(exp, "drives") {
wdu, err := g.getWebDavBaseURL()
if err != nil {
// log error, wrong configuration
logger.Error().
Err(err).
Str("webdav_base", g.config.Spaces.WebDavBase).
Str("webdav_path", g.config.Spaces.WebDavPath).
Msg("error parsing webdav URL")
render.Status(r, http.StatusInternalServerError)
return
}
logger.Debug().Str("id", user.GetId()).Msg("calling list storage spaces with education user id filter")
f := listStorageSpacesUserFilter(user.GetId())
// use the unrestricted flag to get all possible spaces
// users with the canListAllSpaces permission should see all spaces
opaque := utils.AppendPlainToOpaque(nil, "unrestricted", "T")
lspr, err := g.gatewayClient.ListStorageSpaces(r.Context(), &storageprovider.ListStorageSpacesRequest{
Opaque: opaque,
Filters: []*storageprovider.ListStorageSpacesRequest_Filter{f},
})
if err != nil {
// transport error, needs to be fixed by admin
logger.Error().Err(err).Interface("query", r.URL.Query()).Msg("error getting storages: transport error")
render.Status(r, http.StatusInternalServerError)
render.JSON(w, r, user)
return
}
if lspr.GetStatus().GetCode() != cs3rpc.Code_CODE_OK {
logger.Debug().Str("grpc", lspr.GetStatus().GetMessage()).Msg("could not get drive for education user")
// in case of NOT_OK, we can just return the user object with empty drives
render.Status(r, status.HTTPStatusFromCode(http.StatusOK))
render.JSON(w, r, user)
return
}
drives := []libregraph.Drive{}
for _, sp := range lspr.GetStorageSpaces() {
d, err := g.cs3StorageSpaceToDrive(r.Context(), wdu, sp)
if err != nil {
logger.Debug().Err(err).Interface("id", sp.Id).Msg("error converting space to drive")
continue
}
quota, err := g.getDriveQuota(r.Context(), sp)
if err != nil {
logger.Debug().Err(err).Interface("id", sp.Id).Msg("error calling get quota on drive")
}
d.Quota = quota
if slices.Contains(sel, "drive") || slices.Contains(exp, "drive") {
if *d.DriveType == "personal" {
user.Drive = d
}
} else {
drives = append(drives, *d)
user.Drives = drives
}
}
}
render.Status(r, http.StatusOK)
render.JSON(w, r, user)
}
// DeleteEducationUser implements the Service interface.
func (g Graph) DeleteEducationUser(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Msg("calling delete education user")
userID := chi.URLParam(r, "userID")
userID, err := url.PathUnescape(userID)
if err != nil {
logger.Debug().Err(err).Str("id", userID).Msg("could not delete education user: unescaping education user id failed")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping education user id failed")
return
}
if userID == "" {
logger.Debug().Msg("could not delete education user: missing education user id")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing education user id")
return
}
logger.Debug().Str("id", userID).Msg("calling get education user on user backend")
user, err := g.identityEducationBackend.GetEducationUser(r.Context(), userID, r.URL.Query())
if err != nil {
logger.Debug().Err(err).Str("userID", userID).Msg("failed to get education user from backend")
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.UserDeleted{UserID: user.GetId()}
if currentUser, ok := revactx.ContextGetUser(r.Context()); ok {
if currentUser.GetId().GetOpaqueId() == user.GetId() {
logger.Debug().Msg("could not delete education user: self deletion forbidden")
errorcode.NotAllowed.Render(w, r, http.StatusForbidden, "self deletion forbidden")
return
}
e.Executant = currentUser.GetId()
}
if g.gatewayClient != nil {
logger.Debug().
Str("user", user.GetId()).
Msg("calling list spaces with user filter to fetch the personal space for deletion")
opaque := utils.AppendPlainToOpaque(nil, "unrestricted", "T")
f := listStorageSpacesUserFilter(user.GetId())
lspr, err := g.gatewayClient.ListStorageSpaces(r.Context(), &storageprovider.ListStorageSpacesRequest{
Opaque: opaque,
Filters: []*storageprovider.ListStorageSpacesRequest_Filter{f},
})
if err != nil {
// transport error, log as error
logger.Error().Err(err).Msg("could not fetch spaces: transport error")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "could not fetch spaces for deletion, aborting")
return
}
for _, sp := range lspr.GetStorageSpaces() {
if !(sp.SpaceType == "personal" && sp.Owner.Id.OpaqueId == user.GetId()) {
continue
}
// TODO: check if request contains a homespace and if, check if requesting user has the privilege to
// delete it and make sure it is not deleting its own homespace
// needs modification of the cs3api
// Deleting a space a two step process (1. disabling/trashing, 2. purging)
// Do the "disable/trash" step only if the space is not marked as trashed yet:
if _, ok := sp.Opaque.Map["trashed"]; !ok {
_, err := g.gatewayClient.DeleteStorageSpace(r.Context(), &storageprovider.DeleteStorageSpaceRequest{
Id: &storageprovider.StorageSpaceId{
OpaqueId: sp.Id.OpaqueId,
},
})
if err != nil {
logger.Error().Err(err).Msg("could not disable homespace: transport error")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "could not disable homespace, aborting")
return
}
}
purgeFlag := utils.AppendPlainToOpaque(nil, "purge", "")
_, err := g.gatewayClient.DeleteStorageSpace(r.Context(), &storageprovider.DeleteStorageSpaceRequest{
Opaque: purgeFlag,
Id: &storageprovider.StorageSpaceId{
OpaqueId: sp.Id.OpaqueId,
},
})
if err != nil {
// transport error, log as error
logger.Error().Err(err).Msg("could not delete homespace: transport error")
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "could not delete homespace, aborting")
return
}
break
}
}
logger.Debug().Str("id", user.GetId()).Msg("calling delete education user on backend")
err = g.identityEducationBackend.DeleteEducationUser(r.Context(), user.GetId())
if err != nil {
logger.Debug().Err(err).Msg("could not delete education user: 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
}
}
g.publishEvent(e)
render.Status(r, http.StatusNoContent)
render.NoContent(w, r)
}
// PatchEducationUser implements the Service Interface. Updates the specified attributes of an
// ExistingUser
func (g Graph) PatchEducationUser(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Msg("calling patch education user")
nameOrID := chi.URLParam(r, "userID")
nameOrID, err := url.PathUnescape(nameOrID)
if err != nil {
logger.Debug().Err(err).Str("id", nameOrID).Msg("could not update education user: unescaping education user id failed")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "unescaping education user id failed")
return
}
if nameOrID == "" {
logger.Debug().Msg("could not update education user: missing education user id")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "missing education user id")
return
}
changes := libregraph.NewEducationUser()
err = json.NewDecoder(r.Body).Decode(changes)
if err != nil {
logger.Debug().Err(err).Interface("body", r.Body).Msg("could not update education user: invalid request body")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest,
fmt.Sprintf("invalid request body: %s", err.Error()))
return
}
var features []events.UserFeature
if mail, ok := changes.GetMailOk(); ok {
if !isValidEmail(*mail) {
logger.Debug().Str("mail", *mail).Msg("could not update education user: email is not a valid email address")
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest,
fmt.Sprintf("'%s' is not a valid email address", *mail))
return
}
features = append(features, events.UserFeature{Name: "email", Value: *mail})
}
if name, ok := changes.GetDisplayNameOk(); ok {
features = append(features, events.UserFeature{Name: "displayname", Value: *name})
}
logger.Debug().Str("nameid", nameOrID).Interface("changes", *changes).Msg("calling update education user on backend")
u, err := g.identityEducationBackend.UpdateEducationUser(r.Context(), nameOrID, *changes)
if err != nil {
logger.Debug().Err(err).Str("id", nameOrID).Msg("could not update education user: 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.UserFeatureChanged{
UserID: nameOrID,
Features: features,
}
if currentUser, ok := revactx.ContextGetUser(r.Context()); ok {
e.Executant = currentUser.GetId()
}
g.publishEvent(e)
render.Status(r, http.StatusOK) // TODO StatusNoContent when prefer=minimal is used
render.JSON(w, r, u)
}
func sortEducationUsers(req *godata.GoDataRequest, users []*libregraph.EducationUser) ([]*libregraph.EducationUser, error) {
var sorter sort.Interface
if req.Query.OrderBy == nil || len(req.Query.OrderBy.OrderByItems) != 1 {
return users, nil
}
switch req.Query.OrderBy.OrderByItems[0].Field.Value {
case displayNameAttr:
sorter = educationUsersByDisplayName{users}
case "mail":
sorter = educationUsersByMail{users}
case "onPremisesSamAccountName":
sorter = educationUsersByOnPremisesSamAccountName{users}
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 users, nil
}
@@ -0,0 +1,508 @@
package svc_test
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/rgrpc/status"
"github.com/go-chi/chi/v5"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/stretchr/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"
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
"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"
)
type educationUserList struct {
Value []*libregraph.EducationUser
}
var _ = Describe("EducationUsers", func() {
var (
svc service.Service
ctx context.Context
cfg *config.Config
gatewayClient *mocks.GatewayClient
eventsPublisher mocks.Publisher
roleService *mocks.RoleService
identityEducationBackend *identitymocks.EducationBackend
rr *httptest.ResponseRecorder
currentUser = &userv1beta1.User{
Id: &userv1beta1.UserId{
OpaqueId: "user",
},
}
)
BeforeEach(func() {
eventsPublisher.On("Publish", mock.Anything, mock.Anything, mock.Anything).Return(nil)
identityEducationBackend = &identitymocks.EducationBackend{}
roleService = &mocks.RoleService{}
gatewayClient = &mocks.GatewayClient{}
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),
//service.WithRoleService(roleService),
)
})
Describe("GetEducationUsers", func() {
It("handles invalid requests", func() {
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/users?$invalid=true", nil)
svc.GetEducationUsers(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("lists the users", func() {
user := &libregraph.EducationUser{}
user.SetId("user1")
users := []*libregraph.EducationUser{user}
identityEducationBackend.On("GetEducationUsers", mock.Anything, mock.Anything, mock.Anything).Return(users, nil)
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/users", nil)
svc.GetEducationUsers(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
data, err := io.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
res := educationUserList{}
err = json.Unmarshal(data, &res)
Expect(err).ToNot(HaveOccurred())
Expect(len(res.Value)).To(Equal(1))
Expect(res.Value[0].GetId()).To(Equal("user1"))
})
It("sorts", func() {
user := &libregraph.EducationUser{}
user.SetId("user1")
user.SetMail("z@example.com")
user.SetDisplayName("9")
user.SetOnPremisesSamAccountName("9")
user2 := &libregraph.EducationUser{}
user2.SetId("user2")
user2.SetMail("a@example.com")
user2.SetDisplayName("1")
user2.SetOnPremisesSamAccountName("1")
users := []*libregraph.EducationUser{user, user2}
identityEducationBackend.On("GetEducationUsers", mock.Anything, mock.Anything, mock.Anything).Return(users, nil)
getUsers := func(path string) []*libregraph.EducationUser {
r := httptest.NewRequest(http.MethodGet, path, nil)
rec := httptest.NewRecorder()
svc.GetEducationUsers(rec, r)
Expect(rec.Code).To(Equal(http.StatusOK))
data, err := io.ReadAll(rec.Body)
Expect(err).ToNot(HaveOccurred())
res := educationUserList{}
err = json.Unmarshal(data, &res)
Expect(err).ToNot(HaveOccurred())
return res.Value
}
unsorted := getUsers("/graph/v1.0/education/users")
Expect(len(unsorted)).To(Equal(2))
Expect(unsorted[0].GetId()).To(Equal("user1"))
Expect(unsorted[1].GetId()).To(Equal("user2"))
byMail := getUsers("/graph/v1.0/education/users?$orderby=mail")
Expect(len(byMail)).To(Equal(2))
Expect(byMail[0].GetId()).To(Equal("user2"))
Expect(byMail[1].GetId()).To(Equal("user1"))
byMail = getUsers("/graph/v1.0/education/users?$orderby=mail%20asc")
Expect(len(byMail)).To(Equal(2))
Expect(byMail[0].GetId()).To(Equal("user2"))
Expect(byMail[1].GetId()).To(Equal("user1"))
byMail = getUsers("/graph/v1.0/education/users?$orderby=mail%20desc")
Expect(len(byMail)).To(Equal(2))
Expect(byMail[0].GetId()).To(Equal("user1"))
Expect(byMail[1].GetId()).To(Equal("user2"))
byDisplayName := getUsers("/graph/v1.0/education/users?$orderby=displayName")
Expect(len(byDisplayName)).To(Equal(2))
Expect(byDisplayName[0].GetId()).To(Equal("user2"))
Expect(byDisplayName[1].GetId()).To(Equal("user1"))
byDisplayName = getUsers("/graph/v1.0/education/users?$orderby=displayName%20asc")
Expect(len(byDisplayName)).To(Equal(2))
Expect(byDisplayName[0].GetId()).To(Equal("user2"))
Expect(byDisplayName[1].GetId()).To(Equal("user1"))
byDisplayName = getUsers("/graph/v1.0/education/users?$orderby=displayName%20desc")
Expect(len(byDisplayName)).To(Equal(2))
Expect(byDisplayName[0].GetId()).To(Equal("user1"))
Expect(byDisplayName[1].GetId()).To(Equal("user2"))
byOnPremisesSamAccountName := getUsers("/graph/v1.0/education/users?$orderby=onPremisesSamAccountName")
Expect(len(byOnPremisesSamAccountName)).To(Equal(2))
Expect(byOnPremisesSamAccountName[0].GetId()).To(Equal("user2"))
Expect(byOnPremisesSamAccountName[1].GetId()).To(Equal("user1"))
byOnPremisesSamAccountName = getUsers("/graph/v1.0/education/users?$orderby=onPremisesSamAccountName%20asc")
Expect(len(byOnPremisesSamAccountName)).To(Equal(2))
Expect(byOnPremisesSamAccountName[0].GetId()).To(Equal("user2"))
Expect(byOnPremisesSamAccountName[1].GetId()).To(Equal("user1"))
byOnPremisesSamAccountName = getUsers("/graph/v1.0/education/users?$orderby=onPremisesSamAccountName%20desc")
Expect(len(byOnPremisesSamAccountName)).To(Equal(2))
Expect(byOnPremisesSamAccountName[0].GetId()).To(Equal("user1"))
Expect(byOnPremisesSamAccountName[1].GetId()).To(Equal("user2"))
// Handles invalid sort field
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/users?$orderby=invalid", nil)
svc.GetEducationUsers(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
})
Describe("GetEducationUser", func() {
It("handles missing userids", func() {
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/users", nil)
svc.GetEducationUser(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("gets the user", func() {
user := &libregraph.EducationUser{}
user.SetId("user1")
identityEducationBackend.On("GetEducationUser", mock.Anything, mock.Anything, mock.Anything).Return(user, nil)
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/users", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("userID", *user.Id)
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.GetEducationUser(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
data, err := io.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
responseUser := &libregraph.EducationUser{}
err = json.Unmarshal(data, &responseUser)
Expect(err).ToNot(HaveOccurred())
Expect(responseUser.GetId()).To(Equal("user1"))
Expect(len(responseUser.GetDrives())).To(Equal(0))
})
It("includes the personal space if requested", func() {
user := &libregraph.EducationUser{}
user.SetId("user1")
identityEducationBackend.On("GetEducationUser", mock.Anything, mock.Anything, mock.Anything).Return(user, nil)
gatewayClient.On("GetQuota", mock.Anything, mock.Anything, mock.Anything).Return(&provider.GetQuotaResponse{
Status: status.NewOK(ctx),
TotalBytes: 10,
}, nil)
gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything, mock.Anything).Return(&provider.ListStorageSpacesResponse{
Status: status.NewOK(ctx),
StorageSpaces: []*provider.StorageSpace{
{
Id: &provider.StorageSpaceId{OpaqueId: "drive1"},
Root: &provider.ResourceId{SpaceId: "space", OpaqueId: "space"},
SpaceType: "project",
},
{
Id: &provider.StorageSpaceId{OpaqueId: "personal"},
Root: &provider.ResourceId{SpaceId: "personal", OpaqueId: "personal"},
SpaceType: "personal",
},
},
}, nil)
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/users?$expand=drive", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("userID", *user.Id)
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.GetEducationUser(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
data, err := io.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
responseUser := &libregraph.EducationUser{}
err = json.Unmarshal(data, &responseUser)
Expect(err).ToNot(HaveOccurred())
Expect(responseUser.GetId()).To(Equal("user1"))
Expect(*responseUser.GetDrive().Id).To(Equal("personal"))
})
It("includes the drives if requested", func() {
user := &libregraph.EducationUser{}
user.SetId("user1")
identityEducationBackend.On("GetEducationUser", mock.Anything, mock.Anything, mock.Anything).Return(user, nil)
gatewayClient.On("GetQuota", mock.Anything, mock.Anything, mock.Anything).Return(&provider.GetQuotaResponse{
Status: status.NewOK(ctx),
TotalBytes: 10,
}, nil)
gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything, mock.Anything).Return(&provider.ListStorageSpacesResponse{
Status: status.NewOK(ctx),
StorageSpaces: []*provider.StorageSpace{
{
Id: &provider.StorageSpaceId{OpaqueId: "drive1"},
Root: &provider.ResourceId{SpaceId: "space", OpaqueId: "space"},
},
},
}, nil)
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/users?$expand=drives", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("userID", *user.Id)
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.GetEducationUser(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
data, err := io.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
responseUser := &libregraph.EducationUser{}
err = json.Unmarshal(data, &responseUser)
Expect(err).ToNot(HaveOccurred())
Expect(responseUser.GetId()).To(Equal("user1"))
Expect(len(responseUser.GetDrives())).To(Equal(1))
})
})
Describe("PostEducationUser", func() {
var (
user *libregraph.EducationUser
assertHandleBadAttributes = func(user *libregraph.EducationUser) {
userJson, err := json.Marshal(user)
Expect(err).ToNot(HaveOccurred())
r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/users", bytes.NewBuffer(userJson))
svc.PostEducationUser(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
}
)
BeforeEach(func() {
identity := libregraph.ObjectIdentity{}
identity.SetIssuer("issu.er")
identity.SetIssuerAssignedId("our-user.1")
user = &libregraph.EducationUser{}
user.SetDisplayName("Display Name")
user.SetOnPremisesSamAccountName("user")
user.SetMail("user@example.com")
user.SetAccountEnabled(true)
user.SetIdentities([]libregraph.ObjectIdentity{identity})
user.SetPrimaryRole("student")
})
It("handles invalid bodies", func() {
r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/users?$invalid=true", nil)
svc.PostEducationUser(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("handles missing display names", func() {
user.DisplayName = nil
assertHandleBadAttributes(user)
})
It("handles missing OnPremisesSamAccountName", func() {
user.OnPremisesSamAccountName = nil
assertHandleBadAttributes(user)
user.SetOnPremisesSamAccountName("")
assertHandleBadAttributes(user)
})
It("handles bad Mails", func() {
user.Mail = nil
assertHandleBadAttributes(user)
user.SetMail("not-a-mail-address")
assertHandleBadAttributes(user)
})
It("handles set Ids - they are read-only", func() {
user.SetId("/users/user")
assertHandleBadAttributes(user)
})
It("creates a user", func() {
roleService.On("AssignRoleToUser", mock.Anything, mock.Anything).Return(&settingssvc.AssignRoleToUserResponse{}, nil)
identityEducationBackend.On("CreateEducationUser", mock.Anything, mock.Anything).Return(func(ctx context.Context, user libregraph.EducationUser) *libregraph.EducationUser {
user.SetId("/users/user")
return &user
}, nil)
userJson, err := json.Marshal(user)
Expect(err).ToNot(HaveOccurred())
r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/users", bytes.NewBuffer(userJson))
r = r.WithContext(revactx.ContextSetUser(ctx, currentUser))
svc.PostEducationUser(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
})
})
Describe("DeleteEducationUser", func() {
It("handles missing userids", func() {
r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/education/users/{userid}", nil)
svc.DeleteEducationUser(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("prevents a user from deleting themselves", func() {
lu := libregraph.EducationUser{}
lu.SetId(currentUser.Id.OpaqueId)
identityEducationBackend.On("GetEducationUser", mock.Anything, mock.Anything, mock.Anything).Return(&lu, nil)
r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/education/users/{userid}", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("userID", currentUser.Id.OpaqueId)
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.DeleteEducationUser(rr, r)
Expect(rr.Code).To(Equal(http.StatusForbidden))
})
It("deletes a user from deleting themselves", func() {
otheruser := &userv1beta1.User{
Id: &userv1beta1.UserId{
OpaqueId: "otheruser",
},
}
lu := libregraph.EducationUser{}
lu.SetId(otheruser.Id.OpaqueId)
identityEducationBackend.On("GetEducationUser", mock.Anything, mock.Anything, mock.Anything).Return(&lu, nil)
identityEducationBackend.On("DeleteEducationUser", mock.Anything, mock.Anything).Return(nil)
gatewayClient.On("DeleteStorageSpace", mock.Anything, mock.Anything).Return(&provider.DeleteStorageSpaceResponse{
Status: status.NewOK(ctx),
}, nil)
gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything, mock.Anything).Return(&provider.ListStorageSpacesResponse{
Status: status.NewOK(ctx),
StorageSpaces: []*provider.StorageSpace{
{
Opaque: &typesv1beta1.Opaque{},
Id: &provider.StorageSpaceId{OpaqueId: "drive1"},
Root: &provider.ResourceId{SpaceId: "space", OpaqueId: "space"},
SpaceType: "personal",
Owner: otheruser,
},
},
}, nil)
r := httptest.NewRequest(http.MethodDelete, "/graph/v1.0/education/users/{userid}", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("userID", lu.GetId())
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.DeleteEducationUser(rr, r)
Expect(rr.Code).To(Equal(http.StatusNoContent))
gatewayClient.AssertNumberOfCalls(GinkgoT(), "DeleteStorageSpace", 2) // 2 calls for the home space. first trash, then purge
})
})
Describe("PatchEducationUser", func() {
var (
user *libregraph.EducationUser
)
BeforeEach(func() {
user = &libregraph.EducationUser{}
user.SetDisplayName("Display Name")
user.SetOnPremisesSamAccountName("user")
user.SetMail("user@example.com")
user.SetId("/users/user")
identityEducationBackend.On("GetEducationUser", mock.Anything, mock.Anything, mock.Anything).Return(&user, nil)
})
It("handles missing userids", func() {
r := httptest.NewRequest(http.MethodPatch, "/graph/v1.0/education/users/{userid}", nil)
svc.PatchEducationUser(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("handles invalid bodies", func() {
r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/users?$invalid=true", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("userID", user.GetId())
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.PatchEducationUser(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("handles invalid email", func() {
user.SetMail("invalid")
data, err := json.Marshal(user)
Expect(err).ToNot(HaveOccurred())
r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/users?$invalid=true", bytes.NewBuffer(data))
rctx := chi.NewRouteContext()
rctx.URLParams.Add("userID", user.GetId())
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.PatchEducationUser(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
})
It("updates attributes", func() {
identityEducationBackend.On("UpdateEducationUser", mock.Anything, user.GetId(), mock.Anything).Return(user, nil)
user.SetDisplayName("New Display Name")
data, err := json.Marshal(user)
Expect(err).ToNot(HaveOccurred())
r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/education/users?$invalid=true", bytes.NewBuffer(data))
rctx := chi.NewRouteContext()
rctx.URLParams.Add("userID", user.GetId())
r = r.WithContext(context.WithValue(revactx.ContextSetUser(ctx, currentUser), chi.RouteCtxKey, rctx))
svc.PatchEducationUser(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
data, err = io.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
updatedUser := libregraph.EducationUser{}
err = json.Unmarshal(data, &updatedUser)
Expect(err).ToNot(HaveOccurred())
Expect(updatedUser.GetDisplayName()).To(Equal("New Display Name"))
})
})
})
+2 -1
View File
@@ -246,6 +246,7 @@ func (g Graph) DeleteGroup(w http.ResponseWriter, r *http.Request) {
render.NoContent(w, r)
}
// GetGroupMembers implements the Service interface.
func (g Graph) GetGroupMembers(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Msg("calling get group members")
@@ -408,7 +409,7 @@ func sortGroups(req *godata.GoDataRequest, groups []*libregraph.Group) ([]*libre
return groups, nil
}
switch req.Query.OrderBy.OrderByItems[0].Field.Value {
case "displayName":
case displayNameAttr:
sorter = groupsByDisplayName{groups}
default:
return nil, fmt.Errorf("we do not support <%s> as a order parameter", req.Query.OrderBy.OrderByItems[0].Field.Value)
@@ -139,6 +139,31 @@ func (i instrument) DeleteSchoolMember(w http.ResponseWriter, r *http.Request) {
i.next.DeleteSchoolMember(w, r)
}
// GetEducationUsers implements the Service interface.
func (i instrument) GetEducationUsers(w http.ResponseWriter, r *http.Request) {
i.next.GetEducationUsers(w, r)
}
// GetEducationUser implements the Service interface.
func (i instrument) GetEducationUser(w http.ResponseWriter, r *http.Request) {
i.next.GetEducationUser(w, r)
}
// PostEducationUser implements the Service interface.
func (i instrument) PostEducationUser(w http.ResponseWriter, r *http.Request) {
i.next.PostEducationUser(w, r)
}
// DeleteEducationUser implements the Service interface.
func (i instrument) DeleteEducationUser(w http.ResponseWriter, r *http.Request) {
i.next.DeleteEducationUser(w, r)
}
// PatchEducationUser implements the Service interface.
func (i instrument) PatchEducationUser(w http.ResponseWriter, r *http.Request) {
i.next.PatchEducationUser(w, r)
}
// GetDrives implements the Service interface.
func (i instrument) GetDrives(w http.ResponseWriter, r *http.Request) {
i.next.GetDrives(w, r)
+25
View File
@@ -139,6 +139,31 @@ func (l logging) DeleteSchoolMember(w http.ResponseWriter, r *http.Request) {
l.next.DeleteSchoolMember(w, r)
}
// GetEducationUsers implements the Service interface.
func (l logging) GetEducationUsers(w http.ResponseWriter, r *http.Request) {
l.next.GetEducationUsers(w, r)
}
// GetEducationUser implements the Service interface.
func (l logging) GetEducationUser(w http.ResponseWriter, r *http.Request) {
l.next.GetEducationUser(w, r)
}
// PostEducationUser implements the Service interface.
func (l logging) PostEducationUser(w http.ResponseWriter, r *http.Request) {
l.next.PostEducationUser(w, r)
}
// DeleteEducationUser implements the Service interface.
func (l logging) DeleteEducationUser(w http.ResponseWriter, r *http.Request) {
l.next.DeleteEducationUser(w, r)
}
// PatchEducationUser implements the Service interface.
func (l logging) PatchEducationUser(w http.ResponseWriter, r *http.Request) {
l.next.PatchEducationUser(w, r)
}
// GetDrives implements the Service interface.
func (l logging) GetDrives(w http.ResponseWriter, r *http.Request) {
l.next.GetDrives(w, r)
+38
View File
@@ -119,3 +119,41 @@ type schoolsByDisplayName struct {
func (g schoolsByDisplayName) Less(i, j int) bool {
return strings.ToLower(g.schoolSlice[i].GetDisplayName()) < strings.ToLower(g.schoolSlice[j].GetDisplayName())
}
type educationUserSlice []*libregraph.EducationUser
// Len is the number of elements in the collection.
func (d educationUserSlice) Len() int { return len(d) }
// Swap swaps the elements with indexes i and j.
func (d educationUserSlice) Swap(i, j int) { d[i], d[j] = d[j], d[i] }
type educationUsersByDisplayName struct {
educationUserSlice
}
type educationUsersByMail struct {
educationUserSlice
}
type educationUsersByOnPremisesSamAccountName struct {
educationUserSlice
}
// Less reports whether the element with index i
// must sort before the element with index j.
func (u educationUsersByDisplayName) Less(i, j int) bool {
return strings.ToLower(u.educationUserSlice[i].GetDisplayName()) < strings.ToLower(u.educationUserSlice[j].GetDisplayName())
}
// Less reports whether the element with index i
// must sort before the element with index j.
func (u educationUsersByMail) Less(i, j int) bool {
return strings.ToLower(u.educationUserSlice[i].GetMail()) < strings.ToLower(u.educationUserSlice[j].GetMail())
}
// Less reports whether the element with index i
// must sort before the element with index j.
func (u educationUsersByOnPremisesSamAccountName) Less(i, j int) bool {
return strings.ToLower(u.educationUserSlice[i].GetOnPremisesSamAccountName()) < strings.ToLower(u.educationUserSlice[j].GetOnPremisesSamAccountName())
}
+13 -7
View File
@@ -63,12 +63,6 @@ func (g Graph) PostSchool(w http.ResponseWriter, r *http.Request) {
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.
@@ -78,6 +72,18 @@ func (g Graph) PostSchool(w http.ResponseWriter, r *http.Request) {
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
}
if _, ok := school.GetSchoolNumberOk(); !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
}
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())
@@ -391,7 +397,7 @@ func sortSchools(req *godata.GoDataRequest, schools []*libregraph.EducationSchoo
return schools, nil
}
switch req.Query.OrderBy.OrderByItems[0].Field.Value {
case "displayName":
case displayNameAttr:
sorter = schoolsByDisplayName{schools}
default:
return nil, fmt.Errorf("we do not support <%s> as a order parameter", req.Query.OrderBy.OrderByItems[0].Field.Value)
+10 -13
View File
@@ -5,7 +5,7 @@ import (
"context"
"encoding/json"
"errors"
"io/ioutil"
"io"
"net/http"
"net/http/httptest"
@@ -37,7 +37,6 @@ var _ = Describe("Schools", func() {
ctx context.Context
cfg *config.Config
gatewayClient *mocks.GatewayClient
eventsPublisher mocks.Publisher
identityEducationBackend *identitymocks.EducationBackend
rr *httptest.ResponseRecorder
@@ -51,7 +50,6 @@ var _ = Describe("Schools", func() {
)
BeforeEach(func() {
eventsPublisher.On("Publish", mock.Anything, mock.Anything, mock.Anything).Return(nil)
identityEducationBackend = &identitymocks.EducationBackend{}
gatewayClient = &mocks.GatewayClient{}
@@ -71,7 +69,6 @@ var _ = Describe("Schools", func() {
svc = service.NewService(
service.Config(cfg),
service.WithGatewayClient(gatewayClient),
service.EventsPublisher(&eventsPublisher),
service.WithIdentityEducationBackend(identityEducationBackend),
)
})
@@ -91,7 +88,7 @@ var _ = Describe("Schools", func() {
svc.GetSchools(rr, r)
Expect(rr.Code).To(Equal(http.StatusBadRequest))
data, err := ioutil.ReadAll(rr.Body)
data, err := io.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
odataerr := libregraph.OdataError{}
@@ -106,7 +103,7 @@ var _ = Describe("Schools", func() {
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)
data, err := io.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
odataerr := libregraph.OdataError{}
@@ -122,7 +119,7 @@ var _ = Describe("Schools", func() {
svc.GetSchools(rr, r)
Expect(rr.Code).To(Equal(http.StatusInternalServerError))
data, err := ioutil.ReadAll(rr.Body)
data, err := io.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
odataerr := libregraph.OdataError{}
@@ -138,7 +135,7 @@ var _ = Describe("Schools", func() {
svc.GetSchools(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
data, err := ioutil.ReadAll(rr.Body)
data, err := io.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
res := service.ListResponse{}
@@ -154,7 +151,7 @@ var _ = Describe("Schools", func() {
svc.GetSchools(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
data, err := ioutil.ReadAll(rr.Body)
data, err := io.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
res := schoolList{}
@@ -265,6 +262,7 @@ var _ = Describe("Schools", func() {
It("creates the school", func() {
newSchool = libregraph.NewEducationSchool()
newSchool.SetDisplayName("New School")
newSchool.SetSchoolNumber("0034")
newSchoolJson, err := json.Marshal(newSchool)
Expect(err).ToNot(HaveOccurred())
@@ -346,15 +344,14 @@ var _ = Describe("Schools", func() {
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 := libregraph.NewEducationUser()
user.SetId("user")
identityEducationBackend.On("GetSchoolMembers", mock.Anything, mock.Anything, mock.Anything).Return([]*libregraph.User{user}, nil)
identityEducationBackend.On("GetSchoolMembers", mock.Anything, mock.Anything, mock.Anything).Return([]*libregraph.EducationUser{user}, nil)
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/education/schools/{schoolID}/members", nil)
rctx := chi.NewRouteContext()
@@ -363,7 +360,7 @@ var _ = Describe("Schools", func() {
svc.GetSchoolMembers(rr, r)
Expect(rr.Code).To(Equal(http.StatusOK))
data, err := ioutil.ReadAll(rr.Body)
data, err := io.ReadAll(rr.Body)
Expect(err).ToNot(HaveOccurred())
var members []*libregraph.User
+13 -6
View File
@@ -23,7 +23,8 @@ import (
const (
// HeaderPurge defines the header name for the purge header.
HeaderPurge = "Purge"
HeaderPurge = "Purge"
displayNameAttr = "displayName"
)
// Service defines the service handlers.
@@ -55,6 +56,12 @@ type Service interface {
PostSchoolMember(http.ResponseWriter, *http.Request)
DeleteSchoolMember(http.ResponseWriter, *http.Request)
GetEducationUsers(http.ResponseWriter, *http.Request)
GetEducationUser(http.ResponseWriter, *http.Request)
PostEducationUser(http.ResponseWriter, *http.Request)
DeleteEducationUser(http.ResponseWriter, *http.Request)
PatchEducationUser(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)
@@ -251,12 +258,12 @@ func NewService(opts ...Option) Service {
})
})
r.Route("/users", func(r chi.Router) {
r.Get("/", svc.GetUsers)
r.Post("/", svc.PostUser)
r.Get("/", svc.GetEducationUsers)
r.Post("/", svc.PostEducationUser)
r.Route("/{userID}", func(r chi.Router) {
r.Get("/", svc.GetUser)
r.Delete("/", svc.DeleteUser)
r.Patch("/", svc.PatchUser)
r.Get("/", svc.GetEducationUser)
r.Delete("/", svc.DeleteEducationUser)
r.Patch("/", svc.PatchEducationUser)
})
})
r.Route("/classes", func(r chi.Router) {
+25
View File
@@ -135,6 +135,31 @@ func (t tracing) DeleteSchoolMember(w http.ResponseWriter, r *http.Request) {
t.next.DeleteSchoolMember(w, r)
}
// GetEducationUsers implements the Service interface.
func (t tracing) GetEducationUsers(w http.ResponseWriter, r *http.Request) {
t.next.GetEducationUsers(w, r)
}
// GetEducationUser implements the Service interface.
func (t tracing) GetEducationUser(w http.ResponseWriter, r *http.Request) {
t.next.GetEducationUser(w, r)
}
// PostEducationUser implements the Service interface.
func (t tracing) PostEducationUser(w http.ResponseWriter, r *http.Request) {
t.next.PostEducationUser(w, r)
}
// DeleteEducationUser implements the Service interface.
func (t tracing) DeleteEducationUser(w http.ResponseWriter, r *http.Request) {
t.next.DeleteEducationUser(w, r)
}
// PatchEducationUser implements the Service interface.
func (t tracing) PatchEducationUser(w http.ResponseWriter, r *http.Request) {
t.next.PatchEducationUser(w, r)
}
// GetDrives implements the Service interface.
func (t tracing) GetDrives(w http.ResponseWriter, r *http.Request) {
t.next.GetDrives(w, r)
+3 -1
View File
@@ -97,6 +97,7 @@ func (g Graph) GetUsers(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, &ListResponse{Value: users})
}
// PostUser implements the Service interface.
func (g Graph) PostUser(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Interface("body", r.Body).Msg("calling create user")
@@ -278,6 +279,7 @@ func (g Graph) GetUser(w http.ResponseWriter, r *http.Request) {
render.JSON(w, r, user)
}
// DeleteUser implements the Service interface.
func (g Graph) DeleteUser(w http.ResponseWriter, r *http.Request) {
logger := g.logger.SubloggerWithRequestID(r.Context())
logger.Info().Msg("calling delete user")
@@ -489,7 +491,7 @@ func sortUsers(req *godata.GoDataRequest, users []*libregraph.User) ([]*libregra
return users, nil
}
switch req.Query.OrderBy.OrderByItems[0].Field.Value {
case "displayName":
case displayNameAttr:
sorter = usersByDisplayName{users}
case "mail":
sorter = usersByMail{users}