mirror of
https://github.com/Unleash/unleash.git
synced 2025-11-10 01:19:53 +01: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,
|
id: groupAndProjectName,
|
||||||
name: 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(() => {
|
after(() => {
|
||||||
@ -76,7 +89,7 @@ describe('project-access', () => {
|
|||||||
|
|
||||||
cy.intercept(
|
cy.intercept(
|
||||||
'POST',
|
'POST',
|
||||||
`/api/admin/projects/${groupAndProjectName}/role/4/access`
|
`/api/admin/projects/${groupAndProjectName}/access`
|
||||||
).as('assignAccess');
|
).as('assignAccess');
|
||||||
|
|
||||||
cy.get(`[data-testid='${PA_USERS_GROUPS_ID}']`).click();
|
cy.get(`[data-testid='${PA_USERS_GROUPS_ID}']`).click();
|
||||||
@ -95,7 +108,7 @@ describe('project-access', () => {
|
|||||||
|
|
||||||
cy.intercept(
|
cy.intercept(
|
||||||
'POST',
|
'POST',
|
||||||
`/api/admin/projects/${groupAndProjectName}/role/4/access`
|
`/api/admin/projects/${groupAndProjectName}/access`
|
||||||
).as('assignAccess');
|
).as('assignAccess');
|
||||||
|
|
||||||
cy.get(`[data-testid='${PA_USERS_GROUPS_ID}']`).click();
|
cy.get(`[data-testid='${PA_USERS_GROUPS_ID}']`).click();
|
||||||
@ -114,9 +127,10 @@ describe('project-access', () => {
|
|||||||
|
|
||||||
cy.intercept(
|
cy.intercept(
|
||||||
'PUT',
|
'PUT',
|
||||||
`/api/admin/projects/${groupAndProjectName}/groups/${groupIds[0]}/roles/5`
|
`/api/admin/projects/${groupAndProjectName}/groups/${groupIds[0]}/roles`
|
||||||
).as('editAccess');
|
).as('editAccess');
|
||||||
|
|
||||||
|
cy.get(`[data-testid='CancelIcon']`).last().click();
|
||||||
cy.get(`[data-testid='${PA_ROLE_ID}']`).click();
|
cy.get(`[data-testid='${PA_ROLE_ID}']`).click();
|
||||||
cy.contains('update feature toggles within a project').click({
|
cy.contains('update feature toggles within a project').click({
|
||||||
force: true,
|
force: true,
|
||||||
@ -128,12 +142,31 @@ describe('project-access', () => {
|
|||||||
cy.get("td span:contains('Member')").should('have.length', 1);
|
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', () => {
|
it('can remove access', () => {
|
||||||
cy.get(`[data-testid='${PA_REMOVE_BUTTON_ID}']`).first().click();
|
cy.get(`[data-testid='${PA_REMOVE_BUTTON_ID}']`).first().click();
|
||||||
|
|
||||||
cy.intercept(
|
cy.intercept(
|
||||||
'DELETE',
|
'DELETE',
|
||||||
`/api/admin/projects/${groupAndProjectName}/groups/${groupIds[0]}/roles/5`
|
`/api/admin/projects/${groupAndProjectName}/groups/${groupIds[0]}/roles`
|
||||||
).as('removeAccess');
|
).as('removeAccess');
|
||||||
|
|
||||||
cy.contains("Yes, I'm sure").click();
|
cy.contains("Yes, I'm sure").click();
|
||||||
|
|||||||
@ -93,9 +93,13 @@ export const ServiceAccountsTable = () => {
|
|||||||
accessor: (row: any) =>
|
accessor: (row: any) =>
|
||||||
roles.find((role: IRole) => role.id === row.rootRole)
|
roles.find((role: IRole) => role.id === row.rootRole)
|
||||||
?.name || '',
|
?.name || '',
|
||||||
Cell: ({ row: { original: serviceAccount }, value }: any) => (
|
Cell: ({
|
||||||
<RoleCell value={value} roleId={serviceAccount.rootRole} />
|
row: { original: serviceAccount },
|
||||||
),
|
value,
|
||||||
|
}: {
|
||||||
|
row: { original: IServiceAccount };
|
||||||
|
value: string;
|
||||||
|
}) => <RoleCell value={value} role={serviceAccount.rootRole} />,
|
||||||
maxWidth: 120,
|
maxWidth: 120,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -125,9 +125,13 @@ const UsersList = () => {
|
|||||||
accessor: (row: any) =>
|
accessor: (row: any) =>
|
||||||
roles.find((role: IRole) => role.id === row.rootRole)
|
roles.find((role: IRole) => role.id === row.rootRole)
|
||||||
?.name || '',
|
?.name || '',
|
||||||
Cell: ({ row: { original: user }, value }: any) => (
|
Cell: ({
|
||||||
<RoleCell value={value} roleId={user.rootRole} />
|
row: { original: user },
|
||||||
),
|
value,
|
||||||
|
}: {
|
||||||
|
row: { original: IUser };
|
||||||
|
value: string;
|
||||||
|
}) => <RoleCell value={value} role={user.rootRole} />,
|
||||||
maxWidth: 120,
|
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,
|
: theme.palette.neutral.light,
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
fontSize: theme.fontSizes.smallBody,
|
fontSize: theme.fontSizes.smallBody,
|
||||||
borderRadius: theme.shape.borderRadiusMedium,
|
borderRadius: tooltip ? 0 : theme.shape.borderRadiusMedium,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledDescriptionBlock = styled('div')(({ theme }) => ({
|
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 { TooltipLink } from 'component/common/TooltipLink/TooltipLink';
|
||||||
import { RoleDescription } from 'component/common/RoleDescription/RoleDescription';
|
import { RoleDescription } from 'component/common/RoleDescription/RoleDescription';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import { styled } from '@mui/material';
|
||||||
|
|
||||||
interface IRoleCellProps {
|
const StyledRoleDescriptions = styled('div')(({ theme }) => ({
|
||||||
roleId: number;
|
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;
|
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();
|
const { isEnterprise } = useUiConfig();
|
||||||
|
|
||||||
if (isEnterprise()) {
|
if (isEnterprise()) {
|
||||||
|
const rolesArray = roles ? roles : [role];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TextCell>
|
<TextCell>
|
||||||
<TooltipLink
|
<TooltipLink
|
||||||
tooltip={<RoleDescription roleId={roleId} tooltip />}
|
tooltip={
|
||||||
|
<StyledRoleDescriptions>
|
||||||
|
{rolesArray.map(roleId => (
|
||||||
|
<RoleDescription
|
||||||
|
key={roleId}
|
||||||
|
roleId={roleId}
|
||||||
|
tooltip
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</StyledRoleDescriptions>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{value}
|
{value}
|
||||||
</TooltipLink>
|
</TooltipLink>
|
||||||
|
|||||||
@ -35,6 +35,7 @@ import {
|
|||||||
} from 'utils/testIds';
|
} from 'utils/testIds';
|
||||||
import { caseInsensitiveSearch } from 'utils/search';
|
import { caseInsensitiveSearch } from 'utils/search';
|
||||||
import { IServiceAccount } from 'interfaces/service-account';
|
import { IServiceAccount } from 'interfaces/service-account';
|
||||||
|
import { MultipleRoleSelect } from 'component/common/MultipleRoleSelect/MultipleRoleSelect';
|
||||||
import { RoleSelect } from 'component/common/RoleSelect/RoleSelect';
|
import { RoleSelect } from 'component/common/RoleSelect/RoleSelect';
|
||||||
|
|
||||||
const StyledForm = styled('form')(() => ({
|
const StyledForm = styled('form')(() => ({
|
||||||
@ -111,7 +112,7 @@ export const ProjectAccessAssign = ({
|
|||||||
|
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const { refetchProjectAccess } = useProjectAccess(projectId);
|
const { refetchProjectAccess } = useProjectAccess(projectId);
|
||||||
const { addAccessToProject, changeUserRole, changeGroupRole, loading } =
|
const { addAccessToProject, setUserRoles, setGroupRoles, loading } =
|
||||||
useProjectApi();
|
useProjectApi();
|
||||||
const edit = Boolean(selected);
|
const edit = Boolean(selected);
|
||||||
|
|
||||||
@ -181,38 +182,46 @@ export const ProjectAccessAssign = ({
|
|||||||
id === selected?.entity.id && type === selected?.type
|
id === selected?.entity.id && type === selected?.type
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const [role, setRole] = useState<IRole | null>(
|
const [selectedRoles, setRoles] = useState<IRole[]>(
|
||||||
roles.find(({ id }) => id === selected?.entity.roleId) ?? null
|
roles.filter(({ id }) => selected?.entity?.roles?.includes(id))
|
||||||
);
|
);
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
|
roles: selectedRoles.map(({ id }) => id),
|
||||||
|
groups: selectedOptions
|
||||||
|
?.filter(({ type }) => type === ENTITY_TYPE.GROUP)
|
||||||
|
.map(({ id }) => id),
|
||||||
users: selectedOptions
|
users: selectedOptions
|
||||||
?.filter(
|
?.filter(
|
||||||
({ type }) =>
|
({ type }) =>
|
||||||
type === ENTITY_TYPE.USER ||
|
type === ENTITY_TYPE.USER ||
|
||||||
type === ENTITY_TYPE.SERVICE_ACCOUNT
|
type === ENTITY_TYPE.SERVICE_ACCOUNT
|
||||||
)
|
)
|
||||||
.map(({ id }) => ({ id })),
|
.map(({ id }) => id),
|
||||||
groups: selectedOptions
|
|
||||||
?.filter(({ type }) => type === ENTITY_TYPE.GROUP)
|
|
||||||
.map(({ id }) => ({ id })),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!isValid) return;
|
if (!isValid) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!edit) {
|
if (!edit) {
|
||||||
await addAccessToProject(projectId, role.id, payload);
|
await addAccessToProject(projectId, payload);
|
||||||
} else if (
|
} else if (
|
||||||
selected?.type === ENTITY_TYPE.USER ||
|
selected?.type === ENTITY_TYPE.USER ||
|
||||||
selected?.type === ENTITY_TYPE.SERVICE_ACCOUNT
|
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) {
|
} 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();
|
refetchProjectAccess();
|
||||||
navigate(GO_BACK);
|
navigate(GO_BACK);
|
||||||
@ -229,22 +238,24 @@ export const ProjectAccessAssign = ({
|
|||||||
|
|
||||||
const formatApiCode = () => {
|
const formatApiCode = () => {
|
||||||
if (edit) {
|
if (edit) {
|
||||||
return `curl --location --request ${edit ? 'PUT' : 'POST'} '${
|
return `curl --location --request PUT '${
|
||||||
uiConfig.unleashUrl
|
uiConfig.unleashUrl
|
||||||
}/api/admin/projects/${projectId}/${
|
}/api/admin/projects/${projectId}/${
|
||||||
selected?.type === ENTITY_TYPE.USER ||
|
selected?.type === ENTITY_TYPE.USER ||
|
||||||
selected?.type === ENTITY_TYPE.SERVICE_ACCOUNT
|
selected?.type === ENTITY_TYPE.SERVICE_ACCOUNT
|
||||||
? 'users'
|
? 'users'
|
||||||
: 'groups'
|
: 'groups'
|
||||||
}/${selected?.entity.id}/roles/${role?.id}' \\
|
}/${selected?.entity.id}/roles' \\
|
||||||
--header 'Authorization: INSERT_API_KEY'`;
|
--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
|
uiConfig.unleashUrl
|
||||||
}/api/admin/projects/${projectId}/role/${role?.id}/access' \\
|
}/api/admin/projects/${projectId}/access' \\
|
||||||
--header 'Authorization: INSERT_API_KEY' \\
|
--header 'Authorization: INSERT_API_KEY' \\
|
||||||
--header 'Content-Type: application/json' \\
|
--header 'Content-Type: application/json' \\
|
||||||
--data-raw '${JSON.stringify(payload, undefined, 2)}'`;
|
--data-raw '${JSON.stringify(payload, undefined, 2)}'`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createRootGroupWarning = (group?: IGroup): string | undefined => {
|
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 (
|
return (
|
||||||
<SidebarModal
|
<SidebarModal
|
||||||
@ -430,11 +441,28 @@ export const ProjectAccessAssign = ({
|
|||||||
Select the role to assign for this project
|
Select the role to assign for this project
|
||||||
</StyledInputDescription>
|
</StyledInputDescription>
|
||||||
<StyledAutocompleteWrapper>
|
<StyledAutocompleteWrapper>
|
||||||
<RoleSelect
|
<ConditionallyRender
|
||||||
data-testid={PA_ROLE_ID}
|
condition={Boolean(
|
||||||
roles={roles}
|
uiConfig.flags.multipleRoles
|
||||||
value={role}
|
)}
|
||||||
setValue={role => setRole(role || null)}
|
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>
|
</StyledAutocompleteWrapper>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -93,7 +93,7 @@ export const ProjectAccessTable: VFC = () => {
|
|||||||
const { setToastData } = useToast();
|
const { setToastData } = useToast();
|
||||||
|
|
||||||
const { access, refetchProjectAccess } = useProjectAccess(projectId);
|
const { access, refetchProjectAccess } = useProjectAccess(projectId);
|
||||||
const { removeUserFromRole, removeGroupFromRole } = useProjectApi();
|
const { removeUserAccess, removeGroupAccess } = useProjectApi();
|
||||||
const [removeOpen, setRemoveOpen] = useState(false);
|
const [removeOpen, setRemoveOpen] = useState(false);
|
||||||
const [groupOpen, setGroupOpen] = useState(false);
|
const [groupOpen, setGroupOpen] = useState(false);
|
||||||
const [selectedRow, setSelectedRow] = useState<IProjectAccess>();
|
const [selectedRow, setSelectedRow] = useState<IProjectAccess>();
|
||||||
@ -151,11 +151,18 @@ export const ProjectAccessTable: VFC = () => {
|
|||||||
id: 'role',
|
id: 'role',
|
||||||
Header: 'Role',
|
Header: 'Role',
|
||||||
accessor: (row: IProjectAccess) =>
|
accessor: (row: IProjectAccess) =>
|
||||||
access?.roles.find(({ id }) => id === row.entity.roleId)
|
row.entity.roles.length > 1
|
||||||
?.name,
|
? `${row.entity.roles.length} roles`
|
||||||
Cell: ({ value, row: { original: row } }: any) => (
|
: access?.roles.find(
|
||||||
<RoleCell roleId={row.entity.roleId} value={value} />
|
({ 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,
|
maxWidth: 125,
|
||||||
filterName: 'role',
|
filterName: 'role',
|
||||||
},
|
},
|
||||||
@ -345,7 +352,7 @@ export const ProjectAccessTable: VFC = () => {
|
|||||||
|
|
||||||
const removeAccess = async (userOrGroup?: IProjectAccess) => {
|
const removeAccess = async (userOrGroup?: IProjectAccess) => {
|
||||||
if (!userOrGroup) return;
|
if (!userOrGroup) return;
|
||||||
const { id, roleId } = userOrGroup.entity;
|
const { id } = userOrGroup.entity;
|
||||||
let name = userOrGroup.entity.name;
|
let name = userOrGroup.entity.name;
|
||||||
if (userOrGroup.type !== ENTITY_TYPE.GROUP) {
|
if (userOrGroup.type !== ENTITY_TYPE.GROUP) {
|
||||||
const user = userOrGroup.entity as IUser;
|
const user = userOrGroup.entity as IUser;
|
||||||
@ -354,9 +361,9 @@ export const ProjectAccessTable: VFC = () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (userOrGroup.type !== ENTITY_TYPE.GROUP) {
|
if (userOrGroup.type !== ENTITY_TYPE.GROUP) {
|
||||||
await removeUserFromRole(projectId, roleId, id);
|
await removeUserAccess(projectId, id);
|
||||||
} else {
|
} else {
|
||||||
await removeGroupFromRole(projectId, roleId, id);
|
await removeGroupAccess(projectId, id);
|
||||||
}
|
}
|
||||||
refetchProjectAccess();
|
refetchProjectAccess();
|
||||||
setToastData({
|
setToastData({
|
||||||
|
|||||||
@ -9,9 +9,10 @@ interface ICreatePayload {
|
|||||||
defaultStickiness: string;
|
defaultStickiness: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IAccessesPayload {
|
interface IAccessPayload {
|
||||||
users: { id: number }[];
|
roles: number[];
|
||||||
groups: { id: number }[];
|
groups: number[];
|
||||||
|
users: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const useProjectApi = () => {
|
const useProjectApi = () => {
|
||||||
@ -116,93 +117,59 @@ const useProjectApi = () => {
|
|||||||
|
|
||||||
const addAccessToProject = async (
|
const addAccessToProject = async (
|
||||||
projectId: string,
|
projectId: string,
|
||||||
roleId: number,
|
payload: IAccessPayload
|
||||||
accesses: IAccessesPayload
|
|
||||||
) => {
|
) => {
|
||||||
const path = `api/admin/projects/${projectId}/role/${roleId}/access`;
|
const path = `api/admin/projects/${projectId}/access`;
|
||||||
const req = createRequest(path, {
|
const req = createRequest(path, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(accesses),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
return await makeRequest(req.caller, req.id);
|
||||||
const res = await makeRequest(req.caller, req.id);
|
|
||||||
|
|
||||||
return res;
|
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeUserFromRole = async (
|
const removeUserAccess = async (projectId: string, userId: number) => {
|
||||||
projectId: string,
|
const path = `api/admin/projects/${projectId}/users/${userId}/roles`;
|
||||||
roleId: number,
|
|
||||||
userId: number
|
|
||||||
) => {
|
|
||||||
const path = `api/admin/projects/${projectId}/users/${userId}/roles/${roleId}`;
|
|
||||||
const req = createRequest(path, { method: 'DELETE' });
|
const req = createRequest(path, { method: 'DELETE' });
|
||||||
|
|
||||||
try {
|
return await makeRequest(req.caller, req.id);
|
||||||
const res = await makeRequest(req.caller, req.id);
|
|
||||||
|
|
||||||
return res;
|
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeGroupFromRole = async (
|
const removeGroupAccess = async (projectId: string, groupId: number) => {
|
||||||
projectId: string,
|
const path = `api/admin/projects/${projectId}/groups/${groupId}/roles`;
|
||||||
roleId: number,
|
|
||||||
groupId: number
|
|
||||||
) => {
|
|
||||||
const path = `api/admin/projects/${projectId}/groups/${groupId}/roles/${roleId}`;
|
|
||||||
const req = createRequest(path, { method: 'DELETE' });
|
const req = createRequest(path, { method: 'DELETE' });
|
||||||
|
|
||||||
try {
|
return await makeRequest(req.caller, req.id);
|
||||||
const res = await makeRequest(req.caller, req.id);
|
|
||||||
|
|
||||||
return res;
|
|
||||||
} catch (e) {
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const searchProjectUser = async (query: string): Promise<Response> => {
|
const setUserRoles = (
|
||||||
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 = (
|
|
||||||
projectId: string,
|
projectId: string,
|
||||||
roleId: number,
|
roleIds: number[],
|
||||||
userId: number
|
userId: number
|
||||||
) => {
|
) => {
|
||||||
const path = `api/admin/projects/${projectId}/users/${userId}/roles/${roleId}`;
|
const path = `api/admin/projects/${projectId}/users/${userId}/roles`;
|
||||||
const req = createRequest(path, { method: 'PUT' });
|
const req = createRequest(path, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({ roles: roleIds }),
|
||||||
|
});
|
||||||
|
|
||||||
return makeRequest(req.caller, req.id);
|
return makeRequest(req.caller, req.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeGroupRole = (
|
const setGroupRoles = (
|
||||||
projectId: string,
|
projectId: string,
|
||||||
roleId: number,
|
roleIds: number[],
|
||||||
groupId: number
|
groupId: number
|
||||||
) => {
|
) => {
|
||||||
const path = `api/admin/projects/${projectId}/groups/${groupId}/roles/${roleId}`;
|
const path = `api/admin/projects/${projectId}/groups/${groupId}/roles`;
|
||||||
const req = createRequest(path, { method: 'PUT' });
|
const req = createRequest(path, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({ roles: roleIds }),
|
||||||
|
});
|
||||||
|
|
||||||
return makeRequest(req.caller, req.id);
|
return makeRequest(req.caller, req.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const archiveFeatures = async (projectId: string, featureIds: string[]) => {
|
const archiveFeatures = async (projectId: string, featureIds: string[]) => {
|
||||||
const path = `api/admin/projects/${projectId}/archive`;
|
const path = `api/admin/projects/${projectId}/archive`;
|
||||||
const req = createRequest(path, {
|
const req = createRequest(path, {
|
||||||
@ -283,16 +250,15 @@ const useProjectApi = () => {
|
|||||||
addEnvironmentToProject,
|
addEnvironmentToProject,
|
||||||
removeEnvironmentFromProject,
|
removeEnvironmentFromProject,
|
||||||
addAccessToProject,
|
addAccessToProject,
|
||||||
removeUserFromRole,
|
removeUserAccess,
|
||||||
removeGroupFromRole,
|
removeGroupAccess,
|
||||||
changeUserRole,
|
setUserRoles,
|
||||||
changeGroupRole,
|
setGroupRoles,
|
||||||
archiveFeatures,
|
archiveFeatures,
|
||||||
reviveFeatures,
|
reviveFeatures,
|
||||||
staleFeatures,
|
staleFeatures,
|
||||||
deleteFeature,
|
deleteFeature,
|
||||||
deleteFeatures,
|
deleteFeatures,
|
||||||
searchProjectUser,
|
|
||||||
updateDefaultStrategy,
|
updateDefaultStrategy,
|
||||||
errors,
|
errors,
|
||||||
loading,
|
loading,
|
||||||
|
|||||||
@ -20,10 +20,12 @@ export interface IProjectAccess {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IProjectAccessUser extends IUser {
|
export interface IProjectAccessUser extends IUser {
|
||||||
|
roles: number[];
|
||||||
roleId: number;
|
roleId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IProjectAccessGroup extends IGroup {
|
export interface IProjectAccessGroup extends IGroup {
|
||||||
|
roles: number[];
|
||||||
roleId: number;
|
roleId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -57,6 +57,7 @@ export interface IFlags {
|
|||||||
lastSeenByEnvironment?: boolean;
|
lastSeenByEnvironment?: boolean;
|
||||||
newApplicationList?: boolean;
|
newApplicationList?: boolean;
|
||||||
integrationsRework?: boolean;
|
integrationsRework?: boolean;
|
||||||
|
multipleRoles?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IVersionInfo {
|
export interface IVersionInfo {
|
||||||
|
|||||||
@ -92,6 +92,7 @@ exports[`should create default config 1`] = `
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"migrationLock": true,
|
"migrationLock": true,
|
||||||
|
"multipleRoles": false,
|
||||||
"personalAccessTokensKillSwitch": false,
|
"personalAccessTokensKillSwitch": false,
|
||||||
"proPlanAutoCharge": false,
|
"proPlanAutoCharge": false,
|
||||||
"responseTimeWithAppNameKillSwitch": false,
|
"responseTimeWithAppNameKillSwitch": false,
|
||||||
@ -127,6 +128,7 @@ exports[`should create default config 1`] = `
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
"migrationLock": true,
|
"migrationLock": true,
|
||||||
|
"multipleRoles": false,
|
||||||
"personalAccessTokensKillSwitch": false,
|
"personalAccessTokensKillSwitch": false,
|
||||||
"proPlanAutoCharge": false,
|
"proPlanAutoCharge": false,
|
||||||
"responseTimeWithAppNameKillSwitch": false,
|
"responseTimeWithAppNameKillSwitch": false,
|
||||||
|
|||||||
@ -10,11 +10,13 @@ import {
|
|||||||
IRoleWithProject,
|
IRoleWithProject,
|
||||||
IUserPermission,
|
IUserPermission,
|
||||||
IUserRole,
|
IUserRole,
|
||||||
|
IUserWithProjectRoles,
|
||||||
} from '../types/stores/access-store';
|
} from '../types/stores/access-store';
|
||||||
import { IPermission } from '../types/model';
|
import { IPermission } from '../types/model';
|
||||||
import NotFoundError from '../error/notfound-error';
|
import NotFoundError from '../error/notfound-error';
|
||||||
import {
|
import {
|
||||||
ENVIRONMENT_PERMISSION_TYPE,
|
ENVIRONMENT_PERMISSION_TYPE,
|
||||||
|
PROJECT_ROLE_TYPES,
|
||||||
ROOT_PERMISSION_TYPE,
|
ROOT_PERMISSION_TYPE,
|
||||||
} from '../util/constants';
|
} from '../util/constants';
|
||||||
import { Db } from './db';
|
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.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')
|
||||||
.whereNull('g.root_role_id')
|
|
||||||
.andWhere('gu.user_id', '=', userId);
|
.andWhere('gu.user_id', '=', userId);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -167,7 +168,6 @@ export class AccessStore implements IAccessStore {
|
|||||||
.whereNotNull('g.root_role_id')
|
.whereNotNull('g.root_role_id')
|
||||||
.andWhere('gu.user_id', '=', userId);
|
.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);
|
||||||
@ -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[]> {
|
async getRolesForUserId(userId: number): Promise<IRoleWithProject[]> {
|
||||||
return this.db
|
return this.db
|
||||||
.select(['id', 'name', 'type', 'project', 'description'])
|
.select(['id', 'name', 'type', 'project', 'description'])
|
||||||
@ -310,13 +338,13 @@ export class AccessStore implements IAccessStore {
|
|||||||
): Promise<IProjectRoleUsage[]> {
|
): Promise<IProjectRoleUsage[]> {
|
||||||
const query = await this.db.raw(
|
const query = await this.db.raw(
|
||||||
`
|
`
|
||||||
SELECT
|
SELECT
|
||||||
uq.project,
|
uq.project,
|
||||||
sum(uq.user_count) AS user_count,
|
sum(uq.user_count) AS user_count,
|
||||||
sum(uq.svc_account_count) AS svc_account_count,
|
sum(uq.svc_account_count) AS svc_account_count,
|
||||||
sum(uq.group_count) AS group_count
|
sum(uq.group_count) AS group_count
|
||||||
FROM (
|
FROM (
|
||||||
SELECT
|
SELECT
|
||||||
project,
|
project,
|
||||||
0 AS user_count,
|
0 AS user_count,
|
||||||
0 AS svc_account_count,
|
0 AS svc_account_count,
|
||||||
@ -341,12 +369,6 @@ export class AccessStore implements IAccessStore {
|
|||||||
[roleId, roleId],
|
[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 query.rows.map((r) => {
|
||||||
return {
|
return {
|
||||||
project: r.project,
|
project: r.project,
|
||||||
@ -446,7 +468,7 @@ export class AccessStore implements IAccessStore {
|
|||||||
.update('role_id', roleId);
|
.update('role_id', roleId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addAccessToProject(
|
async addRoleAccessToProject(
|
||||||
users: IAccessInfo[],
|
users: IAccessInfo[],
|
||||||
groups: IAccessInfo[],
|
groups: IAccessInfo[],
|
||||||
projectId: string,
|
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(
|
async removeRolesOfTypeForUser(
|
||||||
userId: number,
|
userId: number,
|
||||||
roleTypes: string[],
|
roleTypes: string[],
|
||||||
|
|||||||
@ -10,6 +10,8 @@ import Group, {
|
|||||||
} from '../types/group';
|
} from '../types/group';
|
||||||
import { Db } from './db';
|
import { Db } from './db';
|
||||||
import { BadDataError, FOREIGN_KEY_VIOLATION } from '../error';
|
import { BadDataError, FOREIGN_KEY_VIOLATION } from '../error';
|
||||||
|
import { IGroupWithProjectRoles } from '../types/stores/access-store';
|
||||||
|
import { PROJECT_ROLE_TYPES } from '../util';
|
||||||
|
|
||||||
const T = {
|
const T = {
|
||||||
GROUPS: 'groups',
|
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[]> {
|
async getGroupProjects(groupIds: number[]): Promise<IGroupProject[]> {
|
||||||
const rows = await this.db
|
const rows = await this.db
|
||||||
.select('group_id', 'project')
|
.select('group_id', 'project')
|
||||||
|
|||||||
@ -1,14 +1,17 @@
|
|||||||
import * as permissions from '../types/permissions';
|
import * as permissions from '../types/permissions';
|
||||||
import User, { IProjectUser, IUser } from '../types/user';
|
import User, { IUser } from '../types/user';
|
||||||
import {
|
import {
|
||||||
IAccessInfo,
|
IAccessInfo,
|
||||||
IAccessStore,
|
IAccessStore,
|
||||||
|
IGroupWithProjectRoles,
|
||||||
IProjectRoleUsage,
|
IProjectRoleUsage,
|
||||||
IRole,
|
IRole,
|
||||||
|
IRoleDescriptor,
|
||||||
IRoleWithPermissions,
|
IRoleWithPermissions,
|
||||||
IRoleWithProject,
|
IRoleWithProject,
|
||||||
IUserPermission,
|
IUserPermission,
|
||||||
IUserRole,
|
IUserRole,
|
||||||
|
IUserWithProjectRoles,
|
||||||
} from '../types/stores/access-store';
|
} from '../types/stores/access-store';
|
||||||
import { Logger } from '../logger';
|
import { Logger } from '../logger';
|
||||||
import { IAccountStore, IGroupStore, IUnleashStores } from '../types/stores';
|
import { IAccountStore, IGroupStore, IUnleashStores } from '../types/stores';
|
||||||
@ -35,7 +38,7 @@ import {
|
|||||||
import { DEFAULT_PROJECT } from '../types/project';
|
import { DEFAULT_PROJECT } from '../types/project';
|
||||||
import InvalidOperationError from '../error/invalid-operation-error';
|
import InvalidOperationError from '../error/invalid-operation-error';
|
||||||
import BadDataError from '../error/bad-data-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 { GroupService } from './group-service';
|
||||||
import { IFlagResolver, IUnleashConfig } from 'lib/types';
|
import { IFlagResolver, IUnleashConfig } from 'lib/types';
|
||||||
|
|
||||||
@ -70,6 +73,12 @@ interface IRoleUpdate {
|
|||||||
permissions?: IPermission[];
|
permissions?: IPermission[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AccessWithRoles {
|
||||||
|
roles: IRoleDescriptor[];
|
||||||
|
groups: IGroupWithProjectRoles[];
|
||||||
|
users: IUserWithProjectRoles[];
|
||||||
|
}
|
||||||
|
|
||||||
const isProjectPermission = (permission) => PROJECT_ADMIN.includes(permission);
|
const isProjectPermission = (permission) => PROJECT_ADMIN.includes(permission);
|
||||||
|
|
||||||
export class AccessService {
|
export class AccessService {
|
||||||
@ -235,14 +244,14 @@ export class AccessService {
|
|||||||
return this.store.addGroupToRole(groupId, roleId, createdBy, projectId);
|
return this.store.addGroupToRole(groupId, roleId, createdBy, projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addAccessToProject(
|
async addRoleAccessToProject(
|
||||||
users: IAccessInfo[],
|
users: IAccessInfo[],
|
||||||
groups: IAccessInfo[],
|
groups: IAccessInfo[],
|
||||||
projectId: string,
|
projectId: string,
|
||||||
roleId: number,
|
roleId: number,
|
||||||
createdBy: string,
|
createdBy: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
return this.store.addAccessToProject(
|
return this.store.addRoleAccessToProject(
|
||||||
users,
|
users,
|
||||||
groups,
|
groups,
|
||||||
projectId,
|
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> {
|
async getRoleByName(roleName: string): Promise<IRole> {
|
||||||
return this.roleStore.getRoleByName(roleName);
|
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(
|
async setUserRootRole(
|
||||||
userId: number,
|
userId: number,
|
||||||
role: number | RoleName,
|
role: number | RoleName,
|
||||||
@ -417,7 +486,7 @@ export class AccessService {
|
|||||||
async getProjectUsersForRole(
|
async getProjectUsersForRole(
|
||||||
roleId: number,
|
roleId: number,
|
||||||
projectId?: string,
|
projectId?: string,
|
||||||
): Promise<IProjectUser[]> {
|
): Promise<IUserWithRole[]> {
|
||||||
const userRoleList = await this.store.getProjectUsersForRole(
|
const userRoleList = await this.store.getProjectUsersForRole(
|
||||||
roleId,
|
roleId,
|
||||||
projectId,
|
projectId,
|
||||||
@ -430,28 +499,44 @@ export class AccessService {
|
|||||||
return {
|
return {
|
||||||
...user,
|
...user,
|
||||||
addedAt: role.addedAt!,
|
addedAt: role.addedAt!,
|
||||||
|
roleId,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectRoleAccess(
|
async getProjectUsers(projectId: string): Promise<IUserWithProjectRoles[]> {
|
||||||
projectId: string,
|
const projectUsers = await this.store.getProjectUsers(projectId);
|
||||||
): Promise<[IRole[], IUserWithRole[], IGroupModelWithProjectRole[]]> {
|
|
||||||
|
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 roles = await this.roleStore.getProjectRoles();
|
||||||
|
|
||||||
const users = await Promise.all(
|
const users = await this.getProjectUsers(projectId);
|
||||||
roles.map(async (role) => {
|
|
||||||
const projectUsers = await this.getProjectUsersForRole(
|
|
||||||
role.id,
|
|
||||||
projectId,
|
|
||||||
);
|
|
||||||
return projectUsers.map((u) => ({ ...u, roleId: role.id }));
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const groups = await this.groupService.getProjectGroups(projectId);
|
const groups = await this.groupService.getProjectGroups(projectId);
|
||||||
return [roles, users.flat(), groups];
|
|
||||||
|
return {
|
||||||
|
roles,
|
||||||
|
groups,
|
||||||
|
users,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectRoleUsage(roleId: number): Promise<IProjectRoleUsage[]> {
|
async getProjectRoleUsage(roleId: number): Promise<IProjectRoleUsage[]> {
|
||||||
|
|||||||
@ -147,29 +147,27 @@ export class GroupService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getProjectGroups(
|
async getProjectGroups(
|
||||||
projectId?: string,
|
projectId: string,
|
||||||
): Promise<IGroupModelWithProjectRole[]> {
|
): Promise<IGroupModelWithProjectRole[]> {
|
||||||
const groupRoles = await this.groupStore.getProjectGroupRoles(
|
const projectGroups = await this.groupStore.getProjectGroups(projectId);
|
||||||
projectId,
|
|
||||||
);
|
if (projectGroups.length > 0) {
|
||||||
if (groupRoles.length > 0) {
|
|
||||||
const groups = await this.groupStore.getAllWithId(
|
const groups = await this.groupStore.getAllWithId(
|
||||||
groupRoles.map((a) => a.groupId),
|
projectGroups.map((g) => g.id!),
|
||||||
);
|
);
|
||||||
const groupUsers = await this.groupStore.getAllUsersByGroups(
|
const groupUsers = await this.groupStore.getAllUsersByGroups(
|
||||||
groups.map((g) => g.id),
|
groups.map((g) => g.id!),
|
||||||
);
|
);
|
||||||
|
|
||||||
const users = await this.accountStore.getAllWithId(
|
const users = await this.accountStore.getAllWithId(
|
||||||
groupUsers.map((u) => u.userId),
|
groupUsers.map((u) => u.userId),
|
||||||
);
|
);
|
||||||
return groups.map((group) => {
|
return groups.flatMap((group) => {
|
||||||
const groupRole = groupRoles.find((g) => g.groupId == group.id);
|
return projectGroups
|
||||||
return {
|
.filter((gr) => gr.id === group.id)
|
||||||
...this.mapGroupWithUsers(group, groupUsers, users),
|
.map((groupRole) => ({
|
||||||
roleId: groupRole.roleId,
|
...this.mapGroupWithUsers(group, groupUsers, users),
|
||||||
addedAt: groupRole.createdAt,
|
...groupRole,
|
||||||
};
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { subDays } from 'date-fns';
|
import { subDays } from 'date-fns';
|
||||||
import { ValidationError } from 'joi';
|
import { ValidationError } from 'joi';
|
||||||
import User, { IUser } from '../types/user';
|
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 NameExistsError from '../error/name-exists-error';
|
||||||
import InvalidOperationError from '../error/invalid-operation-error';
|
import InvalidOperationError from '../error/invalid-operation-error';
|
||||||
import { nameType } from '../routes/util';
|
import { nameType } from '../routes/util';
|
||||||
@ -21,7 +21,6 @@ import {
|
|||||||
IProjectWithCount,
|
IProjectWithCount,
|
||||||
IUnleashConfig,
|
IUnleashConfig,
|
||||||
IUnleashStores,
|
IUnleashStores,
|
||||||
IUserWithRole,
|
|
||||||
MOVE_FEATURE_TOGGLE,
|
MOVE_FEATURE_TOGGLE,
|
||||||
PROJECT_CREATED,
|
PROJECT_CREATED,
|
||||||
PROJECT_DELETED,
|
PROJECT_DELETED,
|
||||||
@ -35,7 +34,10 @@ import {
|
|||||||
RoleName,
|
RoleName,
|
||||||
IFlagResolver,
|
IFlagResolver,
|
||||||
ProjectAccessAddedEvent,
|
ProjectAccessAddedEvent,
|
||||||
|
ProjectAccessUserRolesUpdated,
|
||||||
|
ProjectAccessGroupRolesUpdated,
|
||||||
IProjectRoleUsage,
|
IProjectRoleUsage,
|
||||||
|
ProjectAccessUserRolesDeleted,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { IProjectQuery, IProjectStore } from '../types/stores/project-store';
|
import { IProjectQuery, IProjectStore } from '../types/stores/project-store';
|
||||||
import {
|
import {
|
||||||
@ -48,7 +50,7 @@ import { IFeatureTagStore } from 'lib/types/stores/feature-tag-store';
|
|||||||
import ProjectWithoutOwnerError from '../error/project-without-owner-error';
|
import ProjectWithoutOwnerError from '../error/project-without-owner-error';
|
||||||
import { arraysHaveSameItems } from '../util';
|
import { arraysHaveSameItems } from '../util';
|
||||||
import { GroupService } from './group-service';
|
import { GroupService } from './group-service';
|
||||||
import { IGroupModelWithProjectRole, IGroupRole } from 'lib/types/group';
|
import { IGroupRole } from 'lib/types/group';
|
||||||
import { FavoritesService } from './favorites-service';
|
import { FavoritesService } from './favorites-service';
|
||||||
import { calculateAverageTimeToProd } from '../features/feature-toggle/time-to-production/time-to-production';
|
import { calculateAverageTimeToProd } from '../features/feature-toggle/time-to-production/time-to-production';
|
||||||
import { IProjectStatsStore } from 'lib/types/stores/project-stats-store-type';
|
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';
|
const getCreatedBy = (user: IUser) => user.email || user.username || 'unknown';
|
||||||
|
|
||||||
export interface AccessWithRoles {
|
|
||||||
users: IUserWithRole[];
|
|
||||||
roles: IRoleDescriptor[];
|
|
||||||
groups: IGroupModelWithProjectRole[];
|
|
||||||
}
|
|
||||||
|
|
||||||
type Days = number;
|
type Days = number;
|
||||||
type Count = number;
|
type Count = number;
|
||||||
|
|
||||||
@ -335,14 +331,7 @@ export default class ProjectService {
|
|||||||
|
|
||||||
// RBAC methods
|
// RBAC methods
|
||||||
async getAccessToProject(projectId: string): Promise<AccessWithRoles> {
|
async getAccessToProject(projectId: string): Promise<AccessWithRoles> {
|
||||||
const [roles, users, groups] =
|
return this.accessService.getProjectRoleAccess(projectId);
|
||||||
await this.accessService.getProjectRoleAccess(projectId);
|
|
||||||
|
|
||||||
return {
|
|
||||||
roles,
|
|
||||||
users,
|
|
||||||
groups,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deprecated: See addAccess instead.
|
// Deprecated: See addAccess instead.
|
||||||
@ -352,7 +341,7 @@ export default class ProjectService {
|
|||||||
userId: number,
|
userId: number,
|
||||||
createdBy: string,
|
createdBy: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const [roles, users] = await this.accessService.getProjectRoleAccess(
|
const { roles, users } = await this.accessService.getProjectRoleAccess(
|
||||||
projectId,
|
projectId,
|
||||||
);
|
);
|
||||||
const user = await this.accountStore.get(userId);
|
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(
|
async addGroup(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
roleId: number,
|
roleId: number,
|
||||||
@ -484,13 +521,13 @@ export default class ProjectService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async addAccess(
|
async addRoleAccess(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
roleId: number,
|
roleId: number,
|
||||||
usersAndGroups: IProjectAccessModel,
|
usersAndGroups: IProjectAccessModel,
|
||||||
createdBy: string,
|
createdBy: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await this.accessService.addAccessToProject(
|
await this.accessService.addRoleAccessToProject(
|
||||||
usersAndGroups.users,
|
usersAndGroups.users,
|
||||||
usersAndGroups.groups,
|
usersAndGroups.groups,
|
||||||
projectId,
|
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(
|
async findProjectGroupRole(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
roleId: number,
|
roleId: number,
|
||||||
@ -670,7 +798,7 @@ export default class ProjectService {
|
|||||||
async getProjectUsers(
|
async getProjectUsers(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
): Promise<Array<Pick<IUser, 'id' | 'email' | 'username'>>> {
|
): Promise<Array<Pick<IUser, 'id' | 'email' | 'username'>>> {
|
||||||
const [, users, groups] = await this.accessService.getProjectRoleAccess(
|
const { groups, users } = await this.accessService.getProjectRoleAccess(
|
||||||
projectId,
|
projectId,
|
||||||
);
|
);
|
||||||
const actualUsers = users.map((user) => ({
|
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_UPDATED = 'context-field-updated' as const;
|
||||||
export const CONTEXT_FIELD_DELETED = 'context-field-deleted' 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_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_CREATED = 'project-created' as const;
|
||||||
export const PROJECT_UPDATED = 'project-updated' as const;
|
export const PROJECT_UPDATED = 'project-updated' as const;
|
||||||
export const PROJECT_DELETED = 'project-deleted' as const;
|
export const PROJECT_DELETED = 'project-deleted' as const;
|
||||||
@ -162,6 +175,10 @@ export const IEventTypes = [
|
|||||||
CONTEXT_FIELD_UPDATED,
|
CONTEXT_FIELD_UPDATED,
|
||||||
CONTEXT_FIELD_DELETED,
|
CONTEXT_FIELD_DELETED,
|
||||||
PROJECT_ACCESS_ADDED,
|
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_CREATED,
|
||||||
PROJECT_UPDATED,
|
PROJECT_UPDATED,
|
||||||
PROJECT_DELETED,
|
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 {
|
export class SettingCreatedEvent extends BaseEvent {
|
||||||
readonly data: any;
|
readonly data: any;
|
||||||
|
|
||||||
|
|||||||
@ -28,7 +28,8 @@ export type IFlagKey =
|
|||||||
| 'changeRequestReject'
|
| 'changeRequestReject'
|
||||||
| 'customRootRolesKillSwitch'
|
| 'customRootRolesKillSwitch'
|
||||||
| 'newApplicationList'
|
| 'newApplicationList'
|
||||||
| 'integrationsRework';
|
| 'integrationsRework'
|
||||||
|
| 'multipleRoles';
|
||||||
|
|
||||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||||
|
|
||||||
@ -128,6 +129,10 @@ const flags: IFlags = {
|
|||||||
process.env.UNLEASH_INTEGRATIONS,
|
process.env.UNLEASH_INTEGRATIONS,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
|
multipleRoles: parseEnvVarBoolean(
|
||||||
|
process.env.UNLEASH_EXPERIMENTAL_MULTIPLE_ROLES,
|
||||||
|
false,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultExperimentalOptions: IExperimentalOptions = {
|
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';
|
import { Store } from './store';
|
||||||
|
|
||||||
export interface IUserPermission {
|
export interface IUserPermission {
|
||||||
@ -52,6 +53,17 @@ export interface IUserRole {
|
|||||||
addedAt?: Date;
|
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> {
|
export interface IAccessStore extends Store<IRole, number> {
|
||||||
getAvailablePermissions(): Promise<IPermission[]>;
|
getAvailablePermissions(): Promise<IPermission[]>;
|
||||||
|
|
||||||
@ -74,6 +86,8 @@ export interface IAccessStore extends Store<IRole, number> {
|
|||||||
projectId?: string,
|
projectId?: string,
|
||||||
): Promise<IUserRole[]>;
|
): Promise<IUserRole[]>;
|
||||||
|
|
||||||
|
getProjectUsers(projectId?: string): Promise<IUserWithProjectRoles[]>;
|
||||||
|
|
||||||
getUserIdsForRole(roleId: number, projectId?: string): Promise<number[]>;
|
getUserIdsForRole(roleId: number, projectId?: string): Promise<number[]>;
|
||||||
|
|
||||||
getGroupIdsForRole(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,
|
projectId?: string,
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
|
|
||||||
addAccessToProject(
|
addRoleAccessToProject(
|
||||||
users: IAccessInfo[],
|
users: IAccessInfo[],
|
||||||
groups: IAccessInfo[],
|
groups: IAccessInfo[],
|
||||||
projectId: string,
|
projectId: string,
|
||||||
@ -103,6 +117,14 @@ export interface IAccessStore extends Store<IRole, number> {
|
|||||||
createdBy: string,
|
createdBy: string,
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
|
|
||||||
|
addAccessToProject(
|
||||||
|
roles: number[],
|
||||||
|
groups: number[],
|
||||||
|
users: number[],
|
||||||
|
projectId: string,
|
||||||
|
createdBy: string,
|
||||||
|
): Promise<void>;
|
||||||
|
|
||||||
removeUserFromRole(
|
removeUserFromRole(
|
||||||
userId: number,
|
userId: number,
|
||||||
roleId: number,
|
roleId: number,
|
||||||
@ -155,4 +177,26 @@ export interface IAccessStore extends Store<IRole, number> {
|
|||||||
sourceEnvironment: string,
|
sourceEnvironment: string,
|
||||||
destinationEnvironment: string,
|
destinationEnvironment: string,
|
||||||
): Promise<void>;
|
): 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,
|
IGroupRole,
|
||||||
IGroupUser,
|
IGroupUser,
|
||||||
} from '../group';
|
} from '../group';
|
||||||
|
import { IGroupWithProjectRoles } from './access-store';
|
||||||
|
|
||||||
export interface IStoreGroup {
|
export interface IStoreGroup {
|
||||||
name: string;
|
name: string;
|
||||||
@ -34,6 +35,8 @@ export interface IGroupStore extends Store<IGroup, number> {
|
|||||||
|
|
||||||
getProjectGroupRoles(projectId: string): Promise<IGroupRole[]>;
|
getProjectGroupRoles(projectId: string): Promise<IGroupRole[]>;
|
||||||
|
|
||||||
|
getProjectGroups(projectId: string): Promise<IGroupWithProjectRoles[]>;
|
||||||
|
|
||||||
getAllWithId(ids: number[]): Promise<IGroup[]>;
|
getAllWithId(ids: number[]): Promise<IGroup[]>;
|
||||||
|
|
||||||
updateGroupUsers(
|
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(
|
await projectService.addAccess(
|
||||||
project.id,
|
project.id,
|
||||||
createFeatureRole.id,
|
[createFeatureRole.id],
|
||||||
{
|
[group1.id],
|
||||||
users: [{ id: user1.id }],
|
[user1.id],
|
||||||
groups: [{ id: group1.id }],
|
|
||||||
},
|
|
||||||
'some-admin-user',
|
'some-admin-user',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -1142,11 +1140,9 @@ test('Should bulk update of only users', async () => {
|
|||||||
|
|
||||||
await projectService.addAccess(
|
await projectService.addAccess(
|
||||||
project,
|
project,
|
||||||
createFeatureRole.id,
|
[createFeatureRole.id],
|
||||||
{
|
[],
|
||||||
users: [{ id: user1.id }],
|
[user1.id],
|
||||||
groups: [],
|
|
||||||
},
|
|
||||||
'some-admin-user',
|
'some-admin-user',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -1183,15 +1179,88 @@ test('Should allow bulk update of only groups', async () => {
|
|||||||
|
|
||||||
await projectService.addAccess(
|
await projectService.addAccess(
|
||||||
project.id,
|
project.id,
|
||||||
createFeatureRole.id,
|
[createFeatureRole.id],
|
||||||
{
|
[group1.id],
|
||||||
users: [],
|
[],
|
||||||
groups: [{ id: group1.id }],
|
|
||||||
},
|
|
||||||
'some-admin-user',
|
'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 () => {
|
test('should only count active feature toggles for project', async () => {
|
||||||
const project = {
|
const project = {
|
||||||
id: 'only-active',
|
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/explicit-module-boundary-types */
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* 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 User from '../../lib/types/user';
|
||||||
import noLoggerProvider from './no-logger';
|
import noLoggerProvider from './no-logger';
|
||||||
import { IRole } from '../../lib/types/stores/access-store';
|
import { IRole } from '../../lib/types/stores/access-store';
|
||||||
import {
|
import { IAvailablePermissions } from '../../lib/types/model';
|
||||||
IAvailablePermissions,
|
|
||||||
IRoleData,
|
|
||||||
IUserWithRole,
|
|
||||||
} from '../../lib/types/model';
|
|
||||||
import { IGroupModelWithProjectRole } from '../../lib/types/group';
|
|
||||||
|
|
||||||
class AccessServiceMock extends AccessService {
|
class AccessServiceMock extends AccessService {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -78,9 +76,7 @@ class AccessServiceMock extends AccessService {
|
|||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
getProjectRoleAccess(
|
getProjectRoleAccess(projectId: string): Promise<AccessWithRoles> {
|
||||||
projectId: string,
|
|
||||||
): Promise<[IRole[], IUserWithRole[], IGroupModelWithProjectRole[]]> {
|
|
||||||
throw new Error('Method not implemented.');
|
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,
|
IRoleWithProject,
|
||||||
IUserPermission,
|
IUserPermission,
|
||||||
IUserRole,
|
IUserRole,
|
||||||
|
IUserWithProjectRoles,
|
||||||
} from '../../lib/types/stores/access-store';
|
} from '../../lib/types/stores/access-store';
|
||||||
import { IPermission } from 'lib/types/model';
|
import { IPermission } from 'lib/types/model';
|
||||||
import { IRoleStore } from 'lib/types';
|
import { IRoleStore } from 'lib/types';
|
||||||
@ -27,7 +28,7 @@ class AccessStoreMock implements IAccessStore {
|
|||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
addAccessToProject(
|
addRoleAccessToProject(
|
||||||
users: IAccessInfo[],
|
users: IAccessInfo[],
|
||||||
groups: IAccessInfo[],
|
groups: IAccessInfo[],
|
||||||
projectId: string,
|
projectId: string,
|
||||||
@ -37,6 +38,16 @@ class AccessStoreMock implements IAccessStore {
|
|||||||
throw new Error('Method not implemented.');
|
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(
|
updateGroupProjectRole(
|
||||||
userId: number,
|
userId: number,
|
||||||
roleId: number,
|
roleId: number,
|
||||||
@ -97,6 +108,10 @@ class AccessStoreMock implements IAccessStore {
|
|||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getProjectUsers(projectId?: string): Promise<IUserWithProjectRoles[]> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
getProjectRoles(): Promise<IRole[]> {
|
getProjectRoles(): Promise<IRole[]> {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
@ -198,7 +213,7 @@ class AccessStoreMock implements IAccessStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get(key: number): Promise<IRole> {
|
get(key: number): Promise<IRole> {
|
||||||
return Promise.resolve(undefined);
|
throw new Error('Not implemented yet');
|
||||||
}
|
}
|
||||||
|
|
||||||
getAll(): Promise<IRole[]> {
|
getAll(): Promise<IRole[]> {
|
||||||
@ -234,6 +249,45 @@ class AccessStoreMock implements IAccessStore {
|
|||||||
clearPublicSignupUserTokens(userId: number): Promise<void> {
|
clearPublicSignupUserTokens(userId: number): Promise<void> {
|
||||||
return Promise.resolve(undefined);
|
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;
|
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,
|
IGroupRole,
|
||||||
IGroupUser,
|
IGroupUser,
|
||||||
} from '../../lib/types/group';
|
} from '../../lib/types/group';
|
||||||
|
import { IGroupWithProjectRoles } from '../../lib/types/stores/access-store';
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
export default class FakeGroupStore implements IGroupStore {
|
export default class FakeGroupStore implements IGroupStore {
|
||||||
count(): Promise<number> {
|
count(): Promise<number> {
|
||||||
@ -83,6 +84,10 @@ export default class FakeGroupStore implements IGroupStore {
|
|||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getProjectGroups(projectId: string): Promise<IGroupWithProjectRoles[]> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
getGroupProjects(groupIds: number[]): Promise<IGroupProject[]> {
|
getGroupProjects(groupIds: number[]): Promise<IGroupProject[]> {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user