mirror of
https://github.com/grafana/grafana.git
synced 2024-11-26 19:00:54 -06:00
Access control: FGAC for team sync endpoints (#44673)
* add actions for team group sync * extend the hook to allow specifying whether the user is external * move user struct to type package * interface for permission service to allow mocking it * reuse existing permissions * test fix * refactor * linting
This commit is contained in:
parent
bc7e55d99b
commit
602d62ebcc
@ -10,6 +10,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
@ -152,7 +153,7 @@ func (hs *HTTPServer) RemoveTeamMember(c *models.ReqContext) response.Response {
|
||||
}
|
||||
|
||||
teamIDString := strconv.FormatInt(teamId, 10)
|
||||
if _, err := hs.TeamPermissionsService.SetUserPermission(c.Req.Context(), orgId, userId, teamIDString, ""); err != nil {
|
||||
if _, err := hs.TeamPermissionsService.SetUserPermission(c.Req.Context(), orgId, accesscontrol.User{ID: userId}, teamIDString, ""); err != nil {
|
||||
if errors.Is(err, models.ErrTeamNotFound) {
|
||||
return response.Error(404, "Team not found", nil)
|
||||
}
|
||||
@ -171,7 +172,7 @@ func (hs *HTTPServer) RemoveTeamMember(c *models.ReqContext) response.Response {
|
||||
// Stubbable by tests.
|
||||
var addOrUpdateTeamMember = func(ctx context.Context, resourcePermissionService *resourcepermissions.Service, userID, orgID, teamID int64, permission string) error {
|
||||
teamIDString := strconv.FormatInt(teamID, 10)
|
||||
if _, err := resourcePermissionService.SetUserPermission(ctx, orgID, userID, teamIDString, permission); err != nil {
|
||||
if _, err := resourcePermissionService.SetUserPermission(ctx, orgID, accesscontrol.User{ID: userID}, teamIDString, permission); err != nil {
|
||||
return fmt.Errorf("failed setting permissions for user %d in team %d: %w", userID, teamID, err)
|
||||
}
|
||||
return nil
|
||||
|
@ -37,13 +37,18 @@ 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, permission string) (*ResourcePermission, error)
|
||||
SetUserPermission(ctx context.Context, orgID int64, user User, resourceID, permission string) (*ResourcePermission, error)
|
||||
// SetTeamPermission sets permission on resource for a team
|
||||
SetTeamPermission(ctx context.Context, orgID, teamID int64, resourceID, permission 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, permission string) (*ResourcePermission, error)
|
||||
}
|
||||
|
||||
type User struct {
|
||||
ID int64
|
||||
IsExternal bool
|
||||
}
|
||||
|
||||
// Metadata contains user accesses for a given resource
|
||||
// Ex: map[string]bool{"create":true, "delete": true}
|
||||
type Metadata map[string]bool
|
||||
|
@ -5,7 +5,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
@ -81,7 +80,7 @@ func TestAccessControlStore_GetUserPermissions(t *testing.T) {
|
||||
user, team := createUserAndTeam(t, sql, tt.orgID)
|
||||
|
||||
for _, id := range tt.userPermissions {
|
||||
_, err := store.SetUserResourcePermission(context.Background(), tt.orgID, user.Id, accesscontrol.SetResourcePermissionCommand{
|
||||
_, err := store.SetUserResourcePermission(context.Background(), tt.orgID, accesscontrol.User{ID: user.Id}, accesscontrol.SetResourcePermissionCommand{
|
||||
Actions: []string{"dashboards:write"},
|
||||
Resource: "dashboards",
|
||||
ResourceID: id,
|
||||
|
@ -34,20 +34,20 @@ func (p *flatResourcePermission) Managed() bool {
|
||||
}
|
||||
|
||||
func (s *AccessControlStore) SetUserResourcePermission(
|
||||
ctx context.Context, orgID, userID int64,
|
||||
ctx context.Context, orgID int64, user accesscontrol.User,
|
||||
cmd accesscontrol.SetResourcePermissionCommand,
|
||||
hook types.UserResourceHookFunc,
|
||||
) (*accesscontrol.ResourcePermission, error) {
|
||||
if userID == 0 {
|
||||
if user.ID == 0 {
|
||||
return nil, models.ErrUserNotFound
|
||||
}
|
||||
|
||||
var err error
|
||||
var permission *accesscontrol.ResourcePermission
|
||||
err = s.sql.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
permission, err = s.setResourcePermission(sess, orgID, managedUserRoleName(userID), s.userAdder(sess, orgID, userID), cmd)
|
||||
permission, err = s.setResourcePermission(sess, orgID, managedUserRoleName(user.ID), s.userAdder(sess, orgID, user.ID), cmd)
|
||||
if err == nil && hook != nil {
|
||||
return hook(sess, orgID, userID, cmd.ResourceID, cmd.Permission)
|
||||
return hook(sess, orgID, user, cmd.ResourceID, cmd.Permission)
|
||||
}
|
||||
|
||||
return err
|
||||
|
@ -93,7 +93,7 @@ func GenerateDatasourcePermissions(b *testing.B, db *sqlstore.SQLStore, ac *Acce
|
||||
_, err := ac.SetUserResourcePermission(
|
||||
context.Background(),
|
||||
accesscontrol.GlobalOrgID,
|
||||
userIds[i],
|
||||
accesscontrol.User{ID: userIds[i]},
|
||||
accesscontrol.SetResourcePermissionCommand{
|
||||
Actions: []string{dsAction},
|
||||
Resource: dsResource,
|
||||
|
@ -70,11 +70,11 @@ func TestAccessControlStore_SetUserResourcePermission(t *testing.T) {
|
||||
store, _ := setupTestEnv(t)
|
||||
|
||||
for _, s := range test.seeds {
|
||||
_, err := store.SetUserResourcePermission(context.Background(), test.orgID, test.userID, s, nil)
|
||||
_, err := store.SetUserResourcePermission(context.Background(), test.orgID, accesscontrol.User{ID: test.userID}, s, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
added, err := store.SetUserResourcePermission(context.Background(), test.userID, test.userID, accesscontrol.SetResourcePermissionCommand{
|
||||
added, err := store.SetUserResourcePermission(context.Background(), test.userID, accesscontrol.User{ID: test.userID}, accesscontrol.SetResourcePermissionCommand{
|
||||
Actions: test.actions,
|
||||
Resource: test.resource,
|
||||
ResourceID: test.resourceID,
|
||||
@ -352,7 +352,7 @@ func seedResourcePermissions(t *testing.T, store *AccessControlStore, sql *sqlst
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = store.SetUserResourcePermission(context.Background(), 1, u.Id, accesscontrol.SetResourcePermissionCommand{
|
||||
_, err = store.SetUserResourcePermission(context.Background(), 1, accesscontrol.User{ID: u.Id}, accesscontrol.SetResourcePermissionCommand{
|
||||
Actions: actions,
|
||||
Resource: resource,
|
||||
ResourceID: resourceID,
|
||||
|
@ -131,7 +131,7 @@ func (a *api) setUserPermission(c *models.ReqContext) response.Response {
|
||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||
}
|
||||
|
||||
_, err = a.service.SetUserPermission(c.Req.Context(), c.OrgId, userID, resourceID, cmd.Permission)
|
||||
_, err = a.service.SetUserPermission(c.Req.Context(), c.OrgId, accesscontrol.User{ID: userID}, resourceID, cmd.Permission)
|
||||
if err != nil {
|
||||
return response.Error(http.StatusBadRequest, "failed to set user permission", err)
|
||||
}
|
||||
|
@ -160,7 +160,7 @@ func TestApi_getPermissions(t *testing.T) {
|
||||
// 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, "View")
|
||||
_, err = service.SetUserPermission(context.Background(), u.OrgId, accesscontrol.User{ID: u.Id}, tt.resourceID, "View")
|
||||
require.NoError(t, err)
|
||||
|
||||
// seed built in role Admin with "Edit" permission on dashboard 1
|
||||
|
@ -3,6 +3,7 @@ package resourcepermissions
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
)
|
||||
|
||||
@ -28,7 +29,7 @@ type Options struct {
|
||||
// RoleGroup is the group name for the generated fixed roles
|
||||
RoleGroup string
|
||||
// OnSetUser if configured will be called each time a permission is set for a user
|
||||
OnSetUser func(session *sqlstore.DBSession, orgID, userID int64, resourceID, permission string) error
|
||||
OnSetUser func(session *sqlstore.DBSession, orgID int64, user accesscontrol.User, resourceID, permission string) error
|
||||
// OnSetTeam if configured will be called each time a permission is set for a team
|
||||
OnSetTeam func(session *sqlstore.DBSession, orgID, teamID int64, resourceID, permission string) error
|
||||
// OnSetBuiltInRole if configured will be called each time a permission is set for a built-in role
|
||||
|
@ -16,7 +16,8 @@ import (
|
||||
type Store interface {
|
||||
// SetUserResourcePermission sets permission for managed user role on a resource
|
||||
SetUserResourcePermission(
|
||||
ctx context.Context, orgID, userID int64,
|
||||
ctx context.Context, orgID int64,
|
||||
user accesscontrol.User,
|
||||
cmd accesscontrol.SetResourcePermissionCommand,
|
||||
hook types.UserResourceHookFunc,
|
||||
) (*accesscontrol.ResourcePermission, error)
|
||||
@ -100,7 +101,7 @@ func (s *Service) GetPermissions(ctx context.Context, orgID int64, resourceID st
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) SetUserPermission(ctx context.Context, orgID, userID int64, resourceID, permission string) (*accesscontrol.ResourcePermission, error) {
|
||||
func (s *Service) SetUserPermission(ctx context.Context, orgID int64, user accesscontrol.User, resourceID, permission string) (*accesscontrol.ResourcePermission, error) {
|
||||
if !s.options.Assignments.Users {
|
||||
return nil, ErrInvalidAssignment
|
||||
}
|
||||
@ -114,11 +115,11 @@ func (s *Service) SetUserPermission(ctx context.Context, orgID, userID int64, re
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := s.validateUser(ctx, orgID, userID); err != nil {
|
||||
if err := s.validateUser(ctx, orgID, user.ID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.store.SetUserResourcePermission(ctx, orgID, userID, accesscontrol.SetResourcePermissionCommand{
|
||||
return s.store.SetUserResourcePermission(ctx, orgID, user, accesscontrol.SetResourcePermissionCommand{
|
||||
Actions: actions,
|
||||
Permission: permission,
|
||||
ResourceID: resourceID,
|
||||
|
@ -0,0 +1,33 @@
|
||||
package resourcepermissions
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
)
|
||||
|
||||
type MockService struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (m *MockService) GetPermissions(ctx context.Context, orgID int64, resourceID string) ([]accesscontrol.ResourcePermission, error) {
|
||||
mockedArgs := m.Called(ctx, orgID, resourceID)
|
||||
return mockedArgs.Get(0).([]accesscontrol.ResourcePermission), mockedArgs.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockService) SetUserPermission(ctx context.Context, orgID int64, user accesscontrol.User, resourceID, permission string) (*accesscontrol.ResourcePermission, error) {
|
||||
mockedArgs := m.Called(ctx, orgID, user, resourceID, permission)
|
||||
return mockedArgs.Get(0).(*accesscontrol.ResourcePermission), mockedArgs.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockService) SetTeamPermission(ctx context.Context, orgID, teamID int64, resourceID, permission string) (*accesscontrol.ResourcePermission, error) {
|
||||
mockedArgs := m.Called(ctx, orgID, teamID, resourceID, permission)
|
||||
return mockedArgs.Get(0).(*accesscontrol.ResourcePermission), mockedArgs.Error(1)
|
||||
}
|
||||
|
||||
func (m *MockService) SetBuiltInRolePermission(ctx context.Context, orgID int64, builtInRole, resourceID, permission string) (*accesscontrol.ResourcePermission, error) {
|
||||
mockedArgs := m.Called(ctx, orgID, builtInRole, resourceID, permission)
|
||||
return mockedArgs.Get(0).(*accesscontrol.ResourcePermission), mockedArgs.Error(1)
|
||||
}
|
@ -46,13 +46,13 @@ func TestService_SetUserPermission(t *testing.T) {
|
||||
|
||||
var hookCalled bool
|
||||
if tt.callHook {
|
||||
service.options.OnSetUser = func(session *sqlstore.DBSession, orgID, userID int64, resourceID, permission string) error {
|
||||
service.options.OnSetUser = func(session *sqlstore.DBSession, orgID int64, user accesscontrol.User, resourceID, permission string) error {
|
||||
hookCalled = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
_, err = service.SetUserPermission(context.Background(), user.OrgId, user.Id, "1", "")
|
||||
_, err = service.SetUserPermission(context.Background(), user.OrgId, accesscontrol.User{ID: user.Id}, "1", "")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.callHook, hookCalled)
|
||||
})
|
||||
|
@ -1,7 +1,15 @@
|
||||
package types
|
||||
|
||||
import "github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
)
|
||||
|
||||
type UserResourceHookFunc func(session *sqlstore.DBSession, orgID, userID int64, resourceID, permission string) error
|
||||
type UserResourceHookFunc func(session *sqlstore.DBSession, orgID int64, user accesscontrol.User, resourceID, permission string) error
|
||||
type TeamResourceHookFunc func(session *sqlstore.DBSession, orgID, teamID int64, resourceID, permission string) error
|
||||
type BuiltinResourceHookFunc func(session *sqlstore.DBSession, orgID int64, builtInRole, resourceID, permission string) error
|
||||
|
||||
type User struct {
|
||||
ID int64
|
||||
IsExternal bool
|
||||
}
|
||||
|
@ -77,20 +77,20 @@ func ProvideTeamPermissions(router routing.RouteRegister, sql *sqlstore.SQLStore
|
||||
ReaderRoleName: "Team permission reader",
|
||||
WriterRoleName: "Team permission writer",
|
||||
RoleGroup: "Teams",
|
||||
OnSetUser: func(session *sqlstore.DBSession, orgID, userID int64, resourceID, permission string) error {
|
||||
OnSetUser: func(session *sqlstore.DBSession, orgID int64, user accesscontrol.User, resourceID, permission string) error {
|
||||
teamId, err := strconv.ParseInt(resourceID, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch permission {
|
||||
case "Member":
|
||||
return sqlstore.AddOrUpdateTeamMemberHook(session, userID, orgID, teamId, false, 0)
|
||||
return sqlstore.AddOrUpdateTeamMemberHook(session, user.ID, orgID, teamId, user.IsExternal, 0)
|
||||
case "Admin":
|
||||
return sqlstore.AddOrUpdateTeamMemberHook(session, userID, orgID, teamId, false, models.PERMISSION_ADMIN)
|
||||
return sqlstore.AddOrUpdateTeamMemberHook(session, user.ID, orgID, teamId, user.IsExternal, models.PERMISSION_ADMIN)
|
||||
case "":
|
||||
return sqlstore.RemoveTeamMemberHook(session, &models.RemoveTeamMemberCommand{
|
||||
OrgId: orgID,
|
||||
UserId: userID,
|
||||
UserId: user.ID,
|
||||
TeamId: teamId,
|
||||
})
|
||||
default:
|
||||
|
Loading…
Reference in New Issue
Block a user