diff --git a/pkg/server/wireexts_oss.go b/pkg/server/wireexts_oss.go index b970d762951..35e23520c94 100644 --- a/pkg/server/wireexts_oss.go +++ b/pkg/server/wireexts_oss.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/server/backgroundsvcs" "github.com/grafana/grafana/pkg/services/accesscontrol" + acdb "github.com/grafana/grafana/pkg/services/accesscontrol/database" "github.com/grafana/grafana/pkg/services/accesscontrol/ossaccesscontrol" "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/datasources" @@ -58,6 +59,9 @@ var wireExtsBasicSet = wire.NewSet( wire.Bind(new(searchusers.Service), new(*searchusers.OSSService)), signature.ProvideService, wire.Bind(new(plugins.PluginLoaderAuthorizer), new(*signature.UnsignedPluginAuthorizer)), + acdb.ProvideService, + wire.Bind(new(accesscontrol.ResourceStore), new(*acdb.AccessControlStore)), + wire.Bind(new(accesscontrol.PermissionsProvider), new(*acdb.AccessControlStore)), ) var wireExtsSet = wire.NewSet( diff --git a/pkg/services/accesscontrol/accesscontrol.go b/pkg/services/accesscontrol/accesscontrol.go index cd252406bfd..c0c0575891c 100644 --- a/pkg/services/accesscontrol/accesscontrol.go +++ b/pkg/services/accesscontrol/accesscontrol.go @@ -25,6 +25,23 @@ type AccessControl interface { DeclareFixedRoles(...RoleRegistration) error } +type PermissionsProvider interface { + GetUserPermissions(ctx context.Context, query GetUserPermissionsQuery) ([]*Permission, error) +} + +type ResourceStore interface { + // SetUserResourcePermissions sets permissions for managed user role on a resource + SetUserResourcePermissions(ctx context.Context, orgID, userID int64, cmd SetResourcePermissionsCommand) ([]ResourcePermission, error) + // SetTeamResourcePermissions sets permissions for managed team role on a resource + SetTeamResourcePermissions(ctx context.Context, orgID, teamID int64, cmd SetResourcePermissionsCommand) ([]ResourcePermission, error) + // SetBuiltinResourcePermissions sets permissions for managed builtin role on a resource + SetBuiltinResourcePermissions(ctx context.Context, orgID int64, builtinRole string, cmd SetResourcePermissionsCommand) ([]ResourcePermission, error) + // RemoveResourcePermission remove permission for resource + RemoveResourcePermission(ctx context.Context, orgID int64, cmd RemoveResourcePermissionCommand) error + // GetResourcesPermissions will return all permission for all supplied resource ids + GetResourcesPermissions(ctx context.Context, orgID int64, query GetResourcesPermissionsQuery) ([]ResourcePermission, error) +} + func HasAccess(ac AccessControl, c *models.ReqContext) func(fallback func(*models.ReqContext) bool, evaluator Evaluator) bool { return func(fallback func(*models.ReqContext) bool, evaluator Evaluator) bool { if ac.IsDisabled() { diff --git a/pkg/services/accesscontrol/database/database.go b/pkg/services/accesscontrol/database/database.go new file mode 100644 index 00000000000..03ef65993ba --- /dev/null +++ b/pkg/services/accesscontrol/database/database.go @@ -0,0 +1,101 @@ +package database + +import ( + "context" + "strings" + + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/sqlstore" +) + +const ( + globalOrgID = 0 +) + +func ProvideService(sqlStore *sqlstore.SQLStore) *AccessControlStore { + return &AccessControlStore{sqlStore} +} + +type AccessControlStore struct { + sql *sqlstore.SQLStore +} + +func (s *AccessControlStore) GetUserPermissions(ctx context.Context, query accesscontrol.GetUserPermissionsQuery) ([]*accesscontrol.Permission, error) { + result := make([]*accesscontrol.Permission, 0) + err := s.sql.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + filter, params := userRolesFilter(query.OrgID, query.UserID, query.Roles) + + // TODO: optimize this + q := `SELECT + permission.id, + permission.role_id, + permission.action, + permission.scope, + permission.updated, + permission.created + FROM permission + INNER JOIN role ON role.id = permission.role_id + ` + filter + + if err := sess.SQL(q, params...).Find(&result); err != nil { + return err + } + + return nil + }) + + return result, err +} + +func userRolesFilter(orgID, userID int64, roles []string) (string, []interface{}) { + q := ` + WHERE role.id IN ( + SELECT ur.role_id + FROM user_role AS ur + WHERE ur.user_id = ? + AND (ur.org_id = ? OR ur.org_id = ?) + UNION + SELECT tr.role_id FROM team_role as tr + INNER JOIN team_member as tm ON tm.team_id = tr.team_id + WHERE tm.user_id = ? AND tr.org_id = ? + ` + params := []interface{}{userID, orgID, globalOrgID, userID, orgID} + + if len(roles) != 0 { + q += ` + UNION + SELECT br.role_id FROM builtin_role AS br + WHERE role IN (? ` + strings.Repeat(", ?", len(roles)-1) + `) + ` + for _, role := range roles { + params = append(params, role) + } + + q += `AND (br.org_id = ? OR br.org_id = ?)` + params = append(params, orgID, globalOrgID) + } + + q += `)` + + return q, params +} + +func deletePermissions(sess *sqlstore.DBSession, ids []int64) error { + if len(ids) == 0 { + return nil + } + + rawSQL := "DELETE FROM permission WHERE id IN(?" + strings.Repeat(",?", len(ids)-1) + ")" + args := make([]interface{}, 0, len(ids)+1) + args = append(args, rawSQL) + for _, id := range ids { + args = append(args, id) + } + + _, err := sess.Exec(args...) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/services/accesscontrol/database/database_test.go b/pkg/services/accesscontrol/database/database_test.go new file mode 100644 index 00000000000..91532f26e78 --- /dev/null +++ b/pkg/services/accesscontrol/database/database_test.go @@ -0,0 +1,132 @@ +package database + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/sqlstore" +) + +type getUserPermissionsTestCase struct { + desc string + orgID int64 + role string + userPermissions []string + teamPermissions []string + builtinPermissions []string + expected int +} + +func TestAccessControlStore_GetUserPermissions(t *testing.T) { + tests := []getUserPermissionsTestCase{ + { + desc: "should successfully get user, team and builtin permissions", + orgID: 1, + role: "Admin", + userPermissions: []string{"1", "2", "10"}, + teamPermissions: []string{"100", "2"}, + builtinPermissions: []string{"5", "6"}, + expected: 7, + }, + { + desc: "Should not get admin roles", + orgID: 1, + role: "Viewer", + userPermissions: []string{"1", "2", "10"}, + teamPermissions: []string{"100", "2"}, + builtinPermissions: []string{"5", "6"}, + expected: 5, + }, + { + desc: "Should work without org role", + orgID: 1, + role: "", + userPermissions: []string{"1", "2", "10"}, + teamPermissions: []string{"100", "2"}, + builtinPermissions: []string{"5", "6"}, + expected: 5, + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + store, sql := setupTestEnv(t) + + user, team := createUserAndTeam(t, sql, tt.orgID) + + for _, id := range tt.userPermissions { + _, err := store.SetUserResourcePermissions(context.Background(), tt.orgID, user.Id, accesscontrol.SetResourcePermissionsCommand{ + Actions: []string{"dashboards:read"}, + Resource: "dashboards", + ResourceID: id, + }) + require.NoError(t, err) + } + + for _, id := range tt.teamPermissions { + _, err := store.SetTeamResourcePermissions(context.Background(), tt.orgID, team.Id, accesscontrol.SetResourcePermissionsCommand{ + Actions: []string{"dashboards:read"}, + Resource: "dashboards", + ResourceID: id, + }) + require.NoError(t, err) + } + + for _, id := range tt.builtinPermissions { + _, err := store.SetBuiltinResourcePermissions(context.Background(), tt.orgID, "Admin", accesscontrol.SetResourcePermissionsCommand{ + Actions: []string{"dashboards:read"}, + Resource: "dashboards", + ResourceID: id, + }) + require.NoError(t, err) + } + + var roles []string + role := models.RoleType(tt.role) + + if role.IsValid() { + roles = append(roles, string(role)) + for _, c := range role.Children() { + roles = append(roles, string(c)) + } + } + + permissions, err := store.GetUserPermissions(context.Background(), accesscontrol.GetUserPermissionsQuery{ + OrgID: tt.orgID, + UserID: user.Id, + Roles: roles, + }) + + require.NoError(t, err) + assert.Len(t, permissions, tt.expected) + }) + } +} + +func createUserAndTeam(t *testing.T, sql *sqlstore.SQLStore, orgID int64) (*models.User, models.Team) { + t.Helper() + + user, err := sql.CreateUser(context.Background(), models.CreateUserCommand{ + Login: "user", + OrgId: orgID, + }) + require.NoError(t, err) + + team, err := sql.CreateTeam("team", "", orgID) + require.NoError(t, err) + + err = sql.AddTeamMember(user.Id, orgID, team.Id, false, models.PERMISSION_VIEW) + require.NoError(t, err) + + return user, team +} + +func setupTestEnv(t testing.TB) (*AccessControlStore, *sqlstore.SQLStore) { + store := sqlstore.InitTestDB(t) + return ProvideService(store), store +} diff --git a/pkg/services/accesscontrol/database/resource.go b/pkg/services/accesscontrol/database/resource.go new file mode 100644 index 00000000000..2f054c5436d --- /dev/null +++ b/pkg/services/accesscontrol/database/resource.go @@ -0,0 +1,519 @@ +package database + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/sqlstore" +) + +func (s *AccessControlStore) SetUserResourcePermissions(ctx context.Context, orgID, userID int64, cmd accesscontrol.SetResourcePermissionsCommand) ([]accesscontrol.ResourcePermission, error) { + if userID == 0 { + return nil, models.ErrUserNotFound + } + + var err error + var permissions []accesscontrol.ResourcePermission + err = s.sql.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + permissions, err = s.setResourcePermissions(sess, orgID, managedUserRoleName(userID), s.userAdder(sess, orgID, userID), cmd) + if err != nil { + return err + } + + return nil + }) + + if err != nil { + return nil, err + } + + return permissions, nil +} + +func (s *AccessControlStore) SetTeamResourcePermissions(ctx context.Context, orgID, teamID int64, cmd accesscontrol.SetResourcePermissionsCommand) ([]accesscontrol.ResourcePermission, error) { + if teamID == 0 { + return nil, models.ErrTeamNotFound + } + + var err error + var permissions []accesscontrol.ResourcePermission + err = s.sql.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + permissions, err = s.setResourcePermissions(sess, orgID, managedTeamRoleName(teamID), s.teamAdder(sess, orgID, teamID), cmd) + if err != nil { + return err + } + + return nil + }) + + if err != nil { + return nil, err + } + + return permissions, nil +} + +func (s *AccessControlStore) SetBuiltinResourcePermissions(ctx context.Context, orgID int64, builtinRole string, cmd accesscontrol.SetResourcePermissionsCommand) ([]accesscontrol.ResourcePermission, error) { + if !models.RoleType(builtinRole).IsValid() || builtinRole == accesscontrol.RoleGrafanaAdmin { + return nil, fmt.Errorf("invalid role: %s", builtinRole) + } + + var err error + var permissions []accesscontrol.ResourcePermission + + err = s.sql.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + permissions, err = s.setResourcePermissions(sess, orgID, managedBuiltInRoleName(builtinRole), s.builtinRoleAdder(sess, orgID, builtinRole), cmd) + if err != nil { + return err + } + + return nil + }) + + if err != nil { + return nil, err + } + + return permissions, nil +} + +type roleAdder func(roleID int64) error + +func (s *AccessControlStore) setResourcePermissions( + sess *sqlstore.DBSession, orgID int64, roleName string, adder roleAdder, cmd accesscontrol.SetResourcePermissionsCommand, +) ([]accesscontrol.ResourcePermission, error) { + role, err := s.getOrCreateManagedRole(sess, orgID, roleName, adder) + if err != nil { + return nil, err + } + + rawSQL := ` + SELECT + p.* + FROM permission as p + INNER JOIN role r on r.id = p.role_id + WHERE r.id = ? + AND p.scope = ? + ` + + var current []accesscontrol.Permission + if err := sess.SQL(rawSQL, role.ID, getResourceScope(cmd.Resource, cmd.ResourceID)).Find(¤t); err != nil { + return nil, err + } + + missing := make(map[string]struct{}, len(cmd.Actions)) + for _, a := range cmd.Actions { + missing[a] = struct{}{} + } + + var keep []int64 + var remove []int64 + for _, p := range current { + if _, ok := missing[p.Action]; ok { + keep = append(keep, p.ID) + delete(missing, p.Action) + } else if !ok { + remove = append(remove, p.ID) + } + } + + if err := deletePermissions(sess, remove); err != nil { + return nil, err + } + + var permissions []accesscontrol.ResourcePermission + + for action := range missing { + p, err := createResourcePermission(sess, role.ID, action, cmd.Resource, cmd.ResourceID) + if err != nil { + return nil, err + } + permissions = append(permissions, *p) + } + + keptPermissions, err := getManagedPermissions(sess, cmd.ResourceID, keep) + if err != nil { + return nil, err + } + + permissions = append(permissions, keptPermissions...) + return permissions, nil +} + +func (s *AccessControlStore) RemoveResourcePermission(ctx context.Context, orgID int64, cmd accesscontrol.RemoveResourcePermissionCommand) error { + return s.sql.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + var permission accesscontrol.Permission + rawSql := ` + SELECT + p.* + FROM permission p + LEFT JOIN role r ON p.role_id = r.id + WHERE r.name LIKE 'managed:%' + AND r.org_id = ? + AND p.id = ? + AND p.scope = ? + AND p.action IN(?` + strings.Repeat(",?", len(cmd.Actions)-1) + `) + ` + + args := []interface{}{ + orgID, + cmd.PermissionID, + getResourceScope(cmd.Resource, cmd.ResourceID), + } + + for _, a := range cmd.Actions { + args = append(args, a) + } + + exists, err := sess.SQL(rawSql, args...).Get(&permission) + if err != nil { + return err + } + + if !exists { + return nil + } + + return deletePermissions(sess, []int64{permission.ID}) + }) +} + +func (s *AccessControlStore) GetResourcesPermissions(ctx context.Context, orgID int64, query accesscontrol.GetResourcesPermissionsQuery) ([]accesscontrol.ResourcePermission, error) { + var result []accesscontrol.ResourcePermission + + err := s.sql.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + var err error + result, err = getResourcesPermissions(sess, orgID, query, false) + return err + }) + + return result, err +} + +func createResourcePermission(sess *sqlstore.DBSession, roleID int64, action, resource string, resourceID string) (*accesscontrol.ResourcePermission, error) { + permission := managedPermission(action, resource, resourceID) + permission.RoleID = roleID + permission.Created = time.Now() + permission.Updated = time.Now() + + if _, err := sess.Insert(&permission); err != nil { + return nil, err + } + + rawSql := ` + SELECT + p.*, + ? AS resource_id, + ur.user_id AS user_id, + u.login AS user_login, + u.email AS user_email, + tr.team_id AS team_id, + t.name AS team, + t.email AS team_email, + r.name as role_name + FROM permission p + LEFT JOIN role r ON p.role_id = r.id + LEFT JOIN team_role tr ON r.id = tr.role_id + LEFT JOIN team t ON tr.team_id = t.id + LEFT JOIN user_role ur ON r.id = ur.role_id + LEFT JOIN user u ON ur.user_id = u.id + WHERE p.id = ? + ` + + p := &accesscontrol.ResourcePermission{} + if _, err := sess.SQL(rawSql, resourceID, permission.ID).Get(p); err != nil { + return nil, err + } + + return p, nil +} + +func getResourcesPermissions(sess *sqlstore.DBSession, orgID int64, query accesscontrol.GetResourcesPermissionsQuery, managed bool) ([]accesscontrol.ResourcePermission, error) { + result := make([]accesscontrol.ResourcePermission, 0) + + if len(query.Actions) == 0 { + return result, nil + } + + if len(query.ResourceIDs) == 0 { + return result, nil + } + + rawSelect := ` + SELECT + p.*, + r.name as role_name, + ` + + userSelect := rawSelect + ` + ur.user_id AS user_id, + u.login AS user_login, + u.email AS user_email, + 0 AS team_id, + '' AS team, + '' AS team_email, + '' AS built_in_role + ` + + teamSelect := rawSelect + ` + 0 AS user_id, + '' AS user_login, + '' AS user_email, + tr.team_id AS team_id, + t.name AS team, + t.email AS team_email, + '' AS built_in_role + ` + + builtinSelect := rawSelect + ` + 0 AS user_id, + '' AS user_login, + '' AS user_email, + 0 as team_id, + '' AS team, + '' AS team_email, + br.role AS built_in_role + ` + + rawFrom := ` + FROM permission p + INNER JOIN role r ON p.role_id = r.id + ` + userFrom := rawFrom + ` + INNER JOIN user_role ur ON r.id = ur.role_id + INNER JOIN user u ON ur.user_id = u.id + ` + teamFrom := rawFrom + ` + INNER JOIN team_role tr ON r.id = tr.role_id + INNER JOIN team t ON tr.team_id = t.id + ` + + builtinFrom := rawFrom + ` + INNER JOIN builtin_role br ON r.id = br.role_id + ` + where := ` + WHERE (r.org_id = ? OR r.org_id = 0) + AND (p.scope = '*' OR p.scope = ? OR p.scope = ? OR p.scope IN (?` + strings.Repeat(",?", len(query.ResourceIDs)-1) + `)) + AND p.action IN (?` + strings.Repeat(",?", len(query.Actions)-1) + `) + ` + + if managed { + where += `AND r.name LIKE 'managed:%'` + } + + args := []interface{}{ + orgID, + getResourceAllScope(query.Resource), + getResourceAllIDScope(query.Resource), + } + + for _, id := range query.ResourceIDs { + args = append(args, getResourceScope(query.Resource, id)) + } + + for _, a := range query.Actions { + args = append(args, a) + } + + // Need args x3 due to union + initialLength := len(args) + args = append(args, args[:initialLength]...) + args = append(args, args[:initialLength]...) + + user := userSelect + userFrom + where + team := teamSelect + teamFrom + where + builtin := builtinSelect + builtinFrom + where + sql := user + "UNION" + team + "UNION" + builtin + + if err := sess.SQL(sql, args...).Find(&result); err != nil { + return nil, err + } + + scopeAll := getResourceAllScope(query.Resource) + scopeAllIDs := getResourceAllIDScope(query.Resource) + out := make([]accesscontrol.ResourcePermission, 0, len(result)) + + // Add resourceIds and generate permissions for `*`, `resource:*` and `resource:id:*` + // TODO: handle scope with other key prefixes e.g. `resource:name:*` and `resource:name:name` + for _, id := range query.ResourceIDs { + scope := getResourceScope(query.Resource, id) + for _, p := range result { + if p.Scope == scope || p.Scope == scopeAll || p.Scope == scopeAllIDs || p.Scope == "*" { + p.ResourceID = id + out = append(out, p) + } + } + } + + return out, nil +} + +func (s *AccessControlStore) userAdder(sess *sqlstore.DBSession, orgID, userID int64) roleAdder { + return func(roleID int64) error { + if res, err := sess.Query("SELECT 1 FROM user_role WHERE org_id=? AND user_id=? AND role_id=?", orgID, userID, roleID); err != nil { + return err + } else if len(res) == 1 { + return fmt.Errorf("role is already added to this user") + } + + userRole := &accesscontrol.UserRole{ + OrgID: orgID, + UserID: userID, + RoleID: roleID, + Created: time.Now(), + } + + _, err := sess.Insert(userRole) + + return err + } +} + +func (s *AccessControlStore) teamAdder(sess *sqlstore.DBSession, orgID, teamID int64) roleAdder { + return func(roleID int64) error { + if res, err := sess.Query("SELECT 1 FROM team_role WHERE org_id=? AND team_id=? AND role_id=?", orgID, teamID, roleID); err != nil { + return err + } else if len(res) == 1 { + return fmt.Errorf("role is already added to this team") + } + + teamRole := &accesscontrol.TeamRole{ + OrgID: orgID, + TeamID: teamID, + RoleID: roleID, + Created: time.Now(), + } + + _, err := sess.Insert(teamRole) + return err + } +} + +func (s *AccessControlStore) builtinRoleAdder(sess *sqlstore.DBSession, orgID int64, builtinRole string) roleAdder { + return func(roleID int64) error { + if res, err := sess.Query("SELECT 1 FROM builtin_role WHERE role_id=? AND role=? AND org_id=?", roleID, builtinRole, orgID); err != nil { + return err + } else if len(res) == 1 { + return fmt.Errorf("built-in role already has the role granted") + } + + _, err := sess.Table("builtin_role").Insert(accesscontrol.BuiltinRole{ + RoleID: roleID, + OrgID: orgID, + Role: builtinRole, + Updated: time.Now(), + Created: time.Now(), + }) + + return err + } +} + +func (s *AccessControlStore) getOrCreateManagedRole(sess *sqlstore.DBSession, orgID int64, name string, add roleAdder) (*accesscontrol.Role, error) { + role := accesscontrol.Role{OrgID: orgID, Name: name} + has, err := sess.Where("org_id = ? AND name = ?", orgID, name).Get(&role) + + // If managed role does not exist, create it and add it to user/team/builtin + if !has { + uid, err := generateNewRoleUID(sess, orgID) + if err != nil { + return nil, err + } + + role = accesscontrol.Role{ + OrgID: orgID, + Name: name, + UID: uid, + Created: time.Now(), + Updated: time.Now(), + } + + if _, err := sess.Insert(&role); err != nil { + return nil, err + } + + if err := add(role.ID); err != nil { + return nil, err + } + } + + if err != nil { + return nil, err + } + + return &role, nil +} + +func getManagedPermissions(sess *sqlstore.DBSession, resourceID string, ids []int64) ([]accesscontrol.ResourcePermission, error) { + var result []accesscontrol.ResourcePermission + if len(ids) == 0 { + return result, nil + } + + rawSql := ` + SELECT + p.*, + ? AS resource_id, + ur.user_id AS user_id, + u.login AS user_login, + u.email AS user_email, + tr.team_id AS team_id, + t.name AS team, + t.email AS team_email, + r.name as role_name + FROM permission p + INNER JOIN role r ON p.role_id = r.id + LEFT JOIN team_role tr ON r.id = tr.role_id + LEFT JOIN team t ON tr.team_id = t.id + LEFT JOIN user_role ur ON r.id = ur.role_id + LEFT JOIN user u ON ur.user_id = u.id + WHERE p.id IN (?` + strings.Repeat(",?", len(ids)-1) + `) + ` + + args := make([]interface{}, 0, len(ids)+1) + args = append(args, resourceID) + for _, id := range ids { + args = append(args, id) + } + + if err := sess.SQL(rawSql, args...).Find(&result); err != nil { + return nil, err + } + + return result, nil +} + +func managedPermission(action, resource string, resourceID string) accesscontrol.Permission { + return accesscontrol.Permission{ + Action: action, + Scope: getResourceScope(resource, resourceID), + } +} + +func managedUserRoleName(userID int64) string { + return fmt.Sprintf("managed:users:%d:permissions", userID) +} + +func managedTeamRoleName(teamID int64) string { + return fmt.Sprintf("managed:teams:%d:permissions", teamID) +} + +func managedBuiltInRoleName(builtinRole string) string { + return fmt.Sprintf("managed:builtins:%s:permissions", strings.ToLower(builtinRole)) +} + +func getResourceScope(resource string, resourceID string) string { + return fmt.Sprintf("%s:id:%s", resource, resourceID) +} + +func getResourceAllScope(resource string) string { + return fmt.Sprintf("%s:*", resource) +} + +func getResourceAllIDScope(resource string) string { + return fmt.Sprintf("%s:id:*", resource) +} diff --git a/pkg/services/accesscontrol/database/resource_bench_test.go b/pkg/services/accesscontrol/database/resource_bench_test.go new file mode 100644 index 00000000000..ddc42d7f09d --- /dev/null +++ b/pkg/services/accesscontrol/database/resource_bench_test.go @@ -0,0 +1,159 @@ +package database + +import ( + "context" + "fmt" + "math" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/sqlstore" +) + +const ( + dsAction = "datasources:query" + dsResource = "datasources" + PermissionsPerRole = 10 + UsersPerTeam = 10 + permissionsPerDs = 100 +) + +func BenchmarkDSPermissions10_10(b *testing.B) { benchmarkDSPermissions(b, 10, 10) } + +func BenchmarkDSPermissions10_100(b *testing.B) { benchmarkDSPermissions(b, 10, 100) } + +func BenchmarkDSPermissions10_1000(b *testing.B) { benchmarkDSPermissions(b, 10, 1000) } + +func BenchmarkDSPermissions100_10(b *testing.B) { benchmarkDSPermissions(b, 100, 10) } + +func BenchmarkDSPermissions100_100(b *testing.B) { benchmarkDSPermissions(b, 100, 100) } + +func BenchmarkDSPermissions100_1000(b *testing.B) { benchmarkDSPermissions(b, 100, 1000) } + +func BenchmarkDSPermissions1000_10(b *testing.B) { benchmarkDSPermissions(b, 1000, 10) } + +func BenchmarkDSPermissions1000_100(b *testing.B) { benchmarkDSPermissions(b, 1000, 100) } + +func BenchmarkDSPermissions1000_1000(b *testing.B) { benchmarkDSPermissions(b, 1000, 1000) } + +func benchmarkDSPermissions(b *testing.B, dsNum, usersNum int) { + ac, dataSources := setupResourceBenchmark(b, dsNum, usersNum) + // We don't want to measure DB initialization + b.ResetTimer() + + for i := 0; i < b.N; i++ { + getDSPermissions(b, ac, dataSources) + } +} + +func getDSPermissions(b *testing.B, store accesscontrol.ResourceStore, dataSources []int64) { + dsId := dataSources[0] + + permissions, err := store.GetResourcesPermissions(context.Background(), accesscontrol.GlobalOrgID, accesscontrol.GetResourcesPermissionsQuery{ + Actions: []string{dsAction}, + Resource: dsResource, + ResourceIDs: []string{strconv.Itoa(int(dsId))}, + }) + require.NoError(b, err) + assert.GreaterOrEqual(b, len(permissions), 2) +} + +func setupResourceBenchmark(b *testing.B, dsNum, usersNum int) (*AccessControlStore, []int64) { + ac, sql := setupTestEnv(b) + dataSources := GenerateDatasourcePermissions(b, sql, ac, dsNum, usersNum, permissionsPerDs) + return ac, dataSources +} + +func GenerateDatasourcePermissions(b *testing.B, db *sqlstore.SQLStore, ac *AccessControlStore, dsNum, usersNum, permissionsPerDs int) []int64 { + dataSources := make([]int64, 0) + for i := 0; i < dsNum; i++ { + addDSCommand := &models.AddDataSourceCommand{ + OrgId: 0, + Name: fmt.Sprintf("ds_%d", i), + Type: models.DS_GRAPHITE, + Access: models.DS_ACCESS_DIRECT, + Url: "http://test", + } + + _ = db.AddDataSource(context.Background(), addDSCommand) + dataSources = append(dataSources, addDSCommand.Result.Id) + } + + userIds, teamIds := generateTeamsAndUsers(b, db, usersNum) + + for _, dsID := range dataSources { + // Add DS permissions for the users + maxPermissions := int(math.Min(float64(permissionsPerDs), float64(len(userIds)))) + for i := 0; i < maxPermissions; i++ { + _, err := ac.SetUserResourcePermissions( + context.Background(), + accesscontrol.GlobalOrgID, + userIds[i], + accesscontrol.SetResourcePermissionsCommand{ + Actions: []string{dsAction}, + Resource: dsResource, + ResourceID: strconv.Itoa(int(dsID)), + }, + ) + require.NoError(b, err) + } + + // Add DS permissions for the teams + maxPermissions = int(math.Min(float64(permissionsPerDs), float64(len(teamIds)))) + for i := 0; i < maxPermissions; i++ { + _, err := ac.SetTeamResourcePermissions( + context.Background(), + accesscontrol.GlobalOrgID, + teamIds[i], + accesscontrol.SetResourcePermissionsCommand{ + Actions: []string{"datasources:query"}, + Resource: "datasources", + ResourceID: strconv.Itoa(int(dsID)), + }, + ) + require.NoError(b, err) + } + } + + return dataSources +} + +func generateTeamsAndUsers(b *testing.B, db *sqlstore.SQLStore, users int) ([]int64, []int64) { + numberOfTeams := int(math.Ceil(float64(users) / UsersPerTeam)) + globalUserId := 0 + + userIds := make([]int64, 0) + teamIds := make([]int64, 0) + for i := 0; i < numberOfTeams; i++ { + // Create team + teamName := fmt.Sprintf("%s%v", "team", i) + teamEmail := fmt.Sprintf("%s@example.org", teamName) + team, err := db.CreateTeam(teamName, teamEmail, 1) + require.NoError(b, err) + teamId := team.Id + teamIds = append(teamIds, teamId) + + // Create team users + for u := 0; u < UsersPerTeam; u++ { + userName := fmt.Sprintf("%s%v", "user", globalUserId) + userEmail := fmt.Sprintf("%s@example.org", userName) + createUserCmd := models.CreateUserCommand{Email: userEmail, Name: userName, Login: userName, OrgId: 1} + + user, err := db.CreateUser(context.Background(), createUserCmd) + require.NoError(b, err) + userId := user.Id + globalUserId++ + userIds = append(userIds, userId) + + err = db.AddTeamMember(userId, 1, teamId, false, 1) + require.NoError(b, err) + } + } + + return userIds, teamIds +} diff --git a/pkg/services/accesscontrol/database/resource_test.go b/pkg/services/accesscontrol/database/resource_test.go new file mode 100644 index 00000000000..125550d361e --- /dev/null +++ b/pkg/services/accesscontrol/database/resource_test.go @@ -0,0 +1,391 @@ +package database + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/sqlstore" +) + +type setUserResourcePermissionsTest struct { + desc string + orgID int64 + userID int64 + actions []string + resource string + resourceID string + seeds []accesscontrol.SetResourcePermissionsCommand +} + +func TestAccessControlStore_SetUserResourcePermissions(t *testing.T) { + tests := []setUserResourcePermissionsTest{ + { + desc: "should set resource permission for user", + userID: 1, + actions: []string{"datasources:query"}, + resource: "datasources", + resourceID: "1", + }, + { + desc: "should remove resource permission for user", + orgID: 1, + userID: 1, + actions: []string{}, + resource: "datasources", + resourceID: "1", + seeds: []accesscontrol.SetResourcePermissionsCommand{ + { + Actions: []string{"datasources:query"}, + Resource: "datasources", + ResourceID: "1", + }, + }, + }, + { + desc: "should add new resource permission for user", + orgID: 1, + userID: 1, + actions: []string{"datasources:query", "datasources:write"}, + resource: "datasources", + resourceID: "1", + seeds: []accesscontrol.SetResourcePermissionsCommand{ + { + Actions: []string{"datasources:write"}, + Resource: "datasources", + ResourceID: "1", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + store, _ := setupTestEnv(t) + + for _, s := range test.seeds { + _, err := store.SetUserResourcePermissions(context.Background(), test.orgID, test.userID, s) + require.NoError(t, err) + } + + added, err := store.SetUserResourcePermissions(context.Background(), test.userID, test.userID, accesscontrol.SetResourcePermissionsCommand{ + Actions: test.actions, + Resource: test.resource, + ResourceID: test.resourceID, + }) + + require.NoError(t, err) + assert.Len(t, added, len(test.actions)) + for _, p := range added { + assert.Equal(t, getResourceScope(test.resource, test.resourceID), p.Scope) + } + }) + } +} + +type setTeamResourcePermissionsTest struct { + desc string + orgID int64 + teamID int64 + actions []string + resource string + resourceID string + seeds []accesscontrol.SetResourcePermissionsCommand +} + +func TestAccessControlStore_SetTeamResourcePermissions(t *testing.T) { + tests := []setTeamResourcePermissionsTest{ + { + desc: "should add new resource permission for team", + orgID: 1, + teamID: 1, + actions: []string{"datasources:query"}, + resource: "datasources", + resourceID: "1", + }, + { + desc: "should add new resource permission when others exist", + orgID: 1, + teamID: 1, + actions: []string{"datasources:query", "datasources:write"}, + resource: "datasources", + resourceID: "1", + seeds: []accesscontrol.SetResourcePermissionsCommand{ + { + Actions: []string{"datasources:query"}, + Resource: "datasources", + ResourceID: "1", + }, + }, + }, + { + desc: "should remove permissions for team", + orgID: 1, + teamID: 1, + actions: []string{}, + resource: "datasources", + resourceID: "1", + seeds: []accesscontrol.SetResourcePermissionsCommand{ + { + Actions: []string{"datasources:query"}, + Resource: "datasources", + ResourceID: "1", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + store, _ := setupTestEnv(t) + + for _, s := range test.seeds { + _, err := store.SetTeamResourcePermissions(context.Background(), test.orgID, test.teamID, s) + require.NoError(t, err) + } + + added, err := store.SetTeamResourcePermissions(context.Background(), test.orgID, test.teamID, accesscontrol.SetResourcePermissionsCommand{ + Actions: test.actions, + Resource: test.resource, + ResourceID: test.resourceID, + }) + + require.NoError(t, err) + assert.Len(t, added, len(test.actions)) + for _, p := range added { + assert.Equal(t, getResourceScope(test.resource, test.resourceID), p.Scope) + } + }) + } +} + +type setBuiltinResourcePermissionsTest struct { + desc string + orgID int64 + builtinRole string + actions []string + resource string + resourceID string + seeds []accesscontrol.SetResourcePermissionsCommand +} + +func TestAccessControlStore_SetBuiltinResourcePermissions(t *testing.T) { + tests := []setBuiltinResourcePermissionsTest{ + { + desc: "should add new resource permission for builtin role", + orgID: 1, + builtinRole: "Viewer", + actions: []string{"datasources:query"}, + resource: "datasources", + resourceID: "1", + }, + { + desc: "should add new resource permission when others exist", + orgID: 1, + builtinRole: "Viewer", + actions: []string{"datasources:query", "datasources:write"}, + resource: "datasources", + resourceID: "1", + seeds: []accesscontrol.SetResourcePermissionsCommand{ + { + Actions: []string{"datasources:query"}, + Resource: "datasources", + ResourceID: "1", + }, + }, + }, + { + desc: "should remove permissions for builtin role", + orgID: 1, + builtinRole: "Viewer", + actions: []string{}, + resource: "datasources", + resourceID: "1", + seeds: []accesscontrol.SetResourcePermissionsCommand{ + { + Actions: []string{"datasources:query"}, + Resource: "datasources", + ResourceID: "1", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + store, _ := setupTestEnv(t) + + for _, s := range test.seeds { + _, err := store.SetBuiltinResourcePermissions(context.Background(), test.orgID, test.builtinRole, s) + require.NoError(t, err) + } + + added, err := store.SetBuiltinResourcePermissions(context.Background(), test.orgID, test.builtinRole, accesscontrol.SetResourcePermissionsCommand{ + Actions: test.actions, + Resource: test.resource, + ResourceID: test.resourceID, + }) + + require.NoError(t, err) + assert.Len(t, added, len(test.actions)) + for _, p := range added { + assert.Equal(t, getResourceScope(test.resource, test.resourceID), p.Scope) + } + }) + } +} + +type resourcePermission struct { + resource string + resourceID string +} + +type removeResourcePermissionTest struct { + desc string + add resourcePermission + remove resourcePermission + expectedErr error +} + +func TestAccessControlStore_RemoveResourcePermission(t *testing.T) { + tests := []removeResourcePermissionTest{ + { + desc: "should remove resource permission", + add: resourcePermission{ + resource: "datasources", + resourceID: "1", + }, + remove: resourcePermission{ + resource: "datasources", + resourceID: "1", + }, + expectedErr: nil, + }, + { + desc: "should return nil when permission does not exist", + add: resourcePermission{ + resource: "datasources", + resourceID: "1", + }, + remove: resourcePermission{ + resource: "datasources", + resourceID: "2", + }, + expectedErr: nil, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + store, sql := setupTestEnv(t) + + user, err := sql.CreateUser(context.Background(), models.CreateUserCommand{ + Login: "user", + OrgId: 1, + }) + require.NoError(t, err) + + // Seed with permission + seeded, err := store.SetUserResourcePermissions(context.Background(), user.OrgId, user.Id, accesscontrol.SetResourcePermissionsCommand{ + Actions: []string{"datasources:query"}, + Resource: test.add.resource, + ResourceID: test.add.resourceID, + }) + require.NoError(t, err) + + err = store.RemoveResourcePermission(context.Background(), user.OrgId, accesscontrol.RemoveResourcePermissionCommand{ + Actions: []string{"datasources:query"}, + Resource: test.remove.resource, + ResourceID: test.remove.resourceID, + PermissionID: seeded[0].ID, + }) + + if test.expectedErr != nil { + assert.ErrorIs(t, err, test.expectedErr) + } else { + permissions, err := store.GetResourcesPermissions(context.Background(), user.OrgId, accesscontrol.GetResourcesPermissionsQuery{ + Actions: []string{"datasources:query"}, + Resource: test.add.resource, + ResourceIDs: []string{test.add.resourceID}, + }) + assert.NoError(t, err) + if test.add.resourceID != test.remove.resourceID { + assert.Len(t, permissions, 1) + } else { + assert.Len(t, permissions, 0) + } + } + }) + } +} + +type getResourcesPermissionsTest struct { + desc string + numUsers int + actions []string + resource string + resourceIDs []string +} + +func TestAccessControlStore_GetResourcesPermissions(t *testing.T) { + tests := []getResourcesPermissionsTest{ + { + desc: "should return permissions for all resource ids", + numUsers: 3, + actions: []string{"datasources:query"}, + resource: "datasources", + resourceIDs: []string{"1", "2"}, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + store, sql := setupTestEnv(t) + + for _, id := range test.resourceIDs { + seedResourcePermissions(t, store, sql, test.actions, test.resource, id, test.numUsers) + } + + permissions, err := store.GetResourcesPermissions(context.Background(), 1, accesscontrol.GetResourcesPermissionsQuery{ + Actions: test.actions, + Resource: test.resource, + ResourceIDs: test.resourceIDs, + }) + require.NoError(t, err) + + expectedLen := test.numUsers * len(test.resourceIDs) + assert.Len(t, permissions, expectedLen) + }) + } +} + +func seedResourcePermissions(t *testing.T, store *AccessControlStore, sql *sqlstore.SQLStore, actions []string, resource, resourceID string, numUsers int) { + t.Helper() + for i := 0; i < numUsers; i++ { + org, _ := sql.GetOrgByName("test") + + if org == nil { + addedOrg, err := sql.CreateOrgWithMember("test", int64(i)) + require.NoError(t, err) + org = &addedOrg + } + + u, err := sql.CreateUser(context.Background(), models.CreateUserCommand{ + Login: fmt.Sprintf("user:%s%d", resourceID, i), + OrgId: org.Id, + }) + require.NoError(t, err) + + _, err = store.SetUserResourcePermissions(context.Background(), 1, u.Id, accesscontrol.SetResourcePermissionsCommand{ + Actions: actions, + Resource: resource, + ResourceID: resourceID, + }) + require.NoError(t, err) + } +} diff --git a/pkg/services/accesscontrol/database/uid.go b/pkg/services/accesscontrol/database/uid.go new file mode 100644 index 00000000000..c44aa3f2eab --- /dev/null +++ b/pkg/services/accesscontrol/database/uid.go @@ -0,0 +1,26 @@ +package database + +import ( + "fmt" + + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/util" +) + +func generateNewRoleUID(sess *sqlstore.DBSession, orgID int64) (string, error) { + for i := 0; i < 3; i++ { + uid := util.GenerateShortUID() + + exists, err := sess.Where("org_id=? AND uid=?", orgID, uid).Get(&accesscontrol.Role{}) + if err != nil { + return "", err + } + + if !exists { + return uid, nil + } + } + + return "", fmt.Errorf("failed to generate uid") +} diff --git a/pkg/services/accesscontrol/models.go b/pkg/services/accesscontrol/models.go index 59e86235b3b..a59b9a123fd 100644 --- a/pkg/services/accesscontrol/models.go +++ b/pkg/services/accesscontrol/models.go @@ -125,6 +125,34 @@ func fallbackDisplayName(rName string) string { return strings.TrimSpace(strings.Replace(rNameWithoutPrefix, ":", " ", -1)) } +type TeamRole struct { + ID int64 `json:"id" xorm:"pk autoincr 'id'"` + OrgID int64 `json:"orgId" xorm:"org_id"` + RoleID int64 `json:"roleId" xorm:"role_id"` + TeamID int64 `json:"teamId" xorm:"team_id"` + + Created time.Time +} + +type UserRole struct { + ID int64 `json:"id" xorm:"pk autoincr 'id'"` + OrgID int64 `json:"orgId" xorm:"org_id"` + RoleID int64 `json:"roleId" xorm:"role_id"` + UserID int64 `json:"userId" xorm:"user_id"` + + Created time.Time +} + +type BuiltinRole struct { + ID int64 `json:"id" xorm:"pk autoincr 'id'"` + RoleID int64 `json:"roleId" xorm:"role_id"` + OrgID int64 `json:"orgId" xorm:"org_id"` + Role string + + Updated time.Time + Created time.Time +} + // Permission is the model for access control permissions. type Permission struct { ID int64 `json:"-" xorm:"pk autoincr 'id'"` @@ -143,12 +171,58 @@ func (p Permission) OSSPermission() Permission { } } +type GetUserPermissionsQuery struct { + OrgID int64 `json:"-"` + UserID int64 `json:"userId"` + Roles []string +} + // ScopeParams holds the parameters used to fill in scope templates type ScopeParams struct { OrgID int64 URLParams map[string]string } +type ResourcePermission struct { + ID int64 `xorm:"id"` + ResourceID string `xorm:"resource_id"` + RoleName string + Action string + Scope string + UserId int64 + UserLogin string + UserEmail string + TeamId int64 + TeamEmail string + Team string + BuiltInRole string + Created time.Time + Updated time.Time +} + +func (p *ResourcePermission) Managed() bool { + return strings.HasPrefix(p.RoleName, "managed:") +} + +type SetResourcePermissionsCommand struct { + Actions []string + Resource string + ResourceID string +} + +type RemoveResourcePermissionCommand struct { + Resource string + Actions []string + ResourceID string + PermissionID int64 +} + +type GetResourcesPermissionsQuery struct { + Actions []string + Resource string + ResourceIDs []string +} + const ( GlobalOrgID = 0 // Permission actions diff --git a/pkg/services/accesscontrol/resource_manager.go b/pkg/services/accesscontrol/resource_manager.go new file mode 100644 index 00000000000..7b22bcda8ac --- /dev/null +++ b/pkg/services/accesscontrol/resource_manager.go @@ -0,0 +1,109 @@ +package accesscontrol + +import ( + "context" + "fmt" +) + +type ResourceManager struct { + resource string + actions []string + validActions map[string]struct{} + store ResourceStore + validator ResourceValidator +} + +type ResourceValidator func(ctx context.Context, orgID int64, resourceID string) error + +func NewResourceManager(resource string, actions []string, validator ResourceValidator, store ResourceStore) *ResourceManager { + validActions := make(map[string]struct{}, len(actions)) + for _, a := range actions { + validActions[a] = struct{}{} + } + + return &ResourceManager{ + store: store, + actions: actions, + validActions: validActions, + resource: resource, + validator: validator, + } +} + +func (r *ResourceManager) GetPermissions(ctx context.Context, orgID int64, resourceID string) ([]ResourcePermission, error) { + return r.store.GetResourcesPermissions(ctx, orgID, GetResourcesPermissionsQuery{ + Actions: r.actions, + Resource: r.resource, + ResourceIDs: []string{resourceID}, + }) +} + +func (r *ResourceManager) GetPermissionsByIds(ctx context.Context, orgID int64, resourceIDs []string) ([]ResourcePermission, error) { + return r.store.GetResourcesPermissions(ctx, orgID, GetResourcesPermissionsQuery{ + Actions: r.actions, + Resource: r.resource, + ResourceIDs: resourceIDs, + }) +} + +func (r *ResourceManager) SetUserPermissions(ctx context.Context, orgID int64, resourceID string, actions []string, userID int64) ([]ResourcePermission, error) { + if !r.validateActions(actions) { + return nil, fmt.Errorf("invalid actions: %s", actions) + } + + return r.store.SetUserResourcePermissions(ctx, orgID, userID, SetResourcePermissionsCommand{ + Actions: actions, + Resource: r.resource, + ResourceID: resourceID, + }) +} + +func (r *ResourceManager) SetTeamPermission(ctx context.Context, orgID int64, resourceID string, actions []string, teamID int64) ([]ResourcePermission, error) { + if !r.validateActions(actions) { + return nil, fmt.Errorf("invalid action: %s", actions) + } + + return r.store.SetTeamResourcePermissions(ctx, orgID, teamID, SetResourcePermissionsCommand{ + Actions: actions, + Resource: r.resource, + ResourceID: resourceID, + }) +} + +func (r *ResourceManager) SetBuiltinRolePermissions(ctx context.Context, orgID int64, resourceID string, actions []string, builtinRole string) ([]ResourcePermission, error) { + if !r.validateActions(actions) { + return nil, fmt.Errorf("invalid action: %s", actions) + } + + return r.store.SetBuiltinResourcePermissions(ctx, orgID, builtinRole, SetResourcePermissionsCommand{ + Actions: actions, + Resource: r.resource, + ResourceID: resourceID, + }) +} + +func (r *ResourceManager) RemovePermission(ctx context.Context, orgID int64, resourceID string, permissionID int64) error { + return r.store.RemoveResourcePermission(ctx, orgID, RemoveResourcePermissionCommand{ + Actions: r.actions, + Resource: r.resource, + ResourceID: resourceID, + PermissionID: permissionID, + }) +} + +// Validate will run supplied ResourceValidator +func (r *ResourceManager) Validate(ctx context.Context, orgID int64, resourceID string) error { + if r.validator != nil { + return r.validator(ctx, orgID, resourceID) + } + return nil +} + +func (r *ResourceManager) validateActions(actions []string) bool { + for _, a := range actions { + if _, ok := r.validActions[a]; !ok { + return false + } + } + return true +} diff --git a/pkg/services/sqlstore/migrations/accesscontrol/migrations.go b/pkg/services/sqlstore/migrations/accesscontrol/migrations.go new file mode 100644 index 00000000000..e708040569d --- /dev/null +++ b/pkg/services/sqlstore/migrations/accesscontrol/migrations.go @@ -0,0 +1,161 @@ +package accesscontrol + +import "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + +func AddMigration(mg *migrator.Migrator) { + permissionV1 := migrator.Table{ + Name: "permission", + Columns: []*migrator.Column{ + {Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "role_id", Type: migrator.DB_BigInt}, + {Name: "action", Type: migrator.DB_Varchar, Length: 190, Nullable: false}, + {Name: "scope", Type: migrator.DB_Varchar, Length: 190, Nullable: false}, + {Name: "created", Type: migrator.DB_DateTime, Nullable: false}, + {Name: "updated", Type: migrator.DB_DateTime, Nullable: false}, + }, + Indices: []*migrator.Index{ + {Cols: []string{"role_id"}}, + {Cols: []string{"role_id", "action", "scope"}, Type: migrator.UniqueIndex}, + }, + } + + mg.AddMigration("create permission table", migrator.NewAddTableMigration(permissionV1)) + + //------- indexes ------------------ + mg.AddMigration("add unique index permission.role_id", migrator.NewAddIndexMigration(permissionV1, permissionV1.Indices[0])) + mg.AddMigration("add unique index role_id_action_scope", migrator.NewAddIndexMigration(permissionV1, permissionV1.Indices[1])) + + roleV1 := migrator.Table{ + Name: "role", + Columns: []*migrator.Column{ + {Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "name", Type: migrator.DB_NVarchar, Length: 190, Nullable: false}, + {Name: "description", Type: migrator.DB_Text, Nullable: true}, + {Name: "version", Type: migrator.DB_BigInt, Nullable: false}, + {Name: "org_id", Type: migrator.DB_BigInt}, + {Name: "uid", Type: migrator.DB_NVarchar, Length: 40, Nullable: false}, + {Name: "created", Type: migrator.DB_DateTime, Nullable: false}, + {Name: "updated", Type: migrator.DB_DateTime, Nullable: false}, + }, + Indices: []*migrator.Index{ + {Cols: []string{"org_id"}}, + {Cols: []string{"org_id", "name"}, Type: migrator.UniqueIndex}, + {Cols: []string{"org_id", "uid"}, Type: migrator.UniqueIndex}, + }, + } + + mg.AddMigration("create role table", migrator.NewAddTableMigration(roleV1)) + + mg.AddMigration("add column display_name", migrator.NewAddColumnMigration(roleV1, &migrator.Column{ + Name: "display_name", Type: migrator.DB_NVarchar, Length: 190, Nullable: true, + })) + //------- indexes ------------------ + mg.AddMigration("add index role.org_id", migrator.NewAddIndexMigration(roleV1, roleV1.Indices[0])) + mg.AddMigration("add unique index role_org_id_name", migrator.NewAddIndexMigration(roleV1, roleV1.Indices[1])) + mg.AddMigration("add index role_org_id_uid", migrator.NewAddIndexMigration(roleV1, roleV1.Indices[2])) + + teamRoleV1 := migrator.Table{ + Name: "team_role", + Columns: []*migrator.Column{ + {Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "org_id", Type: migrator.DB_BigInt}, + {Name: "team_id", Type: migrator.DB_BigInt}, + {Name: "role_id", Type: migrator.DB_BigInt}, + {Name: "created", Type: migrator.DB_DateTime, Nullable: false}, + }, + Indices: []*migrator.Index{ + {Cols: []string{"org_id"}}, + {Cols: []string{"org_id", "team_id", "role_id"}, Type: migrator.UniqueIndex}, + {Cols: []string{"team_id"}}, + }, + } + + mg.AddMigration("create team role table", migrator.NewAddTableMigration(teamRoleV1)) + + //------- indexes ------------------ + mg.AddMigration("add index team_role.org_id", migrator.NewAddIndexMigration(teamRoleV1, teamRoleV1.Indices[0])) + mg.AddMigration("add unique index team_role_org_id_team_id_role_id", migrator.NewAddIndexMigration(teamRoleV1, teamRoleV1.Indices[1])) + mg.AddMigration("add index team_role.team_id", migrator.NewAddIndexMigration(teamRoleV1, teamRoleV1.Indices[2])) + + userRoleV1 := migrator.Table{ + Name: "user_role", + Columns: []*migrator.Column{ + {Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "org_id", Type: migrator.DB_BigInt}, + {Name: "user_id", Type: migrator.DB_BigInt}, + {Name: "role_id", Type: migrator.DB_BigInt}, + {Name: "created", Type: migrator.DB_DateTime, Nullable: false}, + }, + Indices: []*migrator.Index{ + {Cols: []string{"org_id"}}, + {Cols: []string{"org_id", "user_id", "role_id"}, Type: migrator.UniqueIndex}, + {Cols: []string{"user_id"}}, + }, + } + + mg.AddMigration("create user role table", migrator.NewAddTableMigration(userRoleV1)) + + //------- indexes ------------------ + mg.AddMigration("add index user_role.org_id", migrator.NewAddIndexMigration(userRoleV1, userRoleV1.Indices[0])) + mg.AddMigration("add unique index user_role_org_id_user_id_role_id", migrator.NewAddIndexMigration(userRoleV1, userRoleV1.Indices[1])) + mg.AddMigration("add index user_role.user_id", migrator.NewAddIndexMigration(userRoleV1, userRoleV1.Indices[2])) + + builtinRoleV1 := migrator.Table{ + Name: "builtin_role", + Columns: []*migrator.Column{ + {Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "role", Type: migrator.DB_NVarchar, Length: 190, Nullable: false}, + {Name: "role_id", Type: migrator.DB_BigInt}, + {Name: "created", Type: migrator.DB_DateTime, Nullable: false}, + {Name: "updated", Type: migrator.DB_DateTime, Nullable: false}, + }, + Indices: []*migrator.Index{ + {Cols: []string{"role_id"}}, + {Cols: []string{"role"}}, + }, + } + + mg.AddMigration("create builtin role table", migrator.NewAddTableMigration(builtinRoleV1)) + + //------- indexes ------------------ + mg.AddMigration("add index builtin_role.role_id", migrator.NewAddIndexMigration(builtinRoleV1, builtinRoleV1.Indices[0])) + mg.AddMigration("add index builtin_role.name", migrator.NewAddIndexMigration(builtinRoleV1, builtinRoleV1.Indices[1])) + + // Add org_id column to the builtin_role table + mg.AddMigration("Add column org_id to builtin_role table", migrator.NewAddColumnMigration(builtinRoleV1, &migrator.Column{ + Name: "org_id", Type: migrator.DB_BigInt, Default: "0", + })) + + mg.AddMigration("add index builtin_role.org_id", migrator.NewAddIndexMigration(builtinRoleV1, &migrator.Index{ + Cols: []string{"org_id"}, + })) + + mg.AddMigration("add unique index builtin_role_org_id_role_id_role", migrator.NewAddIndexMigration(builtinRoleV1, &migrator.Index{ + Cols: []string{"org_id", "role_id", "role"}, Type: migrator.UniqueIndex, + })) + + // Make role.uid unique across Grafana instance + mg.AddMigration("Remove unique index role_org_id_uid", migrator.NewDropIndexMigration(roleV1, &migrator.Index{ + Cols: []string{"org_id", "uid"}, Type: migrator.UniqueIndex, + })) + + mg.AddMigration("add unique index role.uid", migrator.NewAddIndexMigration(roleV1, &migrator.Index{ + Cols: []string{"uid"}, Type: migrator.UniqueIndex, + })) + + seedAssignmentV1 := migrator.Table{ + Name: "seed_assignment", + Columns: []*migrator.Column{ + {Name: "builtin_role", Type: migrator.DB_NVarchar, Length: 190, Nullable: false}, + {Name: "role_name", Type: migrator.DB_NVarchar, Length: 190, Nullable: false}, + }, + Indices: []*migrator.Index{ + {Cols: []string{"builtin_role", "role_name"}, Type: migrator.UniqueIndex}, + }, + } + + mg.AddMigration("create seed assignment table", migrator.NewAddTableMigration(seedAssignmentV1)) + + //------- indexes ------------------ + mg.AddMigration("add unique index builtin_role_role_name", migrator.NewAddIndexMigration(seedAssignmentV1, seedAssignmentV1.Indices[0])) +} diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index e595fc26cd8..65b2eb13ced 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -1,6 +1,7 @@ package migrations import ( + "github.com/grafana/grafana/pkg/services/sqlstore/migrations/accesscontrol" "github.com/grafana/grafana/pkg/services/sqlstore/migrations/ualert" . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" ) @@ -55,6 +56,7 @@ func (*OSSMigrations) AddMigration(mg *Migrator) { addSecretsMigration(mg) addKVStoreMigrations(mg) ualert.AddDashboardUIDPanelIDMigration(mg) + accesscontrol.AddMigration(mg) } func addMigrationLogMigrations(mg *Migrator) {