Access control: Refactor managed permission system to create api and frontend components (#42540)

* Refactor resource permissions
* Add frondend components for resource permissions

Co-authored-by: kay delaney <45561153+kaydelaney@users.noreply.github.com>
Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com>
Co-authored-by: Ieva <ieva.vasiljeva@grafana.com>
This commit is contained in:
Karl Persson 2021-12-20 09:52:24 +01:00 committed by GitHub
parent 9cf5623918
commit c3ca2d214d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1643 additions and 343 deletions

View File

@ -168,7 +168,6 @@ type DsPermissionType int
const (
DsPermissionNoAccess DsPermissionType = iota
DsPermissionQuery
DsPermissionRead
)
func (p DsPermissionType) String() string {

View File

@ -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)),

View File

@ -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)
}

View File

@ -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,

View File

@ -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))
}

View File

@ -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)),

View File

@ -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,

View File

@ -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() {

View File

@ -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

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -0,0 +1,8 @@
package resourcepermissions
import "errors"
var (
ErrInvalidActions = errors.New("invalid actions")
ErrInvalidAssignment = errors.New("invalid assignment")
)

View File

@ -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
}

View File

@ -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)
}

View File

@ -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>(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 (
<div className="cta-form" aria-label="Permissions slider">
<CloseButton onClick={onCancel} />
<h5>Add Permission For</h5>
<Form
name="addPermission"
maxWidth="none"
onSubmit={() => onAdd({ userId, teamId, builtInRole, permission, target })}
>
{() => (
<HorizontalGroup>
<Select
aria-label="Role to add new permission to"
value={target}
options={targetOptions}
onChange={(v) => setPermissionTarget(v.value!)}
menuShouldPortal
/>
{target === PermissionTarget.User && (
<UserPicker onSelected={(u) => setUserId(u.value || 0)} className={'width-20'} />
)}
{target === PermissionTarget.Team && (
<TeamPicker onSelected={(t) => setTeamId(t.value?.id || 0)} className={'width-20'} />
)}
{target === PermissionTarget.BuiltInRole && (
<Select
aria-label={'Built-in role picker'}
menuShouldPortal
options={Object.values(OrgRole).map((r) => ({ value: r, label: r }))}
onChange={(r) => setBuiltinRole(r.value || '')}
width={40}
/>
)}
<Select
width={25}
menuShouldPortal
value={permissions.find((p) => p === permission)}
options={permissions.map((p) => ({ label: p, value: p }))}
onChange={(v) => setPermission(v.value || '')}
/>
<Button type="submit" disabled={!isValid()}>
Save
</Button>
</HorizontalGroup>
)}
</Form>
</div>
);
};

View File

@ -0,0 +1,38 @@
import React from 'react';
import { ResourcePermission } from './types';
import { PermissionListItem } from './PermissionListItem';
interface Props {
title: string;
items: ResourcePermission[];
permissionLevels: string[];
canRemove: boolean;
onRemove: (item: ResourcePermission) => void;
onChange: (resourcePermission: ResourcePermission, permission: string) => void;
}
export const PermissionList = ({ title, items, permissionLevels, canRemove, onRemove, onChange }: Props) => {
if (items.length === 0) {
return null;
}
return (
<div>
<h5>{title}</h5>
<table className="filter-table gf-form-group">
<tbody>
{items.map((item, index) => (
<PermissionListItem
item={item}
onRemove={onRemove}
onChange={onChange}
canRemove={canRemove}
key={`${index}-${item.userId}`}
permissionLevels={permissionLevels}
/>
))}
</tbody>
</table>
</div>
);
};

View File

@ -0,0 +1,84 @@
import React from 'react';
import { ResourcePermission } from './types';
import { Button, Icon, Select, Tooltip } from '@grafana/ui';
interface Props {
item: ResourcePermission;
permissionLevels: string[];
canRemove: boolean;
onRemove: (item: ResourcePermission) => void;
onChange: (item: ResourcePermission, permission: string) => void;
}
export const PermissionListItem = ({ item, permissionLevels, canRemove, onRemove, onChange }: Props) => (
<tr>
<td style={{ width: '1%' }}>{getAvatar(item)}</td>
<td style={{ width: '90%' }}>{getDescription(item)}</td>
<td />
<td className="query-keyword">Can</td>
<td>
<div className="gf-form">
<Select
className="width-20"
menuShouldPortal
onChange={(p) => onChange(item, p.value!)}
value={permissionLevels.find((p) => p === item.permission)}
options={permissionLevels.map((p) => ({ value: p, label: p }))}
/>
</div>
</td>
<td>
<Tooltip content={getPermissionInfo(item)}>
<Icon name="info-circle" />
</Tooltip>
</td>
<td>
{item.isManaged ? (
<Button
size="sm"
icon="times"
variant="destructive"
disabled={!canRemove}
onClick={() => onRemove(item)}
aria-label={`Remove permission for ${getName(item)}`}
/>
) : (
<Tooltip content="Provisioned permission">
<Button size="sm" icon="lock" />
</Tooltip>
)}
</td>
</tr>
);
const getAvatar = (item: ResourcePermission) => {
if (item.teamId) {
return <img className="filter-table__avatar" src={item.teamAvatarUrl} alt={`Avatar for team ${item.teamId}`} />;
} else if (item.userId) {
return <img className="filter-table__avatar" src={item.userAvatarUrl} alt={`Avatar for user ${item.userId}`} />;
}
return <Icon size="xl" name="shield" />;
};
const getName = (item: ResourcePermission) => {
if (item.userId) {
return item.userLogin;
}
if (item.teamId) {
return item.team;
}
return item.builtInRole;
};
const getDescription = (item: ResourcePermission) => {
if (item.userId) {
return <span key="name">{item.userLogin} </span>;
} else if (item.teamId) {
return <span key="name">{item.team} </span>;
} else if (item.builtInRole) {
return <span key="name">{item.builtInRole} </span>;
}
return <span key="name" />;
};
const getPermissionInfo = (p: ResourcePermission) => `Actions: ${[...new Set(p.actions)].sort().join(' ')}`;

