diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go index 9bd60f628ad..005c4a1bbde 100644 --- a/pkg/models/datasource.go +++ b/pkg/models/datasource.go @@ -168,7 +168,6 @@ type DsPermissionType int const ( DsPermissionNoAccess DsPermissionType = iota DsPermissionQuery - DsPermissionRead ) func (p DsPermissionType) String() string { diff --git a/pkg/server/wireexts_oss.go b/pkg/server/wireexts_oss.go index ce1c3223f46..9abecb69d7b 100644 --- a/pkg/server/wireexts_oss.go +++ b/pkg/server/wireexts_oss.go @@ -62,7 +62,7 @@ var wireExtsBasicSet = wire.NewSet( signature.ProvideService, wire.Bind(new(plugins.PluginLoaderAuthorizer), new(*signature.UnsignedPluginAuthorizer)), acdb.ProvideService, - wire.Bind(new(accesscontrol.ResourceStore), new(*acdb.AccessControlStore)), + wire.Bind(new(accesscontrol.ResourcePermissionsStore), new(*acdb.AccessControlStore)), wire.Bind(new(accesscontrol.PermissionsProvider), new(*acdb.AccessControlStore)), osskmsproviders.ProvideService, wire.Bind(new(kmsproviders.Service), new(osskmsproviders.Service)), diff --git a/pkg/services/accesscontrol/accesscontrol.go b/pkg/services/accesscontrol/accesscontrol.go index c8869241cda..fb59190cfbe 100644 --- a/pkg/services/accesscontrol/accesscontrol.go +++ b/pkg/services/accesscontrol/accesscontrol.go @@ -29,15 +29,28 @@ 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 +type ResourcePermissionsService interface { + // GetPermissions returns all permissions for given resourceID + GetPermissions(ctx context.Context, orgID int64, resourceID string) ([]ResourcePermission, error) + // SetUserPermission sets permission on resource for a user + SetUserPermission(ctx context.Context, orgID, userID int64, resourceID string, actions []string) (*ResourcePermission, error) + // SetTeamPermission sets permission on resource for a team + SetTeamPermission(ctx context.Context, orgID, teamID int64, resourceID string, actions []string) (*ResourcePermission, error) + // SetBuiltInRolePermission sets permission on resource for a built-in role (Admin, Editor, Viewer) + SetBuiltInRolePermission(ctx context.Context, orgID int64, builtInRole string, resourceID string, actions []string) (*ResourcePermission, error) + // MapActions will map actions for a ResourcePermissions to it's "friendly" name configured in PermissionsToActions map. + MapActions(permission ResourcePermission) string + // MapPermission will map a friendly named permission to it's corresponding actions configured in PermissionsToAction map. + MapPermission(permission string) []string +} + +type ResourcePermissionsStore interface { + // SetUserResourcePermission sets permission for managed user role on a resource + SetUserResourcePermission(ctx context.Context, orgID, userID int64, cmd SetResourcePermissionCommand) (*ResourcePermission, error) + // SetTeamResourcePermission sets permission for managed team role on a resource + SetTeamResourcePermission(ctx context.Context, orgID, teamID int64, cmd SetResourcePermissionCommand) (*ResourcePermission, error) + // SetBuiltinResourcePermission sets permissions for managed builtin role on a resource + SetBuiltInResourcePermission(ctx context.Context, orgID int64, builtinRole string, cmd SetResourcePermissionCommand) (*ResourcePermission, error) // GetResourcesPermissions will return all permission for all supplied resource ids GetResourcesPermissions(ctx context.Context, orgID int64, query GetResourcesPermissionsQuery) ([]ResourcePermission, error) } diff --git a/pkg/services/accesscontrol/database/database_test.go b/pkg/services/accesscontrol/database/database_test.go index 91532f26e78..49d695e8d13 100644 --- a/pkg/services/accesscontrol/database/database_test.go +++ b/pkg/services/accesscontrol/database/database_test.go @@ -60,7 +60,7 @@ func TestAccessControlStore_GetUserPermissions(t *testing.T) { user, team := createUserAndTeam(t, sql, tt.orgID) for _, id := range tt.userPermissions { - _, err := store.SetUserResourcePermissions(context.Background(), tt.orgID, user.Id, accesscontrol.SetResourcePermissionsCommand{ + _, err := store.SetUserResourcePermission(context.Background(), tt.orgID, user.Id, accesscontrol.SetResourcePermissionCommand{ Actions: []string{"dashboards:read"}, Resource: "dashboards", ResourceID: id, @@ -69,7 +69,7 @@ func TestAccessControlStore_GetUserPermissions(t *testing.T) { } for _, id := range tt.teamPermissions { - _, err := store.SetTeamResourcePermissions(context.Background(), tt.orgID, team.Id, accesscontrol.SetResourcePermissionsCommand{ + _, err := store.SetTeamResourcePermission(context.Background(), tt.orgID, team.Id, accesscontrol.SetResourcePermissionCommand{ Actions: []string{"dashboards:read"}, Resource: "dashboards", ResourceID: id, @@ -78,7 +78,7 @@ func TestAccessControlStore_GetUserPermissions(t *testing.T) { } for _, id := range tt.builtinPermissions { - _, err := store.SetBuiltinResourcePermissions(context.Background(), tt.orgID, "Admin", accesscontrol.SetResourcePermissionsCommand{ + _, err := store.SetBuiltInResourcePermission(context.Background(), tt.orgID, "Admin", accesscontrol.SetResourcePermissionCommand{ Actions: []string{"dashboards:read"}, Resource: "dashboards", ResourceID: id, diff --git a/pkg/services/accesscontrol/database/resource.go b/pkg/services/accesscontrol/database/resource_permissions.go similarity index 64% rename from pkg/services/accesscontrol/database/resource.go rename to pkg/services/accesscontrol/database/resource_permissions.go index 05ae41ce6e0..c8880aab974 100644 --- a/pkg/services/accesscontrol/database/resource.go +++ b/pkg/services/accesscontrol/database/resource_permissions.go @@ -11,19 +11,39 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore" ) -func (s *AccessControlStore) SetUserResourcePermissions(ctx context.Context, orgID, userID int64, cmd accesscontrol.SetResourcePermissionsCommand) ([]accesscontrol.ResourcePermission, error) { +type flatResourcePermission 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 *flatResourcePermission) Managed() bool { + return strings.HasPrefix(p.RoleName, "managed:") +} + +func (s *AccessControlStore) SetUserResourcePermission(ctx context.Context, orgID, userID int64, cmd accesscontrol.SetResourcePermissionCommand) (*accesscontrol.ResourcePermission, error) { if userID == 0 { return nil, models.ErrUserNotFound } var err error - var permissions []accesscontrol.ResourcePermission + var permission *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) + permission, err = s.setResourcePermission(sess, orgID, managedUserRoleName(userID), s.userAdder(sess, orgID, userID), cmd) if err != nil { return err } - return nil }) @@ -31,22 +51,22 @@ func (s *AccessControlStore) SetUserResourcePermissions(ctx context.Context, org return nil, err } - return permissions, nil + return permission, nil } -func (s *AccessControlStore) SetTeamResourcePermissions(ctx context.Context, orgID, teamID int64, cmd accesscontrol.SetResourcePermissionsCommand) ([]accesscontrol.ResourcePermission, error) { +func (s *AccessControlStore) SetTeamResourcePermission(ctx context.Context, orgID, teamID int64, cmd accesscontrol.SetResourcePermissionCommand) (*accesscontrol.ResourcePermission, error) { if teamID == 0 { return nil, models.ErrTeamNotFound } var err error - var permissions []accesscontrol.ResourcePermission + var permission *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) + permission, err = s.setResourcePermission(sess, orgID, managedTeamRoleName(teamID), s.teamAdder(sess, orgID, teamID), cmd) if err != nil { return err } - return nil }) @@ -54,38 +74,34 @@ func (s *AccessControlStore) SetTeamResourcePermissions(ctx context.Context, org return nil, err } - return permissions, nil + return permission, 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) +func (s *AccessControlStore) SetBuiltInResourcePermission(ctx context.Context, orgID int64, builtInRole string, cmd accesscontrol.SetResourcePermissionCommand) (*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 + var permission *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 + permission, err = s.setResourcePermission(sess, orgID, managedBuiltInRoleName(builtInRole), s.builtInRoleAdder(sess, orgID, builtInRole), cmd) + return err }) if err != nil { return nil, err } - return permissions, nil + return permission, 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) { +func (s *AccessControlStore) setResourcePermission( + sess *sqlstore.DBSession, orgID int64, roleName string, adder roleAdder, cmd accesscontrol.SetResourcePermissionCommand, +) (*accesscontrol.ResourcePermission, error) { role, err := s.getOrCreateManagedRole(sess, orgID, roleName, adder) if err != nil { return nil, err @@ -125,7 +141,7 @@ func (s *AccessControlStore) setResourcePermissions( return nil, err } - var permissions []accesscontrol.ResourcePermission + var permissions []flatResourcePermission for action := range missing { p, err := createResourcePermission(sess, role.ID, action, cmd.Resource, cmd.ResourceID) @@ -135,51 +151,17 @@ func (s *AccessControlStore) setResourcePermissions( permissions = append(permissions, *p) } - keptPermissions, err := getManagedPermissions(sess, cmd.ResourceID, keep) + keptPermissions, err := getResourcePermissions(sess, cmd.ResourceID, keep) if err != nil { return nil, err } - permissions = append(permissions, keptPermissions...) - return permissions, nil -} + permission := flatPermissionsToResourcePermission(append(permissions, keptPermissions...)) + if permission == nil { + return &accesscontrol.ResourcePermission{}, 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, - accesscontrol.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}) - }) + return permission, nil } func (s *AccessControlStore) GetResourcesPermissions(ctx context.Context, orgID int64, query accesscontrol.GetResourcesPermissionsQuery) ([]accesscontrol.ResourcePermission, error) { @@ -194,7 +176,7 @@ func (s *AccessControlStore) GetResourcesPermissions(ctx context.Context, orgID return result, err } -func createResourcePermission(sess *sqlstore.DBSession, roleID int64, action, resource string, resourceID string) (*accesscontrol.ResourcePermission, error) { +func createResourcePermission(sess *sqlstore.DBSession, roleID int64, action, resource string, resourceID string) (*flatResourcePermission, error) { permission := managedPermission(action, resource, resourceID) permission.RoleID = roleID permission.Created = time.Now() @@ -224,7 +206,7 @@ func createResourcePermission(sess *sqlstore.DBSession, roleID int64, action, re WHERE p.id = ? ` - p := &accesscontrol.ResourcePermission{} + p := &flatResourcePermission{} if _, err := sess.SQL(rawSql, resourceID, permission.ID).Get(p); err != nil { return nil, err } @@ -233,14 +215,12 @@ func createResourcePermission(sess *sqlstore.DBSession, roleID int64, action, re } 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 + return nil, nil } if len(query.ResourceIDs) == 0 { - return result, nil + return nil, nil } rawSelect := ` @@ -329,27 +309,109 @@ func getResourcesPermissions(sess *sqlstore.DBSession, orgID int64, query access builtin := builtinSelect + builtinFrom + where sql := user + "UNION" + team + "UNION" + builtin - if err := sess.SQL(sql, args...).Find(&result); err != nil { + queryResults := make([]flatResourcePermission, 0) + if err := sess.SQL(sql, args...).Find(&queryResults); err != nil { return nil, err } scopeAll := accesscontrol.GetResourceAllScope(query.Resource) scopeAllIDs := accesscontrol.GetResourceAllIDScope(query.Resource) - out := make([]accesscontrol.ResourcePermission, 0, len(result)) + byResource := make(map[string][]flatResourcePermission) // 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 := accesscontrol.GetResourceScope(query.Resource, id) - for _, p := range result { + for _, p := range queryResults { if p.Scope == scope || p.Scope == scopeAll || p.Scope == scopeAllIDs || p.Scope == "*" { p.ResourceID = id - out = append(out, p) + byResource[p.ResourceID] = append(byResource[p.ResourceID], p) } } } - return out, nil + var result []accesscontrol.ResourcePermission + for _, permissions := range byResource { + users, teams, builtins := groupPermissionsByAssignment(permissions) + for _, p := range users { + result = append(result, flatPermissionsToResourcePermissions(p)...) + } + for _, p := range teams { + result = append(result, flatPermissionsToResourcePermissions(p)...) + } + for _, p := range builtins { + result = append(result, flatPermissionsToResourcePermissions(p)...) + } + } + + return result, nil +} + +func groupPermissionsByAssignment(permissions []flatResourcePermission) (map[int64][]flatResourcePermission, map[int64][]flatResourcePermission, map[string][]flatResourcePermission) { + users := make(map[int64][]flatResourcePermission) + teams := make(map[int64][]flatResourcePermission) + builtins := make(map[string][]flatResourcePermission) + + for _, p := range permissions { + if p.UserId != 0 { + users[p.UserId] = append(users[p.UserId], p) + } else if p.TeamId != 0 { + teams[p.TeamId] = append(teams[p.TeamId], p) + } else if p.BuiltInRole != "" { + builtins[p.BuiltInRole] = append(builtins[p.BuiltInRole], p) + } + } + + return users, teams, builtins +} + +func flatPermissionsToResourcePermissions(permissions []flatResourcePermission) []accesscontrol.ResourcePermission { + var managed, provisioned []flatResourcePermission + for _, p := range permissions { + if p.Managed() { + managed = append(managed, p) + } else { + provisioned = append(provisioned, p) + } + } + + var result []accesscontrol.ResourcePermission + if g := flatPermissionsToResourcePermission(managed); g != nil { + result = append(result, *g) + } + if g := flatPermissionsToResourcePermission(provisioned); g != nil { + result = append(result, *g) + } + + return result +} + +func flatPermissionsToResourcePermission(permissions []flatResourcePermission) *accesscontrol.ResourcePermission { + if len(permissions) == 0 { + return nil + } + + actions := make([]string, 0, len(permissions)) + for _, p := range permissions { + actions = append(actions, p.Action) + } + + first := permissions[0] + return &accesscontrol.ResourcePermission{ + ID: first.ID, + ResourceID: first.ResourceID, + RoleName: first.RoleName, + Actions: actions, + Scope: first.Scope, + UserId: first.UserId, + UserLogin: first.UserLogin, + UserEmail: first.UserEmail, + TeamId: first.TeamId, + TeamEmail: first.TeamEmail, + Team: first.Team, + BuiltInRole: first.BuiltInRole, + Created: first.Created, + Updated: first.Updated, + } } func (s *AccessControlStore) userAdder(sess *sqlstore.DBSession, orgID, userID int64) roleAdder { @@ -393,7 +455,7 @@ func (s *AccessControlStore) teamAdder(sess *sqlstore.DBSession, orgID, teamID i } } -func (s *AccessControlStore) builtinRoleAdder(sess *sqlstore.DBSession, orgID int64, builtinRole string) roleAdder { +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 @@ -448,8 +510,8 @@ func (s *AccessControlStore) getOrCreateManagedRole(sess *sqlstore.DBSession, or return &role, nil } -func getManagedPermissions(sess *sqlstore.DBSession, resourceID string, ids []int64) ([]accesscontrol.ResourcePermission, error) { - var result []accesscontrol.ResourcePermission +func getResourcePermissions(sess *sqlstore.DBSession, resourceID string, ids []int64) ([]flatResourcePermission, error) { + var result []flatResourcePermission if len(ids) == 0 { return result, nil } @@ -502,6 +564,6 @@ 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 managedBuiltInRoleName(builtInRole string) string { + return fmt.Sprintf("managed:builtins:%s:permissions", strings.ToLower(builtInRole)) } diff --git a/pkg/services/accesscontrol/database/resource_bench_test.go b/pkg/services/accesscontrol/database/resource_permissions_bench_test.go similarity index 95% rename from pkg/services/accesscontrol/database/resource_bench_test.go rename to pkg/services/accesscontrol/database/resource_permissions_bench_test.go index ddc42d7f09d..f1cebca80f0 100644 --- a/pkg/services/accesscontrol/database/resource_bench_test.go +++ b/pkg/services/accesscontrol/database/resource_permissions_bench_test.go @@ -51,7 +51,7 @@ func benchmarkDSPermissions(b *testing.B, dsNum, usersNum int) { } } -func getDSPermissions(b *testing.B, store accesscontrol.ResourceStore, dataSources []int64) { +func getDSPermissions(b *testing.B, store accesscontrol.ResourcePermissionsStore, dataSources []int64) { dsId := dataSources[0] permissions, err := store.GetResourcesPermissions(context.Background(), accesscontrol.GlobalOrgID, accesscontrol.GetResourcesPermissionsQuery{ @@ -90,11 +90,11 @@ func GenerateDatasourcePermissions(b *testing.B, db *sqlstore.SQLStore, ac *Acce // Add DS permissions for the users maxPermissions := int(math.Min(float64(permissionsPerDs), float64(len(userIds)))) for i := 0; i < maxPermissions; i++ { - _, err := ac.SetUserResourcePermissions( + _, err := ac.SetUserResourcePermission( context.Background(), accesscontrol.GlobalOrgID, userIds[i], - accesscontrol.SetResourcePermissionsCommand{ + accesscontrol.SetResourcePermissionCommand{ Actions: []string{dsAction}, Resource: dsResource, ResourceID: strconv.Itoa(int(dsID)), @@ -106,11 +106,11 @@ func GenerateDatasourcePermissions(b *testing.B, db *sqlstore.SQLStore, ac *Acce // Add DS permissions for the teams maxPermissions = int(math.Min(float64(permissionsPerDs), float64(len(teamIds)))) for i := 0; i < maxPermissions; i++ { - _, err := ac.SetTeamResourcePermissions( + _, err := ac.SetTeamResourcePermission( context.Background(), accesscontrol.GlobalOrgID, teamIds[i], - accesscontrol.SetResourcePermissionsCommand{ + accesscontrol.SetResourcePermissionCommand{ Actions: []string{"datasources:query"}, Resource: "datasources", ResourceID: strconv.Itoa(int(dsID)), diff --git a/pkg/services/accesscontrol/database/resource_test.go b/pkg/services/accesscontrol/database/resource_permissions_test.go similarity index 57% rename from pkg/services/accesscontrol/database/resource_test.go rename to pkg/services/accesscontrol/database/resource_permissions_test.go index 9b822bc3509..67511b944b7 100644 --- a/pkg/services/accesscontrol/database/resource_test.go +++ b/pkg/services/accesscontrol/database/resource_permissions_test.go @@ -13,18 +13,18 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore" ) -type setUserResourcePermissionsTest struct { +type setUserResourcePermissionTest struct { desc string orgID int64 userID int64 actions []string resource string resourceID string - seeds []accesscontrol.SetResourcePermissionsCommand + seeds []accesscontrol.SetResourcePermissionCommand } -func TestAccessControlStore_SetUserResourcePermissions(t *testing.T) { - tests := []setUserResourcePermissionsTest{ +func TestAccessControlStore_SetUserResourcePermission(t *testing.T) { + tests := []setUserResourcePermissionTest{ { desc: "should set resource permission for user", userID: 1, @@ -39,7 +39,7 @@ func TestAccessControlStore_SetUserResourcePermissions(t *testing.T) { actions: []string{}, resource: "datasources", resourceID: "1", - seeds: []accesscontrol.SetResourcePermissionsCommand{ + seeds: []accesscontrol.SetResourcePermissionCommand{ { Actions: []string{"datasources:query"}, Resource: "datasources", @@ -54,7 +54,7 @@ func TestAccessControlStore_SetUserResourcePermissions(t *testing.T) { actions: []string{"datasources:query", "datasources:write"}, resource: "datasources", resourceID: "1", - seeds: []accesscontrol.SetResourcePermissionsCommand{ + seeds: []accesscontrol.SetResourcePermissionCommand{ { Actions: []string{"datasources:write"}, Resource: "datasources", @@ -69,37 +69,39 @@ func TestAccessControlStore_SetUserResourcePermissions(t *testing.T) { store, _ := setupTestEnv(t) for _, s := range test.seeds { - _, err := store.SetUserResourcePermissions(context.Background(), test.orgID, test.userID, s) + _, err := store.SetUserResourcePermission(context.Background(), test.orgID, test.userID, s) require.NoError(t, err) } - added, err := store.SetUserResourcePermissions(context.Background(), test.userID, test.userID, accesscontrol.SetResourcePermissionsCommand{ + added, err := store.SetUserResourcePermission(context.Background(), test.userID, test.userID, accesscontrol.SetResourcePermissionCommand{ 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, accesscontrol.GetResourceScope(test.resource, test.resourceID), p.Scope) + if len(test.actions) == 0 { + assert.Equal(t, accesscontrol.ResourcePermission{}, *added) + } else { + assert.Len(t, added.Actions, len(test.actions)) + assert.Equal(t, accesscontrol.GetResourceScope(test.resource, test.resourceID), added.Scope) } }) } } -type setTeamResourcePermissionsTest struct { +type setTeamResourcePermissionTest struct { desc string orgID int64 teamID int64 actions []string resource string resourceID string - seeds []accesscontrol.SetResourcePermissionsCommand + seeds []accesscontrol.SetResourcePermissionCommand } -func TestAccessControlStore_SetTeamResourcePermissions(t *testing.T) { - tests := []setTeamResourcePermissionsTest{ +func TestAccessControlStore_SetTeamResourcePermission(t *testing.T) { + tests := []setTeamResourcePermissionTest{ { desc: "should add new resource permission for team", orgID: 1, @@ -115,7 +117,7 @@ func TestAccessControlStore_SetTeamResourcePermissions(t *testing.T) { actions: []string{"datasources:query", "datasources:write"}, resource: "datasources", resourceID: "1", - seeds: []accesscontrol.SetResourcePermissionsCommand{ + seeds: []accesscontrol.SetResourcePermissionCommand{ { Actions: []string{"datasources:query"}, Resource: "datasources", @@ -130,7 +132,7 @@ func TestAccessControlStore_SetTeamResourcePermissions(t *testing.T) { actions: []string{}, resource: "datasources", resourceID: "1", - seeds: []accesscontrol.SetResourcePermissionsCommand{ + seeds: []accesscontrol.SetResourcePermissionCommand{ { Actions: []string{"datasources:query"}, Resource: "datasources", @@ -145,41 +147,43 @@ func TestAccessControlStore_SetTeamResourcePermissions(t *testing.T) { store, _ := setupTestEnv(t) for _, s := range test.seeds { - _, err := store.SetTeamResourcePermissions(context.Background(), test.orgID, test.teamID, s) + _, err := store.SetTeamResourcePermission(context.Background(), test.orgID, test.teamID, s) require.NoError(t, err) } - added, err := store.SetTeamResourcePermissions(context.Background(), test.orgID, test.teamID, accesscontrol.SetResourcePermissionsCommand{ + added, err := store.SetTeamResourcePermission(context.Background(), test.orgID, test.teamID, accesscontrol.SetResourcePermissionCommand{ 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, accesscontrol.GetResourceScope(test.resource, test.resourceID), p.Scope) + if len(test.actions) == 0 { + assert.Equal(t, accesscontrol.ResourcePermission{}, *added) + } else { + assert.Len(t, added.Actions, len(test.actions)) + assert.Equal(t, accesscontrol.GetResourceScope(test.resource, test.resourceID), added.Scope) } }) } } -type setBuiltinResourcePermissionsTest struct { +type setBuiltInResourcePermissionTest struct { desc string orgID int64 - builtinRole string + builtInRole string actions []string resource string resourceID string - seeds []accesscontrol.SetResourcePermissionsCommand + seeds []accesscontrol.SetResourcePermissionCommand } -func TestAccessControlStore_SetBuiltinResourcePermissions(t *testing.T) { - tests := []setBuiltinResourcePermissionsTest{ +func TestAccessControlStore_SetBuiltInResourcePermission(t *testing.T) { + tests := []setBuiltInResourcePermissionTest{ { desc: "should add new resource permission for builtin role", orgID: 1, - builtinRole: "Viewer", + builtInRole: "Viewer", actions: []string{"datasources:query"}, resource: "datasources", resourceID: "1", @@ -187,11 +191,11 @@ func TestAccessControlStore_SetBuiltinResourcePermissions(t *testing.T) { { desc: "should add new resource permission when others exist", orgID: 1, - builtinRole: "Viewer", + builtInRole: "Viewer", actions: []string{"datasources:query", "datasources:write"}, resource: "datasources", resourceID: "1", - seeds: []accesscontrol.SetResourcePermissionsCommand{ + seeds: []accesscontrol.SetResourcePermissionCommand{ { Actions: []string{"datasources:query"}, Resource: "datasources", @@ -202,11 +206,11 @@ func TestAccessControlStore_SetBuiltinResourcePermissions(t *testing.T) { { desc: "should remove permissions for builtin role", orgID: 1, - builtinRole: "Viewer", + builtInRole: "Viewer", actions: []string{}, resource: "datasources", resourceID: "1", - seeds: []accesscontrol.SetResourcePermissionsCommand{ + seeds: []accesscontrol.SetResourcePermissionCommand{ { Actions: []string{"datasources:query"}, Resource: "datasources", @@ -221,104 +225,22 @@ func TestAccessControlStore_SetBuiltinResourcePermissions(t *testing.T) { store, _ := setupTestEnv(t) for _, s := range test.seeds { - _, err := store.SetBuiltinResourcePermissions(context.Background(), test.orgID, test.builtinRole, s) + _, err := store.SetBuiltInResourcePermission(context.Background(), test.orgID, test.builtInRole, s) require.NoError(t, err) } - added, err := store.SetBuiltinResourcePermissions(context.Background(), test.orgID, test.builtinRole, accesscontrol.SetResourcePermissionsCommand{ + added, err := store.SetBuiltInResourcePermission(context.Background(), test.orgID, test.builtInRole, accesscontrol.SetResourcePermissionCommand{ 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, accesscontrol.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) + if len(test.actions) == 0 { + assert.Equal(t, accesscontrol.ResourcePermission{}, *added) } 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) - } + assert.Len(t, added.Actions, len(test.actions)) + assert.Equal(t, accesscontrol.GetResourceScope(test.resource, test.resourceID), added.Scope) } }) } @@ -381,7 +303,7 @@ func seedResourcePermissions(t *testing.T, store *AccessControlStore, sql *sqlst }) require.NoError(t, err) - _, err = store.SetUserResourcePermissions(context.Background(), 1, u.Id, accesscontrol.SetResourcePermissionsCommand{ + _, err = store.SetUserResourcePermission(context.Background(), 1, u.Id, accesscontrol.SetResourcePermissionCommand{ Actions: actions, Resource: resource, ResourceID: resourceID, diff --git a/pkg/services/accesscontrol/middleware/middleware.go b/pkg/services/accesscontrol/middleware/middleware.go index b93bf8c8980..8df194ea0c4 100644 --- a/pkg/services/accesscontrol/middleware/middleware.go +++ b/pkg/services/accesscontrol/middleware/middleware.go @@ -138,6 +138,16 @@ func UseGlobalOrg(c *models.ReqContext) (int64, error) { return accesscontrol.GlobalOrgID, nil } +// Disable returns http 404 if shouldDisable is set to true +func Disable(shouldDisable bool) web.Handler { + return func(c *models.ReqContext) { + if shouldDisable { + c.Resp.WriteHeader(http.StatusNotFound) + return + } + } +} + func LoadPermissionsMiddleware(ac accesscontrol.AccessControl) web.Handler { return func(c *models.ReqContext) { if ac.IsDisabled() { diff --git a/pkg/services/accesscontrol/models.go b/pkg/services/accesscontrol/models.go index b505adbc5cc..f618610b63e 100644 --- a/pkg/services/accesscontrol/models.go +++ b/pkg/services/accesscontrol/models.go @@ -187,11 +187,13 @@ type ScopeParams struct { URLParams map[string]string } +// ResourcePermission is structure that holds all actions that either a team / user / builtin-role +// can perform against specific resource. type ResourcePermission struct { - ID int64 `xorm:"id"` - ResourceID string `xorm:"resource_id"` + ID int64 + ResourceID string RoleName string - Action string + Actions []string Scope string UserId int64 UserLogin string @@ -204,23 +206,39 @@ type ResourcePermission struct { Updated time.Time } -func (p *ResourcePermission) Managed() bool { +func (p *ResourcePermission) IsManaged() bool { return strings.HasPrefix(p.RoleName, "managed:") } -type SetResourcePermissionsCommand struct { +func (p *ResourcePermission) Contains(targetActions []string) bool { + if len(p.Actions) < len(targetActions) { + return false + } + + var contain = func(arr []string, s string) bool { + for _, item := range arr { + if item == s { + return true + } + } + return false + } + + for _, a := range targetActions { + if !contain(p.Actions, a) { + return false + } + } + + return true +} + +type SetResourcePermissionCommand 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 diff --git a/pkg/services/accesscontrol/resource_manager.go b/pkg/services/accesscontrol/resource_manager.go deleted file mode 100644 index 7b22bcda8ac..00000000000 --- a/pkg/services/accesscontrol/resource_manager.go +++ /dev/null @@ -1,109 +0,0 @@ -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/accesscontrol/resourcepermissions/api.go b/pkg/services/accesscontrol/resourcepermissions/api.go new file mode 100644 index 00000000000..ed1b5337410 --- /dev/null +++ b/pkg/services/accesscontrol/resourcepermissions/api.go @@ -0,0 +1,178 @@ +package resourcepermissions + +import ( + "fmt" + "net/http" + + "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/middleware" + "github.com/grafana/grafana/pkg/web" +) + +type api struct { + ac accesscontrol.AccessControl + router routing.RouteRegister + service *Service + permissions []string +} + +func newApi(ac accesscontrol.AccessControl, router routing.RouteRegister, manager *Service) *api { + permissions := make([]string, 0, len(manager.permissions)) + // reverse the permissions order for display + for i := len(manager.permissions) - 1; i >= 0; i-- { + permissions = append(permissions, manager.permissions[i]) + } + return &api{ac, router, manager, permissions} +} + +func (a *api) registerEndpoints() { + auth := middleware.Middleware(a.ac) + disable := middleware.Disable(a.ac.IsDisabled()) + a.router.Group(fmt.Sprintf("/api/access-control/%s", a.service.options.Resource), func(r routing.RouteRegister) { + idScope := accesscontrol.Scope(a.service.options.Resource, "id", accesscontrol.Parameter(":resourceID")) + actionWrite, actionRead := fmt.Sprintf("%s.permissions:write", a.service.options.Resource), fmt.Sprintf("%s.permissions:read", a.service.options.Resource) + r.Get("/description", auth(disable, accesscontrol.EvalPermission(actionRead)), routing.Wrap(a.getDescription)) + r.Get("/:resourceID", auth(disable, accesscontrol.EvalPermission(actionRead, idScope)), routing.Wrap(a.getPermissions)) + r.Post("/:resourceID/users/:userID", auth(disable, accesscontrol.EvalPermission(actionWrite, idScope)), routing.Wrap(a.setUserPermission)) + r.Post("/:resourceID/teams/:teamID", auth(disable, accesscontrol.EvalPermission(actionWrite, idScope)), routing.Wrap(a.setTeamPermission)) + r.Post("/:resourceID/builtInRoles/:builtInRole", auth(disable, accesscontrol.EvalPermission(actionWrite, idScope)), routing.Wrap(a.setBuiltinRolePermission)) + }) +} + +type Assignments struct { + Users bool `json:"users"` + Teams bool `json:"teams"` + BuiltInRoles bool `json:"builtInRoles"` +} + +type Description struct { + Assignments Assignments `json:"assignments"` + Permissions []string `json:"permissions"` +} + +func (a *api) getDescription(c *models.ReqContext) response.Response { + return response.JSON(http.StatusOK, &Description{ + Permissions: a.permissions, + Assignments: a.service.options.Assignments, + }) +} + +type resourcePermissionDTO struct { + ID int64 `json:"id"` + ResourceID string `json:"resourceId"` + RoleName string `json:"roleName"` + IsManaged bool `json:"isManaged"` + UserID int64 `json:"userId,omitempty"` + UserLogin string `json:"userLogin,omitempty"` + UserAvatarUrl string `json:"userAvatarUrl,omitempty"` + Team string `json:"team,omitempty"` + TeamID int64 `json:"teamId,omitempty"` + TeamAvatarUrl string `json:"teamAvatarUrl,omitempty"` + BuiltInRole string `json:"builtInRole,omitempty"` + Actions []string `json:"actions"` + Permission string `json:"permission"` +} + +func (a *api) getPermissions(c *models.ReqContext) response.Response { + resourceID := web.Params(c.Req)[":resourceID"] + + permissions, err := a.service.GetPermissions(c.Req.Context(), c.OrgId, resourceID) + if err != nil { + return response.Error(http.StatusInternalServerError, "failed to get permissions", err) + } + + dto := make([]resourcePermissionDTO, 0, len(permissions)) + for _, p := range permissions { + if permission := a.service.MapActions(p); permission != "" { + teamAvatarUrl := "" + if p.TeamId != 0 { + teamAvatarUrl = dtos.GetGravatarUrlWithDefault(p.TeamEmail, p.Team) + } + + dto = append(dto, resourcePermissionDTO{ + ID: p.ID, + ResourceID: p.ResourceID, + RoleName: p.RoleName, + IsManaged: p.IsManaged(), + UserID: p.UserId, + UserLogin: p.UserLogin, + UserAvatarUrl: dtos.GetGravatarUrl(p.UserEmail), + Team: p.Team, + TeamID: p.TeamId, + TeamAvatarUrl: teamAvatarUrl, + BuiltInRole: p.BuiltInRole, + Actions: p.Actions, + Permission: permission, + }) + } + } + + return response.JSON(http.StatusOK, dto) +} + +type setPermissionCommand struct { + Permission string `json:"permission"` +} + +func (a *api) setUserPermission(c *models.ReqContext) response.Response { + userID := c.ParamsInt64(":userID") + resourceID := web.Params(c.Req)[":resourceID"] + + var cmd setPermissionCommand + if err := web.Bind(c.Req, &cmd); err != nil { + return response.Error(http.StatusBadRequest, "bad request data", err) + } + + _, err := a.service.SetUserPermission(c.Req.Context(), c.OrgId, userID, resourceID, a.service.MapPermission(cmd.Permission)) + if err != nil { + return response.Error(http.StatusBadRequest, "failed to set user permission", err) + } + + return permissionSetResponse(cmd) +} + +func (a *api) setTeamPermission(c *models.ReqContext) response.Response { + teamID := c.ParamsInt64(":teamID") + resourceID := web.Params(c.Req)[":resourceID"] + + var cmd setPermissionCommand + if err := web.Bind(c.Req, &cmd); err != nil { + return response.Error(http.StatusBadRequest, "bad request data", err) + } + + _, err := a.service.SetTeamPermission(c.Req.Context(), c.OrgId, teamID, resourceID, a.service.MapPermission(cmd.Permission)) + if err != nil { + return response.Error(http.StatusBadRequest, "failed to set team permission", err) + } + + return permissionSetResponse(cmd) +} + +func (a *api) setBuiltinRolePermission(c *models.ReqContext) response.Response { + builtInRole := web.Params(c.Req)[":builtInRole"] + resourceID := web.Params(c.Req)[":resourceID"] + + cmd := setPermissionCommand{} + if err := web.Bind(c.Req, &cmd); err != nil { + return response.Error(http.StatusBadRequest, "bad request data", err) + } + + _, err := a.service.SetBuiltInRolePermission(c.Req.Context(), c.OrgId, builtInRole, resourceID, a.service.MapPermission(cmd.Permission)) + if err != nil { + return response.Error(http.StatusBadRequest, "failed to set role permission", err) + } + + return permissionSetResponse(cmd) +} + +func permissionSetResponse(cmd setPermissionCommand) response.Response { + message := "Permission updated" + if cmd.Permission == "" { + message = "Permission removed" + } + return response.Success(message) +} diff --git a/pkg/services/accesscontrol/resourcepermissions/api_test.go b/pkg/services/accesscontrol/resourcepermissions/api_test.go new file mode 100644 index 00000000000..cd65799a786 --- /dev/null +++ b/pkg/services/accesscontrol/resourcepermissions/api_test.go @@ -0,0 +1,487 @@ +package resourcepermissions + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "path" + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/accesscontrol/database" + accesscontrolmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/web" +) + +type getDescriptionTestCase struct { + desc string + options Options + permissions []*accesscontrol.Permission + expected Description + expectedStatus int +} + +func TestApi_getDescription(t *testing.T) { + tests := []getDescriptionTestCase{ + { + desc: "should return description", + options: Options{ + Resource: "dashboards", + Assignments: Assignments{ + Users: true, + Teams: true, + BuiltInRoles: true, + }, + PermissionsToActions: map[string][]string{ + "View": {"dashboards:read"}, + "Edit": {"dashboards:read", "dashboards:write", "dashboards:delete"}, + "Admin": {"dashboards:read", "dashboards:write", "dashboards:delete", "dashboards.permissions:read", "dashboards:permissions:write"}, + }, + }, + permissions: []*accesscontrol.Permission{ + {Action: "dashboards.permissions:read"}, + }, + expected: Description{ + Assignments: Assignments{ + Users: true, + Teams: true, + BuiltInRoles: true, + }, + Permissions: []string{"View", "Edit", "Admin"}, + }, + expectedStatus: http.StatusOK, + }, + { + desc: "should only return user assignment", + options: Options{ + Resource: "dashboards", + Assignments: Assignments{ + Users: true, + Teams: false, + BuiltInRoles: false, + }, + PermissionsToActions: map[string][]string{ + "View": {"dashboards:read"}, + }, + }, + permissions: []*accesscontrol.Permission{ + {Action: "dashboards.permissions:read"}, + }, + expected: Description{ + Assignments: Assignments{ + Users: true, + Teams: false, + BuiltInRoles: false, + }, + Permissions: []string{"View"}, + }, + expectedStatus: http.StatusOK, + }, + { + desc: "should return 403 when missing read permission", + options: Options{ + Resource: "dashboards", + Assignments: Assignments{ + Users: true, + Teams: false, + BuiltInRoles: false, + }, + PermissionsToActions: map[string][]string{ + "View": {"dashboards:read"}, + }, + }, + permissions: []*accesscontrol.Permission{}, + expected: Description{}, + expectedStatus: http.StatusForbidden, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + _, server, _ := setupTestEnvironment(t, &models.SignedInUser{}, tt.permissions, tt.options) + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/api/access-control/%s/description", tt.options.Resource), nil) + require.NoError(t, err) + recorder := httptest.NewRecorder() + server.ServeHTTP(recorder, req) + + got := Description{} + require.NoError(t, json.NewDecoder(recorder.Body).Decode(&got)) + assert.Equal(t, tt.expected, got) + if tt.expectedStatus == http.StatusOK { + assert.Equal(t, tt.expectedStatus, recorder.Code) + } + }) + } +} + +type getPermissionsTestCase struct { + desc string + resourceID string + permissions []*accesscontrol.Permission + expectedStatus int +} + +func TestApi_getPermissions(t *testing.T) { + tests := []getPermissionsTestCase{ + { + desc: "expect permissions for resource with id 1", + resourceID: "1", + permissions: []*accesscontrol.Permission{{Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}}, + expectedStatus: 200, + }, + { + desc: "expect http status 403 when missing permission", + resourceID: "1", + permissions: []*accesscontrol.Permission{}, + expectedStatus: 403, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + service, server, sql := setupTestEnvironment(t, &models.SignedInUser{OrgId: 1}, tt.permissions, testOptions) + + // seed team 1 with "Edit" permission on dashboard 1 + team, err := sql.CreateTeam("test", "test@test.com", 1) + require.NoError(t, err) + _, err = service.SetTeamPermission(context.Background(), team.OrgId, team.Id, tt.resourceID, []string{"dashboards:read", "dashboards:write", "dashboards:delete"}) + require.NoError(t, err) + // seed user 1 with "View" permission on dashboard 1 + u, err := sql.CreateUser(context.Background(), models.CreateUserCommand{Login: "test", OrgId: 1}) + require.NoError(t, err) + _, err = service.SetUserPermission(context.Background(), u.OrgId, u.Id, tt.resourceID, []string{"dashboards:read"}) + require.NoError(t, err) + + // seed built in role Admin with "View" permission on dashboard 1 + _, err = service.SetBuiltInRolePermission(context.Background(), 1, "Admin", tt.resourceID, []string{"dashboards:read", "dashboards:write", "dashboards:delete"}) + require.NoError(t, err) + + permissions, recorder := getPermission(t, server, testOptions.Resource, tt.resourceID) + assert.Equal(t, tt.expectedStatus, recorder.Code) + + if tt.expectedStatus == http.StatusOK { + assert.Len(t, permissions, 3, "expected three assignments: user, team, builtin") + for _, p := range permissions { + if p.UserID != 0 { + assert.Equal(t, "View", p.Permission) + } else if p.TeamID != 0 { + assert.Equal(t, "Edit", p.Permission) + } else { + assert.Equal(t, "Edit", p.Permission) + } + } + } + }) + } +} + +type setBuiltinPermissionTestCase struct { + desc string + resourceID string + builtInRole string + expectedStatus int + permission string + permissions []*accesscontrol.Permission +} + +func TestApi_setBuiltinRolePermission(t *testing.T) { + tests := []setBuiltinPermissionTestCase{ + { + desc: "should set Edit permission for Viewer", + resourceID: "1", + builtInRole: "Viewer", + expectedStatus: 200, + permission: "Edit", + permissions: []*accesscontrol.Permission{ + {Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}, + {Action: "dashboards.permissions:write", Scope: "dashboards:id:1"}, + }, + }, + { + desc: "should set View permission for Admin", + resourceID: "1", + builtInRole: "Admin", + expectedStatus: 200, + permission: "View", + permissions: []*accesscontrol.Permission{ + {Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}, + {Action: "dashboards.permissions:write", Scope: "dashboards:id:1"}, + }, + }, + { + desc: "should return http 400 for invalid built in role", + resourceID: "1", + builtInRole: "Invalid", + expectedStatus: http.StatusBadRequest, + permission: "View", + permissions: []*accesscontrol.Permission{ + {Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}, + {Action: "dashboards.permissions:write", Scope: "dashboards:id:1"}, + }, + }, + { + desc: "should set return http 403 when missing permissions", + resourceID: "1", + builtInRole: "Invalid", + expectedStatus: http.StatusForbidden, + permission: "View", + permissions: []*accesscontrol.Permission{ + {Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + _, server, _ := setupTestEnvironment(t, &models.SignedInUser{OrgId: 1}, tt.permissions, testOptions) + + recorder := setPermission(t, server, testOptions.Resource, tt.resourceID, tt.permission, "builtInRoles", tt.builtInRole) + assert.Equal(t, tt.expectedStatus, recorder.Code) + + if tt.expectedStatus == http.StatusOK { + permissions, _ := getPermission(t, server, testOptions.Resource, tt.resourceID) + require.Len(t, permissions, 1) + assert.Equal(t, tt.permission, permissions[0].Permission) + assert.Equal(t, tt.builtInRole, permissions[0].BuiltInRole) + } + }) + } +} + +type setTeamPermissionTestCase struct { + desc string + teamID int64 + resourceID string + expectedStatus int + permission string + permissions []*accesscontrol.Permission +} + +func TestApi_setTeamPermission(t *testing.T) { + tests := []setTeamPermissionTestCase{ + { + desc: "should set Edit permission for team 1", + teamID: 1, + resourceID: "1", + expectedStatus: 200, + permission: "Edit", + permissions: []*accesscontrol.Permission{ + {Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}, + {Action: "dashboards.permissions:write", Scope: "dashboards:id:1"}, + }, + }, + { + desc: "should set View permission for team 1", + teamID: 1, + resourceID: "1", + expectedStatus: 200, + permission: "View", + permissions: []*accesscontrol.Permission{ + {Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}, + {Action: "dashboards.permissions:write", Scope: "dashboards:id:1"}, + }, + }, + { + desc: "should set return http 400 when team does not exist", + teamID: 2, + resourceID: "1", + expectedStatus: http.StatusBadRequest, + permission: "View", + permissions: []*accesscontrol.Permission{ + {Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}, + {Action: "dashboards.permissions:write", Scope: "dashboards:id:1"}, + }, + }, + { + desc: "should set return http 403 when missing permissions", + teamID: 2, + resourceID: "1", + expectedStatus: http.StatusForbidden, + permission: "View", + permissions: []*accesscontrol.Permission{ + {Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + _, server, sql := setupTestEnvironment(t, &models.SignedInUser{OrgId: 1}, tt.permissions, testOptions) + + // seed team + _, err := sql.CreateTeam("test", "test@test.com", 1) + require.NoError(t, err) + + recorder := setPermission(t, server, testOptions.Resource, tt.resourceID, tt.permission, "teams", strconv.Itoa(int(tt.teamID))) + assert.Equal(t, tt.expectedStatus, recorder.Code) + + assert.Equal(t, tt.expectedStatus, recorder.Code) + if tt.expectedStatus == http.StatusOK { + permissions, _ := getPermission(t, server, testOptions.Resource, tt.resourceID) + require.Len(t, permissions, 1) + assert.Equal(t, tt.permission, permissions[0].Permission) + assert.Equal(t, tt.teamID, permissions[0].TeamID) + } + }) + } +} + +type setUserPermissionTestCase struct { + desc string + userID int64 + resourceID string + expectedStatus int + permission string + permissions []*accesscontrol.Permission +} + +func TestApi_setUserPermission(t *testing.T) { + tests := []setUserPermissionTestCase{ + { + desc: "should set Edit permission for user 1", + userID: 1, + resourceID: "1", + expectedStatus: 200, + permission: "Edit", + permissions: []*accesscontrol.Permission{ + {Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}, + {Action: "dashboards.permissions:write", Scope: "dashboards:id:1"}, + }, + }, + { + desc: "should set View permission for user 1", + userID: 1, + resourceID: "1", + expectedStatus: 200, + permission: "View", + permissions: []*accesscontrol.Permission{ + {Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}, + {Action: "dashboards.permissions:write", Scope: "dashboards:id:1"}, + }, + }, + { + desc: "should set return http 400 when user does not exist", + userID: 2, + resourceID: "1", + expectedStatus: http.StatusBadRequest, + permission: "View", + permissions: []*accesscontrol.Permission{ + {Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}, + {Action: "dashboards.permissions:write", Scope: "dashboards:id:1"}, + }, + }, + { + desc: "should set return http 403 when missing permissions", + userID: 2, + resourceID: "1", + expectedStatus: http.StatusForbidden, + permission: "View", + permissions: []*accesscontrol.Permission{ + {Action: "dashboards.permissions:read", Scope: "dashboards:id:1"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + _, server, sql := setupTestEnvironment(t, &models.SignedInUser{OrgId: 1}, tt.permissions, testOptions) + + // seed team + _, err := sql.CreateUser(context.Background(), models.CreateUserCommand{Login: "test", OrgId: 1}) + require.NoError(t, err) + + recorder := setPermission(t, server, testOptions.Resource, tt.resourceID, tt.permission, "users", strconv.Itoa(int(tt.userID))) + assert.Equal(t, tt.expectedStatus, recorder.Code) + + assert.Equal(t, tt.expectedStatus, recorder.Code) + if tt.expectedStatus == http.StatusOK { + permissions, _ := getPermission(t, server, testOptions.Resource, tt.resourceID) + require.Len(t, permissions, 1) + assert.Equal(t, tt.permission, permissions[0].Permission) + assert.Equal(t, tt.userID, permissions[0].UserID) + } + }) + } +} + +func setupTestEnvironment(t *testing.T, user *models.SignedInUser, permissions []*accesscontrol.Permission, ops Options) (*Service, *web.Mux, *sqlstore.SQLStore) { + sql := sqlstore.InitTestDB(t) + store := database.ProvideService(sql) + + service, err := New(ops, routing.NewRouteRegister(), accesscontrolmock.New().WithPermissions(permissions), store) + require.NoError(t, err) + + server := web.New() + server.UseMiddleware(web.Renderer(path.Join(setting.StaticRootPath, "views"), "[[", "]]")) + server.Use(contextProvider(&testContext{user})) + service.api.router.Register(server) + + return service, server, sql +} + +type testContext struct { + user *models.SignedInUser +} + +func contextProvider(tc *testContext) web.Handler { + return func(c *web.Context) { + signedIn := tc.user != nil + reqCtx := &models.ReqContext{ + Context: c, + SignedInUser: tc.user, + IsSignedIn: signedIn, + SkipCache: true, + Logger: log.New("test"), + } + c.Map(reqCtx) + } +} + +var testOptions = Options{ + Resource: "dashboards", + Assignments: Assignments{ + Users: true, + Teams: true, + BuiltInRoles: true, + }, + PermissionsToActions: map[string][]string{ + "View": {"dashboards:read"}, + "Edit": {"dashboards:read", "dashboards:write", "dashboards:delete"}, + }, +} + +func getPermission(t *testing.T, server *web.Mux, resource, resourceID string) ([]resourcePermissionDTO, *httptest.ResponseRecorder) { + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/api/access-control/%s/%s", resource, resourceID), nil) + require.NoError(t, err) + recorder := httptest.NewRecorder() + server.ServeHTTP(recorder, req) + + var permissions []resourcePermissionDTO + if recorder.Code == http.StatusOK { + require.NoError(t, json.NewDecoder(recorder.Body).Decode(&permissions)) + } + return permissions, recorder +} + +func setPermission(t *testing.T, server *web.Mux, resource, resourceID, permission, assignment, assignTo string) *httptest.ResponseRecorder { + body := strings.NewReader(fmt.Sprintf(`{"permission": "%s"}`, permission)) + req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("/api/access-control/%s/%s/%s/%s", resource, resourceID, assignment, assignTo), body) + require.NoError(t, err) + req.Header.Set("Content-Type", "application/json") + recorder := httptest.NewRecorder() + server.ServeHTTP(recorder, req) + + return recorder +} diff --git a/pkg/services/accesscontrol/resourcepermissions/error.go b/pkg/services/accesscontrol/resourcepermissions/error.go new file mode 100644 index 00000000000..1c155f72f58 --- /dev/null +++ b/pkg/services/accesscontrol/resourcepermissions/error.go @@ -0,0 +1,8 @@ +package resourcepermissions + +import "errors" + +var ( + ErrInvalidActions = errors.New("invalid actions") + ErrInvalidAssignment = errors.New("invalid assignment") +) diff --git a/pkg/services/accesscontrol/resourcepermissions/options.go b/pkg/services/accesscontrol/resourcepermissions/options.go new file mode 100644 index 00000000000..4697389126a --- /dev/null +++ b/pkg/services/accesscontrol/resourcepermissions/options.go @@ -0,0 +1,27 @@ +package resourcepermissions + +import ( + "context" +) + +type ResourceValidator func(ctx context.Context, orgID int64, resourceID string) error + +type Options struct { + // Resource is the action and scope prefix that is generated + Resource string + // ResourceValidator is a validator function that will be called before each assignment. + // If set to nil the validator will be skipped + ResourceValidator ResourceValidator + // Assignments decides what we can assign permissions to (users/teams/builtInRoles) + Assignments Assignments + // PermissionsToAction is a map of friendly named permissions and what access control actions they should generate. + // E.g. Edit permissions should generate dashboards:read, dashboards:write and dashboards:delete + PermissionsToActions map[string][]string + + // ReaderRoleName is the display name for the generated fixed reader role + ReaderRoleName string + // WriterRoleName is the display name for the generated fixed writer role + WriterRoleName string + // RoleGroup is the group name for the generated fixed roles + RoleGroup string +} diff --git a/pkg/services/accesscontrol/resourcepermissions/service.go b/pkg/services/accesscontrol/resourcepermissions/service.go new file mode 100644 index 00000000000..8084d630c1c --- /dev/null +++ b/pkg/services/accesscontrol/resourcepermissions/service.go @@ -0,0 +1,230 @@ +package resourcepermissions + +import ( + "context" + "fmt" + "sort" + + "github.com/grafana/grafana/pkg/services/sqlstore" + + "github.com/grafana/grafana/pkg/api/routing" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/accesscontrol" +) + +func New(options Options, router routing.RouteRegister, ac accesscontrol.AccessControl, store accesscontrol.ResourcePermissionsStore) (*Service, error) { + var permissions []string + validActions := make(map[string]struct{}) + for permission, actions := range options.PermissionsToActions { + permissions = append(permissions, permission) + for _, a := range actions { + validActions[a] = struct{}{} + } + } + + // Sort all permissions based on action length. Will be used when mapping between actions to permissions + sort.Slice(permissions, func(i, j int) bool { + return len(options.PermissionsToActions[permissions[i]]) > len(options.PermissionsToActions[permissions[j]]) + }) + + actions := make([]string, 0, len(validActions)) + for action := range validActions { + actions = append(actions, action) + } + + s := &Service{ + ac: ac, + store: store, + options: options, + permissions: permissions, + actions: actions, + validActions: validActions, + } + + s.api = newApi(ac, router, s) + + if err := s.declareFixedRoles(); err != nil { + return nil, err + } + + s.api.registerEndpoints() + + return s, nil +} + +// Service is used to create access control sub system including api / and service for managed resource permission +type Service struct { + ac accesscontrol.AccessControl + store accesscontrol.ResourcePermissionsStore + api *api + + options Options + permissions []string + actions []string + validActions map[string]struct{} +} + +func (s *Service) GetPermissions(ctx context.Context, orgID int64, resourceID string) ([]accesscontrol.ResourcePermission, error) { + return s.store.GetResourcesPermissions(ctx, orgID, accesscontrol.GetResourcesPermissionsQuery{ + Actions: s.actions, + Resource: s.options.Resource, + ResourceIDs: []string{resourceID}, + }) +} + +func (s *Service) SetUserPermission(ctx context.Context, orgID, userID int64, resourceID string, actions []string) (*accesscontrol.ResourcePermission, error) { + if !s.options.Assignments.Users { + return nil, ErrInvalidAssignment + } + + if !s.validateActions(actions) { + return nil, ErrInvalidActions + } + + if err := s.validateResource(ctx, orgID, resourceID); err != nil { + return nil, err + } + + if err := s.validateUser(ctx, orgID, userID); err != nil { + return nil, err + } + + return s.store.SetUserResourcePermission(ctx, orgID, userID, accesscontrol.SetResourcePermissionCommand{ + Actions: actions, + ResourceID: resourceID, + Resource: s.options.Resource, + }) +} + +func (s *Service) SetTeamPermission(ctx context.Context, orgID, teamID int64, resourceID string, actions []string) (*accesscontrol.ResourcePermission, error) { + if !s.options.Assignments.Teams { + return nil, ErrInvalidAssignment + } + if !s.validateActions(actions) { + return nil, ErrInvalidActions + } + + if err := s.validateTeam(ctx, orgID, teamID); err != nil { + return nil, err + } + + if err := s.validateResource(ctx, orgID, resourceID); err != nil { + return nil, err + } + + return s.store.SetTeamResourcePermission(ctx, orgID, teamID, accesscontrol.SetResourcePermissionCommand{ + Actions: actions, + ResourceID: resourceID, + Resource: s.options.Resource, + }) +} + +func (s *Service) SetBuiltInRolePermission(ctx context.Context, orgID int64, builtInRole string, resourceID string, actions []string) (*accesscontrol.ResourcePermission, error) { + if !s.options.Assignments.BuiltInRoles { + return nil, ErrInvalidAssignment + } + + if !s.validateActions(actions) { + return nil, ErrInvalidActions + } + + if err := s.validateBuiltinRole(ctx, builtInRole); err != nil { + return nil, err + } + + if err := s.validateResource(ctx, orgID, resourceID); err != nil { + return nil, err + } + + return s.store.SetBuiltInResourcePermission(ctx, orgID, builtInRole, accesscontrol.SetResourcePermissionCommand{ + Actions: actions, + ResourceID: resourceID, + Resource: s.options.Resource, + }) +} + +func (s *Service) MapActions(permission accesscontrol.ResourcePermission) string { + for _, p := range s.permissions { + if permission.Contains(s.options.PermissionsToActions[p]) { + return p + } + } + return "" +} + +func (s *Service) MapPermission(permission string) []string { + for k, v := range s.options.PermissionsToActions { + if permission == k { + return v + } + } + return []string{} +} + +func (s *Service) validateResource(ctx context.Context, orgID int64, resourceID string) error { + if s.options.ResourceValidator != nil { + return s.options.ResourceValidator(ctx, orgID, resourceID) + } + return nil +} + +func (s *Service) validateActions(actions []string) bool { + for _, a := range actions { + if _, ok := s.validActions[a]; !ok { + return false + } + } + return true +} + +func (s *Service) validateUser(ctx context.Context, orgID, userID int64) error { + if err := sqlstore.GetSignedInUser(ctx, &models.GetSignedInUserQuery{OrgId: orgID, UserId: userID}); err != nil { + return err + } + return nil +} + +func (s *Service) validateTeam(ctx context.Context, orgID, teamID int64) error { + if err := sqlstore.GetTeamById(ctx, &models.GetTeamByIdQuery{OrgId: orgID, Id: teamID}); err != nil { + return err + } + return nil +} + +func (s *Service) validateBuiltinRole(ctx context.Context, builtinRole string) error { + if err := accesscontrol.ValidateBuiltInRoles([]string{builtinRole}); err != nil { + return err + } + return nil +} + +func (s *Service) declareFixedRoles() error { + scopeAll := accesscontrol.Scope(s.options.Resource, "*") + readerRole := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Version: 5, + Name: fmt.Sprintf("fixed:%s.permissions:reader", s.options.Resource), + DisplayName: s.options.ReaderRoleName, + Group: s.options.RoleGroup, + Permissions: []accesscontrol.Permission{ + {Action: fmt.Sprintf("%s.permissions:read", s.options.Resource), Scope: scopeAll}, + }, + }, + Grants: []string{string(models.ROLE_ADMIN)}, + } + + writerRole := accesscontrol.RoleRegistration{ + Role: accesscontrol.RoleDTO{ + Version: 5, + Name: fmt.Sprintf("fixed:%s.permissions:writer", s.options.Resource), + DisplayName: s.options.WriterRoleName, + Group: s.options.RoleGroup, + Permissions: accesscontrol.ConcatPermissions(readerRole.Role.Permissions, []accesscontrol.Permission{ + {Action: fmt.Sprintf("%s.permissions:write", s.options.Resource), Scope: scopeAll}, + }), + }, + Grants: []string{string(models.ROLE_ADMIN)}, + } + + return s.ac.DeclareFixedRoles(readerRole, writerRole) +} diff --git a/public/app/core/components/AccessControl/AddPermission.tsx b/public/app/core/components/AccessControl/AddPermission.tsx new file mode 100644 index 00000000000..17d7495af65 --- /dev/null +++ b/public/app/core/components/AccessControl/AddPermission.tsx @@ -0,0 +1,101 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { UserPicker } from 'app/core/components/Select/UserPicker'; +import { TeamPicker } from 'app/core/components/Select/TeamPicker'; +import { Button, Form, HorizontalGroup, Select } from '@grafana/ui'; +import { OrgRole } from 'app/types/acl'; +import { CloseButton } from 'app/core/components/CloseButton/CloseButton'; +import { Assignments, PermissionTarget, SetPermission } from './types'; + +export interface Props { + permissions: string[]; + assignments: Assignments; + canListUsers: boolean; + onCancel: () => void; + onAdd: (state: SetPermission) => void; +} + +export const AddPermission = ({ permissions, assignments, canListUsers, onAdd, onCancel }: Props) => { + const [target, setPermissionTarget] = useState(PermissionTarget.User); + const [teamId, setTeamId] = useState(0); + const [userId, setUserId] = useState(0); + const [builtInRole, setBuiltinRole] = useState(''); + const [permission, setPermission] = useState(''); + + const targetOptions = useMemo(() => { + const options = []; + if (assignments.users && canListUsers) { + options.push({ value: PermissionTarget.User, label: 'User', isDisabled: false }); + } + if (assignments.teams) { + options.push({ value: PermissionTarget.Team, label: 'Team' }); + } + if (assignments.builtInRoles) { + options.push({ value: PermissionTarget.BuiltInRole, label: 'Role' }); + } + return options; + }, [assignments, canListUsers]); + + useEffect(() => { + if (permissions.length > 0) { + setPermission(permissions[0]); + } + }, [permissions]); + + const isValid = () => + (target === PermissionTarget.Team && teamId > 0) || + (target === PermissionTarget.User && userId > 0) || + (PermissionTarget.BuiltInRole && OrgRole.hasOwnProperty(builtInRole)); + + return ( +
+ +
Add Permission For
+
onAdd({ userId, teamId, builtInRole, permission, target })} + > + {() => ( + + ({ value: r, label: r }))} + onChange={(r) => setBuiltinRole(r.value || '')} + width={40} + /> + )} + + onChange(item, p.value!)} + value={permissionLevels.find((p) => p === item.permission)} + options={permissionLevels.map((p) => ({ value: p, label: p }))} + /> +
+ + + + + + + + {item.isManaged ? ( + + )} + + +
+ + setIsAdding(false)} + /> + + + + +
+ + ); +}; + +const getDescription = async (resource: string): Promise => { + try { + return await getBackendSrv().get(`/api/access-control/${resource}/description`); + } catch (e) { + console.error('failed to load resource description: ', e); + return INITIAL_DESCRIPTION; + } +}; + +const getPermissions = (resource: string, datasourceId: number): Promise => + getBackendSrv().get(`/api/access-control/${resource}/${datasourceId}`); + +const setUserPermission = (resource: string, resourceId: number, userId: number, permission: string) => + setPermission(resource, resourceId, 'users', userId, permission); + +const setTeamPermission = (resource: string, resourceId: number, teamId: number, permission: string) => + setPermission(resource, resourceId, 'teams', teamId, permission); + +const setBuiltInRolePermission = (resource: string, resourceId: number, builtInRole: string, permission: string) => + setPermission(resource, resourceId, 'builtInRoles', builtInRole, permission); + +const setPermission = ( + resource: string, + resourceId: number, + type: 'users' | 'teams' | 'builtInRoles', + typeId: number | string, + permission: string +): Promise => + getBackendSrv().post(`/api/access-control/${resource}/${resourceId}/${type}/${typeId}/`, { permission }); diff --git a/public/app/core/components/AccessControl/index.ts b/public/app/core/components/AccessControl/index.ts new file mode 100644 index 00000000000..753f2b4bccb --- /dev/null +++ b/public/app/core/components/AccessControl/index.ts @@ -0,0 +1 @@ +export * from './Permissions'; diff --git a/public/app/core/components/AccessControl/types.ts b/public/app/core/components/AccessControl/types.ts new file mode 100644 index 00000000000..b6bba211c4c --- /dev/null +++ b/public/app/core/components/AccessControl/types.ts @@ -0,0 +1,38 @@ +export type ResourcePermission = { + id: number; + resourceId: string; + isManaged: boolean; + userId?: number; + userLogin?: string; + userAvatarUrl?: string; + team?: string; + teamId?: number; + teamAvatarUrl?: string; + builtInRole?: string; + actions: string[]; + permission: string; +}; + +export type SetPermission = { + userId?: number; + teamId?: number; + builtInRole?: string; + permission: string; + target: PermissionTarget; +}; + +export enum PermissionTarget { + Team = 'Team', + User = 'User', + BuiltInRole = 'builtInRole', +} +export type Description = { + assignments: Assignments; + permissions: string[]; +}; + +export type Assignments = { + users: boolean; + teams: boolean; + builtInRoles: boolean; +};