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:
Gabriel MABILLE
2022-01-26 14:48:41 +00:00
committed by GitHub
co-authored by ievaVasiljeva gamab
parent 46422a82c8
commit d4f682190f
14 changed files with 615 additions and 145 deletions
+113 -60
View File
@@ -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{}
+5 -5
View File
@@ -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)
})
})