SCIM - Allow groups to be deleted if SCIM group sync is disabled (#111888)

* Load SCIM configuration before running changes in teams

* Update tests

* Allow to delete groups if SCIM group sync is disabled

* Update docs
This commit is contained in:
linoman 2025-10-01 18:07:18 +02:00 committed by GitHub
parent 9bde3267bb
commit f01b1131e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 56 additions and 8 deletions

View File

@ -223,7 +223,7 @@ Team provisioning requires `group_sync_enabled = true` in the SCIM configuration
{{< /admonition >}}
{{< admonition type="warning" >}}
Teams provisioned through SCIM cannot be deleted manually from Grafana - they can only be deleted by removing their corresponding groups from the identity provider.
Teams provisioned through SCIM cannot be deleted manually from Grafana - they can only be deleted by removing their corresponding groups from the identity provider. Optionally, you can disable SCIM group sync to allow manual deletion of teams.
{{< /admonition >}}
For detailed configuration steps specific to the identity provider, see:

View File

@ -431,7 +431,8 @@ func (tapi *TeamAPI) validateTeam(c *contextmodel.ReqContext, teamID int64, prov
return response.Error(http.StatusInternalServerError, "Failed to get Team", err)
}
if teamDTO.IsProvisioned {
isGroupSyncEnabled := tapi.cfg.Raw.Section("auth.scim").Key("group_sync_enabled").MustBool(false)
if isGroupSyncEnabled && teamDTO.IsProvisioned {
return response.Error(http.StatusBadRequest, provisionedMessage, err)
}

View File

@ -299,7 +299,8 @@ func (tapi *TeamAPI) removeTeamMember(c *contextmodel.ReqContext) response.Respo
return response.Error(http.StatusInternalServerError, "Failed to get Team", err)
}
if existingTeam.IsProvisioned {
isGroupSyncEnabled := tapi.cfg.Raw.Section("auth.scim").Key("group_sync_enabled").MustBool(false)
if isGroupSyncEnabled && existingTeam.IsProvisioned {
return response.Error(http.StatusBadRequest, "Team memberships cannot be updated for provisioned teams", err)
}

View File

@ -169,13 +169,15 @@ func TestUpdateTeamMembersAPIEndpoint(t *testing.T) {
})
}
func TestUpdateTeamMembersFromProvisionedTeam(t *testing.T) {
func TestUpdateTeamMembersFromProvisionedTeamWhenGroupSyncIsEnabled(t *testing.T) {
server := SetupAPITestServer(t, &teamtest.FakeService{
ExpectedIsMember: true,
ExpectedTeamDTO: &team.TeamDTO{ID: 1, UID: "a00001", IsProvisioned: true},
}, func(tapi *TeamAPI) {
tapi.cfg.Raw.Section("auth.scim").Key("group_sync_enabled").SetValue("true")
})
t.Run("should not be able to update team member from a provisioned team", func(t *testing.T) {
t.Run("should not be able to update team member from a provisioned team if team sync is enabled", 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"}}),
@ -186,7 +188,7 @@ func TestUpdateTeamMembersFromProvisionedTeam(t *testing.T) {
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) {
t.Run("should not be able to update team member from a provisioned team by team UID if team sync is enabled", 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"}}),
@ -198,6 +200,27 @@ func TestUpdateTeamMembersFromProvisionedTeam(t *testing.T) {
})
}
func TestUpdateTeamMembersFromProvisionedTeamWhenGroupSyncIsDisabled(t *testing.T) {
t.Run("should be able to delete team member from a provisioned team when SCIM group sync is disabled", func(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{}
})
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.StatusOK, res.StatusCode)
require.NoError(t, res.Body.Close())
})
}
func TestDeleteTeamMembersAPIEndpoint(t *testing.T) {
server := SetupAPITestServer(t, nil, func(hs *TeamAPI) {
hs.teamService = &teamtest.FakeService{
@ -236,6 +259,8 @@ func TestDeleteTeamMembersFromProvisionedTeam(t *testing.T) {
ExpectedTeamDTO: &team.TeamDTO{ID: 1, UID: "a00001", IsProvisioned: true},
}
hs.teamPermissionsService = &actest.FakePermissionsService{}
}, func(hs *TeamAPI) {
hs.cfg.Raw.Section("auth.scim").Key("group_sync_enabled").SetValue("true")
})
t.Run("should not be able to delete team member from a provisioned team", func(t *testing.T) {

View File

@ -5,6 +5,7 @@ import { connect, ConnectedProps } from 'react-redux';
import { GrafanaTheme2 } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { config, getBackendSrv } from '@grafana/runtime';
import {
Avatar,
CellProps,
@ -65,6 +66,7 @@ export const TeamList = ({
changeSort,
}: Props) => {
const [roleOptions, setRoleOptions] = useState<Role[]>([]);
const [scimGroupSyncEnabled, setScimGroupSyncEnabled] = useState(false);
const styles = useStyles2(getStyles);
useEffect(() => {
@ -77,6 +79,25 @@ export const TeamList = ({
}
}, []);
useEffect(() => {
const checkSCIMSettings = async () => {
if (!config.featureToggles.enableSCIM) {
setScimGroupSyncEnabled(false);
return;
}
try {
const scimSettings = await getBackendSrv().get(
`/apis/scim.grafana.app/v0alpha1/namespaces/${config.namespace}/config`
);
setScimGroupSyncEnabled(scimSettings?.items[0]?.spec?.enableGroupSync || false);
} catch {
setScimGroupSyncEnabled(false);
}
};
checkSCIMSettings();
}, []);
const canCreate = contextSrv.hasPermission(AccessControlAction.ActionTeamsCreate);
const displayRolePicker = shouldDisplayRolePicker();
@ -198,7 +219,7 @@ export const TeamList = ({
const canReadTeam = contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsRead, original);
const canDelete =
contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsDelete, original) &&
!original.isProvisioned;
(!scimGroupSyncEnabled || !original.isProvisioned);
return (
<Stack direction="row" justifyContent="flex-end" gap={2}>
{canReadTeam && (
@ -226,7 +247,7 @@ export const TeamList = ({
},
},
],
[displayRolePicker, hasFetched, rolesLoading, roleOptions, deleteTeam, styles]
[displayRolePicker, hasFetched, rolesLoading, roleOptions, deleteTeam, styles, scimGroupSyncEnabled]
);
return (