1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +02:00

loosen permissions for change requests (#2682)

This commit is contained in:
Mateusz Kwasniewski 2022-12-14 10:00:14 +01:00 committed by GitHub
parent eb433185a1
commit cb0398ca63
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 572 additions and 121 deletions

View File

@ -170,6 +170,7 @@ export const ChangeRequestOverview: FC = () => {
>
{changeRequest.approvals?.map(approver => (
<ChangeRequestReviewer
key={approver.createdBy.username}
name={
approver.createdBy.username ||
'Unknown user'

View File

@ -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();
});

View File

@ -1,7 +1,6 @@
import { Button, ButtonProps } from '@mui/material';
import { Lock } from '@mui/icons-material';
import AccessContext from 'contexts/AccessContext';
import React, { useContext } from 'react';
import React from 'react';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import {
TooltipResolver,
@ -9,6 +8,10 @@ import {
} from 'component/common/TooltipResolver/TooltipResolver';
import { formatAccessText } from 'utils/formatAccessText';
import { useId } from 'hooks/useId';
import {
useHasRootAccess,
useHasProjectEnvironmentAccess,
} from 'hooks/useHasAccess';
export interface IPermissionButtonProps extends Omit<ButtonProps, 'title'> {
permission: string | string[];
@ -19,85 +22,110 @@ export interface IPermissionButtonProps extends Omit<ButtonProps, 'title'> {
tooltipProps?: Omit<ITooltipResolverProps, 'children'>;
}
const PermissionButton: React.FC<IPermissionButtonProps> = React.forwardRef(
(
{
permission,
variant = 'contained',
color = 'primary',
onClick,
children,
disabled,
projectId,
environmentId,
tooltipProps,
...rest
},
ref
) => {
const { hasAccess } = useContext(AccessContext);
const id = useId();
let access;
interface IPermissionBaseButtonProps extends IPermissionButtonProps {
access: boolean;
}
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);
}
}
export interface IProjectPermissionButtonProps extends IPermissionButtonProps {
projectId: string;
environmentId: string;
}
return access;
};
access = handleAccess();
return (
<TooltipResolver
{...tooltipProps}
title={formatAccessText(access, tooltipProps?.title)}
arrow
>
<span id={id}>
<Button
ref={ref}
onClick={onClick}
disabled={disabled || !access}
aria-labelledby={id}
variant={variant}
color={color}
{...rest}
endIcon={
<>
<ConditionallyRender
condition={!access}
show={<Lock titleAccess="Locked" />}
elseShow={
Boolean(rest.endIcon) && rest.endIcon
}
/>
</>
}
>
{children}
</Button>
</span>
</TooltipResolver>
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,
access,
variant = 'contained',
color = 'primary',
onClick,
children,
disabled,
projectId,
environmentId,
tooltipProps,
...rest
},
ref
) => {
const id = useId();
return (
<TooltipResolver
{...tooltipProps}
title={formatAccessText(access, tooltipProps?.title)}
arrow
>
<span id={id}>
<Button
ref={ref}
onClick={onClick}
disabled={disabled || !access}
aria-labelledby={id}
variant={variant}
color={color}
{...rest}
endIcon={
<>
<ConditionallyRender
condition={!access}
show={<Lock titleAccess="Locked" />}
elseShow={
Boolean(rest.endIcon) &&
rest.endIcon
}
/>
</>
}
>
{children}
</Button>
</span>
</TooltipResolver>
);
}
);
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} />;
}
);

View File

