AccessControl: Add FGAC to orgs endpoints (#39579)

* AccessControl: Add FGAC to orgs endpoints

Co-authored-by: Karl Persson <kalle.persson@grafana.com>
Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>
This commit is contained in:
Gabriel MABILLE 2021-10-27 13:13:59 +02:00 committed by GitHub
parent 35432b1183
commit f6a9132975
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 1052 additions and 40 deletions

View File

@ -200,15 +200,15 @@ func (hs *HTTPServer) registerRoutes() {
// org information available to all users.
apiRoute.Group("/org", func(orgRoute routing.RouteRegister) {
orgRoute.Get("/", routing.Wrap(GetOrgCurrent))
orgRoute.Get("/quotas", routing.Wrap(GetOrgQuotas))
orgRoute.Get("/", authorize(reqSignedIn, ac.EvalPermission(ActionOrgsRead, ScopeOrgCurrentID)), routing.Wrap(GetCurrentOrg))
orgRoute.Get("/quotas", authorize(reqSignedIn, ac.EvalPermission(ActionOrgsQuotasRead, ScopeOrgCurrentID)), routing.Wrap(hs.GetCurrentOrgQuotas))
})
// current org
apiRoute.Group("/org", func(orgRoute routing.RouteRegister) {
userIDScope := ac.Scope("users", "id", ac.Parameter(":userId"))
orgRoute.Put("/", reqOrgAdmin, bind(dtos.UpdateOrgForm{}), routing.Wrap(UpdateOrgCurrent))
orgRoute.Put("/address", reqOrgAdmin, bind(dtos.UpdateOrgAddressForm{}), routing.Wrap(UpdateOrgAddressCurrent))
orgRoute.Put("/", authorize(reqOrgAdmin, ac.EvalPermission(ActionOrgsWrite, ScopeOrgCurrentID)), bind(dtos.UpdateOrgForm{}), routing.Wrap(UpdateCurrentOrg))
orgRoute.Put("/address", authorize(reqOrgAdmin, ac.EvalPermission(ActionOrgsWrite, ScopeOrgCurrentID)), bind(dtos.UpdateOrgAddressForm{}), routing.Wrap(UpdateCurrentOrgAddress))
orgRoute.Get("/users", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead, ac.ScopeUsersAll)), routing.Wrap(hs.GetOrgUsersForCurrentOrg))
orgRoute.Get("/users/search", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead, ac.ScopeUsersAll)), routing.Wrap(hs.SearchOrgUsersWithPaging))
orgRoute.Post("/users", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersAdd, ac.ScopeUsersAll)), quota("user"), bind(models.AddOrgUserCommand{}), routing.Wrap(AddOrgUserToCurrentOrg))
@ -221,8 +221,8 @@ func (hs *HTTPServer) registerRoutes() {
orgRoute.Patch("/invites/:code/revoke", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionUsersCreate)), routing.Wrap(RevokeInvite))
// prefs
orgRoute.Get("/preferences", reqOrgAdmin, routing.Wrap(GetOrgPreferences))
orgRoute.Put("/preferences", reqOrgAdmin, bind(dtos.UpdatePrefsCmd{}), routing.Wrap(UpdateOrgPreferences))
orgRoute.Get("/preferences", authorize(reqOrgAdmin, ac.EvalPermission(ActionOrgsPreferencesRead, ScopeOrgCurrentID)), routing.Wrap(GetOrgPreferences))
orgRoute.Put("/preferences", authorize(reqOrgAdmin, ac.EvalPermission(ActionOrgsPreferencesWrite, ScopeOrgCurrentID)), bind(dtos.UpdatePrefsCmd{}), routing.Wrap(UpdateOrgPreferences))
})
// current org without requirement of user to be org admin
@ -231,30 +231,28 @@ func (hs *HTTPServer) registerRoutes() {
})
// create new org
apiRoute.Post("/orgs", quota("org"), bind(models.CreateOrgCommand{}), routing.Wrap(CreateOrg))
apiRoute.Post("/orgs", authorize(reqSignedIn, ac.EvalPermission(ActionOrgsCreate)), quota("org"), bind(models.CreateOrgCommand{}), routing.Wrap(hs.CreateOrg))
// search all orgs
apiRoute.Get("/orgs", reqGrafanaAdmin, routing.Wrap(SearchOrgs))
apiRoute.Get("/orgs", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionOrgsRead, ScopeOrgsAll)), routing.Wrap(SearchOrgs))
// orgs (admin routes)
apiRoute.Group("/orgs/:orgId", func(orgsRoute routing.RouteRegister) {
userIDScope := ac.Scope("users", "id", ac.Parameter(":userId"))
orgsRoute.Get("/", reqGrafanaAdmin, routing.Wrap(GetOrgByID))
orgsRoute.Put("/", reqGrafanaAdmin, bind(dtos.UpdateOrgForm{}), routing.Wrap(UpdateOrg))
orgsRoute.Put("/address", reqGrafanaAdmin, bind(dtos.UpdateOrgAddressForm{}), routing.Wrap(UpdateOrgAddress))
orgsRoute.Delete("/", reqGrafanaAdmin, routing.Wrap(DeleteOrgByID))
orgsRoute.Get("/", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionOrgsRead, ScopeOrgID)), routing.Wrap(GetOrgByID))
orgsRoute.Put("/", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionOrgsWrite, ScopeOrgID)), bind(dtos.UpdateOrgForm{}), routing.Wrap(UpdateOrg))
orgsRoute.Put("/address", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionOrgsWrite, ScopeOrgID)), bind(dtos.UpdateOrgAddressForm{}), routing.Wrap(UpdateOrgAddress))
orgsRoute.Delete("/", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionOrgsDelete, ScopeOrgID)), routing.Wrap(DeleteOrgByID))
orgsRoute.Get("/users", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionOrgUsersRead, ac.ScopeUsersAll)), routing.Wrap(hs.GetOrgUsers))
orgsRoute.Post("/users", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionOrgUsersAdd, ac.ScopeUsersAll)), bind(models.AddOrgUserCommand{}), routing.Wrap(AddOrgUser))
orgsRoute.Patch("/users/:userId", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionOrgUsersRoleUpdate, userIDScope)), bind(models.UpdateOrgUserCommand{}), routing.Wrap(UpdateOrgUser))
orgsRoute.Delete("/users/:userId", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionOrgUsersRemove, userIDScope)), routing.Wrap(RemoveOrgUser))
orgsRoute.Get("/quotas", reqGrafanaAdmin, routing.Wrap(GetOrgQuotas))
orgsRoute.Put("/quotas/:target", reqGrafanaAdmin, bind(models.UpdateOrgQuotaCmd{}), routing.Wrap(UpdateOrgQuota))
orgsRoute.Get("/quotas", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionOrgsQuotasRead, ScopeOrgID)), routing.Wrap(hs.GetOrgQuotas))
orgsRoute.Put("/quotas/:target", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionOrgsQuotasWrite, ScopeOrgID)), bind(models.UpdateOrgQuotaCmd{}), routing.Wrap(hs.UpdateOrgQuota))
})
// orgs (admin routes)
apiRoute.Group("/orgs/name/:name", func(orgsRoute routing.RouteRegister) {
orgsRoute.Get("/", routing.Wrap(hs.GetOrgByName))
}, reqGrafanaAdmin)
apiRoute.Get("/orgs/name/:name/", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionOrgsRead, ScopeOrgName)), routing.Wrap(hs.GetOrgByName))
// auth api keys
apiRoute.Group("/auth/keys", func(keysRoute routing.RouteRegister) {

View File

@ -3,6 +3,7 @@ package api
import (
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"path/filepath"
@ -16,6 +17,7 @@ import (
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/fs"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/remotecache"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/accesscontrol"
@ -245,3 +247,111 @@ type accessControlTestCase struct {
method string
permissions []*accesscontrol.Permission
}
// accessControlScenarioContext contains the setups for accesscontrol tests
type accessControlScenarioContext struct {
// server we registered hs routes on.
server *web.Mux
// initCtx is used in a middleware to set the initial context
// of the request server side. Can be used to pretend sign in.
initCtx *models.ReqContext
// hs is a minimal HTTPServer for the accesscontrol tests to pass.
hs *HTTPServer
// acmock is an accesscontrol mock used to fake users rights.
acmock *accesscontrolmock.Mock
// db is a test database initialized with InitTestDB
db *sqlstore.SQLStore
// cfg is the setting provider
cfg *setting.Cfg
}
func setAccessControlPermissions(acmock *accesscontrolmock.Mock, perms []*accesscontrol.Permission) {
acmock.GetUserPermissionsFunc = func(_ context.Context, _ *models.SignedInUser) ([]*accesscontrol.Permission, error) {
return perms, nil
}
}
func setInitCtxSignedInViewer(initCtx *models.ReqContext) {
initCtx.IsSignedIn = true
initCtx.SignedInUser = &models.SignedInUser{UserId: testUserID, OrgId: 1, OrgRole: models.ROLE_VIEWER, Login: testUserLogin}
}
func setInitCtxSignedInOrgAdmin(initCtx *models.ReqContext) {
initCtx.IsSignedIn = true
initCtx.SignedInUser = &models.SignedInUser{UserId: testUserID, OrgId: 1, OrgRole: models.ROLE_ADMIN, Login: testUserLogin}
}
func setupHTTPServer(t *testing.T, enableAccessControl bool) accessControlScenarioContext {
t.Helper()
// Use a new conf
cfg := setting.NewCfg()
cfg.FeatureToggles = make(map[string]bool)
// Use an accesscontrol mock
acmock := accesscontrolmock.New()
// Handle accesscontrol enablement
if enableAccessControl {
cfg.FeatureToggles["accesscontrol"] = enableAccessControl
} else {
// Disabling accesscontrol has to be done before registering routes
acmock = acmock.WithDisabled()
}
// Use a test DB
db := sqlstore.InitTestDB(t)
db.Cfg = cfg
bus := bus.GetBus()
// Create minimal HTTP Server
hs := &HTTPServer{
Cfg: cfg,
Bus: bus,
Live: newTestLive(t),
QuotaService: &quota.QuotaService{Cfg: cfg},
RouteRegister: routing.NewRouteRegister(),
AccessControl: acmock,
SQLStore: db,
searchUsersService: searchusers.ProvideUsersService(bus, filters.ProvideOSSSearchUserFilter()),
}
// Instantiate a new Server
m := web.New()
// middleware to set the test initial context
initCtx := &models.ReqContext{}
m.Use(func(c *web.Context) {
initCtx.Context = c
initCtx.Logger = log.New("api-test")
c.Map(initCtx)
})
// Register all routes
hs.registerRoutes()
hs.RouteRegister.Register(m.Router)
return accessControlScenarioContext{
server: m,
initCtx: initCtx,
hs: hs,
acmock: acmock,
db: db,
cfg: cfg,
}
}
func callAPI(server *web.Mux, method, path string, body io.Reader, t *testing.T) *httptest.ResponseRecorder {
req, err := http.NewRequest(method, path, body)
require.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
recorder := httptest.NewRecorder()
server.ServeHTTP(recorder, req)
return recorder
}

View File

@ -5,16 +5,16 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/metrics"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
// GET /api/org
func GetOrgCurrent(c *models.ReqContext) response.Response {
func GetCurrentOrg(c *models.ReqContext) response.Response {
return getOrgHelper(c.OrgId)
}
@ -52,7 +52,7 @@ func (hs *HTTPServer) GetOrgByName(c *models.ReqContext) response.Response {
func getOrgHelper(orgID int64) response.Response {
query := models.GetOrgByIdQuery{Id: orgID}
if err := bus.Dispatch(&query); err != nil {
if err := sqlstore.GetOrgById(&query); err != nil {
if errors.Is(err, models.ErrOrgNotFound) {
return response.Error(404, "Organization not found", err)
}
@ -78,13 +78,14 @@ func getOrgHelper(orgID int64) response.Response {
}
// POST /api/orgs
func CreateOrg(c *models.ReqContext, cmd models.CreateOrgCommand) response.Response {
if !c.IsSignedIn || (!setting.AllowUserOrgCreate && !c.IsGrafanaAdmin) {
func (hs *HTTPServer) CreateOrg(c *models.ReqContext, cmd models.CreateOrgCommand) response.Response {
acEnabled := hs.Cfg.FeatureToggles["accesscontrol"]
if !acEnabled && !(setting.AllowUserOrgCreate || c.IsGrafanaAdmin) {
return response.Error(403, "Access denied", nil)
}
cmd.UserId = c.UserId
if err := bus.Dispatch(&cmd); err != nil {
if err := sqlstore.CreateOrg(&cmd); err != nil {
if errors.Is(err, models.ErrOrgNameTaken) {
return response.Error(409, "Organization name taken", err)
}
@ -100,7 +101,7 @@ func CreateOrg(c *models.ReqContext, cmd models.CreateOrgCommand) response.Respo
}
// PUT /api/org
func UpdateOrgCurrent(c *models.ReqContext, form dtos.UpdateOrgForm) response.Response {
func UpdateCurrentOrg(c *models.ReqContext, form dtos.UpdateOrgForm) response.Response {
return updateOrgHelper(form, c.OrgId)
}
@ -111,7 +112,7 @@ func UpdateOrg(c *models.ReqContext, form dtos.UpdateOrgForm) response.Response
func updateOrgHelper(form dtos.UpdateOrgForm, orgID int64) response.Response {
cmd := models.UpdateOrgCommand{Name: form.Name, OrgId: orgID}
if err := bus.Dispatch(&cmd); err != nil {
if err := sqlstore.UpdateOrg(&cmd); err != nil {
if errors.Is(err, models.ErrOrgNameTaken) {
return response.Error(400, "Organization name taken", err)
}
@ -122,7 +123,7 @@ func updateOrgHelper(form dtos.UpdateOrgForm, orgID int64) response.Response {
}
// PUT /api/org/address
func UpdateOrgAddressCurrent(c *models.ReqContext, form dtos.UpdateOrgAddressForm) response.Response {
func UpdateCurrentOrgAddress(c *models.ReqContext, form dtos.UpdateOrgAddressForm) response.Response {
return updateOrgAddressHelper(form, c.OrgId)
}
@ -144,14 +145,14 @@ func updateOrgAddressHelper(form dtos.UpdateOrgAddressForm, orgID int64) respons
},
}
if err := bus.Dispatch(&cmd); err != nil {
if err := sqlstore.UpdateOrgAddress(&cmd); err != nil {
return response.Error(500, "Failed to update org address", err)
}
return response.Success("Address updated")
}
// GET /api/orgs/:orgId
// DELETE /api/orgs/:orgId
func DeleteOrgByID(c *models.ReqContext) response.Response {
orgID := c.ParamsInt64(":orgId")
// before deleting an org, check if user does not belong to the current org
@ -159,7 +160,7 @@ func DeleteOrgByID(c *models.ReqContext) response.Response {
return response.Error(400, "Can not delete org for current user", nil)
}
if err := bus.Dispatch(&models.DeleteOrgCommand{Id: orgID}); err != nil {
if err := sqlstore.DeleteOrg(&models.DeleteOrgCommand{Id: orgID}); err != nil {
if errors.Is(err, models.ErrOrgNotFound) {
return response.Error(404, "Failed to delete organization. ID not found", nil)
}
@ -183,7 +184,7 @@ func SearchOrgs(c *models.ReqContext) response.Response {
Limit: perPage,
}
if err := bus.Dispatch(&query); err != nil {
if err := sqlstore.SearchOrgs(&query); err != nil {
return response.Error(500, "Failed to search orgs", err)
}

567
pkg/api/org_test.go Normal file
View File

@ -0,0 +1,567 @@
package api
import (
"fmt"
"net/http"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/setting"
)
var (
searchOrgsURL = "/api/orgs/"
getCurrentOrgURL = "/api/org/"
getOrgsURL = "/api/orgs/%v"
getOrgsByNameURL = "/api/orgs/name/%v"
putCurrentOrgURL = "/api/org/"
putOrgsURL = "/api/orgs/%v"
putCurrentOrgAddressURL = "/api/org/address"
putOrgsAddressURL = "/api/orgs/%v/address"
testUpdateOrgNameForm = `{ "name": "TestOrgChanged" }`
testUpdateOrgAddressForm = `{ "address1": "1 test road",
"address2": "2 test road",
"city": "TestCity",
"ZipCode": "TESTZIPCODE",
"State": "TestState",
"Country": "TestCountry" }`
deleteOrgsURL = "/api/orgs/%v"
createOrgsURL = "/api/orgs/"
testCreateOrgCmd = `{ "name": "TestOrg%v"}`
)
func TestAPIEndpoint_CreateOrgs_LegacyAccessControl(t *testing.T) {
sc := setupHTTPServer(t, false)
setInitCtxSignedInViewer(sc.initCtx)
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err)
setting.AllowUserOrgCreate = false
input := strings.NewReader(fmt.Sprintf(testCreateOrgCmd, 2))
t.Run("Viewer cannot create Orgs", func(t *testing.T) {
response := callAPI(sc.server, http.MethodPost, createOrgsURL, input, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
sc.initCtx.SignedInUser.IsGrafanaAdmin = true
input = strings.NewReader(fmt.Sprintf(testCreateOrgCmd, 3))
t.Run("Grafana Admin viewer can create Orgs", func(t *testing.T) {
response := callAPI(sc.server, http.MethodPost, createOrgsURL, input, t)
assert.Equal(t, http.StatusOK, response.Code)
})
sc.initCtx.SignedInUser.IsGrafanaAdmin = false
setting.AllowUserOrgCreate = true
input = strings.NewReader(fmt.Sprintf(testCreateOrgCmd, 4))
t.Run("User viewer can create Orgs when AllowUserOrgCreate setting is true", func(t *testing.T) {
response := callAPI(sc.server, http.MethodPost, createOrgsURL, input, t)
assert.Equal(t, http.StatusOK, response.Code)
})
}
func TestAPIEndpoint_CreateOrgs_AccessControl(t *testing.T) {
sc := setupHTTPServer(t, true)
setInitCtxSignedInViewer(sc.initCtx)
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err)
input := strings.NewReader(fmt.Sprintf(testCreateOrgCmd, 2))
t.Run("AccessControl allows creating Orgs with correct permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsCreate}})
response := callAPI(sc.server, http.MethodPost, createOrgsURL, input, t)
assert.Equal(t, http.StatusOK, response.Code)
})
input = strings.NewReader(fmt.Sprintf(testCreateOrgCmd, 3))
t.Run("AccessControl prevents creating Orgs with incorrect permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: "orgs:invalid"}})
response := callAPI(sc.server, http.MethodPost, createOrgsURL, input, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
}
func TestAPIEndpoint_DeleteOrgs_LegacyAccessControl(t *testing.T) {
sc := setupHTTPServer(t, false)
setInitCtxSignedInViewer(sc.initCtx)
// Create two orgs
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err)
_, err = sc.db.CreateOrgWithMember("TestOrg2", testUserID)
require.NoError(t, err)
t.Run("Viewer cannot delete Orgs", func(t *testing.T) {
response := callAPI(sc.server, http.MethodDelete, fmt.Sprintf(deleteOrgsURL, 2), nil, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
sc.initCtx.SignedInUser.IsGrafanaAdmin = true
t.Run("Grafana Admin viewer can delete Orgs", func(t *testing.T) {
response := callAPI(sc.server, http.MethodDelete, fmt.Sprintf(deleteOrgsURL, 2), nil, t)
assert.Equal(t, http.StatusOK, response.Code)
})
}
func TestAPIEndpoint_DeleteOrgs_AccessControl(t *testing.T) {
sc := setupHTTPServer(t, true)
setInitCtxSignedInViewer(sc.initCtx)
// Create three orgs (to delete org2 then org3)
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err)
_, err = sc.db.CreateOrgWithMember("TestOrg2", testUserID)
require.NoError(t, err)
_, err = sc.db.CreateOrgWithMember("TestOrg3", testUserID)
require.NoError(t, err)
t.Run("AccessControl allows deleting Orgs with correct permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsDelete, Scope: ScopeOrgsAll}})
response := callAPI(sc.server, http.MethodDelete, fmt.Sprintf(deleteOrgsURL, 2), nil, t)
assert.Equal(t, http.StatusOK, response.Code)
})
t.Run("AccessControl allows deleting Orgs with exact permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsDelete, Scope: accesscontrol.Scope("orgs", "id", "3")}})
response := callAPI(sc.server, http.MethodDelete, fmt.Sprintf(deleteOrgsURL, 3), nil, t)
assert.Equal(t, http.StatusOK, response.Code)
})
t.Run("AccessControl prevents deleting Orgs with too narrow permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsDelete, Scope: accesscontrol.Scope("orgs", "id", "1")}})
response := callAPI(sc.server, http.MethodDelete, fmt.Sprintf(deleteOrgsURL, 2), nil, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
t.Run("AccessControl prevents deleting Orgs with incorrect permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: "orgs:invalid"}})
response := callAPI(sc.server, http.MethodDelete, fmt.Sprintf(deleteOrgsURL, 2), nil, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
}
func TestAPIEndpoint_SearchOrgs_LegacyAccessControl(t *testing.T) {
sc := setupHTTPServer(t, false)
setInitCtxSignedInViewer(sc.initCtx)
// Create two orgs
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err)
_, err = sc.db.CreateOrgWithMember("TestOrg2", testUserID)
require.NoError(t, err)
t.Run("Viewer cannot list Orgs", func(t *testing.T) {
response := callAPI(sc.server, http.MethodGet, searchOrgsURL, nil, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
sc.initCtx.SignedInUser.IsGrafanaAdmin = true
t.Run("Grafana Admin viewer can list Orgs", func(t *testing.T) {
response := callAPI(sc.server, http.MethodGet, searchOrgsURL, nil, t)
assert.Equal(t, http.StatusOK, response.Code)
})
}
func TestAPIEndpoint_SearchOrgs_AccessControl(t *testing.T) {
sc := setupHTTPServer(t, true)
setInitCtxSignedInViewer(sc.initCtx)
// Create two orgs
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err)
_, err = sc.db.CreateOrgWithMember("TestOrg2", testUserID)
require.NoError(t, err)
t.Run("AccessControl allows listing Orgs with correct permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsRead, Scope: ScopeOrgsAll}})
response := callAPI(sc.server, http.MethodGet, searchOrgsURL, nil, t)
assert.Equal(t, http.StatusOK, response.Code)
})
t.Run("AccessControl prevents listing Orgs with too narrow permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsRead, Scope: accesscontrol.Scope("orgs", "id", "1")}})
response := callAPI(sc.server, http.MethodGet, searchOrgsURL, nil, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
t.Run("AccessControl prevents listing Orgs with incorrect permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: "orgs:invalid"}})
response := callAPI(sc.server, http.MethodGet, searchOrgsURL, nil, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
}
func TestAPIEndpoint_GetCurrentOrg_LegacyAccessControl(t *testing.T) {
sc := setupHTTPServer(t, false)
setInitCtxSignedInViewer(sc.initCtx)
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err)
t.Run("Viewer can view CurrentOrg", func(t *testing.T) {
response := callAPI(sc.server, http.MethodGet, getCurrentOrgURL, nil, t)
assert.Equal(t, http.StatusOK, response.Code)
})
sc.initCtx.IsSignedIn = false
t.Run("Unsigned user cannot view CurrentOrg", func(t *testing.T) {
response := callAPI(sc.server, http.MethodGet, getCurrentOrgURL, nil, t)
assert.Equal(t, http.StatusUnauthorized, response.Code)
})
}
func TestAPIEndpoint_GetCurrentOrg_AccessControl(t *testing.T) {
sc := setupHTTPServer(t, true)
setInitCtxSignedInViewer(sc.initCtx)
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err)
t.Run("AccessControl allows viewing CurrentOrg with correct permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsRead, Scope: ScopeOrgsAll}})
response := callAPI(sc.server, http.MethodGet, getCurrentOrgURL, nil, t)
assert.Equal(t, http.StatusOK, response.Code)
})
t.Run("AccessControl allows viewing CurrentOrg with exact permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsRead, Scope: accesscontrol.Scope("orgs", "id", "1")}})
response := callAPI(sc.server, http.MethodGet, getCurrentOrgURL, nil, t)
assert.Equal(t, http.StatusOK, response.Code)
})
t.Run("AccessControl prevents viewing CurrentOrg with incorrect permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: "orgs:invalid"}})
response := callAPI(sc.server, http.MethodGet, getCurrentOrgURL, nil, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
}
func TestAPIEndpoint_GetOrg_LegacyAccessControl(t *testing.T) {
sc := setupHTTPServer(t, false)
setInitCtxSignedInViewer(sc.initCtx)
// Create two orgs, to fetch another one than the logged in one
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err)
_, err = sc.db.CreateOrgWithMember("TestOrg2", testUserID)
require.NoError(t, err)
t.Run("Viewer cannot view another Org", func(t *testing.T) {
response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsURL, 2), nil, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
sc.initCtx.SignedInUser.IsGrafanaAdmin = true
t.Run("Grafana admin viewer can view another Org", func(t *testing.T) {
response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsURL, 2), nil, t)
assert.Equal(t, http.StatusOK, response.Code)
})
}
func TestAPIEndpoint_GetOrg_AccessControl(t *testing.T) {
sc := setupHTTPServer(t, true)
setInitCtxSignedInViewer(sc.initCtx)
// Create two orgs, to fetch another one than the logged in one
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err)
_, err = sc.db.CreateOrgWithMember("TestOrg2", testUserID)
require.NoError(t, err)
t.Run("AccessControl allows viewing another org with correct permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsRead, Scope: ScopeOrgsAll}})
response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsURL, 2), nil, t)
assert.Equal(t, http.StatusOK, response.Code)
})
t.Run("AccessControl allows viewing another org with exact permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsRead, Scope: accesscontrol.Scope("orgs", "id", "2")}})
response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsURL, 2), nil, t)
assert.Equal(t, http.StatusOK, response.Code)
})
t.Run("AccessControl prevents viewing another org with too narrow permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsRead, Scope: accesscontrol.Scope("orgs", "id", "1")}})
response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsURL, 2), nil, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
t.Run("AccessControl prevents viewing another org with incorrect permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: "orgs:invalid"}})
response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsURL, 2), nil, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
}
func TestAPIEndpoint_GetOrgByName_LegacyAccessControl(t *testing.T) {
sc := setupHTTPServer(t, false)
setInitCtxSignedInViewer(sc.initCtx)
// Create two orgs, to fetch another one than the logged in one
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err)
_, err = sc.db.CreateOrgWithMember("TestOrg2", testUserID)
require.NoError(t, err)
t.Run("Viewer cannot view another Org", func(t *testing.T) {
response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsByNameURL, "TestOrg2"), nil, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
sc.initCtx.SignedInUser.IsGrafanaAdmin = true
t.Run("Grafana admin viewer can view another Org", func(t *testing.T) {
response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsByNameURL, "TestOrg2"), nil, t)
assert.Equal(t, http.StatusOK, response.Code)
})
}
func TestAPIEndpoint_GetOrgByName_AccessControl(t *testing.T) {
sc := setupHTTPServer(t, true)
setInitCtxSignedInViewer(sc.initCtx)
// Create two orgs, to fetch another one than the logged in one
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err)
_, err = sc.db.CreateOrgWithMember("TestOrg2", testUserID)
require.NoError(t, err)
t.Run("AccessControl allows viewing another org with correct permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsRead, Scope: ScopeOrgsAll}})
response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsByNameURL, "TestOrg2"), nil, t)
assert.Equal(t, http.StatusOK, response.Code)
})
t.Run("AccessControl allows viewing another org with exact permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsRead, Scope: accesscontrol.Scope("orgs", "name", "TestOrg2")}})
response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsByNameURL, "TestOrg2"), nil, t)
assert.Equal(t, http.StatusOK, response.Code)
})
t.Run("AccessControl prevents viewing another org with too narrow permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsRead, Scope: accesscontrol.Scope("orgs", "name", "TestOrg1")}})
response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsByNameURL, "TestOrg2"), nil, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
t.Run("AccessControl prevents viewing another org with incorrect permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: "orgs:invalid"}})
response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsByNameURL, "TestOrg2"), nil, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
}
func TestAPIEndpoint_PutCurrentOrg_LegacyAccessControl(t *testing.T) {
sc := setupHTTPServer(t, false)
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err)
input := strings.NewReader(testUpdateOrgNameForm)
setInitCtxSignedInViewer(sc.initCtx)
t.Run("Viewer cannot update current org", func(t *testing.T) {
response := callAPI(sc.server, http.MethodPut, putCurrentOrgURL, input, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
setInitCtxSignedInOrgAdmin(sc.initCtx)
t.Run("Admin can update current org", func(t *testing.T) {
response := callAPI(sc.server, http.MethodPut, putCurrentOrgURL, input, t)
assert.Equal(t, http.StatusOK, response.Code)
})
}
func TestAPIEndpoint_PutCurrentOrg_AccessControl(t *testing.T) {
sc := setupHTTPServer(t, true)
setInitCtxSignedInViewer(sc.initCtx)
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err)
input := strings.NewReader(testUpdateOrgNameForm)
t.Run("AccessControl allows updating current org with correct permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsWrite, Scope: ScopeOrgsAll}})
response := callAPI(sc.server, http.MethodPut, putCurrentOrgURL, input, t)
assert.Equal(t, http.StatusOK, response.Code)
})
input = strings.NewReader(testUpdateOrgNameForm)
t.Run("AccessControl allows updating current org with exact permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsWrite, Scope: accesscontrol.Scope("orgs", "id", "1")}})
response := callAPI(sc.server, http.MethodPut, putCurrentOrgURL, input, t)
assert.Equal(t, http.StatusOK, response.Code)
})
t.Run("AccessControl prevents updating current org with too narrow permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsWrite, Scope: accesscontrol.Scope("orgs", "id", "2")}})
response := callAPI(sc.server, http.MethodPut, putCurrentOrgURL, input, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
t.Run("AccessControl prevents updating current org with incorrect permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: "orgs:invalid"}})
response := callAPI(sc.server, http.MethodPut, putCurrentOrgURL, input, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
}
func TestAPIEndpoint_PutOrg_LegacyAccessControl(t *testing.T) {
sc := setupHTTPServer(t, false)
setInitCtxSignedInViewer(sc.initCtx)
// Create two orgs, to update another one than the logged in one
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err)
_, err = sc.db.CreateOrgWithMember("TestOrg2", testUserID)
require.NoError(t, err)
input := strings.NewReader(testUpdateOrgNameForm)
t.Run("Viewer cannot update another org", func(t *testing.T) {
response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsURL, 2), input, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
sc.initCtx.SignedInUser.IsGrafanaAdmin = true
t.Run("Grafana Admin can update another org", func(t *testing.T) {
response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsURL, 2), input, t)
assert.Equal(t, http.StatusOK, response.Code)
})
}
func TestAPIEndpoint_PutOrg_AccessControl(t *testing.T) {
sc := setupHTTPServer(t, true)
setInitCtxSignedInViewer(sc.initCtx)
// Create two orgs, to update another one than the logged in one
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err)
_, err = sc.db.CreateOrgWithMember("TestOrg2", testUserID)
require.NoError(t, err)
input := strings.NewReader(testUpdateOrgNameForm)
t.Run("AccessControl allows updating another org with correct permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsWrite, Scope: ScopeOrgsAll}})
response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsURL, 2), input, t)
assert.Equal(t, http.StatusOK, response.Code)
})
input = strings.NewReader(testUpdateOrgNameForm)
t.Run("AccessControl allows updating another org with exact permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsWrite, Scope: accesscontrol.Scope("orgs", "id", "2")}})
response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsURL, 2), input, t)
assert.Equal(t, http.StatusOK, response.Code)
})
input = strings.NewReader(testUpdateOrgNameForm)
t.Run("AccessControl prevents updating another org with too narrow permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsWrite, Scope: accesscontrol.Scope("orgs", "id", "1")}})
response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsURL, 2), input, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
t.Run("AccessControl prevents updating another org with incorrect permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: "orgs:invalid"}})
response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsURL, 2), input, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
}
func TestAPIEndpoint_PutCurrentOrgAddress_LegacyAccessControl(t *testing.T) {
sc := setupHTTPServer(t, false)
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err)
input := strings.NewReader(testUpdateOrgAddressForm)
setInitCtxSignedInViewer(sc.initCtx)
t.Run("Viewer cannot update current org address", func(t *testing.T) {
response := callAPI(sc.server, http.MethodPut, putCurrentOrgAddressURL, input, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
setInitCtxSignedInOrgAdmin(sc.initCtx)
t.Run("Admin can update current org address", func(t *testing.T) {
response := callAPI(sc.server, http.MethodPut, putCurrentOrgAddressURL, input, t)
assert.Equal(t, http.StatusOK, response.Code)
})
}
func TestAPIEndpoint_PutCurrentOrgAddress_AccessControl(t *testing.T) {
sc := setupHTTPServer(t, true)
setInitCtxSignedInViewer(sc.initCtx)
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err)
input := strings.NewReader(testUpdateOrgAddressForm)
t.Run("AccessControl allows updating current org address with correct permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsWrite, Scope: ScopeOrgsAll}})
response := callAPI(sc.server, http.MethodPut, putCurrentOrgAddressURL, input, t)
assert.Equal(t, http.StatusOK, response.Code)
})
input = strings.NewReader(testUpdateOrgAddressForm)
t.Run("AccessControl allows updating current org address with exact permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsWrite, Scope: accesscontrol.Scope("orgs", "id", "1")}})
response := callAPI(sc.server, http.MethodPut, putCurrentOrgAddressURL, input, t)
assert.Equal(t, http.StatusOK, response.Code)
})
t.Run("AccessControl prevents updating current org address with incorrect permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: "orgs:invalid"}})
response := callAPI(sc.server, http.MethodPut, putCurrentOrgAddressURL, input, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
}
func TestAPIEndpoint_PutOrgAddress_LegacyAccessControl(t *testing.T) {
sc := setupHTTPServer(t, false)
setInitCtxSignedInViewer(sc.initCtx)
// Create two orgs, to update another one than the logged in one
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err)
_, err = sc.db.CreateOrgWithMember("TestOrg2", testUserID)
require.NoError(t, err)
input := strings.NewReader(testUpdateOrgAddressForm)
t.Run("Viewer cannot update another org address", func(t *testing.T) {
response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsAddressURL, 2), input, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
sc.initCtx.SignedInUser.IsGrafanaAdmin = true
t.Run("Grafana Admin can update another org address", func(t *testing.T) {
response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsAddressURL, 2), input, t)
assert.Equal(t, http.StatusOK, response.Code)
})
}
func TestAPIEndpoint_PutOrgAddress_AccessControl(t *testing.T) {
sc := setupHTTPServer(t, true)
setInitCtxSignedInViewer(sc.initCtx)
// Create two orgs, to update another one than the logged in one
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err)
_, err = sc.db.CreateOrgWithMember("TestOrg2", testUserID)
require.NoError(t, err)
input := strings.NewReader(testUpdateOrgAddressForm)
t.Run("AccessControl allows updating another org address with correct permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsWrite, Scope: ScopeOrgsAll}})
response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsAddressURL, 2), input, t)
assert.Equal(t, http.StatusOK, response.Code)
})
input = strings.NewReader(testUpdateOrgAddressForm)
t.Run("AccessControl prevents updating another org address with too narrow permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsWrite, Scope: accesscontrol.Scope("orgs", "id", "1")}})
response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsAddressURL, 2), input, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
t.Run("AccessControl prevents updating another org address with incorrect permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: "orgs:invalid"}})
response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsAddressURL, 2), input, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
}

