From 55c7b8add28d4043f0a8c6305774872d2f8e0b12 Mon Sep 17 00:00:00 2001 From: Karl Persson Date: Wed, 24 Aug 2022 13:29:17 +0200 Subject: [PATCH] RBAC: Split up service into several components (#54002) * RBAC: Rename interface to Store * RBAC: Move ranme scopeInjector * RBAC: Rename files to service * RBAC: Rename to service * RBAC: Split up accesscontrol into two components * RBAC: Add DeclareFixedRoles to AccessControl interface * Wire: Fix wire bindings * RBAC: Move resolvers to root * RBAC: Remove invalid test * RBAC: Inject access control service * RBAC: Implement the RoleRegistry interface in fake --- pkg/api/common_test.go | 12 +- pkg/api/http_server.go | 8 +- pkg/api/index.go | 2 +- pkg/api/org_users.go | 4 +- pkg/api/org_users_test.go | 2 +- pkg/cmd/grafana-cli/runner/wire.go | 2 + pkg/cmd/grafana-cli/runner/wireexts_oss.go | 6 +- pkg/server/server_test.go | 2 +- .../usage_stats_providers_registry.go | 2 +- pkg/server/wire.go | 2 + pkg/server/wireexts_oss.go | 6 +- pkg/services/accesscontrol/accesscontrol.go | 41 +- pkg/services/accesscontrol/actest/fake.go | 64 +++ pkg/services/accesscontrol/api/api.go | 11 +- pkg/services/accesscontrol/evaluator_test.go | 28 +- pkg/services/accesscontrol/middleware.go | 31 +- pkg/services/accesscontrol/mock/mock.go | 36 +- pkg/services/accesscontrol/models.go | 6 - .../ossaccesscontrol/accesscontrol.go | 66 +++ .../ossaccesscontrol/accesscontrol_test.go | 84 +++ .../ossaccesscontrol/ossaccesscontrol.go | 205 -------- .../ossaccesscontrol/ossaccesscontrol_test.go | 495 ------------------ .../ossaccesscontrol/permissions_services.go | 22 +- .../accesscontrol/ossaccesscontrol/service.go | 141 +++++ .../ossaccesscontrol/service_test.go | 239 +++++++++ pkg/services/accesscontrol/resolvers.go | 113 +--- pkg/services/accesscontrol/resolvers_test.go | 59 +-- .../resourcepermissions/service.go | 8 +- .../resourcepermissions/service_test.go | 3 +- pkg/services/accesscontrol/roles.go | 4 +- .../guardian/accesscontrol_guardian_test.go | 4 +- .../login/loginservice/loginservice.go | 4 +- .../publicdashboards/api/common_test.go | 3 +- pkg/services/searchV2/auth.go | 2 +- pkg/services/searchV2/service.go | 4 +- pkg/services/serviceaccounts/api/api_test.go | 2 +- pkg/services/user/userimpl/user.go | 4 +- 37 files changed, 769 insertions(+), 958 deletions(-) create mode 100644 pkg/services/accesscontrol/actest/fake.go create mode 100644 pkg/services/accesscontrol/ossaccesscontrol/accesscontrol.go create mode 100644 pkg/services/accesscontrol/ossaccesscontrol/accesscontrol_test.go delete mode 100644 pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go delete mode 100644 pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol_test.go create mode 100644 pkg/services/accesscontrol/ossaccesscontrol/service.go create mode 100644 pkg/services/accesscontrol/ossaccesscontrol/service_test.go diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index d9baf1d2f3e..2a0e5673eb8 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -366,6 +366,7 @@ func setupHTTPServerWithCfgDb( var acmock *accesscontrolmock.Mock var ac accesscontrol.AccessControl + var acService accesscontrol.Service // Defining the accesscontrol service has to be done before registering routes if useFakeAccessControl { @@ -374,13 +375,15 @@ func setupHTTPServerWithCfgDb( acmock = acmock.WithDisabled() } ac = acmock + acService = acmock } else { var err error - ac, err = ossaccesscontrol.ProvideService(cfg, database.ProvideService(db), routeRegister) + acService, err = ossaccesscontrol.ProvideService(cfg, database.ProvideService(db), routeRegister) require.NoError(t, err) + ac = ossaccesscontrol.ProvideAccessControl(cfg, acService) } - teamPermissionService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, routeRegister, db, ac, license) + teamPermissionService, err := ossaccesscontrol.ProvideTeamPermissions(cfg, routeRegister, db, ac, license, acService) require.NoError(t, err) // Create minimal HTTP Server @@ -395,6 +398,7 @@ func setupHTTPServerWithCfgDb( SQLStore: store, License: &licensing.OSSLicensingService{}, AccessControl: ac, + accesscontrolService: acService, teamPermissionsService: teamPermissionService, searchUsersService: searchusers.ProvideUsersService(filters.ProvideOSSSearchUserFilter(), usertest.NewUserServiceFake()), DashboardService: dashboardservice.ProvideDashboardService( @@ -410,7 +414,7 @@ func setupHTTPServerWithCfgDb( } require.NoError(t, hs.declareFixedRoles()) - require.NoError(t, hs.AccessControl.(accesscontrol.RoleRegistry).RegisterFixedRoles(context.Background())) + require.NoError(t, hs.accesscontrolService.(accesscontrol.RoleRegistry).RegisterFixedRoles(context.Background())) // Instantiate a new Server m := web.New() @@ -423,7 +427,7 @@ func setupHTTPServerWithCfgDb( c.Req = c.Req.WithContext(ctxkey.Set(c.Req.Context(), initCtx)) }) - m.Use(accesscontrol.LoadPermissionsMiddleware(hs.AccessControl)) + m.Use(accesscontrol.LoadPermissionsMiddleware(hs.accesscontrolService)) // Register all routes hs.registerRoutes() diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 4591762f744..f28afbb2131 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -182,6 +182,7 @@ type HTTPServer struct { tempUserService tempUser.Service loginAttemptService loginAttempt.Service orgService org.Service + accesscontrolService accesscontrol.Service } type ServerOptions struct { @@ -217,7 +218,9 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi dashboardPermissionsService accesscontrol.DashboardPermissionsService, dashboardVersionService dashver.Service, starService star.Service, csrfService csrf.Service, coremodels *registry.Base, playlistService playlist.Service, apiKeyService apikey.Service, kvStore kvstore.KVStore, secretsMigrator secrets.Migrator, secretsPluginManager plugins.SecretsPluginManager, - publicDashboardsApi *publicdashboardsApi.Api, userService user.Service, tempUserService tempUser.Service, loginAttemptService loginAttempt.Service, orgService org.Service) (*HTTPServer, error) { + publicDashboardsApi *publicdashboardsApi.Api, userService user.Service, tempUserService tempUser.Service, loginAttemptService loginAttempt.Service, orgService org.Service, + accesscontrolService accesscontrol.Service, +) (*HTTPServer, error) { web.Env = cfg.Env m := web.New() @@ -308,6 +311,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi tempUserService: tempUserService, loginAttemptService: loginAttemptService, orgService: orgService, + accesscontrolService: accesscontrolService, } if hs.Listener != nil { hs.log.Debug("Using provided listener") @@ -560,7 +564,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() { m.Use(hs.ContextHandler.Middleware) m.Use(middleware.OrgRedirect(hs.Cfg, hs.SQLStore)) - m.Use(accesscontrol.LoadPermissionsMiddleware(hs.AccessControl)) + m.Use(accesscontrol.LoadPermissionsMiddleware(hs.accesscontrolService)) // needs to be after context handler if hs.Cfg.EnforceDomain { diff --git a/pkg/api/index.go b/pkg/api/index.go index fd98c6b5ecb..2aa402be12a 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -813,7 +813,7 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat } if !hs.AccessControl.IsDisabled() { - userPermissions, err := hs.AccessControl.GetUserPermissions(c.Req.Context(), c.SignedInUser, ac.Options{ReloadCache: false}) + userPermissions, err := hs.accesscontrolService.GetUserPermissions(c.Req.Context(), c.SignedInUser, ac.Options{ReloadCache: false}) if err != nil { return nil, err } diff --git a/pkg/api/org_users.go b/pkg/api/org_users.go index e988f7e7d28..9236422c6fe 100644 --- a/pkg/api/org_users.go +++ b/pkg/api/org_users.go @@ -411,14 +411,14 @@ func (hs *HTTPServer) removeOrgUserHelper(ctx context.Context, cmd *models.Remov if cmd.UserWasDeleted { // This should be called from appropriate service when moved - if err := hs.AccessControl.DeleteUserPermissions(ctx, accesscontrol.GlobalOrgID, cmd.UserId); err != nil { + if err := hs.accesscontrolService.DeleteUserPermissions(ctx, accesscontrol.GlobalOrgID, cmd.UserId); err != nil { hs.log.Warn("failed to delete permissions for user", "userID", cmd.UserId, "orgID", accesscontrol.GlobalOrgID, "err", err) } return response.Success("User deleted") } // This should be called from appropriate service when moved - if err := hs.AccessControl.DeleteUserPermissions(ctx, cmd.OrgId, cmd.UserId); err != nil { + if err := hs.accesscontrolService.DeleteUserPermissions(ctx, cmd.OrgId, cmd.UserId); err != nil { hs.log.Warn("failed to delete permissions for user", "userID", cmd.UserId, "orgID", cmd.OrgId, "err", err) } diff --git a/pkg/api/org_users_test.go b/pkg/api/org_users_test.go index 8f0acefed3e..6650c75bb56 100644 --- a/pkg/api/org_users_test.go +++ b/pkg/api/org_users_test.go @@ -960,7 +960,7 @@ func TestDeleteOrgUsersAPIEndpoint_AccessControl(t *testing.T) { assert.Len(t, getUsersQuery.Result, tc.expectedUserCount) // check all permissions for user is removed in org - permission, err := sc.hs.AccessControl.GetUserPermissions(context.Background(), &user.SignedInUser{UserID: tc.targetUserId, OrgID: tc.targetOrg}, accesscontrol.Options{}) + permission, err := sc.hs.accesscontrolService.GetUserPermissions(context.Background(), &user.SignedInUser{UserID: tc.targetUserId, OrgID: tc.targetOrg}, accesscontrol.Options{}) require.NoError(t, err) assert.Len(t, permission, 0) } diff --git a/pkg/cmd/grafana-cli/runner/wire.go b/pkg/cmd/grafana-cli/runner/wire.go index 5cf247a005f..dca2f0272d9 100644 --- a/pkg/cmd/grafana-cli/runner/wire.go +++ b/pkg/cmd/grafana-cli/runner/wire.go @@ -323,6 +323,8 @@ var wireSet = wire.NewSet( wire.Bind(new(db.DB), new(*sqlstore.SQLStore)), prefimpl.ProvideService, opentsdb.ProvideService, + ossaccesscontrol.ProvideAccessControl, + wire.Bind(new(accesscontrol.AccessControl), new(*ossaccesscontrol.AccessControl)), ) func Initialize(cfg *setting.Cfg) (Runner, error) { diff --git a/pkg/cmd/grafana-cli/runner/wireexts_oss.go b/pkg/cmd/grafana-cli/runner/wireexts_oss.go index 3f987cbb934..b446c963324 100644 --- a/pkg/cmd/grafana-cli/runner/wireexts_oss.go +++ b/pkg/cmd/grafana-cli/runner/wireexts_oss.go @@ -52,8 +52,8 @@ var wireExtsSet = wire.NewSet( wire.Bind(new(models.UserTokenService), new(*auth.UserAuthTokenService)), wire.Bind(new(models.UserTokenBackgroundService), new(*auth.UserAuthTokenService)), ossaccesscontrol.ProvideService, - wire.Bind(new(accesscontrol.RoleRegistry), new(*ossaccesscontrol.OSSAccessControlService)), - wire.Bind(new(accesscontrol.AccessControl), new(*ossaccesscontrol.OSSAccessControlService)), + wire.Bind(new(accesscontrol.Service), new(*ossaccesscontrol.Service)), + wire.Bind(new(accesscontrol.RoleRegistry), new(*ossaccesscontrol.Service)), thumbs.ProvideCrawlerAuthSetupService, wire.Bind(new(thumbs.CrawlerAuthSetupService), new(*thumbs.OSSCrawlerAuthSetupService)), validations.ProvideValidator, @@ -75,7 +75,7 @@ var wireExtsSet = wire.NewSet( provider.ProvideService, wire.Bind(new(plugins.BackendFactoryProvider), new(*provider.Service)), acdb.ProvideService, - wire.Bind(new(accesscontrol.PermissionsStore), new(*acdb.AccessControlStore)), + wire.Bind(new(accesscontrol.Store), new(*acdb.AccessControlStore)), ldap.ProvideGroupsService, wire.Bind(new(ldap.Groups), new(*ldap.OSSGroups)), permissions.ProvideDatasourcePermissionsService, diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index 887c48a516e..6d13013efdb 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -55,7 +55,7 @@ func testServer(t *testing.T, services ...registry.BackgroundService) *Server { secretMigrationService := &migrations.SecretMigrationServiceImpl{ ServerLockService: serverLockService, } - s, err := newServer(Options{}, setting.NewCfg(), nil, &ossaccesscontrol.OSSAccessControlService{}, nil, backgroundsvcs.NewBackgroundServiceRegistry(services...), secretMigrationService, usertest.NewUserServiceFake()) + s, err := newServer(Options{}, setting.NewCfg(), nil, &ossaccesscontrol.Service{}, nil, backgroundsvcs.NewBackgroundServiceRegistry(services...), secretMigrationService, usertest.NewUserServiceFake()) require.NoError(t, err) // Required to skip configuration initialization that causes // DI errors in this test. diff --git a/pkg/server/usagestatssvcs/usage_stats_providers_registry.go b/pkg/server/usagestatssvcs/usage_stats_providers_registry.go index 91cc9e6973d..1030ebc9572 100644 --- a/pkg/server/usagestatssvcs/usage_stats_providers_registry.go +++ b/pkg/server/usagestatssvcs/usage_stats_providers_registry.go @@ -8,7 +8,7 @@ import ( func ProvideUsageStatsProvidersRegistry( thumbsService thumbs.Service, - accesscontrol accesscontrol.AccessControl, + accesscontrol accesscontrol.Service, ) *UsageStatsProvidersRegistry { return NewUsageStatsProvidersRegistry( thumbsService, diff --git a/pkg/server/wire.go b/pkg/server/wire.go index e46a8823ec7..1e311697122 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -323,6 +323,8 @@ var wireBasicSet = wire.NewSet( secretsMigrations.ProvideSecretMigrationService, wire.Bind(new(secretsMigrations.SecretMigrationService), new(*secretsMigrations.SecretMigrationServiceImpl)), userauthimpl.ProvideService, + ossaccesscontrol.ProvideAccessControl, + wire.Bind(new(accesscontrol.AccessControl), new(*ossaccesscontrol.AccessControl)), ) var wireSet = wire.NewSet( diff --git a/pkg/server/wireexts_oss.go b/pkg/server/wireexts_oss.go index d2ca415e96c..697b918c2f5 100644 --- a/pkg/server/wireexts_oss.go +++ b/pkg/server/wireexts_oss.go @@ -47,8 +47,8 @@ var wireExtsBasicSet = wire.NewSet( setting.ProvideProvider, wire.Bind(new(setting.Provider), new(*setting.OSSImpl)), ossaccesscontrol.ProvideService, - wire.Bind(new(accesscontrol.RoleRegistry), new(*ossaccesscontrol.OSSAccessControlService)), - wire.Bind(new(accesscontrol.AccessControl), new(*ossaccesscontrol.OSSAccessControlService)), + wire.Bind(new(accesscontrol.RoleRegistry), new(*ossaccesscontrol.Service)), + wire.Bind(new(accesscontrol.Service), new(*ossaccesscontrol.Service)), thumbs.ProvideCrawlerAuthSetupService, wire.Bind(new(thumbs.CrawlerAuthSetupService), new(*thumbs.OSSCrawlerAuthSetupService)), validations.ProvideValidator, @@ -74,7 +74,7 @@ var wireExtsBasicSet = wire.NewSet( provider.ProvideService, wire.Bind(new(plugins.BackendFactoryProvider), new(*provider.Service)), acdb.ProvideService, - wire.Bind(new(accesscontrol.PermissionsStore), new(*acdb.AccessControlStore)), + wire.Bind(new(accesscontrol.Store), new(*acdb.AccessControlStore)), osskmsproviders.ProvideService, wire.Bind(new(kmsproviders.Service), new(osskmsproviders.Service)), ldap.ProvideGroupsService, diff --git a/pkg/services/accesscontrol/accesscontrol.go b/pkg/services/accesscontrol/accesscontrol.go index 97887d38cf0..0d2d6b13efc 100644 --- a/pkg/services/accesscontrol/accesscontrol.go +++ b/pkg/services/accesscontrol/accesscontrol.go @@ -12,33 +12,32 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -type Options struct { - ReloadCache bool -} - type AccessControl interface { - registry.ProvidesUsageStats - // Evaluate evaluates access to the given resources. Evaluate(ctx context.Context, user *user.SignedInUser, evaluator Evaluator) (bool, error) - - // GetUserPermissions returns user permissions with only action and scope fields set. - GetUserPermissions(ctx context.Context, user *user.SignedInUser, options Options) ([]Permission, error) - - //IsDisabled returns if access control is enabled or not - IsDisabled() bool - - // DeclareFixedRoles allows the caller to declare, to the service, fixed roles and their - // assignments to organization roles ("Viewer", "Editor", "Admin") or "Grafana Admin" - DeclareFixedRoles(...RoleRegistration) error - // RegisterScopeAttributeResolver allows the caller to register a scope resolver for a // specific scope prefix (ex: datasources:name:) - RegisterScopeAttributeResolver(scopePrefix string, resolver ScopeAttributeResolver) + RegisterScopeAttributeResolver(prefix string, resolver ScopeAttributeResolver) + // DeclareFixedRoles allows the caller to declare, to the service, fixed roles and their + // assignments to organization roles ("Viewer", "Editor", "Admin") or "Grafana Admin" + // FIXME: Remove from access control interface and inject service where this is needed + DeclareFixedRoles(registrations ...RoleRegistration) error + //IsDisabled returns if access control is enabled or not + IsDisabled() bool +} +type Service interface { + registry.ProvidesUsageStats + // GetUserPermissions returns user permissions with only action and scope fields set. + GetUserPermissions(ctx context.Context, user *user.SignedInUser, options Options) ([]Permission, error) // DeleteUserPermissions removes all permissions user has in org and all permission to that user // If orgID is set to 0 remove permissions from all orgs DeleteUserPermissions(ctx context.Context, orgID, userID int64) error + // DeclareFixedRoles allows the caller to declare, to the service, fixed roles and their + // assignments to organization roles ("Viewer", "Editor", "Admin") or "Grafana Admin" + DeclareFixedRoles(registrations ...RoleRegistration) error + //IsDisabled returns if access control is enabled or not + IsDisabled() bool } type RoleRegistry interface { @@ -46,7 +45,11 @@ type RoleRegistry interface { RegisterFixedRoles(ctx context.Context) error } -type PermissionsStore interface { +type Options struct { + ReloadCache bool +} + +type Store interface { // GetUserPermissions returns user permissions with only action and scope fields set. GetUserPermissions(ctx context.Context, query GetUserPermissionsQuery) ([]Permission, error) DeleteUserPermissions(ctx context.Context, orgID, userID int64) error diff --git a/pkg/services/accesscontrol/actest/fake.go b/pkg/services/accesscontrol/actest/fake.go new file mode 100644 index 00000000000..58cf5d8bb0c --- /dev/null +++ b/pkg/services/accesscontrol/actest/fake.go @@ -0,0 +1,64 @@ +package actest + +import ( + "context" + + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/user" +) + +var _ accesscontrol.Service = new(FakeService) +var _ accesscontrol.RoleRegistry = new(FakeService) + +type FakeService struct { + ExpectedErr error + ExpectedDisabled bool + ExpectedPermissions []accesscontrol.Permission +} + +func (f FakeService) GetUsageStats(ctx context.Context) map[string]interface{} { + return map[string]interface{}{} +} + +func (f FakeService) GetUserPermissions(ctx context.Context, user *user.SignedInUser, options accesscontrol.Options) ([]accesscontrol.Permission, error) { + return f.ExpectedPermissions, f.ExpectedErr +} + +func (f FakeService) DeleteUserPermissions(ctx context.Context, orgID, userID int64) error { + return f.ExpectedErr +} + +func (f FakeService) DeclareFixedRoles(registrations ...accesscontrol.RoleRegistration) error { + return f.ExpectedErr +} + +func (f FakeService) RegisterFixedRoles(ctx context.Context) error { + return f.ExpectedErr +} + +func (f FakeService) IsDisabled() bool { + return f.ExpectedDisabled +} + +var _ accesscontrol.AccessControl = new(FakeAccessControl) + +type FakeAccessControl struct { + ExpectedErr error + ExpectedDisabled bool + ExpectedEvaluate bool +} + +func (f FakeAccessControl) Evaluate(ctx context.Context, user *user.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) { + return f.ExpectedEvaluate, f.ExpectedErr +} + +func (f FakeAccessControl) RegisterScopeAttributeResolver(prefix string, resolver accesscontrol.ScopeAttributeResolver) { +} + +func (f FakeAccessControl) DeclareFixedRoles(registrations ...accesscontrol.RoleRegistration) error { + return f.ExpectedErr +} + +func (f FakeAccessControl) IsDisabled() bool { + return f.ExpectedDisabled +} diff --git a/pkg/services/accesscontrol/api/api.go b/pkg/services/accesscontrol/api/api.go index 4477dfe9eca..f13ff071f25 100644 --- a/pkg/services/accesscontrol/api/api.go +++ b/pkg/services/accesscontrol/api/api.go @@ -10,9 +10,16 @@ import ( ac "github.com/grafana/grafana/pkg/services/accesscontrol" ) +func NewAccessControlAPI(router routing.RouteRegister, service ac.Service) *AccessControlAPI { + return &AccessControlAPI{ + RouteRegister: router, + Service: service, + } +} + type AccessControlAPI struct { + Service ac.Service RouteRegister routing.RouteRegister - AccessControl ac.AccessControl } func (api *AccessControlAPI) RegisterAPIEndpoints() { @@ -24,7 +31,7 @@ func (api *AccessControlAPI) RegisterAPIEndpoints() { // GET /api/access-control/user/permissions func (api *AccessControlAPI) getUsersPermissions(c *models.ReqContext) response.Response { reloadCache := c.QueryBool("reloadcache") - permissions, err := api.AccessControl.GetUserPermissions(c.Req.Context(), + permissions, err := api.Service.GetUserPermissions(c.Req.Context(), c.SignedInUser, ac.Options{ReloadCache: reloadCache}) if err != nil { response.JSON(http.StatusInternalServerError, err) diff --git a/pkg/services/accesscontrol/evaluator_test.go b/pkg/services/accesscontrol/evaluator_test.go index d37e2d5838e..8d36eba492d 100644 --- a/pkg/services/accesscontrol/evaluator_test.go +++ b/pkg/services/accesscontrol/evaluator_test.go @@ -62,7 +62,7 @@ type injectTestCase struct { desc string expected bool evaluator Evaluator - params ScopeParams + params scopeParams permissions map[string][]string } @@ -72,7 +72,7 @@ func TestPermission_Inject(t *testing.T) { desc: "should inject field", expected: true, evaluator: EvalPermission("orgs:read", Scope("orgs", Field("OrgID"))), - params: ScopeParams{ + params: scopeParams{ OrgID: 3, }, permissions: map[string][]string{ @@ -83,7 +83,7 @@ func TestPermission_Inject(t *testing.T) { desc: "should inject correct param", expected: true, evaluator: EvalPermission("reports:read", Scope("reports", Parameter(":reportId"))), - params: ScopeParams{ + params: scopeParams{ URLParams: map[string]string{ ":id": "10", ":reportId": "1", @@ -97,7 +97,7 @@ func TestPermission_Inject(t *testing.T) { desc: "should fail for nil params", expected: false, evaluator: EvalPermission("reports:read", Scope("reports", Parameter(":reportId"))), - params: ScopeParams{}, + params: scopeParams{}, permissions: map[string][]string{ "reports:read": {"reports:1"}, }, @@ -106,7 +106,7 @@ func TestPermission_Inject(t *testing.T) { desc: "should inject several parameters to one permission", expected: true, evaluator: EvalPermission("reports:read", Scope("reports", Parameter(":reportId"), Parameter(":reportId2"))), - params: ScopeParams{ + params: scopeParams{ URLParams: map[string]string{ ":reportId": "report", ":reportId2": "report2", @@ -120,7 +120,7 @@ func TestPermission_Inject(t *testing.T) { for _, test := range tests { t.Run(test.desc, func(t *testing.T) { - injected, err := test.evaluator.MutateScopes(context.TODO(), ScopeInjector(test.params)) + injected, err := test.evaluator.MutateScopes(context.TODO(), scopeInjector(test.params)) assert.NoError(t, err) ok := injected.Evaluate(test.permissions) assert.Equal(t, test.expected, ok) @@ -185,7 +185,7 @@ func TestAll_Inject(t *testing.T) { EvalPermission("reports:read", Scope("reports", Parameter(":reportId"))), EvalPermission("settings:read", Scope("settings", Parameter(":settingsId"))), ), - params: ScopeParams{ + params: scopeParams{ URLParams: map[string]string{ ":id": "10", ":settingsId": "3", @@ -204,7 +204,7 @@ func TestAll_Inject(t *testing.T) { EvalPermission("orgs:read", Scope("orgs", Field("OrgID"))), EvalPermission("orgs:read", Scope("orgs", Parameter(":orgId"))), ), - params: ScopeParams{ + params: scopeParams{ OrgID: 3, URLParams: map[string]string{ ":orgId": "4", @@ -221,7 +221,7 @@ func TestAll_Inject(t *testing.T) { EvalPermission("settings:read", Scope("reports", Parameter(":settingsId"))), EvalPermission("reports:read", Scope("reports", Parameter(":reportId"))), ), - params: ScopeParams{}, + params: scopeParams{}, permissions: map[string][]string{ "reports:read": {"reports:1"}, "settings:read": {"settings:3"}, @@ -231,7 +231,7 @@ func TestAll_Inject(t *testing.T) { for _, test := range tests { t.Run(test.desc, func(t *testing.T) { - injected, err := test.evaluator.MutateScopes(context.TODO(), ScopeInjector(test.params)) + injected, err := test.evaluator.MutateScopes(context.TODO(), scopeInjector(test.params)) assert.NoError(t, err) ok := injected.Evaluate(test.permissions) assert.NoError(t, err) @@ -295,7 +295,7 @@ func TestAny_Inject(t *testing.T) { EvalPermission("reports:read", Scope("reports", Parameter(":reportId"))), EvalPermission("settings:read", Scope("settings", Parameter(":settingsId"))), ), - params: ScopeParams{ + params: scopeParams{ URLParams: map[string]string{ ":id": "10", ":settingsId": "3", @@ -314,7 +314,7 @@ func TestAny_Inject(t *testing.T) { EvalPermission("orgs:read", Scope("orgs", Field("OrgID"))), EvalPermission("orgs:read", Scope("orgs", Parameter(":orgId"))), ), - params: ScopeParams{ + params: scopeParams{ OrgID: 3, URLParams: map[string]string{ ":orgId": "4", @@ -331,7 +331,7 @@ func TestAny_Inject(t *testing.T) { EvalPermission("settings:read", Scope("reports", Parameter(":settingsId"))), EvalPermission("reports:read", Scope("reports", Parameter(":reportId"))), ), - params: ScopeParams{}, + params: scopeParams{}, permissions: map[string][]string{ "reports:read": {"reports:1"}, "settings:read": {"settings:3"}, @@ -341,7 +341,7 @@ func TestAny_Inject(t *testing.T) { for _, test := range tests { t.Run(test.desc, func(t *testing.T) { - injected, err := test.evaluator.MutateScopes(context.TODO(), ScopeInjector(test.params)) + injected, err := test.evaluator.MutateScopes(context.TODO(), scopeInjector(test.params)) assert.NoError(t, err) ok := injected.Evaluate(test.permissions) assert.NoError(t, err) diff --git a/pkg/services/accesscontrol/middleware.go b/pkg/services/accesscontrol/middleware.go index f6f4f8c0774..a23937f91cc 100644 --- a/pkg/services/accesscontrol/middleware.go +++ b/pkg/services/accesscontrol/middleware.go @@ -1,10 +1,12 @@ package accesscontrol import ( + "bytes" "context" "fmt" "net/http" "strconv" + "text/template" "time" "github.com/grafana/grafana/pkg/models" @@ -27,7 +29,7 @@ func Middleware(ac AccessControl) func(web.Handler, Evaluator) web.Handler { } func authorize(c *models.ReqContext, ac AccessControl, user *user.SignedInUser, evaluator Evaluator) { - injected, err := evaluator.MutateScopes(c.Req.Context(), ScopeInjector(ScopeParams{ + injected, err := evaluator.MutateScopes(c.Req.Context(), scopeInjector(scopeParams{ OrgID: c.OrgID, URLParams: web.Params(c.Req), })) @@ -148,13 +150,13 @@ func UseGlobalOrg(c *models.ReqContext) (int64, error) { return GlobalOrgID, nil } -func LoadPermissionsMiddleware(ac AccessControl) web.Handler { +func LoadPermissionsMiddleware(service Service) web.Handler { return func(c *models.ReqContext) { - if ac.IsDisabled() { + if service.IsDisabled() { return } - permissions, err := ac.GetUserPermissions(c.Req.Context(), c.SignedInUser, + permissions, err := service.GetUserPermissions(c.Req.Context(), c.SignedInUser, Options{ReloadCache: false}) if err != nil { c.JsonApiErr(http.StatusForbidden, "", err) @@ -167,3 +169,24 @@ func LoadPermissionsMiddleware(ac AccessControl) web.Handler { c.SignedInUser.Permissions[c.OrgID] = GroupScopesByAction(permissions) } } + +// scopeParams holds the parameters used to fill in scope templates +type scopeParams struct { + OrgID int64 + URLParams map[string]string +} + +// scopeInjector inject request params into the templated scopes. e.g. "settings:" + eval.Parameters(":id") +func scopeInjector(params scopeParams) ScopeAttributeMutator { + return func(_ context.Context, scope string) ([]string, error) { + tmpl, err := template.New("scope").Parse(scope) + if err != nil { + return nil, err + } + var buf bytes.Buffer + if err = tmpl.Execute(&buf, params); err != nil { + return nil, err + } + return []string{buf.String()}, nil + } +} diff --git a/pkg/services/accesscontrol/mock/mock.go b/pkg/services/accesscontrol/mock/mock.go index fad60e7e518..e9191198867 100644 --- a/pkg/services/accesscontrol/mock/mock.go +++ b/pkg/services/accesscontrol/mock/mock.go @@ -3,13 +3,14 @@ package mock import ( "context" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/user" ) type fullAccessControl interface { accesscontrol.AccessControl - GetUserBuiltInRoles(user *user.SignedInUser) []string + accesscontrol.Service RegisterFixedRoles(context.Context) error } @@ -45,7 +46,7 @@ type Mock struct { RegisterScopeAttributeResolverFunc func(string, accesscontrol.ScopeAttributeResolver) DeleteUserPermissionsFunc func(context.Context, int64) error - scopeResolvers accesscontrol.ScopeResolvers + scopeResolvers accesscontrol.Resolvers } // Ensure the mock stays in line with the interface @@ -57,29 +58,29 @@ func New() *Mock { disabled: false, permissions: []accesscontrol.Permission{}, builtInRoles: []string{}, - scopeResolvers: accesscontrol.NewScopeResolvers(), + scopeResolvers: accesscontrol.NewResolvers(log.NewNopLogger()), } return mock } -func (m Mock) GetUsageStats(ctx context.Context) map[string]interface{} { +func (m *Mock) GetUsageStats(ctx context.Context) map[string]interface{} { return make(map[string]interface{}) } -func (m Mock) WithPermissions(permissions []accesscontrol.Permission) *Mock { +func (m *Mock) WithPermissions(permissions []accesscontrol.Permission) *Mock { m.permissions = permissions - return &m + return m } -func (m Mock) WithDisabled() *Mock { +func (m *Mock) WithDisabled() *Mock { m.disabled = true - return &m + return m } -func (m Mock) WithBuiltInRoles(builtInRoles []string) *Mock { +func (m *Mock) WithBuiltInRoles(builtInRoles []string) *Mock { m.builtInRoles = builtInRoles - return &m + return m } // Evaluate evaluates access to the given resource. @@ -148,21 +149,6 @@ func (m *Mock) DeclareFixedRoles(registrations ...accesscontrol.RoleRegistration return nil } -// GetUserBuiltInRoles returns the list of organizational roles ("Viewer", "Editor", "Admin") -// or "Grafana Admin" associated to a user -// This mock returns m.builtInRoles unless an override is provided. -func (m *Mock) GetUserBuiltInRoles(user *user.SignedInUser) []string { - m.Calls.GetUserBuiltInRoles = append(m.Calls.GetUserBuiltInRoles, []interface{}{user}) - - // Use override if provided - if m.GetUserBuiltInRolesFunc != nil { - return m.GetUserBuiltInRolesFunc(user) - } - - // Otherwise return the BuiltInRoles list - return m.builtInRoles -} - // RegisterFixedRoles registers all roles declared to AccessControl // This mock returns no error unless an override is provided. func (m *Mock) RegisterFixedRoles(ctx context.Context) error { diff --git a/pkg/services/accesscontrol/models.go b/pkg/services/accesscontrol/models.go index 5b94f549fa2..6059c58914b 100644 --- a/pkg/services/accesscontrol/models.go +++ b/pkg/services/accesscontrol/models.go @@ -214,12 +214,6 @@ type GetUserPermissionsQuery struct { TeamIDs []int64 } -// ScopeParams holds the parameters used to fill in scope templates -type ScopeParams struct { - OrgID int64 - URLParams map[string]string -} - // ResourcePermission is structure that holds all actions that either a team / user / builtin-role // can perform against specific resource. type ResourcePermission struct { diff --git a/pkg/services/accesscontrol/ossaccesscontrol/accesscontrol.go b/pkg/services/accesscontrol/ossaccesscontrol/accesscontrol.go new file mode 100644 index 00000000000..929223965f1 --- /dev/null +++ b/pkg/services/accesscontrol/ossaccesscontrol/accesscontrol.go @@ -0,0 +1,66 @@ +package ossaccesscontrol + +import ( + "context" + + "github.com/prometheus/client_golang/prometheus" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/metrics" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" +) + +var _ accesscontrol.AccessControl = new(AccessControl) + +func ProvideAccessControl(cfg *setting.Cfg, service accesscontrol.Service) *AccessControl { + logger := log.New("accesscontrol") + return &AccessControl{ + cfg, logger, accesscontrol.NewResolvers(logger), service, + } +} + +type AccessControl struct { + cfg *setting.Cfg + log log.Logger + resolvers accesscontrol.Resolvers + service accesscontrol.Service +} + +func (a *AccessControl) Evaluate(ctx context.Context, user *user.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) { + timer := prometheus.NewTimer(metrics.MAccessEvaluationsSummary) + defer timer.ObserveDuration() + metrics.MAccessEvaluationCount.Inc() + + if user.Permissions == nil { + user.Permissions = map[int64]map[string][]string{} + } + + if _, ok := user.Permissions[user.OrgID]; !ok { + permissions, err := a.service.GetUserPermissions(ctx, user, accesscontrol.Options{ReloadCache: true}) + if err != nil { + return false, err + } + user.Permissions[user.OrgID] = accesscontrol.GroupScopesByAction(permissions) + } + + resolvedEvaluator, err := evaluator.MutateScopes(ctx, a.resolvers.GetScopeAttributeMutator(user.OrgID)) + if err != nil { + return false, err + } + return resolvedEvaluator.Evaluate(user.Permissions[user.OrgID]), nil +} + +func (a *AccessControl) RegisterScopeAttributeResolver(prefix string, resolver accesscontrol.ScopeAttributeResolver) { + a.resolvers.AddScopeAttributeResolver(prefix, resolver) +} + +func (a *AccessControl) DeclareFixedRoles(registrations ...accesscontrol.RoleRegistration) error { + // FIXME: Remove wrapped call + return a.service.DeclareFixedRoles(registrations...) +} + +func (a *AccessControl) IsDisabled() bool { + return accesscontrol.IsDisabled(a.cfg) +} diff --git a/pkg/services/accesscontrol/ossaccesscontrol/accesscontrol_test.go b/pkg/services/accesscontrol/ossaccesscontrol/accesscontrol_test.go new file mode 100644 index 00000000000..c388ecd516c --- /dev/null +++ b/pkg/services/accesscontrol/ossaccesscontrol/accesscontrol_test.go @@ -0,0 +1,84 @@ +package ossaccesscontrol + +import ( + "context" + "testing" + + "github.com/grafana/grafana/pkg/services/accesscontrol/actest" + + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/assert" +) + +func TestAccessControl_Evaluate(t *testing.T) { + type testCase struct { + desc string + user user.SignedInUser + evaluator accesscontrol.Evaluator + resolverPrefix string + expected bool + expectedErr error + resolver accesscontrol.ScopeAttributeResolver + } + + tests := []testCase{ + { + desc: "expect user to have access when correct permission is stored on user", + user: user.SignedInUser{ + OrgID: 1, + Permissions: map[int64]map[string][]string{ + 1: {accesscontrol.ActionTeamsWrite: {"teams:*"}}, + }, + }, + evaluator: accesscontrol.EvalPermission(accesscontrol.ActionTeamsWrite, "teams:id:1"), + expected: true, + }, + { + desc: "expect user to not have access without required permissions", + user: user.SignedInUser{ + OrgID: 1, + Permissions: map[int64]map[string][]string{ + 1: {accesscontrol.ActionTeamsWrite: {"teams:*"}}, + }, + }, + evaluator: accesscontrol.EvalPermission(accesscontrol.ActionOrgUsersWrite, "users:id:1"), + expected: false, + }, + { + desc: "expect user to have access when resolver translate scope", + user: user.SignedInUser{ + OrgID: 1, + Permissions: map[int64]map[string][]string{ + 1: {accesscontrol.ActionTeamsWrite: {"another:scope"}}, + }, + }, + evaluator: accesscontrol.EvalPermission(accesscontrol.ActionTeamsWrite, "teams:id:1"), + resolverPrefix: "teams:id:", + resolver: accesscontrol.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, scope string) ([]string, error) { + return []string{"another:scope"}, nil + }), + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + fakeService := actest.FakeService{} + ac := ProvideAccessControl(setting.NewCfg(), fakeService) + + if tt.resolver != nil { + ac.RegisterScopeAttributeResolver(tt.resolverPrefix, tt.resolver) + } + + hasAccess, err := ac.Evaluate(context.Background(), &tt.user, tt.evaluator) + assert.Equal(t, tt.expected, hasAccess) + if tt.expectedErr != nil { + assert.Equal(t, tt.expectedErr, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go b/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go deleted file mode 100644 index 3c87174e1ab..00000000000 --- a/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol.go +++ /dev/null @@ -1,205 +0,0 @@ -package ossaccesscontrol - -import ( - "context" - - "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/infra/metrics" - "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/accesscontrol/api" - "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/setting" - "github.com/prometheus/client_golang/prometheus" -) - -func ProvideService(cfg *setting.Cfg, store accesscontrol.PermissionsStore, routeRegister routing.RouteRegister) (*OSSAccessControlService, error) { - var errDeclareRoles error - s := ProvideOSSAccessControl(cfg, store) - if !s.IsDisabled() { - api := api.AccessControlAPI{ - RouteRegister: routeRegister, - AccessControl: s, - } - api.RegisterAPIEndpoints() - - errDeclareRoles = accesscontrol.DeclareFixedRoles(s) - } - - return s, errDeclareRoles -} - -func ProvideOSSAccessControl(cfg *setting.Cfg, store accesscontrol.PermissionsStore) *OSSAccessControlService { - s := &OSSAccessControlService{ - cfg: cfg, - store: store, - log: log.New("accesscontrol"), - scopeResolvers: accesscontrol.NewScopeResolvers(), - roles: accesscontrol.BuildBasicRoleDefinitions(), - } - - return s -} - -// OSSAccessControlService is the service implementing role based access control. -type OSSAccessControlService struct { - log log.Logger - cfg *setting.Cfg - scopeResolvers accesscontrol.ScopeResolvers - store accesscontrol.PermissionsStore - registrations accesscontrol.RegistrationList - roles map[string]*accesscontrol.RoleDTO -} - -func (ac *OSSAccessControlService) IsDisabled() bool { - if ac.cfg == nil { - return true - } - return !ac.cfg.RBACEnabled -} - -func (ac *OSSAccessControlService) GetUsageStats(_ context.Context) map[string]interface{} { - return map[string]interface{}{ - "stats.oss.accesscontrol.enabled.count": ac.getUsageMetrics(), - } -} - -func (ac *OSSAccessControlService) getUsageMetrics() interface{} { - if ac.IsDisabled() { - return 0 - } - - return 1 -} - -// Evaluate evaluates access to the given resources -func (ac *OSSAccessControlService) Evaluate(ctx context.Context, user *user.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) { - timer := prometheus.NewTimer(metrics.MAccessEvaluationsSummary) - defer timer.ObserveDuration() - metrics.MAccessEvaluationCount.Inc() - - if user.Permissions == nil { - user.Permissions = map[int64]map[string][]string{} - } - - if _, ok := user.Permissions[user.OrgID]; !ok { - permissions, err := ac.GetUserPermissions(ctx, user, accesscontrol.Options{ReloadCache: true}) - if err != nil { - return false, err - } - user.Permissions[user.OrgID] = accesscontrol.GroupScopesByAction(permissions) - } - - attributeMutator := ac.scopeResolvers.GetScopeAttributeMutator(user.OrgID) - resolvedEvaluator, err := evaluator.MutateScopes(ctx, attributeMutator) - if err != nil { - return false, err - } - return resolvedEvaluator.Evaluate(user.Permissions[user.OrgID]), nil -} - -var actionsToFetch = append( - TeamAdminActions, append(DashboardAdminActions, FolderAdminActions...)..., -) - -// GetUserPermissions returns user permissions based on built-in roles -func (ac *OSSAccessControlService) GetUserPermissions(ctx context.Context, user *user.SignedInUser, _ accesscontrol.Options) ([]accesscontrol.Permission, error) { - timer := prometheus.NewTimer(metrics.MAccessPermissionsSummary) - defer timer.ObserveDuration() - - permissions := ac.getFixedPermissions(ctx, user) - - dbPermissions, err := ac.store.GetUserPermissions(ctx, accesscontrol.GetUserPermissionsQuery{ - OrgID: user.OrgID, - UserID: user.UserID, - Roles: accesscontrol.GetOrgRoles(user), - TeamIDs: user.Teams, - Actions: actionsToFetch, - }) - if err != nil { - return nil, err - } - - permissions = append(permissions, dbPermissions...) - keywordMutator := ac.scopeResolvers.GetScopeKeywordMutator(user) - for i := range permissions { - // if the permission has a keyword in its scope it will be resolved - permissions[i].Scope, err = keywordMutator(ctx, permissions[i].Scope) - if err != nil { - return nil, err - } - } - - return permissions, nil -} - -func (ac *OSSAccessControlService) getFixedPermissions(ctx context.Context, user *user.SignedInUser) []accesscontrol.Permission { - permissions := make([]accesscontrol.Permission, 0) - - for _, builtin := range accesscontrol.GetOrgRoles(user) { - if basicRole, ok := ac.roles[builtin]; ok { - permissions = append(permissions, basicRole.Permissions...) - } - } - - return permissions -} - -// RegisterFixedRoles registers all declared roles in RAM -func (ac *OSSAccessControlService) RegisterFixedRoles(ctx context.Context) error { - // If accesscontrol is disabled no need to register roles - if ac.IsDisabled() { - return nil - } - ac.registrations.Range(func(registration accesscontrol.RoleRegistration) bool { - ac.registerFixedRole(registration.Role, registration.Grants) - return true - }) - return nil -} - -// RegisterFixedRole saves a fixed role and assigns it to built-in roles -func (ac *OSSAccessControlService) registerFixedRole(role accesscontrol.RoleDTO, builtInRoles []string) { - for br := range accesscontrol.BuiltInRolesWithParents(builtInRoles) { - if basicRole, ok := ac.roles[br]; ok { - basicRole.Permissions = append(basicRole.Permissions, role.Permissions...) - } else { - ac.log.Error("Unknown builtin role", "builtInRole", br) - } - } -} - -// DeclareFixedRoles allow the caller to declare, to the service, fixed roles and their assignments -// to organization roles ("Viewer", "Editor", "Admin") or "Grafana Admin" -func (ac *OSSAccessControlService) DeclareFixedRoles(registrations ...accesscontrol.RoleRegistration) error { - // If accesscontrol is disabled no need to register roles - if ac.IsDisabled() { - return nil - } - - for _, r := range registrations { - err := accesscontrol.ValidateFixedRole(r.Role) - if err != nil { - return err - } - - err = accesscontrol.ValidateBuiltInRoles(r.Grants) - if err != nil { - return err - } - - ac.registrations.Append(r) - } - - return nil -} - -// RegisterScopeAttributeResolver allows the caller to register scope resolvers for a -// specific scope prefix (ex: datasources:name:) -func (ac *OSSAccessControlService) RegisterScopeAttributeResolver(scopePrefix string, resolver accesscontrol.ScopeAttributeResolver) { - ac.scopeResolvers.AddScopeAttributeResolver(scopePrefix, resolver) -} - -func (ac *OSSAccessControlService) DeleteUserPermissions(ctx context.Context, orgID int64, userID int64) error { - return ac.store.DeleteUserPermissions(ctx, orgID, userID) -} diff --git a/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol_test.go b/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol_test.go deleted file mode 100644 index a6f2a5406ce..00000000000 --- a/pkg/services/accesscontrol/ossaccesscontrol/ossaccesscontrol_test.go +++ /dev/null @@ -1,495 +0,0 @@ -package ossaccesscontrol - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/grafana/grafana/pkg/api/routing" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/accesscontrol" - "github.com/grafana/grafana/pkg/services/accesscontrol/database" - "github.com/grafana/grafana/pkg/services/org" - "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/services/user" - "github.com/grafana/grafana/pkg/setting" -) - -func setupTestEnv(t testing.TB) *OSSAccessControlService { - t.Helper() - cfg := setting.NewCfg() - cfg.RBACEnabled = true - - ac := &OSSAccessControlService{ - cfg: cfg, - log: log.New("accesscontrol"), - registrations: accesscontrol.RegistrationList{}, - scopeResolvers: accesscontrol.NewScopeResolvers(), - store: database.ProvideService(sqlstore.InitTestDB(t)), - roles: accesscontrol.BuildBasicRoleDefinitions(), - } - require.NoError(t, ac.RegisterFixedRoles(context.Background())) - return ac -} - -// extractRawPermissionsHelper extracts action and scope fields only from a permission slice -func extractRawPermissionsHelper(perms []accesscontrol.Permission) []accesscontrol.Permission { - res := make([]accesscontrol.Permission, len(perms)) - for i, p := range perms { - res[i] = accesscontrol.Permission{Action: p.Action, Scope: p.Scope} - } - return res -} - -type evaluatingPermissionsTestCase struct { - desc string - user userTestCase - endpoints []endpointTestCase - evalResult bool -} - -type userTestCase struct { - name string - orgRole org.RoleType - isGrafanaAdmin bool -} - -type endpointTestCase struct { - evaluator accesscontrol.Evaluator -} - -func TestEvaluatingPermissions(t *testing.T) { - testCases := []evaluatingPermissionsTestCase{ - { - desc: "should successfully evaluate access to the endpoint", - user: userTestCase{ - name: "testuser", - orgRole: org.RoleViewer, - isGrafanaAdmin: true, - }, - endpoints: []endpointTestCase{ - {evaluator: accesscontrol.EvalPermission(accesscontrol.ActionUsersDisable, accesscontrol.ScopeGlobalUsersAll)}, - {evaluator: accesscontrol.EvalPermission(accesscontrol.ActionUsersEnable, accesscontrol.ScopeGlobalUsersAll)}, - }, - evalResult: true, - }, - { - desc: "should restrict access to the unauthorized endpoints", - user: userTestCase{ - name: "testuser", - orgRole: org.RoleViewer, - isGrafanaAdmin: false, - }, - endpoints: []endpointTestCase{ - {evaluator: accesscontrol.EvalPermission(accesscontrol.ActionUsersCreate, accesscontrol.ScopeGlobalUsersAll)}, - }, - evalResult: false, - }, - } - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - ac := setupTestEnv(t) - - // Use OSS roles for this test to pass - err := accesscontrol.DeclareFixedRoles(ac) - require.NoError(t, err) - - errRegisterRoles := ac.RegisterFixedRoles(context.Background()) - require.NoError(t, errRegisterRoles) - - user := &user.SignedInUser{ - UserID: 1, - OrgID: 1, - Name: tc.user.name, - OrgRole: tc.user.orgRole, - IsGrafanaAdmin: tc.user.isGrafanaAdmin, - } - - for _, endpoint := range tc.endpoints { - result, err := ac.Evaluate(context.Background(), user, endpoint.evaluator) - require.NoError(t, err) - assert.Equal(t, tc.evalResult, result) - } - }) - } -} - -func TestUsageMetrics(t *testing.T) { - tests := []struct { - name string - enabled bool - expectedValue int - }{ - { - name: "Expecting metric with value 0", - enabled: false, - expectedValue: 0, - }, - { - name: "Expecting metric with value 1", - enabled: true, - expectedValue: 1, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - cfg := setting.NewCfg() - cfg.RBACEnabled = tt.enabled - - s, errInitAc := ProvideService( - cfg, - database.ProvideService(sqlstore.InitTestDB(t)), - routing.NewRouteRegister(), - ) - require.NoError(t, errInitAc) - assert.Equal(t, tt.expectedValue, s.GetUsageStats(context.Background())["stats.oss.accesscontrol.enabled.count"]) - }) - } -} - -func TestOSSAccessControlService_RegisterFixedRole(t *testing.T) { - perm := accesscontrol.Permission{Action: "test:test", Scope: "test:*"} - - role := accesscontrol.RoleDTO{ - Version: 1, - Name: "fixed:test:test", - Permissions: []accesscontrol.Permission{perm}, - } - builtInRoles := []string{"Editor"} - - // Admin is going to get the role as well - includedBuiltInRoles := []string{"Editor", "Admin"} - - // Grafana Admin and Viewer won't get the role - excludedbuiltInRoles := []string{"Viewer", "Grafana Admin"} - - ac := setupTestEnv(t) - ac.registerFixedRole(role, builtInRoles) - - for _, br := range includedBuiltInRoles { - builtinRole, ok := ac.roles[br] - assert.True(t, ok) - assert.Contains(t, builtinRole.Permissions, perm) - } - - for _, br := range excludedbuiltInRoles { - builtinRole, ok := ac.roles[br] - assert.True(t, ok) - assert.NotContains(t, builtinRole.Permissions, perm) - } -} - -func TestOSSAccessControlService_DeclareFixedRoles(t *testing.T) { - tests := []struct { - name string - registrations []accesscontrol.RoleRegistration - wantErr bool - err error - }{ - { - name: "should work with empty list", - wantErr: false, - }, - { - name: "should add registration", - registrations: []accesscontrol.RoleRegistration{ - { - Role: accesscontrol.RoleDTO{ - Name: "fixed:test:test", - }, - Grants: []string{"Admin"}, - }, - }, - wantErr: false, - }, - { - name: "should fail registration invalid role name", - registrations: []accesscontrol.RoleRegistration{ - { - Role: accesscontrol.RoleDTO{ - Name: "custom:test:test", - }, - Grants: []string{"Admin"}, - }, - }, - wantErr: true, - err: accesscontrol.ErrFixedRolePrefixMissing, - }, - { - name: "should fail registration invalid builtin role assignment", - registrations: []accesscontrol.RoleRegistration{ - { - Role: accesscontrol.RoleDTO{ - Name: "fixed:test:test", - }, - Grants: []string{"WrongAdmin"}, - }, - }, - wantErr: true, - err: accesscontrol.ErrInvalidBuiltinRole, - }, - { - name: "should add multiple registrations at once", - registrations: []accesscontrol.RoleRegistration{ - { - Role: accesscontrol.RoleDTO{ - Name: "fixed:test:test", - }, - Grants: []string{"Admin"}, - }, - { - Role: accesscontrol.RoleDTO{ - Name: "fixed:test2:test2", - }, - Grants: []string{"Admin"}, - }, - }, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ac := setupTestEnv(t) - - // Reset the registations - ac.registrations = accesscontrol.RegistrationList{} - - // Test - err := ac.DeclareFixedRoles(tt.registrations...) - if tt.wantErr { - require.Error(t, err) - assert.ErrorIs(t, err, tt.err) - return - } - require.NoError(t, err) - - registrationCnt := 0 - ac.registrations.Range(func(registration accesscontrol.RoleRegistration) bool { - registrationCnt++ - return true - }) - assert.Equal(t, len(tt.registrations), registrationCnt, - "expected service registration list to contain all test registrations") - }) - } -} - -func TestOSSAccessControlService_RegisterFixedRoles(t *testing.T) { - tests := []struct { - name string - token models.Licensing - registrations []accesscontrol.RoleRegistration - wantErr bool - }{ - { - name: "should work with empty list", - }, - { - name: "should register and assign role", - registrations: []accesscontrol.RoleRegistration{ - { - Role: accesscontrol.RoleDTO{ - Name: "fixed:test:test", - Permissions: []accesscontrol.Permission{{Action: "test:test"}}, - }, - Grants: []string{"Editor"}, - }, - }, - wantErr: false, - }, - { - name: "should register and assign multiple roles", - registrations: []accesscontrol.RoleRegistration{ - { - Role: accesscontrol.RoleDTO{ - Name: "fixed:test:test", - Permissions: []accesscontrol.Permission{{Action: "test:test"}}, - }, - Grants: []string{"Editor"}, - }, - { - Role: accesscontrol.RoleDTO{ - Name: "fixed:test2:test2", - Permissions: []accesscontrol.Permission{ - {Action: "test:test2"}, - {Action: "test:test3", Scope: "test:*"}, - }, - }, - Grants: []string{"Viewer"}, - }, - }, - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - ac := setupTestEnv(t) - - ac.registrations.Append(tt.registrations...) - - // Test - err := ac.RegisterFixedRoles(context.Background()) - if tt.wantErr { - require.Error(t, err) - return - } - require.NoError(t, err) - - // Check - for _, registration := range tt.registrations { - // Check builtin roles (parents included) have been granted with the permissions - for br := range accesscontrol.BuiltInRolesWithParents(registration.Grants) { - builtinRole, ok := ac.roles[br] - assert.True(t, ok) - for _, expectedPermission := range registration.Role.Permissions { - assert.Contains(t, builtinRole.Permissions, expectedPermission) - } - } - } - }) - } -} - -func TestOSSAccessControlService_GetUserPermissions(t *testing.T) { - testUser := user.SignedInUser{ - UserID: 2, - OrgID: 3, - OrgName: "TestOrg", - OrgRole: org.RoleViewer, - Login: "testUser", - Name: "Test User", - Email: "testuser@example.org", - } - registration := accesscontrol.RoleRegistration{ - Role: accesscontrol.RoleDTO{ - UID: "fixed:test:test", - Name: "fixed:test:test", - Description: "Test role", - Permissions: []accesscontrol.Permission{}, - }, - Grants: []string{"Viewer"}, - } - tests := []struct { - name string - user user.SignedInUser - rawPerm accesscontrol.Permission - wantPerm accesscontrol.Permission - wantErr bool - }{ - { - name: "Translate users:self", - user: testUser, - rawPerm: accesscontrol.Permission{Action: "users:read", Scope: "users:self"}, - wantPerm: accesscontrol.Permission{Action: "users:read", Scope: "users:id:2"}, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Setup - ac := setupTestEnv(t) - - registration.Role.Permissions = []accesscontrol.Permission{tt.rawPerm} - err := ac.DeclareFixedRoles(registration) - require.NoError(t, err) - - err = ac.RegisterFixedRoles(context.Background()) - require.NoError(t, err) - - // Test - userPerms, err := ac.GetUserPermissions(context.Background(), &tt.user, accesscontrol.Options{}) - if tt.wantErr { - assert.Error(t, err, "Expected an error with GetUserPermissions.") - return - } - require.NoError(t, err, "Did not expect an error with GetUserPermissions.") - - rawUserPerms := extractRawPermissionsHelper(userPerms) - - assert.Contains(t, rawUserPerms, tt.wantPerm, "Expected resolution of raw permission") - assert.NotContains(t, rawUserPerms, tt.rawPerm, "Expected raw permission to have been resolved") - }) - } -} - -func TestOSSAccessControlService_Evaluate(t *testing.T) { - testUser := user.SignedInUser{ - UserID: 2, - OrgID: 3, - OrgName: "TestOrg", - OrgRole: org.RoleViewer, - Login: "testUser", - Name: "Test User", - Email: "testuser@example.org", - } - registration := accesscontrol.RoleRegistration{ - Role: accesscontrol.RoleDTO{ - UID: "fixed:test:test", - Name: "fixed:test:test", - Description: "Test role", - Permissions: []accesscontrol.Permission{}, - }, - Grants: []string{"Viewer"}, - } - userLoginScopeSolver := accesscontrol.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, initialScope string) ([]string, error) { - if initialScope == "users:login:testUser" { - return []string{"users:id:2"}, nil - } - return []string{initialScope}, nil - }) - - tests := []struct { - name string - user user.SignedInUser - rawPerm accesscontrol.Permission - evaluator accesscontrol.Evaluator - wantAccess bool - wantErr bool - }{ - { - name: "Should translate users:self", - user: testUser, - rawPerm: accesscontrol.Permission{Action: "users:read", Scope: "users:self"}, - evaluator: accesscontrol.EvalPermission("users:read", "users:id:2"), - wantAccess: true, - wantErr: false, - }, - { - name: "Should translate users:login:testUser", - user: testUser, - rawPerm: accesscontrol.Permission{Action: "users:read", Scope: "users:id:2"}, - evaluator: accesscontrol.EvalPermission("users:read", "users:login:testUser"), - wantAccess: true, - wantErr: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Setup - ac := setupTestEnv(t) - ac.RegisterScopeAttributeResolver("users:login:", userLoginScopeSolver) - - registration.Role.Permissions = []accesscontrol.Permission{tt.rawPerm} - err := ac.DeclareFixedRoles(registration) - require.NoError(t, err) - - err = ac.RegisterFixedRoles(context.Background()) - require.NoError(t, err) - - // Test - hasAccess, err := ac.Evaluate(context.TODO(), &tt.user, tt.evaluator) - if tt.wantErr { - assert.Error(t, err) - return - } - assert.NoError(t, err) - - assert.Equal(t, tt.wantAccess, hasAccess) - }) - } -} diff --git a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go index f24da3e2655..0535e783621 100644 --- a/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go +++ b/pkg/services/accesscontrol/ossaccesscontrol/permissions_services.go @@ -37,7 +37,7 @@ var ( func ProvideTeamPermissions( cfg *setting.Cfg, router routing.RouteRegister, sql *sqlstore.SQLStore, - ac accesscontrol.AccessControl, license models.Licensing, + ac accesscontrol.AccessControl, license models.Licensing, service accesscontrol.Service, ) (*TeamPermissionsService, error) { options := resourcepermissions.Options{ Resource: "teams", @@ -93,7 +93,7 @@ func ProvideTeamPermissions( }, } - srv, err := resourcepermissions.New(options, cfg, router, license, ac, sql) + srv, err := resourcepermissions.New(options, cfg, router, license, ac, service, sql) if err != nil { return nil, err } @@ -109,8 +109,8 @@ var DashboardEditActions = append(DashboardViewActions, []string{dashboards.Acti var DashboardAdminActions = append(DashboardEditActions, []string{dashboards.ActionDashboardsPermissionsRead, dashboards.ActionDashboardsPermissionsWrite}...) func ProvideDashboardPermissions( - cfg *setting.Cfg, router routing.RouteRegister, sql *sqlstore.SQLStore, - ac accesscontrol.AccessControl, license models.Licensing, dashboardStore dashboards.Store, + cfg *setting.Cfg, router routing.RouteRegister, sql *sqlstore.SQLStore, ac accesscontrol.AccessControl, + license models.Licensing, dashboardStore dashboards.Store, service accesscontrol.Service, ) (*DashboardPermissionsService, error) { getDashboard := func(ctx context.Context, orgID int64, resourceID string) (*models.Dashboard, error) { query := &models.GetDashboardQuery{Uid: resourceID, OrgId: orgID} @@ -164,7 +164,7 @@ func ProvideDashboardPermissions( RoleGroup: "Dashboards", } - srv, err := resourcepermissions.New(options, cfg, router, license, ac, sql) + srv, err := resourcepermissions.New(options, cfg, router, license, ac, service, sql) if err != nil { return nil, err } @@ -187,8 +187,8 @@ var FolderEditActions = append(FolderViewActions, []string{ var FolderAdminActions = append(FolderEditActions, []string{dashboards.ActionFoldersPermissionsRead, dashboards.ActionFoldersPermissionsWrite}...) func ProvideFolderPermissions( - cfg *setting.Cfg, router routing.RouteRegister, sql *sqlstore.SQLStore, - accesscontrol accesscontrol.AccessControl, license models.Licensing, dashboardStore dashboards.Store, + cfg *setting.Cfg, router routing.RouteRegister, sql *sqlstore.SQLStore, accesscontrol accesscontrol.AccessControl, + license models.Licensing, dashboardStore dashboards.Store, service accesscontrol.Service, ) (*FolderPermissionsService, error) { options := resourcepermissions.Options{ Resource: "folders", @@ -219,7 +219,7 @@ func ProvideFolderPermissions( WriterRoleName: "Folder permission writer", RoleGroup: "Folders", } - srv, err := resourcepermissions.New(options, cfg, router, license, accesscontrol, sql) + srv, err := resourcepermissions.New(options, cfg, router, license, accesscontrol, service, sql) if err != nil { return nil, err } @@ -263,8 +263,8 @@ type ServiceAccountPermissionsService struct { } func ProvideServiceAccountPermissions( - cfg *setting.Cfg, router routing.RouteRegister, sql *sqlstore.SQLStore, - ac accesscontrol.AccessControl, license models.Licensing, serviceAccountStore serviceaccounts.Store, + cfg *setting.Cfg, router routing.RouteRegister, sql *sqlstore.SQLStore, ac accesscontrol.AccessControl, + license models.Licensing, serviceAccountStore serviceaccounts.Store, service accesscontrol.Service, ) (*ServiceAccountPermissionsService, error) { options := resourcepermissions.Options{ Resource: "serviceaccounts", @@ -291,7 +291,7 @@ func ProvideServiceAccountPermissions( RoleGroup: "Service accounts", } - srv, err := resourcepermissions.New(options, cfg, router, license, ac, sql) + srv, err := resourcepermissions.New(options, cfg, router, license, ac, service, sql) if err != nil { return nil, err } diff --git a/pkg/services/accesscontrol/ossaccesscontrol/service.go b/pkg/services/accesscontrol/ossaccesscontrol/service.go new file mode 100644 index 00000000000..2250dd70f2c --- /dev/null +++ b/pkg/services/accesscontrol/ossaccesscontrol/service.go @@ -0,0 +1,141 @@ +package ossaccesscontrol + +import ( + "context" + + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/infra/metrics" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/api" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/setting" + "github.com/prometheus/client_golang/prometheus" +) + +func ProvideService(cfg *setting.Cfg, store accesscontrol.Store, routeRegister routing.RouteRegister) (*Service, error) { + service := ProvideOSSService(cfg, store) + + if !accesscontrol.IsDisabled(cfg) { + api.NewAccessControlAPI(routeRegister, service).RegisterAPIEndpoints() + if err := accesscontrol.DeclareFixedRoles(service); err != nil { + return nil, err + } + } + + return service, nil +} + +func ProvideOSSService(cfg *setting.Cfg, store accesscontrol.Store) *Service { + s := &Service{ + cfg: cfg, + store: store, + log: log.New("accesscontrol.service"), + roles: accesscontrol.BuildBasicRoleDefinitions(), + } + + return s +} + +// Service is the service implementing role based access control. +type Service struct { + log log.Logger + cfg *setting.Cfg + store accesscontrol.Store + registrations accesscontrol.RegistrationList + roles map[string]*accesscontrol.RoleDTO +} + +func (s *Service) GetUsageStats(_ context.Context) map[string]interface{} { + enabled := 0 + if !accesscontrol.IsDisabled(s.cfg) { + enabled = 1 + } + + return map[string]interface{}{ + "stats.oss.accesscontrol.enabled.count": enabled, + } +} + +var actionsToFetch = append( + TeamAdminActions, append(DashboardAdminActions, FolderAdminActions...)..., +) + +// GetUserPermissions returns user permissions based on built-in roles +func (s *Service) GetUserPermissions(ctx context.Context, user *user.SignedInUser, _ accesscontrol.Options) ([]accesscontrol.Permission, error) { + timer := prometheus.NewTimer(metrics.MAccessPermissionsSummary) + defer timer.ObserveDuration() + + permissions := make([]accesscontrol.Permission, 0) + + for _, builtin := range accesscontrol.GetOrgRoles(user) { + if basicRole, ok := s.roles[builtin]; ok { + permissions = append(permissions, basicRole.Permissions...) + } + } + + dbPermissions, err := s.store.GetUserPermissions(ctx, accesscontrol.GetUserPermissionsQuery{ + OrgID: user.OrgID, + UserID: user.UserID, + Roles: accesscontrol.GetOrgRoles(user), + TeamIDs: user.Teams, + Actions: actionsToFetch, + }) + if err != nil { + return nil, err + } + + return append(permissions, dbPermissions...), nil +} + +func (s *Service) DeleteUserPermissions(ctx context.Context, orgID int64, userID int64) error { + return s.store.DeleteUserPermissions(ctx, orgID, userID) +} + +// DeclareFixedRoles allow the caller to declare, to the service, fixed roles and their assignments +// to organization roles ("Viewer", "Editor", "Admin") or "Grafana Admin" +func (s *Service) DeclareFixedRoles(registrations ...accesscontrol.RoleRegistration) error { + // If accesscontrol is disabled no need to register roles + if accesscontrol.IsDisabled(s.cfg) { + return nil + } + + for _, r := range registrations { + err := accesscontrol.ValidateFixedRole(r.Role) + if err != nil { + return err + } + + err = accesscontrol.ValidateBuiltInRoles(r.Grants) + if err != nil { + return err + } + + s.registrations.Append(r) + } + + return nil +} + +// RegisterFixedRoles registers all declared roles in RAM +func (s *Service) RegisterFixedRoles(ctx context.Context) error { + // If accesscontrol is disabled no need to register roles + if accesscontrol.IsDisabled(s.cfg) { + return nil + } + s.registrations.Range(func(registration accesscontrol.RoleRegistration) bool { + for br := range accesscontrol.BuiltInRolesWithParents(registration.Grants) { + if basicRole, ok := s.roles[br]; ok { + basicRole.Permissions = append(basicRole.Permissions, registration.Role.Permissions...) + } else { + s.log.Error("Unknown builtin role", "builtInRole", br) + } + } + return true + }) + return nil +} + +func (s *Service) IsDisabled() bool { + return accesscontrol.IsDisabled(s.cfg) +} diff --git a/pkg/services/accesscontrol/ossaccesscontrol/service_test.go b/pkg/services/accesscontrol/ossaccesscontrol/service_test.go new file mode 100644 index 00000000000..3aac4e57489 --- /dev/null +++ b/pkg/services/accesscontrol/ossaccesscontrol/service_test.go @@ -0,0 +1,239 @@ +package ossaccesscontrol + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/database" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/setting" +) + +func setupTestEnv(t testing.TB) *Service { + t.Helper() + cfg := setting.NewCfg() + cfg.RBACEnabled = true + + ac := &Service{ + cfg: cfg, + log: log.New("accesscontrol"), + registrations: accesscontrol.RegistrationList{}, + store: database.ProvideService(sqlstore.InitTestDB(t)), + roles: accesscontrol.BuildBasicRoleDefinitions(), + } + require.NoError(t, ac.RegisterFixedRoles(context.Background())) + return ac +} + +func TestUsageMetrics(t *testing.T) { + tests := []struct { + name string + enabled bool + expectedValue int + }{ + { + name: "Expecting metric with value 0", + enabled: false, + expectedValue: 0, + }, + { + name: "Expecting metric with value 1", + enabled: true, + expectedValue: 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cfg := setting.NewCfg() + cfg.RBACEnabled = tt.enabled + + s, errInitAc := ProvideService( + cfg, + database.ProvideService(sqlstore.InitTestDB(t)), + routing.NewRouteRegister(), + ) + require.NoError(t, errInitAc) + assert.Equal(t, tt.expectedValue, s.GetUsageStats(context.Background())["stats.oss.accesscontrol.enabled.count"]) + }) + } +} + +func TestService_DeclareFixedRoles(t *testing.T) { + tests := []struct { + name string + registrations []accesscontrol.RoleRegistration + wantErr bool + err error + }{ + { + name: "should work with empty list", + wantErr: false, + }, + { + name: "should add registration", + registrations: []accesscontrol.RoleRegistration{ + { + Role: accesscontrol.RoleDTO{ + Name: "fixed:test:test", + }, + Grants: []string{"Admin"}, + }, + }, + wantErr: false, + }, + { + name: "should fail registration invalid role name", + registrations: []accesscontrol.RoleRegistration{ + { + Role: accesscontrol.RoleDTO{ + Name: "custom:test:test", + }, + Grants: []string{"Admin"}, + }, + }, + wantErr: true, + err: accesscontrol.ErrFixedRolePrefixMissing, + }, + { + name: "should fail registration invalid builtin role assignment", + registrations: []accesscontrol.RoleRegistration{ + { + Role: accesscontrol.RoleDTO{ + Name: "fixed:test:test", + }, + Grants: []string{"WrongAdmin"}, + }, + }, + wantErr: true, + err: accesscontrol.ErrInvalidBuiltinRole, + }, + { + name: "should add multiple registrations at once", + registrations: []accesscontrol.RoleRegistration{ + { + Role: accesscontrol.RoleDTO{ + Name: "fixed:test:test", + }, + Grants: []string{"Admin"}, + }, + { + Role: accesscontrol.RoleDTO{ + Name: "fixed:test2:test2", + }, + Grants: []string{"Admin"}, + }, + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ac := setupTestEnv(t) + + // Reset the registations + ac.registrations = accesscontrol.RegistrationList{} + + // Test + err := ac.DeclareFixedRoles(tt.registrations...) + if tt.wantErr { + require.Error(t, err) + assert.ErrorIs(t, err, tt.err) + return + } + require.NoError(t, err) + + registrationCnt := 0 + ac.registrations.Range(func(registration accesscontrol.RoleRegistration) bool { + registrationCnt++ + return true + }) + assert.Equal(t, len(tt.registrations), registrationCnt, + "expected service registration list to contain all test registrations") + }) + } +} + +func TestService_RegisterFixedRoles(t *testing.T) { + tests := []struct { + name string + token models.Licensing + registrations []accesscontrol.RoleRegistration + wantErr bool + }{ + { + name: "should work with empty list", + }, + { + name: "should register and assign role", + registrations: []accesscontrol.RoleRegistration{ + { + Role: accesscontrol.RoleDTO{ + Name: "fixed:test:test", + Permissions: []accesscontrol.Permission{{Action: "test:test"}}, + }, + Grants: []string{"Editor"}, + }, + }, + wantErr: false, + }, + { + name: "should register and assign multiple roles", + registrations: []accesscontrol.RoleRegistration{ + { + Role: accesscontrol.RoleDTO{ + Name: "fixed:test:test", + Permissions: []accesscontrol.Permission{{Action: "test:test"}}, + }, + Grants: []string{"Editor"}, + }, + { + Role: accesscontrol.RoleDTO{ + Name: "fixed:test2:test2", + Permissions: []accesscontrol.Permission{ + {Action: "test:test2"}, + {Action: "test:test3", Scope: "test:*"}, + }, + }, + Grants: []string{"Viewer"}, + }, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ac := setupTestEnv(t) + + ac.registrations.Append(tt.registrations...) + + // Test + err := ac.RegisterFixedRoles(context.Background()) + if tt.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + + // Check + for _, registration := range tt.registrations { + // Check builtin roles (parents included) have been granted with the permissions + for br := range accesscontrol.BuiltInRolesWithParents(registration.Grants) { + builtinRole, ok := ac.roles[br] + assert.True(t, ok) + for _, expectedPermission := range registration.Role.Permissions { + assert.Contains(t, builtinRole.Permissions, expectedPermission) + } + } + } + }) + } +} diff --git a/pkg/services/accesscontrol/resolvers.go b/pkg/services/accesscontrol/resolvers.go index b71d5ef8e0d..6fa933dad21 100644 --- a/pkg/services/accesscontrol/resolvers.go +++ b/pkg/services/accesscontrol/resolvers.go @@ -1,41 +1,54 @@ package accesscontrol import ( - "bytes" "context" "fmt" - "text/template" "time" "github.com/grafana/grafana/pkg/infra/localcache" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/services/user" ) +// ScopeAttributeResolver is used to resolve attributes in scopes to one or more scopes that are +// evaluated by logical or. E.g. "dashboards:id:1" -> "dashboards:uid:test-dashboard" or "folder:uid:test-folder" +type ScopeAttributeResolver interface { + Resolve(ctx context.Context, orgID int64, scope string) ([]string, error) +} + +// ScopeAttributeResolverFunc is an adapter to allow functions to implement ScopeAttributeResolver interface +type ScopeAttributeResolverFunc func(ctx context.Context, orgID int64, scope string) ([]string, error) + +func (f ScopeAttributeResolverFunc) Resolve(ctx context.Context, orgID int64, scope string) ([]string, error) { + return f(ctx, orgID, scope) +} + +type ScopeAttributeMutator func(context.Context, string) ([]string, error) + const ( ttl = 30 * time.Second cleanInterval = 2 * time.Minute ) -func NewScopeResolvers() ScopeResolvers { - return ScopeResolvers{ - keywordResolvers: map[string]ScopeKeywordResolver{ - "users:self": userSelfResolver, - }, - attributeResolvers: map[string]ScopeAttributeResolver{}, +func NewResolvers(log log.Logger) Resolvers { + return Resolvers{ + log: log, cache: localcache.New(ttl, cleanInterval), - log: log.New("accesscontrol.resolver"), + attributeResolvers: map[string]ScopeAttributeResolver{}, } } -type ScopeResolvers struct { +type Resolvers struct { log log.Logger cache *localcache.CacheService - keywordResolvers map[string]ScopeKeywordResolver attributeResolvers map[string]ScopeAttributeResolver } -func (s *ScopeResolvers) GetScopeAttributeMutator(orgID int64) ScopeAttributeMutator { +func (s *Resolvers) AddScopeAttributeResolver(prefix string, resolver ScopeAttributeResolver) { + s.log.Debug("adding scope attribute resolver for '%v'", prefix) + s.attributeResolvers[prefix] = resolver +} + +func (s *Resolvers) GetScopeAttributeMutator(orgID int64) ScopeAttributeMutator { return func(ctx context.Context, scope string) ([]string, error) { key := getScopeCacheKey(orgID, scope) // Check cache before computing the scope @@ -60,81 +73,7 @@ func (s *ScopeResolvers) GetScopeAttributeMutator(orgID int64) ScopeAttributeMut } } -func (s *ScopeResolvers) GetScopeKeywordMutator(user *user.SignedInUser) ScopeKeywordMutator { - return func(ctx context.Context, scope string) (string, error) { - if resolver, ok := s.keywordResolvers[scope]; ok { - scopes, err := resolver.Resolve(ctx, user) - if err != nil { - return "", fmt.Errorf("could not resolve %v: %w", scope, err) - } - s.log.Debug("resolved scope", "scope", scope, "resolved_scopes", scopes) - return scopes, nil - } - // By default, the scope remains unchanged - return scope, nil - } -} - -func (s *ScopeResolvers) AddScopeKeywordResolver(keyword string, resolver ScopeKeywordResolver) { - s.log.Debug("adding scope keyword resolver for '%v'", keyword) - s.keywordResolvers[keyword] = resolver -} - -func (s *ScopeResolvers) AddScopeAttributeResolver(prefix string, resolver ScopeAttributeResolver) { - s.log.Debug("adding scope attribute resolver for '%v'", prefix) - s.attributeResolvers[prefix] = resolver -} - -// ScopeAttributeResolver is used to resolve attributes in scopes to one or more scopes that are -// evaluated by logical or. E.g. "dashboards:id:1" -> "dashboards:uid:test-dashboard" or "folder:uid:test-folder" -type ScopeAttributeResolver interface { - Resolve(ctx context.Context, orgID int64, scope string) ([]string, error) -} - -// ScopeAttributeResolverFunc is an adapter to allow functions to implement ScopeAttributeResolver interface -type ScopeAttributeResolverFunc func(ctx context.Context, orgID int64, scope string) ([]string, error) - -func (f ScopeAttributeResolverFunc) Resolve(ctx context.Context, orgID int64, scope string) ([]string, error) { - return f(ctx, orgID, scope) -} - -type ScopeAttributeMutator func(context.Context, string) ([]string, error) - -// ScopeKeywordResolver is used to resolve keywords in scopes e.g. "users:self" -> "user:id:1". -// These type of resolvers is used when fetching stored permissions -type ScopeKeywordResolver interface { - Resolve(ctx context.Context, user *user.SignedInUser) (string, error) -} - -// ScopeKeywordResolverFunc is an adapter to allow functions to implement ScopeKeywordResolver interface -type ScopeKeywordResolverFunc func(ctx context.Context, user *user.SignedInUser) (string, error) - -func (f ScopeKeywordResolverFunc) Resolve(ctx context.Context, user *user.SignedInUser) (string, error) { - return f(ctx, user) -} - -type ScopeKeywordMutator func(context.Context, string) (string, error) - // getScopeCacheKey creates an identifier to fetch and store resolution of scopes in the cache func getScopeCacheKey(orgID int64, scope string) string { return fmt.Sprintf("%s-%v", scope, orgID) } - -//ScopeInjector inject request params into the templated scopes. e.g. "settings:" + eval.Parameters(":id") -func ScopeInjector(params ScopeParams) ScopeAttributeMutator { - return func(_ context.Context, scope string) ([]string, error) { - tmpl, err := template.New("scope").Parse(scope) - if err != nil { - return nil, err - } - var buf bytes.Buffer - if err = tmpl.Execute(&buf, params); err != nil { - return nil, err - } - return []string{buf.String()}, nil - } -} - -var userSelfResolver = ScopeKeywordResolverFunc(func(ctx context.Context, user *user.SignedInUser) (string, error) { - return Scope("users", "id", fmt.Sprintf("%v", user.UserID)), nil -}) diff --git a/pkg/services/accesscontrol/resolvers_test.go b/pkg/services/accesscontrol/resolvers_test.go index 435f19806c3..f7d4f8e8b7d 100644 --- a/pkg/services/accesscontrol/resolvers_test.go +++ b/pkg/services/accesscontrol/resolvers_test.go @@ -4,64 +4,13 @@ import ( "context" "testing" - "github.com/stretchr/testify/assert" - + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/datasources" - "github.com/grafana/grafana/pkg/services/org" - "github.com/grafana/grafana/pkg/services/user" + "github.com/stretchr/testify/assert" ) -func TestResolveKeywordScope(t *testing.T) { - tests := []struct { - name string - user *user.SignedInUser - permission accesscontrol.Permission - want accesscontrol.Permission - wantErr bool - }{ - { - name: "no scope", - user: testUser, - permission: accesscontrol.Permission{Action: "users:read"}, - want: accesscontrol.Permission{Action: "users:read"}, - wantErr: false, - }, - { - name: "user if resolution", - user: testUser, - permission: accesscontrol.Permission{Action: "users:read", Scope: "users:self"}, - want: accesscontrol.Permission{Action: "users:read", Scope: "users:id:2"}, - wantErr: false, - }, - } - for _, tt := range tests { - var err error - t.Run(tt.name, func(t *testing.T) { - resolvers := accesscontrol.NewScopeResolvers() - scopeModifier := resolvers.GetScopeKeywordMutator(tt.user) - tt.permission.Scope, err = scopeModifier(context.TODO(), tt.permission.Scope) - if tt.wantErr { - assert.Error(t, err, "expected an error during the resolution of the scope") - return - } - assert.NoError(t, err) - assert.EqualValues(t, tt.want, tt.permission, "permission did not match expected resolution") - }) - } -} - -var testUser = &user.SignedInUser{ - UserID: 2, - OrgID: 3, - OrgName: "TestOrg", - OrgRole: org.RoleViewer, - Login: "testUser", - Name: "Test User", - Email: "testuser@example.org", -} - -func TestResolveAttributeScope(t *testing.T) { +func TestResolvers_AttributeScope(t *testing.T) { // Calls allow us to see how many times the fakeDataSourceResolution has been called calls := 0 fakeDataSourceResolver := accesscontrol.ScopeAttributeResolverFunc(func(ctx context.Context, orgID int64, initialScope string) ([]string, error) { @@ -142,7 +91,7 @@ func TestResolveAttributeScope(t *testing.T) { }, } for _, tt := range tests { - resolvers := accesscontrol.NewScopeResolvers() + resolvers := accesscontrol.NewResolvers(log.NewNopLogger()) // Reset calls counter calls = 0 diff --git a/pkg/services/accesscontrol/resourcepermissions/service.go b/pkg/services/accesscontrol/resourcepermissions/service.go index df785a1e576..c1c4c5ff39b 100644 --- a/pkg/services/accesscontrol/resourcepermissions/service.go +++ b/pkg/services/accesscontrol/resourcepermissions/service.go @@ -48,8 +48,8 @@ type Store interface { } func New( - options Options, cfg *setting.Cfg, router routing.RouteRegister, - license models.Licensing, ac accesscontrol.AccessControl, sqlStore *sqlstore.SQLStore, + options Options, cfg *setting.Cfg, router routing.RouteRegister, license models.Licensing, + ac accesscontrol.AccessControl, service accesscontrol.Service, sqlStore *sqlstore.SQLStore, ) (*Service, error) { var permissions []string actionSet := make(map[string]struct{}) @@ -79,6 +79,7 @@ func New( permissions: permissions, actions: actions, sqlStore: sqlStore, + service: service, } s.api = newApi(ac, router, s) @@ -96,6 +97,7 @@ func New( type Service struct { cfg *setting.Cfg ac accesscontrol.AccessControl + service accesscontrol.Service store Store api *api license models.Licensing @@ -334,5 +336,5 @@ func (s *Service) declareFixedRoles() error { Grants: []string{string(org.RoleAdmin)}, } - return s.ac.DeclareFixedRoles(readerRole, writerRole) + return s.service.DeclareFixedRoles(readerRole, writerRole) } diff --git a/pkg/services/accesscontrol/resourcepermissions/service_test.go b/pkg/services/accesscontrol/resourcepermissions/service_test.go index 52723568d98..a931ff983e1 100644 --- a/pkg/services/accesscontrol/resourcepermissions/service_test.go +++ b/pkg/services/accesscontrol/resourcepermissions/service_test.go @@ -222,9 +222,10 @@ func setupTestEnvironment(t *testing.T, permissions []accesscontrol.Permission, cfg := setting.NewCfg() license := licensingtest.NewFakeLicensing() license.On("FeatureEnabled", "accesscontrol.enforcement").Return(true).Maybe() + mock := accesscontrolmock.New().WithPermissions(permissions) service, err := New( ops, cfg, routing.NewRouteRegister(), license, - accesscontrolmock.New().WithPermissions(permissions), sql, + accesscontrolmock.New().WithPermissions(permissions), mock, sql, ) require.NoError(t, err) diff --git a/pkg/services/accesscontrol/roles.go b/pkg/services/accesscontrol/roles.go index d8ea695aab8..e906f9f9c11 100644 --- a/pkg/services/accesscontrol/roles.go +++ b/pkg/services/accesscontrol/roles.go @@ -170,7 +170,7 @@ var ( ) // Declare OSS roles to the accesscontrol service -func DeclareFixedRoles(ac AccessControl) error { +func DeclareFixedRoles(service Service) error { ldapReader := RoleRegistration{ Role: ldapReaderRole, Grants: []string{RoleGrafanaAdmin}, @@ -204,7 +204,7 @@ func DeclareFixedRoles(ac AccessControl) error { Grants: []string{RoleGrafanaAdmin}, } - return ac.DeclareFixedRoles(ldapReader, ldapWriter, orgUsersReader, orgUsersWriter, + return service.DeclareFixedRoles(ldapReader, ldapWriter, orgUsersReader, orgUsersWriter, settingsReader, statsReader, usersReader, usersWriter) } diff --git a/pkg/services/guardian/accesscontrol_guardian_test.go b/pkg/services/guardian/accesscontrol_guardian_test.go index 6666154aeef..e97178993f9 100644 --- a/pkg/services/guardian/accesscontrol_guardian_test.go +++ b/pkg/services/guardian/accesscontrol_guardian_test.go @@ -602,10 +602,10 @@ func setupAccessControlGuardianTest(t *testing.T, uid string, permissions []acce license.On("FeatureEnabled", "accesscontrol.enforcement").Return(true).Maybe() folderPermissions, err := ossaccesscontrol.ProvideFolderPermissions( - setting.NewCfg(), routing.NewRouteRegister(), store, ac, license, &dashboards.FakeDashboardStore{}) + setting.NewCfg(), routing.NewRouteRegister(), store, ac, license, &dashboards.FakeDashboardStore{}, ac) require.NoError(t, err) dashboardPermissions, err := ossaccesscontrol.ProvideDashboardPermissions( - setting.NewCfg(), routing.NewRouteRegister(), store, ac, license, &dashboards.FakeDashboardStore{}) + setting.NewCfg(), routing.NewRouteRegister(), store, ac, license, &dashboards.FakeDashboardStore{}, ac) require.NoError(t, err) if dashboardSvc == nil { dashboardSvc = &dashboards.FakeDashboardService{} diff --git a/pkg/services/login/loginservice/loginservice.go b/pkg/services/login/loginservice/loginservice.go index 3fa7c7b4789..44b7f89f58d 100644 --- a/pkg/services/login/loginservice/loginservice.go +++ b/pkg/services/login/loginservice/loginservice.go @@ -22,7 +22,7 @@ func ProvideService( userService user.Service, quotaService quota.Service, authInfoService login.AuthInfoService, - accessControl accesscontrol.AccessControl, + accessControl accesscontrol.Service, ) *Implementation { s := &Implementation{ SQLStore: sqlStore, @@ -40,7 +40,7 @@ type Implementation struct { AuthInfoService login.AuthInfoService QuotaService quota.Service TeamSync login.TeamSyncFunc - accessControl accesscontrol.AccessControl + accessControl accesscontrol.Service } // CreateUser creates inserts a new one. diff --git a/pkg/services/publicdashboards/api/common_test.go b/pkg/services/publicdashboards/api/common_test.go index e0d37100d15..e636e9fa12f 100644 --- a/pkg/services/publicdashboards/api/common_test.go +++ b/pkg/services/publicdashboards/api/common_test.go @@ -62,8 +62,9 @@ func setupTestServer( } var err error - ac, err := ossaccesscontrol.ProvideService(cfg, database.ProvideService(db), rr) + acService, err := ossaccesscontrol.ProvideService(cfg, database.ProvideService(db), rr) require.NoError(t, err) + ac := ossaccesscontrol.ProvideAccessControl(cfg, acService) // build mux m := web.New() diff --git a/pkg/services/searchV2/auth.go b/pkg/services/searchV2/auth.go index bb78ac14ce7..b893668bbdf 100644 --- a/pkg/services/searchV2/auth.go +++ b/pkg/services/searchV2/auth.go @@ -23,7 +23,7 @@ var _ FutureAuthService = (*simpleSQLAuthService)(nil) type simpleSQLAuthService struct { sql *sqlstore.SQLStore - ac accesscontrol.AccessControl + ac accesscontrol.Service } type dashIdQueryResult struct { diff --git a/pkg/services/searchV2/service.go b/pkg/services/searchV2/service.go index a4259b479c7..e385623a0a0 100644 --- a/pkg/services/searchV2/service.go +++ b/pkg/services/searchV2/service.go @@ -56,7 +56,7 @@ type StandardSearchService struct { cfg *setting.Cfg sql *sqlstore.SQLStore auth FutureAuthService // eventually injected from elsewhere - ac accesscontrol.AccessControl + ac accesscontrol.Service logger log.Logger dashboardIndex *searchIndex @@ -64,7 +64,7 @@ type StandardSearchService struct { reIndexCh chan struct{} } -func ProvideService(cfg *setting.Cfg, sql *sqlstore.SQLStore, entityEventStore store.EntityEventsService, ac accesscontrol.AccessControl) SearchService { +func ProvideService(cfg *setting.Cfg, sql *sqlstore.SQLStore, entityEventStore store.EntityEventsService, ac accesscontrol.Service) SearchService { extender := &NoopExtender{} s := &StandardSearchService{ cfg: cfg, diff --git a/pkg/services/serviceaccounts/api/api_test.go b/pkg/services/serviceaccounts/api/api_test.go index c2f2274e11c..a33bf4ad375 100644 --- a/pkg/services/serviceaccounts/api/api_test.go +++ b/pkg/services/serviceaccounts/api/api_test.go @@ -278,7 +278,7 @@ func setupTestServer(t *testing.T, svc *tests.ServiceAccountMock, acmock *accesscontrolmock.Mock, sqlStore *sqlstore.SQLStore, saStore serviceaccounts.Store) (*web.Mux, *ServiceAccountsAPI) { cfg := setting.NewCfg() - saPermissionService, err := ossaccesscontrol.ProvideServiceAccountPermissions(cfg, routing.NewRouteRegister(), sqlStore, acmock, &licensing.OSSLicensingService{}, saStore) + saPermissionService, err := ossaccesscontrol.ProvideServiceAccountPermissions(cfg, routing.NewRouteRegister(), sqlStore, acmock, &licensing.OSSLicensingService{}, saStore, acmock) require.NoError(t, err) a := NewServiceAccountsAPI(cfg, svc, acmock, routerRegister, saStore, saPermissionService) diff --git a/pkg/services/user/userimpl/user.go b/pkg/services/user/userimpl/user.go index dbe0672cfaf..01ccfc45ad6 100644 --- a/pkg/services/user/userimpl/user.go +++ b/pkg/services/user/userimpl/user.go @@ -32,7 +32,7 @@ type Service struct { teamMemberService teamguardian.TeamGuardian userAuthService userauth.Service quotaService quota.Service - accessControlStore accesscontrol.AccessControl + accessControlStore accesscontrol.Service // TODO remove sqlstore sqlStore *sqlstore.SQLStore @@ -48,7 +48,7 @@ func ProvideService( teamMemberService teamguardian.TeamGuardian, userAuthService userauth.Service, quotaService quota.Service, - accessControlStore accesscontrol.AccessControl, + accessControlStore accesscontrol.Service, cfg *setting.Cfg, ss *sqlstore.SQLStore, ) user.Service {