diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index d364ce05258..abc4bd46250 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -213,6 +213,14 @@ func (hs *HTTPServer) ImportDashboard(c *models.ReqContext, apiCmd dtos.ImportDa return response.Error(422, "Dashboard must be set", nil) } + limitReached, err := hs.QuotaService.QuotaReached(c, "dashboard") + if err != nil { + return response.Error(500, "failed to get quota", err) + } + if limitReached { + return response.Error(403, "Quota reached", nil) + } + trimDefaults := c.QueryBoolWithDefault("trimdefaults", true) if trimDefaults && !hs.LoadSchemaService.IsDisabled() { apiCmd.Dashboard, err = hs.LoadSchemaService.DashboardApplyDefaults(apiCmd.Dashboard) diff --git a/pkg/tests/api/dashboards/api_dashboards_test.go b/pkg/tests/api/dashboards/api_dashboards_test.go new file mode 100644 index 00000000000..4025910ef61 --- /dev/null +++ b/pkg/tests/api/dashboards/api_dashboards_test.go @@ -0,0 +1,94 @@ +package dashboards + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "testing" + + "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/tests/testinfra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDashboardQuota(t *testing.T) { + // enable quota and set low dashboard quota + // Setup Grafana and its Database + dashboardQuota := int64(1) + dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{ + DisableAnonymous: true, + EnableQuota: true, + DashboardOrgQuota: &dashboardQuota, + }) + grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path) + // Create user + createUser(t, store, models.CreateUserCommand{ + DefaultOrgRole: string(models.ROLE_ADMIN), + Password: "admin", + Login: "admin", + }) + + t.Run("when quota limit doesn't exceed, importing a dashboard should succeed", func(t *testing.T) { + // Import dashboard + dashboardDataOne, err := simplejson.NewJson([]byte(`{"title":"just testing"}`)) + require.NoError(t, err) + buf1 := &bytes.Buffer{} + err = json.NewEncoder(buf1).Encode(dtos.ImportDashboardCommand{ + Dashboard: dashboardDataOne, + }) + require.NoError(t, err) + u := fmt.Sprintf("http://admin:admin@%s/api/dashboards/import", grafanaListedAddr) + // nolint:gosec + resp, err := http.Post(u, "application/json", buf1) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, resp.StatusCode) + t.Cleanup(func() { + err := resp.Body.Close() + require.NoError(t, err) + }) + b, err := ioutil.ReadAll(resp.Body) + require.NoError(t, err) + dashboardDTO := &plugins.PluginDashboardInfoDTO{} + err = json.Unmarshal(b, dashboardDTO) + require.NoError(t, err) + require.EqualValues(t, 1, dashboardDTO.DashboardId) + }) + + t.Run("when quota limit exceeds importing a dashboard should fail", func(t *testing.T) { + dashboardDataOne, err := simplejson.NewJson([]byte(`{"title":"just testing"}`)) + require.NoError(t, err) + buf1 := &bytes.Buffer{} + err = json.NewEncoder(buf1).Encode(dtos.ImportDashboardCommand{ + Dashboard: dashboardDataOne, + }) + require.NoError(t, err) + u := fmt.Sprintf("http://admin:admin@%s/api/dashboards/import", grafanaListedAddr) + // nolint:gosec + resp, err := http.Post(u, "application/json", buf1) + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, resp.StatusCode) + t.Cleanup(func() { + err := resp.Body.Close() + require.NoError(t, err) + }) + b, err := ioutil.ReadAll(resp.Body) + require.NoError(t, err) + assert.Equal(t, http.StatusForbidden, resp.StatusCode) + require.JSONEq(t, `{"message":"Quota reached"}`, string(b)) + }) +} + +func createUser(t *testing.T, store *sqlstore.SQLStore, cmd models.CreateUserCommand) int64 { + t.Helper() + u, err := store.CreateUser(context.Background(), cmd) + require.NoError(t, err) + return u.Id +} diff --git a/pkg/tests/testinfra/testinfra.go b/pkg/tests/testinfra/testinfra.go index babc48cc123..9d255d40269 100644 --- a/pkg/tests/testinfra/testinfra.go +++ b/pkg/tests/testinfra/testinfra.go @@ -8,6 +8,7 @@ import ( "net/http" "os" "path/filepath" + "strconv" "strings" "testing" "time" @@ -239,6 +240,12 @@ func CreateGrafDir(t *testing.T, opts ...GrafanaOpts) (string, string) { require.NoError(t, err) _, err = quotaSection.NewKey("enabled", "true") require.NoError(t, err) + dashboardQuota := int64(100) + if o.DashboardOrgQuota != nil { + dashboardQuota = *o.DashboardOrgQuota + } + _, err = quotaSection.NewKey("org_dashboard", strconv.FormatInt(dashboardQuota, 10)) + require.NoError(t, err) } if o.DisableAnonymous { anonSect, err := cfg.GetSection("auth.anonymous") @@ -296,6 +303,7 @@ type GrafanaOpts struct { NGAlertAlertmanagerConfigPollInterval time.Duration AnonymousUserRole models.RoleType EnableQuota bool + DashboardOrgQuota *int64 DisableAnonymous bool CatalogAppEnabled bool ViewersCanEdit bool