Skip to content

Commit

Permalink
feat: root roles from groups (#3559)
Browse files Browse the repository at this point in the history
feat: adds a way to specify a root role on a group, which will cause any user entering into that group to take on the permissions of that root role

Co-authored-by: Nuno Góis <github@nunogois.com>
  • Loading branch information
sighphyre and nunogois committed Apr 20, 2023
1 parent 163791a commit 3b42e86
Show file tree
Hide file tree
Showing 21 changed files with 464 additions and 37 deletions.
Expand Up @@ -26,6 +26,8 @@ export const CreateGroup = () => {
setMappingsSSO,
users,
setUsers,
rootRole,
setRootRole,
getGroupPayload,
clearErrors,
errors,
Expand Down Expand Up @@ -95,10 +97,12 @@ export const CreateGroup = () => {
name={name}
description={description}
mappingsSSO={mappingsSSO}
rootRole={rootRole}
users={users}
setName={onSetName}
setDescription={setDescription}
setMappingsSSO={setMappingsSSO}
setRootRole={setRootRole}
setUsers={setUsers}
errors={errors}
handleSubmit={handleSubmit}
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/component/admin/groups/EditGroup/EditGroup.tsx
Expand Up @@ -55,6 +55,8 @@ export const EditGroup = ({
setMappingsSSO,
users,
setUsers,
rootRole,
setRootRole,
getGroupPayload,
clearErrors,
errors,
Expand All @@ -63,7 +65,8 @@ export const EditGroup = ({
group?.name,
group?.description,
group?.mappingsSSO,
group?.users
group?.users,
group?.rootRole
);

const { groups } = useGroups();
Expand Down Expand Up @@ -129,10 +132,12 @@ export const EditGroup = ({
description={description}
mappingsSSO={mappingsSSO}
users={users}
rootRole={rootRole}
setName={onSetName}
setDescription={setDescription}
setMappingsSSO={setMappingsSSO}
setUsers={setUsers}
setRootRole={setRootRole}
errors={errors}
handleSubmit={handleSubmit}
handleCancel={handleCancel}
Expand Down
Expand Up @@ -60,7 +60,8 @@ export const EditGroupUsers: FC<IEditGroupUsersProps> = ({
group.name,
group.description,
group.mappingsSSO,
group.users
group.users,
group.rootRole
);

useEffect(() => {
Expand Down
86 changes: 83 additions & 3 deletions frontend/src/component/admin/groups/GroupForm/GroupForm.tsx
@@ -1,5 +1,5 @@
import React, { FC } from 'react';
import { Box, Button, styled } from '@mui/material';
import { Autocomplete, Box, Button, styled, TextField } from '@mui/material';
import { UG_DESC_ID, UG_NAME_ID } from 'utils/testIds';
import Input from 'component/common/Input/Input';
import { IGroupUser } from 'interfaces/group';
Expand All @@ -10,6 +10,9 @@ import { ItemList } from 'component/common/ItemList/ItemList';
import useAuthSettings from 'hooks/api/getters/useAuthSettings/useAuthSettings';
import { Link } from 'react-router-dom';
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
import { IProjectRole } from 'interfaces/role';
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';

const StyledForm = styled('form')(() => ({
display: 'flex',
Expand Down Expand Up @@ -63,15 +66,34 @@ const StyledDescriptionBlock = styled('div')(({ theme }) => ({
},
}));

const StyledAutocompleteWrapper = styled('div')(({ theme }) => ({
'& > div:first-of-type': {
width: '100%',
maxWidth: theme.spacing(50),
marginBottom: theme.spacing(2),
},
}));

const StyledRoleOption = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
'& > span:last-of-type': {
fontSize: theme.fontSizes.smallerBody,
color: theme.palette.text.secondary,
},
}));

interface IGroupForm {
name: string;
description: string;
mappingsSSO: string[];
users: IGroupUser[];
rootRole: number | null;
setName: (name: string) => void;
setDescription: React.Dispatch<React.SetStateAction<string>>;
setMappingsSSO: React.Dispatch<React.SetStateAction<string[]>>;
setUsers: React.Dispatch<React.SetStateAction<IGroupUser[]>>;
setRootRole: React.Dispatch<React.SetStateAction<number | null>>;
handleSubmit: (e: any) => void;
handleCancel: () => void;
errors: { [key: string]: string };
Expand All @@ -83,23 +105,47 @@ export const GroupForm: FC<IGroupForm> = ({
description,
mappingsSSO,
users,
rootRole,
setName,
setDescription,
setMappingsSSO,
setUsers,
handleSubmit,
handleCancel,
setRootRole,
errors,
mode,
children,
}) => {
const { config: oidcSettings } = useAuthSettings('oidc');
const { config: samlSettings } = useAuthSettings('saml');
const { uiConfig } = useUiConfig();
const { roles } = useUsers();

const isGroupSyncingEnabled =
(oidcSettings?.enabled && oidcSettings.enableGroupSyncing) ||
(samlSettings?.enabled && samlSettings.enableGroupSyncing);

const groupRootRolesEnabled = Boolean(uiConfig.flags.groupRootRoles);

const roleIdToRole = (rootRoleId: number | null): IProjectRole | null => {
return (
roles.find((role: IProjectRole) => role.id === rootRoleId) || null
);
};

const renderRoleOption = (
props: React.HTMLAttributes<HTMLLIElement>,
option: IProjectRole
) => (
<li {...props}>
<StyledRoleOption>
<span>{option.name}</span>
<span>{option.description}</span>
</StyledRoleOption>
</li>
);

return (
<StyledForm onSubmit={handleSubmit}>
<div>
Expand Down Expand Up @@ -146,16 +192,50 @@ export const GroupForm: FC<IGroupForm> = ({
elseShow={() => (
<StyledDescriptionBlock>
<Box sx={{ display: 'flex' }}>
You can enable SSO groups syncronization if
You can enable SSO groups synchronization if
needed
<HelpIcon tooltip="SSO groups syncronization allows SSO groups to be mapped to Unleash groups, so that user group membership is properly synchronized." />
<HelpIcon tooltip="SSO groups synchronization allows SSO groups to be mapped to Unleash groups, so that user group membership is properly synchronized." />
</Box>
<Link data-loading to={`/admin/auth`}>
<span data-loading>View SSO configuration</span>
</Link>
</StyledDescriptionBlock>
)}
/>
<ConditionallyRender
condition={groupRootRolesEnabled}
show={
<>
<StyledInputDescription>
<Box sx={{ display: 'flex' }}>
Do you want to associate a root role with
this group?
<HelpIcon tooltip="When you associate an Admin or Editor role with this group, users in this group will automatically inherit the role globally. Note that groups with a root role association cannot be assigned to projects." />
</Box>
</StyledInputDescription>
<StyledAutocompleteWrapper>
<Autocomplete
data-testid="GROUP_ROOT_ROLE"
size="small"
openOnFocus
value={roleIdToRole(rootRole)}
onChange={(_, newValue) =>
setRootRole(newValue?.id || null)
}
options={roles.filter(
(role: IProjectRole) =>
role.name !== 'Viewer'
)}
renderOption={renderRoleOption}
getOptionLabel={option => option.name}
renderInput={params => (
<TextField {...params} label="Role" />
)}
/>
</StyledAutocompleteWrapper>
</>
}
/>
<ConditionallyRender
condition={mode === 'Create'}
show={
Expand Down
Expand Up @@ -6,6 +6,8 @@ import { GroupCardAvatars } from './GroupCardAvatars/GroupCardAvatars';
import { Badge } from 'component/common/Badge/Badge';
import { GroupCardActions } from './GroupCardActions/GroupCardActions';
import TopicOutlinedIcon from '@mui/icons-material/TopicOutlined';
import { IProjectRole } from 'interfaces/role';
import { IProject } from 'interfaces/project';

const StyledLink = styled(Link)(({ theme }) => ({
textDecoration: 'none',
Expand Down Expand Up @@ -75,14 +77,24 @@ const ProjectBadgeContainer = styled('div')(({ theme }) => ({
flexWrap: 'wrap',
}));

const InfoBadgeDescription = styled('span')(({ theme }) => ({
display: 'flex',
color: theme.palette.text.secondary,
alignItems: 'center',
gap: theme.spacing(1),
fontSize: theme.fontSizes.smallBody,
}));

interface IGroupCardProps {
group: IGroup;
rootRoles: IProjectRole[];
onEditUsers: (group: IGroup) => void;
onRemoveGroup: (group: IGroup) => void;
}

export const GroupCard = ({
group,
rootRoles,
onEditUsers,
onRemoveGroup,
}: IGroupCardProps) => {
Expand All @@ -101,6 +113,26 @@ export const GroupCard = ({
/>
</StyledHeaderActions>
</StyledTitleRow>
<ConditionallyRender
condition={Boolean(group.rootRole)}
show={
<InfoBadgeDescription>
<p>Root role:</p>
<Badge
color="success"
icon={<TopicOutlinedIcon />}
>
{
rootRoles.find(
(role: IProjectRole) =>
role.id === group.rootRole
)?.name
}
</Badge>
</InfoBadgeDescription>
}
/>

<StyledDescription>{group.description}</StyledDescription>
<StyledBottomRow>
<ConditionallyRender
Expand Down Expand Up @@ -143,7 +175,10 @@ export const GroupCard = ({
arrow
describeChild
>
<Badge>Not used</Badge>
<ConditionallyRender
condition={!group.rootRole}
show={<Badge>Not used</Badge>}
/>
</Tooltip>
}
/>
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/component/admin/groups/GroupsList/GroupsList.tsx
Expand Up @@ -18,6 +18,8 @@ import { Add } from '@mui/icons-material';
import { NAVIGATE_TO_CREATE_GROUP } from 'utils/testIds';
import { EditGroupUsers } from '../Group/EditGroupUsers/EditGroupUsers';
import { RemoveGroup } from '../RemoveGroup/RemoveGroup';
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
import { IProjectRole } from 'interfaces/role';

type PageQueryType = Partial<Record<'search', string>>;

Expand Down Expand Up @@ -49,6 +51,7 @@ export const GroupsList: VFC = () => {
const [searchValue, setSearchValue] = useState(
searchParams.get('search') || ''
);
const { roles } = useUsers();

const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));

Expand Down Expand Up @@ -82,6 +85,10 @@ export const GroupsList: VFC = () => {
setRemoveOpen(true);
};

const getBindableRootRoles = () => {
return roles.filter((role: IProjectRole) => role.type === 'root');
};

return (
<PageContent
isLoading={loading}
Expand Down Expand Up @@ -134,6 +141,7 @@ export const GroupsList: VFC = () => {
<Grid key={group.id} item xs={12} md={6}>
<GroupCard
group={group}
rootRoles={getBindableRootRoles()}
onEditUsers={onEditUsers}
onRemoveGroup={onRemoveGroup}
/>
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/component/admin/groups/hooks/useGroupForm.ts
Expand Up @@ -6,14 +6,16 @@ export const useGroupForm = (
initialName = '',
initialDescription = '',
initialMappingsSSO: string[] = [],
initialUsers: IGroupUser[] = []
initialUsers: IGroupUser[] = [],
initialRootRole: number | null = null
) => {
const params = useQueryParams();
const groupQueryName = params.get('name');
const [name, setName] = useState(groupQueryName || initialName);
const [description, setDescription] = useState(initialDescription);
const [mappingsSSO, setMappingsSSO] = useState(initialMappingsSSO);
const [users, setUsers] = useState<IGroupUser[]>(initialUsers);
const [rootRole, setRootRole] = useState<number | null>(initialRootRole);
const [errors, setErrors] = useState({});

const getGroupPayload = () => {
Expand All @@ -24,6 +26,7 @@ export const useGroupForm = (
users: users.map(({ id }) => ({
user: { id },
})),
rootRole: rootRole || undefined,
};
};

Expand All @@ -44,5 +47,7 @@ export const useGroupForm = (
clearErrors,
errors,
setErrors,
rootRole,
setRootRole,
};
};

0 comments on commit 3b42e86

Please sign in to comment.