mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
9cf5623918
commit
c3ca2d214d
@ -168,7 +168,6 @@ type DsPermissionType int
|
||||
const (
|
||||
DsPermissionNoAccess DsPermissionType = iota
|
||||
DsPermissionQuery
|
||||
DsPermissionRead
|
||||
)
|
||||
|
||||
func (p DsPermissionType) String() string {
|
||||
|
@ -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)),
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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))
|
||||
}
|
@ -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)),
|
@ -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,
|
@ -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() {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
178
pkg/services/accesscontrol/resourcepermissions/api.go
Normal file
178
pkg/services/accesscontrol/resourcepermissions/api.go
Normal 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)
|
||||
}
|
487
pkg/services/accesscontrol/resourcepermissions/api_test.go
Normal file
487
pkg/services/accesscontrol/resourcepermissions/api_test.go
Normal 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
|
||||
}
|
8
pkg/services/accesscontrol/resourcepermissions/error.go
Normal file
8
pkg/services/accesscontrol/resourcepermissions/error.go
Normal file
@ -0,0 +1,8 @@
|
||||
package resourcepermissions
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrInvalidActions = errors.New("invalid actions")
|
||||
ErrInvalidAssignment = errors.New("invalid assignment")
|
||||
)
|
27
pkg/services/accesscontrol/resourcepermissions/options.go
Normal file
27
pkg/services/accesscontrol/resourcepermissions/options.go
Normal 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
|
||||
}
|
230
pkg/services/accesscontrol/resourcepermissions/service.go
Normal file
230
pkg/services/accesscontrol/resourcepermissions/service.go
Normal 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)
|
||||
}
|
101
public/app/core/components/AccessControl/AddPermission.tsx
Normal file
101
public/app/core/components/AccessControl/AddPermission.tsx
Normal 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>
|
||||
);
|
||||
};
|
38
public/app/core/components/AccessControl/PermissionList.tsx
Normal file
38
public/app/core/components/AccessControl/PermissionList.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -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(' ')}`;
|
193
public/app/core/components/AccessControl/Permissions.tsx
Normal file
193
public/app/core/components/AccessControl/Permissions.tsx
Normal 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 });
|
1
public/app/core/components/AccessControl/index.ts
Normal file
1
public/app/core/components/AccessControl/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './Permissions';
|
38
public/app/core/components/AccessControl/types.ts
Normal file
38
public/app/core/components/AccessControl/types.ts
Normal 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;
|
||||
};
|
Loading…
Reference in New Issue
Block a user