AccessControl: Implement teams resource service (#43951)
* AccessControl: cover team permissions Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * Add background service as a consumer to resource_services Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * Define actions in roles.go Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * Remove action from accesscontrol model Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * As suggested by kalle * move some changes from branch to the skeleton PR * Add background service as a consumer to resource_services Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * moving resourceservice to the main wire file pt2 * move team related actions so that they can be reused * PR feedback * fix * typo * Access Control: adding hooks for team member endpoints (#43991) * AccessControl: cover team permissions Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * Add background service as a consumer to resource_services Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * Define actions in roles.go Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * Remove action from accesscontrol model Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * As suggested by kalle * add access control to list and add team member endpoint, and hooks for adding team members * member permission type is 0 * add ID scope for team permission checks * add more team actions, use Member for member permission name * protect team member update endpoint with FGAC permissions * update SQL functions for teams and the corresponding tests * also protect team member removal endpoint with FGAC permissions and add a hook to permission service * a few small fixes, provide team permission service to test setup * AccessControl: cover team permissions Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * Add background service as a consumer to resource_services Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * Define actions in roles.go Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * Remove action from accesscontrol model Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com> * As suggested by kalle * move some changes from branch to the skeleton PR * remove resource services from wireexts * remove unneeded actions * linting fix * remove comments * feedback fixes * feedback * simplifying * remove team member within the same transaction * fix a mistake with the error * call the correct sql fction * linting * Access control: tests for team member endpoints (#44177) * tests for team member endpoints * clean up and fix the tests * fixing tests take 2 * don't import enterprise test license * don't import enterprise test license * remove unused variable Co-authored-by: gamab <gabi.mabs@gmail.com> Co-authored-by: Gabriel MABILLE <gamab@users.noreply.github.com> Co-authored-by: ievaVasiljeva <ieva.vasiljeva@grafana.com>
This commit is contained in:
co-authored by
ievaVasiljeva
gamab
parent
46422a82c8
commit
d4f682190f
@@ -312,8 +312,18 @@ const (
|
||||
ActionLicensingDelete = "licensing:delete"
|
||||
ActionLicensingReportsRead = "licensing.reports:read"
|
||||
|
||||
// Team actions
|
||||
ActionTeamsCreate = "teams:create"
|
||||
// Team related actions
|
||||
ActionTeamsCreate = "teams:create"
|
||||
ActionTeamsDelete = "teams:delete"
|
||||
ActionTeamsRead = "teams:read"
|
||||
ActionTeamsWrite = "teams:write"
|
||||
ActionTeamsPermissionsRead = "teams.permissions:read"
|
||||
ActionTeamsPermissionsWrite = "teams.permissions:write"
|
||||
)
|
||||
|
||||
var (
|
||||
// Team scope
|
||||
ScopeTeamsID = Scope("teams", "id", Parameter(":teamId"))
|
||||
)
|
||||
|
||||
const RoleGrafanaAdmin = "Grafana Admin"
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
package resourceservices
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"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/resourcepermissions"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
)
|
||||
|
||||
func ProvideResourceServices(router routing.RouteRegister, sql *sqlstore.SQLStore, ac accesscontrol.AccessControl, store resourcepermissions.Store) (*ResourceServices, error) {
|
||||
teamPermissions, err := ProvideTeamPermissions(router, sql, ac, store)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ResourceServices{services: map[string]*resourcepermissions.Service{
|
||||
"teams": teamPermissions,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
type ResourceServices struct {
|
||||
services map[string]*resourcepermissions.Service
|
||||
}
|
||||
|
||||
func (s *ResourceServices) GetTeamService() *resourcepermissions.Service {
|
||||
return s.services["teams"]
|
||||
}
|
||||
|
||||
var (
|
||||
TeamMemberActions = []string{
|
||||
accesscontrol.ActionTeamsRead,
|
||||
}
|
||||
|
||||
TeamAdminActions = []string{
|
||||
accesscontrol.ActionTeamsRead,
|
||||
accesscontrol.ActionTeamsDelete,
|
||||
accesscontrol.ActionTeamsWrite,
|
||||
accesscontrol.ActionTeamsPermissionsRead,
|
||||
accesscontrol.ActionTeamsPermissionsWrite,
|
||||
}
|
||||
)
|
||||
|
||||
func ProvideTeamPermissions(router routing.RouteRegister, sql *sqlstore.SQLStore, ac accesscontrol.AccessControl, store resourcepermissions.Store) (*resourcepermissions.Service, error) {
|
||||
options := resourcepermissions.Options{
|
||||
Resource: "teams",
|
||||
OnlyManaged: true,
|
||||
ResourceValidator: func(ctx context.Context, orgID int64, resourceID string) error {
|
||||
id, err := strconv.ParseInt(resourceID, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = sql.GetTeamById(context.Background(), &models.GetTeamByIdQuery{
|
||||
OrgId: orgID,
|
||||
Id: id,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
Assignments: resourcepermissions.Assignments{
|
||||
Users: true,
|
||||
Teams: false,
|
||||
BuiltInRoles: false,
|
||||
},
|
||||
PermissionsToActions: map[string][]string{
|
||||
"Member": TeamMemberActions,
|
||||
"Admin": TeamAdminActions,
|
||||
},
|
||||
ReaderRoleName: "Team permission reader",
|
||||
WriterRoleName: "Team permission writer",
|
||||
RoleGroup: "Teams",
|
||||
OnSetUser: func(session *sqlstore.DBSession, orgID, userID int64, 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)
|
||||
case "Admin":
|
||||
return sqlstore.AddOrUpdateTeamMemberHook(session, userID, orgID, teamId, false, models.PERMISSION_ADMIN)
|
||||
case "":
|
||||
return sqlstore.RemoveTeamMemberHook(session, &models.RemoveTeamMemberCommand{
|
||||
OrgId: orgID,
|
||||
UserId: userID,
|
||||
TeamId: teamId,
|
||||
})
|
||||
default:
|
||||
return fmt.Errorf("invalid team permission type %s", permission)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return resourcepermissions.New(options, router, ac, store, sql)
|
||||
}
|
||||
+113
-60
@@ -32,6 +32,7 @@ type TeamStore interface {
|
||||
UpdateTeamMember(ctx context.Context, cmd *models.UpdateTeamMemberCommand) error
|
||||
RemoveTeamMember(ctx context.Context, cmd *models.RemoveTeamMemberCommand) error
|
||||
GetTeamMembers(ctx context.Context, cmd *models.GetTeamMembersQuery) error
|
||||
AddOrUpdateTeamMember(userID, orgID, teamID int64, isExternal bool, permission models.PermissionType) error
|
||||
}
|
||||
|
||||
func getFilteredUsers(signedInUser *models.SignedInUser, hiddenUsers map[string]struct{}) []string {
|
||||
@@ -292,29 +293,13 @@ func (ss *SQLStore) GetTeamsByUser(ctx context.Context, query *models.GetTeamsBy
|
||||
// AddTeamMember adds a user to a team
|
||||
func (ss *SQLStore) AddTeamMember(userID, orgID, teamID int64, isExternal bool, permission models.PermissionType) error {
|
||||
return ss.WithTransactionalDbSession(context.Background(), func(sess *DBSession) error {
|
||||
if res, err := sess.Query("SELECT 1 from team_member WHERE org_id=? and team_id=? and user_id=?",
|
||||
orgID, teamID, userID); err != nil {
|
||||
if isMember, err := isTeamMember(sess, orgID, teamID, userID); err != nil {
|
||||
return err
|
||||
} else if len(res) == 1 {
|
||||
} else if isMember {
|
||||
return models.ErrTeamMemberAlreadyAdded
|
||||
}
|
||||
|
||||
if _, err := teamExists(orgID, teamID, sess); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entity := models.TeamMember{
|
||||
OrgId: orgID,
|
||||
TeamId: teamID,
|
||||
UserId: userID,
|
||||
External: isExternal,
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
Permission: permission,
|
||||
}
|
||||
|
||||
_, err := sess.Insert(&entity)
|
||||
return err
|
||||
return addTeamMember(sess, orgID, teamID, userID, isExternal, permission)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -335,58 +320,126 @@ func getTeamMember(sess *DBSession, orgId int64, teamId int64, userId int64) (mo
|
||||
|
||||
// UpdateTeamMember updates a team member
|
||||
func (ss *SQLStore) UpdateTeamMember(ctx context.Context, cmd *models.UpdateTeamMemberCommand) error {
|
||||
return ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error {
|
||||
member, err := getTeamMember(sess, cmd.OrgId, cmd.TeamId, cmd.UserId)
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
return updateTeamMember(sess, cmd.OrgId, cmd.TeamId, cmd.UserId, cmd.Permission)
|
||||
})
|
||||
}
|
||||
|
||||
func (ss *SQLStore) IsTeamMember(orgId int64, teamId int64, userId int64) (bool, error) {
|
||||
var isMember bool
|
||||
|
||||
err := ss.WithTransactionalDbSession(context.Background(), func(sess *DBSession) error {
|
||||
var err error
|
||||
isMember, err = isTeamMember(sess, orgId, teamId, userId)
|
||||
return err
|
||||
})
|
||||
|
||||
return isMember, err
|
||||
}
|
||||
|
||||
func isTeamMember(sess *DBSession, orgId int64, teamId int64, userId int64) (bool, error) {
|
||||
if res, err := sess.Query("SELECT 1 FROM team_member WHERE org_id=? and team_id=? and user_id=?", orgId, teamId, userId); err != nil {
|
||||
return false, err
|
||||
} else if len(res) != 1 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// AddOrUpdateTeamMemberHook is called from team resource permission service
|
||||
// it adds user to a team or updates user permissions in a team within the given transaction session
|
||||
func AddOrUpdateTeamMemberHook(sess *DBSession, userID, orgID, teamID int64, isExternal bool, permission models.PermissionType) error {
|
||||
isMember, err := isTeamMember(sess, orgID, teamID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if isMember {
|
||||
err = updateTeamMember(sess, orgID, teamID, userID, permission)
|
||||
} else {
|
||||
err = addTeamMember(sess, orgID, teamID, userID, isExternal, permission)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func addTeamMember(sess *DBSession, orgID, teamID, userID int64, isExternal bool, permission models.PermissionType) error {
|
||||
if _, err := teamExists(orgID, teamID, sess); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
entity := models.TeamMember{
|
||||
OrgId: orgID,
|
||||
TeamId: teamID,
|
||||
UserId: userID,
|
||||
External: isExternal,
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
Permission: permission,
|
||||
}
|
||||
|
||||
_, err := sess.Insert(&entity)
|
||||
return err
|
||||
}
|
||||
|
||||
func updateTeamMember(sess *DBSession, orgID, teamID, userID int64, permission models.PermissionType) error {
|
||||
member, err := getTeamMember(sess, orgID, teamID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if permission != models.PERMISSION_ADMIN {
|
||||
permission = 0 // make sure we don't get invalid permission levels in store
|
||||
|
||||
// protect the last team admin
|
||||
_, err := isLastAdmin(sess, orgID, teamID, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if cmd.ProtectLastAdmin {
|
||||
_, err := isLastAdmin(sess, cmd.OrgId, cmd.TeamId, cmd.UserId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if cmd.Permission != models.PERMISSION_ADMIN { // make sure we don't get invalid permission levels in store
|
||||
cmd.Permission = 0
|
||||
}
|
||||
|
||||
member.Permission = cmd.Permission
|
||||
_, err = sess.Cols("permission").Where("org_id=? and team_id=? and user_id=?", cmd.OrgId, cmd.TeamId, cmd.UserId).Update(member)
|
||||
|
||||
return err
|
||||
})
|
||||
member.Permission = permission
|
||||
_, err = sess.Cols("permission").Where("org_id=? and team_id=? and user_id=?", orgID, teamID, userID).Update(member)
|
||||
return err
|
||||
}
|
||||
|
||||
// RemoveTeamMember removes a member from a team
|
||||
func (ss *SQLStore) RemoveTeamMember(ctx context.Context, cmd *models.RemoveTeamMemberCommand) error {
|
||||
return ss.WithTransactionalDbSession(ctx, func(sess *DBSession) error {
|
||||
if _, err := teamExists(cmd.OrgId, cmd.TeamId, sess); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if cmd.ProtectLastAdmin {
|
||||
_, err := isLastAdmin(sess, cmd.OrgId, cmd.TeamId, cmd.UserId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var rawSQL = "DELETE FROM team_member WHERE org_id=? and team_id=? and user_id=?"
|
||||
res, err := sess.Exec(rawSQL, cmd.OrgId, cmd.TeamId, cmd.UserId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err := res.RowsAffected()
|
||||
if rows == 0 {
|
||||
return models.ErrTeamMemberNotFound
|
||||
}
|
||||
|
||||
return err
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
return removeTeamMember(sess, cmd)
|
||||
})
|
||||
}
|
||||
|
||||
// RemoveTeamMemberHook is called from team resource permission service
|
||||
// it removes a member from a team within the given transaction session
|
||||
func RemoveTeamMemberHook(sess *DBSession, cmd *models.RemoveTeamMemberCommand) error {
|
||||
return removeTeamMember(sess, cmd)
|
||||
}
|
||||
|
||||
func removeTeamMember(sess *DBSession, cmd *models.RemoveTeamMemberCommand) error {
|
||||
if _, err := teamExists(cmd.OrgId, cmd.TeamId, sess); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := isLastAdmin(sess, cmd.OrgId, cmd.TeamId, cmd.UserId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var rawSQL = "DELETE FROM team_member WHERE org_id=? and team_id=? and user_id=?"
|
||||
res, err := sess.Exec(rawSQL, cmd.OrgId, cmd.TeamId, cmd.UserId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows, err := res.RowsAffected()
|
||||
if rows == 0 {
|
||||
return models.ErrTeamMemberNotFound
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func isLastAdmin(sess *DBSession, orgId int64, teamId int64, userId int64) (bool, error) {
|
||||
rawSQL := "SELECT user_id FROM team_member WHERE org_id=? and team_id=? and permission=?"
|
||||
userIds := []*int64{}
|
||||
|
||||
@@ -229,24 +229,24 @@ func TestTeamCommandsAndQueries(t *testing.T) {
|
||||
require.Equal(t, len(q2.Result), 0)
|
||||
})
|
||||
|
||||
t.Run("When ProtectLastAdmin is set to true", func(t *testing.T) {
|
||||
t.Run("Should never remove the last admin of a team", func(t *testing.T) {
|
||||
err = sqlStore.AddTeamMember(userIds[0], testOrgID, team1.Id, false, models.PERMISSION_ADMIN)
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("A user should not be able to remove the last admin", func(t *testing.T) {
|
||||
err = sqlStore.RemoveTeamMember(context.Background(), &models.RemoveTeamMemberCommand{OrgId: testOrgID, TeamId: team1.Id, UserId: userIds[0], ProtectLastAdmin: true})
|
||||
err = sqlStore.RemoveTeamMember(context.Background(), &models.RemoveTeamMemberCommand{OrgId: testOrgID, TeamId: team1.Id, UserId: userIds[0]})
|
||||
require.Equal(t, err, models.ErrLastTeamAdmin)
|
||||
})
|
||||
|
||||
t.Run("A user should be able to remove an admin if there are other admins", func(t *testing.T) {
|
||||
err = sqlStore.AddTeamMember(userIds[1], testOrgID, team1.Id, false, models.PERMISSION_ADMIN)
|
||||
require.NoError(t, err)
|
||||
err = sqlStore.RemoveTeamMember(context.Background(), &models.RemoveTeamMemberCommand{OrgId: testOrgID, TeamId: team1.Id, UserId: userIds[0], ProtectLastAdmin: true})
|
||||
err = sqlStore.RemoveTeamMember(context.Background(), &models.RemoveTeamMemberCommand{OrgId: testOrgID, TeamId: team1.Id, UserId: userIds[1]})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("A user should not be able to remove the admin permission for the last admin", func(t *testing.T) {
|
||||
err = sqlStore.UpdateTeamMember(context.Background(), &models.UpdateTeamMemberCommand{OrgId: testOrgID, TeamId: team1.Id, UserId: userIds[0], Permission: 0, ProtectLastAdmin: true})
|
||||
err = sqlStore.UpdateTeamMember(context.Background(), &models.UpdateTeamMemberCommand{OrgId: testOrgID, TeamId: team1.Id, UserId: userIds[0], Permission: 0})
|
||||
require.Error(t, err, models.ErrLastTeamAdmin)
|
||||
})
|
||||
|
||||
@@ -259,7 +259,7 @@ func TestTeamCommandsAndQueries(t *testing.T) {
|
||||
|
||||
err = sqlStore.AddTeamMember(userIds[1], testOrgID, team1.Id, false, models.PERMISSION_ADMIN)
|
||||
require.NoError(t, err)
|
||||
err = sqlStore.UpdateTeamMember(context.Background(), &models.UpdateTeamMemberCommand{OrgId: testOrgID, TeamId: team1.Id, UserId: userIds[0], Permission: 0, ProtectLastAdmin: true})
|
||||
err = sqlStore.UpdateTeamMember(context.Background(), &models.UpdateTeamMemberCommand{OrgId: testOrgID, TeamId: team1.Id, UserId: userIds[0], Permission: 0})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user