diff --git a/pkg/services/accesscontrol/ossaccesscontrol/team.go b/pkg/services/accesscontrol/ossaccesscontrol/team.go
index a2635987fb3..2cbeb4e0174 100644
--- a/pkg/services/accesscontrol/ossaccesscontrol/team.go
+++ b/pkg/services/accesscontrol/ossaccesscontrol/team.go
@@ -2,6 +2,7 @@ package ossaccesscontrol
import (
"context"
+ "errors"
"fmt"
"strconv"
@@ -54,7 +55,7 @@ func ProvideTeamPermissions(
return err
}
- _, err = teamService.GetTeamByID(ctx, &team.GetTeamByIDQuery{
+ existingTeam, err := teamService.GetTeamByID(ctx, &team.GetTeamByIDQuery{
OrgID: orgID,
ID: id,
})
@@ -62,6 +63,10 @@ func ProvideTeamPermissions(
return err
}
+ if existingTeam.IsProvisioned {
+ return errors.New("team permissions cannot be updated for provisioned teams")
+ }
+
return nil
},
Assignments: resourcepermissions.Assignments{
diff --git a/pkg/services/team/teamapi/team.go b/pkg/services/team/teamapi/team.go
index 902bfd8eb30..97bb70e5d47 100644
--- a/pkg/services/team/teamapi/team.go
+++ b/pkg/services/team/teamapi/team.go
@@ -89,6 +89,19 @@ func (tapi *TeamAPI) updateTeam(c *contextmodel.ReqContext) response.Response {
return response.Error(http.StatusBadRequest, "teamId is invalid", err)
}
+ existingTeam, err := tapi.getTeamDTOByID(c, cmd.ID)
+ if err != nil {
+ if errors.Is(err, team.ErrTeamNotFound) {
+ return response.Error(http.StatusNotFound, "Team not found", err)
+ }
+
+ return response.Error(http.StatusInternalServerError, "Failed to get Team", err)
+ }
+
+ if existingTeam.IsProvisioned && existingTeam.Name != cmd.Name {
+ return response.Error(http.StatusBadRequest, "Team name cannot be changed for provisioned teams", err)
+ }
+
if err := tapi.teamService.UpdateTeam(c.Req.Context(), &cmd); err != nil {
if errors.Is(err, team.ErrTeamNameTaken) {
return response.Error(http.StatusBadRequest, "Team name taken", err)
@@ -116,6 +129,11 @@ func (tapi *TeamAPI) deleteTeamByID(c *contextmodel.ReqContext) response.Respons
return response.Error(http.StatusBadRequest, "teamId is invalid", err)
}
+ resp := tapi.validateTeam(c, teamID, "Cannot delete provisioned teams")
+ if resp != nil {
+ return resp
+ }
+
if err := tapi.teamService.DeleteTeam(c.Req.Context(), &team.DeleteTeamCommand{OrgID: orgID, ID: teamID}); err != nil {
if errors.Is(err, team.ErrTeamNotFound) {
return response.Error(http.StatusNotFound, "Failed to delete Team. ID not found", nil)
@@ -392,3 +410,30 @@ func (tapi *TeamAPI) getAccessControlMetadata(c *contextmodel.ReqContext,
ids := map[string]bool{resourceID: true}
return tapi.getMultiAccessControlMetadata(c, prefix, ids)[resourceID]
}
+
+func (tapi *TeamAPI) getTeamDTOByID(c *contextmodel.ReqContext, teamID int64) (*team.TeamDTO, error) {
+ query := team.GetTeamByIDQuery{
+ OrgID: c.SignedInUser.GetOrgID(),
+ ID: teamID,
+ SignedInUser: c.SignedInUser,
+ }
+
+ return tapi.teamService.GetTeamByID(c.Req.Context(), &query)
+}
+
+func (tapi *TeamAPI) validateTeam(c *contextmodel.ReqContext, teamID int64, provisionedMessage string) *response.NormalResponse {
+ teamDTO, err := tapi.getTeamDTOByID(c, teamID)
+ if err != nil {
+ if errors.Is(err, team.ErrTeamNotFound) {
+ return response.Error(http.StatusNotFound, "Team not found", err)
+ }
+
+ return response.Error(http.StatusInternalServerError, "Failed to get Team", err)
+ }
+
+ if teamDTO.IsProvisioned {
+ return response.Error(http.StatusBadRequest, provisionedMessage, err)
+ }
+
+ return nil
+}
diff --git a/pkg/services/team/teamapi/team_members.go b/pkg/services/team/teamapi/team_members.go
index c2b796aaf58..05486473323 100644
--- a/pkg/services/team/teamapi/team_members.go
+++ b/pkg/services/team/teamapi/team_members.go
@@ -83,6 +83,11 @@ func (tapi *TeamAPI) addTeamMember(c *contextmodel.ReqContext) response.Response
return response.Error(http.StatusBadRequest, "teamId is invalid", err)
}
+ resp := tapi.validateTeam(c, teamID, "Team memberships cannot be updated for provisioned teams")
+ if resp != nil {
+ return resp
+ }
+
isTeamMember, err := tapi.teamService.IsTeamMember(c.Req.Context(), c.SignedInUser.GetOrgID(), teamID, cmd.UserID)
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to add team member.", err)
@@ -129,6 +134,11 @@ func (tapi *TeamAPI) updateTeamMember(c *contextmodel.ReqContext) response.Respo
}
orgId := c.SignedInUser.GetOrgID()
+ resp := tapi.validateTeam(c, teamId, "Team memberships cannot be updated for provisioned teams")
+ if resp != nil {
+ return resp
+ }
+
isTeamMember, err := tapi.teamService.IsTeamMember(c.Req.Context(), orgId, teamId, userId)
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to update team member.", err)
@@ -168,6 +178,11 @@ func (tapi *TeamAPI) setTeamMemberships(c *contextmodel.ReqContext) response.Res
}
orgId := c.SignedInUser.GetOrgID()
+ resp := tapi.validateTeam(c, teamId, "Team memberships cannot be updated for provisioned teams")
+ if resp != nil {
+ return resp
+ }
+
teamMemberships, err := tapi.getTeamMembershipUpdates(c.Req.Context(), orgId, teamId, cmd, c.SignedInUser)
if err != nil {
if errors.Is(err, user.ErrUserNotFound) || errors.Is(err, team.ErrTeamNotFound) {
@@ -275,6 +290,19 @@ func (tapi *TeamAPI) removeTeamMember(c *contextmodel.ReqContext) response.Respo
return response.Error(http.StatusBadRequest, "userId is invalid", err)
}
+ existingTeam, err := tapi.getTeamDTOByID(c, teamId)
+ if err != nil {
+ if errors.Is(err, team.ErrTeamNotFound) {
+ return response.Error(http.StatusNotFound, "Team not found", err)
+ }
+
+ return response.Error(http.StatusInternalServerError, "Failed to get Team", err)
+ }
+
+ if existingTeam.IsProvisioned {
+ return response.Error(http.StatusBadRequest, "Team memberships cannot be updated for provisioned teams", err)
+ }
+
teamIDString := strconv.FormatInt(teamId, 10)
if _, err := tapi.teamPermissionsService.SetUserPermission(c.Req.Context(), orgId, accesscontrol.User{ID: userId}, teamIDString, ""); err != nil {
if errors.Is(err, team.ErrTeamNotFound) {
diff --git a/pkg/services/team/teamapi/team_members_test.go b/pkg/services/team/teamapi/team_members_test.go
index 23c241434fb..f14c01d1c73 100644
--- a/pkg/services/team/teamapi/team_members_test.go
+++ b/pkg/services/team/teamapi/team_members_test.go
@@ -169,9 +169,41 @@ func TestUpdateTeamMembersAPIEndpoint(t *testing.T) {
})
}
+func TestUpdateTeamMembersFromProvisionedTeam(t *testing.T) {
+ server := SetupAPITestServer(t, &teamtest.FakeService{
+ ExpectedIsMember: true,
+ ExpectedTeamDTO: &team.TeamDTO{ID: 1, UID: "a00001", IsProvisioned: true},
+ })
+
+ t.Run("should not be able to update team member from a provisioned team", func(t *testing.T) {
+ req := webtest.RequestWithSignedInUser(
+ server.NewRequest(http.MethodPut, "/api/teams/1/members/1", strings.NewReader("{\"permission\": 1}")),
+ authedUserWithPermissions(1, 1, []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsPermissionsWrite, Scope: "teams:id:1"}}),
+ )
+ res, err := server.SendJSON(req)
+ require.NoError(t, err)
+ assert.Equal(t, http.StatusBadRequest, res.StatusCode)
+ require.NoError(t, res.Body.Close())
+ })
+
+ t.Run("should not be able to update team member from a provisioned team by team UID", func(t *testing.T) {
+ req := webtest.RequestWithSignedInUser(
+ server.NewRequest(http.MethodPut, "/api/teams/a00001/members/1", strings.NewReader("{\"permission\": 1}")),
+ authedUserWithPermissions(1, 1, []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsPermissionsWrite, Scope: "teams:id:1"}}),
+ )
+ res, err := server.SendJSON(req)
+ require.NoError(t, err)
+ assert.Equal(t, http.StatusBadRequest, res.StatusCode)
+ require.NoError(t, res.Body.Close())
+ })
+}
+
func TestDeleteTeamMembersAPIEndpoint(t *testing.T) {
server := SetupAPITestServer(t, nil, func(hs *TeamAPI) {
- hs.teamService = &teamtest.FakeService{ExpectedIsMember: true}
+ hs.teamService = &teamtest.FakeService{
+ ExpectedIsMember: true,
+ ExpectedTeamDTO: &team.TeamDTO{ID: 1, UID: "a00001"},
+ }
hs.teamPermissionsService = &actest.FakePermissionsService{}
})
@@ -197,6 +229,27 @@ func TestDeleteTeamMembersAPIEndpoint(t *testing.T) {
})
}
+func TestDeleteTeamMembersFromProvisionedTeam(t *testing.T) {
+ server := SetupAPITestServer(t, nil, func(hs *TeamAPI) {
+ hs.teamService = &teamtest.FakeService{
+ ExpectedIsMember: true,
+ ExpectedTeamDTO: &team.TeamDTO{ID: 1, UID: "a00001", IsProvisioned: true},
+ }
+ hs.teamPermissionsService = &actest.FakePermissionsService{}
+ })
+
+ t.Run("should not be able to delete team member from a provisioned team", func(t *testing.T) {
+ req := webtest.RequestWithSignedInUser(
+ server.NewRequest(http.MethodDelete, "/api/teams/1/members/1", nil),
+ authedUserWithPermissions(1, 1, []accesscontrol.Permission{{Action: accesscontrol.ActionTeamsPermissionsWrite, Scope: "teams:id:1"}}),
+ )
+ res, err := server.SendJSON(req)
+ require.NoError(t, err)
+ assert.Equal(t, http.StatusBadRequest, res.StatusCode)
+ require.NoError(t, res.Body.Close())
+ })
+}
+
func Test_getTeamMembershipUpdates(t *testing.T) {
type testCase struct {
description string
diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx
index 135151d4b7f..df9c99f210a 100644
--- a/public/app/features/teams/TeamList.tsx
+++ b/public/app/features/teams/TeamList.tsx
@@ -16,6 +16,7 @@ import {
LinkButton,
Pagination,
Stack,
+ Tag,
TextLink,
useStyles2,
} from '@grafana/ui';
@@ -43,6 +44,7 @@ const skeletonData: TeamWithRoles[] = new Array(3).fill(null).map((_, index) =>
memberCount: 0,
name: '',
orgId: 0,
+ isProvisioned: false,
}));
export const TeamList = ({
@@ -166,6 +168,16 @@ export const TeamList = ({
},
]
: []),
+ {
+ id: 'isProvisioned',
+ header: '',
+ cell: ({ cell: { value } }: Cell<'isProvisioned'>) => {
+ if (!hasFetched) {
+ return ;
+ }
+ return !!value &&
+ },
+ },
{
id: 'actions',
header: '',
@@ -181,7 +193,7 @@ export const TeamList = ({
}
const canReadTeam = contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsRead, original);
- const canDelete = contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsDelete, original);
+ const canDelete = contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsDelete, original) && !original.isProvisioned;
return (
{canReadTeam && (
diff --git a/public/app/features/teams/TeamPermissions.tsx b/public/app/features/teams/TeamPermissions.tsx
index ac7873b5ffb..349b2531dab 100644
--- a/public/app/features/teams/TeamPermissions.tsx
+++ b/public/app/features/teams/TeamPermissions.tsx
@@ -9,11 +9,15 @@ type TeamPermissionsProps = {
// TeamPermissions component replaces TeamMembers component when the accesscontrol feature flag is set
const TeamPermissions = (props: TeamPermissionsProps) => {
- const canSetPermissions = contextSrv.hasPermissionInMetadata(
+ let canSetPermissions = contextSrv.hasPermissionInMetadata(
AccessControlAction.ActionTeamsPermissionsWrite,
props.team
);
+ if (props.team.isProvisioned) {
+ canSetPermissions = false;
+ }
+
return (
{
{
memberCount: i,
accessControl: { isEditor: false },
orgId: 0,
+ isProvisioned: false,
...overrides,
};
};
diff --git a/public/app/features/teams/state/navModel.ts b/public/app/features/teams/state/navModel.ts
index 5205e8aac98..4b225312b40 100644
--- a/public/app/features/teams/state/navModel.ts
+++ b/public/app/features/teams/state/navModel.ts
@@ -17,6 +17,7 @@ const loadingTeam = {
accessControl: { isEditor: false },
orgId: 0,
updated: '',
+ isProvisioned: false,
};
export function buildNavModel(team: Team): NavModelItem {
diff --git a/public/app/types/teams.ts b/public/app/types/teams.ts
index 26f4e6f3aa7..123954636bc 100644
--- a/public/app/types/teams.ts
+++ b/public/app/types/teams.ts
@@ -44,6 +44,10 @@ export interface Team extends WithAccessControlMetadata {
* OrgId is the ID of an organisation the team belongs to.
*/
orgId: number;
+ /**
+ * isProvisioned is set if the team has been provisioned from IdP.
+ */
+ isProvisioned: boolean;
}
export interface TeamWithRoles extends Team {