mirror of https://github.com/grafana/grafana.git
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:
parent
9bde3267bb
commit
f01b1131e7
|
@ -223,7 +223,7 @@ Team provisioning requires `group_sync_enabled = true` in the SCIM configuration
|
||||||
{{< /admonition >}}
|
{{< /admonition >}}
|
||||||
|
|
||||||
{{< admonition type="warning" >}}
|
{{< 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 >}}
|
{{< /admonition >}}
|
||||||
|
|
||||||
For detailed configuration steps specific to the identity provider, see:
|
For detailed configuration steps specific to the identity provider, see:
|
||||||
|
|
|
@ -431,7 +431,8 @@ func (tapi *TeamAPI) validateTeam(c *contextmodel.ReqContext, teamID int64, prov
|
||||||
return response.Error(http.StatusInternalServerError, "Failed to get Team", err)
|
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)
|
return response.Error(http.StatusBadRequest, provisionedMessage, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -299,7 +299,8 @@ func (tapi *TeamAPI) removeTeamMember(c *contextmodel.ReqContext) response.Respo
|
||||||
return response.Error(http.StatusInternalServerError, "Failed to get Team", err)
|
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)
|
return response.Error(http.StatusBadRequest, "Team memberships cannot be updated for provisioned teams", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -169,13 +169,15 @@ func TestUpdateTeamMembersAPIEndpoint(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUpdateTeamMembersFromProvisionedTeam(t *testing.T) {
|
func TestUpdateTeamMembersFromProvisionedTeamWhenGroupSyncIsEnabled(t *testing.T) {
|
||||||
server := SetupAPITestServer(t, &teamtest.FakeService{
|
server := SetupAPITestServer(t, &teamtest.FakeService{
|
||||||
ExpectedIsMember: true,
|
ExpectedIsMember: true,
|
||||||
ExpectedTeamDTO: &team.TeamDTO{ID: 1, UID: "a00001", IsProvisioned: 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(
|
req := webtest.RequestWithSignedInUser(
|
||||||
server.NewRequest(http.MethodPut, "/api/teams/1/members/1", strings.NewReader("{\"permission\": 1}")),
|
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"}}),
|
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())
|
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(
|
req := webtest.RequestWithSignedInUser(
|
||||||
server.NewRequest(http.MethodPut, "/api/teams/a00001/members/1", strings.NewReader("{\"permission\": 1}")),
|
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"}}),
|
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) {
|
func TestDeleteTeamMembersAPIEndpoint(t *testing.T) {
|
||||||
server := SetupAPITestServer(t, nil, func(hs *TeamAPI) {
|
server := SetupAPITestServer(t, nil, func(hs *TeamAPI) {
|
||||||
hs.teamService = &teamtest.FakeService{
|
hs.teamService = &teamtest.FakeService{
|
||||||
|
@ -236,6 +259,8 @@ func TestDeleteTeamMembersFromProvisionedTeam(t *testing.T) {
|
||||||
ExpectedTeamDTO: &team.TeamDTO{ID: 1, UID: "a00001", IsProvisioned: true},
|
ExpectedTeamDTO: &team.TeamDTO{ID: 1, UID: "a00001", IsProvisioned: true},
|
||||||
}
|
}
|
||||||
hs.teamPermissionsService = &actest.FakePermissionsService{}
|
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) {
|
t.Run("should not be able to delete team member from a provisioned team", func(t *testing.T) {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { connect, ConnectedProps } from 'react-redux';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Trans, t } from '@grafana/i18n';
|
import { Trans, t } from '@grafana/i18n';
|
||||||
|
import { config, getBackendSrv } from '@grafana/runtime';
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
CellProps,
|
CellProps,
|
||||||
|
@ -65,6 +66,7 @@ export const TeamList = ({
|
||||||
changeSort,
|
changeSort,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [roleOptions, setRoleOptions] = useState<Role[]>([]);
|
const [roleOptions, setRoleOptions] = useState<Role[]>([]);
|
||||||
|
const [scimGroupSyncEnabled, setScimGroupSyncEnabled] = useState(false);
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
useEffect(() => {
|
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 canCreate = contextSrv.hasPermission(AccessControlAction.ActionTeamsCreate);
|
||||||
const displayRolePicker = shouldDisplayRolePicker();
|
const displayRolePicker = shouldDisplayRolePicker();
|
||||||
|
|
||||||
|
@ -198,7 +219,7 @@ export const TeamList = ({
|
||||||
const canReadTeam = contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsRead, original);
|
const canReadTeam = contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsRead, original);
|
||||||
const canDelete =
|
const canDelete =
|
||||||
contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsDelete, original) &&
|
contextSrv.hasPermissionInMetadata(AccessControlAction.ActionTeamsDelete, original) &&
|
||||||
!original.isProvisioned;
|
(!scimGroupSyncEnabled || !original.isProvisioned);
|
||||||
return (
|
return (
|
||||||
<Stack direction="row" justifyContent="flex-end" gap={2}>
|
<Stack direction="row" justifyContent="flex-end" gap={2}>
|
||||||
{canReadTeam && (
|
{canReadTeam && (
|
||||||
|
@ -226,7 +247,7 @@ export const TeamList = ({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[displayRolePicker, hasFetched, rolesLoading, roleOptions, deleteTeam, styles]
|
[displayRolePicker, hasFetched, rolesLoading, roleOptions, deleteTeam, styles, scimGroupSyncEnabled]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
Loading…
Reference in New Issue