mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-15 01:16:22 +02:00
feat: multiple project roles (#4512)
https://linear.app/unleash/issue/2-1128/change-the-api-to-support-adding-multiple-roles-to-a-usergroup-on-a https://linear.app/unleash/issue/2-1125/be-able-to-fetch-all-roles-for-a-user-in-a-project https://linear.app/unleash/issue/2-1127/adapt-the-ui-to-be-able-to-do-a-multi-select-on-role-permissions-for - Allows assigning project roles to groups with root roles - Implements new methods that support assigning, editing, removing and retrieving multiple project roles in project access, along with other auxiliary methods - Adds new events for updating and removing assigned roles - Adapts `useProjectApi` to new methods that use new endpoints that support multiple roles - Adds the `multipleRoles` feature flag that controls the possibility of selecting multiple roles on the UI - Adapts `ProjectAccessAssign` to support multiple role, using the new methods - Adds a new `MultipleRoleSelect` component that allows you to select multiple roles based on the `RoleSelect` component - Adapts the `RoleCell` component to support either a single role or multiple roles - Updates the `access.spec.ts` Cypress e2e test to reflect our new logic - Updates `access-service.e2e.test.ts` with tests covering the multiple roles logic and covering some corner cases - Updates `project-service.e2e.test.ts` to adapt to the new logic, adding a test that covers adding access with `[roles], [groups], [users]` - Misc refactors and boy scouting  --------- Co-authored-by: David Leek <david@getunleash.io> Co-authored-by: Mateusz Kwasniewski <kwasniewski.mateusz@gmail.com> Co-authored-by: Nuno Góis <github@nunogois.com>
This commit is contained in:
parent
1f96c1646c
commit
21b4ada577
@ -47,6 +47,19 @@ describe('project-access', () => {
|
||||
id: groupAndProjectName,
|
||||
name: groupAndProjectName,
|
||||
});
|
||||
|
||||
cy.intercept('GET', `${baseUrl}/api/admin/ui-config`, req => {
|
||||
req.headers['cache-control'] =
|
||||
'no-cache, no-store, must-revalidate';
|
||||
req.on('response', res => {
|
||||
if (res.body) {
|
||||
res.body.flags = {
|
||||
...res.body.flags,
|
||||
multipleRoles: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
@ -76,7 +89,7 @@ describe('project-access', () => {
|
||||
|
||||
cy.intercept(
|
||||
'POST',
|
||||
`/api/admin/projects/${groupAndProjectName}/role/4/access`
|
||||
`/api/admin/projects/${groupAndProjectName}/access`
|
||||
).as('assignAccess');
|
||||
|
||||
cy.get(`[data-testid='${PA_USERS_GROUPS_ID}']`).click();
|
||||
@ -95,7 +108,7 @@ describe('project-access', () => {
|
||||
|
||||
cy.intercept(
|
||||
'POST',
|
||||
`/api/admin/projects/${groupAndProjectName}/role/4/access`
|
||||
`/api/admin/projects/${groupAndProjectName}/access`
|
||||
).as('assignAccess');
|
||||
|
||||
cy.get(`[data-testid='${PA_USERS_GROUPS_ID}']`).click();
|
||||
@ -114,9 +127,10 @@ describe('project-access', () => {
|
||||
|
||||
cy.intercept(
|
||||
'PUT',
|
||||
`/api/admin/projects/${groupAndProjectName}/groups/${groupIds[0]}/roles/5`
|
||||
`/api/admin/projects/${groupAndProjectName}/groups/${groupIds[0]}/roles`
|
||||
).as('editAccess');
|
||||
|
||||
cy.get(`[data-testid='CancelIcon']`).last().click();
|
||||
cy.get(`[data-testid='${PA_ROLE_ID}']`).click();
|
||||
cy.contains('update feature toggles within a project').click({
|
||||
force: true,
|
||||
@ -128,12 +142,31 @@ describe('project-access', () => {
|
||||
cy.get("td span:contains('Member')").should('have.length', 1);
|
||||
});
|
||||
|
||||
it('can edit role to multiple roles', () => {
|
||||
cy.get(`[data-testid='${PA_EDIT_BUTTON_ID}']`).first().click();
|
||||
|
||||
cy.intercept(
|
||||
'PUT',
|
||||
`/api/admin/projects/${groupAndProjectName}/groups/${groupIds[0]}/roles`
|
||||
).as('editAccess');
|
||||
|
||||
cy.get(`[data-testid='${PA_ROLE_ID}']`).click();
|
||||
cy.contains('full control over the project').click({
|
||||
force: true,
|
||||
});
|
||||
|
||||
cy.get(`[data-testid='${PA_ASSIGN_CREATE_ID}']`).click();
|
||||
cy.wait('@editAccess');
|
||||
cy.get("td span:contains('Owner')").should('have.length', 2);
|
||||
cy.get("td span:contains('2 roles')").should('have.length', 1);
|
||||
});
|
||||
|
||||
it('can remove access', () => {
|
||||
cy.get(`[data-testid='${PA_REMOVE_BUTTON_ID}']`).first().click();
|
||||
|
||||
cy.intercept(
|
||||
'DELETE',
|
||||
`/api/admin/projects/${groupAndProjectName}/groups/${groupIds[0]}/roles/5`
|
||||
`/api/admin/projects/${groupAndProjectName}/groups/${groupIds[0]}/roles`
|
||||
).as('removeAccess');
|
||||
|
||||
cy.contains("Yes, I'm sure").click();
|
||||
|
@ -93,9 +93,13 @@ export const ServiceAccountsTable = () => {
|
||||
accessor: (row: any) =>
|
||||
roles.find((role: IRole) => role.id === row.rootRole)
|
||||
?.name || '',
|
||||
Cell: ({ row: { original: serviceAccount }, value }: any) => (
|
||||
<RoleCell value={value} roleId={serviceAccount.rootRole} />
|
||||
),
|
||||
Cell: ({
|
||||
row: { original: serviceAccount },
|
||||
value,
|
||||
}: {
|
||||
row: { original: IServiceAccount };
|
||||
value: string;
|
||||
}) => <RoleCell value={value} role={serviceAccount.rootRole} />,
|
||||
maxWidth: 120,
|
||||
},
|
||||
{
|
||||
|
@ -125,9 +125,13 @@ const UsersList = () => {
|
||||
accessor: (row: any) =>
|
||||
roles.find((role: IRole) => role.id === row.rootRole)
|
||||
?.name || '',
|
||||
Cell: ({ row: { original: user }, value }: any) => (
|
||||
<RoleCell value={value} roleId={user.rootRole} />
|
||||
),
|
||||
Cell: ({
|
||||
row: { original: user },
|
||||
value,
|
||||
}: {
|
||||
row: { original: IUser };
|
||||
value: string;
|
||||
}) => <RoleCell value={value} role={user.rootRole} />,
|
||||
maxWidth: 120,
|
||||
},
|
||||
{
|
||||
|
@ -0,0 +1,89 @@
|
||||
import {
|
||||
Autocomplete,
|
||||
AutocompleteProps,
|
||||
AutocompleteRenderOptionState,
|
||||
Checkbox,
|
||||
TextField,
|
||||
styled,
|
||||
} from '@mui/material';
|
||||
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
|
||||
import CheckBoxIcon from '@mui/icons-material/CheckBox';
|
||||
import { IRole } from 'interfaces/role';
|
||||
import { RoleDescription } from '../RoleDescription/RoleDescription';
|
||||
import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender';
|
||||
|
||||
const StyledRoleOption = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
'& > span:last-of-type': {
|
||||
fontSize: theme.fontSizes.smallerBody,
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
}));
|
||||
|
||||
interface IMultipleRoleSelectProps
|
||||
extends Partial<AutocompleteProps<IRole, true, false, false>> {
|
||||
roles: IRole[];
|
||||
value: IRole[];
|
||||
setValue: (role: IRole[]) => void;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export const MultipleRoleSelect = ({
|
||||
roles,
|
||||
value,
|
||||
setValue,
|
||||
required,
|
||||
...rest
|
||||
}: IMultipleRoleSelectProps) => {
|
||||
const renderRoleOption = (
|
||||
props: React.HTMLAttributes<HTMLLIElement>,
|
||||
option: IRole,
|
||||
state: AutocompleteRenderOptionState
|
||||
) => (
|
||||
<li {...props}>
|
||||
<Checkbox
|
||||
icon={<CheckBoxOutlineBlankIcon fontSize="small" />}
|
||||
checkedIcon={<CheckBoxIcon fontSize="small" />}
|
||||
style={{ marginRight: 8 }}
|
||||
checked={state.selected}
|
||||
/>
|
||||
<StyledRoleOption>
|
||||
<span>{option.name}</span>
|
||||
<span>{option.description}</span>
|
||||
</StyledRoleOption>
|
||||
</li>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Autocomplete
|
||||
multiple
|
||||
disableCloseOnSelect
|
||||
openOnFocus
|
||||
size="small"
|
||||
value={value}
|
||||
onChange={(_, roles) => setValue(roles)}
|
||||
options={roles}
|
||||
renderOption={renderRoleOption}
|
||||
getOptionLabel={option => option.name}
|
||||
renderInput={params => (
|
||||
<TextField {...params} label="Role" required={required} />
|
||||
)}
|
||||
{...rest}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={value.length > 0}
|
||||
show={() =>
|
||||
value.map(({ id }) => (
|
||||
<RoleDescription
|
||||
key={id}
|
||||
sx={{ marginTop: 1 }}
|
||||
roleId={id}
|
||||
/>
|
||||
))
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
@ -21,7 +21,7 @@ const StyledDescription = styled('div', {
|
||||
: theme.palette.neutral.light,
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
borderRadius: theme.shape.borderRadiusMedium,
|
||||
borderRadius: tooltip ? 0 : theme.shape.borderRadiusMedium,
|
||||
}));
|
||||
|
||||
const StyledDescriptionBlock = styled('div')(({ theme }) => ({
|
||||
|
@ -3,20 +3,52 @@ import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
|
||||
import { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
|
||||
import { RoleDescription } from 'component/common/RoleDescription/RoleDescription';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { styled } from '@mui/material';
|
||||
|
||||
interface IRoleCellProps {
|
||||
roleId: number;
|
||||
const StyledRoleDescriptions = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: theme.spacing(0.5),
|
||||
'& > *:not(:last-child)': {
|
||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||
paddingBottom: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
type TSingleRoleProps = {
|
||||
value: string;
|
||||
}
|
||||
role: number;
|
||||
roles?: never;
|
||||
};
|
||||
|
||||
export const RoleCell: VFC<IRoleCellProps> = ({ roleId, value }) => {
|
||||
type TMultipleRolesProps = {
|
||||
value: string;
|
||||
roles: number[];
|
||||
role?: never;
|
||||
};
|
||||
|
||||
type TRoleCellProps = TSingleRoleProps | TMultipleRolesProps;
|
||||
|
||||
export const RoleCell: VFC<TRoleCellProps> = ({ role, roles, value }) => {
|
||||
const { isEnterprise } = useUiConfig();
|
||||
|
||||
if (isEnterprise()) {
|
||||
const rolesArray = roles ? roles : [role];
|
||||
|
||||
return (
|
||||
<TextCell>
|
||||
<TooltipLink
|
||||
tooltip={<RoleDescription roleId={roleId} tooltip />}
|
||||
tooltip={
|
||||
<StyledRoleDescriptions>
|
||||
{rolesArray.map(roleId => (
|
||||
<RoleDescription
|
||||
key={roleId}
|
||||
roleId={roleId}
|
||||
tooltip
|
||||
/>
|
||||
))}
|
||||
</StyledRoleDescriptions>
|
||||
}
|
||||
>
|
||||
{value}
|
||||
</TooltipLink>
|
||||
|
@ -35,6 +35,7 @@ import {
|
||||
} from 'utils/testIds';
|
||||
import { caseInsensitiveSearch } from 'utils/search';
|
||||
import { IServiceAccount } from 'interfaces/service-account';
|
||||
import { MultipleRoleSelect } from 'component/common/MultipleRoleSelect/MultipleRoleSelect';
|
||||
import { RoleSelect } from 'component/common/RoleSelect/RoleSelect';
|
||||
|
||||
const StyledForm = styled('form')(() => ({
|
||||
@ -111,7 +112,7 @@ export const ProjectAccessAssign = ({
|
||||
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const { refetchProjectAccess } = useProjectAccess(projectId);
|
||||
const { addAccessToProject, changeUserRole, changeGroupRole, loading } =
|
||||
const { addAccessToProject, setUserRoles, setGroupRoles, loading } =
|
||||
useProjectApi();
|
||||
const edit = Boolean(selected);
|
||||
|
||||
@ -181,38 +182,46 @@ export const ProjectAccessAssign = ({
|
||||
id === selected?.entity.id && type === selected?.type
|
||||
)
|
||||
);
|
||||
const [role, setRole] = useState<IRole | null>(
|
||||
roles.find(({ id }) => id === selected?.entity.roleId) ?? null
|
||||
const [selectedRoles, setRoles] = useState<IRole[]>(
|
||||
roles.filter(({ id }) => selected?.entity?.roles?.includes(id))
|
||||
);
|
||||
|
||||
const payload = {
|
||||
roles: selectedRoles.map(({ id }) => id),
|
||||
groups: selectedOptions
|
||||
?.filter(({ type }) => type === ENTITY_TYPE.GROUP)
|
||||
.map(({ id }) => id),
|
||||
users: selectedOptions
|
||||
?.filter(
|
||||
({ type }) =>
|
||||
type === ENTITY_TYPE.USER ||
|
||||
type === ENTITY_TYPE.SERVICE_ACCOUNT
|
||||
)
|
||||
.map(({ id }) => ({ id })),
|
||||
groups: selectedOptions
|
||||
?.filter(({ type }) => type === ENTITY_TYPE.GROUP)
|
||||
.map(({ id }) => ({ id })),
|
||||
.map(({ id }) => id),
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!isValid) return;
|
||||
|
||||
try {
|
||||
if (!edit) {
|
||||
await addAccessToProject(projectId, role.id, payload);
|
||||
await addAccessToProject(projectId, payload);
|
||||
} else if (
|
||||
selected?.type === ENTITY_TYPE.USER ||
|
||||
selected?.type === ENTITY_TYPE.SERVICE_ACCOUNT
|
||||
) {
|
||||
await changeUserRole(projectId, role.id, selected.entity.id);
|
||||
await setUserRoles(
|
||||
projectId,
|
||||
selectedRoles.map(({ id }) => id),
|
||||
selected.entity.id
|
||||
);
|
||||
} else if (selected?.type === ENTITY_TYPE.GROUP) {
|
||||
await changeGroupRole(projectId, role.id, selected.entity.id);
|
||||
await setGroupRoles(
|
||||
projectId,
|
||||
selectedRoles.map(({ id }) => id),
|
||||
selected.entity.id
|
||||
);
|
||||
}
|
||||
refetchProjectAccess();
|
||||
navigate(GO_BACK);
|
||||
@ -229,22 +238,24 @@ export const ProjectAccessAssign = ({
|
||||
|
||||
const formatApiCode = () => {
|
||||
if (edit) {
|
||||
return `curl --location --request ${edit ? 'PUT' : 'POST'} '${
|
||||
return `curl --location --request PUT '${
|
||||
uiConfig.unleashUrl
|
||||
}/api/admin/projects/${projectId}/${
|
||||
selected?.type === ENTITY_TYPE.USER ||
|
||||
selected?.type === ENTITY_TYPE.SERVICE_ACCOUNT
|
||||
? 'users'
|
||||
: 'groups'
|
||||
}/${selected?.entity.id}/roles/${role?.id}' \\
|
||||
--header 'Authorization: INSERT_API_KEY'`;
|
||||
}/${selected?.entity.id}/roles' \\
|
||||
--header 'Authorization: INSERT_API_KEY' \\
|
||||
--header 'Content-Type: application/json' \\
|
||||
--data-raw '${JSON.stringify({ roles: payload.roles }, undefined, 2)}'`;
|
||||
}
|
||||
return `curl --location --request ${edit ? 'PUT' : 'POST'} '${
|
||||
return `curl --location --request POST '${
|
||||
uiConfig.unleashUrl
|
||||
}/api/admin/projects/${projectId}/role/${role?.id}/access' \\
|
||||
--header 'Authorization: INSERT_API_KEY' \\
|
||||
--header 'Content-Type: application/json' \\
|
||||
--data-raw '${JSON.stringify(payload, undefined, 2)}'`;
|
||||
}/api/admin/projects/${projectId}/access' \\
|
||||
--header 'Authorization: INSERT_API_KEY' \\
|
||||
--header 'Content-Type: application/json' \\
|
||||
--data-raw '${JSON.stringify(payload, undefined, 2)}'`;
|
||||
};
|
||||
|
||||
const createRootGroupWarning = (group?: IGroup): string | undefined => {
|
||||
@ -308,7 +319,7 @@ export const ProjectAccessAssign = ({
|
||||
);
|
||||
};
|
||||
|
||||
const isValid = selectedOptions.length > 0 && role;
|
||||
const isValid = selectedOptions.length > 0 && selectedRoles.length > 0;
|
||||
|
||||
return (
|
||||
<SidebarModal
|
||||
@ -430,11 +441,28 @@ export const ProjectAccessAssign = ({
|
||||
Select the role to assign for this project
|
||||
</StyledInputDescription>
|
||||
<StyledAutocompleteWrapper>
|
||||
<RoleSelect
|
||||
data-testid={PA_ROLE_ID}
|
||||
roles={roles}
|
||||
value={role}
|
||||
setValue={role => setRole(role || null)}
|
||||
<ConditionallyRender
|
||||
condition={Boolean(
|
||||
uiConfig.flags.multipleRoles
|
||||
)}
|
||||
show={() => (
|
||||
<MultipleRoleSelect
|
||||
data-testid={PA_ROLE_ID}
|
||||
roles={roles}
|
||||
value={selectedRoles}
|
||||
setValue={setRoles}
|
||||
/>
|
||||
)}
|
||||
elseShow={() => (
|
||||
<RoleSelect
|
||||
data-testid={PA_ROLE_ID}
|
||||
roles={roles}
|
||||
value={selectedRoles[0]}
|
||||
setValue={role =>
|
||||
setRoles(role ? [role] : [])
|
||||
}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</StyledAutocompleteWrapper>
|
||||
</div>
|
||||
|
@ -93,7 +93,7 @@ export const ProjectAccessTable: VFC = () => {
|
||||
const { setToastData } = useToast();
|
||||
|
||||
const { access, refetchProjectAccess } = useProjectAccess(projectId);
|
||||
const { removeUserFromRole, removeGroupFromRole } = useProjectApi();
|
||||
const { removeUserAccess, removeGroupAccess } = useProjectApi();
|
||||
const [removeOpen, setRemoveOpen] = useState(false);
|
||||
const [groupOpen, setGroupOpen] = useState(false);
|
||||
const [selectedRow, setSelectedRow] = useState<IProjectAccess>();
|
||||
@ -151,11 +151,18 @@ export const ProjectAccessTable: VFC = () => {
|
||||
id: 'role',
|
||||
Header: 'Role',
|
||||
accessor: (row: IProjectAccess) =>
|
||||
access?.roles.find(({ id }) => id === row.entity.roleId)
|
||||
?.name,
|
||||
Cell: ({ value, row: { original: row } }: any) => (
|
||||
<RoleCell roleId={row.entity.roleId} value={value} />
|
||||
),
|
||||
row.entity.roles.length > 1
|
||||
? `${row.entity.roles.length} roles`
|
||||
: access?.roles.find(
|
||||
({ id }) => id === row.entity.roleId
|
||||
)?.name,
|
||||
Cell: ({
|
||||
value,
|
||||
row: { original: row },
|
||||
}: {
|
||||
row: { original: IProjectAccess };
|
||||
value: string;
|
||||
}) => <RoleCell value={value} roles={row.entity.roles} />,
|
||||
maxWidth: 125,
|
||||
filterName: 'role',
|
||||
},
|
||||
@ -345,7 +352,7 @@ export const ProjectAccessTable: VFC = () => {
|
||||
|
||||
const removeAccess = async (userOrGroup?: IProjectAccess) => {
|
||||
if (!userOrGroup) return;
|
||||
const { id, roleId } = userOrGroup.entity;
|
||||
const { id } = userOrGroup.entity;
|
||||
let name = userOrGroup.entity.name;
|
||||
if (userOrGroup.type !== ENTITY_TYPE.GROUP) {
|
||||
const user = userOrGroup.entity as IUser;
|
||||
@ -354,9 +361,9 @@ export const ProjectAccessTable: VFC = () => {
|
||||
|
||||
try {
|
||||
if (userOrGroup.type !== ENTITY_TYPE.GROUP) {
|
||||
await removeUserFromRole(projectId, roleId, id);
|
||||
await removeUserAccess(projectId, id);
|
||||
} else {
|
||||
await removeGroupFromRole(projectId, roleId, id);
|
||||
await removeGroupAccess(projectId, id);
|
||||
}
|
||||
refetchProjectAccess();
|
||||
setToastData({
|
||||
|
@ -9,9 +9,10 @@ interface ICreatePayload {
|
||||
defaultStickiness: string;
|
||||
}
|
||||
|
||||
interface IAccessesPayload {
|
||||
users: { id: number }[];
|
||||
groups: { id: number }[];
|
||||
interface IAccessPayload {
|
||||
roles: number[];
|
||||
groups: number[];
|
||||
users: number[];
|
||||
}
|
||||
|
||||
const useProjectApi = () => {
|
||||
@ -116,93 +117,59 @@ const useProjectApi = () => {
|
||||
|
||||
const addAccessToProject = async (
|
||||
projectId: string,
|
||||
roleId: number,
|
||||
accesses: IAccessesPayload
|
||||
payload: IAccessPayload
|
||||
) => {
|
||||
const path = `api/admin/projects/${projectId}/role/${roleId}/access`;
|
||||
const path = `api/admin/projects/${projectId}/access`;
|
||||
const req = createRequest(path, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(accesses),
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
try {
|
||||
const res = await makeRequest(req.caller, req.id);
|
||||
|
||||
return res;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
return await makeRequest(req.caller, req.id);
|
||||
};
|
||||
|
||||
const removeUserFromRole = async (
|
||||
projectId: string,
|
||||
roleId: number,
|
||||
userId: number
|
||||
) => {
|
||||
const path = `api/admin/projects/${projectId}/users/${userId}/roles/${roleId}`;
|
||||
const removeUserAccess = async (projectId: string, userId: number) => {
|
||||
const path = `api/admin/projects/${projectId}/users/${userId}/roles`;
|
||||
const req = createRequest(path, { method: 'DELETE' });
|
||||
|
||||
try {
|
||||
const res = await makeRequest(req.caller, req.id);
|
||||
|
||||
return res;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
return await makeRequest(req.caller, req.id);
|
||||
};
|
||||
|
||||
const removeGroupFromRole = async (
|
||||
projectId: string,
|
||||
roleId: number,
|
||||
groupId: number
|
||||
) => {
|
||||
const path = `api/admin/projects/${projectId}/groups/${groupId}/roles/${roleId}`;
|
||||
const removeGroupAccess = async (projectId: string, groupId: number) => {
|
||||
const path = `api/admin/projects/${projectId}/groups/${groupId}/roles`;
|
||||
const req = createRequest(path, { method: 'DELETE' });
|
||||
|
||||
try {
|
||||
const res = await makeRequest(req.caller, req.id);
|
||||
|
||||
return res;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
return await makeRequest(req.caller, req.id);
|
||||
};
|
||||
|
||||
const searchProjectUser = async (query: string): Promise<Response> => {
|
||||
const path = `api/admin/user-admin/search?q=${query}`;
|
||||
|
||||
const req = createRequest(path, { method: 'GET' });
|
||||
|
||||
try {
|
||||
const res = await makeRequest(req.caller, req.id);
|
||||
|
||||
return res;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const changeUserRole = (
|
||||
const setUserRoles = (
|
||||
projectId: string,
|
||||
roleId: number,
|
||||
roleIds: number[],
|
||||
userId: number
|
||||
) => {
|
||||
const path = `api/admin/projects/${projectId}/users/${userId}/roles/${roleId}`;
|
||||
const req = createRequest(path, { method: 'PUT' });
|
||||
const path = `api/admin/projects/${projectId}/users/${userId}/roles`;
|
||||
const req = createRequest(path, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ roles: roleIds }),
|
||||
});
|
||||
|
||||
return makeRequest(req.caller, req.id);
|
||||
};
|
||||
|
||||
const changeGroupRole = (
|
||||
const setGroupRoles = (
|
||||
projectId: string,
|
||||
roleId: number,
|
||||
roleIds: number[],
|
||||
groupId: number
|
||||
) => {
|
||||
const path = `api/admin/projects/${projectId}/groups/${groupId}/roles/${roleId}`;
|
||||
const req = createRequest(path, { method: 'PUT' });
|
||||
const path = `api/admin/projects/${projectId}/groups/${groupId}/roles`;
|
||||
const req = createRequest(path, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ roles: roleIds }),
|
||||
});
|
||||
|
||||
return makeRequest(req.caller, req.id);
|
||||
};
|
||||
|
||||
const archiveFeatures = async (projectId: string, featureIds: string[]) => {
|
||||
const path = `api/admin/projects/${projectId}/archive`;
|
||||
const req = createRequest(path, {
|
||||
@ -283,16 +250,15 @@ const useProjectApi = () => {
|
||||
addEnvironmentToProject,
|
||||
removeEnvironmentFromProject,
|
||||
addAccessToProject,
|
||||
removeUserFromRole,
|
||||
removeGroupFromRole,
|
||||
changeUserRole,
|
||||
changeGroupRole,
|
||||
removeUserAccess,
|
||||
removeGroupAccess,
|
||||
setUserRoles,
|
||||
setGroupRoles,
|
||||
archiveFeatures,
|
||||
reviveFeatures,
|
||||
staleFeatures,
|
||||
deleteFeature,
|
||||
deleteFeatures,
|
||||
searchProjectUser,
|
||||
updateDefaultStrategy,
|
||||
errors,
|
||||
loading,
|
||||
|
@ -20,10 +20,12 @@ export interface IProjectAccess {
|
||||
}
|
||||
|
||||
export interface IProjectAccessUser extends IUser {
|
||||
roles: number[];
|
||||
roleId: number;
|
||||
}
|
||||
|
||||
export interface IProjectAccessGroup extends IGroup {
|
||||
roles: number[];
|
||||
roleId: number;
|
||||
}
|
||||
|
||||
|
@ -57,6 +57,7 @@ export interface IFlags {
|
||||
lastSeenByEnvironment?: boolean;
|
||||
newApplicationList?: boolean;
|
||||
integrationsRework?: boolean;
|
||||
multipleRoles?: boolean;
|
||||
}
|
||||
|
||||
export interface IVersionInfo {
|
||||
|
@ -92,6 +92,7 @@ exports[`should create default config 1`] = `
|
||||
},
|
||||
},
|
||||
"migrationLock": true,
|
||||
"multipleRoles": false,
|
||||
"personalAccessTokensKillSwitch": false,
|
||||
"proPlanAutoCharge": false,
|
||||
"responseTimeWithAppNameKillSwitch": false,
|
||||
@ -127,6 +128,7 @@ exports[`should create default config 1`] = `
|
||||
},
|
||||
},
|
||||
"migrationLock": true,
|
||||
"multipleRoles": false,
|
||||
"personalAccessTokensKillSwitch": false,
|
||||
"proPlanAutoCharge": false,
|
||||
"responseTimeWithAppNameKillSwitch": false,
|
||||
|
@ -10,11 +10,13 @@ import {
|
||||
IRoleWithProject,
|
||||
IUserPermission,
|
||||
IUserRole,
|
||||
IUserWithProjectRoles,
|
||||
} from '../types/stores/access-store';
|
||||
import { IPermission } from '../types/model';
|
||||
import NotFoundError from '../error/notfound-error';
|
||||
import {
|
||||
ENVIRONMENT_PERMISSION_TYPE,
|
||||
PROJECT_ROLE_TYPES,
|
||||
ROOT_PERMISSION_TYPE,
|
||||
} from '../util/constants';
|
||||
import { Db } from './db';
|
||||
@ -144,7 +146,6 @@ 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')
|
||||
.whereNull('g.root_role_id')
|
||||
.andWhere('gu.user_id', '=', userId);
|
||||
});
|
||||
|
||||
@ -167,7 +168,6 @@ export class AccessStore implements IAccessStore {
|
||||
.whereNotNull('g.root_role_id')
|
||||
.andWhere('gu.user_id', '=', userId);
|
||||
});
|
||||
|
||||
const rows = await userPermissionQuery;
|
||||
stopTimer();
|
||||
return rows.map(this.mapUserPermission);
|
||||
@ -281,6 +281,34 @@ export class AccessStore implements IAccessStore {
|
||||
}));
|
||||
}
|
||||
|
||||
async getProjectUsers(
|
||||
projectId?: string,
|
||||
): Promise<IUserWithProjectRoles[]> {
|
||||
const rows = await this.db
|
||||
.select(['user_id', 'ru.created_at', 'ru.role_id'])
|
||||
.from<IRole>(`${T.ROLE_USER} AS ru`)
|
||||
.join(`${T.ROLES} as r`, 'ru.role_id', 'id')
|
||||
.whereIn('r.type', PROJECT_ROLE_TYPES)
|
||||
.andWhere('ru.project', projectId);
|
||||
|
||||
return rows.reduce((acc, row) => {
|
||||
const existingUser = acc.find((user) => user.id === row.user_id);
|
||||
|
||||
if (existingUser) {
|
||||
existingUser.roles.push(row.role_id);
|
||||
} else {
|
||||
acc.push({
|
||||
id: row.user_id,
|
||||
addedAt: row.created_at,
|
||||
roleId: row.role_id,
|
||||
roles: [row.role_id],
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
async getRolesForUserId(userId: number): Promise<IRoleWithProject[]> {
|
||||
return this.db
|
||||
.select(['id', 'name', 'type', 'project', 'description'])
|
||||
@ -310,13 +338,13 @@ export class AccessStore implements IAccessStore {
|
||||
): Promise<IProjectRoleUsage[]> {
|
||||
const query = await this.db.raw(
|
||||
`
|
||||
SELECT
|
||||
SELECT
|
||||
uq.project,
|
||||
sum(uq.user_count) AS user_count,
|
||||
sum(uq.svc_account_count) AS svc_account_count,
|
||||
sum(uq.group_count) AS group_count
|
||||
FROM (
|
||||
SELECT
|
||||
SELECT
|
||||
project,
|
||||
0 AS user_count,
|
||||
0 AS svc_account_count,
|
||||
@ -341,12 +369,6 @@ export class AccessStore implements IAccessStore {
|
||||
[roleId, roleId],
|
||||
);
|
||||
|
||||
/*
|
||||
const rows2 = await this.db(T.ROLE_USER)
|
||||
.select('project', this.db.raw('count(project) as user_count'))
|
||||
.where('role_id', roleId)
|
||||
.groupBy('project');
|
||||
*/
|
||||
return query.rows.map((r) => {
|
||||
return {
|
||||
project: r.project,
|
||||
@ -446,7 +468,7 @@ export class AccessStore implements IAccessStore {
|
||||
.update('role_id', roleId);
|
||||
}
|
||||
|
||||
async addAccessToProject(
|
||||
async addRoleAccessToProject(
|
||||
users: IAccessInfo[],
|
||||
groups: IAccessInfo[],
|
||||
projectId: string,
|
||||
@ -486,6 +508,181 @@ export class AccessStore implements IAccessStore {
|
||||
});
|
||||
}
|
||||
|
||||
async addAccessToProject(
|
||||
roles: number[],
|
||||
groups: number[],
|
||||
users: number[],
|
||||
projectId: string,
|
||||
createdBy: string,
|
||||
): Promise<void> {
|
||||
const validatedProjectRoleIds = await this.db(T.ROLES)
|
||||
.select('id')
|
||||
.whereIn('id', roles)
|
||||
.whereIn('type', PROJECT_ROLE_TYPES)
|
||||
.pluck('id');
|
||||
|
||||
const groupRows = groups.flatMap((group) =>
|
||||
validatedProjectRoleIds.map((role) => ({
|
||||
group_id: group,
|
||||
project: projectId,
|
||||
role_id: role,
|
||||
created_by: createdBy,
|
||||
})),
|
||||
);
|
||||
|
||||
const userRows = users.flatMap((user) =>
|
||||
validatedProjectRoleIds.map((role) => ({
|
||||
user_id: user,
|
||||
project: projectId,
|
||||
role_id: role,
|
||||
})),
|
||||
);
|
||||
|
||||
await this.db.transaction(async (tx) => {
|
||||
if (groupRows.length > 0) {
|
||||
await tx(T.GROUP_ROLE)
|
||||
.insert(groupRows)
|
||||
.onConflict(['project', 'role_id', 'group_id'])
|
||||
.merge();
|
||||
}
|
||||
if (userRows.length > 0) {
|
||||
await tx(T.ROLE_USER)
|
||||
.insert(userRows)
|
||||
.onConflict(['project', 'role_id', 'user_id'])
|
||||
.merge();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async setProjectRolesForUser(
|
||||
projectId: string,
|
||||
userId: number,
|
||||
roles: number[],
|
||||
): Promise<void> {
|
||||
const projectRoleIds = await this.db(T.ROLES)
|
||||
.select('id')
|
||||
.whereIn('type', PROJECT_ROLE_TYPES)
|
||||
.pluck('id');
|
||||
|
||||
const projectRoleIdsSet = new Set(projectRoleIds);
|
||||
|
||||
const userRows = roles
|
||||
.filter((role) => projectRoleIdsSet.has(role))
|
||||
.map((role) => ({
|
||||
user_id: userId,
|
||||
project: projectId,
|
||||
role_id: role,
|
||||
}));
|
||||
|
||||
await this.db.transaction(async (tx) => {
|
||||
await tx(T.ROLE_USER)
|
||||
.where('project', projectId)
|
||||
.andWhere('user_id', userId)
|
||||
.whereIn('role_id', projectRoleIds)
|
||||
.delete();
|
||||
|
||||
if (userRows.length > 0) {
|
||||
await tx(T.ROLE_USER)
|
||||
.insert(userRows)
|
||||
.onConflict(['project', 'role_id', 'user_id'])
|
||||
.ignore();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getProjectRolesForUser(
|
||||
projectId: string,
|
||||
userId: number,
|
||||
): Promise<number[]> {
|
||||
const rows = await this.db(`${T.ROLE_USER} as ru`)
|
||||
.join(`${T.ROLES} as r`, 'ru.role_id', 'r.id')
|
||||
.select('ru.role_id')
|
||||
.where('ru.project', projectId)
|
||||
.whereIn('r.type', PROJECT_ROLE_TYPES)
|
||||
.andWhere('ru.user_id', userId);
|
||||
return rows.map((r) => r.role_id as number);
|
||||
}
|
||||
|
||||
async setProjectRolesForGroup(
|
||||
projectId: string,
|
||||
groupId: number,
|
||||
roles: number[],
|
||||
createdBy: string,
|
||||
): Promise<void> {
|
||||
const projectRoleIds = await this.db(T.ROLES)
|
||||
.select('id')
|
||||
.whereIn('type', PROJECT_ROLE_TYPES)
|
||||
.pluck('id');
|
||||
|
||||
const projectRoleIdsSet = new Set(projectRoleIds);
|
||||
|
||||
const groupRows = roles
|
||||
.filter((role) => projectRoleIdsSet.has(role))
|
||||
.map((role) => ({
|
||||
group_id: groupId,
|
||||
project: projectId,
|
||||
role_id: role,
|
||||
created_by: createdBy,
|
||||
}));
|
||||
|
||||
await this.db.transaction(async (tx) => {
|
||||
await tx(T.GROUP_ROLE)
|
||||
.where('project', projectId)
|
||||
.andWhere('group_id', groupId)
|
||||
.whereIn('role_id', projectRoleIds)
|
||||
.delete();
|
||||
if (groupRows.length > 0) {
|
||||
await tx(T.GROUP_ROLE)
|
||||
.insert(groupRows)
|
||||
.onConflict(['project', 'role_id', 'group_id'])
|
||||
.ignore();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getProjectRolesForGroup(
|
||||
projectId: string,
|
||||
groupId: number,
|
||||
): Promise<number[]> {
|
||||
const rows = await this.db(`${T.GROUP_ROLE} as gr`)
|
||||
.join(`${T.ROLES} as r`, 'gr.role_id', 'r.id')
|
||||
.select('gr.role_id')
|
||||
.where('gr.project', projectId)
|
||||
.whereIn('r.type', PROJECT_ROLE_TYPES)
|
||||
.andWhere('gr.group_id', groupId);
|
||||
return rows.map((row) => row.role_id as number);
|
||||
}
|
||||
|
||||
async removeUserAccess(projectId: string, userId: number): Promise<void> {
|
||||
return this.db(T.ROLE_USER)
|
||||
.where({
|
||||
user_id: userId,
|
||||
project: projectId,
|
||||
})
|
||||
.whereIn(
|
||||
'role_id',
|
||||
this.db(T.ROLES)
|
||||
.select('id as role_id')
|
||||
.whereIn('type', PROJECT_ROLE_TYPES),
|
||||
)
|
||||
.delete();
|
||||
}
|
||||
|
||||
async removeGroupAccess(projectId: string, groupId: number): Promise<void> {
|
||||
return this.db(T.GROUP_ROLE)
|
||||
.where({
|
||||
group_id: groupId,
|
||||
project: projectId,
|
||||
})
|
||||
.whereIn(
|
||||
'role_id',
|
||||
this.db(T.ROLES)
|
||||
.select('id as role_id')
|
||||
.whereIn('type', PROJECT_ROLE_TYPES),
|
||||
)
|
||||
.delete();
|
||||
}
|
||||
|
||||
async removeRolesOfTypeForUser(
|
||||
userId: number,
|
||||
roleTypes: string[],
|
||||
|
@ -10,6 +10,8 @@ import Group, {
|
||||
} from '../types/group';
|
||||
import { Db } from './db';
|
||||
import { BadDataError, FOREIGN_KEY_VIOLATION } from '../error';
|
||||
import { IGroupWithProjectRoles } from '../types/stores/access-store';
|
||||
import { PROJECT_ROLE_TYPES } from '../util';
|
||||
|
||||
const T = {
|
||||
GROUPS: 'groups',
|
||||
@ -116,6 +118,36 @@ export default class GroupStore implements IGroupStore {
|
||||
});
|
||||
}
|
||||
|
||||
async getProjectGroups(
|
||||
projectId: string,
|
||||
): Promise<IGroupWithProjectRoles[]> {
|
||||
const rows = await this.db
|
||||
.select(['gr.group_id', 'gr.created_at', 'gr.role_id'])
|
||||
.from(`${T.GROUP_ROLE} AS gr`)
|
||||
.join(`${T.ROLES} as r`, 'gr.role_id', 'r.id')
|
||||
.whereIn('r.type', PROJECT_ROLE_TYPES)
|
||||
.andWhere('project', projectId);
|
||||
|
||||
return rows.reduce((acc, row) => {
|
||||
const existingGroup = acc.find(
|
||||
(group) => group.id === row.group_id,
|
||||
);
|
||||
|
||||
if (existingGroup) {
|
||||
existingGroup.roles.push(row.role_id);
|
||||
} else {
|
||||
acc.push({
|
||||
id: row.group_id,
|
||||
addedAt: row.created_at,
|
||||
roleId: row.role_id,
|
||||
roles: [row.role_id],
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
async getGroupProjects(groupIds: number[]): Promise<IGroupProject[]> {
|
||||
const rows = await this.db
|
||||
.select('group_id', 'project')
|
||||
|
@ -1,14 +1,17 @@
|
||||
import * as permissions from '../types/permissions';
|
||||
import User, { IProjectUser, IUser } from '../types/user';
|
||||
import User, { IUser } from '../types/user';
|
||||
import {
|
||||
IAccessInfo,
|
||||
IAccessStore,
|
||||
IGroupWithProjectRoles,
|
||||
IProjectRoleUsage,
|
||||
IRole,
|
||||
IRoleDescriptor,
|
||||
IRoleWithPermissions,
|
||||
IRoleWithProject,
|
||||
IUserPermission,
|
||||
IUserRole,
|
||||
IUserWithProjectRoles,
|
||||
} from '../types/stores/access-store';
|
||||
import { Logger } from '../logger';
|
||||
import { IAccountStore, IGroupStore, IUnleashStores } from '../types/stores';
|
||||
@ -35,7 +38,7 @@ import {
|
||||
import { DEFAULT_PROJECT } from '../types/project';
|
||||
import InvalidOperationError from '../error/invalid-operation-error';
|
||||
import BadDataError from '../error/bad-data-error';
|
||||
import { IGroup, IGroupModelWithProjectRole } from '../types/group';
|
||||
import { IGroup } from '../types/group';
|
||||
import { GroupService } from './group-service';
|
||||
import { IFlagResolver, IUnleashConfig } from 'lib/types';
|
||||
|
||||
@ -70,6 +73,12 @@ interface IRoleUpdate {
|
||||
permissions?: IPermission[];
|
||||
}
|
||||
|
||||
export interface AccessWithRoles {
|
||||
roles: IRoleDescriptor[];
|
||||
groups: IGroupWithProjectRoles[];
|
||||
users: IUserWithProjectRoles[];
|
||||
}
|
||||
|
||||
const isProjectPermission = (permission) => PROJECT_ADMIN.includes(permission);
|
||||
|
||||
export class AccessService {
|
||||
@ -235,14 +244,14 @@ export class AccessService {
|
||||
return this.store.addGroupToRole(groupId, roleId, createdBy, projectId);
|
||||
}
|
||||
|
||||
async addAccessToProject(
|
||||
async addRoleAccessToProject(
|
||||
users: IAccessInfo[],
|
||||
groups: IAccessInfo[],
|
||||
projectId: string,
|
||||
roleId: number,
|
||||
createdBy: string,
|
||||
): Promise<void> {
|
||||
return this.store.addAccessToProject(
|
||||
return this.store.addRoleAccessToProject(
|
||||
users,
|
||||
groups,
|
||||
projectId,
|
||||
@ -251,10 +260,70 @@ export class AccessService {
|
||||
);
|
||||
}
|
||||
|
||||
async addAccessToProject(
|
||||
roles: number[],
|
||||
groups: number[],
|
||||
users: number[],
|
||||
projectId: string,
|
||||
createdBy: string,
|
||||
): Promise<void> {
|
||||
return this.store.addAccessToProject(
|
||||
roles,
|
||||
groups,
|
||||
users,
|
||||
projectId,
|
||||
createdBy,
|
||||
);
|
||||
}
|
||||
|
||||
async setProjectRolesForUser(
|
||||
projectId: string,
|
||||
userId: number,
|
||||
roles: number[],
|
||||
): Promise<void> {
|
||||
await this.store.setProjectRolesForUser(projectId, userId, roles);
|
||||
}
|
||||
|
||||
async getProjectRolesForUser(
|
||||
projectId: string,
|
||||
userId: number,
|
||||
): Promise<number[]> {
|
||||
return this.store.getProjectRolesForUser(projectId, userId);
|
||||
}
|
||||
|
||||
async setProjectRolesForGroup(
|
||||
projectId: string,
|
||||
groupId: number,
|
||||
roles: number[],
|
||||
createdBy: string,
|
||||
): Promise<void> {
|
||||
await this.store.setProjectRolesForGroup(
|
||||
projectId,
|
||||
groupId,
|
||||
roles,
|
||||
createdBy,
|
||||
);
|
||||
}
|
||||
|
||||
async getProjectRolesForGroup(
|
||||
projectId: string,
|
||||
groupId: number,
|
||||
): Promise<number[]> {
|
||||
return this.store.getProjectRolesForGroup(projectId, groupId);
|
||||
}
|
||||
|
||||
async getRoleByName(roleName: string): Promise<IRole> {
|
||||
return this.roleStore.getRoleByName(roleName);
|
||||
}
|
||||
|
||||
async removeUserAccess(projectId: string, userId: number): Promise<void> {
|
||||
await this.store.removeUserAccess(projectId, userId);
|
||||
}
|
||||
|
||||
async removeGroupAccess(projectId: string, groupId: number): Promise<void> {
|
||||
await this.store.removeGroupAccess(projectId, groupId);
|
||||
}
|
||||
|
||||
async setUserRootRole(
|
||||
userId: number,
|
||||
role: number | RoleName,
|
||||
@ -417,7 +486,7 @@ export class AccessService {
|
||||
async getProjectUsersForRole(
|
||||
roleId: number,
|
||||
projectId?: string,
|
||||
): Promise<IProjectUser[]> {
|
||||
): Promise<IUserWithRole[]> {
|
||||
const userRoleList = await this.store.getProjectUsersForRole(
|
||||
roleId,
|
||||
projectId,
|
||||
@ -430,28 +499,44 @@ export class AccessService {
|
||||
return {
|
||||
...user,
|
||||
addedAt: role.addedAt!,
|
||||
roleId,
|
||||
};
|
||||
});
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
async getProjectRoleAccess(
|
||||
projectId: string,
|
||||
): Promise<[IRole[], IUserWithRole[], IGroupModelWithProjectRole[]]> {
|
||||
async getProjectUsers(projectId: string): Promise<IUserWithProjectRoles[]> {
|
||||
const projectUsers = await this.store.getProjectUsers(projectId);
|
||||
|
||||
if (projectUsers.length > 0) {
|
||||
const users = await this.accountStore.getAllWithId(
|
||||
projectUsers.map((u) => u.id),
|
||||
);
|
||||
return users.flatMap((user) => {
|
||||
return projectUsers
|
||||
.filter((u) => u.id === user.id)
|
||||
.map((groupUser) => ({
|
||||
...user,
|
||||
...groupUser,
|
||||
}));
|
||||
});
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
async getProjectRoleAccess(projectId: string): Promise<AccessWithRoles> {
|
||||
const roles = await this.roleStore.getProjectRoles();
|
||||
|
||||
const users = await Promise.all(
|
||||
roles.map(async (role) => {
|
||||
const projectUsers = await this.getProjectUsersForRole(
|
||||
role.id,
|
||||
projectId,
|
||||
);
|
||||
return projectUsers.map((u) => ({ ...u, roleId: role.id }));
|
||||
}),
|
||||
);
|
||||
const users = await this.getProjectUsers(projectId);
|
||||
|
||||
const groups = await this.groupService.getProjectGroups(projectId);
|
||||
return [roles, users.flat(), groups];
|
||||
|
||||
return {
|
||||
roles,
|
||||
groups,
|
||||
users,
|
||||
};
|
||||
}
|
||||
|
||||
async getProjectRoleUsage(roleId: number): Promise<IProjectRoleUsage[]> {
|
||||
|
@ -147,29 +147,27 @@ export class GroupService {
|
||||
}
|
||||
|
||||
async getProjectGroups(
|
||||
projectId?: string,
|
||||
projectId: string,
|
||||
): Promise<IGroupModelWithProjectRole[]> {
|
||||
const groupRoles = await this.groupStore.getProjectGroupRoles(
|
||||
projectId,
|
||||
);
|
||||
if (groupRoles.length > 0) {
|
||||
const projectGroups = await this.groupStore.getProjectGroups(projectId);
|
||||
|
||||
if (projectGroups.length > 0) {
|
||||
const groups = await this.groupStore.getAllWithId(
|
||||
groupRoles.map((a) => a.groupId),
|
||||
projectGroups.map((g) => g.id!),
|
||||
);
|
||||
const groupUsers = await this.groupStore.getAllUsersByGroups(
|
||||
groups.map((g) => g.id),
|
||||
groups.map((g) => g.id!),
|
||||
);
|
||||
|
||||
const users = await this.accountStore.getAllWithId(
|
||||
groupUsers.map((u) => u.userId),
|
||||
);
|
||||
return groups.map((group) => {
|
||||
const groupRole = groupRoles.find((g) => g.groupId == group.id);
|
||||
return {
|
||||
...this.mapGroupWithUsers(group, groupUsers, users),
|
||||
roleId: groupRole.roleId,
|
||||
addedAt: groupRole.createdAt,
|
||||
};
|
||||
return groups.flatMap((group) => {
|
||||
return projectGroups
|
||||
.filter((gr) => gr.id === group.id)
|
||||
.map((groupRole) => ({
|
||||
...this.mapGroupWithUsers(group, groupUsers, users),
|
||||
...groupRole,
|
||||
}));
|
||||
});
|
||||
}
|
||||
return [];
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { subDays } from 'date-fns';
|
||||
import { ValidationError } from 'joi';
|
||||
import User, { IUser } from '../types/user';
|
||||
import { AccessService } from './access-service';
|
||||
import { AccessService, AccessWithRoles } from './access-service';
|
||||
import NameExistsError from '../error/name-exists-error';
|
||||
import InvalidOperationError from '../error/invalid-operation-error';
|
||||
import { nameType } from '../routes/util';
|
||||
@ -21,7 +21,6 @@ import {
|
||||
IProjectWithCount,
|
||||
IUnleashConfig,
|
||||
IUnleashStores,
|
||||
IUserWithRole,
|
||||
MOVE_FEATURE_TOGGLE,
|
||||
PROJECT_CREATED,
|
||||
PROJECT_DELETED,
|
||||
@ -35,7 +34,10 @@ import {
|
||||
RoleName,
|
||||
IFlagResolver,
|
||||
ProjectAccessAddedEvent,
|
||||
ProjectAccessUserRolesUpdated,
|
||||
ProjectAccessGroupRolesUpdated,
|
||||
IProjectRoleUsage,
|
||||
ProjectAccessUserRolesDeleted,
|
||||
} from '../types';
|
||||
import { IProjectQuery, IProjectStore } from '../types/stores/project-store';
|
||||
import {
|
||||
@ -48,7 +50,7 @@ import { IFeatureTagStore } from 'lib/types/stores/feature-tag-store';
|
||||
import ProjectWithoutOwnerError from '../error/project-without-owner-error';
|
||||
import { arraysHaveSameItems } from '../util';
|
||||
import { GroupService } from './group-service';
|
||||
import { IGroupModelWithProjectRole, IGroupRole } from 'lib/types/group';
|
||||
import { IGroupRole } from 'lib/types/group';
|
||||
import { FavoritesService } from './favorites-service';
|
||||
import { calculateAverageTimeToProd } from '../features/feature-toggle/time-to-production/time-to-production';
|
||||
import { IProjectStatsStore } from 'lib/types/stores/project-stats-store-type';
|
||||
@ -57,12 +59,6 @@ import { PermissionError } from '../error';
|
||||
|
||||
const getCreatedBy = (user: IUser) => user.email || user.username || 'unknown';
|
||||
|
||||
export interface AccessWithRoles {
|
||||
users: IUserWithRole[];
|
||||
roles: IRoleDescriptor[];
|
||||
groups: IGroupModelWithProjectRole[];
|
||||
}
|
||||
|
||||
type Days = number;
|
||||
type Count = number;
|
||||
|
||||
@ -335,14 +331,7 @@ export default class ProjectService {
|
||||
|
||||
// RBAC methods
|
||||
async getAccessToProject(projectId: string): Promise<AccessWithRoles> {
|
||||
const [roles, users, groups] =
|
||||
await this.accessService.getProjectRoleAccess(projectId);
|
||||
|
||||
return {
|
||||
roles,
|
||||
users,
|
||||
groups,
|
||||
};
|
||||
return this.accessService.getProjectRoleAccess(projectId);
|
||||
}
|
||||
|
||||
// Deprecated: See addAccess instead.
|
||||
@ -352,7 +341,7 @@ export default class ProjectService {
|
||||
userId: number,
|
||||
createdBy: string,
|
||||
): Promise<void> {
|
||||
const [roles, users] = await this.accessService.getProjectRoleAccess(
|
||||
const { roles, users } = await this.accessService.getProjectRoleAccess(
|
||||
projectId,
|
||||
);
|
||||
const user = await this.accountStore.get(userId);
|
||||
@ -413,6 +402,54 @@ export default class ProjectService {
|
||||
);
|
||||
}
|
||||
|
||||
async removeUserAccess(
|
||||
projectId: string,
|
||||
userId: number,
|
||||
createdBy: string,
|
||||
): Promise<void> {
|
||||
const existingRoles = await this.accessService.getProjectRolesForUser(
|
||||
projectId,
|
||||
userId,
|
||||
);
|
||||
|
||||
await this.accessService.removeUserAccess(projectId, userId);
|
||||
|
||||
await this.eventStore.store(
|
||||
new ProjectAccessUserRolesDeleted({
|
||||
project: projectId,
|
||||
createdBy,
|
||||
preData: {
|
||||
roles: existingRoles,
|
||||
userId,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async removeGroupAccess(
|
||||
projectId: string,
|
||||
groupId: number,
|
||||
createdBy: string,
|
||||
): Promise<void> {
|
||||
const existingRoles = await this.accessService.getProjectRolesForGroup(
|
||||
projectId,
|
||||
groupId,
|
||||
);
|
||||
|
||||
await this.accessService.removeGroupAccess(projectId, groupId);
|
||||
|
||||
await this.eventStore.store(
|
||||
new ProjectAccessUserRolesDeleted({
|
||||
project: projectId,
|
||||
createdBy,
|
||||
preData: {
|
||||
roles: existingRoles,
|
||||
groupId,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async addGroup(
|
||||
projectId: string,
|
||||
roleId: number,
|
||||
@ -484,13 +521,13 @@ export default class ProjectService {
|
||||
);
|
||||
}
|
||||
|
||||
async addAccess(
|
||||
async addRoleAccess(
|
||||
projectId: string,
|
||||
roleId: number,
|
||||
usersAndGroups: IProjectAccessModel,
|
||||
createdBy: string,
|
||||
): Promise<void> {
|
||||
await this.accessService.addAccessToProject(
|
||||
await this.accessService.addRoleAccessToProject(
|
||||
usersAndGroups.users,
|
||||
usersAndGroups.groups,
|
||||
projectId,
|
||||
@ -511,6 +548,97 @@ export default class ProjectService {
|
||||
);
|
||||
}
|
||||
|
||||
async addAccess(
|
||||
projectId: string,
|
||||
roles: number[],
|
||||
groups: number[],
|
||||
users: number[],
|
||||
createdBy: string,
|
||||
): Promise<void> {
|
||||
await this.accessService.addAccessToProject(
|
||||
roles,
|
||||
groups,
|
||||
users,
|
||||
projectId,
|
||||
createdBy,
|
||||
);
|
||||
|
||||
await this.eventStore.store(
|
||||
new ProjectAccessAddedEvent({
|
||||
project: projectId,
|
||||
createdBy,
|
||||
data: {
|
||||
roles,
|
||||
groups,
|
||||
users,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async setRolesForUser(
|
||||
projectId: string,
|
||||
userId: number,
|
||||
roles: number[],
|
||||
createdByUserName: string,
|
||||
): Promise<void> {
|
||||
const existingRoles = await this.accessService.getProjectRolesForUser(
|
||||
projectId,
|
||||
userId,
|
||||
);
|
||||
await this.accessService.setProjectRolesForUser(
|
||||
projectId,
|
||||
userId,
|
||||
roles,
|
||||
);
|
||||
await this.eventStore.store(
|
||||
new ProjectAccessUserRolesUpdated({
|
||||
project: projectId,
|
||||
createdBy: createdByUserName,
|
||||
data: {
|
||||
roles,
|
||||
userId,
|
||||
},
|
||||
preData: {
|
||||
roles: existingRoles,
|
||||
userId,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async setRolesForGroup(
|
||||
projectId: string,
|
||||
groupId: number,
|
||||
roles: number[],
|
||||
createdBy: string,
|
||||
): Promise<void> {
|
||||
const existingRoles = await this.accessService.getProjectRolesForGroup(
|
||||
projectId,
|
||||
groupId,
|
||||
);
|
||||
await this.accessService.setProjectRolesForGroup(
|
||||
projectId,
|
||||
groupId,
|
||||
roles,
|
||||
createdBy,
|
||||
);
|
||||
await this.eventStore.store(
|
||||
new ProjectAccessGroupRolesUpdated({
|
||||
project: projectId,
|
||||
createdBy,
|
||||
data: {
|
||||
roles,
|
||||
groupId,
|
||||
},
|
||||
preData: {
|
||||
roles: existingRoles,
|
||||
groupId,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async findProjectGroupRole(
|
||||
projectId: string,
|
||||
roleId: number,
|
||||
@ -670,7 +798,7 @@ export default class ProjectService {
|
||||
async getProjectUsers(
|
||||
projectId: string,
|
||||
): Promise<Array<Pick<IUser, 'id' | 'email' | 'username'>>> {
|
||||
const [, users, groups] = await this.accessService.getProjectRoleAccess(
|
||||
const { groups, users } = await this.accessService.getProjectRoleAccess(
|
||||
projectId,
|
||||
);
|
||||
const actualUsers = users.map((user) => ({
|
||||
|
@ -43,6 +43,19 @@ export const CONTEXT_FIELD_CREATED = 'context-field-created' as const;
|
||||
export const CONTEXT_FIELD_UPDATED = 'context-field-updated' as const;
|
||||
export const CONTEXT_FIELD_DELETED = 'context-field-deleted' as const;
|
||||
export const PROJECT_ACCESS_ADDED = 'project-access-added' as const;
|
||||
|
||||
export const PROJECT_ACCESS_USER_ROLES_UPDATED =
|
||||
'project-access-user-roles-updated';
|
||||
|
||||
export const PROJECT_ACCESS_GROUP_ROLES_UPDATED =
|
||||
'project-access-group-roles-updated';
|
||||
|
||||
export const PROJECT_ACCESS_USER_ROLES_DELETED =
|
||||
'project-access-user-roles-deleted';
|
||||
|
||||
export const PROJECT_ACCESS_GROUP_ROLES_DELETED =
|
||||
'project-access-group-roles-deleted';
|
||||
|
||||
export const PROJECT_CREATED = 'project-created' as const;
|
||||
export const PROJECT_UPDATED = 'project-updated' as const;
|
||||
export const PROJECT_DELETED = 'project-deleted' as const;
|
||||
@ -162,6 +175,10 @@ export const IEventTypes = [
|
||||
CONTEXT_FIELD_UPDATED,
|
||||
CONTEXT_FIELD_DELETED,
|
||||
PROJECT_ACCESS_ADDED,
|
||||
PROJECT_ACCESS_USER_ROLES_UPDATED,
|
||||
PROJECT_ACCESS_GROUP_ROLES_UPDATED,
|
||||
PROJECT_ACCESS_USER_ROLES_DELETED,
|
||||
PROJECT_ACCESS_GROUP_ROLES_DELETED,
|
||||
PROJECT_CREATED,
|
||||
PROJECT_UPDATED,
|
||||
PROJECT_DELETED,
|
||||
@ -817,6 +834,100 @@ export class ProjectAccessAddedEvent extends BaseEvent {
|
||||
}
|
||||
}
|
||||
|
||||
export class ProjectAccessUserRolesUpdated extends BaseEvent {
|
||||
readonly project: string;
|
||||
|
||||
readonly data: any;
|
||||
|
||||
readonly preData: any;
|
||||
|
||||
/**
|
||||
* @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization
|
||||
*/
|
||||
constructor(p: {
|
||||
project: string;
|
||||
createdBy: string | IUser;
|
||||
data: any;
|
||||
preData: any;
|
||||
}) {
|
||||
super(PROJECT_ACCESS_USER_ROLES_UPDATED, p.createdBy);
|
||||
const { project, data, preData } = p;
|
||||
this.project = project;
|
||||
this.data = data;
|
||||
this.preData = preData;
|
||||
}
|
||||
}
|
||||
|
||||
export class ProjectAccessGroupRolesUpdated extends BaseEvent {
|
||||
readonly project: string;
|
||||
|
||||
readonly data: any;
|
||||
|
||||
readonly preData: any;
|
||||
|
||||
/**
|
||||
* @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization
|
||||
*/
|
||||
constructor(p: {
|
||||
project: string;
|
||||
createdBy: string | IUser;
|
||||
data: any;
|
||||
preData: any;
|
||||
}) {
|
||||
super(PROJECT_ACCESS_GROUP_ROLES_UPDATED, p.createdBy);
|
||||
const { project, data, preData } = p;
|
||||
this.project = project;
|
||||
this.data = data;
|
||||
this.preData = preData;
|
||||
}
|
||||
}
|
||||
|
||||
export class ProjectAccessUserRolesDeleted extends BaseEvent {
|
||||
readonly project: string;
|
||||
|
||||
readonly data: null;
|
||||
|
||||
readonly preData: any;
|
||||
|
||||
/**
|
||||
* @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization
|
||||
*/
|
||||
constructor(p: {
|
||||
project: string;
|
||||
createdBy: string | IUser;
|
||||
preData: any;
|
||||
}) {
|
||||
super(PROJECT_ACCESS_USER_ROLES_DELETED, p.createdBy);
|
||||
const { project, preData } = p;
|
||||
this.project = project;
|
||||
this.data = null;
|
||||
this.preData = preData;
|
||||
}
|
||||
}
|
||||
|
||||
export class ProjectAccessGroupRolesDeleted extends BaseEvent {
|
||||
readonly project: string;
|
||||
|
||||
readonly data: null;
|
||||
|
||||
readonly preData: any;
|
||||
|
||||
/**
|
||||
* @param createdBy accepts a string for backward compatibility. Prefer using IUser for standardization
|
||||
*/
|
||||
constructor(p: {
|
||||
project: string;
|
||||
createdBy: string | IUser;
|
||||
preData: any;
|
||||
}) {
|
||||
super(PROJECT_ACCESS_GROUP_ROLES_DELETED, p.createdBy);
|
||||
const { project, preData } = p;
|
||||
this.project = project;
|
||||
this.data = null;
|
||||
this.preData = preData;
|
||||
}
|
||||
}
|
||||
|
||||
export class SettingCreatedEvent extends BaseEvent {
|
||||
readonly data: any;
|
||||
|
||||
|
@ -28,7 +28,8 @@ export type IFlagKey =
|
||||
| 'changeRequestReject'
|
||||
| 'customRootRolesKillSwitch'
|
||||
| 'newApplicationList'
|
||||
| 'integrationsRework';
|
||||
| 'integrationsRework'
|
||||
| 'multipleRoles';
|
||||
|
||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||
|
||||
@ -128,6 +129,10 @@ const flags: IFlags = {
|
||||
process.env.UNLEASH_INTEGRATIONS,
|
||||
false,
|
||||
),
|
||||
multipleRoles: parseEnvVarBoolean(
|
||||
process.env.UNLEASH_EXPERIMENTAL_MULTIPLE_ROLES,
|
||||
false,
|
||||
),
|
||||
};
|
||||
|
||||
export const defaultExperimentalOptions: IExperimentalOptions = {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { IPermission } from '../model';
|
||||
import { IGroupModelWithProjectRole } from '../group';
|
||||
import { IPermission, IUserWithRole } from '../model';
|
||||
import { Store } from './store';
|
||||
|
||||
export interface IUserPermission {
|
||||
@ -52,6 +53,17 @@ export interface IUserRole {
|
||||
addedAt?: Date;
|
||||
}
|
||||
|
||||
interface IEntityWithProjectRoles {
|
||||
roles?: number[];
|
||||
}
|
||||
|
||||
export interface IUserWithProjectRoles
|
||||
extends IUserWithRole,
|
||||
IEntityWithProjectRoles {}
|
||||
export interface IGroupWithProjectRoles
|
||||
extends IGroupModelWithProjectRole,
|
||||
IEntityWithProjectRoles {}
|
||||
|
||||
export interface IAccessStore extends Store<IRole, number> {
|
||||
getAvailablePermissions(): Promise<IPermission[]>;
|
||||
|
||||
@ -74,6 +86,8 @@ export interface IAccessStore extends Store<IRole, number> {
|
||||
projectId?: string,
|
||||
): Promise<IUserRole[]>;
|
||||
|
||||
getProjectUsers(projectId?: string): Promise<IUserWithProjectRoles[]>;
|
||||
|
||||
getUserIdsForRole(roleId: number, projectId?: string): Promise<number[]>;
|
||||
|
||||
getGroupIdsForRole(roleId: number, projectId?: string): Promise<number[]>;
|
||||
@ -95,7 +109,7 @@ export interface IAccessStore extends Store<IRole, number> {
|
||||
projectId?: string,
|
||||
): Promise<void>;
|
||||
|
||||
addAccessToProject(
|
||||
addRoleAccessToProject(
|
||||
users: IAccessInfo[],
|
||||
groups: IAccessInfo[],
|
||||
projectId: string,
|
||||
@ -103,6 +117,14 @@ export interface IAccessStore extends Store<IRole, number> {
|
||||
createdBy: string,
|
||||
): Promise<void>;
|
||||
|
||||
addAccessToProject(
|
||||
roles: number[],
|
||||
groups: number[],
|
||||
users: number[],
|
||||
projectId: string,
|
||||
createdBy: string,
|
||||
): Promise<void>;
|
||||
|
||||
removeUserFromRole(
|
||||
userId: number,
|
||||
roleId: number,
|
||||
@ -155,4 +177,26 @@ export interface IAccessStore extends Store<IRole, number> {
|
||||
sourceEnvironment: string,
|
||||
destinationEnvironment: string,
|
||||
): Promise<void>;
|
||||
|
||||
setProjectRolesForUser(
|
||||
projectId: string,
|
||||
userId: number,
|
||||
roles: number[],
|
||||
): Promise<void>;
|
||||
getProjectRolesForUser(
|
||||
projectId: string,
|
||||
userId: number,
|
||||
): Promise<number[]>;
|
||||
setProjectRolesForGroup(
|
||||
projectId: string,
|
||||
groupId: number,
|
||||
roles: number[],
|
||||
createdBy: string,
|
||||
): Promise<void>;
|
||||
getProjectRolesForGroup(
|
||||
projectId: string,
|
||||
groupId: number,
|
||||
): Promise<number[]>;
|
||||
removeUserAccess(projectId: string, userId: number): Promise<void>;
|
||||
removeGroupAccess(projectId: string, groupId: number): Promise<void>;
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import Group, {
|
||||
IGroupRole,
|
||||
IGroupUser,
|
||||
} from '../group';
|
||||
import { IGroupWithProjectRoles } from './access-store';
|
||||
|
||||
export interface IStoreGroup {
|
||||
name: string;
|
||||
@ -34,6 +35,8 @@ export interface IGroupStore extends Store<IGroup, number> {
|
||||
|
||||
getProjectGroupRoles(projectId: string): Promise<IGroupRole[]>;
|
||||
|
||||
getProjectGroups(projectId: string): Promise<IGroupWithProjectRoles[]>;
|
||||
|
||||
getAllWithId(ids: number[]): Promise<IGroup[]>;
|
||||
|
||||
updateGroupUsers(
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1109,11 +1109,9 @@ test('Should allow bulk update of group permissions', async () => {
|
||||
|
||||
await projectService.addAccess(
|
||||
project.id,
|
||||
createFeatureRole.id,
|
||||
{
|
||||
users: [{ id: user1.id }],
|
||||
groups: [{ id: group1.id }],
|
||||
},
|
||||
[createFeatureRole.id],
|
||||
[group1.id],
|
||||
[user1.id],
|
||||
'some-admin-user',
|
||||
);
|
||||
});
|
||||
@ -1142,11 +1140,9 @@ test('Should bulk update of only users', async () => {
|
||||
|
||||
await projectService.addAccess(
|
||||
project,
|
||||
createFeatureRole.id,
|
||||
{
|
||||
users: [{ id: user1.id }],
|
||||
groups: [],
|
||||
},
|
||||
[createFeatureRole.id],
|
||||
[],
|
||||
[user1.id],
|
||||
'some-admin-user',
|
||||
);
|
||||
});
|
||||
@ -1183,15 +1179,88 @@ test('Should allow bulk update of only groups', async () => {
|
||||
|
||||
await projectService.addAccess(
|
||||
project.id,
|
||||
createFeatureRole.id,
|
||||
{
|
||||
users: [],
|
||||
groups: [{ id: group1.id }],
|
||||
},
|
||||
[createFeatureRole.id],
|
||||
[group1.id],
|
||||
[],
|
||||
'some-admin-user',
|
||||
);
|
||||
});
|
||||
|
||||
test('Should allow permutations of roles, groups and users when adding a new access', async () => {
|
||||
const project = {
|
||||
id: 'project-access-permutations',
|
||||
name: 'project-access-permutations',
|
||||
mode: 'open' as const,
|
||||
defaultStickiness: 'clientId',
|
||||
};
|
||||
|
||||
await projectService.createProject(project, user.id);
|
||||
|
||||
const group1 = await stores.groupStore.create({
|
||||
name: 'permutation-group-1',
|
||||
description: '',
|
||||
});
|
||||
|
||||
const group2 = await stores.groupStore.create({
|
||||
name: 'permutation-group-2',
|
||||
description: '',
|
||||
});
|
||||
|
||||
const user1 = await stores.userStore.insert({
|
||||
name: 'permutation-user-1',
|
||||
email: 'pu1@getunleash.io',
|
||||
});
|
||||
|
||||
const user2 = await stores.userStore.insert({
|
||||
name: 'permutation-user-2',
|
||||
email: 'pu2@getunleash.io',
|
||||
});
|
||||
|
||||
const role1 = await accessService.createRole({
|
||||
name: 'permutation-role-1',
|
||||
description: '',
|
||||
permissions: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'CREATE_FEATURE',
|
||||
displayName: 'Create feature toggles',
|
||||
type: 'project',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const role2 = await accessService.createRole({
|
||||
name: 'permutation-role-2',
|
||||
description: '',
|
||||
permissions: [
|
||||
{
|
||||
id: 7,
|
||||
name: 'UPDATE_FEATURE',
|
||||
displayName: 'Update feature toggles',
|
||||
type: 'project',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await projectService.addAccess(
|
||||
project.id,
|
||||
[role1.id, role2.id],
|
||||
[group1.id, group2.id],
|
||||
[user1.id, user2.id],
|
||||
'some-admin-user',
|
||||
);
|
||||
|
||||
const { users, groups } = await projectService.getAccessToProject(
|
||||
project.id,
|
||||
);
|
||||
|
||||
expect(users).toHaveLength(2);
|
||||
expect(groups).toHaveLength(2);
|
||||
|
||||
expect(users[0].roles).toStrictEqual([role1.id, role2.id]);
|
||||
expect(groups[0].roles).toStrictEqual([role1.id, role2.id]);
|
||||
});
|
||||
|
||||
test('should only count active feature toggles for project', async () => {
|
||||
const project = {
|
||||
id: 'only-active',
|
||||
|
16
src/test/fixtures/access-service-mock.ts
vendored
16
src/test/fixtures/access-service-mock.ts
vendored
@ -1,15 +1,13 @@
|
||||
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
import { AccessService } from '../../lib/services/access-service';
|
||||
import {
|
||||
AccessService,
|
||||
AccessWithRoles,
|
||||
} from '../../lib/services/access-service';
|
||||
import User from '../../lib/types/user';
|
||||
import noLoggerProvider from './no-logger';
|
||||
import { IRole } from '../../lib/types/stores/access-store';
|
||||
import {
|
||||
IAvailablePermissions,
|
||||
IRoleData,
|
||||
IUserWithRole,
|
||||
} from '../../lib/types/model';
|
||||
import { IGroupModelWithProjectRole } from '../../lib/types/group';
|
||||
import { IAvailablePermissions } from '../../lib/types/model';
|
||||
|
||||
class AccessServiceMock extends AccessService {
|
||||
constructor() {
|
||||
@ -78,9 +76,7 @@ class AccessServiceMock extends AccessService {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getProjectRoleAccess(
|
||||
projectId: string,
|
||||
): Promise<[IRole[], IUserWithRole[], IGroupModelWithProjectRole[]]> {
|
||||
getProjectRoleAccess(projectId: string): Promise<AccessWithRoles> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
|
58
src/test/fixtures/fake-access-store.ts
vendored
58
src/test/fixtures/fake-access-store.ts
vendored
@ -7,6 +7,7 @@ import {
|
||||
IRoleWithProject,
|
||||
IUserPermission,
|
||||
IUserRole,
|
||||
IUserWithProjectRoles,
|
||||
} from '../../lib/types/stores/access-store';
|
||||
import { IPermission } from 'lib/types/model';
|
||||
import { IRoleStore } from 'lib/types';
|
||||
@ -27,7 +28,7 @@ class AccessStoreMock implements IAccessStore {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
addAccessToProject(
|
||||
addRoleAccessToProject(
|
||||
users: IAccessInfo[],
|
||||
groups: IAccessInfo[],
|
||||
projectId: string,
|
||||
@ -37,6 +38,16 @@ class AccessStoreMock implements IAccessStore {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
addAccessToProject(
|
||||
roles: number[],
|
||||
groups: number[],
|
||||
users: number[],
|
||||
projectId: string,
|
||||
createdBy: string,
|
||||
): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
updateGroupProjectRole(
|
||||
userId: number,
|
||||
roleId: number,
|
||||
@ -97,6 +108,10 @@ class AccessStoreMock implements IAccessStore {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getProjectUsers(projectId?: string): Promise<IUserWithProjectRoles[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getProjectRoles(): Promise<IRole[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
@ -198,7 +213,7 @@ class AccessStoreMock implements IAccessStore {
|
||||
}
|
||||
|
||||
get(key: number): Promise<IRole> {
|
||||
return Promise.resolve(undefined);
|
||||
throw new Error('Not implemented yet');
|
||||
}
|
||||
|
||||
getAll(): Promise<IRole[]> {
|
||||
@ -234,6 +249,45 @@ class AccessStoreMock implements IAccessStore {
|
||||
clearPublicSignupUserTokens(userId: number): Promise<void> {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
getProjectRolesForGroup(
|
||||
projectId: string,
|
||||
groupId: number,
|
||||
): Promise<number[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getProjectRolesForUser(
|
||||
projectId: string,
|
||||
userId: number,
|
||||
): Promise<number[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
setProjectRolesForGroup(
|
||||
projectId: string,
|
||||
groupId: number,
|
||||
roles: number[],
|
||||
createdBy: string,
|
||||
): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
setProjectRolesForUser(
|
||||
projectId: string,
|
||||
userId: number,
|
||||
roles: number[],
|
||||
): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
removeUserAccess(projectId: string, userId: number): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
removeGroupAccess(projectId: string, groupId: number): Promise<void> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AccessStoreMock;
|
||||
|
5
src/test/fixtures/fake-group-store.ts
vendored
5
src/test/fixtures/fake-group-store.ts
vendored
@ -7,6 +7,7 @@ import Group, {
|
||||
IGroupRole,
|
||||
IGroupUser,
|
||||
} from '../../lib/types/group';
|
||||
import { IGroupWithProjectRoles } from '../../lib/types/stores/access-store';
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
export default class FakeGroupStore implements IGroupStore {
|
||||
count(): Promise<number> {
|
||||
@ -83,6 +84,10 @@ export default class FakeGroupStore implements IGroupStore {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getProjectGroups(projectId: string): Promise<IGroupWithProjectRoles[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getGroupProjects(groupIds: number[]): Promise<IGroupProject[]> {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user