Skip to content

Commit

Permalink
Access Control: Add rolepicker when Fine Grained Access Control is en…
Browse files Browse the repository at this point in the history
…abled (#48347)
  • Loading branch information
briangann committed May 23, 2022
1 parent 060af78 commit 5fc5899
Show file tree
Hide file tree
Showing 10 changed files with 256 additions and 44 deletions.
10 changes: 7 additions & 3 deletions public/app/core/components/RolePicker/RolePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ export interface Props {
disabled?: boolean;
builtinRolesDisabled?: boolean;
showBuiltInRole?: boolean;
onRolesChange: (newRoles: string[]) => void;
onRolesChange: (newRoles: Role[]) => void;
onBuiltinRoleChange?: (newRole: OrgRole) => void;
updateDisabled?: boolean;
}

export const RolePicker = ({
Expand All @@ -30,6 +31,7 @@ export const RolePicker = ({
showBuiltInRole,
onRolesChange,
onBuiltinRoleChange,
updateDisabled,
}: Props): JSX.Element | null => {
const [isOpen, setOpen] = useState(false);
const [selectedRoles, setSelectedRoles] = useState<Role[]>(appliedRoles);
Expand All @@ -39,8 +41,9 @@ export const RolePicker = ({
const ref = useRef<HTMLDivElement>(null);

useEffect(() => {
setSelectedBuiltInRole(builtInRole);
setSelectedRoles(appliedRoles);
}, [appliedRoles]);
}, [appliedRoles, builtInRole]);

useEffect(() => {
const dimensions = ref?.current?.getBoundingClientRect();
Expand Down Expand Up @@ -94,7 +97,7 @@ export const RolePicker = ({
setSelectedBuiltInRole(role);
};

const onUpdate = (newRoles: string[], newBuiltInRole?: OrgRole) => {
const onUpdate = (newRoles: Role[], newBuiltInRole?: OrgRole) => {
if (onBuiltinRoleChange && newBuiltInRole) {
onBuiltinRoleChange(newBuiltInRole);
}
Expand Down Expand Up @@ -144,6 +147,7 @@ export const RolePicker = ({
showGroups={query.length === 0 || query.trim() === ''}
builtinRolesDisabled={builtinRolesDisabled}
showBuiltInRole={showBuiltInRole}
updateDisabled={updateDisabled || false}
offset={offset}
/>
)}
Expand Down
9 changes: 6 additions & 3 deletions public/app/core/components/RolePicker/RolePickerMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,9 @@ interface RolePickerMenuProps {
showBuiltInRole?: boolean;
onSelect: (roles: Role[]) => void;
onBuiltInRoleSelect?: (role: OrgRole) => void;
onUpdate: (newRoles: string[], newBuiltInRole?: OrgRole) => void;
onUpdate: (newRoles: Role[], newBuiltInRole?: OrgRole) => void;
onClear?: () => void;
updateDisabled?: boolean;
offset: number;
}

Expand All @@ -55,6 +56,7 @@ export const RolePickerMenu = ({
onBuiltInRoleSelect,
onUpdate,
onClear,
updateDisabled,
offset,
}: RolePickerMenuProps): JSX.Element => {
const [selectedOptions, setSelectedOptions] = useState<Role[]>(appliedRoles);
Expand Down Expand Up @@ -166,11 +168,12 @@ export const RolePickerMenu = ({

const onUpdateInternal = () => {
const selectedCustomRoles: string[] = [];
// TODO: needed?
for (const key in selectedOptions) {
const roleUID = selectedOptions[key]?.uid;
selectedCustomRoles.push(roleUID);
}
onUpdate(selectedCustomRoles, selectedBuiltInRole);
onUpdate(selectedOptions, selectedBuiltInRole);
};

return (
Expand Down Expand Up @@ -270,7 +273,7 @@ export const RolePickerMenu = ({
Clear all
</Button>
<Button size="sm" onClick={onUpdateInternal}>
Update
{updateDisabled ? `Apply` : `Update`}
</Button>
</HorizontalGroup>
</div>
Expand Down
3 changes: 2 additions & 1 deletion public/app/core/components/RolePicker/TeamRolePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useAsyncFn } from 'react-use';
import { Role } from 'app/types';

import { RolePicker } from './RolePicker';
// @ts-ignore
import { fetchTeamRoles, updateTeamRoles } from './api';

export interface Props {
Expand All @@ -29,7 +30,7 @@ export const TeamRolePicker: FC<Props> = ({ teamId, orgId, roleOptions, disabled
getTeamRoles();
}, [orgId, teamId, getTeamRoles]);

const onRolesChange = async (roles: string[]) => {
const onRolesChange = async (roles: Role[]) => {
await updateTeamRoles(roles, teamId, orgId);
await getTeamRoles();
};
Expand Down
35 changes: 28 additions & 7 deletions public/app/core/components/RolePicker/UserRolePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export interface Props {
builtInRoles?: { [key: string]: Role[] };
disabled?: boolean;
builtinRolesDisabled?: boolean;
updateDisabled?: boolean;
onApplyRoles?: (newRoles: Role[], userId: number, orgId: number | undefined) => void;
pendingRoles?: Role[];
}

export const UserRolePicker: FC<Props> = ({
Expand All @@ -27,9 +30,17 @@ export const UserRolePicker: FC<Props> = ({
builtInRoles,
disabled,
builtinRolesDisabled,
updateDisabled,
onApplyRoles,
pendingRoles,
}) => {
const [{ loading, value: appliedRoles = [] }, getUserRoles] = useAsyncFn(async () => {
try {
if (updateDisabled) {
if (pendingRoles?.length! > 0) {
return pendingRoles;
}
}
if (contextSrv.hasPermission(AccessControlAction.ActionUserRolesList)) {
return await fetchUserRoles(userId, orgId);
}
Expand All @@ -38,29 +49,39 @@ export const UserRolePicker: FC<Props> = ({
console.error('Error loading options');
}
return [];
}, [orgId, userId]);
}, [orgId, userId, pendingRoles]);

useEffect(() => {
getUserRoles();
}, [orgId, userId, getUserRoles]);
// only load roles when there is an Org selected
if (orgId) {
getUserRoles();
}
}, [orgId, getUserRoles, pendingRoles]);

const onRolesChange = async (roles: string[]) => {
await updateUserRoles(roles, userId, orgId);
await getUserRoles();
const onRolesChange = async (roles: Role[]) => {
if (!updateDisabled) {
await updateUserRoles(roles, userId, orgId);
await getUserRoles();
} else {
if (onApplyRoles) {
onApplyRoles(roles, userId, orgId);
}
}
};

return (
<RolePicker
appliedRoles={appliedRoles}
builtInRole={builtInRole}
onRolesChange={onRolesChange}
onBuiltinRoleChange={onBuiltinRoleChange}
roleOptions={roleOptions}
appliedRoles={appliedRoles}
builtInRoles={builtInRoles}
isLoading={loading}
disabled={disabled}
builtinRolesDisabled={builtinRolesDisabled}
showBuiltInRole
updateDisabled={updateDisabled || false}
/>
);
};
7 changes: 5 additions & 2 deletions public/app/core/components/RolePicker/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,12 @@ export const fetchUserRoles = async (userId: number, orgId?: number): Promise<Ro
}
};

export const updateUserRoles = (roleUids: string[], userId: number, orgId?: number) => {
export const updateUserRoles = (roles: Role[], userId: number, orgId?: number) => {
let userRolesUrl = `/api/access-control/users/${userId}/roles`;
if (orgId) {
userRolesUrl += `?targetOrgId=${orgId}`;
}
const roleUids = roles.flatMap((x) => x.uid);
return getBackendSrv().put(userRolesUrl, {
orgId,
roleUids,
Expand All @@ -66,11 +67,13 @@ export const fetchTeamRoles = async (teamId: number, orgId?: number): Promise<Ro
}
};

export const updateTeamRoles = (roleUids: string[], teamId: number, orgId?: number) => {
export const updateTeamRoles = (roles: Role[], teamId: number, orgId?: number) => {
let teamRolesUrl = `/api/access-control/teams/${teamId}/roles`;
if (orgId) {
teamRolesUrl += `?targetOrgId=${orgId}`;
}
const roleUids = roles.flatMap((x) => x.uid);

return getBackendSrv().put(teamRolesUrl, {
orgId,
roleUids,
Expand Down
16 changes: 13 additions & 3 deletions public/app/core/components/Select/OrgPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useAsyncFn } from 'react-use';
import { SelectableValue } from '@grafana/data';
import { AsyncSelect } from '@grafana/ui';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { Organization } from 'app/types';
import { Organization, UserOrg } from 'app/types';

export type OrgSelectItem = SelectableValue<Organization>;

Expand All @@ -13,9 +13,10 @@ export interface Props {
className?: string;
inputId?: string;
autoFocus?: boolean;
excludeOrgs?: UserOrg[];
}

export function OrgPicker({ onSelected, className, inputId, autoFocus }: Props) {
export function OrgPicker({ onSelected, className, inputId, autoFocus, excludeOrgs }: Props) {
// For whatever reason the autoFocus prop doesn't seem to work
// with AsyncSelect, hence this workaround. Maybe fixed in a later version?
useEffect(() => {
Expand All @@ -26,7 +27,16 @@ export function OrgPicker({ onSelected, className, inputId, autoFocus }: Props)

const [orgOptionsState, getOrgOptions] = useAsyncFn(async () => {
const orgs: Organization[] = await getBackendSrv().get('/api/orgs');
return orgs.map((org) => ({ value: { id: org.id, name: org.name }, label: org.name }));
const allOrgs = orgs.map((org) => ({ value: { id: org.id, name: org.name }, label: org.name }));
if (excludeOrgs) {
let idArray = excludeOrgs.map((anOrg) => anOrg.orgId);
const filteredOrgs = allOrgs.filter((item) => {
return !idArray.includes(item.value.id);
});
return filteredOrgs;
} else {
return allOrgs;
}
});

return (
Expand Down
Loading

0 comments on commit 5fc5899

Please sign in to comment.