View File

@ -5,6 +5,7 @@ import (
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
)
const (
@ -33,7 +34,7 @@ func GetUserPreferences(c *models.ReqContext) response.Response {
func getPreferencesFor(orgID, userID, teamID int64) response.Response {
prefsQuery := models.GetPreferencesQuery{UserId: userID, OrgId: orgID, TeamId: teamID}
if err := bus.Dispatch(&prefsQuery); err != nil {
if err := sqlstore.GetPreferences(&prefsQuery); err != nil {
return response.Error(500, "Failed to get preferences", err)
}
@ -66,7 +67,7 @@ func updatePreferencesFor(orgID, userID, teamId int64, dtoCmd *dtos.UpdatePrefsC
HomeDashboardId: dtoCmd.HomeDashboardID,
}
if err := bus.Dispatch(&saveCmd); err != nil {
if err := sqlstore.SavePreferences(&saveCmd); err != nil {
return response.Error(500, "Failed to save preferences", err)
}

111
pkg/api/preferences_test.go Normal file
View File

@ -0,0 +1,111 @@
package api
import (
"net/http"
"strings"
"testing"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var (
getOrgPreferencesURL = "/api/org/preferences/"
putOrgPreferencesURL = "/api/org/preferences/"
testUpdateOrgPreferencesCmd = `{ "theme": "light", "homeDashboardId": 1 }`
)
func TestAPIEndpoint_GetCurrentOrgPreferences_LegacyAccessControl(t *testing.T) {
sc := setupHTTPServer(t, false)
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err)
setInitCtxSignedInViewer(sc.initCtx)
t.Run("Viewer cannot get org preferences", func(t *testing.T) {
response := callAPI(sc.server, http.MethodGet, getOrgPreferencesURL, nil, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
setInitCtxSignedInOrgAdmin(sc.initCtx)
t.Run("Org Admin can get org preferences", func(t *testing.T) {
response := callAPI(sc.server, http.MethodGet, getOrgPreferencesURL, nil, t)
assert.Equal(t, http.StatusOK, response.Code)
})
}
func TestAPIEndpoint_GetCurrentOrgPreferences_AccessControl(t *testing.T) {
sc := setupHTTPServer(t, true)
setInitCtxSignedInViewer(sc.initCtx)
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err)
t.Run("AccessControl allows getting org preferences with correct permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsPreferencesRead, Scope: ScopeOrgsAll}})
response := callAPI(sc.server, http.MethodGet, getOrgPreferencesURL, nil, t)
assert.Equal(t, http.StatusOK, response.Code)
})
t.Run("AccessControl allows getting org preferences with exact permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsPreferencesRead, Scope: accesscontrol.Scope("orgs", "id", "1")}})
response := callAPI(sc.server, http.MethodGet, getOrgPreferencesURL, nil, t)
assert.Equal(t, http.StatusOK, response.Code)
})
t.Run("AccessControl prevents getting org preferences with incorrect permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: "orgs:invalid"}})
response := callAPI(sc.server, http.MethodGet, getOrgPreferencesURL, nil, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
}
func TestAPIEndpoint_PutCurrentOrgPreferences_LegacyAccessControl(t *testing.T) {
sc := setupHTTPServer(t, false)
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err)
setInitCtxSignedInViewer(sc.initCtx)
input := strings.NewReader(testUpdateOrgPreferencesCmd)
t.Run("Viewer cannot update org preferences", func(t *testing.T) {
response := callAPI(sc.server, http.MethodPut, putOrgPreferencesURL, input, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
setInitCtxSignedInOrgAdmin(sc.initCtx)
input = strings.NewReader(testUpdateOrgPreferencesCmd)
t.Run("Org Admin can update org preferences", func(t *testing.T) {
response := callAPI(sc.server, http.MethodPut, putOrgPreferencesURL, input, t)
assert.Equal(t, http.StatusOK, response.Code)
})
}
func TestAPIEndpoint_PutCurrentOrgPreferences_AccessControl(t *testing.T) {
sc := setupHTTPServer(t, true)
setInitCtxSignedInViewer(sc.initCtx)
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err)
input := strings.NewReader(testUpdateOrgPreferencesCmd)
t.Run("AccessControl allows updating org preferences with correct permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsPreferencesWrite, Scope: ScopeOrgsAll}})
response := callAPI(sc.server, http.MethodPut, putOrgPreferencesURL, input, t)
assert.Equal(t, http.StatusOK, response.Code)
})
input = strings.NewReader(testUpdateOrgPreferencesCmd)
t.Run("AccessControl allows updating org preferences with exact permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsPreferencesWrite, Scope: accesscontrol.Scope("orgs", "id", "1")}})
response := callAPI(sc.server, http.MethodPut, putOrgPreferencesURL, input, t)
assert.Equal(t, http.StatusOK, response.Code)
})
input = strings.NewReader(testUpdateOrgPreferencesCmd)
t.Run("AccessControl prevents updating org preferences with incorrect permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: "orgs:invalid"}})
response := callAPI(sc.server, http.MethodPut, putOrgPreferencesURL, input, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
}

View File

@ -8,31 +8,39 @@ import (
"github.com/grafana/grafana/pkg/web"
)
func GetOrgQuotas(c *models.ReqContext) response.Response {
if !setting.Quota.Enabled {
func (hs *HTTPServer) GetCurrentOrgQuotas(c *models.ReqContext) response.Response {
return hs.getOrgQuotasHelper(c, c.OrgId)
}
func (hs *HTTPServer) GetOrgQuotas(c *models.ReqContext) response.Response {
return hs.getOrgQuotasHelper(c, c.ParamsInt64(":orgId"))
}
func (hs *HTTPServer) getOrgQuotasHelper(c *models.ReqContext, orgID int64) response.Response {
if !hs.Cfg.Quota.Enabled {
return response.Error(404, "Quotas not enabled", nil)
}
query := models.GetOrgQuotasQuery{OrgId: c.ParamsInt64(":orgId")}
query := models.GetOrgQuotasQuery{OrgId: orgID}
if err := bus.DispatchCtx(c.Req.Context(), &query); err != nil {
if err := hs.SQLStore.GetOrgQuotas(c.Req.Context(), &query); err != nil {
return response.Error(500, "Failed to get org quotas", err)
}
return response.JSON(200, query.Result)
}
func UpdateOrgQuota(c *models.ReqContext, cmd models.UpdateOrgQuotaCmd) response.Response {
if !setting.Quota.Enabled {
func (hs *HTTPServer) UpdateOrgQuota(c *models.ReqContext, cmd models.UpdateOrgQuotaCmd) response.Response {
if !hs.Cfg.Quota.Enabled {
return response.Error(404, "Quotas not enabled", nil)
}
cmd.OrgId = c.ParamsInt64(":orgId")
cmd.Target = web.Params(c.Req)[":target"]
if _, ok := setting.Quota.Org.ToMap()[cmd.Target]; !ok {
if _, ok := hs.Cfg.Quota.Org.ToMap()[cmd.Target]; !ok {
return response.Error(404, "Invalid quota target", nil)
}
if err := bus.DispatchCtx(c.Req.Context(), &cmd); err != nil {
if err := hs.SQLStore.UpdateOrgQuota(c.Req.Context(), &cmd); err != nil {
return response.Error(500, "Failed to update org quotas", err)
}
return response.Success("Organization quota updated")

216
pkg/api/quota_test.go Normal file
View File

@ -0,0 +1,216 @@
package api
import (
"fmt"
"net/http"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/setting"
)
var (
getCurrentOrgQuotasURL = "/api/org/quotas"
getOrgsQuotasURL = "/api/orgs/%v/quotas"
putOrgsQuotasURL = "/api/orgs/%v/quotas/%v"
testUpdateOrgQuotaCmd = `{ "limit": 20 }`
)
var testOrgQuota = setting.OrgQuota{
User: 10,
DataSource: 10,
Dashboard: 10,
ApiKey: 10,
AlertRule: 10,
}
func TestAPIEndpoint_GetCurrentOrgQuotas_LegacyAccessControl(t *testing.T) {
sc := setupHTTPServer(t, false)
setInitCtxSignedInViewer(sc.initCtx)
sc.hs.Cfg.Quota.Enabled = true
sc.hs.Cfg.Quota.Org = &testOrgQuota
// Required while sqlstore quota.go relies on setting global variables
setting.Quota = sc.hs.Cfg.Quota
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err)
t.Run("Viewer can view CurrentOrgQuotas", func(t *testing.T) {
response := callAPI(sc.server, http.MethodGet, getCurrentOrgQuotasURL, nil, t)
assert.Equal(t, http.StatusOK, response.Code)
})
sc.initCtx.IsSignedIn = false
t.Run("Unsigned user cannot view CurrentOrgQuotas", func(t *testing.T) {
response := callAPI(sc.server, http.MethodGet, getCurrentOrgQuotasURL, nil, t)
assert.Equal(t, http.StatusUnauthorized, response.Code)
})
}
func TestAPIEndpoint_GetCurrentOrgQuotas_AccessControl(t *testing.T) {
sc := setupHTTPServer(t, true)
setInitCtxSignedInViewer(sc.initCtx)
sc.hs.Cfg.Quota.Enabled = true
sc.hs.Cfg.Quota.Org = &testOrgQuota
// Required while sqlstore quota.go relies on setting global variables
setting.Quota = sc.hs.Cfg.Quota
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err)
t.Run("AccessControl allows viewing CurrentOrgQuotas with correct permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsQuotasRead, Scope: ScopeOrgsAll}})
response := callAPI(sc.server, http.MethodGet, getCurrentOrgQuotasURL, nil, t)
assert.Equal(t, http.StatusOK, response.Code)
})
t.Run("AccessControl allows viewing CurrentOrgQuotas with exact permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsQuotasRead, Scope: accesscontrol.Scope("orgs", "id", "1")}})
response := callAPI(sc.server, http.MethodGet, getCurrentOrgQuotasURL, nil, t)
assert.Equal(t, http.StatusOK, response.Code)
})
t.Run("AccessControl prevents viewing CurrentOrgQuotas with incorrect permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: "orgs:invalid"}})
response := callAPI(sc.server, http.MethodGet, getCurrentOrgQuotasURL, nil, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
}
func TestAPIEndpoint_GetOrgQuotas_LegacyAccessControl(t *testing.T) {
sc := setupHTTPServer(t, false)
setInitCtxSignedInViewer(sc.initCtx)
sc.hs.Cfg.Quota.Enabled = true
sc.hs.Cfg.Quota.Org = &testOrgQuota
// Required while sqlstore quota.go relies on setting global variables
setting.Quota = sc.hs.Cfg.Quota
// Create two orgs, to fetch another one than the logged in one
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err)
_, err = sc.db.CreateOrgWithMember("TestOrg2", testUserID)
require.NoError(t, err)
t.Run("Viewer cannot view another org quotas", func(t *testing.T) {
response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsQuotasURL, 2), nil, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
sc.initCtx.SignedInUser.IsGrafanaAdmin = true
t.Run("Grafana admin viewer can view another org quotas", func(t *testing.T) {
response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsQuotasURL, 2), nil, t)
assert.Equal(t, http.StatusOK, response.Code)
})
}
func TestAPIEndpoint_GetOrgQuotas_AccessControl(t *testing.T) {
sc := setupHTTPServer(t, true)
setInitCtxSignedInViewer(sc.initCtx)
sc.hs.Cfg.Quota.Enabled = true
sc.hs.Cfg.Quota.Org = &testOrgQuota
// Required while sqlstore quota.go relies on setting global variables
setting.Quota = sc.hs.Cfg.Quota
// Create two orgs, to fetch another one than the logged in one
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err)
_, err = sc.db.CreateOrgWithMember("TestOrg2", testUserID)
require.NoError(t, err)
t.Run("AccessControl allows viewing another org quotas with correct permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsQuotasRead, Scope: ScopeOrgsAll}})
response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsQuotasURL, 2), nil, t)
assert.Equal(t, http.StatusOK, response.Code)
})
t.Run("AccessControl allows viewing another org quotas with exact permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsQuotasRead, Scope: accesscontrol.Scope("orgs", "id", "2")}})
response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsQuotasURL, 2), nil, t)
assert.Equal(t, http.StatusOK, response.Code)
})
t.Run("AccessControl prevents viewing another org quotas with too narrow permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsQuotasRead, Scope: accesscontrol.Scope("orgs", "id", "1")}})
response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsQuotasURL, 2), nil, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
t.Run("AccessControl prevents viewing another org quotas with incorrect permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: "orgs:invalid"}})
response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsQuotasURL, 2), nil, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
}
func TestAPIEndpoint_PutOrgQuotas_LegacyAccessControl(t *testing.T) {
sc := setupHTTPServer(t, false)
setInitCtxSignedInViewer(sc.initCtx)
sc.hs.Cfg.Quota.Enabled = true
sc.hs.Cfg.Quota.Org = &testOrgQuota
// Create two orgs, to update another one than the logged in one
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err)
_, err = sc.db.CreateOrgWithMember("TestOrg2", testUserID)
require.NoError(t, err)
input := strings.NewReader(testUpdateOrgQuotaCmd)
t.Run("Viewer cannot update another org quotas", func(t *testing.T) {
response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsQuotasURL, 2, "org_user"), input, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
sc.initCtx.SignedInUser.IsGrafanaAdmin = true
input = strings.NewReader(testUpdateOrgQuotaCmd)
t.Run("Grafana admin viewer can update another org quotas", func(t *testing.T) {
response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsQuotasURL, 2, "org_user"), input, t)
assert.Equal(t, http.StatusOK, response.Code)
})
}
func TestAPIEndpoint_PutOrgQuotas_AccessControl(t *testing.T) {
sc := setupHTTPServer(t, true)
setInitCtxSignedInViewer(sc.initCtx)
sc.hs.Cfg.Quota.Enabled = true
sc.hs.Cfg.Quota.Org = &testOrgQuota
// Create two orgs, to update another one than the logged in one
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
require.NoError(t, err)
_, err = sc.db.CreateOrgWithMember("TestOrg2", testUserID)
require.NoError(t, err)
input := strings.NewReader(testUpdateOrgQuotaCmd)
t.Run("AccessControl allows updating another org quotas with correct permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsQuotasWrite, Scope: ScopeOrgsAll}})
response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsQuotasURL, 2, "org_user"), input, t)
assert.Equal(t, http.StatusOK, response.Code)
})
input = strings.NewReader(testUpdateOrgQuotaCmd)
t.Run("AccessControl allows updating another org quotas with exact permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsQuotasWrite, Scope: accesscontrol.Scope("orgs", "id", "2")}})
response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsQuotasURL, 2, "org_user"), input, t)
assert.Equal(t, http.StatusOK, response.Code)
})
input = strings.NewReader(testUpdateOrgQuotaCmd)
t.Run("AccessControl prevents updating another org quotas with too narrow permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsQuotasWrite, Scope: accesscontrol.Scope("orgs", "id", "1")}})
response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsQuotasURL, 2, "org_user"), input, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
input = strings.NewReader(testUpdateOrgQuotaCmd)
t.Run("AccessControl prevents updating another org quotas with incorrect permissions", func(t *testing.T) {
setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: "orgs:invalid"}})
response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsQuotasURL, 2, "org_user"), input, t)
assert.Equal(t, http.StatusForbidden, response.Code)
})
}