mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	Merge remote-tracking branch 'origin/v4' into v4
This commit is contained in:
		
						commit
						de94a4af2c
					
				| @ -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,6 +275,8 @@ export const ProjectAccessAssign = ({ | |||||||
|             optionUser = option.entity as IUser; |             optionUser = option.entity as IUser; | ||||||
|         } |         } | ||||||
|         return ( |         return ( | ||||||
|  |             <Tooltip title={createRootGroupWarning(optionGroup)}> | ||||||
|  |                 <span> | ||||||
|                     <li {...props}> |                     <li {...props}> | ||||||
|                         <Checkbox |                         <Checkbox | ||||||
|                             icon={<CheckBoxOutlineBlankIcon fontSize="small" />} |                             icon={<CheckBoxOutlineBlankIcon fontSize="small" />} | ||||||
| @ -278,18 +287,24 @@ export const ProjectAccessAssign = ({ | |||||||
|                         <ConditionallyRender |                         <ConditionallyRender | ||||||
|                             condition={option.type === ENTITY_TYPE.GROUP} |                             condition={option.type === ENTITY_TYPE.GROUP} | ||||||
|                             show={ |                             show={ | ||||||
|  |                                 <span> | ||||||
|                                     <StyledGroupOption> |                                     <StyledGroupOption> | ||||||
|                                         <span>{optionGroup?.name}</span> |                                         <span>{optionGroup?.name}</span> | ||||||
|                             <span>{optionGroup?.userCount} users</span> |                                         <span> | ||||||
|  |                                             {optionGroup?.userCount} users | ||||||
|  |                                         </span> | ||||||
|                                     </StyledGroupOption> |                                     </StyledGroupOption> | ||||||
|  |                                 </span> | ||||||
|                             } |                             } | ||||||
|                             elseShow={ |                             elseShow={ | ||||||
|                                 <StyledUserOption> |                                 <StyledUserOption> | ||||||
|                                     <span> |                                     <span> | ||||||
|                                 {optionUser?.name || optionUser?.username} |                                         {optionUser?.name || | ||||||
|  |                                             optionUser?.username} | ||||||
|                                     </span> |                                     </span> | ||||||
|                                     <span> |                                     <span> | ||||||
|                                 {optionUser?.name && optionUser?.username |                                         {optionUser?.name && | ||||||
|  |                                         optionUser?.username | ||||||
|                                             ? optionUser?.username |                                             ? optionUser?.username | ||||||
|                                             : optionUser?.email} |                                             : optionUser?.email} | ||||||
|                                     </span> |                                     </span> | ||||||
| @ -297,6 +312,8 @@ export const ProjectAccessAssign = ({ | |||||||
|                             } |                             } | ||||||
|                         /> |                         /> | ||||||
|                     </li> |                     </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 { | ||||||
|  | |||||||
| @ -52,6 +52,8 @@ export interface IFlags { | |||||||
|     projectScopedStickiness?: boolean; |     projectScopedStickiness?: boolean; | ||||||
|     personalAccessTokensKillSwitch?: boolean; |     personalAccessTokensKillSwitch?: boolean; | ||||||
|     demo?: boolean; |     demo?: 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, | ||||||
| @ -104,6 +105,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( | ||||||
|  | |||||||
| @ -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, | ||||||
|  |     ); | ||||||
|  | }; | ||||||
| @ -1938,6 +1938,11 @@ exports[`should serve the OpenAPI spec 1`] = ` | |||||||
|             }, |             }, | ||||||
|             "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