@ -1,6 +1,5 @@
import { IconButton, IconButtonProps } from '@mui/material';
import React, { ReactNode, useContext } from 'react';
import AccessContext from 'contexts/AccessContext';
import React, { ReactNode } from 'react';
import { Link } from 'react-router-dom';
import {
ITooltipResolverProps,
@ -8,6 +7,10 @@ import {
} from 'component/common/TooltipResolver/TooltipResolver';
import { formatAccessText } from 'utils/formatAccessText';
import { useId } from 'hooks/useId';
import {
useHasProjectEnvironmentAccess,
useHasRootAccess,
} from 'hooks/useHasAccess';
interface IPermissionIconButtonProps {
permission: string;
@ -33,7 +36,33 @@ interface ILinkProps extends IPermissionIconButtonProps {
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,
projectId,
children,
@ -41,18 +70,8 @@ const PermissionIconButton = ({
tooltipProps,
disabled,
...rest
}: IButtonProps | ILinkProps) => {
const { hasAccess } = useContext(AccessContext);
}: (IButtonProps | ILinkProps) & { access: boolean }) => {
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 (
<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;

View File

@ -1,8 +1,11 @@
import { Switch, SwitchProps } from '@mui/material';
import AccessContext from 'contexts/AccessContext';
import React, { useContext } from 'react';
import React from 'react';
import { formatAccessText } from 'utils/formatAccessText';
import { TooltipResolver } from 'component/common/TooltipResolver/TooltipResolver';
import {
useHasProjectEnvironmentAccess,
useHasRootAccess,
} from 'hooks/useHasAccess';
interface IPermissionSwitchProps extends SwitchProps {
permission: string;
@ -14,11 +17,42 @@ interface IPermissionSwitchProps extends SwitchProps {
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,
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) => {
const {
access,
permission,
tooltip,
disabled,
@ -29,17 +63,6 @@ const PermissionSwitch = React.forwardRef<
...rest
} = 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 (
<TooltipResolver title={formatAccessText(access, tooltip)} arrow>
<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;

View File

@ -150,6 +150,7 @@ export const FeatureStrategyCreate = () => {
}
>
<FeatureStrategyForm
projectId={projectId}
feature={data}
strategy={strategy}
setStrategy={setStrategy}

View File

@ -166,6 +166,7 @@ export const FeatureStrategyEdit = () => {
}
>
<FeatureStrategyForm
projectId={projectId}
feature={data}
strategy={strategy}
setStrategy={setStrategy}

View File

@ -1,4 +1,4 @@
import React, { useState, useContext } from 'react';
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Alert, Button } from '@mui/material';
import {
@ -15,7 +15,6 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { STRATEGY_FORM_SUBMIT_ID } from 'utils/testIds';
import { useConstraintsValidation } from 'hooks/api/getters/useConstraintsValidation/useConstraintsValidation';
import AccessContext from 'contexts/AccessContext';
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
import { FeatureStrategySegment } from 'component/feature/FeatureStrategy/FeatureStrategySegment/FeatureStrategySegment';
import { ISegment } from 'interfaces/segment';
@ -30,9 +29,11 @@ import {
import { formatFeaturePath } from '../FeatureStrategyEdit/FeatureStrategyEdit';
import { useChangeRequestInReviewWarning } from 'hooks/useChangeRequestInReviewWarning';
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
import { useHasProjectEnvironmentAccess } from 'hooks/useHasAccess';
interface IFeatureStrategyFormProps {
feature: IFeatureToggle;
projectId: string;
environmentId: string;
permission: string;
onSubmit: () => void;
@ -48,6 +49,7 @@ interface IFeatureStrategyFormProps {
}
export const FeatureStrategyForm = ({
projectId,
feature,
environmentId,
permission,
@ -64,7 +66,11 @@ export const FeatureStrategyForm = ({
const [showProdGuard, setShowProdGuard] = useState(false);
const hasValidConstraints = useConstraintsValidation(strategy.constraints);
const enableProdGuard = useFeatureStrategyProdGuard(feature, environmentId);
const { hasAccess } = useContext(AccessContext);
const access = useHasProjectEnvironmentAccess(
permission,
environmentId,
projectId
);
const { strategyDefinition } = useStrategy(strategy?.name);
const { data } = usePendingChangeRequests(feature.project);
@ -205,11 +211,7 @@ export const FeatureStrategyForm = ({
setStrategy={setStrategy}
validateParameter={validateParameter}
errors={errors}
hasAccess={hasAccess(
permission,
feature.project,
environmentId
)}
hasAccess={access}
/>
<hr className={styles.hr} />
<div className={styles.buttons}>

View File

@ -11,7 +11,6 @@ import { AddToPhotos as CopyIcon, Lock } from '@mui/icons-material';
import { IFeatureStrategyPayload } from 'interfaces/strategy';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { IFeatureEnvironment } from 'interfaces/featureToggle';
import AccessContext from 'contexts/AccessContext';
import { CREATE_FEATURE_STRATEGY } from 'component/providers/AccessProvider/permissions';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
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 { formatUnknownError } from 'utils/formatUnknownError';
import { useChangeRequestAddStrategy } from 'hooks/useChangeRequestAddStrategy';
import { ChangeRequestDialogue } from '../../../../../../../../../changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog';
import { CopyStrategyMessage } from '../../../../../../../../../changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/CopyStrategyMessage';
import { ChangeRequestDialogue } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog';
import { CopyStrategyMessage } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/CopyStrategyMessage';
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
import { useCheckProjectAccess } from 'hooks/useHasAccess';
import { STRATEGY_FORM_COPY_ID } from 'utils/testIds';
interface ICopyStrategyIconMenuProps {
environmentId: string;
@ -50,7 +51,7 @@ export const CopyStrategyIconMenu: VFC<ICopyStrategyIconMenuProps> = ({
const onClose = () => {
setAnchorEl(null);
};
const { hasAccess } = useContext(AccessContext);
const checkAccess = useCheckProjectAccess(projectId);
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
const {
@ -98,7 +99,7 @@ export const CopyStrategyIconMenu: VFC<ICopyStrategyIconMenuProps> = ({
};
const enabled = environments.some(environment =>
hasAccess(CREATE_FEATURE_STRATEGY, projectId, environment)
checkAccess(CREATE_FEATURE_STRATEGY, environment)
);
return (
@ -132,6 +133,7 @@ export const CopyStrategyIconMenu: VFC<ICopyStrategyIconMenuProps> = ({
onClick={(event: MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
}}
data-testid={STRATEGY_FORM_COPY_ID}
disabled={!enabled}
>
<CopyIcon />
@ -148,9 +150,8 @@ export const CopyStrategyIconMenu: VFC<ICopyStrategyIconMenuProps> = ({
}}
>
{environments.map(environment => {
const access = hasAccess(
const access = checkAccess(
CREATE_FEATURE_STRATEGY,
projectId,
environment
);

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

View File

@ -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 STRATEGY_FORM_SUBMIT_ID = 'STRATEGY_FORM_SUBMIT_ID';
export const STRATEGY_FORM_REMOVE_ID = 'STRATEGY_FORM_REMOVE_ID';
export const STRATEGY_FORM_COPY_ID = 'STRATEGY_FORM_COPY_ID';
/* SPLASH */
export const CLOSE_SPLASH = 'CLOSE_SPLASH';