View File

@ -0,0 +1,193 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { sortBy } from 'lodash';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { Button } from '@grafana/ui';
import { SlideDown } from 'app/core/components/Animations/SlideDown';
import { AddPermission } from './AddPermission';
import { PermissionList } from './PermissionList';
import { PermissionTarget, ResourcePermission, SetPermission, Description } from './types';
const EMPTY_PERMISSION = '';
const INITIAL_DESCRIPTION: Description = {
permissions: [],
assignments: {
teams: false,
users: false,
builtInRoles: false,
},
};
export type Props = {
resource: string;
resourceId: number;
canListUsers: boolean;
canSetPermissions: boolean;
};
export const Permissions = ({ resource, resourceId, canListUsers, canSetPermissions }: Props) => {
const [isAdding, setIsAdding] = useState(false);
const [items, setItems] = useState<ResourcePermission[]>([]);
const [desc, setDesc] = useState(INITIAL_DESCRIPTION);
const fetchItems = useCallback(() => {
return getPermissions(resource, resourceId).then((r) => setItems(r));
}, [resource, resourceId]);
useEffect(() => {
getDescription(resource).then((r) => {
setDesc(r);
return fetchItems();
});
}, [resource, resourceId, fetchItems]);
const onAdd = (state: SetPermission) => {
let promise: Promise<void> | null = null;
if (state.target === PermissionTarget.User) {
promise = setUserPermission(resource, resourceId, state.userId!, state.permission);
} else if (state.target === PermissionTarget.Team) {
promise = setTeamPermission(resource, resourceId, state.teamId!, state.permission);
} else if (state.target === PermissionTarget.BuiltInRole) {
promise = setBuiltInRolePermission(resource, resourceId, state.builtInRole!, state.permission);
}
if (promise !== null) {
promise.then(fetchItems);
}
};
const onRemove = (item: ResourcePermission) => {
let promise: Promise<void> | null = null;
if (item.userId) {
promise = setUserPermission(resource, resourceId, item.userId, EMPTY_PERMISSION);
} else if (item.teamId) {
promise = setTeamPermission(resource, resourceId, item.teamId, EMPTY_PERMISSION);
} else if (item.builtInRole) {
promise = setBuiltInRolePermission(resource, resourceId, item.builtInRole, EMPTY_PERMISSION);
}
if (promise !== null) {
promise.then(fetchItems);
}
};
const onChange = (item: ResourcePermission, permission: string) => {
if (item.permission === permission) {
return;
}
if (item.userId) {
onAdd({ permission, userId: item.userId, target: PermissionTarget.User });
} else if (item.teamId) {
onAdd({ permission, teamId: item.teamId, target: PermissionTarget.Team });
} else if (item.builtInRole) {
onAdd({ permission, builtInRole: item.builtInRole, target: PermissionTarget.BuiltInRole });
}
};
const teams = useMemo(
() =>
sortBy(
items.filter((i) => i.teamId),
['team']
),
[items]
);
const users = useMemo(
() =>
sortBy(
items.filter((i) => i.userId),
['userLogin']
),
[items]
);
const builtInRoles = useMemo(
() =>
sortBy(
items.filter((i) => i.builtInRole),
['builtInRole']
),
[items]
);
return (
<div>
<div className="page-action-bar">
<h3 className="page-sub-heading">Permissions</h3>
<div className="page-action-bar__spacer" />
{canSetPermissions && (
<Button variant={'primary'} key="add-permission" onClick={() => setIsAdding(true)}>
Add a permission
</Button>
)}
</div>
<div>
<SlideDown in={isAdding}>
<AddPermission
onAdd={onAdd}
permissions={desc.permissions}
assignments={desc.assignments}
canListUsers={canListUsers}
onCancel={() => setIsAdding(false)}
/>
</SlideDown>
<PermissionList
title="Roles"
items={builtInRoles}
permissionLevels={desc.permissions}
onChange={onChange}
onRemove={onRemove}
canRemove={canSetPermissions}
/>
<PermissionList
title="Users"
items={users}
permissionLevels={desc.permissions}
onChange={onChange}
onRemove={onRemove}
canRemove={canSetPermissions}
/>
<PermissionList
title="Teams"
items={teams}
permissionLevels={desc.permissions}
onChange={onChange}
onRemove={onRemove}
canRemove={canSetPermissions}
/>
</div>
</div>
);
};
const getDescription = async (resource: string): Promise<Description> => {
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<ResourcePermission[]> =>
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<void> =>
getBackendSrv().post(`/api/access-control/${resource}/${resourceId}/${type}/${typeId}/`, { permission });

View File

@ -0,0 +1 @@
export * from './Permissions';

View File

@ -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;
};