1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01: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:
Simon Hornby 2023-04-20 12:29:30 +02:00 committed by GitHub
parent 163791a094
commit 3b42e866ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 464 additions and 37 deletions

View File

@ -26,6 +26,8 @@ export const CreateGroup = () => {
setMappingsSSO,
users,
setUsers,
rootRole,
setRootRole,
getGroupPayload,
clearErrors,
errors,
@ -95,10 +97,12 @@ export const CreateGroup = () => {
name={name}
description={description}
mappingsSSO={mappingsSSO}
rootRole={rootRole}
users={users}
setName={onSetName}
setDescription={setDescription}
setMappingsSSO={setMappingsSSO}
setRootRole={setRootRole}
setUsers={setUsers}
errors={errors}
handleSubmit={handleSubmit}

View File

@ -55,6 +55,8 @@ export const EditGroup = ({
setMappingsSSO,
users,
setUsers,
rootRole,
setRootRole,
getGroupPayload,
clearErrors,
errors,
@ -63,7 +65,8 @@ export const EditGroup = ({
group?.name,
group?.description,
group?.mappingsSSO,
group?.users
group?.users,
group?.rootRole
);
const { groups } = useGroups();
@ -129,10 +132,12 @@ export const EditGroup = ({
description={description}
mappingsSSO={mappingsSSO}
users={users}
rootRole={rootRole}
setName={onSetName}
setDescription={setDescription}
setMappingsSSO={setMappingsSSO}
setUsers={setUsers}
setRootRole={setRootRole}
errors={errors}
handleSubmit={handleSubmit}
handleCancel={handleCancel}

View File

@ -60,7 +60,8 @@ export const EditGroupUsers: FC<IEditGroupUsersProps> = ({
group.name,
group.description,
group.mappingsSSO,
group.users
group.users,
group.rootRole
);
useEffect(() => {

View File

@ -1,5 +1,5 @@
import React, { FC } from 'react';
import { Box, Button, styled } from '@mui/material';
import { Autocomplete, Box, Button, styled, TextField } from '@mui/material';
import { UG_DESC_ID, UG_NAME_ID } from 'utils/testIds';
import Input from 'component/common/Input/Input';
import { IGroupUser } from 'interfaces/group';
@ -10,6 +10,9 @@ import { ItemList } from 'component/common/ItemList/ItemList';
import useAuthSettings from 'hooks/api/getters/useAuthSettings/useAuthSettings';
import { Link } from 'react-router-dom';
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
import { IProjectRole } from 'interfaces/role';
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
const StyledForm = styled('form')(() => ({
display: 'flex',
@ -63,15 +66,34 @@ const StyledDescriptionBlock = styled('div')(({ theme }) => ({
},
}));
const StyledAutocompleteWrapper = styled('div')(({ theme }) => ({
'& > div:first-of-type': {
width: '100%',
maxWidth: theme.spacing(50),
marginBottom: theme.spacing(2),
},
}));
const StyledRoleOption = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
'& > span:last-of-type': {
fontSize: theme.fontSizes.smallerBody,
color: theme.palette.text.secondary,
},
}));
interface IGroupForm {
name: string;
description: string;
mappingsSSO: string[];
users: IGroupUser[];
rootRole: number | null;
setName: (name: string) => void;
setDescription: React.Dispatch<React.SetStateAction<string>>;
setMappingsSSO: React.Dispatch<React.SetStateAction<string[]>>;
setUsers: React.Dispatch<React.SetStateAction<IGroupUser[]>>;
setRootRole: React.Dispatch<React.SetStateAction<number | null>>;
handleSubmit: (e: any) => void;
handleCancel: () => void;
errors: { [key: string]: string };
@ -83,23 +105,47 @@ export const GroupForm: FC<IGroupForm> = ({
description,
mappingsSSO,
users,
rootRole,
setName,
setDescription,
setMappingsSSO,
setUsers,
handleSubmit,
handleCancel,
setRootRole,
errors,
mode,
children,
}) => {
const { config: oidcSettings } = useAuthSettings('oidc');
const { config: samlSettings } = useAuthSettings('saml');
const { uiConfig } = useUiConfig();
const { roles } = useUsers();
const isGroupSyncingEnabled =
(oidcSettings?.enabled && oidcSettings.enableGroupSyncing) ||
(samlSettings?.enabled && samlSettings.enableGroupSyncing);
const groupRootRolesEnabled = Boolean(uiConfig.flags.groupRootRoles);
const roleIdToRole = (rootRoleId: number | null): IProjectRole | null => {
return (
roles.find((role: IProjectRole) => role.id === rootRoleId) || null
);
};
const renderRoleOption = (
props: React.HTMLAttributes<HTMLLIElement>,
option: IProjectRole
) => (
<li {...props}>
<StyledRoleOption>
<span>{option.name}</span>
<span>{option.description}</span>
</StyledRoleOption>
</li>
);
return (
<StyledForm onSubmit={handleSubmit}>
<div>
@ -146,9 +192,9 @@ export const GroupForm: FC<IGroupForm> = ({
elseShow={() => (
<StyledDescriptionBlock>
<Box sx={{ display: 'flex' }}>
You can enable SSO groups syncronization if
You can enable SSO groups synchronization if
needed
<HelpIcon tooltip="SSO groups syncronization allows SSO groups to be mapped to Unleash groups, so that user group membership is properly synchronized." />
<HelpIcon tooltip="SSO groups synchronization allows SSO groups to be mapped to Unleash groups, so that user group membership is properly synchronized." />
</Box>
<Link data-loading to={`/admin/auth`}>
<span data-loading>View SSO configuration</span>
@ -156,6 +202,40 @@ export const GroupForm: FC<IGroupForm> = ({
</StyledDescriptionBlock>
)}
/>
<ConditionallyRender
condition={groupRootRolesEnabled}
show={
<>
<StyledInputDescription>
<Box sx={{ display: 'flex' }}>
Do you want to associate a root role with
this group?
<HelpIcon tooltip="When you associate an Admin or Editor role with this group, users in this group will automatically inherit the role globally. Note that groups with a root role association cannot be assigned to projects." />
</Box>
</StyledInputDescription>
<StyledAutocompleteWrapper>
<Autocomplete
data-testid="GROUP_ROOT_ROLE"
size="small"
openOnFocus
value={roleIdToRole(rootRole)}
onChange={(_, newValue) =>
setRootRole(newValue?.id || null)
}
options={roles.filter(
(role: IProjectRole) =>
role.name !== 'Viewer'
)}
renderOption={renderRoleOption}
getOptionLabel={option => option.name}
renderInput={params => (
<TextField {...params} label="Role" />
)}
/>
</StyledAutocompleteWrapper>
</>
}
/>
<ConditionallyRender
condition={mode === 'Create'}
show={

View File

@ -6,6 +6,8 @@ import { GroupCardAvatars } from './GroupCardAvatars/GroupCardAvatars';
import { Badge } from 'component/common/Badge/Badge';
import { GroupCardActions } from './GroupCardActions/GroupCardActions';
import TopicOutlinedIcon from '@mui/icons-material/TopicOutlined';
import { IProjectRole } from 'interfaces/role';
import { IProject } from 'interfaces/project';
const StyledLink = styled(Link)(({ theme }) => ({
textDecoration: 'none',
@ -75,14 +77,24 @@ const ProjectBadgeContainer = styled('div')(({ theme }) => ({
flexWrap: 'wrap',
}));
const InfoBadgeDescription = styled('span')(({ theme }) => ({
display: 'flex',
color: theme.palette.text.secondary,
alignItems: 'center',
gap: theme.spacing(1),
fontSize: theme.fontSizes.smallBody,
}));
interface IGroupCardProps {
group: IGroup;
rootRoles: IProjectRole[];
onEditUsers: (group: IGroup) => void;
onRemoveGroup: (group: IGroup) => void;
}
export const GroupCard = ({
group,
rootRoles,
onEditUsers,
onRemoveGroup,
}: IGroupCardProps) => {
@ -101,6 +113,26 @@ export const GroupCard = ({
/>
</StyledHeaderActions>
</StyledTitleRow>
<ConditionallyRender
condition={Boolean(group.rootRole)}
show={
<InfoBadgeDescription>
<p>Root role:</p>
<Badge
color="success"
icon={<TopicOutlinedIcon />}
>
{
rootRoles.find(
(role: IProjectRole) =>
role.id === group.rootRole
)?.name
}
</Badge>
</InfoBadgeDescription>
}
/>
<StyledDescription>{group.description}</StyledDescription>
<StyledBottomRow>
<ConditionallyRender
@ -143,7 +175,10 @@ export const GroupCard = ({
arrow
describeChild
>
<Badge>Not used</Badge>
<ConditionallyRender
condition={!group.rootRole}
show={<Badge>Not used</Badge>}
/>
</Tooltip>
}
/>

View File

@ -18,6 +18,8 @@ import { Add } from '@mui/icons-material';
import { NAVIGATE_TO_CREATE_GROUP } from 'utils/testIds';
import { EditGroupUsers } from '../Group/EditGroupUsers/EditGroupUsers';
import { RemoveGroup } from '../RemoveGroup/RemoveGroup';
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
import { IProjectRole } from 'interfaces/role';
type PageQueryType = Partial<Record<'search', string>>;
@ -49,6 +51,7 @@ export const GroupsList: VFC = () => {
const [searchValue, setSearchValue] = useState(
searchParams.get('search') || ''
);
const { roles } = useUsers();
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'));
@ -82,6 +85,10 @@ export const GroupsList: VFC = () => {
setRemoveOpen(true);
};
const getBindableRootRoles = () => {
return roles.filter((role: IProjectRole) => role.type === 'root');
};
return (
<PageContent
isLoading={loading}
@ -134,6 +141,7 @@ export const GroupsList: VFC = () => {
<Grid key={group.id} item xs={12} md={6}>
<GroupCard
group={group}
rootRoles={getBindableRootRoles()}
onEditUsers={onEditUsers}
onRemoveGroup={onRemoveGroup}
/>

View File

@ -6,7 +6,8 @@ export const useGroupForm = (
initialName = '',
initialDescription = '',
initialMappingsSSO: string[] = [],
initialUsers: IGroupUser[] = []
initialUsers: IGroupUser[] = [],
initialRootRole: number | null = null
) => {
const params = useQueryParams();
const groupQueryName = params.get('name');
@ -14,6 +15,7 @@ export const useGroupForm = (
const [description, setDescription] = useState(initialDescription);
const [mappingsSSO, setMappingsSSO] = useState(initialMappingsSSO);
const [users, setUsers] = useState<IGroupUser[]>(initialUsers);
const [rootRole, setRootRole] = useState<number | null>(initialRootRole);
const [errors, setErrors] = useState({});
const getGroupPayload = () => {
@ -24,6 +26,7 @@ export const useGroupForm = (
users: users.map(({ id }) => ({
user: { id },
})),
rootRole: rootRole || undefined,
};
};
@ -44,5 +47,7 @@ export const useGroupForm = (
clearErrors,
errors,
setErrors,
rootRole,
setRootRole,
};
};

View File

@ -6,6 +6,7 @@ import {
Checkbox,
styled,
TextField,
Tooltip,
} from '@mui/material';
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
import CheckBoxIcon from '@mui/icons-material/CheckBox';
@ -255,6 +256,12 @@ export const ProjectAccessAssign = ({
--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 = (
props: React.HTMLAttributes<HTMLLIElement>,
option: IAccessOption,
@ -268,35 +275,45 @@ export const ProjectAccessAssign = ({
optionUser = option.entity as IUser;
}
return (
<li {...props}>
<Checkbox
icon={<CheckBoxOutlineBlankIcon fontSize="small" />}
checkedIcon={<CheckBoxIcon fontSize="small" />}
style={{ marginRight: 8 }}
checked={selected}
/>
<ConditionallyRender
condition={option.type === ENTITY_TYPE.GROUP}
show={
<StyledGroupOption>
<span>{optionGroup?.name}</span>
<span>{optionGroup?.userCount} users</span>
</StyledGroupOption>
}
elseShow={
<StyledUserOption>
<span>
{optionUser?.name || optionUser?.username}
</span>
<span>
{optionUser?.name && optionUser?.username
? optionUser?.username
: optionUser?.email}
</span>
</StyledUserOption>
}
/>
</li>
<Tooltip title={createRootGroupWarning(optionGroup)}>
<span>
<li {...props}>
<Checkbox
icon={<CheckBoxOutlineBlankIcon fontSize="small" />}
checkedIcon={<CheckBoxIcon fontSize="small" />}
style={{ marginRight: 8 }}
checked={selected}
/>
<ConditionallyRender
condition={option.type === ENTITY_TYPE.GROUP}
show={
<span>
<StyledGroupOption>
<span>{optionGroup?.name}</span>
<span>
{optionGroup?.userCount} users
</span>
</StyledGroupOption>
</span>
}
elseShow={
<StyledUserOption>
<span>
{optionUser?.name ||
optionUser?.username}
</span>
<span>
{optionUser?.name &&
optionUser?.username
? optionUser?.username
: optionUser?.email}
</span>
</StyledUserOption>
}
/>
</li>
</span>
</Tooltip>
);
};
@ -346,6 +363,14 @@ export const ProjectAccessAssign = ({
disableCloseOnSelect
disabled={edit}
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) => {
if (
event.type === 'keydown' &&

View File

@ -10,6 +10,7 @@ export interface IGroup {
addedAt?: string;
userCount?: number;
mappingsSSO: string[];
rootRole?: number;
}
export interface IGroupUser extends IUser {

View File

@ -53,6 +53,7 @@ export interface IFlags {
personalAccessTokensKillSwitch?: boolean;
demo?: boolean;
strategyTitle?: boolean;
groupRootRoles?: boolean;
}
export interface IVersionInfo {

View File

@ -77,6 +77,7 @@ exports[`should create default config 1`] = `
"embedProxy": true,
"embedProxyFrontend": true,
"featuresExportImport": true,
"groupRootRoles": false,
"loginHistory": false,
"maintenanceMode": false,
"messageBanner": false,
@ -105,6 +106,7 @@ exports[`should create default config 1`] = `
"embedProxy": true,
"embedProxyFrontend": true,
"featuresExportImport": true,
"groupRootRoles": false,
"loginHistory": false,
"maintenanceMode": false,
"messageBanner": false,

View File

@ -142,8 +142,30 @@ export class AccessStore implements IAccessStore {
.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.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;
stopTimer();
return rows.map(this.mapUserPermission);

View File

@ -28,6 +28,7 @@ const GROUP_COLUMNS = [
'mappings_sso',
'created_at',
'created_by',
'root_role_id',
];
const rowToGroup = (row) => {
@ -41,6 +42,7 @@ const rowToGroup = (row) => {
mappingsSSO: row.mappings_sso,
createdAt: row.created_at,
createdBy: row.created_by,
rootRole: row.root_role_id,
});
};
@ -53,6 +55,7 @@ const rowToGroupUser = (row) => {
groupId: row.group_id,
joinedAt: row.created_at,
createdBy: row.created_by,
rootRoleId: row.root_role_id,
};
};
@ -60,6 +63,7 @@ const groupToRow = (group: IStoreGroup) => ({
name: group.name,
description: group.description,
mappings_sso: JSON.stringify(group.mappingsSSO),
root_role_id: group.rootRole || null,
});
export default class GroupStore implements IGroupStore {
@ -124,9 +128,11 @@ export default class GroupStore implements IGroupStore {
'u.id as user_id',
'gu.created_at',
'gu.created_by',
'g.root_role_id',
)
.from(`${T.GROUP_USER} AS gu`)
.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);
return rows.map(rowToGroupUser);
}

View File

@ -24,6 +24,12 @@ export const groupSchema = {
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: {
type: 'string',
nullable: true,

View File

@ -341,6 +341,7 @@ export default class UserAdminController extends Controller {
id: g.id,
name: g.name,
userCount: g.users.length,
rootRole: g.rootRole,
} as IGroup;
});
this.openApiService.respondWithValidation(

View File

@ -76,6 +76,10 @@ const flags = {
process.env.UNLEASH_EXPERIMENTAL_OPTIMAL_304_DIFFER,
false,
),
groupRootRoles: parseEnvVarBoolean(
process.env.UNLEASH_EXPERIMENTAL_ROOT_ROLE_GROUPS,
false,
),
migrationLock: parseEnvVarBoolean(process.env.MIGRATION_LOCK, false),
demo: parseEnvVarBoolean(process.env.UNLEASH_DEMO, false),
strategyTitle: parseEnvVarBoolean(

View File

@ -6,6 +6,7 @@ export interface IGroup {
name: string;
description?: string;
mappingsSSO?: string[];
rootRole?: number;
createdAt?: Date;
userCount?: number;
createdBy?: string;
@ -15,6 +16,7 @@ export interface IGroupUser {
groupId: number;
userId: number;
joinedAt: Date;
rootRoleId?: number;
seenAt?: Date;
createdBy?: string;
}
@ -58,6 +60,8 @@ export default class Group implements IGroup {
name: string;
rootRole?: number;
description: string;
mappingsSSO: string[];
@ -67,6 +71,7 @@ export default class Group implements IGroup {
name,
description,
mappingsSSO,
rootRole,
createdBy,
createdAt,
}: IGroup) {
@ -78,6 +83,7 @@ export default class Group implements IGroup {
this.id = id;
this.name = name;
this.rootRole = rootRole;
this.description = description;
this.mappingsSSO = mappingsSSO;
this.createdBy = createdBy;

View File

@ -12,6 +12,7 @@ export interface IStoreGroup {
name: string;
description?: string;
mappingsSSO?: string[];
rootRole?: number;
}
export interface IGroupStore extends Store<IGroup, number> {

View 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,
);
};

View File

@ -2357,6 +2357,11 @@ The provider you choose for your addon dictates what properties the \`parameters
},
"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": {
"items": {
"$ref": "#/components/schemas/groupUserModelSchema",

View File

@ -44,6 +44,13 @@ const createUserViewerAccess = async (name, email) => {
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 defaultEnv = 'default';
const developmentEnv = 'development';
@ -1062,3 +1069,181 @@ test('Should allow user to take multiple group roles and have expected permissio
),
).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);
});