From 8057b9298de715d8cb4354e83198f6cdde5ead7b Mon Sep 17 00:00:00 2001 From: Eric Leijonmarck Date: Wed, 15 Nov 2023 13:33:38 +0000 Subject: [PATCH] Revert "RBAC: remove dashboard ACL logic from dash store, service #78130 (#78198) Revert "RBAC: remove dashboard ACL logic from dash store and service (#78130)" This reverts commit dd54931147cda6683ac1a1c1070dcd1324b1bdec. --- pkg/api/admin_users.go | 6 + pkg/services/dashboards/dashboard.go | 6 + .../dashboards/dashboard_service_mock.go | 54 ++++ pkg/services/dashboards/database/acl.go | 103 ++++++ pkg/services/dashboards/database/acl_test.go | 299 ++++++++++++++++++ pkg/services/dashboards/database/database.go | 30 ++ .../database/database_folder_test.go | 27 -- .../dashboards/database/database_test.go | 15 + pkg/services/dashboards/models.go | 6 + .../dashboards/service/dashboard_service.go | 12 + .../service/dashboard_service_test.go | 45 +++ pkg/services/dashboards/store_mock.go | 54 ++++ pkg/services/team/teamimpl/store_test.go | 145 +++++++++ pkg/services/user/userimpl/store_test.go | 151 +++++++++ 14 files changed, 926 insertions(+), 27 deletions(-) create mode 100644 pkg/services/dashboards/database/acl.go create mode 100644 pkg/services/dashboards/database/acl_test.go diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go index 65b231323bc..6539fb3de0b 100644 --- a/pkg/api/admin_users.go +++ b/pkg/api/admin_users.go @@ -227,6 +227,12 @@ func (hs *HTTPServer) AdminDeleteUser(c *contextmodel.ReqContext) response.Respo } return nil }) + g.Go(func() error { + if err := hs.DashboardService.DeleteACLByUser(ctx, cmd.UserID); err != nil { + return err + } + return nil + }) g.Go(func() error { if err := hs.preferenceService.DeleteByUser(ctx, cmd.UserID); err != nil { return err diff --git a/pkg/services/dashboards/dashboard.go b/pkg/services/dashboards/dashboard.go index c94da1f3f3b..dd2a8268de7 100644 --- a/pkg/services/dashboards/dashboard.go +++ b/pkg/services/dashboards/dashboard.go @@ -17,12 +17,15 @@ type DashboardService interface { DeleteDashboard(ctx context.Context, dashboardId int64, orgId int64) error FindDashboards(ctx context.Context, query *FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) GetDashboard(ctx context.Context, query *GetDashboardQuery) (*Dashboard, error) + GetDashboardACLInfoList(ctx context.Context, query *GetDashboardACLInfoListQuery) ([]*DashboardACLInfoDTO, error) GetDashboards(ctx context.Context, query *GetDashboardsQuery) ([]*Dashboard, error) GetDashboardTags(ctx context.Context, query *GetDashboardTagsQuery) ([]*DashboardTagCloudItem, error) GetDashboardUIDByID(ctx context.Context, query *GetDashboardRefByIDQuery) (*DashboardRef, error) ImportDashboard(ctx context.Context, dto *SaveDashboardDTO) (*Dashboard, error) SaveDashboard(ctx context.Context, dto *SaveDashboardDTO, allowUiUpdate bool) (*Dashboard, error) SearchDashboards(ctx context.Context, query *FindPersistedDashboardsQuery) (model.HitList, error) + UpdateDashboardACL(ctx context.Context, uid int64, items []*DashboardACL) error + DeleteACLByUser(ctx context.Context, userID int64) error CountInFolder(ctx context.Context, orgID int64, folderUID string, user identity.Requester) (int64, error) } @@ -53,6 +56,7 @@ type Store interface { DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *DeleteOrphanedProvisionedDashboardsCommand) error FindDashboards(ctx context.Context, query *FindPersistedDashboardsQuery) ([]DashboardSearchProjection, error) GetDashboard(ctx context.Context, query *GetDashboardQuery) (*Dashboard, error) + GetDashboardACLInfoList(ctx context.Context, query *GetDashboardACLInfoListQuery) ([]*DashboardACLInfoDTO, error) GetDashboardUIDByID(ctx context.Context, query *GetDashboardRefByIDQuery) (*DashboardRef, error) GetDashboards(ctx context.Context, query *GetDashboardsQuery) ([]*Dashboard, error) // GetDashboardsByPluginID retrieves dashboards identified by plugin. @@ -66,8 +70,10 @@ type Store interface { SaveDashboard(ctx context.Context, cmd SaveDashboardCommand) (*Dashboard, error) SaveProvisionedDashboard(ctx context.Context, cmd SaveDashboardCommand, provisioning *DashboardProvisioning) (*Dashboard, error) UnprovisionDashboard(ctx context.Context, id int64) error + UpdateDashboardACL(ctx context.Context, uid int64, items []*DashboardACL) error // ValidateDashboardBeforeSave validates a dashboard before save. ValidateDashboardBeforeSave(ctx context.Context, dashboard *Dashboard, overwrite bool) (bool, error) + DeleteACLByUser(context.Context, int64) error Count(context.Context, *quota.ScopeParameters) (*quota.Map, error) // CountDashboardsInFolder returns the number of dashboards associated with diff --git a/pkg/services/dashboards/dashboard_service_mock.go b/pkg/services/dashboards/dashboard_service_mock.go index 987252cab7d..e4c47eb8c5e 100644 --- a/pkg/services/dashboards/dashboard_service_mock.go +++ b/pkg/services/dashboards/dashboard_service_mock.go @@ -64,6 +64,20 @@ func (_m *FakeDashboardService) CountInFolder(ctx context.Context, orgID int64, return r0, r1 } +// DeleteACLByUser provides a mock function with given fields: ctx, userID +func (_m *FakeDashboardService) DeleteACLByUser(ctx context.Context, userID int64) error { + ret := _m.Called(ctx, userID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok { + r0 = rf(ctx, userID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // DeleteDashboard provides a mock function with given fields: ctx, dashboardId, orgId func (_m *FakeDashboardService) DeleteDashboard(ctx context.Context, dashboardId int64, orgId int64) error { ret := _m.Called(ctx, dashboardId, orgId) @@ -130,6 +144,32 @@ func (_m *FakeDashboardService) GetDashboard(ctx context.Context, query *GetDash return r0, r1 } +// GetDashboardACLInfoList provides a mock function with given fields: ctx, query +func (_m *FakeDashboardService) GetDashboardACLInfoList(ctx context.Context, query *GetDashboardACLInfoListQuery) ([]*DashboardACLInfoDTO, error) { + ret := _m.Called(ctx, query) + + var r0 []*DashboardACLInfoDTO + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardACLInfoListQuery) ([]*DashboardACLInfoDTO, error)); ok { + return rf(ctx, query) + } + if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardACLInfoListQuery) []*DashboardACLInfoDTO); ok { + r0 = rf(ctx, query) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*DashboardACLInfoDTO) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *GetDashboardACLInfoListQuery) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetDashboardTags provides a mock function with given fields: ctx, query func (_m *FakeDashboardService) GetDashboardTags(ctx context.Context, query *GetDashboardTagsQuery) ([]*DashboardTagCloudItem, error) { ret := _m.Called(ctx, query) @@ -286,6 +326,20 @@ func (_m *FakeDashboardService) SearchDashboards(ctx context.Context, query *Fin return r0, r1 } +// UpdateDashboardACL provides a mock function with given fields: ctx, uid, items +func (_m *FakeDashboardService) UpdateDashboardACL(ctx context.Context, uid int64, items []*DashboardACL) error { + ret := _m.Called(ctx, uid, items) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64, []*DashboardACL) error); ok { + r0 = rf(ctx, uid, items) + } else { + r0 = ret.Error(0) + } + + return r0 +} + type mockConstructorTestingTNewFakeDashboardService interface { mock.TestingT Cleanup(func()) diff --git a/pkg/services/dashboards/database/acl.go b/pkg/services/dashboards/database/acl.go new file mode 100644 index 00000000000..5fb35298280 --- /dev/null +++ b/pkg/services/dashboards/database/acl.go @@ -0,0 +1,103 @@ +package database + +import ( + "context" + + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/services/dashboards" +) + +// GetDashboardACLInfoList returns a list of permissions for a dashboard. They can be fetched from three +// different places. +// 1) Permissions for the dashboard +// 2) permissions for its parent folder +// 3) if no specific permissions have been set for the dashboard or its parent folder then get the default permissions +func (d *dashboardStore) GetDashboardACLInfoList(ctx context.Context, query *dashboards.GetDashboardACLInfoListQuery) ([]*dashboards.DashboardACLInfoDTO, error) { + queryResult := make([]*dashboards.DashboardACLInfoDTO, 0) + outerErr := d.store.WithDbSession(ctx, func(dbSession *db.Session) error { + falseStr := d.store.GetDialect().BooleanStr(false) + + if query.DashboardID == 0 { + sql := `SELECT + da.id, + da.org_id, + da.dashboard_id, + da.user_id, + da.team_id, + da.permission, + da.role, + da.created, + da.updated, + '' as user_login, + '' as user_email, + '' as team, + '' as title, + '' as slug, + '' as uid,` + + falseStr + ` AS is_folder,` + + falseStr + ` AS inherited + FROM dashboard_acl as da + WHERE da.dashboard_id = -1` + return dbSession.SQL(sql).Find(&queryResult) + } + + rawSQL := ` + -- get permissions for the dashboard and its parent folder + SELECT + da.id, + da.org_id, + da.dashboard_id, + da.user_id, + da.team_id, + da.permission, + da.role, + da.created, + da.updated, + u.login AS user_login, + u.email AS user_email, + ug.name AS team, + ug.email AS team_email, + d.title, + d.slug, + d.uid, + d.is_folder, + CASE WHEN (da.dashboard_id = -1 AND d.folder_id > 0) OR da.dashboard_id = d.folder_id THEN ` + d.store.GetDialect().BooleanStr(true) + ` ELSE ` + falseStr + ` END AS inherited + FROM dashboard as d + LEFT JOIN dashboard folder on folder.id = d.folder_id + LEFT JOIN dashboard_acl AS da ON + da.dashboard_id = d.id OR + da.dashboard_id = d.folder_id OR + ( + -- include default permissions --> + da.org_id = -1 AND ( + (folder.id IS NOT NULL AND folder.has_acl = ` + falseStr + `) OR + (folder.id IS NULL AND d.has_acl = ` + falseStr + `) + ) + ) + LEFT JOIN ` + d.store.GetDialect().Quote("user") + ` AS u ON u.id = da.user_id + LEFT JOIN team ug on ug.id = da.team_id + WHERE d.org_id = ? AND d.id = ? AND da.id IS NOT NULL + ORDER BY da.id ASC + ` + + return dbSession.SQL(rawSQL, query.OrgID, query.DashboardID).Find(&queryResult) + }) + + if outerErr != nil { + return nil, outerErr + } + + for _, p := range queryResult { + p.PermissionName = p.Permission.String() + } + + return queryResult, nil +} + +func (d *dashboardStore) DeleteACLByUser(ctx context.Context, userID int64) error { + return d.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error { + var rawSQL = "DELETE FROM dashboard_acl WHERE user_id = ?" + _, err := sess.Exec(rawSQL, userID) + return err + }) +} diff --git a/pkg/services/dashboards/database/acl_test.go b/pkg/services/dashboards/database/acl_test.go new file mode 100644 index 00000000000..ccf54681704 --- /dev/null +++ b/pkg/services/dashboards/database/acl_test.go @@ -0,0 +1,299 @@ +package database + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/org" + "github.com/grafana/grafana/pkg/services/org/orgimpl" + "github.com/grafana/grafana/pkg/services/quota/quotaimpl" + "github.com/grafana/grafana/pkg/services/quota/quotatest" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" + "github.com/grafana/grafana/pkg/services/tag/tagimpl" + "github.com/grafana/grafana/pkg/services/team/teamimpl" + "github.com/grafana/grafana/pkg/services/user" + "github.com/grafana/grafana/pkg/services/user/userimpl" +) + +func TestIntegrationDashboardACLDataAccess(t *testing.T) { + if testing.Short() { + t.Skip("skipping integration test") + } + var sqlStore *sqlstore.SQLStore + var currentUser user.User + var savedFolder, childDash *dashboards.Dashboard + var dashboardStore dashboards.Store + + setup := func(t *testing.T) int64 { + sqlStore = db.InitTestDB(t) + quotaService := quotatest.New(false, nil) + var err error + dashboardStore, err = ProvideDashboardStore(sqlStore, sqlStore.Cfg, testFeatureToggles, tagimpl.ProvideService(sqlStore), quotaService) + require.NoError(t, err) + currentUser = createUser(t, sqlStore, "viewer", "Viewer", false) + savedFolder = insertTestDashboard(t, dashboardStore, "1 test dash folder", 1, 0, "", true, "prod", "webapp") + childDash = insertTestDashboard(t, dashboardStore, "2 test dash", 1, savedFolder.ID, savedFolder.UID, false, "prod", "webapp") + return currentUser.OrgID + } + + t.Run("Dashboard permission with userId and teamId set to 0", func(t *testing.T) { + orgID := setup(t) + err := updateDashboardACL(t, dashboardStore, savedFolder.ID, dashboards.DashboardACL{ + OrgID: orgID, + DashboardID: savedFolder.ID, + Permission: dashboards.PERMISSION_EDIT, + }) + require.Equal(t, dashboards.ErrDashboardACLInfoMissing, err) + }) + + t.Run("Folder acl should include default acl", func(t *testing.T) { + orgID := setup(t) + query := dashboards.GetDashboardACLInfoListQuery{DashboardID: savedFolder.ID, OrgID: orgID} + + queryResult, err := dashboardStore.GetDashboardACLInfoList(context.Background(), &query) + require.Nil(t, err) + + require.Equal(t, 2, len(queryResult)) + defaultPermissionsId := int64(-1) + require.Equal(t, defaultPermissionsId, queryResult[0].DashboardID) + require.Equal(t, org.RoleViewer, *queryResult[0].Role) + require.False(t, queryResult[0].Inherited) + require.Equal(t, defaultPermissionsId, queryResult[1].DashboardID) + require.Equal(t, org.RoleEditor, *queryResult[1].Role) + require.False(t, queryResult[1].Inherited) + }) + + t.Run("Dashboard acl should include acl for parent folder", func(t *testing.T) { + orgID := setup(t) + query := dashboards.GetDashboardACLInfoListQuery{DashboardID: childDash.ID, OrgID: orgID} + + queryResult, err := dashboardStore.GetDashboardACLInfoList(context.Background(), &query) + require.Nil(t, err) + + require.Equal(t, 2, len(queryResult)) + defaultPermissionsId := int64(-1) + require.Equal(t, defaultPermissionsId, queryResult[0].DashboardID) + require.Equal(t, org.RoleViewer, *queryResult[0].Role) + require.True(t, queryResult[0].Inherited) + require.Equal(t, defaultPermissionsId, queryResult[1].DashboardID) + require.Equal(t, org.RoleEditor, *queryResult[1].Role) + require.True(t, queryResult[1].Inherited) + }) + + t.Run("Folder with removed default permissions returns no acl items", func(t *testing.T) { + orgID := setup(t) + err := dashboardStore.UpdateDashboardACL(context.Background(), savedFolder.ID, nil) + require.Nil(t, err) + + query := dashboards.GetDashboardACLInfoListQuery{DashboardID: childDash.ID, OrgID: orgID} + queryResult, err := dashboardStore.GetDashboardACLInfoList(context.Background(), &query) + require.Nil(t, err) + + require.Equal(t, 0, len(queryResult)) + }) + + t.Run("Given a dashboard folder and a user", func(t *testing.T) { + t.Run("Given dashboard folder permission", func(t *testing.T) { + orgID := setup(t) + err := updateDashboardACL(t, dashboardStore, savedFolder.ID, dashboards.DashboardACL{ + OrgID: orgID, + UserID: currentUser.ID, + DashboardID: savedFolder.ID, + Permission: dashboards.PERMISSION_EDIT, + }) + require.Nil(t, err) + + t.Run("When reading dashboard acl should include acl for parent folder", func(t *testing.T) { + query := dashboards.GetDashboardACLInfoListQuery{DashboardID: childDash.ID, OrgID: orgID} + + queryResult, err := dashboardStore.GetDashboardACLInfoList(context.Background(), &query) + require.Nil(t, err) + + require.Equal(t, 1, len(queryResult)) + require.Equal(t, savedFolder.ID, queryResult[0].DashboardID) + }) + + t.Run("Given child dashboard permission", func(t *testing.T) { + err := updateDashboardACL(t, dashboardStore, childDash.ID, dashboards.DashboardACL{ + OrgID: orgID, + UserID: currentUser.ID, + DashboardID: childDash.ID, + Permission: dashboards.PERMISSION_EDIT, + }) + require.Nil(t, err) + + t.Run("When reading dashboard acl should include acl for parent folder and child", func(t *testing.T) { + query := dashboards.GetDashboardACLInfoListQuery{OrgID: orgID, DashboardID: childDash.ID} + + queryResult, err := dashboardStore.GetDashboardACLInfoList(context.Background(), &query) + require.Nil(t, err) + + require.Equal(t, 2, len(queryResult)) + require.Equal(t, savedFolder.ID, queryResult[0].DashboardID) + require.True(t, queryResult[0].Inherited) + require.Equal(t, childDash.ID, queryResult[1].DashboardID) + require.False(t, queryResult[1].Inherited) + }) + }) + }) + + t.Run("Reading dashboard acl should include default acl for parent folder and the child acl", func(t *testing.T) { + orgID := setup(t) + err := updateDashboardACL(t, dashboardStore, childDash.ID, dashboards.DashboardACL{ + OrgID: 1, + UserID: currentUser.ID, + DashboardID: childDash.ID, + Permission: dashboards.PERMISSION_EDIT, + }) + require.Nil(t, err) + + query := dashboards.GetDashboardACLInfoListQuery{OrgID: orgID, DashboardID: childDash.ID} + + queryResult, err := dashboardStore.GetDashboardACLInfoList(context.Background(), &query) + require.Nil(t, err) + + defaultPermissionsId := int64(-1) + require.Equal(t, 3, len(queryResult)) + require.Equal(t, defaultPermissionsId, queryResult[0].DashboardID) + require.Equal(t, org.RoleViewer, *queryResult[0].Role) + require.True(t, queryResult[0].Inherited) + require.Equal(t, defaultPermissionsId, queryResult[1].DashboardID) + require.Equal(t, org.RoleEditor, *queryResult[1].Role) + require.True(t, queryResult[1].Inherited) + require.Equal(t, childDash.ID, queryResult[2].DashboardID) + require.False(t, queryResult[2].Inherited) + }) + + t.Run("Add and delete dashboard permission", func(t *testing.T) { + orgID := setup(t) + err := updateDashboardACL(t, dashboardStore, savedFolder.ID, dashboards.DashboardACL{ + OrgID: 1, + UserID: currentUser.ID, + DashboardID: savedFolder.ID, + Permission: dashboards.PERMISSION_EDIT, + }) + require.Nil(t, err) + + q1 := &dashboards.GetDashboardACLInfoListQuery{DashboardID: savedFolder.ID, OrgID: orgID} + q1Result, err := dashboardStore.GetDashboardACLInfoList(context.Background(), q1) + require.Nil(t, err) + + require.Equal(t, savedFolder.ID, q1Result[0].DashboardID) + require.Equal(t, dashboards.PERMISSION_EDIT, q1Result[0].Permission) + require.Equal(t, "Edit", q1Result[0].PermissionName) + require.Equal(t, currentUser.ID, q1Result[0].UserID) + require.Equal(t, currentUser.Login, q1Result[0].UserLogin) + require.Equal(t, currentUser.Email, q1Result[0].UserEmail) + + err = updateDashboardACL(t, dashboardStore, savedFolder.ID) + require.Nil(t, err) + + q3 := &dashboards.GetDashboardACLInfoListQuery{DashboardID: savedFolder.ID, OrgID: orgID} + q3Result, err := dashboardStore.GetDashboardACLInfoList(context.Background(), q3) + require.Nil(t, err) + require.Equal(t, 0, len(q3Result)) + }) + + t.Run("Should be able to add a user permission for a team", func(t *testing.T) { + orgID := setup(t) + teamSvc := teamimpl.ProvideService(sqlStore, sqlStore.Cfg) + team1, err := teamSvc.CreateTeam("group1 name", "", 1) + require.Nil(t, err) + + err = updateDashboardACL(t, dashboardStore, savedFolder.ID, dashboards.DashboardACL{ + OrgID: 1, + TeamID: team1.ID, + DashboardID: savedFolder.ID, + Permission: dashboards.PERMISSION_EDIT, + }) + require.Nil(t, err) + + q1 := &dashboards.GetDashboardACLInfoListQuery{DashboardID: savedFolder.ID, OrgID: orgID} + q1Result, err := dashboardStore.GetDashboardACLInfoList(context.Background(), q1) + require.Nil(t, err) + require.Equal(t, savedFolder.ID, q1Result[0].DashboardID) + require.Equal(t, dashboards.PERMISSION_EDIT, q1Result[0].Permission) + require.Equal(t, team1.ID, q1Result[0].TeamID) + }) + + t.Run("Should be able to update an existing permission for a team", func(t *testing.T) { + orgID := setup(t) + teamSvc := teamimpl.ProvideService(sqlStore, sqlStore.Cfg) + team1, err := teamSvc.CreateTeam("group1 name", "", 1) + require.Nil(t, err) + err = updateDashboardACL(t, dashboardStore, savedFolder.ID, dashboards.DashboardACL{ + OrgID: 1, + TeamID: team1.ID, + DashboardID: savedFolder.ID, + Permission: dashboards.PERMISSION_ADMIN, + }) + require.Nil(t, err) + + q3 := &dashboards.GetDashboardACLInfoListQuery{DashboardID: savedFolder.ID, OrgID: orgID} + q3Result, err := dashboardStore.GetDashboardACLInfoList(context.Background(), q3) + require.Nil(t, err) + require.Equal(t, 1, len(q3Result)) + require.Equal(t, savedFolder.ID, q3Result[0].DashboardID) + require.Equal(t, dashboards.PERMISSION_ADMIN, q3Result[0].Permission) + require.Equal(t, team1.ID, q3Result[0].TeamID) + }) + }) + + t.Run("Default permissions for root folder dashboards", func(t *testing.T) { + orgID := setup(t) + var rootFolderId int64 = 0 + //sqlStore := db.InitTestDB(t) + + query := dashboards.GetDashboardACLInfoListQuery{DashboardID: rootFolderId, OrgID: orgID} + + queryResult, err := dashboardStore.GetDashboardACLInfoList(context.Background(), &query) + require.Nil(t, err) + + require.Equal(t, 2, len(queryResult)) + defaultPermissionsId := int64(-1) + require.Equal(t, defaultPermissionsId, queryResult[0].DashboardID) + require.Equal(t, org.RoleViewer, *queryResult[0].Role) + require.False(t, queryResult[0].Inherited) + require.Equal(t, defaultPermissionsId, queryResult[1].DashboardID) + require.Equal(t, org.RoleEditor, *queryResult[1].Role) + require.False(t, queryResult[1].Inherited) + }) + + t.Run("Delete acl by user", func(t *testing.T) { + setup(t) + err := dashboardStore.DeleteACLByUser(context.Background(), currentUser.ID) + require.NoError(t, err) + }) +} + +func createUser(t *testing.T, sqlStore *sqlstore.SQLStore, name string, role string, isAdmin bool) user.User { + t.Helper() + sqlStore.Cfg.AutoAssignOrg = true + sqlStore.Cfg.AutoAssignOrgId = 1 + sqlStore.Cfg.AutoAssignOrgRole = role + + qs := quotaimpl.ProvideService(sqlStore, sqlStore.Cfg) + orgService, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, qs) + require.NoError(t, err) + usrSvc, err := userimpl.ProvideService(sqlStore, orgService, sqlStore.Cfg, nil, nil, qs, supportbundlestest.NewFakeBundleService()) + require.NoError(t, err) + + o, err := orgService.CreateWithMember(context.Background(), &org.CreateOrgCommand{Name: fmt.Sprintf("test org %d", time.Now().UnixNano())}) + require.NoError(t, err) + + currentUserCmd := user.CreateUserCommand{Login: name, Email: name + "@test.com", Name: "a " + name, IsAdmin: isAdmin, OrgID: o.ID} + currentUser, err := usrSvc.Create(context.Background(), ¤tUserCmd) + require.NoError(t, err) + orgs, err := orgService.GetUserOrgList(context.Background(), &org.GetUserOrgListQuery{UserID: currentUser.ID}) + require.NoError(t, err) + require.Equal(t, org.RoleType(role), orgs[0].Role) + require.Equal(t, o.ID, orgs[0].OrgID) + return *currentUser +} diff --git a/pkg/services/dashboards/database/database.go b/pkg/services/dashboards/database/database.go index afe994f096c..2b52dae49fa 100644 --- a/pkg/services/dashboards/database/database.go +++ b/pkg/services/dashboards/database/database.go @@ -171,6 +171,36 @@ func (d *dashboardStore) SaveDashboard(ctx context.Context, cmd dashboards.SaveD return result, err } +func (d *dashboardStore) UpdateDashboardACL(ctx context.Context, dashboardID int64, items []*dashboards.DashboardACL) error { + return d.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error { + // delete existing items + _, err := sess.Exec("DELETE FROM dashboard_acl WHERE dashboard_id=?", dashboardID) + if err != nil { + return fmt.Errorf("deleting from dashboard_acl failed: %w", err) + } + + for _, item := range items { + if item.UserID == 0 && item.TeamID == 0 && (item.Role == nil || !item.Role.IsValid()) { + return dashboards.ErrDashboardACLInfoMissing + } + + if item.DashboardID == 0 { + return dashboards.ErrDashboardPermissionDashboardEmpty + } + + sess.Nullable("user_id", "team_id") + if _, err := sess.Insert(item); err != nil { + return err + } + } + + // Update dashboard HasACL flag + dashboard := dashboards.Dashboard{HasACL: true} + _, err = sess.Cols("has_acl").Where("id=?", dashboardID).Update(&dashboard) + return err + }) +} + func (d *dashboardStore) SaveAlerts(ctx context.Context, dashID int64, alerts []*alertmodels.Alert) error { return d.store.WithTransactionalDbSession(ctx, func(sess *db.Session) error { existingAlerts, err := GetAlertsByDashboardId2(dashID, sess) diff --git a/pkg/services/dashboards/database/database_folder_test.go b/pkg/services/dashboards/database/database_folder_test.go index c1846a57817..4748a241aaf 100644 --- a/pkg/services/dashboards/database/database_folder_test.go +++ b/pkg/services/dashboards/database/database_folder_test.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "testing" - "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -24,7 +23,6 @@ import ( "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" - "github.com/grafana/grafana/pkg/services/quota/quotaimpl" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" @@ -452,28 +450,3 @@ func moveDashboard(t *testing.T, dashboardStore dashboards.Store, orgId int64, d return dash } - -func createUser(t *testing.T, sqlStore *sqlstore.SQLStore, name string, role string, isAdmin bool) user.User { - t.Helper() - sqlStore.Cfg.AutoAssignOrg = true - sqlStore.Cfg.AutoAssignOrgId = 1 - sqlStore.Cfg.AutoAssignOrgRole = role - - qs := quotaimpl.ProvideService(sqlStore, sqlStore.Cfg) - orgService, err := orgimpl.ProvideService(sqlStore, sqlStore.Cfg, qs) - require.NoError(t, err) - usrSvc, err := userimpl.ProvideService(sqlStore, orgService, sqlStore.Cfg, nil, nil, qs, supportbundlestest.NewFakeBundleService()) - require.NoError(t, err) - - o, err := orgService.CreateWithMember(context.Background(), &org.CreateOrgCommand{Name: fmt.Sprintf("test org %d", time.Now().UnixNano())}) - require.NoError(t, err) - - currentUserCmd := user.CreateUserCommand{Login: name, Email: name + "@test.com", Name: "a " + name, IsAdmin: isAdmin, OrgID: o.ID} - currentUser, err := usrSvc.Create(context.Background(), ¤tUserCmd) - require.NoError(t, err) - orgs, err := orgService.GetUserOrgList(context.Background(), &org.GetUserOrgListQuery{UserID: currentUser.ID}) - require.NoError(t, err) - require.Equal(t, org.RoleType(role), orgs[0].Role) - require.Equal(t, o.ID, orgs[0].OrgID) - return *currentUser -} diff --git a/pkg/services/dashboards/database/database_test.go b/pkg/services/dashboards/database/database_test.go index 997289dcaa4..af7238567d6 100644 --- a/pkg/services/dashboards/database/database_test.go +++ b/pkg/services/dashboards/database/database_test.go @@ -1183,6 +1183,21 @@ func insertTestDashboardForPlugin(t *testing.T, dashboardStore dashboards.Store, return dash } +func updateDashboardACL(t *testing.T, dashboardStore dashboards.Store, dashboardID int64, + items ...dashboards.DashboardACL) error { + t.Helper() + + var itemPtrs []*dashboards.DashboardACL + for _, it := range items { + item := it + item.Created = time.Now() + item.Updated = time.Now() + itemPtrs = append(itemPtrs, &item) + } + + return dashboardStore.UpdateDashboardACL(context.Background(), dashboardID, itemPtrs) +} + // testSearchDashboards is a (near) copy of the dashboard service // SearchDashboards, which is a wrapper around FindDashboards. func testSearchDashboards(d dashboards.Store, query *dashboards.FindPersistedDashboardsQuery) (model.HitList, error) { diff --git a/pkg/services/dashboards/models.go b/pkg/services/dashboards/models.go index 187ddd9624f..98c2a59eacd 100644 --- a/pkg/services/dashboards/models.go +++ b/pkg/services/dashboards/models.go @@ -473,6 +473,12 @@ func (dto *DashboardACLInfoDTO) IsDuplicateOf(other *DashboardACLInfoDTO) bool { return dto.hasSameRoleAs(other) || dto.hasSameUserAs(other) || dto.hasSameTeamAs(other) } +// QUERIES +type GetDashboardACLInfoListQuery struct { + DashboardID int64 + OrgID int64 +} + type FindPersistedDashboardsQuery struct { Title string OrgId int64 diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 8f23c35d42b..9e480d7d88e 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -221,6 +221,10 @@ func resolveUserID(user identity.Requester, log log.Logger) (int64, error) { return userID, nil } +func (dr *DashboardServiceImpl) UpdateDashboardACL(ctx context.Context, uid int64, items []*dashboards.DashboardACL) error { + return dr.dashboardStore.UpdateDashboardACL(ctx, uid, items) +} + func (dr *DashboardServiceImpl) DeleteOrphanedProvisionedDashboards(ctx context.Context, cmd *dashboards.DeleteOrphanedProvisionedDashboardsCommand) error { return dr.dashboardStore.DeleteOrphanedProvisionedDashboards(ctx, cmd) } @@ -580,10 +584,18 @@ func makeQueryResult(query *dashboards.FindPersistedDashboardsQuery, res []dashb return hitList } +func (dr *DashboardServiceImpl) GetDashboardACLInfoList(ctx context.Context, query *dashboards.GetDashboardACLInfoListQuery) ([]*dashboards.DashboardACLInfoDTO, error) { + return dr.dashboardStore.GetDashboardACLInfoList(ctx, query) +} + func (dr *DashboardServiceImpl) GetDashboardTags(ctx context.Context, query *dashboards.GetDashboardTagsQuery) ([]*dashboards.DashboardTagCloudItem, error) { return dr.dashboardStore.GetDashboardTags(ctx, query) } +func (dr *DashboardServiceImpl) DeleteACLByUser(ctx context.Context, userID int64) error { + return dr.dashboardStore.DeleteACLByUser(ctx, userID) +} + func (dr DashboardServiceImpl) CountInFolder(ctx context.Context, orgID int64, folderUID string, u identity.Requester) (int64, error) { folder, err := dr.folderService.Get(ctx, &folder.GetFolderQuery{UID: &folderUID, OrgID: orgID, SignedInUser: u}) if err != nil { diff --git a/pkg/services/dashboards/service/dashboard_service_test.go b/pkg/services/dashboards/service/dashboard_service_test.go index da00e5355c8..5a907f3716e 100644 --- a/pkg/services/dashboards/service/dashboard_service_test.go +++ b/pkg/services/dashboards/service/dashboard_service_test.go @@ -219,6 +219,14 @@ func TestDashboardService(t *testing.T) { err := service.DeleteDashboard(context.Background(), 1, 1) require.NoError(t, err) }) + + // t.Run("Delete ACL by user", func(t *testing.T) { + // fakeStore := dashboards.FakeDashboardStore{} + // args := 1 + // fakeStore.On("DeleteACLByUser", mock.Anything, args).Return(nil).Once() + // err := service.DeleteACLByUser(context.Background(), 1) + // require.NoError(t, err) + // }) }) t.Run("Count dashboards in folder", func(t *testing.T) { @@ -240,4 +248,41 @@ func TestDashboardService(t *testing.T) { require.NoError(t, err) }) }) + + t.Run("Delete user by acl", func(t *testing.T) { + fakeStore := dashboards.FakeDashboardStore{} + fakeStore.On("DeleteACLByUser", mock.Anything, mock.AnythingOfType("int64")).Return(nil) + defer fakeStore.AssertExpectations(t) + + service := &DashboardServiceImpl{ + cfg: setting.NewCfg(), + log: log.New("test.logger"), + dashboardStore: &fakeStore, + dashAlertExtractor: &dummyDashAlertExtractor{}, + } + err := service.DeleteACLByUser(context.Background(), 1) + require.NoError(t, err) + }) + + t.Run("When org user is deleted", func(t *testing.T) { + fakeStore := dashboards.FakeDashboardStore{} + fakeStore.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(nil, nil) + t.Run("Should remove dependent permissions for deleted org user", func(t *testing.T) { + permQuery := &dashboards.GetDashboardACLInfoListQuery{DashboardID: 1, OrgID: 1} + + permQueryResult, err := fakeStore.GetDashboardACLInfoList(context.Background(), permQuery) + require.NoError(t, err) + + require.Equal(t, len(permQueryResult), 0) + }) + + t.Run("Should not remove dashboard permissions for same user in another org", func(t *testing.T) { + fakeStore := dashboards.FakeDashboardStore{} + fakeStore.On("GetDashboardACLInfoList", mock.Anything, mock.AnythingOfType("*dashboards.GetDashboardACLInfoListQuery")).Return(nil, nil) + permQuery := &dashboards.GetDashboardACLInfoListQuery{DashboardID: 2, OrgID: 3} + + _, err := fakeStore.GetDashboardACLInfoList(context.Background(), permQuery) + require.NoError(t, err) + }) + }) } diff --git a/pkg/services/dashboards/store_mock.go b/pkg/services/dashboards/store_mock.go index d4a696d6ae0..0e23d220237 100644 --- a/pkg/services/dashboards/store_mock.go +++ b/pkg/services/dashboards/store_mock.go @@ -66,6 +66,20 @@ func (_m *FakeDashboardStore) CountDashboardsInFolder(ctx context.Context, reque return r0, r1 } +// DeleteACLByUser provides a mock function with given fields: _a0, _a1 +func (_m *FakeDashboardStore) DeleteACLByUser(_a0 context.Context, _a1 int64) error { + ret := _m.Called(_a0, _a1) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // DeleteDashboard provides a mock function with given fields: ctx, cmd func (_m *FakeDashboardStore) DeleteDashboard(ctx context.Context, cmd *DeleteDashboardCommand) error { ret := _m.Called(ctx, cmd) @@ -160,6 +174,32 @@ func (_m *FakeDashboardStore) GetDashboard(ctx context.Context, query *GetDashbo return r0, r1 } +// GetDashboardACLInfoList provides a mock function with given fields: ctx, query +func (_m *FakeDashboardStore) GetDashboardACLInfoList(ctx context.Context, query *GetDashboardACLInfoListQuery) ([]*DashboardACLInfoDTO, error) { + ret := _m.Called(ctx, query) + + var r0 []*DashboardACLInfoDTO + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardACLInfoListQuery) ([]*DashboardACLInfoDTO, error)); ok { + return rf(ctx, query) + } + if rf, ok := ret.Get(0).(func(context.Context, *GetDashboardACLInfoListQuery) []*DashboardACLInfoDTO); ok { + r0 = rf(ctx, query) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*DashboardACLInfoDTO) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *GetDashboardACLInfoListQuery) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetDashboardTags provides a mock function with given fields: ctx, query func (_m *FakeDashboardStore) GetDashboardTags(ctx context.Context, query *GetDashboardTagsQuery) ([]*DashboardTagCloudItem, error) { ret := _m.Called(ctx, query) @@ -422,6 +462,20 @@ func (_m *FakeDashboardStore) UnprovisionDashboard(ctx context.Context, id int64 return r0 } +// UpdateDashboardACL provides a mock function with given fields: ctx, uid, items +func (_m *FakeDashboardStore) UpdateDashboardACL(ctx context.Context, uid int64, items []*DashboardACL) error { + ret := _m.Called(ctx, uid, items) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64, []*DashboardACL) error); ok { + r0 = rf(ctx, uid, items) + } else { + r0 = ret.Error(0) + } + + return r0 +} + // ValidateDashboardBeforeSave provides a mock function with given fields: ctx, dashboard, overwrite func (_m *FakeDashboardStore) ValidateDashboardBeforeSave(ctx context.Context, dashboard *Dashboard, overwrite bool) (bool, error) { ret := _m.Called(ctx, dashboard, overwrite) diff --git a/pkg/services/team/teamimpl/store_test.go b/pkg/services/team/teamimpl/store_test.go index 4a184c0e145..78d91645bd5 100644 --- a/pkg/services/team/teamimpl/store_test.go +++ b/pkg/services/team/teamimpl/store_test.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -350,6 +351,30 @@ func TestIntegrationTeamCommandsAndQueries(t *testing.T) { }) }) + t.Run("Should be able to remove a group with users and permissions", func(t *testing.T) { + groupID := team2.ID + err := teamSvc.AddTeamMember(userIds[1], testOrgID, groupID, false, 0) + require.NoError(t, err) + err = teamSvc.AddTeamMember(userIds[2], testOrgID, groupID, false, 0) + require.NoError(t, err) + err = updateDashboardACL(t, sqlStore, 1, &dashboards.DashboardACL{ + DashboardID: 1, OrgID: testOrgID, Permission: dashboards.PERMISSION_EDIT, TeamID: groupID, + }) + require.NoError(t, err) + err = teamSvc.DeleteTeam(context.Background(), &team.DeleteTeamCommand{OrgID: testOrgID, ID: groupID}) + require.NoError(t, err) + + query := &team.GetTeamByIDQuery{OrgID: testOrgID, ID: groupID} + _, err = teamSvc.GetTeamByID(context.Background(), query) + require.Equal(t, err, team.ErrTeamNotFound) + + permQuery := &dashboards.GetDashboardACLInfoListQuery{DashboardID: 1, OrgID: testOrgID} + permQueryResult, err := getDashboardACLInfoList(sqlStore, permQuery) + require.NoError(t, err) + + require.Equal(t, len(permQueryResult), 0) + }) + t.Run("Should not return hidden users in team member count", func(t *testing.T) { sqlStore = db.InitTestDB(t) setup() @@ -621,3 +646,123 @@ func hasWildcardScope(user identity.Requester, action string) bool { } return false } + +// TODO: Use FakeDashboardStore when org has its own service +func updateDashboardACL(t *testing.T, sqlStore *sqlstore.SQLStore, dashboardID int64, items ...*dashboards.DashboardACL) error { + t.Helper() + + err := sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { + _, err := sess.Exec("DELETE FROM dashboard_acl WHERE dashboard_id=?", dashboardID) + if err != nil { + return fmt.Errorf("deleting from dashboard_acl failed: %w", err) + } + + for _, item := range items { + item.Created = time.Now() + item.Updated = time.Now() + if item.UserID == 0 && item.TeamID == 0 && (item.Role == nil || !item.Role.IsValid()) { + return dashboards.ErrDashboardACLInfoMissing + } + + if item.DashboardID == 0 { + return dashboards.ErrDashboardPermissionDashboardEmpty + } + + sess.Nullable("user_id", "team_id") + if _, err := sess.Insert(item); err != nil { + return err + } + } + + // Update dashboard HasACL flag + dashboard := dashboards.Dashboard{HasACL: true} + _, err = sess.Cols("has_acl").Where("id=?", dashboardID).Update(&dashboard) + return err + }) + return err +} + +// This function was copied from pkg/services/dashboards/database to circumvent +// import cycles. When this org-related code is refactored into a service the +// tests can the real GetDashboardACLInfoList functions +func getDashboardACLInfoList(s *sqlstore.SQLStore, query *dashboards.GetDashboardACLInfoListQuery) ([]*dashboards.DashboardACLInfoDTO, error) { + queryResult := make([]*dashboards.DashboardACLInfoDTO, 0) + outerErr := s.WithDbSession(context.Background(), func(dbSession *db.Session) error { + falseStr := s.GetDialect().BooleanStr(false) + + if query.DashboardID == 0 { + sql := `SELECT + da.id, + da.org_id, + da.dashboard_id, + da.user_id, + da.team_id, + da.permission, + da.role, + da.created, + da.updated, + '' as user_login, + '' as user_email, + '' as team, + '' as title, + '' as slug, + '' as uid,` + + falseStr + ` AS is_folder,` + + falseStr + ` AS inherited + FROM dashboard_acl as da + WHERE da.dashboard_id = -1` + return dbSession.SQL(sql).Find(&queryResult) + } + + rawSQL := ` + -- get permissions for the dashboard and its parent folder + SELECT + da.id, + da.org_id, + da.dashboard_id, + da.user_id, + da.team_id, + da.permission, + da.role, + da.created, + da.updated, + u.login AS user_login, + u.email AS user_email, + ug.name AS team, + ug.email AS team_email, + d.title, + d.slug, + d.uid, + d.is_folder, + CASE WHEN (da.dashboard_id = -1 AND d.folder_id > 0) OR da.dashboard_id = d.folder_id THEN ` + s.GetDialect().BooleanStr(true) + ` ELSE ` + falseStr + ` END AS inherited + FROM dashboard as d + LEFT JOIN dashboard folder on folder.id = d.folder_id + LEFT JOIN dashboard_acl AS da ON + da.dashboard_id = d.id OR + da.dashboard_id = d.folder_id OR + ( + -- include default permissions --> + da.org_id = -1 AND ( + (folder.id IS NOT NULL AND folder.has_acl = ` + falseStr + `) OR + (folder.id IS NULL AND d.has_acl = ` + falseStr + `) + ) + ) + LEFT JOIN ` + s.GetDialect().Quote("user") + ` AS u ON u.id = da.user_id + LEFT JOIN team ug on ug.id = da.team_id + WHERE d.org_id = ? AND d.id = ? AND da.id IS NOT NULL + ORDER BY da.id ASC + ` + + return dbSession.SQL(rawSQL, query.OrgID, query.DashboardID).Find(&queryResult) + }) + + if outerErr != nil { + return nil, outerErr + } + + for _, p := range queryResult { + p.PermissionName = p.Permission.String() + } + + return queryResult, nil +} diff --git a/pkg/services/user/userimpl/store_test.go b/pkg/services/user/userimpl/store_test.go index 21a0cfbbee0..ae6ced18c31 100644 --- a/pkg/services/user/userimpl/store_test.go +++ b/pkg/services/user/userimpl/store_test.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/services/org" "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/quota/quotaimpl" @@ -377,6 +378,12 @@ func TestIntegrationUserDataAccess(t *testing.T) { }) require.Nil(t, err) + err = updateDashboardACL(t, ss, 1, &dashboards.DashboardACL{ + DashboardID: 1, OrgID: users[0].OrgID, UserID: users[1].ID, + Permission: dashboards.PERMISSION_EDIT, + }) + require.Nil(t, err) + ss.CacheService.Flush() query := &user.GetSignedInUserQuery{OrgID: users[1].OrgID, UserID: users[1].ID} @@ -519,10 +526,22 @@ func TestIntegrationUserDataAccess(t *testing.T) { }) require.Nil(t, err) + err = updateDashboardACL(t, ss, 1, &dashboards.DashboardACL{ + DashboardID: 1, OrgID: users[0].OrgID, UserID: users[1].ID, + Permission: dashboards.PERMISSION_EDIT, + }) + require.Nil(t, err) + // When the user is deleted err = userStore.Delete(context.Background(), users[1].ID) require.Nil(t, err) + permQuery := &dashboards.GetDashboardACLInfoListQuery{DashboardID: 1, OrgID: users[0].OrgID} + permQueryResult, err := userStore.getDashboardACLInfoList(permQuery) + require.Nil(t, err) + + require.Len(t, permQueryResult, 0) + // A user is an org member and has been assigned permissions // Re-init DB ss = db.InitTestDB(t) @@ -541,6 +560,12 @@ func TestIntegrationUserDataAccess(t *testing.T) { }) require.Nil(t, err) + err = updateDashboardACL(t, ss, 1, &dashboards.DashboardACL{ + DashboardID: 1, OrgID: users[0].OrgID, UserID: users[1].ID, + Permission: dashboards.PERMISSION_EDIT, + }) + require.Nil(t, err) + ss.CacheService.Flush() query3 := &user.GetSignedInUserQuery{OrgID: users[1].OrgID, UserID: users[1].ID} @@ -566,6 +591,12 @@ func TestIntegrationUserDataAccess(t *testing.T) { // the user is deleted err = userStore.Delete(context.Background(), users[1].ID) require.Nil(t, err) + + permQuery = &dashboards.GetDashboardACLInfoListQuery{DashboardID: 1, OrgID: users[0].OrgID} + permQueryResult, err = userStore.getDashboardACLInfoList(permQuery) + require.Nil(t, err) + + require.Len(t, permQueryResult, 0) }) t.Run("Testing DB - return list of users that the SignedInUser has permission to read", func(t *testing.T) { @@ -916,6 +947,41 @@ func createFiveTestUsers(t *testing.T, svc user.Service, fn func(i int) *user.Cr return users } +// TODO: Use FakeDashboardStore when org has its own service +func updateDashboardACL(t *testing.T, sqlStore db.DB, dashboardID int64, items ...*dashboards.DashboardACL) error { + t.Helper() + + err := sqlStore.WithDbSession(context.Background(), func(sess *db.Session) error { + _, err := sess.Exec("DELETE FROM dashboard_acl WHERE dashboard_id=?", dashboardID) + if err != nil { + return fmt.Errorf("deleting from dashboard_acl failed: %w", err) + } + + for _, item := range items { + item.Created = time.Now() + item.Updated = time.Now() + if item.UserID == 0 && item.TeamID == 0 && (item.Role == nil || !item.Role.IsValid()) { + return dashboards.ErrDashboardACLInfoMissing + } + + if item.DashboardID == 0 { + return dashboards.ErrDashboardPermissionDashboardEmpty + } + + sess.Nullable("user_id", "team_id") + if _, err := sess.Insert(item); err != nil { + return err + } + } + + // Update dashboard HasACL flag + dashboard := dashboards.Dashboard{HasACL: true} + _, err = sess.Cols("has_acl").Where("id=?", dashboardID).Update(&dashboard) + return err + }) + return err +} + func TestMetricsUsage(t *testing.T) { ss := db.InitTestDB(t) userStore := ProvideStore(ss, setting.NewCfg()) @@ -963,6 +1029,91 @@ func TestMetricsUsage(t *testing.T) { }) } +// This function was copied from pkg/services/dashboards/database to circumvent +// import cycles. When this org-related code is refactored into a service the +// tests can the real GetDashboardACLInfoList functions +func (ss *sqlStore) getDashboardACLInfoList(query *dashboards.GetDashboardACLInfoListQuery) ([]*dashboards.DashboardACLInfoDTO, error) { + queryResult := make([]*dashboards.DashboardACLInfoDTO, 0) + outerErr := ss.db.WithDbSession(context.Background(), func(dbSession *db.Session) error { + falseStr := ss.dialect.BooleanStr(false) + + if query.DashboardID == 0 { + sql := `SELECT + da.id, + da.org_id, + da.dashboard_id, + da.user_id, + da.team_id, + da.permission, + da.role, + da.created, + da.updated, + '' as user_login, + '' as user_email, + '' as team, + '' as title, + '' as slug, + '' as uid,` + + falseStr + ` AS is_folder,` + + falseStr + ` AS inherited + FROM dashboard_acl as da + WHERE da.dashboard_id = -1` + return dbSession.SQL(sql).Find(&queryResult) + } + + rawSQL := ` + -- get permissions for the dashboard and its parent folder + SELECT + da.id, + da.org_id, + da.dashboard_id, + da.user_id, + da.team_id, + da.permission, + da.role, + da.created, + da.updated, + u.login AS user_login, + u.email AS user_email, + ug.name AS team, + ug.email AS team_email, + d.title, + d.slug, + d.uid, + d.is_folder, + CASE WHEN (da.dashboard_id = -1 AND d.folder_id > 0) OR da.dashboard_id = d.folder_id THEN ` + ss.dialect.BooleanStr(true) + ` ELSE ` + falseStr + ` END AS inherited + FROM dashboard as d + LEFT JOIN dashboard folder on folder.id = d.folder_id + LEFT JOIN dashboard_acl AS da ON + da.dashboard_id = d.id OR + da.dashboard_id = d.folder_id OR + ( + -- include default permissions --> + da.org_id = -1 AND ( + (folder.id IS NOT NULL AND folder.has_acl = ` + falseStr + `) OR + (folder.id IS NULL AND d.has_acl = ` + falseStr + `) + ) + ) + LEFT JOIN ` + ss.dialect.Quote("user") + ` AS u ON u.id = da.user_id + LEFT JOIN team ug on ug.id = da.team_id + WHERE d.org_id = ? AND d.id = ? AND da.id IS NOT NULL + ORDER BY da.id ASC + ` + + return dbSession.SQL(rawSQL, query.OrgID, query.DashboardID).Find(&queryResult) + }) + + if outerErr != nil { + return nil, outerErr + } + + for _, p := range queryResult { + p.PermissionName = p.Permission.String() + } + + return queryResult, nil +} + func createOrgAndUserSvc(t *testing.T, store db.DB, cfg *setting.Cfg) (org.Service, user.Service) { t.Helper()