From a60953c8f970fc61ac3f4ad6e3e3b96c53ee68f2 Mon Sep 17 00:00:00 2001 From: Ieva Date: Wed, 20 Nov 2024 17:37:12 +0000 Subject: [PATCH] GAS: Show mapped roles in role picker (#96681) * add group mapping UID returned mapped roles * request mapped roles from the frontend, but don't attempt to update mapped roles * lock mapped roles and show a pop-up message about why a role is locked * update role selectors to not allow deselecting a mapped role * swagger gen * simplify and set mapped as bool instead of mapping UID array * swagger gen --- pkg/services/accesscontrol/models.go | 1 + public/api-enterprise-spec.json | 3 ++ public/api-merged.json | 3 ++ .../RolePicker/RoleMenuGroupsSection.tsx | 6 +++- .../components/RolePicker/RoleMenuOption.tsx | 16 ++++++++- .../components/RolePicker/RolePickerMenu.tsx | 36 ++++++++++++++----- .../RolePicker/RolePickerSubMenu.tsx | 1 + public/app/core/components/RolePicker/api.ts | 7 ++-- public/app/features/admin/api.ts | 5 ++- public/app/features/users/state/actions.ts | 5 ++- public/app/types/accessControl.ts | 1 + public/openapi3.json | 3 ++ 12 files changed, 72 insertions(+), 15 deletions(-) diff --git a/pkg/services/accesscontrol/models.go b/pkg/services/accesscontrol/models.go index 9971f674a95..dc12171eaa5 100644 --- a/pkg/services/accesscontrol/models.go +++ b/pkg/services/accesscontrol/models.go @@ -81,6 +81,7 @@ type RoleDTO struct { Group string `xorm:"group_name" json:"group"` Permissions []Permission `json:"permissions,omitempty"` Delegatable *bool `json:"delegatable,omitempty"` + Mapped bool `json:"mapped,omitempty"` Hidden bool `json:"hidden,omitempty"` ID int64 `json:"-" xorm:"pk autoincr 'id'"` diff --git a/public/api-enterprise-spec.json b/public/api-enterprise-spec.json index 957d01fcb14..a6da300456c 100644 --- a/public/api-enterprise-spec.json +++ b/public/api-enterprise-spec.json @@ -7123,6 +7123,9 @@ "hidden": { "type": "boolean" }, + "mapped": { + "type": "boolean" + }, "name": { "type": "string" }, diff --git a/public/api-merged.json b/public/api-merged.json index f54a90d84d6..8e1dde8fa74 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -19793,6 +19793,9 @@ "hidden": { "type": "boolean" }, + "mapped": { + "type": "boolean" + }, "name": { "type": "string" }, diff --git a/public/app/core/components/RolePicker/RoleMenuGroupsSection.tsx b/public/app/core/components/RolePicker/RoleMenuGroupsSection.tsx index 90695287595..bae0a0aeedf 100644 --- a/public/app/core/components/RolePicker/RoleMenuGroupsSection.tsx +++ b/public/app/core/components/RolePicker/RoleMenuGroupsSection.tsx @@ -78,7 +78,10 @@ export const RoleMenuGroupsSection = forwardRef + isNotDelegatable(option) || selectedOptions.find((opt) => opt.uid === option.uid && opt.mapped) + )} onChange={onGroupChange} onOpenSubMenu={onOpenSubMenu} onCloseSubMenu={onCloseSubMenu} @@ -102,6 +105,7 @@ export const RoleMenuGroupsSection = forwardRef opt.uid === option.uid))} disabled={isNotDelegatable(option)} + mapped={!!(option.uid && selectedOptions.find((opt) => opt.uid === option.uid && opt.mapped))} onChange={onRoleChange} hideDescription /> diff --git a/public/app/core/components/RolePicker/RoleMenuOption.tsx b/public/app/core/components/RolePicker/RoleMenuOption.tsx index 8fabb22eb8e..b5ef7b02358 100644 --- a/public/app/core/components/RolePicker/RoleMenuOption.tsx +++ b/public/app/core/components/RolePicker/RoleMenuOption.tsx @@ -13,14 +13,23 @@ interface RoleMenuOptionProps { isSelected?: boolean; isFocused?: boolean; disabled?: boolean; + mapped?: boolean; hideDescription?: boolean; } export const RoleMenuOption = forwardRef>( - ({ data, isFocused, isSelected, disabled, onChange, hideDescription }, ref) => { + ({ data, isFocused, isSelected, disabled, mapped, onChange, hideDescription }, ref) => { const theme = useTheme2(); const styles = getSelectStyles(theme); const customStyles = useStyles2(getStyles); + disabled = disabled || mapped; + let disabledMessage = ''; + if (disabled) { + disabledMessage = 'You do not have permissions to assign this role.'; + if (mapped) { + disabledMessage = 'Role assignment cannot be removed because the role is mapped through group sync.'; + } + } const wrapperClassName = cx( styles.option, @@ -51,6 +60,11 @@ export const RoleMenuOption = forwardRef{data.displayName || data.name} {!hideDescription && data.description &&
{data.description}
} + {disabledMessage && ( + + + + )} {data.description && ( diff --git a/public/app/core/components/RolePicker/RolePickerMenu.tsx b/public/app/core/components/RolePicker/RolePickerMenu.tsx index 9ee528c1e9b..4ac739941ea 100644 --- a/public/app/core/components/RolePicker/RolePickerMenu.tsx +++ b/public/app/core/components/RolePicker/RolePickerMenu.tsx @@ -157,8 +157,15 @@ export const RolePickerMenu = ({ return selectedGroupOptions.length > 0 && selectedGroupOptions.length < groupOptions!.options.length; }; + const changeableGroupRolesSelected = (groupType: GroupType, group: string) => { + const selectedGroupOptions = getSelectedGroupOptions(group); + const changeableGroupOptions = selectedGroupOptions.filter((role) => role.delegatable && !role.mapped); + const groupOptions = rolesCollection[groupType]?.optionGroup.find((g) => g.value === group); + return changeableGroupOptions.length > 0 && changeableGroupOptions.length < groupOptions!.options.length; + }; + const onChange = (option: Role) => { - if (selectedOptions.find((role) => role.uid === option.uid)) { + if (selectedOptions.find((role) => role.uid === option.uid && !role.mapped)) { setSelectedOptions(selectedOptions.filter((role) => role.uid !== option.uid)); } else { setSelectedOptions([...selectedOptions, option]); @@ -174,12 +181,21 @@ export const RolePickerMenu = ({ return; } - if (groupSelected(groupType, value) || groupPartiallySelected(groupType, value)) { - setSelectedOptions(selectedOptions.filter((role) => !group.options.find((option) => role.uid === option.uid))); - } else { - const groupOptions = group.options.filter((role) => role.delegatable); + if (groupSelected(groupType, value) || changeableGroupRolesSelected(groupType, value)) { + const mappedGroupOptions = selectedOptions.filter((option) => + group.options.find((role) => role.uid === option.uid && option.mapped) + ); const restOptions = selectedOptions.filter((role) => !group.options.find((option) => role.uid === option.uid)); - setSelectedOptions([...restOptions, ...groupOptions]); + setSelectedOptions([...restOptions, ...mappedGroupOptions]); + } else { + const mappedGroupOptions = selectedOptions.filter((option) => + group.options.find((role) => role.uid === option.uid && role.delegatable) + ); + const groupOptions = group.options.filter( + (role) => role.delegatable && !selectedOptions.find((option) => role.uid === option.uid && option.mapped) + ); + const restOptions = selectedOptions.filter((role) => !group.options.find((option) => role.uid === option.uid)); + setSelectedOptions([...restOptions, ...groupOptions, ...mappedGroupOptions]); } }; @@ -188,13 +204,17 @@ export const RolePickerMenu = ({ }; const onClearInternal = async () => { - setSelectedOptions([]); + const mappedRoles = selectedOptions.filter((role) => role.mapped); + const nonDelegatableRoles = options.filter((role) => + selectedOptions.find((option) => role.uid === option.uid && !role.delegatable) + ); + setSelectedOptions([...mappedRoles, ...nonDelegatableRoles]); }; const onClearSubMenu = (group: string) => { const options = selectedOptions.filter((role) => { const roleGroup = getRoleGroup(role); - return roleGroup !== group; + return roleGroup !== group || role.mapped; }); setSelectedOptions(options); }; diff --git a/public/app/core/components/RolePicker/RolePickerSubMenu.tsx b/public/app/core/components/RolePicker/RolePickerSubMenu.tsx index 647ba41b301..1197796442b 100644 --- a/public/app/core/components/RolePicker/RolePickerSubMenu.tsx +++ b/public/app/core/components/RolePicker/RolePickerSubMenu.tsx @@ -57,6 +57,7 @@ export const RolePickerSubMenu = ({ disabled={ !!(option.uid && disabledOptions?.find((opt) => opt.uid === option.uid)) || isNotDelegatable(option) } + mapped={!!(option.uid && selectedOptions.find((opt) => opt.uid === option.uid && opt.mapped))} onChange={onSelect} hideDescription /> diff --git a/public/app/core/components/RolePicker/api.ts b/public/app/core/components/RolePicker/api.ts index 978fe333c4d..cb282da079a 100644 --- a/public/app/core/components/RolePicker/api.ts +++ b/public/app/core/components/RolePicker/api.ts @@ -16,9 +16,9 @@ export const fetchRoleOptions = async (orgId?: number): Promise => { }; export const fetchUserRoles = async (userId: number, orgId?: number): Promise => { - let userRolesUrl = `/api/access-control/users/${userId}/roles`; + let userRolesUrl = `/api/access-control/users/${userId}/roles?includeMapped=true`; if (orgId) { - userRolesUrl += `?targetOrgId=${orgId}`; + userRolesUrl += `&targetOrgId=${orgId}`; } try { const roles = await getBackendSrv().get(userRolesUrl); @@ -39,7 +39,8 @@ export const updateUserRoles = (roles: Role[], userId: number, orgId?: number) = if (orgId) { userRolesUrl += `?targetOrgId=${orgId}`; } - const roleUids = roles.flatMap((x) => x.uid); + const filteredRoles = roles.filter((role) => !role.mapped); + const roleUids = filteredRoles.flatMap((x) => x.uid); return getBackendSrv().put(userRolesUrl, { orgId, roleUids, diff --git a/public/app/features/admin/api.ts b/public/app/features/admin/api.ts index 49542e53b36..0db8de7ed13 100644 --- a/public/app/features/admin/api.ts +++ b/public/app/features/admin/api.ts @@ -19,7 +19,10 @@ export const getOrgUsers = async (orgId: UrlQueryValue, page: number) => { export const getUsersRoles = async (orgId: number, users: OrgUser[]) => { const userIds = users.map((u) => u.userId); - const roles = await getBackendSrv().post(`/api/access-control/users/roles/search`, { userIds, orgId }); + const roles = await getBackendSrv().post(`/api/access-control/users/roles/search?includeMapped=true`, { + userIds, + orgId, + }); users.forEach((u) => { u.roles = roles ? roles[u.userId] || [] : []; }); diff --git a/public/app/features/users/state/actions.ts b/public/app/features/users/state/actions.ts index a0f52473e1b..9dbc7672f63 100644 --- a/public/app/features/users/state/actions.ts +++ b/public/app/features/users/state/actions.ts @@ -36,7 +36,10 @@ export function loadUsers(): ThunkResult { dispatch(rolesFetchBegin()); const orgId = contextSrv.user.orgId; const userIds = users?.orgUsers.map((u: OrgUser) => u.userId); - const roles = await getBackendSrv().post(`/api/access-control/users/roles/search`, { userIds, orgId }); + const roles = await getBackendSrv().post(`/api/access-control/users/roles/search?includeMapped=true`, { + userIds, + orgId, + }); users.orgUsers.forEach((u: OrgUser) => { u.roles = roles ? roles[u.userId] || [] : []; }); diff --git a/public/app/types/accessControl.ts b/public/app/types/accessControl.ts index 4ae6fcfda75..e5148bf8758 100644 --- a/public/app/types/accessControl.ts +++ b/public/app/types/accessControl.ts @@ -167,6 +167,7 @@ export interface Role { group: string; global: boolean; delegatable?: boolean; + mapped?: boolean; version: number; created: string; updated: string; diff --git a/public/openapi3.json b/public/openapi3.json index 67c19b89eae..3552f7d812a 100644 --- a/public/openapi3.json +++ b/public/openapi3.json @@ -9751,6 +9751,9 @@ "hidden": { "type": "boolean" }, + "mapped": { + "type": "boolean" + }, "name": { "type": "string" },