From d4616cfe267d158bc237f63b85a1b42a5ef78d04 Mon Sep 17 00:00:00 2001 From: Artur Wierzbicki Date: Thu, 28 Apr 2022 22:16:23 +0400 Subject: [PATCH] Search: support auth filter based on access control/rbac (#48350) * #45498: add rbac support in searchv2 * to revert: fix dependency cycle * Revert "to revert: fix dependency cycle" This reverts commit 1ffbee73ec6e57561889981b69d229e0b649720e. * added a TODO for caching user permissions * add orgId to `getDashboardReadFilter` * use orgId from signedInUser * goimport --- .../manager/manager_integration_test.go | 2 +- pkg/services/searchV2/auth.go | 30 +++++--- pkg/services/searchV2/service.go | 74 ++++++++++++++----- 3 files changed, 77 insertions(+), 29 deletions(-) diff --git a/pkg/plugins/manager/manager_integration_test.go b/pkg/plugins/manager/manager_integration_test.go index a79742404c2..f27b77a9cff 100644 --- a/pkg/plugins/manager/manager_integration_test.go +++ b/pkg/plugins/manager/manager_integration_test.go @@ -86,7 +86,7 @@ func TestPluginManager_int_init(t *testing.T) { pg := postgres.ProvideService(cfg) my := mysql.ProvideService(cfg, hcp) ms := mssql.ProvideService(cfg) - sv2 := searchV2.ProvideService(cfg, sqlstore.InitTestDB(t), nil) + sv2 := searchV2.ProvideService(cfg, sqlstore.InitTestDB(t), nil, nil) graf := grafanads.ProvideService(cfg, sv2, nil) coreRegistry := coreplugin.ProvideCoreRegistry(am, cw, cm, es, grap, idb, lk, otsdb, pr, tmpo, td, pg, my, ms, graf) diff --git a/pkg/services/searchV2/auth.go b/pkg/services/searchV2/auth.go index 4f2d8cb6706..35e02808ca9 100644 --- a/pkg/services/searchV2/auth.go +++ b/pkg/services/searchV2/auth.go @@ -4,8 +4,10 @@ import ( "context" "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/services/sqlstore/permissions" + "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" ) // ResourceFilter checks if a given a uid (resource identifier) check if we have the requested permission @@ -16,32 +18,40 @@ type FutureAuthService interface { GetDashboardReadFilter(user *models.SignedInUser) (ResourceFilter, error) } +var _ FutureAuthService = (*simpleSQLAuthService)(nil) + type simpleSQLAuthService struct { sql *sqlstore.SQLStore + ac accesscontrol.AccessControl } type dashIdQueryResult struct { UID string `xorm:"uid"` } -func (a *simpleSQLAuthService) GetDashboardReadFilter(user *models.SignedInUser) (ResourceFilter, error) { - // this filter works on the legacy `dashboard_acl` table - // we will also need to use `accesscontrol` after https://github.com/grafana/grafana/pull/44702/files is merged - // see https://github.com/grafana/grafana/blob/e355bd6d3a04111b8c9959f85e81beabbeb746bf/pkg/services/sqlstore/permissions/dashboard.go#L84 - filter := permissions.DashboardPermissionFilter{ - OrgRole: user.OrgRole, - OrgId: user.OrgId, - Dialect: a.sql.Dialect, - UserId: user.UserId, - PermissionLevel: models.PERMISSION_VIEW, +func (a *simpleSQLAuthService) getDashboardTableAuthFilter(user *models.SignedInUser) searchstore.FilterWhere { + if a.ac.IsDisabled() { + return permissions.DashboardPermissionFilter{ + OrgRole: user.OrgRole, + OrgId: user.OrgId, + Dialect: a.sql.Dialect, + UserId: user.UserId, + PermissionLevel: models.PERMISSION_VIEW, + } } + return permissions.NewAccessControlDashboardPermissionFilter(user, models.PERMISSION_VIEW, searchstore.TypeDashboard) +} + +func (a *simpleSQLAuthService) GetDashboardReadFilter(user *models.SignedInUser) (ResourceFilter, error) { + filter := a.getDashboardTableAuthFilter(user) rows := make([]*dashIdQueryResult, 0) err := a.sql.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { sql, params := filter.Where() sess.Table("dashboard"). Where(sql, params...). + Where("org_id = ?", user.OrgId). Cols("uid") err := sess.Find(&rows) diff --git a/pkg/services/searchV2/service.go b/pkg/services/searchV2/service.go index 4ee5d82063e..17a098edd45 100644 --- a/pkg/services/searchV2/service.go +++ b/pkg/services/searchV2/service.go @@ -3,12 +3,14 @@ package searchV2 import ( "context" "encoding/json" + "errors" "fmt" "strconv" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/searchV2/extract" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -25,17 +27,20 @@ type StandardSearchService struct { cfg *setting.Cfg sql *sqlstore.SQLStore auth FutureAuthService // eventually injected from elsewhere + ac accesscontrol.AccessControl logger log.Logger dashboardIndex *dashboardIndex } -func ProvideService(cfg *setting.Cfg, sql *sqlstore.SQLStore, entityEventStore store.EntityEventsService) SearchService { +func ProvideService(cfg *setting.Cfg, sql *sqlstore.SQLStore, entityEventStore store.EntityEventsService, ac accesscontrol.AccessControl) SearchService { return &StandardSearchService{ cfg: cfg, sql: sql, + ac: ac, auth: &simpleSQLAuthService{ sql: sql, + ac: ac, }, dashboardIndex: newDashboardIndex(newSQLDashboardLoader(sql), entityEventStore), logger: log.New("searchV2"), @@ -53,6 +58,53 @@ func (s *StandardSearchService) Run(ctx context.Context) error { return s.dashboardIndex.run(ctx) } +func (s *StandardSearchService) getUser(ctx context.Context, backendUser *backend.User, orgId int64) (*models.SignedInUser, error) { + // TODO: get user & user's permissions from the request context + + getSignedInUserQuery := &models.GetSignedInUserQuery{ + Login: backendUser.Login, + Email: backendUser.Email, + OrgId: orgId, + } + + err := s.sql.GetSignedInUser(ctx, getSignedInUserQuery) + if err != nil { + s.logger.Error("Error while retrieving user", "error", err, "email", backendUser.Email) + return nil, errors.New("auth error") + } + + if getSignedInUserQuery.Result == nil { + s.logger.Error("No user found", "email", backendUser.Email) + return nil, errors.New("auth error") + } + + user := getSignedInUserQuery.Result + + if s.ac.IsDisabled() { + return user, nil + } + + if user.Permissions == nil { + user.Permissions = make(map[int64]map[string][]string) + } + + if _, ok := user.Permissions[orgId]; ok { + // permissions as part of the `s.sql.GetSignedInUser` query - return early + return user, nil + } + + // TODO: ensure this is cached + permissions, err := s.ac.GetUserPermissions(ctx, user, + accesscontrol.Options{ReloadCache: false}) + if err != nil { + s.logger.Error("failed to retrieve user permissions", "error", err, "email", backendUser.Email) + return nil, errors.New("auth error") + } + + user.Permissions[orgId] = accesscontrol.GroupScopesByAction(permissions) + return user, nil +} + func (s *StandardSearchService) DoDashboardQuery(ctx context.Context, user *backend.User, orgId int64, _ DashboardQuery) *backend.DataResponse { rsp := &backend.DataResponse{} @@ -62,27 +114,13 @@ func (s *StandardSearchService) DoDashboardQuery(ctx context.Context, user *back return rsp } - // TODO - get user from context? - getSignedInUserQuery := &models.GetSignedInUserQuery{ - Login: user.Login, - Email: user.Email, - OrgId: orgId, - } - - err = s.sql.GetSignedInUser(ctx, getSignedInUserQuery) + signedInUser, err := s.getUser(ctx, user, orgId) if err != nil { - s.logger.Error("Error while retrieving user", "error", err) - rsp.Error = fmt.Errorf("auth error") + rsp.Error = err return rsp } - if getSignedInUserQuery.Result == nil { - s.logger.Error("No user found", "email", user.Email) - rsp.Error = fmt.Errorf("auth error") - return rsp - } - - dashboards, err = s.applyAuthFilter(getSignedInUserQuery.Result, dashboards) + dashboards, err = s.applyAuthFilter(signedInUser, dashboards) if err != nil { rsp.Error = err return rsp