diff --git a/pkg/api/api.go b/pkg/api/api.go index 8fd4bb4cec9..c0bf807941d 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -54,7 +54,7 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/datasources/", authorize(reqOrgAdmin, dataSourcesConfigurationAccessEvaluator), hs.Index) r.Get("/datasources/new", authorize(reqOrgAdmin, dataSourcesNewAccessEvaluator), hs.Index) r.Get("/datasources/edit/*", authorize(reqOrgAdmin, dataSourcesEditAccessEvaluator), hs.Index) - r.Get("/org/users", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead, ac.ScopeUsersAll)), hs.Index) + r.Get("/org/users", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead)), hs.Index) r.Get("/org/users/new", reqOrgAdmin, hs.Index) r.Get("/org/users/invite", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionUsersCreate)), hs.Index) r.Get("/org/teams", reqCanAccessTeams, hs.Index) @@ -206,8 +206,8 @@ func (hs *HTTPServer) registerRoutes() { userIDScope := ac.Scope("users", "id", ac.Parameter(":userId")) orgRoute.Put("/", authorize(reqOrgAdmin, ac.EvalPermission(ActionOrgsWrite)), routing.Wrap(UpdateCurrentOrg)) orgRoute.Put("/address", authorize(reqOrgAdmin, ac.EvalPermission(ActionOrgsWrite)), routing.Wrap(UpdateCurrentOrgAddress)) - orgRoute.Get("/users", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead, ac.ScopeUsersAll)), routing.Wrap(hs.GetOrgUsersForCurrentOrg)) - orgRoute.Get("/users/search", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead, ac.ScopeUsersAll)), routing.Wrap(hs.SearchOrgUsersWithPaging)) + orgRoute.Get("/users", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead)), routing.Wrap(hs.GetOrgUsersForCurrentOrg)) + orgRoute.Get("/users/search", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead)), routing.Wrap(hs.SearchOrgUsersWithPaging)) orgRoute.Post("/users", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersAdd, ac.ScopeUsersAll)), quota("user"), routing.Wrap(hs.AddOrgUserToCurrentOrg)) orgRoute.Patch("/users/:userId", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRoleUpdate, userIDScope)), routing.Wrap(hs.UpdateOrgUserForCurrentOrg)) orgRoute.Delete("/users/:userId", authorize(reqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRemove, userIDScope)), routing.Wrap(hs.RemoveOrgUserForCurrentOrg)) @@ -224,7 +224,7 @@ func (hs *HTTPServer) registerRoutes() { // current org without requirement of user to be org admin apiRoute.Group("/org", func(orgRoute routing.RouteRegister) { - orgRoute.Get("/users/lookup", authorize(reqOrgAdminFolderAdminOrTeamAdmin, ac.EvalPermission(ac.ActionOrgUsersRead, ac.ScopeUsersAll)), routing.Wrap(hs.GetOrgUsersForCurrentOrgLookup)) + orgRoute.Get("/users/lookup", authorize(reqOrgAdminFolderAdminOrTeamAdmin, ac.EvalPermission(ac.ActionOrgUsersRead)), routing.Wrap(hs.GetOrgUsersForCurrentOrgLookup)) }) // create new org diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index eb6f4d6b16b..fcc35d823b1 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -22,6 +22,7 @@ import ( "github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/accesscontrol" + acmiddleware "github.com/grafana/grafana/pkg/services/accesscontrol/middleware" accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" "github.com/grafana/grafana/pkg/services/auth" @@ -355,6 +356,8 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont c.Map(initCtx) }) + m.Use(acmiddleware.LoadPermissionsMiddleware(hs.AccessControl)) + // Register all routes hs.registerRoutes() hs.RouteRegister.Register(m.Router) diff --git a/pkg/api/index.go b/pkg/api/index.go index 9de80ede62b..bf23ec86abc 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -251,7 +251,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto }) } - if hasAccess(ac.ReqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead, ac.ScopeUsersAll)) { + if hasAccess(ac.ReqOrgAdmin, ac.EvalPermission(ac.ActionOrgUsersRead)) { configNodes = append(configNodes, &dtos.NavLink{ Text: "Users", Id: "users", diff --git a/pkg/api/org_users.go b/pkg/api/org_users.go index 7cd32b7bba5..275528f5619 100644 --- a/pkg/api/org_users.go +++ b/pkg/api/org_users.go @@ -71,6 +71,7 @@ func (hs *HTTPServer) GetOrgUsersForCurrentOrg(c *models.ReqContext) response.Re OrgId: c.OrgId, Query: c.Query("query"), Limit: c.QueryInt("limit"), + User: c.SignedInUser, }, c.SignedInUser) if err != nil { @@ -86,6 +87,7 @@ func (hs *HTTPServer) GetOrgUsersForCurrentOrgLookup(c *models.ReqContext) respo OrgId: c.OrgId, Query: c.Query("query"), Limit: c.QueryInt("limit"), + User: c.SignedInUser, }, c.SignedInUser) if err != nil { @@ -124,6 +126,7 @@ func (hs *HTTPServer) GetOrgUsers(c *models.ReqContext) response.Response { OrgId: c.ParamsInt64(":orgId"), Query: "", Limit: 0, + User: c.SignedInUser, }, c.SignedInUser) if err != nil { @@ -183,8 +186,9 @@ func (hs *HTTPServer) SearchOrgUsersWithPaging(c *models.ReqContext) response.Re query := &models.SearchOrgUsersQuery{ OrgID: c.OrgId, Query: c.Query("query"), - Limit: perPage, Page: page, + Limit: perPage, + User: c.SignedInUser, } if err := hs.SQLStore.SearchOrgUsers(ctx, query); err != nil { diff --git a/pkg/api/org_users_test.go b/pkg/api/org_users_test.go index af135129a78..0299b177512 100644 --- a/pkg/api/org_users_test.go +++ b/pkg/api/org_users_test.go @@ -38,6 +38,7 @@ func TestOrgUsersAPIEndpoint_userLoggedIn(t *testing.T) { hs := &HTTPServer{Cfg: settings} sqlStore := sqlstore.InitTestDB(t) + sqlStore.Cfg = settings hs.SQLStore = sqlStore loggedInUserScenario(t, "When calling GET on", "api/org/users", func(sc *scenarioContext) { @@ -556,7 +557,10 @@ func TestPostOrgUsersAPIEndpoint_AccessControl(t *testing.T) { require.NoError(t, err) assert.EqualValuesf(t, tc.expectedMessage, message, "server did not answer expected message") - getUsersQuery := models.GetOrgUsersQuery{OrgId: tc.targetOrg} + getUsersQuery := models.GetOrgUsersQuery{OrgId: tc.targetOrg, User: &models.SignedInUser{ + OrgId: tc.targetOrg, + Permissions: map[int64]map[string][]string{tc.targetOrg: {"org.users:read": {"users:*"}}}, + }} err = sc.db.GetOrgUsers(context.Background(), &getUsersQuery) require.NoError(t, err) assert.Len(t, getUsersQuery.Result, tc.expectedUserCount) @@ -799,7 +803,13 @@ func TestDeleteOrgUsersAPIEndpoint_AccessControl(t *testing.T) { require.NoError(t, err) assert.Equal(t, tc.expectedMessage, message) - getUsersQuery := models.GetOrgUsersQuery{OrgId: tc.targetOrg} + getUsersQuery := models.GetOrgUsersQuery{ + OrgId: tc.targetOrg, + User: &models.SignedInUser{ + OrgId: tc.targetOrg, + Permissions: map[int64]map[string][]string{tc.targetOrg: {accesscontrol.ActionOrgUsersRead: {accesscontrol.ScopeUsersAll}}}, + }, + } err = sc.db.GetOrgUsers(context.Background(), &getUsersQuery) require.NoError(t, err) assert.Len(t, getUsersQuery.Result, tc.expectedUserCount) diff --git a/pkg/models/org_user.go b/pkg/models/org_user.go index 5498536de89..f03f5741ab9 100644 --- a/pkg/models/org_user.go +++ b/pkg/models/org_user.go @@ -112,6 +112,7 @@ type GetOrgUsersQuery struct { Limit int IsServiceAccount bool + User *SignedInUser Result []*OrgUserDTO } @@ -122,6 +123,7 @@ type SearchOrgUsersQuery struct { Limit int IsServiceAccount bool + User *SignedInUser Result SearchOrgUsersQueryResult } diff --git a/pkg/services/accesscontrol/filter.go b/pkg/services/accesscontrol/filter.go index 298206a4bec..97073be1472 100644 --- a/pkg/services/accesscontrol/filter.go +++ b/pkg/services/accesscontrol/filter.go @@ -10,7 +10,9 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore/migrator" ) -var sqlIDAcceptList = map[string]struct{}{} +var sqlIDAcceptList = map[string]struct{}{ + "org_user.user_id": {}, +} type SQLDialect interface { DriverName() string @@ -97,3 +99,12 @@ func postgresQuery(scopes []string, sqlID, prefix string) (string, []interface{} ) `, sqlID, sqlID), args } + +// SetAcceptListForTest allow us to mutate the list for blackbox testing +func SetAcceptListForTest(list map[string]struct{}) func() { + original := sqlIDAcceptList + sqlIDAcceptList = list + return func() { + sqlIDAcceptList = original + } +} diff --git a/pkg/services/accesscontrol/filter_bench_test.go b/pkg/services/accesscontrol/filter_bench_test.go index 26602e6043e..6bb63dcd0b0 100644 --- a/pkg/services/accesscontrol/filter_bench_test.go +++ b/pkg/services/accesscontrol/filter_bench_test.go @@ -1,4 +1,4 @@ -package accesscontrol +package accesscontrol_test import ( "context" @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/sqlstore" ) @@ -21,19 +22,20 @@ func benchmarkFilter(b *testing.B, numDs, numPermissions int) { b.ResetTimer() // set sqlIDAcceptList before running tests - sqlIDAcceptList = map[string]struct{}{ + restore := accesscontrol.SetAcceptListForTest(map[string]struct{}{ "data_source.id": {}, - } + }) + defer restore() for i := 0; i < b.N; i++ { baseSql := `SELECT data_source.* FROM data_source WHERE` - query, args, err := Filter( + query, args, err := accesscontrol.Filter( context.Background(), &FakeDriver{name: "sqlite3"}, "data_source.id", "datasources", "datasources:read", - &models.SignedInUser{OrgId: 1, Permissions: map[int64]map[string][]string{1: GroupScopesByAction(permissions)}}, + &models.SignedInUser{OrgId: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(permissions)}}, ) require.NoError(b, err) @@ -46,7 +48,7 @@ func benchmarkFilter(b *testing.B, numDs, numPermissions int) { } } -func setupFilterBenchmark(b *testing.B, numDs, numPermissions int) (*sqlstore.SQLStore, []*Permission) { +func setupFilterBenchmark(b *testing.B, numDs, numPermissions int) (*sqlstore.SQLStore, []*accesscontrol.Permission) { b.Helper() store := sqlstore.InitTestDB(b) @@ -62,11 +64,11 @@ func setupFilterBenchmark(b *testing.B, numDs, numPermissions int) (*sqlstore.SQ numPermissions = numDs } - permissions := make([]*Permission, 0, numPermissions) + permissions := make([]*accesscontrol.Permission, 0, numPermissions) for i := 1; i <= numPermissions; i++ { - permissions = append(permissions, &Permission{ + permissions = append(permissions, &accesscontrol.Permission{ Action: "datasources:read", - Scope: Scope("datasources", "id", strconv.Itoa(i)), + Scope: accesscontrol.Scope("datasources", "id", strconv.Itoa(i)), }) } diff --git a/pkg/services/accesscontrol/filter_test.go b/pkg/services/accesscontrol/filter_test.go index d0125c56f61..a081a91371f 100644 --- a/pkg/services/accesscontrol/filter_test.go +++ b/pkg/services/accesscontrol/filter_test.go @@ -1,4 +1,4 @@ -package accesscontrol +package accesscontrol_test import ( "context" @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/require" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/sqlstore" ) @@ -18,7 +19,7 @@ type filterTest struct { sqlID string action string prefix string - permissions []*Permission + permissions []*accesscontrol.Permission expectedQuery string expectedArgs []interface{} } @@ -31,7 +32,7 @@ func TestFilter(t *testing.T) { sqlID: "data_source.id", prefix: "datasources", action: "datasources:query", - permissions: []*Permission{ + permissions: []*accesscontrol.Permission{ {Action: "datasources:query", Scope: "datasources:id:1"}, {Action: "datasources:query", Scope: "datasources:id:2"}, {Action: "datasources:query", Scope: "datasources:id:3"}, @@ -65,7 +66,7 @@ func TestFilter(t *testing.T) { sqlID: "dashboard.id", prefix: "dashboards", action: "dashboards:read", - permissions: []*Permission{ + permissions: []*accesscontrol.Permission{ {Action: "dashboards:read", Scope: "dashboards:id:1"}, {Action: "dashboards:read", Scope: "dashboards:id:2"}, {Action: "dashboards:read", Scope: "dashboards:id:5"}, @@ -95,7 +96,7 @@ func TestFilter(t *testing.T) { sqlID: "user.id", prefix: "users", action: "users:read", - permissions: []*Permission{ + permissions: []*accesscontrol.Permission{ {Action: "users:read", Scope: "users:id:1"}, {Action: "users:read", Scope: "users:id:100"}, // Other permissions @@ -123,21 +124,22 @@ func TestFilter(t *testing.T) { } // set sqlIDAcceptList before running tests - sqlIDAcceptList = map[string]struct{}{ + restore := accesscontrol.SetAcceptListForTest(map[string]struct{}{ "user.id": {}, "dashboard.id": {}, "data_source.id": {}, - } + }) + defer restore() for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { - query, args, err := Filter( + query, args, err := accesscontrol.Filter( context.Background(), FakeDriver{name: tt.driverName}, tt.sqlID, tt.prefix, tt.action, - &models.SignedInUser{OrgId: 1, Permissions: map[int64]map[string][]string{1: GroupScopesByAction(tt.permissions)}}, + &models.SignedInUser{OrgId: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}}, ) require.NoError(t, err) assert.Equal(t, tt.expectedQuery, query) @@ -153,7 +155,7 @@ func TestFilter(t *testing.T) { type filterDatasourcesTestCase struct { desc string sqlID string - permissions []*Permission + permissions []*accesscontrol.Permission expectedDataSources []string expectErr bool } @@ -163,7 +165,7 @@ func TestFilter_Datasources(t *testing.T) { { desc: "expect all data sources to be returned", sqlID: "data_source.id", - permissions: []*Permission{ + permissions: []*accesscontrol.Permission{ {Action: "datasources:read", Scope: "datasources:*"}, }, expectedDataSources: []string{"ds:1", "ds:2", "ds:3", "ds:4", "ds:5", "ds:6", "ds:7", "ds:8", "ds:9", "ds:10"}, @@ -171,13 +173,13 @@ func TestFilter_Datasources(t *testing.T) { { desc: "expect no data sources to be returned", sqlID: "data_source.id", - permissions: []*Permission{}, + permissions: []*accesscontrol.Permission{}, expectedDataSources: []string{}, }, { desc: "expect data sources with id 3, 7 and 8 to be returned", sqlID: "data_source.id", - permissions: []*Permission{ + permissions: []*accesscontrol.Permission{ {Action: "datasources:read", Scope: "datasources:id:3"}, {Action: "datasources:read", Scope: "datasources:id:7"}, {Action: "datasources:read", Scope: "datasources:id:8"}, @@ -187,7 +189,7 @@ func TestFilter_Datasources(t *testing.T) { { desc: "expect error if sqlID is not in the accept list", sqlID: "other.id", - permissions: []*Permission{ + permissions: []*accesscontrol.Permission{ {Action: "datasources:read", Scope: "datasources:id:3"}, {Action: "datasources:read", Scope: "datasources:id:7"}, {Action: "datasources:read", Scope: "datasources:id:8"}, @@ -198,9 +200,10 @@ func TestFilter_Datasources(t *testing.T) { } // set sqlIDAcceptList before running tests - sqlIDAcceptList = map[string]struct{}{ + restore := accesscontrol.SetAcceptListForTest(map[string]struct{}{ "data_source.id": {}, - } + }) + defer restore() for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { @@ -216,13 +219,13 @@ func TestFilter_Datasources(t *testing.T) { } baseSql := `SELECT data_source.* FROM data_source WHERE` - query, args, err := Filter( + query, args, err := accesscontrol.Filter( context.Background(), &FakeDriver{name: "sqlite3"}, tt.sqlID, "datasources", "datasources:read", - &models.SignedInUser{OrgId: 1, Permissions: map[int64]map[string][]string{1: GroupScopesByAction(tt.permissions)}}, + &models.SignedInUser{OrgId: 1, Permissions: map[int64]map[string][]string{1: accesscontrol.GroupScopesByAction(tt.permissions)}}, ) if !tt.expectErr { diff --git a/pkg/services/sqlstore/org_users.go b/pkg/services/sqlstore/org_users.go index 898ace7dff1..6040a1f0634 100644 --- a/pkg/services/sqlstore/org_users.go +++ b/pkg/services/sqlstore/org_users.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/util" ) @@ -111,6 +112,15 @@ func (ss *SQLStore) GetOrgUsers(ctx context.Context, query *models.GetOrgUsersQu // service accounts table in the modelling whereConditions = append(whereConditions, fmt.Sprintf("%s.is_service_account = %t", x.Dialect().Quote("user"), query.IsServiceAccount)) + if ss.Cfg.FeatureToggles["accesscontrol"] { + q, args, err := accesscontrol.Filter(ctx, ss.Dialect, "org_user.user_id", "users", "org.users:read", query.User) + if err != nil { + return err + } + whereConditions = append(whereConditions, q) + whereParams = append(whereParams, args...) + } + if query.Query != "" { queryWithWildcards := "%" + query.Query + "%" whereConditions = append(whereConditions, "(email "+dialect.LikeStr()+" ? OR name "+dialect.LikeStr()+" ? OR login "+dialect.LikeStr()+" ?)") @@ -165,6 +175,15 @@ func (ss *SQLStore) SearchOrgUsers(ctx context.Context, query *models.SearchOrgU // service accounts table in the modelling whereConditions = append(whereConditions, fmt.Sprintf("%s.is_service_account = %t", x.Dialect().Quote("user"), query.IsServiceAccount)) + if ss.Cfg.FeatureToggles["accesscontrol"] { + q, args, err := accesscontrol.Filter(ctx, ss.Dialect, "org_user.user_id", "users", "org.users:read", query.User) + if err != nil { + return err + } + whereConditions = append(whereConditions, q) + whereParams = append(whereParams, args...) + } + if query.Query != "" { queryWithWildcards := "%" + query.Query + "%" whereConditions = append(whereConditions, "(email "+dialect.LikeStr()+" ? OR name "+dialect.LikeStr()+" ? OR login "+dialect.LikeStr()+" ?)") diff --git a/pkg/services/sqlstore/org_users_test.go b/pkg/services/sqlstore/org_users_test.go new file mode 100644 index 00000000000..216ce124bf7 --- /dev/null +++ b/pkg/services/sqlstore/org_users_test.go @@ -0,0 +1,176 @@ +package sqlstore + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/models" + ac "github.com/grafana/grafana/pkg/services/accesscontrol" +) + +type getOrgUsersTestCase struct { + desc string + query *models.GetOrgUsersQuery + expectedNumUsers int +} + +func TestSQLStore_GetOrgUsers(t *testing.T) { + tests := []getOrgUsersTestCase{ + { + desc: "should return all users", + query: &models.GetOrgUsersQuery{ + OrgId: 1, + User: &models.SignedInUser{ + OrgId: 1, + Permissions: map[int64]map[string][]string{1: {ac.ActionOrgUsersRead: {ac.ScopeUsersAll}}}, + }, + }, + expectedNumUsers: 10, + }, + { + desc: "should return no users", + query: &models.GetOrgUsersQuery{ + OrgId: 1, + User: &models.SignedInUser{ + OrgId: 1, + Permissions: map[int64]map[string][]string{1: {ac.ActionOrgUsersRead: {""}}}, + }, + }, + expectedNumUsers: 0, + }, + { + desc: "should return some users", + query: &models.GetOrgUsersQuery{ + OrgId: 1, + User: &models.SignedInUser{ + OrgId: 1, + Permissions: map[int64]map[string][]string{1: {ac.ActionOrgUsersRead: { + "users:id:1", + "users:id:5", + "users:id:9", + }}}, + }, + }, + expectedNumUsers: 3, + }, + } + + store := InitTestDB(t) + store.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true} + seedOrgUsers(t, store, 10) + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + err := store.GetOrgUsers(context.Background(), tt.query) + require.NoError(t, err) + require.Len(t, tt.query.Result, tt.expectedNumUsers) + + if !hasWildcardScope(tt.query.User, ac.ActionOrgUsersRead) { + for _, u := range tt.query.Result { + assert.Contains(t, tt.query.User.Permissions[tt.query.User.OrgId][ac.ActionOrgUsersRead], fmt.Sprintf("users:id:%d", u.UserId)) + } + } + }) + } +} + +type searchOrgUsersTestCase struct { + desc string + query *models.SearchOrgUsersQuery + expectedNumUsers int +} + +func TestSQLStore_SearchOrgUsers(t *testing.T) { + tests := []searchOrgUsersTestCase{ + { + desc: "should return all users", + query: &models.SearchOrgUsersQuery{ + OrgID: 1, + User: &models.SignedInUser{ + OrgId: 1, + Permissions: map[int64]map[string][]string{1: {ac.ActionOrgUsersRead: {ac.ScopeUsersAll}}}, + }, + }, + expectedNumUsers: 10, + }, + { + desc: "should return no users", + query: &models.SearchOrgUsersQuery{ + OrgID: 1, + User: &models.SignedInUser{ + OrgId: 1, + Permissions: map[int64]map[string][]string{1: {ac.ActionOrgUsersRead: {""}}}, + }, + }, + expectedNumUsers: 0, + }, + { + desc: "should return some users", + query: &models.SearchOrgUsersQuery{ + OrgID: 1, + User: &models.SignedInUser{ + OrgId: 1, + Permissions: map[int64]map[string][]string{1: {ac.ActionOrgUsersRead: { + "users:id:1", + "users:id:5", + "users:id:9", + }}}, + }, + }, + expectedNumUsers: 3, + }, + } + + store := InitTestDB(t) + store.Cfg.FeatureToggles = map[string]bool{"accesscontrol": true} + seedOrgUsers(t, store, 10) + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + err := store.SearchOrgUsers(context.Background(), tt.query) + require.NoError(t, err) + assert.Len(t, tt.query.Result.OrgUsers, tt.expectedNumUsers) + + if !hasWildcardScope(tt.query.User, ac.ActionOrgUsersRead) { + for _, u := range tt.query.Result.OrgUsers { + assert.Contains(t, tt.query.User.Permissions[tt.query.User.OrgId][ac.ActionOrgUsersRead], fmt.Sprintf("users:id:%d", u.UserId)) + } + } + }) + } +} + +func seedOrgUsers(t *testing.T, store *SQLStore, numUsers int) { + t.Helper() + // Seed users + for i := 1; i <= numUsers; i++ { + user, err := store.CreateUser(context.Background(), models.CreateUserCommand{ + Login: fmt.Sprintf("user-%d", i), + OrgId: 1, + }) + require.NoError(t, err) + + if i != 1 { + err = store.AddOrgUser(context.Background(), &models.AddOrgUserCommand{ + Role: "Viewer", + OrgId: 1, + UserId: user.Id, + }) + require.NoError(t, err) + } + } +} + +func hasWildcardScope(user *models.SignedInUser, action string) bool { + for _, scope := range user.Permissions[user.OrgId][action] { + if strings.HasSuffix(scope, ":*") { + return true + } + } + return false +}