From b4d3745d74032dc59240e054c634da71eecfa9b3 Mon Sep 17 00:00:00 2001 From: Xenia N Date: Wed, 2 Oct 2024 18:53:41 +0500 Subject: [PATCH 1/2] hotfix(codeql): Remove cronjob trigger (#1108) --- .github/workflows/codeql-analysis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index d9e3e7f90..c8cfb51ea 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -5,8 +5,6 @@ on: branches: [ master ] pull_request: branches: [ master ] - schedule: - - cron: '33 20 * * 2' jobs: analyze: From 1fd8c2fa8084aacc242671d7d0d4fc55b722b13a Mon Sep 17 00:00:00 2001 From: Danil Tarasov <87192879+almostinf@users.noreply.github.com> Date: Thu, 3 Oct 2024 14:53:44 +0300 Subject: [PATCH 2/2] feat(api): contact template validation (#1100) --- api/controller/contact.go | 30 +++++ api/controller/contact_test.go | 171 +++++++++++++++++++++++----- api/handler/contact.go | 23 +++- api/handler/contact_test.go | 199 ++++++++++++++++++++++++++------- api/handler/handler.go | 9 +- api/handler/team_contact.go | 10 +- api/middleware/context.go | 10 ++ api/middleware/middleware.go | 6 + cmd/api/config.go | 17 +++ cmd/api/config_test.go | 35 ++++++ cmd/api/main.go | 5 + 11 files changed, 442 insertions(+), 73 deletions(-) diff --git a/api/controller/contact.go b/api/controller/contact.go index 4874624be..e1a7573de 100644 --- a/api/controller/contact.go +++ b/api/controller/contact.go @@ -4,6 +4,7 @@ import ( "bytes" "errors" "fmt" + "regexp" "time" "github.com/go-graphite/carbonapi/date" @@ -53,6 +54,7 @@ func GetContactById(database moira.Database, contactID string) (*dto.Contact, *a func CreateContact( dataBase moira.Database, auth *api.Authorization, + contactsTemplate []api.WebContact, contact *dto.Contact, userLogin, teamID string, @@ -74,6 +76,7 @@ func CreateContact( Type: contact.Type, Value: contact.Value, } + if contactData.ID == "" { uuid4, err := uuid.NewV4() if err != nil { @@ -90,12 +93,18 @@ func CreateContact( } } + if err := validateContact(contactsTemplate, contactData); err != nil { + return api.ErrorInvalidRequest(err) + } + if err := dataBase.SaveContact(&contactData); err != nil { return api.ErrorInternalServer(err) } + contact.User = contactData.User contact.ID = contactData.ID contact.TeamID = contactData.Team + return nil } @@ -103,6 +112,7 @@ func CreateContact( func UpdateContact( dataBase moira.Database, auth *api.Authorization, + contactsTemplate []api.WebContact, contactDTO dto.Contact, contactData moira.ContactData, ) (dto.Contact, *api.ErrorResponse) { @@ -119,6 +129,10 @@ func UpdateContact( contactData.Team = contactDTO.TeamID } + if err := validateContact(contactsTemplate, contactData); err != nil { + return contactDTO, api.ErrorInvalidRequest(err) + } + if err := dataBase.SaveContact(&contactData); err != nil { return contactDTO, api.ErrorInternalServer(err) } @@ -265,3 +279,19 @@ func isAllowedToUseContactType(auth *api.Authorization, userLogin string, contac return isAllowedContactType || isAdmin || !isAuthEnabled } + +func validateContact(contactsTemplate []api.WebContact, contact moira.ContactData) error { + var validationPattern string + for _, contactTemplate := range contactsTemplate { + if contactTemplate.ContactType == contact.Type { + validationPattern = contactTemplate.ValidationRegex + break + } + } + + if matched, err := regexp.MatchString(validationPattern, contact.Value); !matched || err != nil { + return fmt.Errorf("contact value doesn't match regex: '%s'", validationPattern) + } + + return nil +} diff --git a/api/controller/contact_test.go b/api/controller/contact_test.go index 8c398b528..c0eb92cc8 100644 --- a/api/controller/contact_test.go +++ b/api/controller/contact_test.go @@ -127,6 +127,13 @@ func TestCreateContact(t *testing.T) { }, } + contactsTemplate := []api.WebContact{ + { + ContactType: contactType, + ValidationRegex: "@mail.com", + }, + } + Convey("Create for user", t, func() { Convey("Success", func() { contact := &dto.Contact{ @@ -134,7 +141,7 @@ func TestCreateContact(t *testing.T) { Type: contactType, } dataBase.EXPECT().SaveContact(gomock.Any()).Return(nil) - err := CreateContact(dataBase, auth, contact, userLogin, "") + err := CreateContact(dataBase, auth, contactsTemplate, contact, userLogin, "") So(err, ShouldBeNil) So(contact.User, ShouldResemble, userLogin) }) @@ -153,7 +160,7 @@ func TestCreateContact(t *testing.T) { } dataBase.EXPECT().GetContact(contact.ID).Return(moira.ContactData{}, database.ErrNil) dataBase.EXPECT().SaveContact(&expectedContact).Return(nil) - err := CreateContact(dataBase, auth, &contact, userLogin, "") + err := CreateContact(dataBase, auth, contactsTemplate, &contact, userLogin, "") So(err, ShouldBeNil) So(contact.User, ShouldResemble, userLogin) So(contact.ID, ShouldResemble, contact.ID) @@ -166,7 +173,7 @@ func TestCreateContact(t *testing.T) { Type: contactType, } dataBase.EXPECT().GetContact(contact.ID).Return(moira.ContactData{}, nil) - err := CreateContact(dataBase, auth, contact, userLogin, "") + err := CreateContact(dataBase, auth, contactsTemplate, contact, userLogin, "") So(err, ShouldResemble, api.ErrorInvalidRequest(fmt.Errorf("contact with this ID already exists"))) }) @@ -178,10 +185,34 @@ func TestCreateContact(t *testing.T) { } err := fmt.Errorf("oooops! Can not write contact") dataBase.EXPECT().GetContact(contact.ID).Return(moira.ContactData{}, err) - expected := CreateContact(dataBase, auth, contact, userLogin, "") + expected := CreateContact(dataBase, auth, contactsTemplate, contact, userLogin, "") So(expected, ShouldResemble, api.ErrorInternalServer(err)) }) + contactsTemplate = []api.WebContact{ + { + ContactType: contactType, + ValidationRegex: "@yandex.ru", + }, + } + + Convey("Error invalid contact value", func() { + contact := &dto.Contact{ + Value: contactValue, + Type: contactType, + } + expectedErr := api.ErrorInvalidRequest(fmt.Errorf("contact value doesn't match regex: '%s'", "@yandex.ru")) + err := CreateContact(dataBase, auth, contactsTemplate, contact, userLogin, "") + So(err, ShouldResemble, expectedErr) + }) + + contactsTemplate = []api.WebContact{ + { + ContactType: contactType, + ValidationRegex: "@mail.com", + }, + } + Convey("Error create now allowed contact", func() { contact := &dto.Contact{ ID: uuid.Must(uuid.NewV4()).String(), @@ -189,7 +220,7 @@ func TestCreateContact(t *testing.T) { Type: notAllowedContactType, } expectedErr := api.ErrorInvalidRequest(ErrNotAllowedContactType) - err := CreateContact(dataBase, auth, contact, userLogin, "") + err := CreateContact(dataBase, auth, contactsTemplate, contact, userLogin, "") So(err, ShouldResemble, expectedErr) }) @@ -215,7 +246,7 @@ func TestCreateContact(t *testing.T) { dataBase.EXPECT().GetContact(contact.ID).Return(moira.ContactData{}, database.ErrNil) dataBase.EXPECT().SaveContact(&expectedContact).Return(nil) - err := CreateContact(dataBase, auth, contact, userLogin, "") + err := CreateContact(dataBase, auth, contactsTemplate, contact, userLogin, "") So(err, ShouldBeNil) }) @@ -226,7 +257,7 @@ func TestCreateContact(t *testing.T) { } err := fmt.Errorf("oooops! Can not write contact") dataBase.EXPECT().SaveContact(gomock.Any()).Return(err) - expected := CreateContact(dataBase, auth, contact, userLogin, "") + expected := CreateContact(dataBase, auth, contactsTemplate, contact, userLogin, "") So(expected, ShouldResemble, &api.ErrorResponse{ ErrorText: err.Error(), HTTPStatusCode: http.StatusInternalServerError, @@ -243,7 +274,7 @@ func TestCreateContact(t *testing.T) { Type: contactType, } dataBase.EXPECT().SaveContact(gomock.Any()).Return(nil) - err := CreateContact(dataBase, auth, contact, "", teamID) + err := CreateContact(dataBase, auth, contactsTemplate, contact, "", teamID) So(err, ShouldBeNil) So(contact.TeamID, ShouldResemble, teamID) }) @@ -262,7 +293,7 @@ func TestCreateContact(t *testing.T) { } dataBase.EXPECT().GetContact(contact.ID).Return(moira.ContactData{}, database.ErrNil) dataBase.EXPECT().SaveContact(&expectedContact).Return(nil) - err := CreateContact(dataBase, auth, &contact, "", teamID) + err := CreateContact(dataBase, auth, contactsTemplate, &contact, "", teamID) So(err, ShouldBeNil) So(contact.TeamID, ShouldResemble, teamID) So(contact.ID, ShouldResemble, contact.ID) @@ -284,7 +315,7 @@ func TestCreateContact(t *testing.T) { } dataBase.EXPECT().GetContact(contact.ID).Return(moira.ContactData{}, database.ErrNil) dataBase.EXPECT().SaveContact(&expectedContact).Return(nil) - err := CreateContact(dataBase, auth, &contact, "", teamID) + err := CreateContact(dataBase, auth, contactsTemplate, &contact, "", teamID) So(err, ShouldBeNil) So(contact.TeamID, ShouldResemble, teamID) So(contact.Name, ShouldResemble, expectedContact.Name) @@ -297,7 +328,7 @@ func TestCreateContact(t *testing.T) { Type: contactType, } dataBase.EXPECT().GetContact(contact.ID).Return(moira.ContactData{}, nil) - err := CreateContact(dataBase, auth, contact, "", teamID) + err := CreateContact(dataBase, auth, contactsTemplate, contact, "", teamID) So(err, ShouldResemble, api.ErrorInvalidRequest(fmt.Errorf("contact with this ID already exists"))) }) @@ -309,7 +340,7 @@ func TestCreateContact(t *testing.T) { } err := fmt.Errorf("oooops! Can not write contact") dataBase.EXPECT().GetContact(contact.ID).Return(moira.ContactData{}, err) - expected := CreateContact(dataBase, auth, contact, "", teamID) + expected := CreateContact(dataBase, auth, contactsTemplate, contact, "", teamID) So(expected, ShouldResemble, api.ErrorInternalServer(err)) }) @@ -320,7 +351,7 @@ func TestCreateContact(t *testing.T) { Type: notAllowedContactType, } expectedErr := api.ErrorInvalidRequest(ErrNotAllowedContactType) - err := CreateContact(dataBase, auth, contact, "", teamID) + err := CreateContact(dataBase, auth, contactsTemplate, contact, "", teamID) So(err, ShouldResemble, expectedErr) }) @@ -346,7 +377,7 @@ func TestCreateContact(t *testing.T) { dataBase.EXPECT().GetContact(contact.ID).Return(moira.ContactData{}, database.ErrNil) dataBase.EXPECT().SaveContact(&expectedContact).Return(nil) - err := CreateContact(dataBase, auth, contact, "", teamID) + err := CreateContact(dataBase, auth, contactsTemplate, contact, "", teamID) So(err, ShouldBeNil) }) @@ -357,7 +388,7 @@ func TestCreateContact(t *testing.T) { } err := fmt.Errorf("oooops! Can not write contact") dataBase.EXPECT().SaveContact(gomock.Any()).Return(err) - expected := CreateContact(dataBase, auth, contact, "", teamID) + expected := CreateContact(dataBase, auth, contactsTemplate, contact, "", teamID) So(expected, ShouldResemble, &api.ErrorResponse{ ErrorText: err.Error(), HTTPStatusCode: http.StatusInternalServerError, @@ -389,6 +420,8 @@ func TestAdminsCreatesContact(t *testing.T) { }, } + contactsTemplate := []api.WebContact{} + Convey("Create for user", t, func() { Convey("The same user", func() { contact := &dto.Contact{ @@ -397,7 +430,7 @@ func TestAdminsCreatesContact(t *testing.T) { User: userLogin, } dataBase.EXPECT().SaveContact(gomock.Any()).Return(nil) - err := CreateContact(dataBase, auth, contact, userLogin, "") + err := CreateContact(dataBase, auth, contactsTemplate, contact, userLogin, "") So(err, ShouldBeNil) So(contact.User, ShouldResemble, userLogin) }) @@ -409,7 +442,7 @@ func TestAdminsCreatesContact(t *testing.T) { User: adminLogin, } dataBase.EXPECT().SaveContact(gomock.Any()).Return(nil) - err := CreateContact(dataBase, auth, contact, adminLogin, "") + err := CreateContact(dataBase, auth, contactsTemplate, contact, adminLogin, "") So(err, ShouldBeNil) So(contact.User, ShouldResemble, adminLogin) }) @@ -421,7 +454,7 @@ func TestAdminsCreatesContact(t *testing.T) { User: adminLogin, } dataBase.EXPECT().SaveContact(gomock.Any()).Return(nil) - err := CreateContact(dataBase, auth, contact, userLogin, "") + err := CreateContact(dataBase, auth, contactsTemplate, contact, userLogin, "") So(err, ShouldBeNil) So(contact.User, ShouldResemble, userLogin) }) @@ -433,7 +466,7 @@ func TestAdminsCreatesContact(t *testing.T) { User: userLogin, } dataBase.EXPECT().SaveContact(gomock.Any()).Return(nil) - err := CreateContact(dataBase, auth, contact, adminLogin, "") + err := CreateContact(dataBase, auth, contactsTemplate, contact, adminLogin, "") So(err, ShouldBeNil) So(contact.User, ShouldResemble, userLogin) }) @@ -445,7 +478,7 @@ func TestAdminsCreatesContact(t *testing.T) { User: userLogin, } dataBase.EXPECT().SaveContact(gomock.Any()).Return(nil) - err := CreateContact(dataBase, auth, contact, adminLogin, "") + err := CreateContact(dataBase, auth, contactsTemplate, contact, adminLogin, "") So(err, ShouldBeNil) So(contact.User, ShouldResemble, userLogin) }) @@ -472,6 +505,13 @@ func TestUpdateContact(t *testing.T) { }, } + contactsTemplate := []api.WebContact{ + { + ContactType: contactType, + ValidationRegex: "@mail.com", + }, + } + Convey("User update", t, func() { Convey("Success", func() { contactDTO := dto.Contact{ @@ -488,7 +528,7 @@ func TestUpdateContact(t *testing.T) { User: userLogin, } dataBase.EXPECT().SaveContact(&contact).Return(nil) - expectedContact, err := UpdateContact(dataBase, auth, contactDTO, moira.ContactData{ID: contactID, User: userLogin}) + expectedContact, err := UpdateContact(dataBase, auth, contactsTemplate, contactDTO, moira.ContactData{ID: contactID, User: userLogin}) So(err, ShouldBeNil) So(expectedContact.User, ShouldResemble, userLogin) So(expectedContact.ID, ShouldResemble, contactID) @@ -512,7 +552,7 @@ func TestUpdateContact(t *testing.T) { User: newUser, } dataBase.EXPECT().SaveContact(&contact).Return(nil) - expectedContact, err := UpdateContact(dataBase, auth, contactDTO, moira.ContactData{ID: contactID, User: userLogin}) + expectedContact, err := UpdateContact(dataBase, auth, contactsTemplate, contactDTO, moira.ContactData{ID: contactID, User: userLogin}) So(err, ShouldBeNil) So(expectedContact.User, ShouldResemble, newUser) So(expectedContact.ID, ShouldResemble, contactID) @@ -526,13 +566,41 @@ func TestUpdateContact(t *testing.T) { } expectedErr := api.ErrorInvalidRequest(ErrNotAllowedContactType) contactID := uuid.Must(uuid.NewV4()).String() - expectedContact, err := UpdateContact(dataBase, auth, contactDTO, moira.ContactData{ID: contactID, User: userLogin}) + expectedContact, err := UpdateContact(dataBase, auth, contactsTemplate, contactDTO, moira.ContactData{ID: contactID, User: userLogin}) So(err, ShouldResemble, expectedErr) So(expectedContact.User, ShouldResemble, contactDTO.User) So(expectedContact.ID, ShouldResemble, contactDTO.ID) So(expectedContact.Name, ShouldResemble, contactDTO.Name) }) + contactsTemplate = []api.WebContact{ + { + ContactType: contactType, + ValidationRegex: "@yandex.ru", + }, + } + + Convey("Error invalid contact value", func() { + contactDTO := dto.Contact{ + Value: contactValue, + Type: contactType, + } + expectedErr := api.ErrorInvalidRequest(fmt.Errorf("contact value doesn't match regex: '%s'", "@yandex.ru")) + contactID := uuid.Must(uuid.NewV4()).String() + expectedContact, err := UpdateContact(dataBase, auth, contactsTemplate, contactDTO, moira.ContactData{ID: contactID, User: userLogin}) + So(err, ShouldResemble, expectedErr) + So(expectedContact.User, ShouldResemble, contactDTO.User) + So(expectedContact.ID, ShouldResemble, contactDTO.ID) + So(expectedContact.Name, ShouldResemble, contactDTO.Name) + }) + + contactsTemplate = []api.WebContact{ + { + ContactType: contactType, + ValidationRegex: "@mail.com", + }, + } + Convey("Successfully update not allowed contact with disabled auth", func() { auth.Enabled = false defer func() { @@ -554,7 +622,7 @@ func TestUpdateContact(t *testing.T) { } dataBase.EXPECT().SaveContact(&contact).Return(nil) - expectedContact, err := UpdateContact(dataBase, auth, contactDTO, moira.ContactData{ID: contactID, User: userLogin}) + expectedContact, err := UpdateContact(dataBase, auth, contactsTemplate, contactDTO, moira.ContactData{ID: contactID, User: userLogin}) So(err, ShouldBeNil) So(expectedContact.User, ShouldResemble, userLogin) So(expectedContact.ID, ShouldResemble, contactID) @@ -575,7 +643,7 @@ func TestUpdateContact(t *testing.T) { } err := fmt.Errorf("oooops") dataBase.EXPECT().SaveContact(&contact).Return(err) - expectedContact, actual := UpdateContact(dataBase, auth, contactDTO, contact) + expectedContact, actual := UpdateContact(dataBase, auth, contactsTemplate, contactDTO, contact) So(actual, ShouldResemble, api.ErrorInternalServer(err)) So(expectedContact.User, ShouldResemble, contactDTO.User) So(expectedContact.ID, ShouldResemble, contactDTO.ID) @@ -596,7 +664,7 @@ func TestUpdateContact(t *testing.T) { Team: teamID, } dataBase.EXPECT().SaveContact(&contact).Return(nil) - expectedContact, err := UpdateContact(dataBase, auth, contactDTO, moira.ContactData{ID: contactID, Team: teamID}) + expectedContact, err := UpdateContact(dataBase, auth, contactsTemplate, contactDTO, moira.ContactData{ID: contactID, Team: teamID}) So(err, ShouldBeNil) So(expectedContact.TeamID, ShouldResemble, teamID) So(expectedContact.ID, ShouldResemble, contactID) @@ -617,7 +685,7 @@ func TestUpdateContact(t *testing.T) { Team: newTeam, } dataBase.EXPECT().SaveContact(&contact).Return(nil) - expectedContact, err := UpdateContact(dataBase, auth, contactDTO, moira.ContactData{ID: contactID, Team: teamID}) + expectedContact, err := UpdateContact(dataBase, auth, contactsTemplate, contactDTO, moira.ContactData{ID: contactID, Team: teamID}) So(err, ShouldBeNil) So(expectedContact.TeamID, ShouldResemble, newTeam) So(expectedContact.ID, ShouldResemble, contactID) @@ -637,7 +705,7 @@ func TestUpdateContact(t *testing.T) { } err := fmt.Errorf("oooops") dataBase.EXPECT().SaveContact(&contact).Return(err) - expectedContact, actual := UpdateContact(dataBase, auth, contactDTO, contact) + expectedContact, actual := UpdateContact(dataBase, auth, contactsTemplate, contactDTO, contact) So(actual, ShouldResemble, api.ErrorInternalServer(err)) So(expectedContact.TeamID, ShouldResemble, contactDTO.TeamID) So(expectedContact.ID, ShouldResemble, contactDTO.ID) @@ -965,3 +1033,50 @@ func Test_isContactExists(t *testing.T) { }) }) } + +func TestValidateContact(t *testing.T) { + const ( + contactType = "phone" + contactValue = "+79998887766" + ) + + Convey("Test validateContact", t, func() { + contact := moira.ContactData{ + Type: contactType, + Value: contactValue, + } + + Convey("With empty contactsTemplate", func() { + contactsTemplate := []api.WebContact{} + + err := validateContact(contactsTemplate, contact) + So(err, ShouldBeNil) + }) + + Convey("With not matched regex pattern", func() { + contactsTemplate := []api.WebContact{ + { + ContactType: contactType, + ValidationRegex: "^9\\d{9}$", + }, + } + + notMatchedErr := fmt.Errorf("contact value doesn't match regex: '%s'", "^9\\d{9}$") + + err := validateContact(contactsTemplate, contact) + So(err, ShouldResemble, notMatchedErr) + }) + + Convey("With matched regex pattern", func() { + contactsTemplate := []api.WebContact{ + { + ContactType: contactType, + ValidationRegex: `^\+79\d{9}$`, + }, + } + + err := validateContact(contactsTemplate, contact) + So(err, ShouldBeNil) + }) + }) +} diff --git a/api/handler/contact.go b/api/handler/contact.go index ba31d1f2d..9d5647464 100644 --- a/api/handler/contact.go +++ b/api/handler/contact.go @@ -101,8 +101,16 @@ func createNewContact(writer http.ResponseWriter, request *http.Request) { userLogin := middleware.GetLogin(request) auth := middleware.GetAuth(request) - - if err := controller.CreateContact(database, auth, contact, userLogin, contact.TeamID); err != nil { + contactsTemplate := middleware.GetContactsTemplate(request) + + if err := controller.CreateContact( + database, + auth, + contactsTemplate, + contact, + userLogin, + contact.TeamID, + ); err != nil { render.Render(writer, request, err) //nolint return } @@ -155,8 +163,15 @@ func updateContact(writer http.ResponseWriter, request *http.Request) { contactData := request.Context().Value(contactKey).(moira.ContactData) auth := middleware.GetAuth(request) - - contactDTO, err := controller.UpdateContact(database, auth, contactDTO, contactData) + contactsTemplate := middleware.GetContactsTemplate(request) + + contactDTO, err := controller.UpdateContact( + database, + auth, + contactsTemplate, + contactDTO, + contactData, + ) if err != nil { render.Render(writer, request, err) //nolint return diff --git a/api/handler/contact_test.go b/api/handler/contact_test.go index cc2ebc028..4b64b4fde 100644 --- a/api/handler/contact_test.go +++ b/api/handler/contact_test.go @@ -20,13 +20,14 @@ import ( ) const ( - ContactIDKey = "contactID" - ContactKey = "contact" - AuthKey = "auth" - LoginKey = "login" - defaultContact = "testContact" - defaultLogin = "testLogin" - defaultTeamID = "testTeamID" + testContactIDKey = "contactID" + testContactKey = "contact" + testAuthKey = "auth" + testLoginKey = "login" + testContactsTemplateKey = "contactsTemplate" + defaultContact = "testContact" + defaultLogin = "testLogin" + defaultTeamID = "testTeamID" ) func TestGetAllContacts(t *testing.T) { @@ -136,8 +137,8 @@ func TestGetContactById(t *testing.T) { } testRequest := httptest.NewRequest(http.MethodGet, "/contact/"+contactID, nil) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactIDKey, contactID)) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{ID: contactID})) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactIDKey, contactID)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{ID: contactID})) getContactById(responseWriter, testRequest) @@ -165,8 +166,8 @@ func TestGetContactById(t *testing.T) { } testRequest := httptest.NewRequest(http.MethodGet, "/contact/"+contactID, nil) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactIDKey, contactID)) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{ID: contactID})) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactIDKey, contactID)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{ID: contactID})) getContactById(responseWriter, testRequest) @@ -203,6 +204,13 @@ func TestCreateNewContact(t *testing.T) { }, } + contactsTemplate := []api.WebContact{ + { + ContactType: "mail", + ValidationRegex: "@skbkontur.ru", + }, + } + newContactDto := &dto.Contact{ ID: defaultContact, Name: "Mail Alerts", @@ -228,8 +236,9 @@ func TestCreateNewContact(t *testing.T) { database = mockDb testRequest := httptest.NewRequest(http.MethodPut, "/contact", bytes.NewBuffer(jsonContact)) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), LoginKey, login)) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), AuthKey, auth)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testLoginKey, login)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, auth)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate)) testRequest.Header.Add("content-type", "application/json") createNewContact(responseWriter, testRequest) @@ -260,8 +269,9 @@ func TestCreateNewContact(t *testing.T) { database = mockDb testRequest := httptest.NewRequest(http.MethodPut, "/contact", bytes.NewBuffer(jsonContact)) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), LoginKey, login)) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), "auth", auth)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testLoginKey, login)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, auth)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate)) testRequest.Header.Add("content-type", "application/json") createNewContact(responseWriter, testRequest) @@ -301,8 +311,9 @@ func TestCreateNewContact(t *testing.T) { database = mockDb testRequest := httptest.NewRequest(http.MethodPut, "/contact", bytes.NewBuffer(jsonContact)) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), LoginKey, login)) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), "auth", auth)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testLoginKey, login)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, auth)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate)) testRequest.Header.Add("content-type", "application/json") createNewContact(responseWriter, testRequest) @@ -332,8 +343,9 @@ func TestCreateNewContact(t *testing.T) { database = mockDb testRequest := httptest.NewRequest(http.MethodPut, "/contact", bytes.NewBuffer(jsonContact)) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), LoginKey, login)) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), "auth", auth)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testLoginKey, login)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, auth)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate)) testRequest.Header.Add("content-type", "application/json") createNewContact(responseWriter, testRequest) @@ -351,6 +363,52 @@ func TestCreateNewContact(t *testing.T) { So(response.StatusCode, ShouldEqual, http.StatusInternalServerError) }) + Convey("Invalid request when trying to create a new contact with invalid value", func() { + expected := &api.ErrorResponse{ + StatusText: "Invalid request", + ErrorText: "contact value doesn't match regex: '@yandex.ru'", + } + jsonContact, err := json.Marshal(newContactDto) + So(err, ShouldBeNil) + + contactsTemplate = []api.WebContact{ + { + ContactType: "mail", + ValidationRegex: "@yandex.ru", + }, + } + + mockDb.EXPECT().GetContact(newContactDto.ID).Return(moira.ContactData{}, db.ErrNil).Times(1) + database = mockDb + + testRequest := httptest.NewRequest(http.MethodPut, "/contact", bytes.NewBuffer(jsonContact)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testLoginKey, login)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, auth)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate)) + testRequest.Header.Add("content-type", "application/json") + + createNewContact(responseWriter, testRequest) + + response := responseWriter.Result() + defer response.Body.Close() + contentBytes, err := io.ReadAll(response.Body) + So(err, ShouldBeNil) + contents := string(contentBytes) + actual := &api.ErrorResponse{} + err = json.Unmarshal([]byte(contents), actual) + So(err, ShouldBeNil) + + So(actual, ShouldResemble, expected) + So(response.StatusCode, ShouldEqual, http.StatusBadRequest) + }) + + contactsTemplate = []api.WebContact{ + { + ContactType: "mail", + ValidationRegex: "@skbkontur.ru", + }, + } + Convey("Trying to create a contact when both userLogin and teamID specified", func() { newContactDto.TeamID = defaultTeamID defer func() { @@ -365,8 +423,9 @@ func TestCreateNewContact(t *testing.T) { So(err, ShouldBeNil) testRequest := httptest.NewRequest(http.MethodPut, "/contact", bytes.NewBuffer(jsonContact)) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), LoginKey, login)) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), "auth", auth)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testLoginKey, login)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, auth)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate)) testRequest.Header.Add("content-type", "application/json") createNewContact(responseWriter, testRequest) @@ -405,6 +464,13 @@ func TestUpdateContact(t *testing.T) { TeamID: "", } + contactsTemplate := []api.WebContact{ + { + ContactType: "mail", + ValidationRegex: "@skbkontur.ru", + }, + } + Convey("Successful contact updated", func() { jsonContact, err := json.Marshal(updatedContactDto) So(err, ShouldBeNil) @@ -419,15 +485,15 @@ func TestUpdateContact(t *testing.T) { database = mockDb testRequest := httptest.NewRequest(http.MethodPut, "/contact/"+contactID, bytes.NewBuffer(jsonContact)) - - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{ + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{ ID: contactID, Name: updatedContactDto.Name, Type: updatedContactDto.Type, Value: updatedContactDto.Value, User: updatedContactDto.User, })) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), AuthKey, &api.Authorization{ + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, &api.Authorization{ AllowedContactTypes: map[string]struct{}{ updatedContactDto.Type: {}, }, @@ -460,15 +526,15 @@ func TestUpdateContact(t *testing.T) { So(err, ShouldBeNil) testRequest := httptest.NewRequest(http.MethodPut, "/contact/"+contactID, bytes.NewBuffer(jsonContact)) - - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{ + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{ ID: contactID, Name: updatedContactDto.Name, Type: updatedContactDto.Type, Value: updatedContactDto.Value, User: updatedContactDto.User, })) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), AuthKey, &api.Authorization{ + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, &api.Authorization{ AllowedContactTypes: map[string]struct{}{ updatedContactDto.Type: {}, }, @@ -495,6 +561,60 @@ func TestUpdateContact(t *testing.T) { So(response.StatusCode, ShouldEqual, http.StatusBadRequest) }) + Convey("Invalid request when trying to update contact with invalid value", func() { + expected := &api.ErrorResponse{ + StatusText: "Invalid request", + ErrorText: "contact value doesn't match regex: '@yandex.ru'", + } + jsonContact, err := json.Marshal(updatedContactDto) + So(err, ShouldBeNil) + + contactsTemplate = []api.WebContact{ + { + ContactType: "mail", + ValidationRegex: "@yandex.ru", + }, + } + + testRequest := httptest.NewRequest(http.MethodPut, "/contact", bytes.NewBuffer(jsonContact)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{ + ID: contactID, + Name: updatedContactDto.Name, + Type: updatedContactDto.Type, + Value: updatedContactDto.Value, + User: updatedContactDto.User, + })) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, &api.Authorization{ + AllowedContactTypes: map[string]struct{}{ + updatedContactDto.Type: {}, + }, + })) + + testRequest.Header.Add("content-type", "application/json") + + updateContact(responseWriter, testRequest) + + response := responseWriter.Result() + defer response.Body.Close() + contentBytes, err := io.ReadAll(response.Body) + So(err, ShouldBeNil) + contents := string(contentBytes) + actual := &api.ErrorResponse{} + err = json.Unmarshal([]byte(contents), actual) + So(err, ShouldBeNil) + + So(actual, ShouldResemble, expected) + So(response.StatusCode, ShouldEqual, http.StatusBadRequest) + }) + + contactsTemplate = []api.WebContact{ + { + ContactType: "mail", + ValidationRegex: "@skbkontur.ru", + }, + } + Convey("Internal error when trying to update contact", func() { expected := &api.ErrorResponse{ StatusText: "Internal Server Error", @@ -513,13 +633,14 @@ func TestUpdateContact(t *testing.T) { database = mockDb testRequest := httptest.NewRequest(http.MethodPut, "/contact/"+contactID, bytes.NewBuffer(jsonContact)) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{ + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactsTemplateKey, contactsTemplate)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{ ID: contactID, Type: updatedContactDto.Type, Value: updatedContactDto.Value, User: updatedContactDto.User, })) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), AuthKey, &api.Authorization{ + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testAuthKey, &api.Authorization{ AllowedContactTypes: map[string]struct{}{ updatedContactDto.Type: {}, }, @@ -561,7 +682,7 @@ func TestRemoveContact(t *testing.T) { database = mockDb testRequest := httptest.NewRequest(http.MethodDelete, "/contact/"+contactID, nil) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{ + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{ ID: contactID, Type: "mail", Value: "moira@skbkontur.ru", @@ -587,7 +708,7 @@ func TestRemoveContact(t *testing.T) { database = mockDb testRequest := httptest.NewRequest(http.MethodDelete, "/contact/"+contactID, nil) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{ + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{ ID: contactID, Type: "mail", Value: "moira@skbkontur.ru", @@ -614,7 +735,7 @@ func TestRemoveContact(t *testing.T) { database = mockDb testRequest := httptest.NewRequest(http.MethodDelete, "/contact/"+contactID, nil) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{ + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{ ID: contactID, Type: "mail", Value: "moira@skbkontur.ru", @@ -642,7 +763,7 @@ func TestRemoveContact(t *testing.T) { database = mockDb testRequest := httptest.NewRequest(http.MethodDelete, "/contact/"+contactID, nil) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{ + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{ ID: contactID, Type: "mail", Value: "moira@skbkontur.ru", @@ -679,7 +800,7 @@ func TestRemoveContact(t *testing.T) { database = mockDb testRequest := httptest.NewRequest(http.MethodDelete, "/contact/"+contactID, nil) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{ + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{ ID: contactID, Type: "mail", Value: "moira@skbkontur.ru", @@ -718,7 +839,7 @@ func TestRemoveContact(t *testing.T) { database = mockDb testRequest := httptest.NewRequest(http.MethodDelete, "/contact/"+contactID, nil) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{ + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{ ID: contactID, Type: "mail", Value: "moira@skbkontur.ru", @@ -762,7 +883,7 @@ func TestRemoveContact(t *testing.T) { database = mockDb testRequest := httptest.NewRequest(http.MethodDelete, "/contact/"+contactID, nil) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{ + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{ ID: contactID, Type: "mail", Value: "moira@skbkontur.ru", @@ -797,7 +918,7 @@ func TestRemoveContact(t *testing.T) { database = mockDb testRequest := httptest.NewRequest(http.MethodDelete, "/contact/"+contactID, nil) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactKey, moira.ContactData{ + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactKey, moira.ContactData{ ID: contactID, Type: "mail", Value: "moira@skbkontur.ru", @@ -837,7 +958,7 @@ func TestSendTestContactNotification(t *testing.T) { database = mockDb testRequest := httptest.NewRequest(http.MethodPost, "/contact/"+contactID+"/test", nil) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactIDKey, contactID)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactIDKey, contactID)) sendTestContactNotification(responseWriter, testRequest) @@ -861,7 +982,7 @@ func TestSendTestContactNotification(t *testing.T) { database = mockDb testRequest := httptest.NewRequest(http.MethodPost, "/contact/"+contactID+"/test", nil) - testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), ContactIDKey, contactID)) + testRequest = testRequest.WithContext(middleware.SetContextValueForTest(testRequest.Context(), testContactIDKey, contactID)) sendTestContactNotification(responseWriter, testRequest) diff --git a/api/handler/handler.go b/api/handler/handler.go index 27647a8c3..1ac3f953d 100644 --- a/api/handler/handler.go +++ b/api/handler/handler.go @@ -38,6 +38,11 @@ func NewHandler( ) http.Handler { database = db searchIndex = index + var contactsTemplate []api.WebContact + if webConfig != nil { + contactsTemplate = webConfig.Contacts + } + router := chi.NewRouter() router.Use(render.SetContentType(render.ContentTypeJSON)) router.Use(moiramiddle.UserContext) @@ -111,7 +116,9 @@ func NewHandler( router.Route("/subscription", subscription) router.Route("/notification", notification) router.Route("/teams", teams) - router.Route("/contact", func(router chi.Router) { + router.With(moiramiddle.ContactsTemplateContext( + contactsTemplate, + )).Route("/contact", func(router chi.Router) { contact(router) contactEvents(router) }) diff --git a/api/handler/team_contact.go b/api/handler/team_contact.go index 8d515afba..58f19aaaa 100644 --- a/api/handler/team_contact.go +++ b/api/handler/team_contact.go @@ -40,8 +40,16 @@ func createNewTeamContact(writer http.ResponseWriter, request *http.Request) { teamID := middleware.GetTeamID(request) auth := middleware.GetAuth(request) + contactsTemplate := middleware.GetContactsTemplate(request) - if err := controller.CreateContact(database, auth, contact, "", teamID); err != nil { + if err := controller.CreateContact( + database, + auth, + contactsTemplate, + contact, + "", + teamID, + ); err != nil { render.Render(writer, request, err) //nolint:errcheck return } diff --git a/api/middleware/context.go b/api/middleware/context.go index 0d9e26f31..a53a3f1b8 100644 --- a/api/middleware/context.go +++ b/api/middleware/context.go @@ -36,6 +36,16 @@ func SearchIndexContext(searcher moira.Searcher) func(next http.Handler) http.Ha } } +// ContactsTemplateContext sets to requests context contacts template. +func ContactsTemplateContext(contactsTemplate []api.WebContact) func(next http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + ctx := context.WithValue(request.Context(), contactsTemplateKey, contactsTemplate) + next.ServeHTTP(writer, request.WithContext(ctx)) + }) + } +} + // UserContext get x-webauth-user header and sets it in request context, if header is empty sets empty string. func UserContext(next http.Handler) http.Handler { return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { diff --git a/api/middleware/middleware.go b/api/middleware/middleware.go index 80d65066d..d3df8e40d 100644 --- a/api/middleware/middleware.go +++ b/api/middleware/middleware.go @@ -20,6 +20,7 @@ func (key ContextKey) String() string { var ( databaseKey ContextKey = "database" searcherKey ContextKey = "searcher" + contactsTemplateKey ContextKey = "contactsTemplate" triggerIDKey ContextKey = "triggerID" clustersMetricTTLKey ContextKey = "clustersMetricTTL" populateKey ContextKey = "populated" @@ -49,6 +50,11 @@ func GetDatabase(request *http.Request) moira.Database { return request.Context().Value(databaseKey).(moira.Database) } +// GetContactsTemplate gets contacts template from request context. +func GetContactsTemplate(request *http.Request) []api.WebContact { + return request.Context().Value(contactsTemplateKey).([]api.WebContact) +} + // GetLogin gets user login string from request context, which was sets in UserContext middleware. func GetLogin(request *http.Request) string { if request.Context() != nil && request.Context().Value(loginKey) != nil { diff --git a/cmd/api/config.go b/cmd/api/config.go index 4647bb3f1..75fa2ceea 100644 --- a/cmd/api/config.go +++ b/cmd/api/config.go @@ -1,6 +1,8 @@ package main import ( + "fmt" + "regexp" "time" "github.com/moira-alert/moira" @@ -136,6 +138,21 @@ func (auth *authorization) toApiConfig(webConfig *webConfig) api.Authorization { } } +func (config *webConfig) validate() error { + for _, contactTemplate := range config.ContactsTemplate { + validationRegex := contactTemplate.ValidationRegex + if validationRegex == "" { + continue + } + + if _, err := regexp.Compile(validationRegex); err != nil { + return fmt.Errorf("contact template regex error '%s': %w", validationRegex, err) + } + } + + return nil +} + func (config *webConfig) getSettings(isRemoteEnabled bool, remotes cmd.RemotesConfig) *api.WebConfig { webContacts := make([]api.WebContact, 0, len(config.ContactsTemplate)) diff --git a/cmd/api/config_test.go b/cmd/api/config_test.go index 4777e70ea..c55a67d68 100644 --- a/cmd/api/config_test.go +++ b/cmd/api/config_test.go @@ -248,3 +248,38 @@ func Test_webConfig_getSettings(t *testing.T) { }) }) } + +func Test_webConfig_validate(t *testing.T) { + Convey("With empty web config", t, func() { + config := webConfig{} + + err := config.validate() + So(err, ShouldBeNil) + }) + + Convey("With invalid contact template pattern", t, func() { + config := webConfig{ + ContactsTemplate: []webContact{ + { + ValidationRegex: "**", + }, + }, + } + + err := config.validate() + So(err, ShouldNotBeNil) + }) + + Convey("With valid contact template pattern", t, func() { + config := webConfig{ + ContactsTemplate: []webContact{ + { + ValidationRegex: ".*", + }, + }, + } + + err := config.validate() + So(err, ShouldBeNil) + }) +} diff --git a/cmd/api/main.go b/cmd/api/main.go index 9f87d2fd9..6a9f78e7d 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -58,6 +58,11 @@ func main() { os.Exit(1) } + if err = applicationConfig.Web.validate(); err != nil { + fmt.Fprintf(os.Stderr, "Can not configure web config: %s\n", err.Error()) + os.Exit(1) + } + apiConfig := applicationConfig.API.getSettings( applicationConfig.ClustersMetricTTL(), applicationConfig.Web.getFeatureFlags(),