mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-31 13:47:02 +02:00
loosen permissions for change requests (#2682)
This commit is contained in:
parent
eb433185a1
commit
cb0398ca63
@ -170,6 +170,7 @@ export const ChangeRequestOverview: FC = () => {
|
|||||||
>
|
>
|
||||||
{changeRequest.approvals?.map(approver => (
|
{changeRequest.approvals?.map(approver => (
|
||||||
<ChangeRequestReviewer
|
<ChangeRequestReviewer
|
||||||
|
key={approver.createdBy.username}
|
||||||
name={
|
name={
|
||||||
approver.createdBy.username ||
|
approver.createdBy.username ||
|
||||||
'Unknown user'
|
'Unknown user'
|
||||||
|
@ -0,0 +1,242 @@
|
|||||||
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
|
import { MemoryRouter, Routes, Route } from 'react-router-dom';
|
||||||
|
import { FeatureView } from '../feature/FeatureView/FeatureView';
|
||||||
|
import { ThemeProvider } from 'themes/ThemeProvider';
|
||||||
|
import { AccessProvider } from '../providers/AccessProvider/AccessProvider';
|
||||||
|
import { AnnouncerProvider } from '../common/Announcer/AnnouncerProvider/AnnouncerProvider';
|
||||||
|
import { testServerRoute, testServerSetup } from '../../utils/testServer';
|
||||||
|
import { UIProviderContainer } from '../providers/UIProvider/UIProviderContainer';
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { IPermission } from '../../interfaces/user';
|
||||||
|
|
||||||
|
const server = testServerSetup();
|
||||||
|
|
||||||
|
const changeRequestsEnabledIn = (
|
||||||
|
env: 'development' | 'production' | 'custom'
|
||||||
|
) =>
|
||||||
|
testServerRoute(
|
||||||
|
server,
|
||||||
|
'/api/admin/projects/default/change-requests/config',
|
||||||
|
[
|
||||||
|
{
|
||||||
|
environment: 'development',
|
||||||
|
type: 'development',
|
||||||
|
requiredApprovals: null,
|
||||||
|
changeRequestEnabled: env === 'development',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
environment: 'production',
|
||||||
|
type: 'production',
|
||||||
|
requiredApprovals: 1,
|
||||||
|
changeRequestEnabled: env === 'production',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
environment: 'custom',
|
||||||
|
type: 'production',
|
||||||
|
requiredApprovals: null,
|
||||||
|
changeRequestEnabled: env === 'custom',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const uiConfigForEnterprise = () =>
|
||||||
|
testServerRoute(server, '/api/admin/ui-config', {
|
||||||
|
environment: 'Open Source',
|
||||||
|
flags: {
|
||||||
|
changeRequests: true,
|
||||||
|
},
|
||||||
|
versionInfo: {
|
||||||
|
current: { oss: '4.18.0-beta.5', enterprise: '4.17.0-beta.1' },
|
||||||
|
},
|
||||||
|
disablePasswordAuth: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const setupOtherRoutes = (feature: string) => {
|
||||||
|
testServerRoute(
|
||||||
|
server,
|
||||||
|
'api/admin/projects/default/change-requests/pending',
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
testServerRoute(server, '/api/admin/projects/default', {});
|
||||||
|
testServerRoute(server, `api/admin/client-metrics/features/${feature}`, {
|
||||||
|
version: 1,
|
||||||
|
maturity: 'stable',
|
||||||
|
featureName: feature,
|
||||||
|
lastHourUsage: [],
|
||||||
|
seenApplications: [],
|
||||||
|
});
|
||||||
|
testServerRoute(server, `api/admin/features/${feature}/tags`, {
|
||||||
|
version: 1,
|
||||||
|
tags: [],
|
||||||
|
});
|
||||||
|
testServerRoute(server, `api/admin/strategies`, {
|
||||||
|
version: 1,
|
||||||
|
strategies: [
|
||||||
|
{
|
||||||
|
displayName: 'Standard',
|
||||||
|
name: 'default',
|
||||||
|
editable: false,
|
||||||
|
description:
|
||||||
|
'The standard strategy is strictly on / off for your entire userbase.',
|
||||||
|
parameters: [],
|
||||||
|
deprecated: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'UserIDs',
|
||||||
|
name: 'userWithId',
|
||||||
|
editable: false,
|
||||||
|
description:
|
||||||
|
'Enable the feature for a specific set of userIds.',
|
||||||
|
parameters: [
|
||||||
|
{
|
||||||
|
name: 'userIds',
|
||||||
|
type: 'list',
|
||||||
|
description: '',
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
deprecated: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const userHasPermissions = (permissions: Array<IPermission>) => {
|
||||||
|
testServerRoute(server, 'api/admin/user', {
|
||||||
|
user: {
|
||||||
|
isAPI: false,
|
||||||
|
id: 2,
|
||||||
|
name: 'Test',
|
||||||
|
email: 'test@getunleash.ai',
|
||||||
|
imageUrl:
|
||||||
|
'https://gravatar.com/avatar/e55646b526ff342ff8b43721f0cbdd8e?size=42&default=retro',
|
||||||
|
seenAt: '2022-11-29T08:21:52.581Z',
|
||||||
|
loginAttempts: 0,
|
||||||
|
createdAt: '2022-11-21T10:10:33.074Z',
|
||||||
|
},
|
||||||
|
permissions,
|
||||||
|
feedback: [],
|
||||||
|
splash: {},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const featureEnvironments = (
|
||||||
|
feature: string,
|
||||||
|
environments: Array<{ name: string; strategies: Array<string> }>
|
||||||
|
) => {
|
||||||
|
testServerRoute(server, `/api/admin/projects/default/features/${feature}`, {
|
||||||
|
environments: environments.map(env => ({
|
||||||
|
name: env.name,
|
||||||
|
enabled: false,
|
||||||
|
type: 'production',
|
||||||
|
sortOrder: 1,
|
||||||
|
strategies: env.strategies.map(strategy => ({
|
||||||
|
name: strategy,
|
||||||
|
id: Math.random(),
|
||||||
|
constraints: [],
|
||||||
|
parameters: [],
|
||||||
|
sortOrder: 1,
|
||||||
|
})),
|
||||||
|
})),
|
||||||
|
name: feature,
|
||||||
|
impressionData: false,
|
||||||
|
description: '',
|
||||||
|
project: 'default',
|
||||||
|
stale: false,
|
||||||
|
variants: [],
|
||||||
|
createdAt: '2022-11-14T08:16:33.338Z',
|
||||||
|
lastSeenAt: null,
|
||||||
|
type: 'release',
|
||||||
|
archived: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const UnleashUiSetup: FC<{ path: string; pathTemplate: string }> = ({
|
||||||
|
children,
|
||||||
|
path,
|
||||||
|
pathTemplate,
|
||||||
|
}) => (
|
||||||
|
<UIProviderContainer>
|
||||||
|
<AccessProvider>
|
||||||
|
<MemoryRouter initialEntries={[path]}>
|
||||||
|
<ThemeProvider>
|
||||||
|
<AnnouncerProvider>
|
||||||
|
<Routes>
|
||||||
|
<Route path={pathTemplate} element={children} />
|
||||||
|
</Routes>
|
||||||
|
</AnnouncerProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</MemoryRouter>
|
||||||
|
</AccessProvider>
|
||||||
|
</UIProviderContainer>
|
||||||
|
);
|
||||||
|
|
||||||
|
const strategiesAreDisplayed = async (
|
||||||
|
firstStrategy: string,
|
||||||
|
secondStrategy: string
|
||||||
|
) => {
|
||||||
|
await screen.findByText(firstStrategy);
|
||||||
|
await screen.findByText(secondStrategy);
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteButtonsActiveInChangeRequestEnv = async () => {
|
||||||
|
const deleteButtons = screen.getAllByTestId('STRATEGY_FORM_REMOVE_ID');
|
||||||
|
expect(deleteButtons.length).toBe(2);
|
||||||
|
|
||||||
|
// wait for change request config to be loaded
|
||||||
|
await waitFor(() => {
|
||||||
|
// production
|
||||||
|
const productionStrategyDeleteButton = deleteButtons[0];
|
||||||
|
expect(productionStrategyDeleteButton).not.toBeDisabled();
|
||||||
|
|
||||||
|
// custom env
|
||||||
|
const customEnvStrategyDeleteButton = deleteButtons[1];
|
||||||
|
expect(customEnvStrategyDeleteButton).toBeDisabled();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyButtonsActiveInOtherEnv = async () => {
|
||||||
|
const copyButtons = screen.getAllByTestId('STRATEGY_FORM_COPY_ID');
|
||||||
|
expect(copyButtons.length).toBe(2);
|
||||||
|
|
||||||
|
// production
|
||||||
|
const productionStrategyCopyButton = copyButtons[0];
|
||||||
|
expect(productionStrategyCopyButton).toBeDisabled();
|
||||||
|
|
||||||
|
// custom env
|
||||||
|
const customEnvStrategyCopyButton = copyButtons[1];
|
||||||
|
expect(customEnvStrategyCopyButton).not.toBeDisabled();
|
||||||
|
};
|
||||||
|
|
||||||
|
test('user without priviledges can only perform change requests actions', async () => {
|
||||||
|
const project = 'default';
|
||||||
|
const featureName = 'test';
|
||||||
|
featureEnvironments(featureName, [
|
||||||
|
{ name: 'development', strategies: [] },
|
||||||
|
{ name: 'production', strategies: ['userWithId'] },
|
||||||
|
{ name: 'custom', strategies: ['default'] },
|
||||||
|
]);
|
||||||
|
userHasPermissions([
|
||||||
|
{
|
||||||
|
project,
|
||||||
|
environment: 'production',
|
||||||
|
permission: 'APPLY_CHANGE_REQUEST',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
changeRequestsEnabledIn('production');
|
||||||
|
uiConfigForEnterprise();
|
||||||
|
setupOtherRoutes(featureName);
|
||||||
|
|
||||||
|
render(
|
||||||
|
<UnleashUiSetup
|
||||||
|
pathTemplate="/projects/:projectId/features/:featureId/*"
|
||||||
|
path={`/projects/${project}/features/${featureName}`}
|
||||||
|
>
|
||||||
|
<FeatureView />
|
||||||
|
</UnleashUiSetup>
|
||||||
|
);
|
||||||
|
|
||||||
|
await strategiesAreDisplayed('UserIDs', 'Standard');
|
||||||
|
await deleteButtonsActiveInChangeRequestEnv();
|
||||||
|
await copyButtonsActiveInOtherEnv();
|
||||||
|
});
|
@ -1,7 +1,6 @@
|
|||||||
import { Button, ButtonProps } from '@mui/material';
|
import { Button, ButtonProps } from '@mui/material';
|
||||||
import { Lock } from '@mui/icons-material';
|
import { Lock } from '@mui/icons-material';
|
||||||
import AccessContext from 'contexts/AccessContext';
|
import React from 'react';
|
||||||
import React, { useContext } from 'react';
|
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import {
|
import {
|
||||||
TooltipResolver,
|
TooltipResolver,
|
||||||
@ -9,6 +8,10 @@ import {
|
|||||||
} from 'component/common/TooltipResolver/TooltipResolver';
|
} from 'component/common/TooltipResolver/TooltipResolver';
|
||||||
import { formatAccessText } from 'utils/formatAccessText';
|
import { formatAccessText } from 'utils/formatAccessText';
|
||||||
import { useId } from 'hooks/useId';
|
import { useId } from 'hooks/useId';
|
||||||
|
import {
|
||||||
|
useHasRootAccess,
|
||||||
|
useHasProjectEnvironmentAccess,
|
||||||
|
} from 'hooks/useHasAccess';
|
||||||
|
|
||||||
export interface IPermissionButtonProps extends Omit<ButtonProps, 'title'> {
|
export interface IPermissionButtonProps extends Omit<ButtonProps, 'title'> {
|
||||||
permission: string | string[];
|
permission: string | string[];
|
||||||
@ -19,10 +22,44 @@ export interface IPermissionButtonProps extends Omit<ButtonProps, 'title'> {
|
|||||||
tooltipProps?: Omit<ITooltipResolverProps, 'children'>;
|
tooltipProps?: Omit<ITooltipResolverProps, 'children'>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PermissionButton: React.FC<IPermissionButtonProps> = React.forwardRef(
|
interface IPermissionBaseButtonProps extends IPermissionButtonProps {
|
||||||
|
access: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IProjectPermissionButtonProps extends IPermissionButtonProps {
|
||||||
|
projectId: string;
|
||||||
|
environmentId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProjectEnvironmentPermissionButton: React.FC<IProjectPermissionButtonProps> =
|
||||||
|
React.forwardRef((props, ref) => {
|
||||||
|
const access = useHasProjectEnvironmentAccess(
|
||||||
|
props.permission,
|
||||||
|
props.environmentId,
|
||||||
|
props.projectId
|
||||||
|
);
|
||||||
|
|
||||||
|
return <BasePermissionButton {...props} access={access} ref={ref} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
const RootPermissionButton: React.FC<IPermissionButtonProps> = React.forwardRef(
|
||||||
|
(props, ref) => {
|
||||||
|
const access = useHasRootAccess(
|
||||||
|
props.permission,
|
||||||
|
props.environmentId,
|
||||||
|
props.projectId
|
||||||
|
);
|
||||||
|
|
||||||
|
return <BasePermissionButton {...props} access={access} ref={ref} />;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const BasePermissionButton: React.FC<IPermissionBaseButtonProps> =
|
||||||
|
React.forwardRef(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
permission,
|
permission,
|
||||||
|
access,
|
||||||
variant = 'contained',
|
variant = 'contained',
|
||||||
color = 'primary',
|
color = 'primary',
|
||||||
onClick,
|
onClick,
|
||||||
@ -35,36 +72,7 @@ const PermissionButton: React.FC<IPermissionButtonProps> = React.forwardRef(
|
|||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const { hasAccess } = useContext(AccessContext);
|
|
||||||
const id = useId();
|
const id = useId();
|
||||||
let access;
|
|
||||||
|
|
||||||
const handleAccess = () => {
|
|
||||||
let access;
|
|
||||||
if (Array.isArray(permission)) {
|
|
||||||
access = permission.some(permission => {
|
|
||||||
if (projectId && environmentId) {
|
|
||||||
return hasAccess(permission, projectId, environmentId);
|
|
||||||
} else if (projectId) {
|
|
||||||
return hasAccess(permission, projectId);
|
|
||||||
} else {
|
|
||||||
return hasAccess(permission);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if (projectId && environmentId) {
|
|
||||||
access = hasAccess(permission, projectId, environmentId);
|
|
||||||
} else if (projectId) {
|
|
||||||
access = hasAccess(permission, projectId);
|
|
||||||
} else {
|
|
||||||
access = hasAccess(permission);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return access;
|
|
||||||
};
|
|
||||||
|
|
||||||
access = handleAccess();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipResolver
|
<TooltipResolver
|
||||||
@ -87,7 +95,8 @@ const PermissionButton: React.FC<IPermissionButtonProps> = React.forwardRef(
|
|||||||
condition={!access}
|
condition={!access}
|
||||||
show={<Lock titleAccess="Locked" />}
|
show={<Lock titleAccess="Locked" />}
|
||||||
elseShow={
|
elseShow={
|
||||||
Boolean(rest.endIcon) && rest.endIcon
|
Boolean(rest.endIcon) &&
|
||||||
|
rest.endIcon
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
@ -101,4 +110,23 @@ const PermissionButton: React.FC<IPermissionButtonProps> = React.forwardRef(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const PermissionButton: React.FC<IPermissionButtonProps> = React.forwardRef(
|
||||||
|
(props, ref) => {
|
||||||
|
if (
|
||||||
|
typeof props.projectId !== 'undefined' &&
|
||||||
|
typeof props.environmentId !== 'undefined'
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<ProjectEnvironmentPermissionButton
|
||||||
|
{...props}
|
||||||
|
environmentId={props.environmentId}
|
||||||
|
projectId={props.projectId}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <RootPermissionButton {...props} ref={ref} />;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export default PermissionButton;
|
export default PermissionButton;
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { IconButton, IconButtonProps } from '@mui/material';
|
import { IconButton, IconButtonProps } from '@mui/material';
|
||||||
import React, { ReactNode, useContext } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import AccessContext from 'contexts/AccessContext';
|
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
ITooltipResolverProps,
|
ITooltipResolverProps,
|
||||||
@ -8,6 +7,10 @@ import {
|
|||||||
} from 'component/common/TooltipResolver/TooltipResolver';
|
} from 'component/common/TooltipResolver/TooltipResolver';
|
||||||
import { formatAccessText } from 'utils/formatAccessText';
|
import { formatAccessText } from 'utils/formatAccessText';
|
||||||
import { useId } from 'hooks/useId';
|
import { useId } from 'hooks/useId';
|
||||||
|
import {
|
||||||
|
useHasProjectEnvironmentAccess,
|
||||||
|
useHasRootAccess,
|
||||||
|
} from 'hooks/useHasAccess';
|
||||||
|
|
||||||
interface IPermissionIconButtonProps {
|
interface IPermissionIconButtonProps {
|
||||||
permission: string;
|
permission: string;
|
||||||
@ -33,7 +36,33 @@ interface ILinkProps extends IPermissionIconButtonProps {
|
|||||||
to: string;
|
to: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PermissionIconButton = ({
|
const RootPermissionIconButton = (props: IButtonProps | ILinkProps) => {
|
||||||
|
const access = useHasRootAccess(
|
||||||
|
props.permission,
|
||||||
|
props.environmentId,
|
||||||
|
props.projectId
|
||||||
|
);
|
||||||
|
|
||||||
|
return <BasePermissionIconButton {...props} access={access} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ProjectEnvironmentPermissionIconButton = (
|
||||||
|
props: (IButtonProps | ILinkProps) & {
|
||||||
|
environmentId: string;
|
||||||
|
projectId: string;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const access = useHasProjectEnvironmentAccess(
|
||||||
|
props.permission,
|
||||||
|
props.environmentId,
|
||||||
|
props.projectId
|
||||||
|
);
|
||||||
|
|
||||||
|
return <BasePermissionIconButton {...props} access={access} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const BasePermissionIconButton = ({
|
||||||
|
access,
|
||||||
permission,
|
permission,
|
||||||
projectId,
|
projectId,
|
||||||
children,
|
children,
|
||||||
@ -41,18 +70,8 @@ const PermissionIconButton = ({
|
|||||||
tooltipProps,
|
tooltipProps,
|
||||||
disabled,
|
disabled,
|
||||||
...rest
|
...rest
|
||||||
}: IButtonProps | ILinkProps) => {
|
}: (IButtonProps | ILinkProps) & { access: boolean }) => {
|
||||||
const { hasAccess } = useContext(AccessContext);
|
|
||||||
const id = useId();
|
const id = useId();
|
||||||
let access;
|
|
||||||
|
|
||||||
if (projectId && environmentId) {
|
|
||||||
access = hasAccess(permission, projectId, environmentId);
|
|
||||||
} else if (projectId) {
|
|
||||||
access = hasAccess(permission, projectId);
|
|
||||||
} else {
|
|
||||||
access = hasAccess(permission);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipResolver
|
<TooltipResolver
|
||||||
@ -75,4 +94,20 @@ const PermissionIconButton = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const PermissionIconButton = (props: IButtonProps | ILinkProps) => {
|
||||||
|
if (
|
||||||
|
typeof props.projectId !== 'undefined' &&
|
||||||
|
typeof props.environmentId !== 'undefined'
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<ProjectEnvironmentPermissionIconButton
|
||||||
|
{...props}
|
||||||
|
projectId={props.projectId}
|
||||||
|
environmentId={props.environmentId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <RootPermissionIconButton {...props} />;
|
||||||
|
};
|
||||||
|
|
||||||
export default PermissionIconButton;
|
export default PermissionIconButton;
|
||||||
|
@ -1,8 +1,11 @@
|
|||||||
import { Switch, SwitchProps } from '@mui/material';
|
import { Switch, SwitchProps } from '@mui/material';
|
||||||
import AccessContext from 'contexts/AccessContext';
|
import React from 'react';
|
||||||
import React, { useContext } from 'react';
|
|
||||||
import { formatAccessText } from 'utils/formatAccessText';
|
import { formatAccessText } from 'utils/formatAccessText';
|
||||||
import { TooltipResolver } from 'component/common/TooltipResolver/TooltipResolver';
|
import { TooltipResolver } from 'component/common/TooltipResolver/TooltipResolver';
|
||||||
|
import {
|
||||||
|
useHasProjectEnvironmentAccess,
|
||||||
|
useHasRootAccess,
|
||||||
|
} from 'hooks/useHasAccess';
|
||||||
|
|
||||||
interface IPermissionSwitchProps extends SwitchProps {
|
interface IPermissionSwitchProps extends SwitchProps {
|
||||||
permission: string;
|
permission: string;
|
||||||
@ -14,11 +17,42 @@ interface IPermissionSwitchProps extends SwitchProps {
|
|||||||
checked: boolean;
|
checked: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PermissionSwitch = React.forwardRef<
|
interface IBasePermissionSwitchProps extends IPermissionSwitchProps {
|
||||||
|
access: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProjectenvironmentPermissionSwitch = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
IPermissionSwitchProps & { projectId: string; environmentId: string }
|
||||||
|
>((props, ref) => {
|
||||||
|
const access = useHasProjectEnvironmentAccess(
|
||||||
|
props.permission,
|
||||||
|
props.environmentId,
|
||||||
|
props.projectId
|
||||||
|
);
|
||||||
|
|
||||||
|
return <BasePermissionSwitch {...props} access={access} ref={ref} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
const RootPermissionSwitch = React.forwardRef<
|
||||||
HTMLButtonElement,
|
HTMLButtonElement,
|
||||||
IPermissionSwitchProps
|
IPermissionSwitchProps
|
||||||
|
>((props, ref) => {
|
||||||
|
const access = useHasRootAccess(
|
||||||
|
props.permission,
|
||||||
|
props.environmentId,
|
||||||
|
props.projectId
|
||||||
|
);
|
||||||
|
|
||||||
|
return <BasePermissionSwitch {...props} access={access} ref={ref} />;
|
||||||
|
});
|
||||||
|
|
||||||
|
const BasePermissionSwitch = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
IBasePermissionSwitchProps
|
||||||
>((props, ref) => {
|
>((props, ref) => {
|
||||||
const {
|
const {
|
||||||
|
access,
|
||||||
permission,
|
permission,
|
||||||
tooltip,
|
tooltip,
|
||||||
disabled,
|
disabled,
|
||||||
@ -29,17 +63,6 @@ const PermissionSwitch = React.forwardRef<
|
|||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const { hasAccess } = useContext(AccessContext);
|
|
||||||
|
|
||||||
let access;
|
|
||||||
if (projectId && environmentId) {
|
|
||||||
access = hasAccess(permission, projectId, environmentId);
|
|
||||||
} else if (projectId) {
|
|
||||||
access = hasAccess(permission, projectId);
|
|
||||||
} else {
|
|
||||||
access = hasAccess(permission);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipResolver title={formatAccessText(access, tooltip)} arrow>
|
<TooltipResolver title={formatAccessText(access, tooltip)} arrow>
|
||||||
<span data-loading>
|
<span data-loading>
|
||||||
@ -56,4 +79,24 @@ const PermissionSwitch = React.forwardRef<
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const PermissionSwitch = React.forwardRef<
|
||||||
|
HTMLButtonElement,
|
||||||
|
IPermissionSwitchProps
|
||||||
|
>((props, ref) => {
|
||||||
|
if (
|
||||||
|
typeof props.projectId !== 'undefined' &&
|
||||||
|
typeof props.environmentId !== 'undefined'
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<ProjectenvironmentPermissionSwitch
|
||||||
|
{...props}
|
||||||
|
projectId={props.projectId}
|
||||||
|
environmentId={props.environmentId}
|
||||||
|
ref={ref}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <RootPermissionSwitch {...props} ref={ref} />;
|
||||||
|
});
|
||||||
|
|
||||||
export default PermissionSwitch;
|
export default PermissionSwitch;
|
||||||
|
@ -150,6 +150,7 @@ export const FeatureStrategyCreate = () => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<FeatureStrategyForm
|
<FeatureStrategyForm
|
||||||
|
projectId={projectId}
|
||||||
feature={data}
|
feature={data}
|
||||||
strategy={strategy}
|
strategy={strategy}
|
||||||
setStrategy={setStrategy}
|
setStrategy={setStrategy}
|
||||||
|
@ -166,6 +166,7 @@ export const FeatureStrategyEdit = () => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<FeatureStrategyForm
|
<FeatureStrategyForm
|
||||||
|
projectId={projectId}
|
||||||
feature={data}
|
feature={data}
|
||||||
strategy={strategy}
|
strategy={strategy}
|
||||||
setStrategy={setStrategy}
|
setStrategy={setStrategy}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, useContext } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Alert, Button } from '@mui/material';
|
import { Alert, Button } from '@mui/material';
|
||||||
import {
|
import {
|
||||||
@ -15,7 +15,6 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
|||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { STRATEGY_FORM_SUBMIT_ID } from 'utils/testIds';
|
import { STRATEGY_FORM_SUBMIT_ID } from 'utils/testIds';
|
||||||
import { useConstraintsValidation } from 'hooks/api/getters/useConstraintsValidation/useConstraintsValidation';
|
import { useConstraintsValidation } from 'hooks/api/getters/useConstraintsValidation/useConstraintsValidation';
|
||||||
import AccessContext from 'contexts/AccessContext';
|
|
||||||
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
||||||
import { FeatureStrategySegment } from 'component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegment';
|
import { FeatureStrategySegment } from 'component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegment';
|
||||||
import { ISegment } from 'interfaces/segment';
|
import { ISegment } from 'interfaces/segment';
|
||||||
@ -30,9 +29,11 @@ import {
|
|||||||
import { formatFeaturePath } from '../FeatureStrategyEdit/FeatureStrategyEdit';
|
import { formatFeaturePath } from '../FeatureStrategyEdit/FeatureStrategyEdit';
|
||||||
import { useChangeRequestInReviewWarning } from 'hooks/useChangeRequestInReviewWarning';
|
import { useChangeRequestInReviewWarning } from 'hooks/useChangeRequestInReviewWarning';
|
||||||
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
|
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
|
||||||
|
import { useHasProjectEnvironmentAccess } from 'hooks/useHasAccess';
|
||||||
|
|
||||||
interface IFeatureStrategyFormProps {
|
interface IFeatureStrategyFormProps {
|
||||||
feature: IFeatureToggle;
|
feature: IFeatureToggle;
|
||||||
|
projectId: string;
|
||||||
environmentId: string;
|
environmentId: string;
|
||||||
permission: string;
|
permission: string;
|
||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
@ -48,6 +49,7 @@ interface IFeatureStrategyFormProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const FeatureStrategyForm = ({
|
export const FeatureStrategyForm = ({
|
||||||
|
projectId,
|
||||||
feature,
|
feature,
|
||||||
environmentId,
|
environmentId,
|
||||||
permission,
|
permission,
|
||||||
@ -64,7 +66,11 @@ export const FeatureStrategyForm = ({
|
|||||||
const [showProdGuard, setShowProdGuard] = useState(false);
|
const [showProdGuard, setShowProdGuard] = useState(false);
|
||||||
const hasValidConstraints = useConstraintsValidation(strategy.constraints);
|
const hasValidConstraints = useConstraintsValidation(strategy.constraints);
|
||||||
const enableProdGuard = useFeatureStrategyProdGuard(feature, environmentId);
|
const enableProdGuard = useFeatureStrategyProdGuard(feature, environmentId);
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const access = useHasProjectEnvironmentAccess(
|
||||||
|
permission,
|
||||||
|
environmentId,
|
||||||
|
projectId
|
||||||
|
);
|
||||||
const { strategyDefinition } = useStrategy(strategy?.name);
|
const { strategyDefinition } = useStrategy(strategy?.name);
|
||||||
|
|
||||||
const { data } = usePendingChangeRequests(feature.project);
|
const { data } = usePendingChangeRequests(feature.project);
|
||||||
@ -205,11 +211,7 @@ export const FeatureStrategyForm = ({
|
|||||||
setStrategy={setStrategy}
|
setStrategy={setStrategy}
|
||||||
validateParameter={validateParameter}
|
validateParameter={validateParameter}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
hasAccess={hasAccess(
|
hasAccess={access}
|
||||||
permission,
|
|
||||||
feature.project,
|
|
||||||
environmentId
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
<hr className={styles.hr} />
|
<hr className={styles.hr} />
|
||||||
<div className={styles.buttons}>
|
<div className={styles.buttons}>
|
||||||
|
@ -11,7 +11,6 @@ import { AddToPhotos as CopyIcon, Lock } from '@mui/icons-material';
|
|||||||
import { IFeatureStrategyPayload } from 'interfaces/strategy';
|
import { IFeatureStrategyPayload } from 'interfaces/strategy';
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { IFeatureEnvironment } from 'interfaces/featureToggle';
|
import { IFeatureEnvironment } from 'interfaces/featureToggle';
|
||||||
import AccessContext from 'contexts/AccessContext';
|
|
||||||
import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
|
import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||||
@ -20,9 +19,11 @@ import useToast from 'hooks/useToast';
|
|||||||
import { useFeatureImmutable } from 'hooks/api/getters/useFeature/useFeatureImmutable';
|
import { useFeatureImmutable } from 'hooks/api/getters/useFeature/useFeatureImmutable';
|
||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
import { useChangeRequestAddStrategy } from 'hooks/useChangeRequestAddStrategy';
|
import { useChangeRequestAddStrategy } from 'hooks/useChangeRequestAddStrategy';
|
||||||
import { ChangeRequestDialogue } from '../../../../../../../../../changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog';
|
import { ChangeRequestDialogue } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog';
|
||||||
import { CopyStrategyMessage } from '../../../../../../../../../changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/CopyStrategyMessage';
|
import { CopyStrategyMessage } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/CopyStrategyMessage';
|
||||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||||
|
import { useCheckProjectAccess } from 'hooks/useHasAccess';
|
||||||
|
import { STRATEGY_FORM_COPY_ID } from 'utils/testIds';
|
||||||
|
|
||||||
interface ICopyStrategyIconMenuProps {
|
interface ICopyStrategyIconMenuProps {
|
||||||
environmentId: string;
|
environmentId: string;
|
||||||
@ -50,7 +51,7 @@ export const CopyStrategyIconMenu: VFC<ICopyStrategyIconMenuProps> = ({
|
|||||||
const onClose = () => {
|
const onClose = () => {
|
||||||
setAnchorEl(null);
|
setAnchorEl(null);
|
||||||
};
|
};
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const checkAccess = useCheckProjectAccess(projectId);
|
||||||
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -98,7 +99,7 @@ export const CopyStrategyIconMenu: VFC<ICopyStrategyIconMenuProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const enabled = environments.some(environment =>
|
const enabled = environments.some(environment =>
|
||||||
hasAccess(CREATE_FEATURE_STRATEGY, projectId, environment)
|
checkAccess(CREATE_FEATURE_STRATEGY, environment)
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -132,6 +133,7 @@ export const CopyStrategyIconMenu: VFC<ICopyStrategyIconMenuProps> = ({
|
|||||||
onClick={(event: MouseEvent<HTMLButtonElement>) => {
|
onClick={(event: MouseEvent<HTMLButtonElement>) => {
|
||||||
setAnchorEl(event.currentTarget);
|
setAnchorEl(event.currentTarget);
|
||||||
}}
|
}}
|
||||||
|
data-testid={STRATEGY_FORM_COPY_ID}
|
||||||
disabled={!enabled}
|
disabled={!enabled}
|
||||||
>
|
>
|
||||||
<CopyIcon />
|
<CopyIcon />
|
||||||
@ -148,9 +150,8 @@ export const CopyStrategyIconMenu: VFC<ICopyStrategyIconMenuProps> = ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{environments.map(environment => {
|
{environments.map(environment => {
|
||||||
const access = hasAccess(
|
const access = checkAccess(
|
||||||
CREATE_FEATURE_STRATEGY,
|
CREATE_FEATURE_STRATEGY,
|
||||||
projectId,
|
|
||||||
environment
|
environment
|
||||||
);
|
);
|
||||||
|
|
||||||
|
96
frontend/src/hooks/useHasAccess.ts
Normal file
96
frontend/src/hooks/useHasAccess.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import AccessContext from '../contexts/AccessContext';
|
||||||
|
import { useChangeRequestsEnabled } from './useChangeRequestsEnabled';
|
||||||
|
import {
|
||||||
|
CREATE_FEATURE_STRATEGY,
|
||||||
|
UPDATE_FEATURE_STRATEGY,
|
||||||
|
DELETE_FEATURE_STRATEGY,
|
||||||
|
UPDATE_FEATURE_ENVIRONMENT,
|
||||||
|
} from '../component/providers/AccessProvider/permissions';
|
||||||
|
|
||||||
|
const useCheckProjectPermissions = (projectId?: string) => {
|
||||||
|
const { hasAccess } = useContext(AccessContext);
|
||||||
|
|
||||||
|
const checkPermission = (
|
||||||
|
permission: string,
|
||||||
|
projectId?: string,
|
||||||
|
environmentId?: string
|
||||||
|
) => {
|
||||||
|
if (projectId && environmentId) {
|
||||||
|
return hasAccess(permission, projectId, environmentId);
|
||||||
|
} else if (projectId) {
|
||||||
|
return hasAccess(permission, projectId);
|
||||||
|
} else {
|
||||||
|
return hasAccess(permission);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkPermissions = (
|
||||||
|
permissions: string | string[],
|
||||||
|
projectId?: string,
|
||||||
|
environmentId?: string
|
||||||
|
) => {
|
||||||
|
if (Array.isArray(permissions)) {
|
||||||
|
return permissions.some(permission =>
|
||||||
|
checkPermission(permission, projectId, environmentId)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return checkPermission(permissions, projectId, environmentId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (permissions: string | string[], environmentId?: string) => {
|
||||||
|
return checkPermissions(permissions, projectId, environmentId);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useCheckProjectAccess = (projectId: string) => {
|
||||||
|
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
||||||
|
const checkAccess = useCheckProjectPermissions(projectId);
|
||||||
|
|
||||||
|
return (permission: string, environment: string) => {
|
||||||
|
return (
|
||||||
|
isChangeRequestConfigured(environment) ||
|
||||||
|
checkAccess(permission, environment)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const ALLOWED_CHANGE_REQUEST_PERMISSIONS = [
|
||||||
|
CREATE_FEATURE_STRATEGY,
|
||||||
|
UPDATE_FEATURE_STRATEGY,
|
||||||
|
DELETE_FEATURE_STRATEGY,
|
||||||
|
UPDATE_FEATURE_ENVIRONMENT,
|
||||||
|
];
|
||||||
|
|
||||||
|
const intersect = (array1: string[], array2: string[]) => {
|
||||||
|
return array1.filter(value => array2.includes(value)).length > 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useHasProjectEnvironmentAccess = (
|
||||||
|
permission: string | string[],
|
||||||
|
environmentId: string,
|
||||||
|
projectId: string
|
||||||
|
) => {
|
||||||
|
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
|
||||||
|
const checkAccess = useCheckProjectPermissions(projectId);
|
||||||
|
const changeRequestMode = isChangeRequestConfigured(environmentId);
|
||||||
|
const emptyArray: string[] = [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
(changeRequestMode &&
|
||||||
|
intersect(
|
||||||
|
ALLOWED_CHANGE_REQUEST_PERMISSIONS,
|
||||||
|
emptyArray.concat(permission)
|
||||||
|
)) ||
|
||||||
|
checkAccess(permission, environmentId)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useHasRootAccess = (
|
||||||
|
permissions: string | string[],
|
||||||
|
environmentId?: string,
|
||||||
|
projectId?: string
|
||||||
|
) => {
|
||||||
|
return useCheckProjectPermissions(projectId)(permissions, environmentId);
|
||||||
|
};
|
@ -61,6 +61,7 @@ export const STRATEGY_INPUT_LIST = 'STRATEGY_INPUT_LIST';
|
|||||||
export const ADD_TO_STRATEGY_INPUT_LIST = 'ADD_TO_STRATEGY_INPUT_LIST';
|
export const ADD_TO_STRATEGY_INPUT_LIST = 'ADD_TO_STRATEGY_INPUT_LIST';
|
||||||
export const STRATEGY_FORM_SUBMIT_ID = 'STRATEGY_FORM_SUBMIT_ID';
|
export const STRATEGY_FORM_SUBMIT_ID = 'STRATEGY_FORM_SUBMIT_ID';
|
||||||
export const STRATEGY_FORM_REMOVE_ID = 'STRATEGY_FORM_REMOVE_ID';
|
export const STRATEGY_FORM_REMOVE_ID = 'STRATEGY_FORM_REMOVE_ID';
|
||||||
|
export const STRATEGY_FORM_COPY_ID = 'STRATEGY_FORM_COPY_ID';
|
||||||
|
|
||||||
/* SPLASH */
|
/* SPLASH */
|
||||||
export const CLOSE_SPLASH = 'CLOSE_SPLASH';
|
export const CLOSE_SPLASH = 'CLOSE_SPLASH';
|
||||||
|
Loading…
Reference in New Issue
Block a user