RBAC: Explain why org role selection is disabled for externally synced users (#72274)

* Move builtin role selector to separate component

* Show message if basic roles picker disabled

* Show explanation in OSS
This commit is contained in:
Alexander Zobnin 2023-07-25 16:16:07 +03:00 committed by GitHub
parent 783a8527c5
commit d6e43a44bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 76 additions and 14 deletions

View File

@ -0,0 +1,46 @@
import React from 'react';
import { SelectableValue } from '@grafana/data';
import { Icon, RadioButtonGroup, Tooltip, useStyles2, useTheme2 } from '@grafana/ui';
import { OrgRole } from 'app/types';
import { getStyles } from './styles';
const BasicRoles = Object.values(OrgRole).filter((r) => r !== OrgRole.None);
const BasicRoleOption: Array<SelectableValue<OrgRole>> = BasicRoles.map((r) => ({
label: r,
value: r,
}));
interface Props {
value?: OrgRole;
onChange: (value: OrgRole) => void;
disabled?: boolean;
disabledMesssage?: string;
}
export const BuiltinRoleSelector = ({ value, onChange, disabled, disabledMesssage }: Props) => {
const styles = useStyles2(getStyles);
const theme = useTheme2();
return (
<>
<div className={styles.groupHeader}>
<span style={{ marginRight: theme.spacing(1) }}>Basic roles</span>
{disabled && disabledMesssage && (
<Tooltip placement="right-end" interactive={true} content={<div>{disabledMesssage}</div>}>
<Icon name="question-circle" />
</Tooltip>
)}
</div>
<RadioButtonGroup
className={styles.basicRoleSelector}
options={BasicRoleOption}
value={value}
onChange={onChange}
fullWidth={true}
disabled={disabled}
/>
</>
);
};

View File

@ -14,6 +14,7 @@ export interface Props {
isLoading?: boolean; isLoading?: boolean;
disabled?: boolean; disabled?: boolean;
basicRoleDisabled?: boolean; basicRoleDisabled?: boolean;
basicRoleDisabledMessage?: string;
showBasicRole?: boolean; showBasicRole?: boolean;
onRolesChange: (newRoles: Role[]) => void; onRolesChange: (newRoles: Role[]) => void;
onBasicRoleChange?: (newRole: OrgRole) => void; onBasicRoleChange?: (newRole: OrgRole) => void;
@ -32,6 +33,7 @@ export const RolePicker = ({
disabled, disabled,
isLoading, isLoading,
basicRoleDisabled, basicRoleDisabled,
basicRoleDisabledMessage,
showBasicRole, showBasicRole,
onRolesChange, onRolesChange,
onBasicRoleChange, onBasicRoleChange,
@ -184,6 +186,7 @@ export const RolePicker = ({
onUpdate={onUpdate} onUpdate={onUpdate}
showGroups={query.length === 0 || query.trim() === ''} showGroups={query.length === 0 || query.trim() === ''}
basicRoleDisabled={basicRoleDisabled} basicRoleDisabled={basicRoleDisabled}
disabledMessage={basicRoleDisabledMessage}
showBasicRole={showBasicRole} showBasicRole={showBasicRole}
updateDisabled={basicRoleDisabled && !canUpdateRoles} updateDisabled={basicRoleDisabled && !canUpdateRoles}
apply={apply} apply={apply}

View File

@ -1,11 +1,11 @@
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { SelectableValue } from '@grafana/data'; import { Button, CustomScrollbar, HorizontalGroup, useStyles2, useTheme2 } from '@grafana/ui';
import { Button, CustomScrollbar, HorizontalGroup, RadioButtonGroup, useStyles2, useTheme2 } from '@grafana/ui';
import { getSelectStyles } from '@grafana/ui/src/components/Select/getSelectStyles'; import { getSelectStyles } from '@grafana/ui/src/components/Select/getSelectStyles';
import { OrgRole, Role } from 'app/types'; import { OrgRole, Role } from 'app/types';
import { BuiltinRoleSelector } from './BuiltinRoleSelector';
import { RoleMenuGroupsSection } from './RoleMenuGroupsSection'; import { RoleMenuGroupsSection } from './RoleMenuGroupsSection';
import { MENU_MAX_HEIGHT } from './constants'; import { MENU_MAX_HEIGHT } from './constants';
import { getStyles } from './styles'; import { getStyles } from './styles';
@ -29,12 +29,6 @@ interface RolesCollectionEntry {
roles: Role[]; roles: Role[];
} }
const BasicRoles = Object.values(OrgRole).filter((r) => r !== OrgRole.None);
const BasicRoleOption: Array<SelectableValue<OrgRole>> = BasicRoles.map((r) => ({
label: r,
value: r,
}));
const fixedRoleGroupNames: Record<string, string> = { const fixedRoleGroupNames: Record<string, string> = {
ldap: 'LDAP', ldap: 'LDAP',
current: 'Current org', current: 'Current org',
@ -46,6 +40,7 @@ interface RolePickerMenuProps {
appliedRoles: Role[]; appliedRoles: Role[];
showGroups?: boolean; showGroups?: boolean;
basicRoleDisabled?: boolean; basicRoleDisabled?: boolean;
disabledMessage?: string;
showBasicRole?: boolean; showBasicRole?: boolean;
onSelect: (roles: Role[]) => void; onSelect: (roles: Role[]) => void;
onBasicRoleSelect?: (role: OrgRole) => void; onBasicRoleSelect?: (role: OrgRole) => void;
@ -61,6 +56,7 @@ export const RolePickerMenu = ({
appliedRoles, appliedRoles,
showGroups, showGroups,
basicRoleDisabled, basicRoleDisabled,
disabledMessage,
showBasicRole, showBasicRole,
onSelect, onSelect,
onBasicRoleSelect, onBasicRoleSelect,
@ -206,14 +202,11 @@ export const RolePickerMenu = ({
<CustomScrollbar autoHide={false} autoHeightMax={`${MENU_MAX_HEIGHT}px`} hideHorizontalTrack hideVerticalTrack> <CustomScrollbar autoHide={false} autoHeightMax={`${MENU_MAX_HEIGHT}px`} hideHorizontalTrack hideVerticalTrack>
{showBasicRole && ( {showBasicRole && (
<div className={customStyles.menuSection}> <div className={customStyles.menuSection}>
<div className={customStyles.groupHeader}>Basic roles</div> <BuiltinRoleSelector
<RadioButtonGroup
className={customStyles.basicRoleSelector}
options={BasicRoleOption}
value={selectedBuiltInRole} value={selectedBuiltInRole}
onChange={onSelectedBuiltinRoleChange} onChange={onSelectedBuiltinRoleChange}
fullWidth={true}
disabled={basicRoleDisabled} disabled={basicRoleDisabled}
disabledMesssage={disabledMessage}
/> />
</div> </div>
)} )}

View File

@ -15,6 +15,7 @@ export interface Props {
roleOptions: Role[]; roleOptions: Role[];
disabled?: boolean; disabled?: boolean;
basicRoleDisabled?: boolean; basicRoleDisabled?: boolean;
basicRoleDisabledMessage?: string;
/** /**
* Set whether the component should send a request with the new roles to the * Set whether the component should send a request with the new roles to the
* backend in UserRolePicker.onRolesChange (apply=false), or call {@link onApplyRoles} * backend in UserRolePicker.onRolesChange (apply=false), or call {@link onApplyRoles}
@ -40,6 +41,7 @@ export const UserRolePicker = ({
roleOptions, roleOptions,
disabled, disabled,
basicRoleDisabled, basicRoleDisabled,
basicRoleDisabledMessage,
apply = false, apply = false,
onApplyRoles, onApplyRoles,
pendingRoles, pendingRoles,
@ -91,6 +93,7 @@ export const UserRolePicker = ({
isLoading={loading} isLoading={loading}
disabled={disabled} disabled={disabled}
basicRoleDisabled={basicRoleDisabled} basicRoleDisabled={basicRoleDisabled}
basicRoleDisabledMessage={basicRoleDisabledMessage}
showBasicRole showBasicRole
apply={apply} apply={apply}
canUpdateRoles={canUpdateRoles} canUpdateRoles={canUpdateRoles}

View File

@ -209,6 +209,8 @@ class UnThemedOrgRow extends PureComponent<OrgRowProps> {
roleOptions={this.state.roleOptions} roleOptions={this.state.roleOptions}
onBasicRoleChange={this.onBasicRoleChange} onBasicRoleChange={this.onBasicRoleChange}
basicRoleDisabled={rolePickerDisabled} basicRoleDisabled={rolePickerDisabled}
basicRoleDisabledMessage="This user's role is not editable because it is synchronized from your auth provider.
Refer to the Grafana authentication docs for details."
/> />
</div> </div>
{isExternalUser && <ExternalUserTooltip lockMessage={lockMessage} />} {isExternalUser && <ExternalUserTooltip lockMessage={lockMessage} />}

View File

@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { OrgRole } from '@grafana/data'; import { OrgRole } from '@grafana/data';
import { Button, ConfirmModal } from '@grafana/ui'; import { Button, ConfirmModal, Icon, Tooltip } from '@grafana/ui';
import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker'; import { UserRolePicker } from 'app/core/components/RolePicker/UserRolePicker';
import { fetchRoleOptions } from 'app/core/components/RolePicker/api'; import { fetchRoleOptions } from 'app/core/components/RolePicker/api';
import { TagBadge } from 'app/core/components/TagFilter/TagBadge'; import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
@ -11,6 +11,9 @@ import { AccessControlAction, OrgUser, Role } from 'app/types';
import { OrgRolePicker } from '../admin/OrgRolePicker'; import { OrgRolePicker } from '../admin/OrgRolePicker';
const disabledRoleMessage = `This user's role is not editable because it is synchronized from your auth provider.
Refer to the Grafana authentication docs for details.`;
export interface Props { export interface Props {
users: OrgUser[]; users: OrgUser[];
orgId?: number; orgId?: number;
@ -49,6 +52,7 @@ export const UsersTable = ({ users, orgId, onRoleChange, onRemoveUser }: Props)
<th>Name</th> <th>Name</th>
<th>Seen</th> <th>Seen</th>
<th>Role</th> <th>Role</th>
<th />
<th style={{ width: '34px' }} /> <th style={{ width: '34px' }} />
<th>Origin</th> <th>Origin</th>
<th></th> <th></th>
@ -97,6 +101,7 @@ export const UsersTable = ({ users, orgId, onRoleChange, onRemoveUser }: Props)
basicRole={user.role} basicRole={user.role}
onBasicRoleChange={(newRole) => onRoleChange(newRole, user)} onBasicRoleChange={(newRole) => onRoleChange(newRole, user)}
basicRoleDisabled={basicRoleDisabled} basicRoleDisabled={basicRoleDisabled}
basicRoleDisabledMessage={disabledRoleMessage}
/> />
) : ( ) : (
<OrgRolePicker <OrgRolePicker
@ -108,6 +113,16 @@ export const UsersTable = ({ users, orgId, onRoleChange, onRemoveUser }: Props)
)} )}
</td> </td>
<td>
{basicRoleDisabled && (
<div style={{ display: 'flex', alignItems: 'center' }}>
<Tooltip content={disabledRoleMessage}>
<Icon name="question-circle" style={{ marginLeft: '8px' }} />
</Tooltip>
</div>
)}
</td>
<td className="width-1 text-center"> <td className="width-1 text-center">
{user.isDisabled && <span className="label label-tag label-tag--gray">Disabled</span>} {user.isDisabled && <span className="label label-tag label-tag--gray">Disabled</span>}
</td> </td>