mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-27 01:19:00 +02:00
feat: root roles from groups (#3559)
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>
This commit is contained in:
parent
163791a094
commit
3b42e866ec
@ -26,6 +26,8 @@ export const CreateGroup = () => {
|
|||||||
setMappingsSSO,
|
setMappingsSSO,
|
||||||
users,
|
users,
|
||||||
setUsers,
|
setUsers,
|
||||||
|
rootRole,
|
||||||
|
setRootRole,
|
||||||
getGroupPayload,
|
getGroupPayload,
|
||||||
clearErrors,
|
clearErrors,
|
||||||
errors,
|
errors,
|
||||||
@ -95,10 +97,12 @@ export const CreateGroup = () => {
|
|||||||
name={name}
|
name={name}
|
||||||
description={description}
|
description={description}
|
||||||
mappingsSSO={mappingsSSO}
|
mappingsSSO={mappingsSSO}
|
||||||
|
rootRole={rootRole}
|
||||||
users={users}
|
users={users}
|
||||||
setName={onSetName}
|
setName={onSetName}
|
||||||
setDescription={setDescription}
|
setDescription={setDescription}
|
||||||
setMappingsSSO={setMappingsSSO}
|
setMappingsSSO={setMappingsSSO}
|
||||||
|
setRootRole={setRootRole}
|
||||||
setUsers={setUsers}
|
setUsers={setUsers}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
|
@ -55,6 +55,8 @@ export const EditGroup = ({
|
|||||||
setMappingsSSO,
|
setMappingsSSO,
|
||||||
users,
|
users,
|
||||||
setUsers,
|
setUsers,
|
||||||
|
rootRole,
|
||||||
|
setRootRole,
|
||||||
getGroupPayload,
|
getGroupPayload,
|
||||||
clearErrors,
|
clearErrors,
|
||||||
errors,
|
errors,
|
||||||
@ -63,7 +65,8 @@ export const EditGroup = ({
|
|||||||
group?.name,
|
group?.name,
|
||||||
group?.description,
|
group?.description,
|
||||||
group?.mappingsSSO,
|
group?.mappingsSSO,
|
||||||
group?.users
|
group?.users,
|
||||||
|
group?.rootRole
|
||||||
);
|
);
|
||||||
|
|
||||||
const { groups } = useGroups();
|
const { groups } = useGroups();
|
||||||
@ -129,10 +132,12 @@ export const EditGroup = ({
|
|||||||
description={description}
|
description={description}
|
||||||
mappingsSSO={mappingsSSO}
|
mappingsSSO={mappingsSSO}
|
||||||
users={users}
|
users={users}
|
||||||
|
rootRole={rootRole}
|
||||||
setName={onSetName}
|
setName={onSetName}
|
||||||
setDescription={setDescription}
|
setDescription={setDescription}
|
||||||
setMappingsSSO={setMappingsSSO}
|
setMappingsSSO={setMappingsSSO}
|
||||||
setUsers={setUsers}
|
setUsers={setUsers}
|
||||||
|
setRootRole={setRootRole}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
handleCancel={handleCancel}
|
handleCancel={handleCancel}
|
||||||
|
@ -60,7 +60,8 @@ export const EditGroupUsers: FC<IEditGroupUsersProps> = ({
|
|||||||
group.name,
|
group.name,
|
||||||
group.description,
|
group.description,
|
||||||
group.mappingsSSO,
|
group.mappingsSSO,
|
||||||
group.users
|
group.users,
|
||||||
|
group.rootRole
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { FC } from 'react';
|
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 { UG_DESC_ID, UG_NAME_ID } from 'utils/testIds';
|
||||||
import Input from 'component/common/Input/Input';
|
import Input from 'component/common/Input/Input';
|
||||||
import { IGroupUser } from 'interfaces/group';
|
import { IGroupUser } from 'interfaces/group';
|
||||||
@ -10,6 +10,9 @@ import { ItemList } from 'component/common/ItemList/ItemList';
|
|||||||
import useAuthSettings from 'hooks/api/getters/useAuthSettings/useAuthSettings';
|
import useAuthSettings from 'hooks/api/getters/useAuthSettings/useAuthSettings';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
|
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')(() => ({
|
const StyledForm = styled('form')(() => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -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 {
|
interface IGroupForm {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
mappingsSSO: string[];
|
mappingsSSO: string[];
|
||||||
users: IGroupUser[];
|
users: IGroupUser[];
|
||||||
|
rootRole: number | null;
|
||||||
setName: (name: string) => void;
|
setName: (name: string) => void;
|
||||||
setDescription: React.Dispatch<React.SetStateAction<string>>;
|
setDescription: React.Dispatch<React.SetStateAction<string>>;
|
||||||
setMappingsSSO: React.Dispatch<React.SetStateAction<string[]>>;
|
setMappingsSSO: React.Dispatch<React.SetStateAction<string[]>>;
|
||||||
setUsers: React.Dispatch<React.SetStateAction<IGroupUser[]>>;
|
setUsers: React.Dispatch<React.SetStateAction<IGroupUser[]>>;
|
||||||
|
setRootRole: React.Dispatch<React.SetStateAction<number | null>>;
|
||||||
handleSubmit: (e: any) => void;
|
handleSubmit: (e: any) => void;
|
||||||
handleCancel: () => void;
|
handleCancel: () => void;
|
||||||
errors: { [key: string]: string };
|
errors: { [key: string]: string };
|
||||||
@ -83,23 +105,47 @@ export const GroupForm: FC<IGroupForm> = ({
|
|||||||
description,
|
description,
|
||||||
mappingsSSO,
|
mappingsSSO,
|
||||||
users,
|
users,
|
||||||
|
rootRole,
|
||||||
setName,
|
setName,
|
||||||
setDescription,
|
setDescription,
|
||||||
setMappingsSSO,
|
setMappingsSSO,
|
||||||
setUsers,
|
setUsers,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
handleCancel,
|
handleCancel,
|
||||||
|
setRootRole,
|
||||||
errors,
|
errors,
|
||||||
mode,
|
mode,
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const { config: oidcSettings } = useAuthSettings('oidc');
|
const { config: oidcSettings } = useAuthSettings('oidc');
|
||||||
const { config: samlSettings } = useAuthSettings('saml');
|
const { config: samlSettings } = useAuthSettings('saml');
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
|
const { roles } = useUsers();
|
||||||
|
|
||||||
const isGroupSyncingEnabled =
|
const isGroupSyncingEnabled =
|
||||||
(oidcSettings?.enabled && oidcSettings.enableGroupSyncing) ||
|
(oidcSettings?.enabled && oidcSettings.enableGroupSyncing) ||
|
||||||
(samlSettings?.enabled && samlSettings.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 (
|
return (
|
||||||
<StyledForm onSubmit={handleSubmit}>
|
<StyledForm onSubmit={handleSubmit}>
|
||||||
<div>
|
<div>
|
||||||
@ -146,9 +192,9 @@ export const GroupForm: FC<IGroupForm> = ({
|
|||||||
elseShow={() => (
|
elseShow={() => (
|
||||||
<StyledDescriptionBlock>
|
<StyledDescriptionBlock>
|
||||||
<Box sx={{ display: 'flex' }}>
|
<Box sx={{ display: 'flex' }}>
|
||||||
You can enable SSO groups syncronization if
|
You can enable SSO groups synchronization if
|
||||||
needed
|
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>
|
</Box>
|
||||||
<Link data-loading to={`/admin/auth`}>
|
<Link data-loading to={`/admin/auth`}>
|
||||||
<span data-loading>View SSO configuration</span>
|
<span data-loading>View SSO configuration</span>
|
||||||
@ -156,6 +202,40 @@ export const GroupForm: FC<IGroupForm> = ({
|
|||||||
</StyledDescriptionBlock>
|
</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
|
<ConditionallyRender
|
||||||
condition={mode === 'Create'}
|
condition={mode === 'Create'}
|
||||||
show={
|
show={
|
||||||
|
@ -6,6 +6,8 @@ import { GroupCardAvatars } from './GroupCardAvatars/GroupCardAvatars';
|
|||||||
import { Badge } from 'component/common/Badge/Badge';
|
import { Badge } from 'component/common/Badge/Badge';
|
||||||
import { GroupCardActions } from './GroupCardActions/GroupCardActions';
|
import { GroupCardActions } from './GroupCardActions/GroupCardActions';
|
||||||
import TopicOutlinedIcon from '@mui/icons-material/TopicOutlined';
|
import TopicOutlinedIcon from '@mui/icons-material/TopicOutlined';
|
||||||
|
import { IProjectRole } from 'interfaces/role';
|
||||||
|
import { IProject } from 'interfaces/project';
|
||||||
|
|
||||||
const StyledLink = styled(Link)(({ theme }) => ({
|
const StyledLink = styled(Link)(({ theme }) => ({
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
@ -75,14 +77,24 @@ const ProjectBadgeContainer = styled('div')(({ theme }) => ({
|
|||||||
flexWrap: 'wrap',
|
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 {
|
interface IGroupCardProps {
|
||||||
group: IGroup;
|
group: IGroup;
|
||||||
|
rootRoles: IProjectRole[];
|
||||||
onEditUsers: (group: IGroup) => void;
|
onEditUsers: (group: IGroup) => void;
|
||||||
onRemoveGroup: (group: IGroup) => void;
|
onRemoveGroup: (group: IGroup) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GroupCard = ({
|
export const GroupCard = ({
|
||||||
group,
|
group,
|
||||||
|
rootRoles,
|
||||||
onEditUsers,
|
onEditUsers,
|
||||||
onRemoveGroup,
|
onRemoveGroup,
|
||||||
}: IGroupCardProps) => {
|
}: IGroupCardProps) => {
|
||||||
@ -101,6 +113,26 @@ export const GroupCard = ({
|
|||||||
/>
|
/>
|
||||||
</StyledHeaderActions>
|
</StyledHeaderActions>
|
||||||
</StyledTitleRow>
|
</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>
|
<StyledDescription>{group.description}</StyledDescription>
|
||||||
<StyledBottomRow>
|
<StyledBottomRow>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
@ -143,7 +175,10 @@ export const GroupCard = ({
|
|||||||
arrow
|
arrow
|
||||||
describeChild
|
describeChild
|
||||||
>
|
>
|
||||||
<Badge>Not used</Badge>
|
<ConditionallyRender
|
||||||
|
condition={!group.rootRole}
|
||||||
|
show={<Badge>Not used</Badge>}
|
||||||
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@ -18,6 +18,8 @@ import { Add } from '@mui/icons-material';
|
|||||||
import { NAVIGATE_TO_CREATE_GROUP } from 'utils/testIds';
|
import { NAVIGATE_TO_CREATE_GROUP } from 'utils/testIds';
|
||||||
import { EditGroupUsers } from '../Group/EditGroupUsers/EditGroupUsers';
|
import { EditGroupUsers } from '../Group/EditGroupUsers/EditGroupUsers';
|
||||||
import { RemoveGroup } from '../RemoveGroup/RemoveGroup';
|
import { RemoveGroup } from '../RemoveGroup/RemoveGroup';
|
||||||
|
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
|
||||||
|
import { IProjectRole } from 'interfaces/role';
|
||||||
|
|
||||||
type PageQueryType = Partial<Record<'search', string>>;
|
type PageQueryType = Partial<Record<'search', string>>;
|
||||||
|
|
||||||
@ -49,6 +51,7 @@ export const GroupsList: VFC = () => {
|
|||||||
const [searchValue, setSearchValue] = useState(
|
const [searchValue, setSearchValue] = useState(
|
||||||
searchParams.get('search') || ''
|
searchParams.get('search') || ''
|
||||||
);
|
);
|
||||||
|
const { roles } = useUsers();
|
||||||
|
|
||||||
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
|
|
||||||
@ -82,6 +85,10 @@ export const GroupsList: VFC = () => {
|
|||||||
setRemoveOpen(true);
|
setRemoveOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getBindableRootRoles = () => {
|
||||||
|
return roles.filter((role: IProjectRole) => role.type === 'root');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent
|
<PageContent
|
||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
@ -134,6 +141,7 @@ export const GroupsList: VFC = () => {
|
|||||||
<Grid key={group.id} item xs={12} md={6}>
|
<Grid key={group.id} item xs={12} md={6}>
|
||||||
<GroupCard
|
<GroupCard
|
||||||
group={group}
|
group={group}
|
||||||
|
rootRoles={getBindableRootRoles()}
|
||||||
onEditUsers={onEditUsers}
|
onEditUsers={onEditUsers}
|
||||||
onRemoveGroup={onRemoveGroup}
|
onRemoveGroup={onRemoveGroup}
|
||||||
/>
|
/>
|
||||||
|
@ -6,7 +6,8 @@ export const useGroupForm = (
|
|||||||
initialName = '',
|
initialName = '',
|
||||||
initialDescription = '',
|
initialDescription = '',
|
||||||
initialMappingsSSO: string[] = [],
|
initialMappingsSSO: string[] = [],
|
||||||
initialUsers: IGroupUser[] = []
|
initialUsers: IGroupUser[] = [],
|
||||||
|
initialRootRole: number | null = null
|
||||||
) => {
|
) => {
|
||||||
const params = useQueryParams();
|
const params = useQueryParams();
|
||||||
const groupQueryName = params.get('name');
|
const groupQueryName = params.get('name');
|
||||||
@ -14,6 +15,7 @@ export const useGroupForm = (
|
|||||||
const [description, setDescription] = useState(initialDescription);
|
const [description, setDescription] = useState(initialDescription);
|
||||||
const [mappingsSSO, setMappingsSSO] = useState(initialMappingsSSO);
|
const [mappingsSSO, setMappingsSSO] = useState(initialMappingsSSO);
|
||||||
const [users, setUsers] = useState<IGroupUser[]>(initialUsers);
|
const [users, setUsers] = useState<IGroupUser[]>(initialUsers);
|
||||||
|
const [rootRole, setRootRole] = useState<number | null>(initialRootRole);
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
|
|
||||||
const getGroupPayload = () => {
|
const getGroupPayload = () => {
|
||||||
@ -24,6 +26,7 @@ export const useGroupForm = (
|
|||||||
users: users.map(({ id }) => ({
|
users: users.map(({ id }) => ({
|
||||||
user: { id },
|
user: { id },
|
||||||
})),
|
})),
|
||||||
|
rootRole: rootRole || undefined,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -44,5 +47,7 @@ export const useGroupForm = (
|
|||||||
clearErrors,
|
clearErrors,
|
||||||
errors,
|
errors,
|
||||||
setErrors,
|
setErrors,
|
||||||
|
rootRole,
|
||||||
|
setRootRole,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -6,6 +6,7 @@ import {
|
|||||||
Checkbox,
|
Checkbox,
|
||||||
styled,
|
styled,
|
||||||
TextField,
|
TextField,
|
||||||
|
Tooltip,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
|
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
|
||||||
import CheckBoxIcon from '@mui/icons-material/CheckBox';
|
import CheckBoxIcon from '@mui/icons-material/CheckBox';
|
||||||
@ -255,6 +256,12 @@ export const ProjectAccessAssign = ({
|
|||||||
--data-raw '${JSON.stringify(payload, undefined, 2)}'`;
|
--data-raw '${JSON.stringify(payload, undefined, 2)}'`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createRootGroupWarning = (group?: IGroup): string | undefined => {
|
||||||
|
if (group && Boolean(group.rootRole)) {
|
||||||
|
return 'This group has an Admin or Editor role associated with it. Groups with a root role association cannot be assigned to projects, and users in this group already have the role applied globally.';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const renderOption = (
|
const renderOption = (
|
||||||
props: React.HTMLAttributes<HTMLLIElement>,
|
props: React.HTMLAttributes<HTMLLIElement>,
|
||||||
option: IAccessOption,
|
option: IAccessOption,
|
||||||
@ -268,35 +275,45 @@ export const ProjectAccessAssign = ({
|
|||||||
optionUser = option.entity as IUser;
|
optionUser = option.entity as IUser;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<li {...props}>
|
<Tooltip title={createRootGroupWarning(optionGroup)}>
|
||||||
<Checkbox
|
<span>
|
||||||
icon={<CheckBoxOutlineBlankIcon fontSize="small" />}
|
<li {...props}>
|
||||||
checkedIcon={<CheckBoxIcon fontSize="small" />}
|
<Checkbox
|
||||||
style={{ marginRight: 8 }}
|
icon={<CheckBoxOutlineBlankIcon fontSize="small" />}
|
||||||
checked={selected}
|
checkedIcon={<CheckBoxIcon fontSize="small" />}
|
||||||
/>
|
style={{ marginRight: 8 }}
|
||||||
<ConditionallyRender
|
checked={selected}
|
||||||
condition={option.type === ENTITY_TYPE.GROUP}
|
/>
|
||||||
show={
|
<ConditionallyRender
|
||||||
<StyledGroupOption>
|
condition={option.type === ENTITY_TYPE.GROUP}
|
||||||
<span>{optionGroup?.name}</span>
|
show={
|
||||||
<span>{optionGroup?.userCount} users</span>
|
<span>
|
||||||
</StyledGroupOption>
|
<StyledGroupOption>
|
||||||
}
|
<span>{optionGroup?.name}</span>
|
||||||
elseShow={
|
<span>
|
||||||
<StyledUserOption>
|
{optionGroup?.userCount} users
|
||||||
<span>
|
</span>
|
||||||
{optionUser?.name || optionUser?.username}
|
</StyledGroupOption>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
}
|
||||||
{optionUser?.name && optionUser?.username
|
elseShow={
|
||||||
? optionUser?.username
|
<StyledUserOption>
|
||||||
: optionUser?.email}
|
<span>
|
||||||
</span>
|
{optionUser?.name ||
|
||||||
</StyledUserOption>
|
optionUser?.username}
|
||||||
}
|
</span>
|
||||||
/>
|
<span>
|
||||||
</li>
|
{optionUser?.name &&
|
||||||
|
optionUser?.username
|
||||||
|
? optionUser?.username
|
||||||
|
: optionUser?.email}
|
||||||
|
</span>
|
||||||
|
</StyledUserOption>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -346,6 +363,14 @@ export const ProjectAccessAssign = ({
|
|||||||
disableCloseOnSelect
|
disableCloseOnSelect
|
||||||
disabled={edit}
|
disabled={edit}
|
||||||
value={selectedOptions}
|
value={selectedOptions}
|
||||||
|
getOptionDisabled={option => {
|
||||||
|
if (option.type === ENTITY_TYPE.GROUP) {
|
||||||
|
const optionGroup =
|
||||||
|
option.entity as IGroup;
|
||||||
|
return Boolean(optionGroup.rootRole);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}}
|
||||||
onChange={(event, newValue, reason) => {
|
onChange={(event, newValue, reason) => {
|
||||||
if (
|
if (
|
||||||
event.type === 'keydown' &&
|
event.type === 'keydown' &&
|
||||||
|
@ -10,6 +10,7 @@ export interface IGroup {
|
|||||||
addedAt?: string;
|
addedAt?: string;
|
||||||
userCount?: number;
|
userCount?: number;
|
||||||
mappingsSSO: string[];
|
mappingsSSO: string[];
|
||||||
|
rootRole?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IGroupUser extends IUser {
|
export interface IGroupUser extends IUser {
|
||||||
|
@ -53,6 +53,7 @@ export interface IFlags {
|
|||||||
personalAccessTokensKillSwitch?: boolean;
|
personalAccessTokensKillSwitch?: boolean;
|
||||||
demo?: boolean;
|
demo?: boolean;
|
||||||
strategyTitle?: boolean;
|
strategyTitle?: boolean;
|
||||||
|
groupRootRoles?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IVersionInfo {
|
export interface IVersionInfo {
|
||||||
|
@ -77,6 +77,7 @@ exports[`should create default config 1`] = `
|
|||||||
"embedProxy": true,
|
"embedProxy": true,
|
||||||
"embedProxyFrontend": true,
|
"embedProxyFrontend": true,
|
||||||
"featuresExportImport": true,
|
"featuresExportImport": true,
|
||||||
|
"groupRootRoles": false,
|
||||||
"loginHistory": false,
|
"loginHistory": false,
|
||||||
"maintenanceMode": false,
|
"maintenanceMode": false,
|
||||||
"messageBanner": false,
|
"messageBanner": false,
|
||||||
@ -105,6 +106,7 @@ exports[`should create default config 1`] = `
|
|||||||
"embedProxy": true,
|
"embedProxy": true,
|
||||||
"embedProxyFrontend": true,
|
"embedProxyFrontend": true,
|
||||||
"featuresExportImport": true,
|
"featuresExportImport": true,
|
||||||
|
"groupRootRoles": false,
|
||||||
"loginHistory": false,
|
"loginHistory": false,
|
||||||
"maintenanceMode": false,
|
"maintenanceMode": false,
|
||||||
"messageBanner": false,
|
"messageBanner": false,
|
||||||
|
@ -142,8 +142,30 @@ export class AccessStore implements IAccessStore {
|
|||||||
.join(`${T.GROUP_ROLE} AS gr`, 'gu.group_id', 'gr.group_id')
|
.join(`${T.GROUP_ROLE} AS gr`, 'gu.group_id', 'gr.group_id')
|
||||||
.join(`${T.ROLE_PERMISSION} AS rp`, 'rp.role_id', 'gr.role_id')
|
.join(`${T.ROLE_PERMISSION} AS rp`, 'rp.role_id', 'gr.role_id')
|
||||||
.join(`${T.PERMISSIONS} AS p`, 'p.id', 'rp.permission_id')
|
.join(`${T.PERMISSIONS} AS p`, 'p.id', 'rp.permission_id')
|
||||||
.where('gu.user_id', '=', userId);
|
.whereNull('g.root_role_id')
|
||||||
|
.andWhere('gu.user_id', '=', userId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
userPermissionQuery = userPermissionQuery.union((db) => {
|
||||||
|
db.select(
|
||||||
|
this.db.raw("'default' as project"),
|
||||||
|
'permission',
|
||||||
|
'environment',
|
||||||
|
'p.type',
|
||||||
|
'g.root_role_id as role_id',
|
||||||
|
)
|
||||||
|
.from<IPermissionRow>(`${T.GROUP_USER} as gu`)
|
||||||
|
.join(`${T.GROUPS} AS g`, 'g.id', 'gu.group_id')
|
||||||
|
.join(
|
||||||
|
`${T.ROLE_PERMISSION} as rp`,
|
||||||
|
'rp.role_id',
|
||||||
|
'g.root_role_id',
|
||||||
|
)
|
||||||
|
.join(`${T.PERMISSIONS} as p`, 'p.id', 'rp.permission_id')
|
||||||
|
.whereNotNull('g.root_role_id')
|
||||||
|
.andWhere('gu.user_id', '=', userId);
|
||||||
|
});
|
||||||
|
|
||||||
const rows = await userPermissionQuery;
|
const rows = await userPermissionQuery;
|
||||||
stopTimer();
|
stopTimer();
|
||||||
return rows.map(this.mapUserPermission);
|
return rows.map(this.mapUserPermission);
|
||||||
|
@ -28,6 +28,7 @@ const GROUP_COLUMNS = [
|
|||||||
'mappings_sso',
|
'mappings_sso',
|
||||||
'created_at',
|
'created_at',
|
||||||
'created_by',
|
'created_by',
|
||||||
|
'root_role_id',
|
||||||
];
|
];
|
||||||
|
|
||||||
const rowToGroup = (row) => {
|
const rowToGroup = (row) => {
|
||||||
@ -41,6 +42,7 @@ const rowToGroup = (row) => {
|
|||||||
mappingsSSO: row.mappings_sso,
|
mappingsSSO: row.mappings_sso,
|
||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
createdBy: row.created_by,
|
createdBy: row.created_by,
|
||||||
|
rootRole: row.root_role_id,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -53,6 +55,7 @@ const rowToGroupUser = (row) => {
|
|||||||
groupId: row.group_id,
|
groupId: row.group_id,
|
||||||
joinedAt: row.created_at,
|
joinedAt: row.created_at,
|
||||||
createdBy: row.created_by,
|
createdBy: row.created_by,
|
||||||
|
rootRoleId: row.root_role_id,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -60,6 +63,7 @@ const groupToRow = (group: IStoreGroup) => ({
|
|||||||
name: group.name,
|
name: group.name,
|
||||||
description: group.description,
|
description: group.description,
|
||||||
mappings_sso: JSON.stringify(group.mappingsSSO),
|
mappings_sso: JSON.stringify(group.mappingsSSO),
|
||||||
|
root_role_id: group.rootRole || null,
|
||||||
});
|
});
|
||||||
|
|
||||||
export default class GroupStore implements IGroupStore {
|
export default class GroupStore implements IGroupStore {
|
||||||
@ -124,9 +128,11 @@ export default class GroupStore implements IGroupStore {
|
|||||||
'u.id as user_id',
|
'u.id as user_id',
|
||||||
'gu.created_at',
|
'gu.created_at',
|
||||||
'gu.created_by',
|
'gu.created_by',
|
||||||
|
'g.root_role_id',
|
||||||
)
|
)
|
||||||
.from(`${T.GROUP_USER} AS gu`)
|
.from(`${T.GROUP_USER} AS gu`)
|
||||||
.join(`${T.USERS} AS u`, 'u.id', 'gu.user_id')
|
.join(`${T.USERS} AS u`, 'u.id', 'gu.user_id')
|
||||||
|
.join(`${T.GROUPS} AS g`, 'g.id', 'gu.group_id')
|
||||||
.whereIn('gu.group_id', groupIds);
|
.whereIn('gu.group_id', groupIds);
|
||||||
return rows.map(rowToGroupUser);
|
return rows.map(rowToGroupUser);
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,12 @@ export const groupSchema = {
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
rootRole: {
|
||||||
|
type: 'number',
|
||||||
|
nullable: true,
|
||||||
|
description:
|
||||||
|
'A role id that is used as the root role for all users in this group. This can be either the id of the Editor or Admin role.',
|
||||||
|
},
|
||||||
createdBy: {
|
createdBy: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
nullable: true,
|
nullable: true,
|
||||||
|
@ -341,6 +341,7 @@ export default class UserAdminController extends Controller {
|
|||||||
id: g.id,
|
id: g.id,
|
||||||
name: g.name,
|
name: g.name,
|
||||||
userCount: g.users.length,
|
userCount: g.users.length,
|
||||||
|
rootRole: g.rootRole,
|
||||||
} as IGroup;
|
} as IGroup;
|
||||||
});
|
});
|
||||||
this.openApiService.respondWithValidation(
|
this.openApiService.respondWithValidation(
|
||||||
|
@ -76,6 +76,10 @@ const flags = {
|
|||||||
process.env.UNLEASH_EXPERIMENTAL_OPTIMAL_304_DIFFER,
|
process.env.UNLEASH_EXPERIMENTAL_OPTIMAL_304_DIFFER,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
|
groupRootRoles: parseEnvVarBoolean(
|
||||||
|
process.env.UNLEASH_EXPERIMENTAL_ROOT_ROLE_GROUPS,
|
||||||
|
false,
|
||||||
|
),
|
||||||
migrationLock: parseEnvVarBoolean(process.env.MIGRATION_LOCK, false),
|
migrationLock: parseEnvVarBoolean(process.env.MIGRATION_LOCK, false),
|
||||||
demo: parseEnvVarBoolean(process.env.UNLEASH_DEMO, false),
|
demo: parseEnvVarBoolean(process.env.UNLEASH_DEMO, false),
|
||||||
strategyTitle: parseEnvVarBoolean(
|
strategyTitle: parseEnvVarBoolean(
|
||||||
|
@ -6,6 +6,7 @@ export interface IGroup {
|
|||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
mappingsSSO?: string[];
|
mappingsSSO?: string[];
|
||||||
|
rootRole?: number;
|
||||||
createdAt?: Date;
|
createdAt?: Date;
|
||||||
userCount?: number;
|
userCount?: number;
|
||||||
createdBy?: string;
|
createdBy?: string;
|
||||||
@ -15,6 +16,7 @@ export interface IGroupUser {
|
|||||||
groupId: number;
|
groupId: number;
|
||||||
userId: number;
|
userId: number;
|
||||||
joinedAt: Date;
|
joinedAt: Date;
|
||||||
|
rootRoleId?: number;
|
||||||
seenAt?: Date;
|
seenAt?: Date;
|
||||||
createdBy?: string;
|
createdBy?: string;
|
||||||
}
|
}
|
||||||
@ -58,6 +60,8 @@ export default class Group implements IGroup {
|
|||||||
|
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
rootRole?: number;
|
||||||
|
|
||||||
description: string;
|
description: string;
|
||||||
|
|
||||||
mappingsSSO: string[];
|
mappingsSSO: string[];
|
||||||
@ -67,6 +71,7 @@ export default class Group implements IGroup {
|
|||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
mappingsSSO,
|
mappingsSSO,
|
||||||
|
rootRole,
|
||||||
createdBy,
|
createdBy,
|
||||||
createdAt,
|
createdAt,
|
||||||
}: IGroup) {
|
}: IGroup) {
|
||||||
@ -78,6 +83,7 @@ export default class Group implements IGroup {
|
|||||||
|
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
|
this.rootRole = rootRole;
|
||||||
this.description = description;
|
this.description = description;
|
||||||
this.mappingsSSO = mappingsSSO;
|
this.mappingsSSO = mappingsSSO;
|
||||||
this.createdBy = createdBy;
|
this.createdBy = createdBy;
|
||||||
|
@ -12,6 +12,7 @@ export interface IStoreGroup {
|
|||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
mappingsSSO?: string[];
|
mappingsSSO?: string[];
|
||||||
|
rootRole?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IGroupStore extends Store<IGroup, number> {
|
export interface IGroupStore extends Store<IGroup, number> {
|
||||||
|
24
src/migrations/20230414105818-add-root-role-to-groups.js
Normal file
24
src/migrations/20230414105818-add-root-role-to-groups.js
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
exports.up = function (db, callback) {
|
||||||
|
db.runSql(
|
||||||
|
`
|
||||||
|
ALTER TABLE groups ADD COLUMN root_role_id INTEGER DEFAULT NULL;
|
||||||
|
ALTER TABLE groups
|
||||||
|
ADD CONSTRAINT fk_group_role_id
|
||||||
|
FOREIGN KEY(root_role_id)
|
||||||
|
REFERENCES roles(id);
|
||||||
|
`,
|
||||||
|
callback,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = function (db, callback) {
|
||||||
|
db.runSql(
|
||||||
|
`
|
||||||
|
ALTER TABLE groups DROP CONSTRAINT fk_group_role_id;
|
||||||
|
ALTER TABLE groups DROP COLUMN root_role_id;
|
||||||
|
`,
|
||||||
|
callback,
|
||||||
|
);
|
||||||
|
};
|
@ -2357,6 +2357,11 @@ The provider you choose for your addon dictates what properties the \`parameters
|
|||||||
},
|
},
|
||||||
"type": "array",
|
"type": "array",
|
||||||
},
|
},
|
||||||
|
"rootRole": {
|
||||||
|
"description": "A role id that is used as the root role for all users in this group. This can be either the id of the Editor or Admin role.",
|
||||||
|
"nullable": true,
|
||||||
|
"type": "number",
|
||||||
|
},
|
||||||
"users": {
|
"users": {
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/components/schemas/groupUserModelSchema",
|
"$ref": "#/components/schemas/groupUserModelSchema",
|
||||||
|
@ -44,6 +44,13 @@ const createUserViewerAccess = async (name, email) => {
|
|||||||
return user;
|
return user;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createUserAdminAccess = async (name, email) => {
|
||||||
|
const { userStore } = stores;
|
||||||
|
const user = await userStore.insert({ name, email });
|
||||||
|
await accessService.addUserToRole(user.id, adminRole.id, 'default');
|
||||||
|
return user;
|
||||||
|
};
|
||||||
|
|
||||||
const hasCommonProjectAccess = async (user, projectName, condition) => {
|
const hasCommonProjectAccess = async (user, projectName, condition) => {
|
||||||
const defaultEnv = 'default';
|
const defaultEnv = 'default';
|
||||||
const developmentEnv = 'development';
|
const developmentEnv = 'development';
|
||||||
@ -1062,3 +1069,181 @@ test('Should allow user to take multiple group roles and have expected permissio
|
|||||||
),
|
),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Should allow user to take on root role through a group that has a root role defined', async () => {
|
||||||
|
const groupStore = stores.groupStore;
|
||||||
|
|
||||||
|
const viewerUser = await createUserViewerAccess(
|
||||||
|
'Vincent Viewer',
|
||||||
|
'vincent@getunleash.io',
|
||||||
|
);
|
||||||
|
|
||||||
|
const groupWithRootAdminRole = await groupStore.create({
|
||||||
|
name: 'GroupThatGrantsAdminRights',
|
||||||
|
description: '',
|
||||||
|
rootRole: adminRole.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await groupStore.addUsersToGroup(
|
||||||
|
groupWithRootAdminRole.id!,
|
||||||
|
[{ user: viewerUser }],
|
||||||
|
'Admin',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await accessService.hasPermission(viewerUser, permissions.ADMIN),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should not elevate permissions for a user that is not present in a root role group', async () => {
|
||||||
|
const groupStore = stores.groupStore;
|
||||||
|
|
||||||
|
const viewerUser = await createUserViewerAccess(
|
||||||
|
'Violet Viewer',
|
||||||
|
'violet@getunleash.io',
|
||||||
|
);
|
||||||
|
|
||||||
|
const viewerUserNotInGroup = await createUserViewerAccess(
|
||||||
|
'Veronica Viewer',
|
||||||
|
'veronica@getunleash.io',
|
||||||
|
);
|
||||||
|
|
||||||
|
const groupWithRootAdminRole = await groupStore.create({
|
||||||
|
name: 'GroupThatGrantsAdminRights',
|
||||||
|
description: '',
|
||||||
|
rootRole: adminRole.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await groupStore.addUsersToGroup(
|
||||||
|
groupWithRootAdminRole.id!,
|
||||||
|
[{ user: viewerUser }],
|
||||||
|
'Admin',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await accessService.hasPermission(viewerUser, permissions.ADMIN),
|
||||||
|
).toBe(true);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await accessService.hasPermission(
|
||||||
|
viewerUserNotInGroup,
|
||||||
|
permissions.ADMIN,
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should not reduce permissions for an admin user that enters an editor group', async () => {
|
||||||
|
const groupStore = stores.groupStore;
|
||||||
|
|
||||||
|
const adminUser = await createUserAdminAccess(
|
||||||
|
'Austin Admin',
|
||||||
|
'austin@getunleash.io',
|
||||||
|
);
|
||||||
|
|
||||||
|
const groupWithRootEditorRole = await groupStore.create({
|
||||||
|
name: 'GroupThatGrantsEditorRights',
|
||||||
|
description: '',
|
||||||
|
rootRole: editorRole.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await groupStore.addUsersToGroup(
|
||||||
|
groupWithRootEditorRole.id!,
|
||||||
|
[{ user: adminUser }],
|
||||||
|
'Admin',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await accessService.hasPermission(adminUser, permissions.ADMIN),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should not change permissions for a user in a group without a root role', async () => {
|
||||||
|
const groupStore = stores.groupStore;
|
||||||
|
|
||||||
|
const viewerUser = await createUserViewerAccess(
|
||||||
|
'Virgil Viewer',
|
||||||
|
'virgil@getunleash.io',
|
||||||
|
);
|
||||||
|
|
||||||
|
const groupWithoutRootRole = await groupStore.create({
|
||||||
|
name: 'GroupWithNoRootRole',
|
||||||
|
description: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const preAddedToGroupPermissions =
|
||||||
|
await accessService.getPermissionsForUser(viewerUser);
|
||||||
|
|
||||||
|
await groupStore.addUsersToGroup(
|
||||||
|
groupWithoutRootRole.id!,
|
||||||
|
[{ user: viewerUser }],
|
||||||
|
'Admin',
|
||||||
|
);
|
||||||
|
|
||||||
|
const postAddedToGroupPermissions =
|
||||||
|
await accessService.getPermissionsForUser(viewerUser);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
JSON.stringify(preAddedToGroupPermissions) ===
|
||||||
|
JSON.stringify(postAddedToGroupPermissions),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should add permissions to user when a group is given a root role after the user has been added to the group', async () => {
|
||||||
|
const groupStore = stores.groupStore;
|
||||||
|
|
||||||
|
const viewerUser = await createUserViewerAccess(
|
||||||
|
'Vera Viewer',
|
||||||
|
'vera@getunleash.io',
|
||||||
|
);
|
||||||
|
|
||||||
|
const groupWithoutRootRole = await groupStore.create({
|
||||||
|
name: 'GroupWithNoRootRole',
|
||||||
|
description: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
await groupStore.addUsersToGroup(
|
||||||
|
groupWithoutRootRole.id!,
|
||||||
|
[{ user: viewerUser }],
|
||||||
|
'Admin',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await accessService.hasPermission(viewerUser, permissions.ADMIN),
|
||||||
|
).toBe(false);
|
||||||
|
|
||||||
|
await groupStore.update({
|
||||||
|
id: groupWithoutRootRole.id!,
|
||||||
|
name: 'GroupWithNoRootRole',
|
||||||
|
rootRole: adminRole.id,
|
||||||
|
users: [{ user: viewerUser }],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await accessService.hasPermission(viewerUser, permissions.ADMIN),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should give full project access to the default project to user in a group with an editor root role', async () => {
|
||||||
|
const projectName = 'default';
|
||||||
|
|
||||||
|
const groupStore = stores.groupStore;
|
||||||
|
|
||||||
|
const viewerUser = await createUserViewerAccess(
|
||||||
|
'Vee viewer',
|
||||||
|
'vee@getunleash.io',
|
||||||
|
);
|
||||||
|
|
||||||
|
const groupWithRootEditorRole = await groupStore.create({
|
||||||
|
name: 'GroupThatGrantsEditorRights',
|
||||||
|
description: '',
|
||||||
|
rootRole: editorRole.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await groupStore.addUsersToGroup(
|
||||||
|
groupWithRootEditorRole.id!,
|
||||||
|
[{ user: viewerUser }],
|
||||||
|
'Admin',
|
||||||
|
);
|
||||||
|
|
||||||
|
await hasFullProjectAccess(viewerUser, projectName, true);
|
||||||
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user