diff --git a/services/graph/pkg/config/config.go b/services/graph/pkg/config/config.go index 90602fbec62..f1603b29288 100644 --- a/services/graph/pkg/config/config.go +++ b/services/graph/pkg/config/config.go @@ -94,6 +94,8 @@ type LDAPEducationConfig struct { SchoolNameAttribute string `yaml:"school_name_attribute" env:"GRAPH_LDAP_SCHOOL_NAME_ATTRIBUTE" desc:"LDAP Attribute to use for the name of a school."` SchoolNumberAttribute string `yaml:"school_number_attribute" env:"GRAPH_LDAP_SCHOOL_NUMBER_ATTRIBUTE" desc:"LDAP Attribute to use for the number of a school."` SchoolIDAttribute string `yaml:"school_id_attribute" env:"GRAPH_LDAP_SCHOOL_ID_ATTRIBUTE" desc:"LDAP Attribute to use as the unique id for schools. This should be a stable globally unique ID like a UUID."` + + SchoolTerminationGraceDays int `yaml:"school_termination_min_grace_days" env:"GRAPH_LDAP_SCHOOL_TERMINATION_MIN_GRACE_DAYS" desc:"When setting a 'terminationDate' for a school, require the date to be at least this number of days in the future."` } type Identity struct { diff --git a/services/graph/pkg/service/v0/educationschools.go b/services/graph/pkg/service/v0/educationschools.go index 1f601246fb6..eb7b10d7e7b 100644 --- a/services/graph/pkg/service/v0/educationschools.go +++ b/services/graph/pkg/service/v0/educationschools.go @@ -80,6 +80,16 @@ func (g Graph) PostEducationSchool(w http.ResponseWriter, r *http.Request) { return } + // validate terminationDate attribute, needs to be "far enough" in the future, terminationDate can be nil (means + // termination date is to be deleted + if terminationDate, ok := school.GetTerminationDateOk(); ok && terminationDate != nil { + err = g.validateTerminationDate(*terminationDate) + if err != nil { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error()) + } + return + } + if school, err = g.identityEducationBackend.CreateEducationSchool(r.Context(), *school); err != nil { logger.Debug().Err(err).Interface("school", school).Msg("could not create school: backend error") errorcode.RenderError(w, r, err) @@ -126,6 +136,16 @@ func (g Graph) PatchEducationSchool(w http.ResponseWriter, r *http.Request) { return } + // validate terminationDate attribute, needs to be "far enough" in the future, terminationDate can be nil (means + // termination date is to be deleted + if terminationDate, ok := school.GetTerminationDateOk(); ok && terminationDate != nil { + err = g.validateTerminationDate(*terminationDate) + if err != nil { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error()) + return + } + } + if school, err = g.identityEducationBackend.UpdateEducationSchool(r.Context(), schoolID, *school); err != nil { logger.Debug().Err(err).Interface("school", school).Msg("could not update school: backend error") errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) @@ -566,6 +586,19 @@ func (g Graph) DeleteEducationSchoolClass(w http.ResponseWriter, r *http.Request render.NoContent(w, r) } +func (g Graph) validateTerminationDate(terminationDate time.Time) error { + if terminationDate.Before(time.Now()) { + return fmt.Errorf("can not set a termination date in the past") + } + graceDays := g.config.Identity.LDAP.EducationConfig.SchoolTerminationGraceDays + if graceDays != 0 { + if terminationDate.Before(time.Now().Add(time.Duration(graceDays) * 24 * time.Hour)) { + return fmt.Errorf("termination needs to be at least %d day(s) in the future", graceDays) + } + } + return nil +} + func sortEducationSchools(req *godata.GoDataRequest, schools []*libregraph.EducationSchool) ([]*libregraph.EducationSchool, error) { if req.Query.OrderBy == nil || len(req.Query.OrderBy.OrderByItems) != 1 { return schools, nil diff --git a/services/graph/pkg/service/v0/educationschools_test.go b/services/graph/pkg/service/v0/educationschools_test.go index 8936ee64350..a4424c9b46b 100644 --- a/services/graph/pkg/service/v0/educationschools_test.go +++ b/services/graph/pkg/service/v0/educationschools_test.go @@ -74,6 +74,7 @@ var _ = Describe("Schools", func() { cfg = defaults.FullDefaultConfig() cfg.Identity.LDAP.CACert = "" // skip the startup checks, we don't use LDAP at all in this tests + cfg.Identity.LDAP.EducationConfig.SchoolTerminationGraceDays = 30 cfg.TokenManager.JWTSecret = "loremipsum" cfg.Commons = &shared.Commons{} cfg.GRPCClientTLS = &shared.GRPCClientTLS{} @@ -294,8 +295,17 @@ var _ = Describe("Schools", func() { schoolUpdate.SetDisplayName("New School Name") schoolUpdateJson, _ := json.Marshal(schoolUpdate) + schoolUpdatePast := libregraph.NewEducationSchool() + schoolUpdatePast.SetTerminationDate(time.Now().Add(-time.Hour * 1)) + schoolUpdatePastJson, _ := json.Marshal(schoolUpdatePast) + schoolUpdateBeforeGrace := libregraph.NewEducationSchool() + schoolUpdateBeforeGrace.SetTerminationDate(time.Now().Add(24 * 10 * time.Hour)) + schoolUpdateBeforeGraceJson, _ := json.Marshal(schoolUpdateBeforeGrace) + schoolUpdatePastGrace := libregraph.NewEducationSchool() + schoolUpdatePastGrace.SetTerminationDate(time.Now().Add(24 * 31 * time.Hour)) + schoolUpdatePastGraceJson, _ := json.Marshal(schoolUpdatePastGrace) BeforeEach(func() { identityEducationBackend.On("UpdateEducationSchool", mock.Anything, mock.Anything, mock.Anything).Return(schoolUpdate, nil) @@ -315,6 +325,9 @@ var _ = Describe("Schools", func() { Entry("handles missing or empty school id", "", bytes.NewBufferString(""), http.StatusBadRequest), Entry("handles malformed school id", "school%id", bytes.NewBuffer(schoolUpdateJson), http.StatusBadRequest), Entry("updates the school", "school-id", bytes.NewBuffer(schoolUpdateJson), http.StatusOK), + Entry("fails to set a termination date in the past", "school-id", bytes.NewBuffer(schoolUpdatePastJson), http.StatusBadRequest), + Entry("fails to set a termination date before grace period", "school-id", bytes.NewBuffer(schoolUpdateBeforeGraceJson), http.StatusBadRequest), + Entry("succeeds to set a termination date past the grace period", "school-id", bytes.NewBuffer(schoolUpdatePastGraceJson), http.StatusOK), ) })