mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
e06ad2a6ef
commit
a60953c8f9
@ -81,6 +81,7 @@ type RoleDTO struct {
|
|||||||
Group string `xorm:"group_name" json:"group"`
|
Group string `xorm:"group_name" json:"group"`
|
||||||
Permissions []Permission `json:"permissions,omitempty"`
|
Permissions []Permission `json:"permissions,omitempty"`
|
||||||
Delegatable *bool `json:"delegatable,omitempty"`
|
Delegatable *bool `json:"delegatable,omitempty"`
|
||||||
|
Mapped bool `json:"mapped,omitempty"`
|
||||||
Hidden bool `json:"hidden,omitempty"`
|
Hidden bool `json:"hidden,omitempty"`
|
||||||
|
|
||||||
ID int64 `json:"-" xorm:"pk autoincr 'id'"`
|
ID int64 `json:"-" xorm:"pk autoincr 'id'"`
|
||||||
|
@ -7123,6 +7123,9 @@
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"mapped": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
@ -19793,6 +19793,9 @@
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"mapped": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
@ -78,7 +78,10 @@ export const RoleMenuGroupsSection = forwardRef<HTMLDivElement, RoleMenuGroupsSe
|
|||||||
value={groupOption.value}
|
value={groupOption.value}
|
||||||
isSelected={groupSelected(groupOption.value) || groupPartiallySelected(groupOption.value)}
|
isSelected={groupSelected(groupOption.value) || groupPartiallySelected(groupOption.value)}
|
||||||
partiallySelected={groupPartiallySelected(groupOption.value)}
|
partiallySelected={groupPartiallySelected(groupOption.value)}
|
||||||
disabled={groupOption.options?.every(isNotDelegatable)}
|
disabled={groupOption.options?.every(
|
||||||
|
(option) =>
|
||||||
|
isNotDelegatable(option) || selectedOptions.find((opt) => opt.uid === option.uid && opt.mapped)
|
||||||
|
)}
|
||||||
onChange={onGroupChange}
|
onChange={onGroupChange}
|
||||||
onOpenSubMenu={onOpenSubMenu}
|
onOpenSubMenu={onOpenSubMenu}
|
||||||
onCloseSubMenu={onCloseSubMenu}
|
onCloseSubMenu={onCloseSubMenu}
|
||||||
@ -102,6 +105,7 @@ export const RoleMenuGroupsSection = forwardRef<HTMLDivElement, RoleMenuGroupsSe
|
|||||||
key={option.uid}
|
key={option.uid}
|
||||||
isSelected={!!(option.uid && !!selectedOptions.find((opt) => opt.uid === option.uid))}
|
isSelected={!!(option.uid && !!selectedOptions.find((opt) => opt.uid === option.uid))}
|
||||||
disabled={isNotDelegatable(option)}
|
disabled={isNotDelegatable(option)}
|
||||||
|
mapped={!!(option.uid && selectedOptions.find((opt) => opt.uid === option.uid && opt.mapped))}
|
||||||
onChange={onRoleChange}
|
onChange={onRoleChange}
|
||||||
hideDescription
|
hideDescription
|
||||||
/>
|
/>
|
||||||
|
@ -13,14 +13,23 @@ interface RoleMenuOptionProps {
|
|||||||
isSelected?: boolean;
|
isSelected?: boolean;
|
||||||
isFocused?: boolean;
|
isFocused?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
mapped?: boolean;
|
||||||
hideDescription?: boolean;
|
hideDescription?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RoleMenuOption = forwardRef<HTMLDivElement, React.PropsWithChildren<RoleMenuOptionProps>>(
|
export const RoleMenuOption = forwardRef<HTMLDivElement, React.PropsWithChildren<RoleMenuOptionProps>>(
|
||||||
({ data, isFocused, isSelected, disabled, onChange, hideDescription }, ref) => {
|
({ data, isFocused, isSelected, disabled, mapped, onChange, hideDescription }, ref) => {
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const styles = getSelectStyles(theme);
|
const styles = getSelectStyles(theme);
|
||||||
const customStyles = useStyles2(getStyles);
|
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(
|
const wrapperClassName = cx(
|
||||||
styles.option,
|
styles.option,
|
||||||
@ -51,6 +60,11 @@ export const RoleMenuOption = forwardRef<HTMLDivElement, React.PropsWithChildren
|
|||||||
<span>{data.displayName || data.name}</span>
|
<span>{data.displayName || data.name}</span>
|
||||||
{!hideDescription && data.description && <div className={styles.optionDescription}>{data.description}</div>}
|
{!hideDescription && data.description && <div className={styles.optionDescription}>{data.description}</div>}
|
||||||
</div>
|
</div>
|
||||||
|
{disabledMessage && (
|
||||||
|
<Tooltip content={disabledMessage}>
|
||||||
|
<Icon name="lock" />
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
{data.description && (
|
{data.description && (
|
||||||
<Tooltip content={data.description}>
|
<Tooltip content={data.description}>
|
||||||
<Icon name="info-circle" className={customStyles.menuOptionInfoSign} />
|
<Icon name="info-circle" className={customStyles.menuOptionInfoSign} />
|
||||||
|
@ -157,8 +157,15 @@ export const RolePickerMenu = ({
|
|||||||
return selectedGroupOptions.length > 0 && selectedGroupOptions.length < groupOptions!.options.length;
|
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) => {
|
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));
|
setSelectedOptions(selectedOptions.filter((role) => role.uid !== option.uid));
|
||||||
} else {
|
} else {
|
||||||
setSelectedOptions([...selectedOptions, option]);
|
setSelectedOptions([...selectedOptions, option]);
|
||||||
@ -174,12 +181,21 @@ export const RolePickerMenu = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (groupSelected(groupType, value) || groupPartiallySelected(groupType, value)) {
|
if (groupSelected(groupType, value) || changeableGroupRolesSelected(groupType, value)) {
|
||||||
setSelectedOptions(selectedOptions.filter((role) => !group.options.find((option) => role.uid === option.uid)));
|
const mappedGroupOptions = selectedOptions.filter((option) =>
|
||||||
} else {
|
group.options.find((role) => role.uid === option.uid && option.mapped)
|
||||||
const groupOptions = group.options.filter((role) => role.delegatable);
|
);
|
||||||
const restOptions = selectedOptions.filter((role) => !group.options.find((option) => role.uid === option.uid));
|
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 () => {
|
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 onClearSubMenu = (group: string) => {
|
||||||
const options = selectedOptions.filter((role) => {
|
const options = selectedOptions.filter((role) => {
|
||||||
const roleGroup = getRoleGroup(role);
|
const roleGroup = getRoleGroup(role);
|
||||||
return roleGroup !== group;
|
return roleGroup !== group || role.mapped;
|
||||||
});
|
});
|
||||||
setSelectedOptions(options);
|
setSelectedOptions(options);
|
||||||
};
|
};
|
||||||
|
@ -57,6 +57,7 @@ export const RolePickerSubMenu = ({
|
|||||||
disabled={
|
disabled={
|
||||||
!!(option.uid && disabledOptions?.find((opt) => opt.uid === option.uid)) || isNotDelegatable(option)
|
!!(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}
|
onChange={onSelect}
|
||||||
hideDescription
|
hideDescription
|
||||||
/>
|
/>
|
||||||
|
@ -16,9 +16,9 @@ export const fetchRoleOptions = async (orgId?: number): Promise<Role[]> => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const fetchUserRoles = async (userId: number, orgId?: number): Promise<Role[]> => {
|
export const fetchUserRoles = async (userId: number, orgId?: number): Promise<Role[]> => {
|
||||||
let userRolesUrl = `/api/access-control/users/${userId}/roles`;
|
let userRolesUrl = `/api/access-control/users/${userId}/roles?includeMapped=true`;
|
||||||
if (orgId) {
|
if (orgId) {
|
||||||
userRolesUrl += `?targetOrgId=${orgId}`;
|
userRolesUrl += `&targetOrgId=${orgId}`;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const roles = await getBackendSrv().get(userRolesUrl);
|
const roles = await getBackendSrv().get(userRolesUrl);
|
||||||
@ -39,7 +39,8 @@ export const updateUserRoles = (roles: Role[], userId: number, orgId?: number) =
|
|||||||
if (orgId) {
|
if (orgId) {
|
||||||
userRolesUrl += `?targetOrgId=${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, {
|
return getBackendSrv().put(userRolesUrl, {
|
||||||
orgId,
|
orgId,
|
||||||
roleUids,
|
roleUids,
|
||||||
|
@ -19,7 +19,10 @@ export const getOrgUsers = async (orgId: UrlQueryValue, page: number) => {
|
|||||||
|
|
||||||
export const getUsersRoles = async (orgId: number, users: OrgUser[]) => {
|
export const getUsersRoles = async (orgId: number, users: OrgUser[]) => {
|
||||||
const userIds = users.map((u) => u.userId);
|
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) => {
|
users.forEach((u) => {
|
||||||
u.roles = roles ? roles[u.userId] || [] : [];
|
u.roles = roles ? roles[u.userId] || [] : [];
|
||||||
});
|
});
|
||||||
|
@ -36,7 +36,10 @@ export function loadUsers(): ThunkResult<void> {
|
|||||||
dispatch(rolesFetchBegin());
|
dispatch(rolesFetchBegin());
|
||||||
const orgId = contextSrv.user.orgId;
|
const orgId = contextSrv.user.orgId;
|
||||||
const userIds = users?.orgUsers.map((u: OrgUser) => u.userId);
|
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) => {
|
users.orgUsers.forEach((u: OrgUser) => {
|
||||||
u.roles = roles ? roles[u.userId] || [] : [];
|
u.roles = roles ? roles[u.userId] || [] : [];
|
||||||
});
|
});
|
||||||
|
@ -167,6 +167,7 @@ export interface Role {
|
|||||||
group: string;
|
group: string;
|
||||||
global: boolean;
|
global: boolean;
|
||||||
delegatable?: boolean;
|
delegatable?: boolean;
|
||||||
|
mapped?: boolean;
|
||||||
version: number;
|
version: number;
|
||||||
created: string;
|
created: string;
|
||||||
updated: string;
|
updated: string;
|
||||||
|
@ -9751,6 +9751,9 @@
|
|||||||
"hidden": {
|
"hidden": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"mapped": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user