Skip to content

Commit

Permalink
feat(api): Add readonly flag to api (#963)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tetrergeru authored Nov 15, 2023
1 parent 5fc5a29 commit 138341f
Show file tree
Hide file tree
Showing 10 changed files with 273 additions and 35 deletions.
2 changes: 2 additions & 0 deletions api/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ type Config struct {
GraphiteLocalMetricTTL time.Duration
GraphiteRemoteMetricTTL time.Duration
PrometheusRemoteMetricTTL time.Duration
Flags FeatureFlags
}

// WebConfig is container for web ui configuration parameters.
Expand All @@ -33,4 +34,5 @@ type FeatureFlags struct {
IsPlottingDefaultOn bool `json:"isPlottingDefaultOn"`
IsPlottingAvailable bool `json:"isPlottingAvailable"`
IsSubscriptionToAllTagsAvailable bool `json:"isSubscriptionToAllTagsAvailable"`
IsReadonlyEnabled bool `json:"isReadonlyEnabled"`
}
46 changes: 30 additions & 16 deletions api/handler/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,14 @@ const contactKey moiramiddle.ContextKey = "contact"
const subscriptionKey moiramiddle.ContextKey = "subscription"

// NewHandler creates new api handler request uris based on github.com/go-chi/chi
func NewHandler(db moira.Database, log moira.Logger, index moira.Searcher, config *api.Config, metricSourceProvider *metricSource.SourceProvider, webConfigContent []byte) http.Handler {
func NewHandler(
db moira.Database,
log moira.Logger,
index moira.Searcher,
config *api.Config,
metricSourceProvider *metricSource.SourceProvider,
webConfigContent []byte,
) http.Handler {
database = db
searchIndex = index
router := chi.NewRouter()
Expand Down Expand Up @@ -85,23 +92,30 @@ func NewHandler(db moira.Database, log moira.Logger, index moira.Searcher, confi
// @tag.description APIs for interacting with Moira users
router.Route("/api", func(router chi.Router) {
router.Use(moiramiddle.DatabaseContext(database))
router.Get("/config", getWebConfig(webConfigContent))
router.Route("/user", user)
router.With(moiramiddle.Triggers(config.GraphiteLocalMetricTTL, config.GraphiteRemoteMetricTTL, config.PrometheusRemoteMetricTTL)).Route("/trigger", triggers(metricSourceProvider, searchIndex))
router.Route("/tag", tag)
router.Route("/pattern", pattern)
router.Route("/event", event)
router.Route("/subscription", subscription)
router.Route("/notification", notification)
router.Route("/health", health)
router.Route("/teams", teams)
router.Route("/contact", func(router chi.Router) {
contact(router)
contactEvents(router)
router.Route("/", func(router chi.Router) {
router.Use(moiramiddle.ReadOnlyMiddleware(config))
router.Get("/config", getWebConfig(webConfigContent))
router.Route("/user", user)
router.With(moiramiddle.Triggers(
config.GraphiteLocalMetricTTL,
config.GraphiteRemoteMetricTTL,
config.PrometheusRemoteMetricTTL,
)).Route("/trigger", triggers(metricSourceProvider, searchIndex))
router.Route("/tag", tag)
router.Route("/pattern", pattern)
router.Route("/event", event)
router.Route("/subscription", subscription)
router.Route("/notification", notification)
router.Route("/teams", teams)
router.Route("/contact", func(router chi.Router) {
contact(router)
contactEvents(router)
})
router.Get("/swagger/*", httpSwagger.Handler(
httpSwagger.URL("/api/swagger/doc.json"),
))
})
router.Get("/swagger/*", httpSwagger.Handler(
httpSwagger.URL("/api/swagger/doc.json"),
))
})

if config.EnableCORS {
Expand Down
101 changes: 101 additions & 0 deletions api/handler/handler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package handler

import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"

"github.com/golang/mock/gomock"
"github.com/moira-alert/moira/api"
"github.com/moira-alert/moira/api/dto"
"github.com/moira-alert/moira/logging/zerolog_adapter"
mock_moira_alert "github.com/moira-alert/moira/mock/moira-alert"
. "github.com/smartystreets/goconvey/convey"
)

func TestReadonlyMode(t *testing.T) {
Convey("Test readonly mode enabled", t, func() {
mockCtrl := gomock.NewController(t)
defer mockCtrl.Finish()

responseWriter := httptest.NewRecorder()
mockDb := mock_moira_alert.NewMockDatabase(mockCtrl)
database = mockDb

logger, _ := zerolog_adapter.GetLogger("Test")
config := &api.Config{Flags: api.FeatureFlags{IsReadonlyEnabled: true}}
expectedConfig := []byte("Expected config")
handler := NewHandler(mockDb, logger, nil, config, nil, expectedConfig)

Convey("Get notifier health", func() {
mockDb.EXPECT().GetNotifierState().Return("OK", nil).Times(1)

expected := &dto.NotifierState{
State: "OK",
}

testRequest := httptest.NewRequest(http.MethodGet, "/api/health/notifier", nil)

handler.ServeHTTP(responseWriter, testRequest)

response := responseWriter.Result()
defer response.Body.Close()
content, _ := io.ReadAll(response.Body)
actual := &dto.NotifierState{}
err := json.Unmarshal(content, actual)
So(err, ShouldBeNil)

So(actual, ShouldResemble, expected)
So(response.StatusCode, ShouldEqual, http.StatusOK)
})

Convey("Put notifier health", func() {
mockDb.EXPECT().SetNotifierState("OK").Return(nil).Times(1)

state := &dto.NotifierState{
State: "OK",
}

stateBytes, err := json.Marshal(state)
So(err, ShouldBeNil)

testRequest := httptest.NewRequest(http.MethodPut, "/api/health/notifier", bytes.NewReader(stateBytes))

handler.ServeHTTP(responseWriter, testRequest)

response := responseWriter.Result()
defer response.Body.Close()
So(response.StatusCode, ShouldEqual, http.StatusOK)
})

Convey("Put new trigger", func() {
trigger := &dto.Trigger{}
triggerBytes, err := json.Marshal(trigger)
So(err, ShouldBeNil)

testRequest := httptest.NewRequest(http.MethodPut, "/api/trigger", bytes.NewReader(triggerBytes))

handler.ServeHTTP(responseWriter, testRequest)

response := responseWriter.Result()
defer response.Body.Close()
So(response.StatusCode, ShouldEqual, http.StatusForbidden)
})

Convey("Get contact", func() {
testRequest := httptest.NewRequest(http.MethodGet, "/api/config", nil)

handler.ServeHTTP(responseWriter, testRequest)

response := responseWriter.Result()
defer response.Body.Close()
actual, _ := io.ReadAll(response.Body)

So(response.StatusCode, ShouldEqual, http.StatusOK)
So(actual, ShouldResemble, expectedConfig)
})
})
}
23 changes: 11 additions & 12 deletions api/middleware/context_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package middleware_test
package middleware

import (
"io"
"net/http"
"net/http/httptest"
"testing"

"github.com/moira-alert/moira/api/middleware"
. "github.com/smartystreets/goconvey/convey"
)

Expand All @@ -26,7 +25,7 @@ func TestPaginateMiddleware(t *testing.T) {
testRequest := httptest.NewRequest(http.MethodGet, "/test?"+param, nil)
handler := func(w http.ResponseWriter, r *http.Request) {}

middlewareFunc := middleware.Paginate(defaultPage, defaultSize)
middlewareFunc := Paginate(defaultPage, defaultSize)
wrappedHandler := middlewareFunc(http.HandlerFunc(handler))

wrappedHandler.ServeHTTP(responseWriter, testRequest)
Expand All @@ -41,7 +40,7 @@ func TestPaginateMiddleware(t *testing.T) {
testRequest := httptest.NewRequest(http.MethodGet, "/test?p=0%&size=100", nil)
handler := func(w http.ResponseWriter, r *http.Request) {}

middlewareFunc := middleware.Paginate(defaultPage, defaultSize)
middlewareFunc := Paginate(defaultPage, defaultSize)
wrappedHandler := middlewareFunc(http.HandlerFunc(handler))

wrappedHandler.ServeHTTP(responseWriter, testRequest)
Expand Down Expand Up @@ -69,7 +68,7 @@ func TestPagerMiddleware(t *testing.T) {
testRequest := httptest.NewRequest(http.MethodGet, "/test?"+param, nil)
handler := func(w http.ResponseWriter, r *http.Request) {}

middlewareFunc := middleware.Pager(defaultCreatePager, defaultPagerID)
middlewareFunc := Pager(defaultCreatePager, defaultPagerID)
wrappedHandler := middlewareFunc(http.HandlerFunc(handler))

wrappedHandler.ServeHTTP(responseWriter, testRequest)
Expand All @@ -84,7 +83,7 @@ func TestPagerMiddleware(t *testing.T) {
testRequest := httptest.NewRequest(http.MethodGet, "/test?pagerID=test%&createPager=true", nil)
handler := func(w http.ResponseWriter, r *http.Request) {}

middlewareFunc := middleware.Pager(defaultCreatePager, defaultPagerID)
middlewareFunc := Pager(defaultCreatePager, defaultPagerID)
wrappedHandler := middlewareFunc(http.HandlerFunc(handler))

wrappedHandler.ServeHTTP(responseWriter, testRequest)
Expand All @@ -108,7 +107,7 @@ func TestPopulateMiddleware(t *testing.T) {
testRequest := httptest.NewRequest(http.MethodGet, "/test?populated=true", nil)
handler := func(w http.ResponseWriter, r *http.Request) {}

middlewareFunc := middleware.Populate(defaultPopulated)
middlewareFunc := Populate(defaultPopulated)
wrappedHandler := middlewareFunc(http.HandlerFunc(handler))

wrappedHandler.ServeHTTP(responseWriter, testRequest)
Expand All @@ -122,7 +121,7 @@ func TestPopulateMiddleware(t *testing.T) {
testRequest := httptest.NewRequest(http.MethodGet, "/test?populated%=true", nil)
handler := func(w http.ResponseWriter, r *http.Request) {}

middlewareFunc := middleware.Populate(defaultPopulated)
middlewareFunc := Populate(defaultPopulated)
wrappedHandler := middlewareFunc(http.HandlerFunc(handler))

wrappedHandler.ServeHTTP(responseWriter, testRequest)
Expand Down Expand Up @@ -150,7 +149,7 @@ func TestDateRangeMiddleware(t *testing.T) {
testRequest := httptest.NewRequest(http.MethodGet, "/test?"+param, nil)
handler := func(w http.ResponseWriter, r *http.Request) {}

middlewareFunc := middleware.DateRange(defaultFrom, defaultTo)
middlewareFunc := DateRange(defaultFrom, defaultTo)
wrappedHandler := middlewareFunc(http.HandlerFunc(handler))

wrappedHandler.ServeHTTP(responseWriter, testRequest)
Expand All @@ -165,7 +164,7 @@ func TestDateRangeMiddleware(t *testing.T) {
testRequest := httptest.NewRequest(http.MethodGet, "/test?from=-2hours%&to=now", nil)
handler := func(w http.ResponseWriter, r *http.Request) {}

middlewareFunc := middleware.DateRange(defaultFrom, defaultTo)
middlewareFunc := DateRange(defaultFrom, defaultTo)
wrappedHandler := middlewareFunc(http.HandlerFunc(handler))

wrappedHandler.ServeHTTP(responseWriter, testRequest)
Expand All @@ -189,7 +188,7 @@ func TestTargetNameMiddleware(t *testing.T) {
testRequest := httptest.NewRequest(http.MethodGet, "/test?target=test", nil)
handler := func(w http.ResponseWriter, r *http.Request) {}

middlewareFunc := middleware.TargetName(defaultTargetName)
middlewareFunc := TargetName(defaultTargetName)
wrappedHandler := middlewareFunc(http.HandlerFunc(handler))

wrappedHandler.ServeHTTP(responseWriter, testRequest)
Expand All @@ -203,7 +202,7 @@ func TestTargetNameMiddleware(t *testing.T) {
testRequest := httptest.NewRequest(http.MethodGet, "/test?target%=test", nil)
handler := func(w http.ResponseWriter, r *http.Request) {}

middlewareFunc := middleware.TargetName(defaultTargetName)
middlewareFunc := TargetName(defaultTargetName)
wrappedHandler := middlewareFunc(http.HandlerFunc(handler))

wrappedHandler.ServeHTTP(responseWriter, testRequest)
Expand Down
29 changes: 29 additions & 0 deletions api/middleware/readonly_mode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package middleware

import (
"net/http"

"github.com/go-chi/render"
"github.com/moira-alert/moira/api"
)

// ReadOnlyMiddleware returns 403 for mutating queries if readonly mode is enabled
func ReadOnlyMiddleware(config *api.Config) func(next http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
if config.Flags.IsReadonlyEnabled && isMutatingMethod(r.Method) {
render.Render(w, r, api.ErrorForbidden("Moira is currently in read-only mode")) //nolint:errcheck
return
}
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
}

func isMutatingMethod(method string) bool {
return method == http.MethodPut ||
method == http.MethodPost ||
method == http.MethodPatch ||
method == http.MethodDelete
}
73 changes: 73 additions & 0 deletions api/middleware/readonly_mode_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package middleware

import (
"net/http"
"net/http/httptest"
"testing"

"github.com/moira-alert/moira/api"
. "github.com/smartystreets/goconvey/convey"
)

func TestReadonlyModeMiddleware(t *testing.T) {
Convey("Given readonly mode is disabled", t, func() {
config := &api.Config{Flags: api.FeatureFlags{IsReadonlyEnabled: false}}

Convey("Performing get request", func() {
actual := PerformRequestWithReadonlyModeMiddleware(config, http.MethodGet)

So(actual, ShouldEqual, http.StatusOK)
})
Convey("Performing put request", func() {
actual := PerformRequestWithReadonlyModeMiddleware(config, http.MethodPut)

So(actual, ShouldEqual, http.StatusOK)
})
})

Convey("Given readonly mode is enabled", t, func() {
config := &api.Config{Flags: api.FeatureFlags{IsReadonlyEnabled: true}}

Convey("Performing get request", func() {
actual := PerformRequestWithReadonlyModeMiddleware(config, http.MethodGet)

So(actual, ShouldEqual, http.StatusOK)
})
Convey("Performing put request", func() {
actual := PerformRequestWithReadonlyModeMiddleware(config, http.MethodPut)

So(actual, ShouldEqual, http.StatusForbidden)
})
Convey("Performing post request", func() {
actual := PerformRequestWithReadonlyModeMiddleware(config, http.MethodPost)

So(actual, ShouldEqual, http.StatusForbidden)
})
Convey("Performing patch request", func() {
actual := PerformRequestWithReadonlyModeMiddleware(config, http.MethodPatch)

So(actual, ShouldEqual, http.StatusForbidden)
})
Convey("Performing delete request", func() {
actual := PerformRequestWithReadonlyModeMiddleware(config, http.MethodDelete)

So(actual, ShouldEqual, http.StatusForbidden)
})
})
}

func PerformRequestWithReadonlyModeMiddleware(config *api.Config, method string) int {
responseWriter := httptest.NewRecorder()

testRequest := httptest.NewRequest(method, "/test", nil)

handler := func(w http.ResponseWriter, r *http.Request) {}
middlewareFunc := ReadOnlyMiddleware(config)
wrappedHandler := middlewareFunc(http.HandlerFunc(handler))

wrappedHandler.ServeHTTP(responseWriter, testRequest)
response := responseWriter.Result()
defer response.Body.Close()

return response.StatusCode
}
Loading

0 comments on commit 138341f

Please sign in to comment.