From 818b8739c038ccf77f8fbe4dc63b0cf92bec44c7 Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Wed, 17 Nov 2021 10:12:28 +0100 Subject: [PATCH] AccessControl: Remove scopes from orgs endpoints (#41709) * AccessControl: Check permissions in target org * Remove org scopes and add an authorizeInOrg middleware * Use query result org id and perform users permission check globally for GetOrgByName * Remove scope translation for orgs current * Suggestion from Ieva --- pkg/api/api.go | 46 +- pkg/api/common_test.go | 55 +- pkg/api/org_test.go | 490 ++++++------- pkg/api/org_users.go | 39 +- pkg/api/org_users_test.go | 645 +++++++++++++++--- pkg/api/preferences_test.go | 28 +- pkg/api/quota_test.go | 114 +--- pkg/api/roles.go | 26 +- .../accesscontrol/middleware/middleware.go | 77 ++- .../ossaccesscontrol/ossaccesscontrol_test.go | 39 +- pkg/services/accesscontrol/scope.go | 7 +- 11 files changed, 981 insertions(+), 585 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 9bc1e6278c9..98557e7d6c6 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -33,6 +33,7 @@ func (hs *HTTPServer) registerRoutes() { reqSnapshotPublicModeOrSignedIn := middleware.SnapshotPublicModeOrSignedIn(hs.Cfg) redirectFromLegacyPanelEditURL := middleware.RedirectFromLegacyPanelEditURL(hs.Cfg) authorize := acmiddleware.Middleware(hs.AccessControl) + authorizeInOrg := acmiddleware.AuthorizeInOrgMiddleware(hs.AccessControl, hs.SQLStore) quota := middleware.Quota(hs.QuotaService) bind := binding.Bind @@ -200,20 +201,20 @@ func (hs *HTTPServer) registerRoutes() { // org information available to all users. apiRoute.Group("/org", func(orgRoute routing.RouteRegister) { - orgRoute.Get("/", authorize(reqSignedIn, ac.EvalPermission(ActionOrgsRead, ScopeOrgCurrentID)), routing.Wrap(GetCurrentOrg)) - orgRoute.Get("/quotas", authorize(reqSignedIn, ac.EvalPermission(ActionOrgsQuotasRead, ScopeOrgCurrentID)), routing.Wrap(hs.GetCurrentOrgQuotas)) + orgRoute.Get("/", authorize(reqSignedIn, ac.EvalPermission(ActionOrgsRead)), routing.Wrap(GetCurrentOrg)) + orgRoute.Get("/quotas", authorize(reqSignedIn, ac.EvalPermission(ActionOrgsQuotasRead)), routing.Wrap(hs.GetCurrentOrgQuotas)) }) // current org apiRoute.Group("/org", func(orgRoute routing.RouteRegister) { userIDScope := ac.Scope("users", "id", ac.Parameter(":userId")) - 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.Put("/", authorize(reqOrgAdmin, ac.EvalPermission(ActionOrgsWrite)), bind(dtos.UpdateOrgForm{}), routing.Wrap(UpdateCurrentOrg)) + orgRoute.Put("/address", authorize(reqOrgAdmin, ac.EvalPermission(ActionOrgsWrite)), 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)) - orgRoute.Patch("/users/:userId", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRoleUpdate, userIDScope)), bind(models.UpdateOrgUserCommand{}), routing.Wrap(UpdateOrgUserForCurrentOrg)) - orgRoute.Delete("/users/:userId", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRemove, userIDScope)), routing.Wrap(RemoveOrgUserForCurrentOrg)) + orgRoute.Post("/users", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersAdd, ac.ScopeUsersAll)), quota("user"), bind(models.AddOrgUserCommand{}), routing.Wrap(hs.AddOrgUserToCurrentOrg)) + orgRoute.Patch("/users/:userId", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRoleUpdate, userIDScope)), bind(models.UpdateOrgUserCommand{}), routing.Wrap(hs.UpdateOrgUserForCurrentOrg)) + orgRoute.Delete("/users/:userId", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRemove, userIDScope)), routing.Wrap(hs.RemoveOrgUserForCurrentOrg)) // invites orgRoute.Get("/invites", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionUsersCreate)), routing.Wrap(GetPendingOrgInvites)) @@ -221,8 +222,8 @@ func (hs *HTTPServer) registerRoutes() { orgRoute.Patch("/invites/:code/revoke", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionUsersCreate)), routing.Wrap(RevokeInvite)) // prefs - orgRoute.Get("/preferences", authorize(reqOrgAdmin, ac.EvalPermission(ActionOrgsPreferencesRead, ScopeOrgCurrentID)), routing.Wrap(hs.GetOrgPreferences)) - orgRoute.Put("/preferences", authorize(reqOrgAdmin, ac.EvalPermission(ActionOrgsPreferencesWrite, ScopeOrgCurrentID)), bind(dtos.UpdatePrefsCmd{}), routing.Wrap(hs.UpdateOrgPreferences)) + orgRoute.Get("/preferences", authorize(reqOrgAdmin, ac.EvalPermission(ActionOrgsPreferencesRead)), routing.Wrap(hs.GetOrgPreferences)) + orgRoute.Put("/preferences", authorize(reqOrgAdmin, ac.EvalPermission(ActionOrgsPreferencesWrite)), bind(dtos.UpdatePrefsCmd{}), routing.Wrap(hs.UpdateOrgPreferences)) }) // current org without requirement of user to be org admin @@ -231,27 +232,28 @@ func (hs *HTTPServer) registerRoutes() { }) // create new org - apiRoute.Post("/orgs", authorize(reqSignedIn, ac.EvalPermission(ActionOrgsCreate)), quota("org"), bind(models.CreateOrgCommand{}), routing.Wrap(hs.CreateOrg)) + apiRoute.Post("/orgs", authorizeInOrg(reqSignedIn, acmiddleware.UseGlobalOrg, ac.EvalPermission(ActionOrgsCreate)), quota("org"), bind(models.CreateOrgCommand{}), routing.Wrap(hs.CreateOrg)) // search all orgs - apiRoute.Get("/orgs", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionOrgsRead, ScopeOrgsAll)), routing.Wrap(SearchOrgs)) + apiRoute.Get("/orgs", authorizeInOrg(reqGrafanaAdmin, acmiddleware.UseGlobalOrg, ac.EvalPermission(ActionOrgsRead)), routing.Wrap(SearchOrgs)) // orgs (admin routes) apiRoute.Group("/orgs/:orgId", func(orgsRoute routing.RouteRegister) { - 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", reqGrafanaAdmin, routing.Wrap(hs.GetOrgUsers)) - orgsRoute.Post("/users", reqGrafanaAdmin, bind(models.AddOrgUserCommand{}), routing.Wrap(AddOrgUser)) - orgsRoute.Patch("/users/:userId", reqGrafanaAdmin, bind(models.UpdateOrgUserCommand{}), routing.Wrap(UpdateOrgUser)) - orgsRoute.Delete("/users/:userId", reqGrafanaAdmin, routing.Wrap(RemoveOrgUser)) - 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)) + userIDScope := ac.Scope("users", "id", ac.Parameter(":userId")) + orgsRoute.Get("/", authorizeInOrg(reqGrafanaAdmin, acmiddleware.UseOrgFromContextParams, ac.EvalPermission(ActionOrgsRead)), routing.Wrap(GetOrgByID)) + orgsRoute.Put("/", authorizeInOrg(reqGrafanaAdmin, acmiddleware.UseOrgFromContextParams, ac.EvalPermission(ActionOrgsWrite)), bind(dtos.UpdateOrgForm{}), routing.Wrap(UpdateOrg)) + orgsRoute.Put("/address", authorizeInOrg(reqGrafanaAdmin, acmiddleware.UseOrgFromContextParams, ac.EvalPermission(ActionOrgsWrite)), bind(dtos.UpdateOrgAddressForm{}), routing.Wrap(UpdateOrgAddress)) + orgsRoute.Delete("/", authorizeInOrg(reqGrafanaAdmin, acmiddleware.UseOrgFromContextParams, ac.EvalPermission(ActionOrgsDelete)), routing.Wrap(DeleteOrgByID)) + orgsRoute.Get("/users", authorizeInOrg(reqGrafanaAdmin, acmiddleware.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersRead, ac.ScopeUsersAll)), routing.Wrap(hs.GetOrgUsers)) + orgsRoute.Post("/users", authorizeInOrg(reqGrafanaAdmin, acmiddleware.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersAdd, ac.ScopeUsersAll)), bind(models.AddOrgUserCommand{}), routing.Wrap(hs.AddOrgUser)) + orgsRoute.Patch("/users/:userId", authorizeInOrg(reqGrafanaAdmin, acmiddleware.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersRoleUpdate, userIDScope)), bind(models.UpdateOrgUserCommand{}), routing.Wrap(hs.UpdateOrgUser)) + orgsRoute.Delete("/users/:userId", authorizeInOrg(reqGrafanaAdmin, acmiddleware.UseOrgFromContextParams, ac.EvalPermission(ac.ActionOrgUsersRemove, userIDScope)), routing.Wrap(hs.RemoveOrgUser)) + orgsRoute.Get("/quotas", authorizeInOrg(reqGrafanaAdmin, acmiddleware.UseOrgFromContextParams, ac.EvalPermission(ActionOrgsQuotasRead)), routing.Wrap(hs.GetOrgQuotas)) + orgsRoute.Put("/quotas/:target", authorizeInOrg(reqGrafanaAdmin, acmiddleware.UseOrgFromContextParams, ac.EvalPermission(ActionOrgsQuotasWrite)), bind(models.UpdateOrgQuotaCmd{}), routing.Wrap(hs.UpdateOrgQuota)) }) // orgs (admin routes) - apiRoute.Get("/orgs/name/:name/", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionOrgsRead, ScopeOrgName)), routing.Wrap(hs.GetOrgByName)) + apiRoute.Get("/orgs/name/:name/", authorizeInOrg(reqGrafanaAdmin, acmiddleware.UseGlobalOrg, ac.EvalPermission(ActionOrgsRead)), routing.Wrap(hs.GetOrgByName)) // auth api keys apiRoute.Group("/auth/keys", func(keysRoute routing.RouteRegister) { diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index 7dcf856fb0d..d01e926e261 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -9,9 +9,7 @@ import ( "path/filepath" "testing" - "github.com/grafana/grafana/pkg/services/searchusers/filters" - - "github.com/grafana/grafana/pkg/services/searchusers" + "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" @@ -19,17 +17,20 @@ import ( "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/infra/usagestats" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/contexthandler" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/rendering" + "github.com/grafana/grafana/pkg/services/searchusers" + "github.com/grafana/grafana/pkg/services/searchusers/filters" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" - "github.com/stretchr/testify/require" ) func loggedInUserScenario(t *testing.T, desc string, url string, fn scenarioFunc) { @@ -270,12 +271,21 @@ type accessControlScenarioContext struct { 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 setAccessControlPermissions(acmock *accesscontrolmock.Mock, perms []*accesscontrol.Permission, org int64) { + acmock.GetUserPermissionsFunc = func(_ context.Context, u *models.SignedInUser) ([]*accesscontrol.Permission, error) { + if u.OrgId == org { + return perms, nil + } + return nil, nil } } +// setInitCtxSignedInUser sets a copy of the user in initCtx +func setInitCtxSignedInUser(initCtx *models.ReqContext, user models.SignedInUser) { + initCtx.IsSignedIn = true + initCtx.SignedInUser = &user +} + func setInitCtxSignedInViewer(initCtx *models.ReqContext) { initCtx.IsSignedIn = true initCtx.SignedInUser = &models.SignedInUser{UserId: testUserID, OrgId: 1, OrgRole: models.ROLE_VIEWER, Login: testUserLogin} @@ -286,22 +296,17 @@ func setInitCtxSignedInOrgAdmin(initCtx *models.ReqContext) { initCtx.SignedInUser = &models.SignedInUser{UserId: testUserID, OrgId: 1, OrgRole: models.ROLE_ADMIN, Login: testUserLogin} } -func setupHTTPServer(t *testing.T, enableAccessControl bool) accessControlScenarioContext { +func setupHTTPServer(t *testing.T, useFakeAccessControl bool, enableAccessControl bool) accessControlScenarioContext { t.Helper() + var acmock *accesscontrolmock.Mock + var ac *ossaccesscontrol.OSSAccessControlService + // 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 @@ -317,11 +322,27 @@ func setupHTTPServer(t *testing.T, enableAccessControl bool) accessControlScenar Live: newTestLive(t), QuotaService: "a.QuotaService{Cfg: cfg}, RouteRegister: routing.NewRouteRegister(), - AccessControl: acmock, SQLStore: db, searchUsersService: searchusers.ProvideUsersService(bus, filters.ProvideOSSSearchUserFilter()), } + // Defining the accesscontrol service has to be done before registering routes + if useFakeAccessControl { + acmock = accesscontrolmock.New() + if !enableAccessControl { + acmock = acmock.WithDisabled() + } + hs.AccessControl = acmock + } else { + ac = ossaccesscontrol.ProvideService(cfg, &usagestats.UsageStatsMock{T: t}) + hs.AccessControl = ac + // Perform role registration + err := hs.declareFixedRoles() + require.NoError(t, err) + err = ac.RegisterFixedRoles() + require.NoError(t, err) + } + // Instantiate a new Server m := web.New() diff --git a/pkg/api/org_test.go b/pkg/api/org_test.go index eeafd4bcf75..afee711cae8 100644 --- a/pkg/api/org_test.go +++ b/pkg/api/org_test.go @@ -1,6 +1,7 @@ package api import ( + "context" "fmt" "net/http" "strings" @@ -9,7 +10,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" ) @@ -37,13 +40,170 @@ var ( testCreateOrgCmd = `{ "name": "TestOrg%v"}` ) -func TestAPIEndpoint_CreateOrgs_LegacyAccessControl(t *testing.T) { - sc := setupHTTPServer(t, false) +// `/api/org` endpoints test + +func TestAPIEndpoint_GetCurrentOrg_LegacyAccessControl(t *testing.T) { + sc := setupHTTPServer(t, true, 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, 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}}, sc.initCtx.OrgId) + response := callAPI(sc.server, http.MethodGet, getCurrentOrgURL, nil, t) + assert.Equal(t, http.StatusOK, response.Code) + }) + t.Run("AccessControl prevents viewing CurrentOrg with correct permissions in another org", func(t *testing.T) { + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsRead}}, 2) + response := callAPI(sc.server, http.MethodGet, getCurrentOrgURL, nil, t) + assert.Equal(t, http.StatusForbidden, response.Code) + }) + t.Run("AccessControl prevents viewing CurrentOrg with incorrect permissions", func(t *testing.T) { + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: "orgs:invalid"}}, sc.initCtx.OrgId) + response := callAPI(sc.server, http.MethodGet, getCurrentOrgURL, nil, t) + assert.Equal(t, http.StatusForbidden, response.Code) + }) +} + +func TestAPIEndpoint_PutCurrentOrg_LegacyAccessControl(t *testing.T) { + sc := setupHTTPServer(t, true, 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, true) setInitCtxSignedInViewer(sc.initCtx) + _, err := sc.db.CreateOrgWithMember("TestOrg", sc.initCtx.UserId) + 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}}, sc.initCtx.OrgId) + response := callAPI(sc.server, http.MethodPut, putCurrentOrgURL, input, t) + assert.Equal(t, http.StatusOK, response.Code) + }) + + t.Run("AccessControl prevents updating current org with correct permissions in another org", func(t *testing.T) { + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsWrite}}, 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"}}, sc.initCtx.OrgId) + response := callAPI(sc.server, http.MethodPut, putCurrentOrgURL, input, t) + assert.Equal(t, http.StatusForbidden, response.Code) + }) +} + +func TestAPIEndpoint_PutCurrentOrgAddress_LegacyAccessControl(t *testing.T) { + sc := setupHTTPServer(t, true, 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, 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}}, sc.initCtx.OrgId) + response := callAPI(sc.server, http.MethodPut, putCurrentOrgAddressURL, input, t) + assert.Equal(t, http.StatusOK, response.Code) + }) + + input = strings.NewReader(testUpdateOrgAddressForm) + t.Run("AccessControl prevents updating current org address with correct permissions in another org", func(t *testing.T) { + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsWrite}}, 2) + response := callAPI(sc.server, http.MethodPut, putCurrentOrgAddressURL, input, t) + assert.Equal(t, http.StatusForbidden, 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"}}, sc.initCtx.OrgId) + response := callAPI(sc.server, http.MethodPut, putCurrentOrgAddressURL, input, t) + assert.Equal(t, http.StatusForbidden, response.Code) + }) +} + +// `/api/orgs/` endpoints test + +// setupOrgsDBForAccessControlTests stores users and create specified number of orgs +func setupOrgsDBForAccessControlTests(t *testing.T, db sqlstore.SQLStore, user models.SignedInUser, orgsCount int) { + t.Helper() + + _, err := db.CreateUser(context.Background(), models.CreateUserCommand{Email: user.Email, SkipOrgSetup: true, Login: user.Login}) + require.NoError(t, err) + + // Create `orgsCount` orgs + for i := 1; i <= orgsCount; i++ { + _, err = db.CreateOrgWithMember(fmt.Sprintf("TestOrg%v", i), 0) + require.NoError(t, err) + err = db.AddOrgUser(context.Background(), &models.AddOrgUserCommand{LoginOrEmail: user.Login, Role: user.OrgRole, OrgId: int64(i), UserId: user.UserId}) + require.NoError(t, err) + } +} + +func TestAPIEndpoint_CreateOrgs_LegacyAccessControl(t *testing.T) { + sc := setupHTTPServer(t, true, false) + setInitCtxSignedInViewer(sc.initCtx) + setting.AllowUserOrgCreate = false input := strings.NewReader(fmt.Sprintf(testCreateOrgCmd, 2)) t.Run("Viewer cannot create Orgs", func(t *testing.T) { @@ -68,36 +228,31 @@ func TestAPIEndpoint_CreateOrgs_LegacyAccessControl(t *testing.T) { } func TestAPIEndpoint_CreateOrgs_AccessControl(t *testing.T) { - sc := setupHTTPServer(t, true) + sc := setupHTTPServer(t, true, true) setInitCtxSignedInViewer(sc.initCtx) - _, err := sc.db.CreateOrgWithMember("TestOrg", testUserID) - require.NoError(t, err) + setupOrgsDBForAccessControlTests(t, *sc.db, *sc.initCtx.SignedInUser, 0) 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}}) + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsCreate}}, accesscontrol.GlobalOrgID) 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"}}) + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: "orgs:invalid"}}, accesscontrol.GlobalOrgID) 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) + sc := setupHTTPServer(t, true, 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) + setupOrgsDBForAccessControlTests(t, *sc.db, *sc.initCtx.SignedInUser, 2) t.Run("Viewer cannot delete Orgs", func(t *testing.T) { response := callAPI(sc.server, http.MethodDelete, fmt.Sprintf(deleteOrgsURL, 2), nil, t) @@ -112,49 +267,32 @@ func TestAPIEndpoint_DeleteOrgs_LegacyAccessControl(t *testing.T) { } func TestAPIEndpoint_DeleteOrgs_AccessControl(t *testing.T) { - sc := setupHTTPServer(t, true) + sc := setupHTTPServer(t, true, 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) + setupOrgsDBForAccessControlTests(t, *sc.db, *sc.initCtx.SignedInUser, 2) - t.Run("AccessControl allows deleting Orgs with correct permissions", func(t *testing.T) { - setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsDelete, Scope: ScopeOrgsAll}}) + t.Run("AccessControl prevents deleting Orgs with incorrect permissions", func(t *testing.T) { + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: "orgs:invalid"}}, 2) 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) + assert.Equal(t, http.StatusForbidden, 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")}}) + t.Run("AccessControl prevents deleting Orgs with correct permissions in another org", func(t *testing.T) { + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsDelete}}, 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"}}) + t.Run("AccessControl allows deleting Orgs with correct permissions", func(t *testing.T) { + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsDelete}}, 2) response := callAPI(sc.server, http.MethodDelete, fmt.Sprintf(deleteOrgsURL, 2), nil, t) - assert.Equal(t, http.StatusForbidden, response.Code) + assert.Equal(t, http.StatusOK, response.Code) }) } func TestAPIEndpoint_SearchOrgs_LegacyAccessControl(t *testing.T) { - sc := setupHTTPServer(t, false) + sc := setupHTTPServer(t, true, 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) @@ -168,84 +306,32 @@ func TestAPIEndpoint_SearchOrgs_LegacyAccessControl(t *testing.T) { } func TestAPIEndpoint_SearchOrgs_AccessControl(t *testing.T) { - sc := setupHTTPServer(t, true) + sc := setupHTTPServer(t, true, 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}}) + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsRead}}, accesscontrol.GlobalOrgID) 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")}}) + t.Run("AccessControl prevents listing Orgs with correct permissions not granted globally", func(t *testing.T) { + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsRead}}, 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"}}) + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: "orgs:invalid"}}, accesscontrol.GlobalOrgID) 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) + sc := setupHTTPServer(t, true, 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) + setupOrgsDBForAccessControlTests(t, *sc.db, *sc.initCtx.SignedInUser, 2) t.Run("Viewer cannot view another Org", func(t *testing.T) { response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsURL, 2), nil, t) @@ -260,46 +346,35 @@ func TestAPIEndpoint_GetOrg_LegacyAccessControl(t *testing.T) { } func TestAPIEndpoint_GetOrg_AccessControl(t *testing.T) { - sc := setupHTTPServer(t, true) + sc := setupHTTPServer(t, true, 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) + setupOrgsDBForAccessControlTests(t, *sc.db, *sc.initCtx.SignedInUser, 2) t.Run("AccessControl allows viewing another org with correct permissions", func(t *testing.T) { - setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsRead, Scope: ScopeOrgsAll}}) + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsRead}}, 2) 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")}}) + t.Run("AccessControl prevents viewing another org with correct permissions in another org", func(t *testing.T) { + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsRead}}, 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"}}) + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: "orgs:invalid"}}, 2) 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) + sc := setupHTTPServer(t, true, 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) + setupOrgsDBForAccessControlTests(t, *sc.db, *sc.initCtx.SignedInUser, 2) t.Run("Viewer cannot view another Org", func(t *testing.T) { response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsByNameURL, "TestOrg2"), nil, t) @@ -314,101 +389,30 @@ func TestAPIEndpoint_GetOrgByName_LegacyAccessControl(t *testing.T) { } func TestAPIEndpoint_GetOrgByName_AccessControl(t *testing.T) { - sc := setupHTTPServer(t, true) + sc := setupHTTPServer(t, true, 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) + setupOrgsDBForAccessControlTests(t, *sc.db, *sc.initCtx.SignedInUser, 2) 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")}}) + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsRead}}, accesscontrol.GlobalOrgID) 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"}}) + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: "orgs:invalid"}}, accesscontrol.GlobalOrgID) 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) + sc := setupHTTPServer(t, true, 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) + setupOrgsDBForAccessControlTests(t, *sc.db, *sc.initCtx.SignedInUser, 2) input := strings.NewReader(testUpdateOrgNameForm) @@ -425,101 +429,38 @@ func TestAPIEndpoint_PutOrg_LegacyAccessControl(t *testing.T) { } func TestAPIEndpoint_PutOrg_AccessControl(t *testing.T) { - sc := setupHTTPServer(t, true) + sc := setupHTTPServer(t, true, 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) + setupOrgsDBForAccessControlTests(t, *sc.db, *sc.initCtx.SignedInUser, 2) 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")}}) + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsWrite}}, 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")}}) + t.Run("AccessControl prevents updating another org with correct permissions in another org", func(t *testing.T) { + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsWrite}}, 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"}}) + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: "orgs:invalid"}}, 2) 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) + sc := setupHTTPServer(t, true, 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) + setupOrgsDBForAccessControlTests(t, *sc.db, *sc.initCtx.SignedInUser, 2) input := strings.NewReader(testUpdateOrgAddressForm) @@ -536,31 +477,28 @@ func TestAPIEndpoint_PutOrgAddress_LegacyAccessControl(t *testing.T) { } func TestAPIEndpoint_PutOrgAddress_AccessControl(t *testing.T) { - sc := setupHTTPServer(t, true) + sc := setupHTTPServer(t, true, 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) + setupOrgsDBForAccessControlTests(t, *sc.db, *sc.initCtx.SignedInUser, 2) 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}}) + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsWrite}}, 2) 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")}}) + t.Run("AccessControl prevents updating another org address with correct permissions in the current org", func(t *testing.T) { + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsWrite}}, 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"}}) + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: "orgs:invalid"}}, 2) response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsAddressURL, 2), input, t) assert.Equal(t, http.StatusForbidden, response.Code) }) diff --git a/pkg/api/org_users.go b/pkg/api/org_users.go index a536bd54e03..0b08d38211f 100644 --- a/pkg/api/org_users.go +++ b/pkg/api/org_users.go @@ -6,30 +6,29 @@ 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/models" "github.com/grafana/grafana/pkg/util" ) // POST /api/org/users -func AddOrgUserToCurrentOrg(c *models.ReqContext, cmd models.AddOrgUserCommand) response.Response { +func (hs *HTTPServer) AddOrgUserToCurrentOrg(c *models.ReqContext, cmd models.AddOrgUserCommand) response.Response { cmd.OrgId = c.OrgId - return addOrgUserHelper(c.Req.Context(), cmd) + return hs.addOrgUserHelper(c.Req.Context(), cmd) } // POST /api/orgs/:orgId/users -func AddOrgUser(c *models.ReqContext, cmd models.AddOrgUserCommand) response.Response { +func (hs *HTTPServer) AddOrgUser(c *models.ReqContext, cmd models.AddOrgUserCommand) response.Response { cmd.OrgId = c.ParamsInt64(":orgId") - return addOrgUserHelper(c.Req.Context(), cmd) + return hs.addOrgUserHelper(c.Req.Context(), cmd) } -func addOrgUserHelper(ctx context.Context, cmd models.AddOrgUserCommand) response.Response { +func (hs *HTTPServer) addOrgUserHelper(ctx context.Context, cmd models.AddOrgUserCommand) response.Response { if !cmd.Role.IsValid() { return response.Error(400, "Invalid role specified", nil) } userQuery := models.GetUserByLoginQuery{LoginOrEmail: cmd.LoginOrEmail} - err := bus.DispatchCtx(ctx, &userQuery) + err := hs.SQLStore.GetUserByLogin(ctx, &userQuery) if err != nil { return response.Error(404, "User not found", nil) } @@ -38,7 +37,7 @@ func addOrgUserHelper(ctx context.Context, cmd models.AddOrgUserCommand) respons cmd.UserId = userToAdd.Id - if err := bus.DispatchCtx(ctx, &cmd); err != nil { + if err := hs.SQLStore.AddOrgUser(ctx, &cmd); err != nil { if errors.Is(err, models.ErrOrgUserAlreadyAdded) { return response.JSON(409, util.DynMap{ "message": "User is already member of this organization", @@ -169,24 +168,24 @@ func (hs *HTTPServer) SearchOrgUsersWithPaging(ctx context.Context, c *models.Re } // PATCH /api/org/users/:userId -func UpdateOrgUserForCurrentOrg(c *models.ReqContext, cmd models.UpdateOrgUserCommand) response.Response { +func (hs *HTTPServer) UpdateOrgUserForCurrentOrg(c *models.ReqContext, cmd models.UpdateOrgUserCommand) response.Response { cmd.OrgId = c.OrgId cmd.UserId = c.ParamsInt64(":userId") - return updateOrgUserHelper(c.Req.Context(), cmd) + return hs.updateOrgUserHelper(c.Req.Context(), cmd) } // PATCH /api/orgs/:orgId/users/:userId -func UpdateOrgUser(c *models.ReqContext, cmd models.UpdateOrgUserCommand) response.Response { +func (hs *HTTPServer) UpdateOrgUser(c *models.ReqContext, cmd models.UpdateOrgUserCommand) response.Response { cmd.OrgId = c.ParamsInt64(":orgId") cmd.UserId = c.ParamsInt64(":userId") - return updateOrgUserHelper(c.Req.Context(), cmd) + return hs.updateOrgUserHelper(c.Req.Context(), cmd) } -func updateOrgUserHelper(ctx context.Context, cmd models.UpdateOrgUserCommand) response.Response { +func (hs *HTTPServer) updateOrgUserHelper(ctx context.Context, cmd models.UpdateOrgUserCommand) response.Response { if !cmd.Role.IsValid() { return response.Error(400, "Invalid role specified", nil) } - if err := bus.DispatchCtx(ctx, &cmd); err != nil { + if err := hs.SQLStore.UpdateOrgUser(ctx, &cmd); err != nil { if errors.Is(err, models.ErrLastOrgAdmin) { return response.Error(400, "Cannot change role so that there is no organization admin left", nil) } @@ -197,8 +196,8 @@ func updateOrgUserHelper(ctx context.Context, cmd models.UpdateOrgUserCommand) r } // DELETE /api/org/users/:userId -func RemoveOrgUserForCurrentOrg(c *models.ReqContext) response.Response { - return removeOrgUserHelper(c.Req.Context(), &models.RemoveOrgUserCommand{ +func (hs *HTTPServer) RemoveOrgUserForCurrentOrg(c *models.ReqContext) response.Response { + return hs.removeOrgUserHelper(c.Req.Context(), &models.RemoveOrgUserCommand{ UserId: c.ParamsInt64(":userId"), OrgId: c.OrgId, ShouldDeleteOrphanedUser: true, @@ -206,15 +205,15 @@ func RemoveOrgUserForCurrentOrg(c *models.ReqContext) response.Response { } // DELETE /api/orgs/:orgId/users/:userId -func RemoveOrgUser(c *models.ReqContext) response.Response { - return removeOrgUserHelper(c.Req.Context(), &models.RemoveOrgUserCommand{ +func (hs *HTTPServer) RemoveOrgUser(c *models.ReqContext) response.Response { + return hs.removeOrgUserHelper(c.Req.Context(), &models.RemoveOrgUserCommand{ UserId: c.ParamsInt64(":userId"), OrgId: c.ParamsInt64(":orgId"), }) } -func removeOrgUserHelper(ctx context.Context, cmd *models.RemoveOrgUserCommand) response.Response { - if err := bus.DispatchCtx(ctx, cmd); err != nil { +func (hs *HTTPServer) removeOrgUserHelper(ctx context.Context, cmd *models.RemoveOrgUserCommand) response.Response { + if err := hs.SQLStore.RemoveOrgUser(ctx, cmd); err != nil { if errors.Is(err, models.ErrLastOrgAdmin) { return response.Error(400, "Cannot remove last organization admin", nil) } diff --git a/pkg/api/org_users_test.go b/pkg/api/org_users_test.go index 4115e456e6c..fb506bdfaab 100644 --- a/pkg/api/org_users_test.go +++ b/pkg/api/org_users_test.go @@ -3,27 +3,22 @@ package api import ( "context" "encoding/json" + "fmt" "net/http" - "net/http/httptest" + "strings" "testing" "time" - "github.com/grafana/grafana/pkg/services/searchusers/filters" - - "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/services/searchusers" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/api/dtos" - "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" - accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" - "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/util" ) func setUpGetOrgUsersDB(t *testing.T, sqlStore *sqlstore.SQLStore) { @@ -137,39 +132,9 @@ func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) { }) } -func setupOrgUsersAPIcontext(t *testing.T, role models.RoleType) (*scenarioContext, *sqlstore.SQLStore) { - cfg := setting.NewCfg() - db := sqlstore.InitTestDB(t) - - hs := &HTTPServer{ - Cfg: cfg, - QuotaService: "a.QuotaService{Cfg: cfg}, - RouteRegister: routing.NewRouteRegister(), - AccessControl: accesscontrolmock.New().WithDisabled(), - SQLStore: db, - searchUsersService: searchusers.ProvideUsersService(bus.New(), filters.ProvideOSSSearchUserFilter()), - } - - sc := setupScenarioContext(t, "/api/org/users/lookup") - // Create a middleware to pretend user is logged in - pretendSignInMiddleware := func(c *models.ReqContext) { - sc.context = c - sc.context.UserId = testUserID - sc.context.OrgId = testOrgID - sc.context.Login = testUserLogin - sc.context.OrgRole = role - sc.context.IsSignedIn = true - } - sc.m.Use(pretendSignInMiddleware) - - hs.registerRoutes() - hs.RouteRegister.Register(sc.m.Router) - - return sc, db -} - func TestOrgUsersAPIEndpoint_LegacyAccessControl_FolderAdmin(t *testing.T) { - sc, db := setupOrgUsersAPIcontext(t, models.ROLE_VIEWER) + sc := setupHTTPServer(t, true, false) + setInitCtxSignedInViewer(sc.initCtx) // Create a dashboard folder cmd := models.SaveDashboardCommand{ @@ -182,7 +147,7 @@ func TestOrgUsersAPIEndpoint_LegacyAccessControl_FolderAdmin(t *testing.T) { "tags": "prod", }), } - folder, err := db.SaveDashboard(cmd) + folder, err := sc.db.SaveDashboard(cmd) require.NoError(t, err) require.NotNil(t, folder) @@ -197,58 +162,41 @@ func TestOrgUsersAPIEndpoint_LegacyAccessControl_FolderAdmin(t *testing.T) { Updated: time.Now(), }, } - err = db.UpdateDashboardACL(folder.Id, acls) - require.NoError(t, err) - - sc.resp = httptest.NewRecorder() - - sc.req, err = http.NewRequest(http.MethodGet, "/api/org/users/lookup", nil) + err = sc.db.UpdateDashboardACL(folder.Id, acls) require.NoError(t, err) - sc.exec() - assert.Equal(t, http.StatusOK, sc.resp.Code) + response := callAPI(sc.server, http.MethodGet, "/api/org/users/lookup", nil, t) + assert.Equal(t, http.StatusOK, response.Code) } func TestOrgUsersAPIEndpoint_LegacyAccessControl_TeamAdmin(t *testing.T) { - sc, db := setupOrgUsersAPIcontext(t, models.ROLE_VIEWER) + sc := setupHTTPServer(t, true, false) + setInitCtxSignedInViewer(sc.initCtx) // Setup store teams - team1, err := db.CreateTeam("testteam1", "testteam1@example.org", testOrgID) + team1, err := sc.db.CreateTeam("testteam1", "testteam1@example.org", testOrgID) require.NoError(t, err) - err = db.AddTeamMember(testUserID, testOrgID, team1.Id, false, models.PERMISSION_ADMIN) + err = sc.db.AddTeamMember(testUserID, testOrgID, team1.Id, false, models.PERMISSION_ADMIN) require.NoError(t, err) - sc.resp = httptest.NewRecorder() - - sc.req, err = http.NewRequest(http.MethodGet, "/api/org/users/lookup", nil) - require.NoError(t, err) - - sc.exec() - assert.Equal(t, http.StatusOK, sc.resp.Code) + response := callAPI(sc.server, http.MethodGet, "/api/org/users/lookup", nil, t) + assert.Equal(t, http.StatusOK, response.Code) } func TestOrgUsersAPIEndpoint_LegacyAccessControl_Admin(t *testing.T) { - sc, _ := setupOrgUsersAPIcontext(t, models.ROLE_ADMIN) - sc.resp = httptest.NewRecorder() - - var err error - sc.req, err = http.NewRequest(http.MethodGet, "/api/org/users/lookup", nil) - require.NoError(t, err) + sc := setupHTTPServer(t, true, false) + setInitCtxSignedInOrgAdmin(sc.initCtx) - sc.exec() - assert.Equal(t, http.StatusOK, sc.resp.Code) + response := callAPI(sc.server, http.MethodGet, "/api/org/users/lookup", nil, t) + assert.Equal(t, http.StatusOK, response.Code) } func TestOrgUsersAPIEndpoint_LegacyAccessControl_Viewer(t *testing.T) { - sc, _ := setupOrgUsersAPIcontext(t, models.ROLE_VIEWER) - sc.resp = httptest.NewRecorder() - - var err error - sc.req, err = http.NewRequest(http.MethodGet, "/api/org/users/lookup", nil) - require.NoError(t, err) + sc := setupHTTPServer(t, true, false) + setInitCtxSignedInViewer(sc.initCtx) - sc.exec() - assert.Equal(t, http.StatusForbidden, sc.resp.Code) + response := callAPI(sc.server, http.MethodGet, "/api/org/users/lookup", nil, t) + assert.Equal(t, http.StatusForbidden, response.Code) } func TestOrgUsersAPIEndpoint_AccessControl(t *testing.T) { @@ -271,29 +219,534 @@ func TestOrgUsersAPIEndpoint_AccessControl(t *testing.T) { for _, test := range tests { t.Run(test.desc, func(t *testing.T) { - cfg := setting.NewCfg() - sc, hs := setupAccessControlScenarioContext(t, cfg, test.url, test.permissions) - - // Create a middleware to pretend user is logged in - pretendSignInMiddleware := func(c *models.ReqContext) { - sc.context = c - sc.context.UserId = testUserID - sc.context.OrgId = testOrgID - sc.context.Login = testUserLogin - sc.context.OrgRole = models.ROLE_VIEWER - sc.context.IsSignedIn = true + sc := setupHTTPServer(t, true, true) + setInitCtxSignedInViewer(sc.initCtx) + setAccessControlPermissions(sc.acmock, test.permissions, sc.initCtx.OrgId) + + response := callAPI(sc.server, http.MethodGet, test.url, nil, t) + assert.Equal(t, test.expectedCode, response.Code) + }) + } +} + +var ( + testServerAdminViewer = models.SignedInUser{ + UserId: 1, + OrgId: 1, + OrgName: "TestOrg1", + OrgRole: models.ROLE_VIEWER, + Login: "testServerAdmin", + Name: "testServerAdmin", + Email: "testServerAdmin@example.org", + OrgCount: 2, + IsGrafanaAdmin: true, + IsAnonymous: false, + } + + testAdminOrg2 = models.SignedInUser{ + UserId: 2, + OrgId: 2, + OrgName: "TestOrg2", + OrgRole: models.ROLE_ADMIN, + Login: "testAdmin", + Name: "testAdmin", + Email: "testAdmin@example.org", + OrgCount: 1, + IsGrafanaAdmin: false, + IsAnonymous: false, + } + + testEditorOrg1 = models.SignedInUser{ + UserId: 3, + OrgId: 1, + OrgName: "TestOrg1", + OrgRole: models.ROLE_EDITOR, + Login: "testEditor", + Name: "testEditor", + Email: "testEditor@example.org", + OrgCount: 1, + IsGrafanaAdmin: false, + IsAnonymous: false, + } +) + +// setupOrgUsersDBForAccessControlTests creates three users placed in two orgs +// Org1: testServerAdminViewer, testEditorOrg1 +// Org2: testServerAdminViewer, testAdminOrg2 +func setupOrgUsersDBForAccessControlTests(t *testing.T, db sqlstore.SQLStore) { + t.Helper() + + var err error + + _, err = db.CreateUser(context.Background(), models.CreateUserCommand{Email: testServerAdminViewer.Email, SkipOrgSetup: true, Login: testServerAdminViewer.Login}) + require.NoError(t, err) + _, err = db.CreateUser(context.Background(), models.CreateUserCommand{Email: testAdminOrg2.Email, SkipOrgSetup: true, Login: testAdminOrg2.Login}) + require.NoError(t, err) + _, err = db.CreateUser(context.Background(), models.CreateUserCommand{Email: testEditorOrg1.Email, SkipOrgSetup: true, Login: testEditorOrg1.Login}) + require.NoError(t, err) + + // Create both orgs with server admin + _, err = db.CreateOrgWithMember(testServerAdminViewer.OrgName, testServerAdminViewer.UserId) + require.NoError(t, err) + _, err = db.CreateOrgWithMember(testAdminOrg2.OrgName, testServerAdminViewer.UserId) + require.NoError(t, err) + + err = db.AddOrgUser(context.Background(), &models.AddOrgUserCommand{LoginOrEmail: testAdminOrg2.Login, Role: testAdminOrg2.OrgRole, OrgId: testAdminOrg2.OrgId, UserId: testAdminOrg2.UserId}) + require.NoError(t, err) + err = db.AddOrgUser(context.Background(), &models.AddOrgUserCommand{LoginOrEmail: testEditorOrg1.Login, Role: testEditorOrg1.OrgRole, OrgId: testEditorOrg1.OrgId, UserId: testEditorOrg1.UserId}) + require.NoError(t, err) +} + +func TestGetOrgUsersAPIEndpoint_AccessControl(t *testing.T) { + url := "/api/orgs/%v/users/" + type testCase struct { + name string + enableAccessControl bool + expectedCode int + expectedUserCount int + user models.SignedInUser + targetOrg int64 + } + + tests := []testCase{ + { + name: "server admin can get users in his org (legacy)", + enableAccessControl: false, + expectedCode: http.StatusOK, + expectedUserCount: 2, + user: testServerAdminViewer, + targetOrg: testServerAdminViewer.OrgId, + }, + { + name: "server admin can get users in another org (legacy)", + enableAccessControl: false, + expectedCode: http.StatusOK, + expectedUserCount: 2, + user: testServerAdminViewer, + targetOrg: 2, + }, + { + name: "org admin cannot get users in his org (legacy)", + enableAccessControl: false, + expectedCode: http.StatusForbidden, + user: testAdminOrg2, + targetOrg: testAdminOrg2.OrgId, + }, + { + name: "org admin cannot get users in another org (legacy)", + enableAccessControl: false, + expectedCode: http.StatusForbidden, + user: testAdminOrg2, + targetOrg: 1, + }, + { + name: "server admin can get users in his org", + enableAccessControl: true, + expectedCode: http.StatusOK, + expectedUserCount: 2, + user: testServerAdminViewer, + targetOrg: testServerAdminViewer.OrgId, + }, + { + name: "server admin can get users in another org", + enableAccessControl: true, + expectedCode: http.StatusOK, + expectedUserCount: 2, + user: testServerAdminViewer, + targetOrg: 2, + }, + { + name: "org admin can get users in his org", + enableAccessControl: true, + expectedCode: http.StatusOK, + expectedUserCount: 2, + user: testAdminOrg2, + targetOrg: testAdminOrg2.OrgId, + }, + { + name: "org admin cannot get users in another org", + enableAccessControl: true, + expectedCode: http.StatusForbidden, + user: testAdminOrg2, + targetOrg: 1, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + sc := setupHTTPServer(t, false, tc.enableAccessControl) + setupOrgUsersDBForAccessControlTests(t, *sc.db) + setInitCtxSignedInUser(sc.initCtx, tc.user) + + // Perform test + response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(url, tc.targetOrg), nil, t) + require.Equal(t, tc.expectedCode, response.Code) + + if tc.expectedCode != http.StatusForbidden { + var userList []*models.OrgUserDTO + err := json.NewDecoder(response.Body).Decode(&userList) + require.NoError(t, err) + + assert.Len(t, userList, tc.expectedUserCount) + } + }) + } +} + +func TestPostOrgUsersAPIEndpoint_AccessControl(t *testing.T) { + url := "/api/orgs/%v/users/" + type testCase struct { + name string + enableAccessControl bool + user models.SignedInUser + targetOrg int64 + input string + expectedCode int + expectedMessage util.DynMap + expectedUserCount int + } + + tests := []testCase{ + { + name: "server admin can add users to his org (legacy)", + enableAccessControl: false, + user: testServerAdminViewer, + targetOrg: testServerAdminViewer.OrgId, + input: `{"loginOrEmail": "` + testAdminOrg2.Login + `", "role": "` + string(testAdminOrg2.OrgRole) + `"}`, + expectedCode: http.StatusOK, + expectedMessage: util.DynMap{"message": "User added to organization", "userId": float64(testAdminOrg2.UserId)}, + expectedUserCount: 3, + }, + { + name: "server admin can add users to another org (legacy)", + enableAccessControl: false, + user: testServerAdminViewer, + targetOrg: 2, + input: `{"loginOrEmail": "` + testEditorOrg1.Login + `", "role": "` + string(testEditorOrg1.OrgRole) + `"}`, + expectedCode: http.StatusOK, + expectedMessage: util.DynMap{"message": "User added to organization", "userId": float64(testEditorOrg1.UserId)}, + expectedUserCount: 3, + }, + { + name: "org admin cannot add users to his org (legacy)", + enableAccessControl: false, + expectedCode: http.StatusForbidden, + user: testAdminOrg2, + targetOrg: testAdminOrg2.OrgId, + input: `{"loginOrEmail": "` + testEditorOrg1.Login + `", "role": "` + string(testEditorOrg1.OrgRole) + `"}`, + }, + { + name: "org admin cannot add users to another org (legacy)", + enableAccessControl: false, + expectedCode: http.StatusForbidden, + user: testAdminOrg2, + targetOrg: 1, + input: `{"loginOrEmail": "` + testAdminOrg2.Login + `", "role": "` + string(testAdminOrg2.OrgRole) + `"}`, + }, + { + name: "server admin can add users to his org", + enableAccessControl: true, + user: testServerAdminViewer, + targetOrg: testServerAdminViewer.OrgId, + input: `{"loginOrEmail": "` + testAdminOrg2.Login + `", "role": "` + string(testAdminOrg2.OrgRole) + `"}`, + expectedCode: http.StatusOK, + expectedMessage: util.DynMap{"message": "User added to organization", "userId": float64(testAdminOrg2.UserId)}, + expectedUserCount: 3, + }, + { + name: "server admin can add users to another org", + enableAccessControl: true, + user: testServerAdminViewer, + targetOrg: 2, + input: `{"loginOrEmail": "` + testEditorOrg1.Login + `", "role": "` + string(testEditorOrg1.OrgRole) + `"}`, + expectedCode: http.StatusOK, + expectedMessage: util.DynMap{"message": "User added to organization", "userId": float64(testEditorOrg1.UserId)}, + expectedUserCount: 3, + }, + { + name: "org admin can add users to his org", + enableAccessControl: true, + user: testAdminOrg2, + targetOrg: testAdminOrg2.OrgId, + input: `{"loginOrEmail": "` + testEditorOrg1.Login + `", "role": "` + string(testEditorOrg1.OrgRole) + `"}`, + expectedCode: http.StatusOK, + expectedMessage: util.DynMap{"message": "User added to organization", "userId": float64(testEditorOrg1.UserId)}, + expectedUserCount: 3, + }, + { + name: "org admin cannot add users to another org", + enableAccessControl: true, + expectedCode: http.StatusForbidden, + user: testAdminOrg2, + targetOrg: 1, + input: `{"loginOrEmail": "` + testAdminOrg2.Login + `", "role": "` + string(testAdminOrg2.OrgRole) + `"}`, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + sc := setupHTTPServer(t, false, tc.enableAccessControl) + setupOrgUsersDBForAccessControlTests(t, *sc.db) + setInitCtxSignedInUser(sc.initCtx, tc.user) + + // Perform request + input := strings.NewReader(tc.input) + response := callAPI(sc.server, http.MethodPost, fmt.Sprintf(url, tc.targetOrg), input, t) + assert.Equal(t, tc.expectedCode, response.Code) + + if tc.expectedCode != http.StatusForbidden { + // Check result + var message util.DynMap + err := json.NewDecoder(response.Body).Decode(&message) + require.NoError(t, err) + assert.EqualValuesf(t, tc.expectedMessage, message, "server did not answer expected message") + + getUsersQuery := models.GetOrgUsersQuery{OrgId: tc.targetOrg} + err = sc.db.GetOrgUsers(context.Background(), &getUsersQuery) + require.NoError(t, err) + assert.Len(t, getUsersQuery.Result, tc.expectedUserCount) } - sc.m.Use(pretendSignInMiddleware) + }) + } +} - sc.resp = httptest.NewRecorder() - hs.SettingsProvider = &setting.OSSImpl{Cfg: cfg} +func TestPatchOrgUsersAPIEndpoint_AccessControl(t *testing.T) { + url := "/api/orgs/%v/users/%v" + type testCase struct { + name string + enableAccessControl bool + user models.SignedInUser + targetUserId int64 + targetOrg int64 + input string + expectedCode int + expectedMessage util.DynMap + expectedUserRole models.RoleType + } - var err error - sc.req, err = http.NewRequest(test.method, test.url, nil) - require.NoError(t, err) + tests := []testCase{ + { + name: "server admin can update users in his org (legacy)", + enableAccessControl: false, + user: testServerAdminViewer, + targetUserId: testEditorOrg1.UserId, + targetOrg: testServerAdminViewer.OrgId, + input: `{"role": "Viewer"}`, + expectedCode: http.StatusOK, + expectedMessage: util.DynMap{"message": "Organization user updated"}, + expectedUserRole: models.ROLE_VIEWER, + }, + { + name: "server admin can update users in another org (legacy)", + enableAccessControl: false, + user: testServerAdminViewer, + targetUserId: testServerAdminViewer.UserId, + targetOrg: 2, + input: `{"role": "Editor"}`, + expectedCode: http.StatusOK, + expectedMessage: util.DynMap{"message": "Organization user updated"}, + expectedUserRole: models.ROLE_EDITOR, + }, + { + name: "org admin cannot update users in his org (legacy)", + enableAccessControl: false, + user: testAdminOrg2, + targetUserId: testServerAdminViewer.UserId, + targetOrg: testAdminOrg2.OrgId, + input: `{"role": "Editor"}`, + expectedCode: http.StatusForbidden, + }, + { + name: "org admin cannot update users in another org (legacy)", + enableAccessControl: false, + user: testAdminOrg2, + targetUserId: testServerAdminViewer.UserId, + targetOrg: 1, + input: `{"role": "Editor"}`, + expectedCode: http.StatusForbidden, + }, + { + name: "server admin can update users in his org", + enableAccessControl: true, + user: testServerAdminViewer, + targetUserId: testEditorOrg1.UserId, + targetOrg: testServerAdminViewer.OrgId, + input: `{"role": "Viewer"}`, + expectedCode: http.StatusOK, + expectedMessage: util.DynMap{"message": "Organization user updated"}, + expectedUserRole: models.ROLE_VIEWER, + }, + { + name: "server admin can update users in another org", + enableAccessControl: true, + user: testServerAdminViewer, + targetUserId: testServerAdminViewer.UserId, + targetOrg: 2, + input: `{"role": "Editor"}`, + expectedCode: http.StatusOK, + expectedMessage: util.DynMap{"message": "Organization user updated"}, + expectedUserRole: models.ROLE_EDITOR, + }, + { + name: "org admin can update users in his org", + enableAccessControl: true, + user: testAdminOrg2, + targetUserId: testServerAdminViewer.UserId, + targetOrg: testAdminOrg2.OrgId, + input: `{"role": "Editor"}`, + expectedCode: http.StatusOK, + expectedMessage: util.DynMap{"message": "Organization user updated"}, + expectedUserRole: models.ROLE_EDITOR, + }, + { + name: "org admin cannot update users in another org", + enableAccessControl: true, + user: testAdminOrg2, + targetUserId: testServerAdminViewer.UserId, + targetOrg: 1, + input: `{"role": "Editor"}`, + expectedCode: http.StatusForbidden, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + sc := setupHTTPServer(t, false, tc.enableAccessControl) + setupOrgUsersDBForAccessControlTests(t, *sc.db) + setInitCtxSignedInUser(sc.initCtx, tc.user) + + // Perform request + input := strings.NewReader(tc.input) + setInitCtxSignedInUser(sc.initCtx, tc.user) + response := callAPI(sc.server, http.MethodPatch, fmt.Sprintf(url, tc.targetOrg, tc.targetUserId), input, t) + assert.Equal(t, tc.expectedCode, response.Code) + + if tc.expectedCode != http.StatusForbidden { + // Check result + var message util.DynMap + err := json.NewDecoder(response.Body).Decode(&message) + require.NoError(t, err) + assert.Equal(t, tc.expectedMessage, message) - sc.exec() - assert.Equal(t, test.expectedCode, sc.resp.Code) + getUserQuery := models.GetSignedInUserQuery{ + UserId: tc.targetUserId, + OrgId: tc.targetOrg, + } + err = sqlstore.GetSignedInUser(context.TODO(), &getUserQuery) + require.NoError(t, err) + assert.Equal(t, tc.expectedUserRole, getUserQuery.Result.OrgRole) + } + }) + } +} + +func TestDeleteOrgUsersAPIEndpoint_AccessControl(t *testing.T) { + url := "/api/orgs/%v/users/%v" + type testCase struct { + name string + enableAccessControl bool + user models.SignedInUser + targetUserId int64 + targetOrg int64 + expectedCode int + expectedMessage util.DynMap + expectedUserCount int + } + + tests := []testCase{ + { + name: "server admin can delete users from his org (legacy)", + enableAccessControl: false, + user: testServerAdminViewer, + targetUserId: testEditorOrg1.UserId, + targetOrg: testServerAdminViewer.OrgId, + expectedCode: http.StatusOK, + expectedMessage: util.DynMap{"message": "User removed from organization"}, + expectedUserCount: 1, + }, + { + name: "server admin can delete users from another org (legacy)", + enableAccessControl: false, + user: testServerAdminViewer, + targetUserId: testServerAdminViewer.UserId, + targetOrg: 2, + expectedCode: http.StatusOK, + expectedMessage: util.DynMap{"message": "User removed from organization"}, + expectedUserCount: 1, + }, + { + name: "org admin can delete users from his org (legacy)", + enableAccessControl: false, + user: testAdminOrg2, + targetUserId: testServerAdminViewer.UserId, + targetOrg: testAdminOrg2.OrgId, + expectedCode: http.StatusForbidden, + }, + { + name: "org admin cannot delete users from another org (legacy)", + enableAccessControl: false, + user: testAdminOrg2, + targetUserId: testEditorOrg1.UserId, + targetOrg: 1, + expectedCode: http.StatusForbidden, + }, + { + name: "server admin can delete users from his org", + enableAccessControl: true, + user: testServerAdminViewer, + targetUserId: testEditorOrg1.UserId, + targetOrg: testServerAdminViewer.OrgId, + expectedCode: http.StatusOK, + expectedMessage: util.DynMap{"message": "User removed from organization"}, + expectedUserCount: 1, + }, + { + name: "server admin can delete users from another org", + enableAccessControl: true, + user: testServerAdminViewer, + targetUserId: testServerAdminViewer.UserId, + targetOrg: 2, + expectedCode: http.StatusOK, + expectedMessage: util.DynMap{"message": "User removed from organization"}, + expectedUserCount: 1, + }, + { + name: "org admin can delete users from his org", + enableAccessControl: true, + user: testAdminOrg2, + targetUserId: testServerAdminViewer.UserId, + targetOrg: testAdminOrg2.OrgId, + expectedCode: http.StatusOK, + expectedMessage: util.DynMap{"message": "User removed from organization"}, + expectedUserCount: 1, + }, + { + name: "org admin cannot delete users from another org", + enableAccessControl: true, + user: testAdminOrg2, + targetUserId: testEditorOrg1.UserId, + targetOrg: 1, + expectedCode: http.StatusForbidden, + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + sc := setupHTTPServer(t, false, tc.enableAccessControl) + setupOrgUsersDBForAccessControlTests(t, *sc.db) + setInitCtxSignedInUser(sc.initCtx, tc.user) + + response := callAPI(sc.server, http.MethodDelete, fmt.Sprintf(url, tc.targetOrg, tc.targetUserId), nil, t) + assert.Equal(t, tc.expectedCode, response.Code) + + if tc.expectedCode != http.StatusForbidden { + // Check result + var message util.DynMap + err := json.NewDecoder(response.Body).Decode(&message) + require.NoError(t, err) + assert.Equal(t, tc.expectedMessage, message) + + getUsersQuery := models.GetOrgUsersQuery{OrgId: tc.targetOrg} + err = sc.db.GetOrgUsers(context.Background(), &getUsersQuery) + require.NoError(t, err) + assert.Len(t, getUsersQuery.Result, tc.expectedUserCount) + } }) } } diff --git a/pkg/api/preferences_test.go b/pkg/api/preferences_test.go index fdba4bbaed0..5cc5c69c6b9 100644 --- a/pkg/api/preferences_test.go +++ b/pkg/api/preferences_test.go @@ -18,7 +18,7 @@ var ( ) func TestAPIEndpoint_GetCurrentOrgPreferences_LegacyAccessControl(t *testing.T) { - sc := setupHTTPServer(t, false) + sc := setupHTTPServer(t, true, false) _, err := sc.db.CreateOrgWithMember("TestOrg", testUserID) require.NoError(t, err) @@ -37,31 +37,31 @@ func TestAPIEndpoint_GetCurrentOrgPreferences_LegacyAccessControl(t *testing.T) } func TestAPIEndpoint_GetCurrentOrgPreferences_AccessControl(t *testing.T) { - sc := setupHTTPServer(t, true) + sc := setupHTTPServer(t, true, 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}}) + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsPreferencesRead}}, sc.initCtx.OrgId) 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")}}) + t.Run("AccessControl prevents getting org preferences with correct permissions in another org", func(t *testing.T) { + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsPreferencesRead}}, 2) response := callAPI(sc.server, http.MethodGet, getOrgPreferencesURL, nil, t) - assert.Equal(t, http.StatusOK, response.Code) + assert.Equal(t, http.StatusForbidden, response.Code) }) t.Run("AccessControl prevents getting org preferences with incorrect permissions", func(t *testing.T) { - setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: "orgs:invalid"}}) + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: "orgs:invalid"}}, sc.initCtx.OrgId) 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) + sc := setupHTTPServer(t, true, false) _, err := sc.db.CreateOrgWithMember("TestOrg", testUserID) require.NoError(t, err) @@ -82,7 +82,7 @@ func TestAPIEndpoint_PutCurrentOrgPreferences_LegacyAccessControl(t *testing.T) } func TestAPIEndpoint_PutCurrentOrgPreferences_AccessControl(t *testing.T) { - sc := setupHTTPServer(t, true) + sc := setupHTTPServer(t, true, true) setInitCtxSignedInViewer(sc.initCtx) _, err := sc.db.CreateOrgWithMember("TestOrg", testUserID) @@ -90,21 +90,21 @@ func TestAPIEndpoint_PutCurrentOrgPreferences_AccessControl(t *testing.T) { 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}}) + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsPreferencesWrite}}, sc.initCtx.OrgId) 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")}}) + t.Run("AccessControl prevents updating org preferences with correct permissions in another org", func(t *testing.T) { + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsPreferencesWrite}}, 2) response := callAPI(sc.server, http.MethodPut, putOrgPreferencesURL, input, t) - assert.Equal(t, http.StatusOK, response.Code) + assert.Equal(t, http.StatusForbidden, 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"}}) + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: "orgs:invalid"}}, sc.initCtx.OrgId) response := callAPI(sc.server, http.MethodPut, putOrgPreferencesURL, input, t) assert.Equal(t, http.StatusForbidden, response.Code) }) diff --git a/pkg/api/quota_test.go b/pkg/api/quota_test.go index 0cc7c1571e2..3deaab02bf7 100644 --- a/pkg/api/quota_test.go +++ b/pkg/api/quota_test.go @@ -7,7 +7,6 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/setting" @@ -29,17 +28,24 @@ var testOrgQuota = setting.OrgQuota{ AlertRule: 10, } -func TestAPIEndpoint_GetCurrentOrgQuotas_LegacyAccessControl(t *testing.T) { - sc := setupHTTPServer(t, false) - setInitCtxSignedInViewer(sc.initCtx) +// setupDBAndSettingsForAccessControlQuotaTests stores users and create two orgs +func setupDBAndSettingsForAccessControlQuotaTests(t *testing.T, sc accessControlScenarioContext) { + t.Helper() 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) + // Create two orgs with the context user + setupOrgsDBForAccessControlTests(t, *sc.db, *sc.initCtx.SignedInUser, 2) +} + +func TestAPIEndpoint_GetCurrentOrgQuotas_LegacyAccessControl(t *testing.T) { + sc := setupHTTPServer(t, true, false) + setInitCtxSignedInViewer(sc.initCtx) + + setupDBAndSettingsForAccessControlQuotaTests(t, sc) t.Run("Viewer can view CurrentOrgQuotas", func(t *testing.T) { response := callAPI(sc.server, http.MethodGet, getCurrentOrgQuotasURL, nil, t) @@ -54,48 +60,33 @@ func TestAPIEndpoint_GetCurrentOrgQuotas_LegacyAccessControl(t *testing.T) { } func TestAPIEndpoint_GetCurrentOrgQuotas_AccessControl(t *testing.T) { - sc := setupHTTPServer(t, true) + sc := setupHTTPServer(t, true, 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) + setupDBAndSettingsForAccessControlQuotaTests(t, sc) t.Run("AccessControl allows viewing CurrentOrgQuotas with correct permissions", func(t *testing.T) { - setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsQuotasRead, Scope: ScopeOrgsAll}}) + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsQuotasRead}}, sc.initCtx.OrgId) 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")}}) + t.Run("AccessControl prevents viewing CurrentOrgQuotas with correct permissions in another org", func(t *testing.T) { + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsQuotasRead}}, 2) response := callAPI(sc.server, http.MethodGet, getCurrentOrgQuotasURL, nil, t) - assert.Equal(t, http.StatusOK, response.Code) + assert.Equal(t, http.StatusForbidden, response.Code) }) t.Run("AccessControl prevents viewing CurrentOrgQuotas with incorrect permissions", func(t *testing.T) { - setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: "orgs:invalid"}}) + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: "orgs:invalid"}}, sc.initCtx.OrgId) 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) + sc := setupHTTPServer(t, true, 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) + setupDBAndSettingsForAccessControlQuotaTests(t, sc) t.Run("Viewer cannot view another org quotas", func(t *testing.T) { response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsQuotasURL, 2), nil, t) @@ -110,54 +101,33 @@ func TestAPIEndpoint_GetOrgQuotas_LegacyAccessControl(t *testing.T) { } func TestAPIEndpoint_GetOrgQuotas_AccessControl(t *testing.T) { - sc := setupHTTPServer(t, true) + sc := setupHTTPServer(t, true, 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) + setupDBAndSettingsForAccessControlQuotaTests(t, sc) t.Run("AccessControl allows viewing another org quotas with correct permissions", func(t *testing.T) { - setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsQuotasRead, Scope: ScopeOrgsAll}}) + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsQuotasRead}}, 2) 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")}}) + t.Run("AccessControl prevents viewing another org quotas with correct permissions in another org", func(t *testing.T) { + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsQuotasRead}}, 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"}}) + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: "orgs:invalid"}}, 2) 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) + sc := setupHTTPServer(t, true, 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) + setupDBAndSettingsForAccessControlQuotaTests(t, sc) input := strings.NewReader(testUpdateOrgQuotaCmd) t.Run("Viewer cannot update another org quotas", func(t *testing.T) { @@ -174,42 +144,28 @@ func TestAPIEndpoint_PutOrgQuotas_LegacyAccessControl(t *testing.T) { } func TestAPIEndpoint_PutOrgQuotas_AccessControl(t *testing.T) { - sc := setupHTTPServer(t, true) + sc := setupHTTPServer(t, true, 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) + setupDBAndSettingsForAccessControlQuotaTests(t, sc) 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")}}) + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsQuotasWrite}}, 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")}}) + t.Run("AccessControl prevents updating another org quotas with correct permissions in another org", func(t *testing.T) { + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsQuotasWrite}}, 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"}}) + setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: "orgs:invalid"}}, 2) response := callAPI(sc.server, http.MethodPut, fmt.Sprintf(putOrgsQuotasURL, 2, "org_user"), input, t) assert.Equal(t, http.StatusForbidden, response.Code) }) diff --git a/pkg/api/roles.go b/pkg/api/roles.go index ac376b33bc5..771084bd361 100644 --- a/pkg/api/roles.go +++ b/pkg/api/roles.go @@ -38,12 +38,6 @@ var ( ScopeDatasourceID = accesscontrol.Scope("datasources", "id", accesscontrol.Parameter(":id")) ScopeDatasourceUID = accesscontrol.Scope("datasources", "uid", accesscontrol.Parameter(":uid")) ScopeDatasourceName = accesscontrol.Scope("datasources", "name", accesscontrol.Parameter(":name")) - - ScopeOrgsAll = accesscontrol.Scope("orgs", "*") - ScopeOrgID = accesscontrol.Scope("orgs", "id", accesscontrol.Parameter(":orgId")) - ScopeOrgCurrentID = accesscontrol.Scope("orgs", "id", accesscontrol.Field("OrgID")) - ScopeOrgName = accesscontrol.Scope("orgs", "name", accesscontrol.Parameter(":name")) - ScopeOrgCurrent = accesscontrol.Scope("orgs", "current") ) // declareFixedRoles declares to the AccessControl service fixed roles and their @@ -121,17 +115,15 @@ func (hs *HTTPServer) declareFixedRoles() error { }, { Role: accesscontrol.RoleDTO{ - Version: 1, + Version: 2, Name: "fixed:current:org:reader", Description: "Read current organization and its quotas.", Permissions: []accesscontrol.Permission{ { Action: ActionOrgsRead, - Scope: ScopeOrgCurrent, }, { Action: ActionOrgsQuotasRead, - Scope: ScopeOrgCurrent, }, }, }, @@ -139,29 +131,24 @@ func (hs *HTTPServer) declareFixedRoles() error { }, { Role: accesscontrol.RoleDTO{ - Version: 1, + Version: 2, Name: "fixed:current:org:writer", Description: "Read current organization, its quotas, and its preferences. Write current organization and its preferences.", Permissions: []accesscontrol.Permission{ { Action: ActionOrgsRead, - Scope: ScopeOrgCurrent, }, { Action: ActionOrgsQuotasRead, - Scope: ScopeOrgCurrent, }, { Action: ActionOrgsPreferencesRead, - Scope: ScopeOrgCurrent, }, { Action: ActionOrgsWrite, - Scope: ScopeOrgCurrent, }, { Action: ActionOrgsPreferencesWrite, - Scope: ScopeOrgCurrent, }, }, }, @@ -169,34 +156,29 @@ func (hs *HTTPServer) declareFixedRoles() error { }, { Role: accesscontrol.RoleDTO{ - Version: 1, + Version: 2, Name: "fixed:orgs:writer", Description: "Create, read, write, or delete an organization. Read or write an organization's quotas.", Permissions: []accesscontrol.Permission{ {Action: ActionOrgsCreate}, { Action: ActionOrgsRead, - Scope: ScopeOrgsAll, }, { Action: ActionOrgsWrite, - Scope: ScopeOrgsAll, }, { Action: ActionOrgsDelete, - Scope: ScopeOrgsAll, }, { Action: ActionOrgsQuotasRead, - Scope: ScopeOrgsAll, }, { Action: ActionOrgsQuotasWrite, - Scope: ScopeOrgsAll, }, }, }, - Grants: []string{string(accesscontrol.RoleGrafanaAdmin)}, + Grants: []string{accesscontrol.RoleGrafanaAdmin}, }, } diff --git a/pkg/services/accesscontrol/middleware/middleware.go b/pkg/services/accesscontrol/middleware/middleware.go index b2067b02ffb..3b91fefeb13 100644 --- a/pkg/services/accesscontrol/middleware/middleware.go +++ b/pkg/services/accesscontrol/middleware/middleware.go @@ -7,11 +7,26 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + "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" ) +func authorize(c *models.ReqContext, ac accesscontrol.AccessControl, user *models.SignedInUser, evaluator accesscontrol.Evaluator) { + injected, err := evaluator.Inject(buildScopeParams(c)) + if err != nil { + c.JsonApiErr(http.StatusInternalServerError, "Internal server error", err) + return + } + + hasAccess, err := ac.Evaluate(c.Req.Context(), user, injected) + if !hasAccess || err != nil { + Deny(c, injected, err) + return + } +} + func Middleware(ac accesscontrol.AccessControl) func(web.Handler, accesscontrol.Evaluator) web.Handler { return func(fallback web.Handler, evaluator accesscontrol.Evaluator) web.Handler { if ac.IsDisabled() { @@ -19,17 +34,7 @@ func Middleware(ac accesscontrol.AccessControl) func(web.Handler, accesscontrol. } return func(c *models.ReqContext) { - injected, err := evaluator.Inject(buildScopeParams(c)) - if err != nil { - c.JsonApiErr(http.StatusInternalServerError, "Internal server error", err) - return - } - - hasAccess, err := ac.Evaluate(c.Req.Context(), c.SignedInUser, injected) - if !hasAccess || err != nil { - Deny(c, injected, err) - return - } + authorize(c, ac, c.SignedInUser, evaluator) } } } @@ -82,3 +87,53 @@ func buildScopeParams(c *models.ReqContext) accesscontrol.ScopeParams { URLParams: web.Params(c.Req), } } + +type OrgIDGetter func(c *models.ReqContext) (int64, error) + +func AuthorizeInOrgMiddleware(ac accesscontrol.AccessControl, db *sqlstore.SQLStore) func(web.Handler, OrgIDGetter, accesscontrol.Evaluator) web.Handler { + return func(fallback web.Handler, getTargetOrg OrgIDGetter, evaluator accesscontrol.Evaluator) web.Handler { + if ac.IsDisabled() { + return fallback + } + + return func(c *models.ReqContext) { + // using a copy of the user not to modify the signedInUser, yet perform the permission evaluation in another org + userCopy := *(c.SignedInUser) + orgID, err := getTargetOrg(c) + if err != nil { + Deny(c, nil, fmt.Errorf("failed to get target org: %w", err)) + return + } + if orgID == accesscontrol.GlobalOrgID { + userCopy.OrgId = orgID + userCopy.OrgName = "" + userCopy.OrgRole = "" + } else { + query := models.GetSignedInUserQuery{UserId: c.UserId, OrgId: orgID} + if err := db.GetSignedInUserWithCacheCtx(c.Req.Context(), &query); err != nil { + Deny(c, nil, fmt.Errorf("failed to authenticate user in target org: %w", err)) + return + } + userCopy.OrgId = query.Result.OrgId + userCopy.OrgName = query.Result.OrgName + userCopy.OrgRole = query.Result.OrgRole + } + + authorize(c, ac, &userCopy, evaluator) + } + } +} + +func UseOrgFromContextParams(c *models.ReqContext) (int64, error) { + orgID := c.ParamsInt64(":orgId") + // Special case of macaron handling invalid params + if orgID == 0 { + return 0, models.ErrOrgNotFound + } + + return orgID, nil +} + +func UseGlobalOrg(c *models.ReqContext) (int64, error) { + return accesscontrol.GlobalOrgID, nil +} diff --git a/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol_test.go b/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol_test.go index 6dc8a425805..5020807abbe 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol_test.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol_test.go @@ -5,13 +5,14 @@ import ( "fmt" "testing" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/setting" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) func setupTestEnv(t testing.TB) *OSSAccessControlService { @@ -19,7 +20,14 @@ func setupTestEnv(t testing.TB) *OSSAccessControlService { cfg := setting.NewCfg() cfg.FeatureToggles = map[string]bool{"accesscontrol": true} - ac := ProvideService(cfg, &usagestats.UsageStatsMock{T: t}) + + ac := &OSSAccessControlService{ + Cfg: cfg, + UsageStats: &usagestats.UsageStatsMock{T: t}, + Log: log.New("accesscontrol"), + registrations: accesscontrol.RegistrationList{}, + scopeResolver: accesscontrol.NewScopeResolver(), + } return ac } @@ -77,8 +85,8 @@ func TestEvaluatingPermissions(t *testing.T) { desc: "should successfully evaluate access to the endpoint", user: userTestCase{ name: "testuser", - orgRole: "Grafana Admin", - isGrafanaAdmin: false, + orgRole: models.ROLE_VIEWER, + isGrafanaAdmin: true, }, endpoints: []endpointTestCase{ {evaluator: accesscontrol.EvalPermission(accesscontrol.ActionUsersDisable, accesscontrol.ScopeGlobalUsersAll)}, @@ -501,7 +509,7 @@ func TestOSSAccessControlService_RegisterFixedRoles(t *testing.T) { } func TestOSSAccessControlService_GetUserPermissions(t *testing.T) { - testUser := &models.SignedInUser{ + testUser := models.SignedInUser{ UserId: 2, OrgId: 3, OrgName: "TestOrg", @@ -522,18 +530,11 @@ func TestOSSAccessControlService_GetUserPermissions(t *testing.T) { } tests := []struct { name string - user *models.SignedInUser + user models.SignedInUser rawPerm accesscontrol.Permission wantPerm accesscontrol.Permission wantErr bool }{ - { - name: "Translate orgs:current", - user: testUser, - rawPerm: accesscontrol.Permission{Action: "orgs:read", Scope: "orgs:current"}, - wantPerm: accesscontrol.Permission{Action: "orgs:read", Scope: "orgs:id:3"}, - wantErr: false, - }, { name: "Translate users:self", user: testUser, @@ -550,13 +551,7 @@ func TestOSSAccessControlService_GetUserPermissions(t *testing.T) { }) // Setup - ac := &OSSAccessControlService{ - Cfg: setting.NewCfg(), - UsageStats: &usagestats.UsageStatsMock{T: t}, - Log: log.New("accesscontrol-test"), - registrations: accesscontrol.RegistrationList{}, - scopeResolver: accesscontrol.NewScopeResolver(), - } + ac := setupTestEnv(t) ac.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true} registration.Role.Permissions = []accesscontrol.Permission{tt.rawPerm} @@ -567,7 +562,7 @@ func TestOSSAccessControlService_GetUserPermissions(t *testing.T) { require.NoError(t, err) // Test - userPerms, err := ac.GetUserPermissions(context.TODO(), tt.user) + userPerms, err := ac.GetUserPermissions(context.TODO(), &tt.user) if tt.wantErr { assert.Error(t, err, "Expected an error with GetUserPermissions.") return diff --git a/pkg/services/accesscontrol/scope.go b/pkg/services/accesscontrol/scope.go index e7f10bba45f..853313a4d3d 100644 --- a/pkg/services/accesscontrol/scope.go +++ b/pkg/services/accesscontrol/scope.go @@ -42,16 +42,11 @@ type ScopeResolver struct { func NewScopeResolver() ScopeResolver { return ScopeResolver{ keywordResolvers: map[string]KeywordScopeResolveFunc{ - "orgs:current": resolveCurrentOrg, - "users:self": resolveUserSelf, + "users:self": resolveUserSelf, }, } } -func resolveCurrentOrg(u *models.SignedInUser) (string, error) { - return Scope("orgs", "id", fmt.Sprintf("%v", u.OrgId)), nil -} - func resolveUserSelf(u *models.SignedInUser) (string, error) { return Scope("users", "id", fmt.Sprintf("%v", u.UserId)), nil }