mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-28 00:17:12 +01:00
Merge branch 'main' into gitar_simplifyProjectOverview_true
This commit is contained in:
commit
6006544dcc
@ -1,4 +1,4 @@
|
||||
import { Alert, Tab, Tabs } from '@mui/material';
|
||||
import { Tab, Tabs } from '@mui/material';
|
||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
@ -15,7 +15,6 @@ import { TabPanel } from 'component/common/TabNav/TabPanel/TabPanel';
|
||||
import { usePageTitle } from 'hooks/usePageTitle';
|
||||
|
||||
export const AuthSettings = () => {
|
||||
const { authenticationType } = useUiConfig().uiConfig;
|
||||
const { uiConfig, isEnterprise } = useUiConfig();
|
||||
|
||||
const tabs = [
|
||||
@ -35,17 +34,14 @@ export const AuthSettings = () => {
|
||||
label: 'Google',
|
||||
component: <GoogleAuth />,
|
||||
},
|
||||
{
|
||||
label: 'SCIM',
|
||||
component: <ScimSettings />,
|
||||
},
|
||||
].filter(
|
||||
(item) => uiConfig.flags?.googleAuthEnabled || item.label !== 'Google',
|
||||
);
|
||||
|
||||
if (isEnterprise()) {
|
||||
tabs.push({
|
||||
label: 'SCIM',
|
||||
component: <ScimSettings />,
|
||||
});
|
||||
}
|
||||
|
||||
const [activeTab, setActiveTab] = useState(0);
|
||||
usePageTitle(`Single sign-on: ${tabs[activeTab].label}`);
|
||||
|
||||
@ -56,7 +52,7 @@ export const AuthSettings = () => {
|
||||
withTabs
|
||||
header={
|
||||
<ConditionallyRender
|
||||
condition={authenticationType === 'enterprise'}
|
||||
condition={isEnterprise()}
|
||||
show={
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
@ -85,41 +81,7 @@ export const AuthSettings = () => {
|
||||
}
|
||||
>
|
||||
<ConditionallyRender
|
||||
condition={authenticationType === 'open-source'}
|
||||
show={<PremiumFeature feature='sso' />}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={authenticationType === 'demo'}
|
||||
show={
|
||||
<Alert severity='warning'>
|
||||
You are running Unleash in demo mode. You have
|
||||
to use the Enterprise edition in order configure
|
||||
Single Sign-on.
|
||||
</Alert>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={authenticationType === 'custom'}
|
||||
show={
|
||||
<Alert severity='warning'>
|
||||
You have decided to use custom authentication
|
||||
type. You have to use the Enterprise edition in
|
||||
order configure Single Sign-on from the user
|
||||
interface.
|
||||
</Alert>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={authenticationType === 'hosted'}
|
||||
show={
|
||||
<Alert severity='info'>
|
||||
Your Unleash instance is managed by the Unleash
|
||||
team.
|
||||
</Alert>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={authenticationType === 'enterprise'}
|
||||
condition={isEnterprise()}
|
||||
show={
|
||||
<div>
|
||||
{tabs.map((tab, index) => (
|
||||
@ -133,6 +95,7 @@ export const AuthSettings = () => {
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
elseShow={<PremiumFeature feature='sso' />}
|
||||
/>
|
||||
</PageContent>
|
||||
</PermissionGuard>
|
||||
|
@ -1,4 +1,3 @@
|
||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||
import type React from 'react';
|
||||
import { useState } from 'react';
|
||||
import { TextField, Box } from '@mui/material';
|
||||
@ -7,23 +6,30 @@ import { useUiConfigApi } from 'hooks/api/actions/useUiConfigApi/useUiConfigApi'
|
||||
import useToast from 'hooks/useToast';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { useId } from 'hooks/useId';
|
||||
import { ADMIN, UPDATE_CORS } from '@server/types/permissions';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
|
||||
interface ICorsFormProps {
|
||||
frontendApiOrigins: string[] | undefined;
|
||||
}
|
||||
|
||||
export const CorsForm = ({ frontendApiOrigins }: ICorsFormProps) => {
|
||||
const { setFrontendSettings } = useUiConfigApi();
|
||||
const { setFrontendSettings, setCors } = useUiConfigApi();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const [value, setValue] = useState(formatInputValue(frontendApiOrigins));
|
||||
const inputFieldId = useId();
|
||||
const helpTextId = useId();
|
||||
const isGranularPermissionsEnabled = useUiFlag('granularAdminPermissions');
|
||||
|
||||
const onSubmit = async (event: React.FormEvent) => {
|
||||
try {
|
||||
const split = parseInputValue(value);
|
||||
event.preventDefault();
|
||||
if (isGranularPermissionsEnabled) {
|
||||
await setCors(split);
|
||||
} else {
|
||||
await setFrontendSettings(split);
|
||||
}
|
||||
setValue(formatInputValue(split));
|
||||
setToastData({ text: 'Settings saved', type: 'success' });
|
||||
} catch (error) {
|
||||
@ -67,7 +73,7 @@ export const CorsForm = ({ frontendApiOrigins }: ICorsFormProps) => {
|
||||
style: { fontFamily: 'monospace', fontSize: '0.8em' },
|
||||
}}
|
||||
/>
|
||||
<UpdateButton permission={ADMIN} />
|
||||
<UpdateButton permission={[ADMIN, UPDATE_CORS]} />
|
||||
</Box>
|
||||
</form>
|
||||
);
|
||||
|
@ -1,15 +1,15 @@
|
||||
import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
|
||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||
import { Box } from '@mui/material';
|
||||
import { CorsHelpAlert } from 'component/admin/cors/CorsHelpAlert';
|
||||
import { CorsForm } from 'component/admin/cors/CorsForm';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { ADMIN, UPDATE_CORS } from '@server/types/permissions';
|
||||
|
||||
export const CorsAdmin = () => (
|
||||
<div>
|
||||
<PermissionGuard permissions={ADMIN}>
|
||||
<PermissionGuard permissions={[ADMIN, UPDATE_CORS]}>
|
||||
<CorsPage />
|
||||
</PermissionGuard>
|
||||
</div>
|
||||
|
@ -23,6 +23,7 @@ const StyledGridContainer = styled('div')(({ theme }) => ({
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
|
||||
gap: theme.spacing(2),
|
||||
gridAutoRows: '1fr',
|
||||
}));
|
||||
|
||||
type PageQueryType = Partial<Record<'search', string>>;
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Box, styled } from '@mui/material';
|
||||
import { InviteLinkBar } from '../InviteLinkBar/InviteLinkBar';
|
||||
import { useUiFlag } from 'hooks/useUiFlag';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { LicensedUsersBox } from './LicensedUsersBox';
|
||||
|
||||
@ -24,9 +23,8 @@ const StyledElement = styled(Box)(({ theme }) => ({
|
||||
}));
|
||||
|
||||
export const UsersHeader = () => {
|
||||
const licensedUsers = useUiFlag('licensedUsers');
|
||||
const { isOss } = useUiConfig();
|
||||
const licensedUsersEnabled = licensedUsers && !isOss();
|
||||
const licensedUsersEnabled = !isOss();
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||
import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
|
||||
import { EventLog } from 'component/events/EventLog/EventLog';
|
||||
import { READ_LOGS, ADMIN } from '@server/types/permissions';
|
||||
|
||||
export const EventPage = () => (
|
||||
<PermissionGuard permissions={ADMIN}>
|
||||
<PermissionGuard permissions={[ADMIN, READ_LOGS]}>
|
||||
<EventLog title='Event log' />
|
||||
</PermissionGuard>
|
||||
);
|
||||
|
@ -3,6 +3,7 @@ import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuar
|
||||
import { LoginHistoryTable } from './LoginHistoryTable/LoginHistoryTable';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
|
||||
import { READ_LOGS } from '@server/types/permissions';
|
||||
|
||||
export const LoginHistory = () => {
|
||||
const { isEnterprise } = useUiConfig();
|
||||
@ -13,7 +14,7 @@ export const LoginHistory = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PermissionGuard permissions={ADMIN}>
|
||||
<PermissionGuard permissions={[ADMIN, READ_LOGS]}>
|
||||
<LoginHistoryTable />
|
||||
</PermissionGuard>
|
||||
</div>
|
||||
|
@ -71,9 +71,14 @@ const StyledAccordion = styled(Accordion)(({ theme }) => ({
|
||||
const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({
|
||||
boxShadow: 'none',
|
||||
padding: theme.spacing(1.5, 2),
|
||||
borderRadius: theme.shape.borderRadiusMedium,
|
||||
[theme.breakpoints.down(400)]: {
|
||||
padding: theme.spacing(1, 2),
|
||||
},
|
||||
'&.Mui-focusVisible': {
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
padding: theme.spacing(0.5, 2, 0.3, 2),
|
||||
},
|
||||
}));
|
||||
|
||||
const StyledAccordionDetails = styled(AccordionDetails)(({ theme }) => ({
|
||||
|
@ -57,6 +57,10 @@ export const MilestoneCardName = ({
|
||||
setEditMode(false);
|
||||
}
|
||||
}}
|
||||
onClick={(ev) => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!editMode && (
|
||||
|
@ -5,6 +5,9 @@ export const useUiConfigApi = () => {
|
||||
propagateErrors: true,
|
||||
});
|
||||
|
||||
/**
|
||||
* @deprecated remove when `granularAdminPermissions` flag is removed
|
||||
*/
|
||||
const setFrontendSettings = async (
|
||||
frontendApiOrigins: string[],
|
||||
): Promise<void> => {
|
||||
@ -19,8 +22,18 @@ export const useUiConfigApi = () => {
|
||||
await makeRequest(req.caller, req.id);
|
||||
};
|
||||
|
||||
const setCors = async (frontendApiOrigins: string[]): Promise<void> => {
|
||||
const req = createRequest(
|
||||
'api/admin/ui-config/cors',
|
||||
{ method: 'POST', body: JSON.stringify({ frontendApiOrigins }) },
|
||||
'setCors',
|
||||
);
|
||||
await makeRequest(req.caller, req.id);
|
||||
};
|
||||
|
||||
return {
|
||||
setFrontendSettings,
|
||||
setCors,
|
||||
loading,
|
||||
errors,
|
||||
};
|
||||
|
@ -89,7 +89,6 @@ export type UiFlags = {
|
||||
productivityReportEmail?: boolean;
|
||||
showUserDeviceCount?: boolean;
|
||||
flagOverviewRedesign?: boolean;
|
||||
licensedUsers?: boolean;
|
||||
granularAdminPermissions?: boolean;
|
||||
};
|
||||
|
||||
|
@ -167,7 +167,7 @@
|
||||
"stoppable": "^1.1.0",
|
||||
"ts-toolbelt": "^9.6.0",
|
||||
"type-is": "^1.6.18",
|
||||
"unleash-client": "^6.3.3",
|
||||
"unleash-client": "^6.4.0",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
@ -9,10 +9,8 @@ import type {
|
||||
import type { Logger } from '../../logger';
|
||||
|
||||
import type { FeatureConfigurationClient } from '../feature-toggle/types/feature-toggle-strategies-store-type';
|
||||
import type {
|
||||
RevisionDeltaEntry,
|
||||
ClientFeatureToggleDelta,
|
||||
} from './delta/client-feature-toggle-delta';
|
||||
import type { ClientFeatureToggleDelta } from './delta/client-feature-toggle-delta';
|
||||
import type { ClientFeaturesDeltaSchema } from '../../openapi';
|
||||
|
||||
export class ClientFeatureToggleService {
|
||||
private logger: Logger;
|
||||
@ -44,7 +42,7 @@ export class ClientFeatureToggleService {
|
||||
async getClientDelta(
|
||||
revisionId: number | undefined,
|
||||
query: IFeatureToggleQuery,
|
||||
): Promise<RevisionDeltaEntry | undefined> {
|
||||
): Promise<ClientFeaturesDeltaSchema | undefined> {
|
||||
if (this.clientFeatureToggleDelta !== null) {
|
||||
return this.clientFeatureToggleDelta.getDelta(revisionId, query);
|
||||
} else {
|
||||
|
@ -140,3 +140,37 @@ const syncRevisions = async () => {
|
||||
// @ts-ignore
|
||||
await app.services.clientFeatureToggleService.clientFeatureToggleDelta.onUpdateRevisionEvent();
|
||||
};
|
||||
|
||||
test('archived features should not be returned as updated', async () => {
|
||||
await app.createFeature('base_feature');
|
||||
await syncRevisions();
|
||||
const { body } = await app.request.get('/api/client/delta').expect(200);
|
||||
const currentRevisionId = body.revisionId;
|
||||
|
||||
expect(body).toMatchObject({
|
||||
updated: [
|
||||
{
|
||||
name: 'base_feature',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await app.archiveFeature('base_feature');
|
||||
await app.createFeature('new_feature');
|
||||
|
||||
await syncRevisions();
|
||||
|
||||
const { body: deltaBody } = await app.request
|
||||
.get('/api/client/delta')
|
||||
.set('If-None-Match', currentRevisionId)
|
||||
.expect(200);
|
||||
|
||||
expect(deltaBody).toMatchObject({
|
||||
updated: [
|
||||
{
|
||||
name: 'new_feature',
|
||||
},
|
||||
],
|
||||
removed: ['base_feature'],
|
||||
});
|
||||
});
|
@ -17,8 +17,10 @@ import type { OpenApiService } from '../../../services/openapi-service';
|
||||
import { NONE } from '../../../types/permissions';
|
||||
import { createResponseSchema } from '../../../openapi/util/create-response-schema';
|
||||
import type { ClientFeatureToggleService } from '../client-feature-toggle-service';
|
||||
import type { RevisionDeltaEntry } from './client-feature-toggle-delta';
|
||||
import { clientFeaturesDeltaSchema } from '../../../openapi';
|
||||
import {
|
||||
type ClientFeaturesDeltaSchema,
|
||||
clientFeaturesDeltaSchema,
|
||||
} from '../../../openapi';
|
||||
import type { QueryOverride } from '../client-feature-toggle.controller';
|
||||
|
||||
export default class ClientFeatureToggleDeltaController extends Controller {
|
||||
@ -75,7 +77,7 @@ export default class ClientFeatureToggleDeltaController extends Controller {
|
||||
|
||||
async getDelta(
|
||||
req: IAuthRequest,
|
||||
res: Response<RevisionDeltaEntry>,
|
||||
res: Response<ClientFeaturesDeltaSchema>,
|
||||
): Promise<void> {
|
||||
if (!this.flagResolver.isEnabled('deltaApi')) {
|
||||
throw new NotFoundError();
|
||||
|
@ -4,7 +4,7 @@ import type { FeatureConfigurationClient } from '../../feature-toggle/types/feat
|
||||
export interface FeatureConfigurationDeltaClient
|
||||
extends FeatureConfigurationClient {
|
||||
description: string;
|
||||
impressionData: false;
|
||||
impressionData: boolean;
|
||||
}
|
||||
|
||||
export interface IClientFeatureToggleDeltaReadModel {
|
||||
|
@ -17,6 +17,7 @@ import type {
|
||||
import { CLIENT_DELTA_MEMORY } from '../../../metric-events';
|
||||
import type EventEmitter from 'events';
|
||||
import type { Logger } from '../../../logger';
|
||||
import type { ClientFeaturesDeltaSchema } from '../../../openapi';
|
||||
|
||||
type DeletedFeature = {
|
||||
name: string;
|
||||
@ -79,6 +80,7 @@ const filterRevisionByProject = (
|
||||
(feature) =>
|
||||
projects.includes('*') || projects.includes(feature.project),
|
||||
);
|
||||
|
||||
return { ...revision, updated, removed };
|
||||
};
|
||||
|
||||
@ -153,7 +155,7 @@ export class ClientFeatureToggleDelta {
|
||||
async getDelta(
|
||||
sdkRevisionId: number | undefined,
|
||||
query: IFeatureToggleQuery,
|
||||
): Promise<RevisionDeltaEntry | undefined> {
|
||||
): Promise<ClientFeaturesDeltaSchema | undefined> {
|
||||
const projects = query.project ? query.project : ['*'];
|
||||
const environment = query.environment ? query.environment : 'default';
|
||||
// TODO: filter by tags, what is namePrefix? anything else?
|
||||
@ -181,9 +183,10 @@ export class ClientFeatureToggleDelta {
|
||||
projects,
|
||||
);
|
||||
|
||||
const revisionResponse = {
|
||||
const revisionResponse: ClientFeaturesDeltaSchema = {
|
||||
...compressedRevision,
|
||||
segments: this.segments,
|
||||
removed: compressedRevision.removed.map((feature) => feature.name),
|
||||
};
|
||||
|
||||
return Promise.resolve(revisionResponse);
|
||||
@ -197,6 +200,9 @@ export class ClientFeatureToggleDelta {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used in client-feature-delta-api.e2e.test.ts, do not remove
|
||||
*/
|
||||
public resetDelta() {
|
||||
this.delta = {};
|
||||
}
|
||||
@ -217,6 +223,7 @@ export class ClientFeatureToggleDelta {
|
||||
...new Set(
|
||||
changeEvents
|
||||
.filter((event) => event.featureName)
|
||||
.filter((event) => event.type !== 'feature-archived')
|
||||
.map((event) => event.featureName!),
|
||||
),
|
||||
];
|
||||
|
@ -40,6 +40,7 @@ import type { UpdateContextFieldSchema } from '../../openapi/spec/update-context
|
||||
import type { CreateContextFieldSchema } from '../../openapi/spec/create-context-field-schema';
|
||||
import { extractUserIdFromUser } from '../../util';
|
||||
import type { LegalValueSchema } from '../../openapi';
|
||||
import type { WithTransactional } from '../../db/transaction';
|
||||
|
||||
interface ContextParam {
|
||||
contextField: string;
|
||||
@ -50,7 +51,7 @@ interface DeleteLegalValueParam extends ContextParam {
|
||||
}
|
||||
|
||||
export class ContextController extends Controller {
|
||||
private contextService: ContextService;
|
||||
private transactionalContextService: WithTransactional<ContextService>;
|
||||
|
||||
private openApiService: OpenApiService;
|
||||
|
||||
@ -59,14 +60,17 @@ export class ContextController extends Controller {
|
||||
constructor(
|
||||
config: IUnleashConfig,
|
||||
{
|
||||
contextService,
|
||||
transactionalContextService,
|
||||
openApiService,
|
||||
}: Pick<IUnleashServices, 'contextService' | 'openApiService'>,
|
||||
}: Pick<
|
||||
IUnleashServices,
|
||||
'transactionalContextService' | 'openApiService'
|
||||
>,
|
||||
) {
|
||||
super(config);
|
||||
this.openApiService = openApiService;
|
||||
this.logger = config.getLogger('/admin-api/context.ts');
|
||||
this.contextService = contextService;
|
||||
this.transactionalContextService = transactionalContextService;
|
||||
|
||||
this.route({
|
||||
method: 'get',
|
||||
@ -257,7 +261,9 @@ export class ContextController extends Controller {
|
||||
res: Response<ContextFieldsSchema>,
|
||||
): Promise<void> {
|
||||
res.status(200)
|
||||
.json(serializeDates(await this.contextService.getAll()))
|
||||
.json(
|
||||
serializeDates(await this.transactionalContextService.getAll()),
|
||||
)
|
||||
.end();
|
||||
}
|
||||
|
||||
@ -268,7 +274,7 @@ export class ContextController extends Controller {
|
||||
try {
|
||||
const name = req.params.contextField;
|
||||
const contextField =
|
||||
await this.contextService.getContextField(name);
|
||||
await this.transactionalContextService.getContextField(name);
|
||||
this.openApiService.respondWithValidation(
|
||||
200,
|
||||
res,
|
||||
@ -286,9 +292,8 @@ export class ContextController extends Controller {
|
||||
): Promise<void> {
|
||||
const value = req.body;
|
||||
|
||||
const result = await this.contextService.createContextField(
|
||||
value,
|
||||
req.audit,
|
||||
const result = await this.transactionalContextService.transactional(
|
||||
(service) => service.createContextField(value, req.audit),
|
||||
);
|
||||
|
||||
this.openApiService.respondWithValidation(
|
||||
@ -307,9 +312,8 @@ export class ContextController extends Controller {
|
||||
const name = req.params.contextField;
|
||||
const contextField = req.body;
|
||||
|
||||
await this.contextService.updateContextField(
|
||||
{ ...contextField, name },
|
||||
req.audit,
|
||||
await this.transactionalContextService.transactional((service) =>
|
||||
service.updateContextField({ ...contextField, name }, req.audit),
|
||||
);
|
||||
res.status(200).end();
|
||||
}
|
||||
@ -321,9 +325,8 @@ export class ContextController extends Controller {
|
||||
const name = req.params.contextField;
|
||||
const legalValue = req.body;
|
||||
|
||||
await this.contextService.updateLegalValue(
|
||||
{ name, legalValue },
|
||||
req.audit,
|
||||
await this.transactionalContextService.transactional((service) =>
|
||||
service.updateLegalValue({ name, legalValue }, req.audit),
|
||||
);
|
||||
res.status(200).end();
|
||||
}
|
||||
@ -335,9 +338,8 @@ export class ContextController extends Controller {
|
||||
const name = req.params.contextField;
|
||||
const legalValue = req.params.legalValue;
|
||||
|
||||
await this.contextService.deleteLegalValue(
|
||||
{ name, legalValue },
|
||||
req.audit,
|
||||
await this.transactionalContextService.transactional((service) =>
|
||||
service.deleteLegalValue({ name, legalValue }, req.audit),
|
||||
);
|
||||
res.status(200).end();
|
||||
}
|
||||
@ -348,7 +350,9 @@ export class ContextController extends Controller {
|
||||
): Promise<void> {
|
||||
const name = req.params.contextField;
|
||||
|
||||
await this.contextService.deleteContextField(name, req.audit);
|
||||
await this.transactionalContextService.transactional((service) =>
|
||||
service.deleteContextField(name, req.audit),
|
||||
);
|
||||
res.status(200).end();
|
||||
}
|
||||
|
||||
@ -358,7 +362,7 @@ export class ContextController extends Controller {
|
||||
): Promise<void> {
|
||||
const { name } = req.body;
|
||||
|
||||
await this.contextService.validateName(name);
|
||||
await this.transactionalContextService.validateName(name);
|
||||
res.status(200).end();
|
||||
}
|
||||
|
||||
@ -369,7 +373,7 @@ export class ContextController extends Controller {
|
||||
const { contextField } = req.params;
|
||||
const { user } = req;
|
||||
const contextFields =
|
||||
await this.contextService.getStrategiesByContextField(
|
||||
await this.transactionalContextService.getStrategiesByContextField(
|
||||
contextField,
|
||||
extractUserIdFromUser(user),
|
||||
);
|
||||
|
@ -6,7 +6,12 @@ import {
|
||||
} from '../../../test/e2e/helpers/test-helper';
|
||||
import getLogger from '../../../test/fixtures/no-logger';
|
||||
import type { FeatureSearchQueryParameters } from '../../openapi/spec/feature-search-query-parameters';
|
||||
import { DEFAULT_PROJECT, type IUnleashStores } from '../../types';
|
||||
import {
|
||||
CREATE_FEATURE_STRATEGY,
|
||||
DEFAULT_PROJECT,
|
||||
type IUnleashStores,
|
||||
UPDATE_FEATURE_ENVIRONMENT,
|
||||
} from '../../types';
|
||||
import { DEFAULT_ENV } from '../../util';
|
||||
|
||||
let app: IUnleashTest;
|
||||
@ -29,7 +34,7 @@ beforeAll(async () => {
|
||||
);
|
||||
stores = db.stores;
|
||||
|
||||
await app.request
|
||||
const { body } = await app.request
|
||||
.post(`/auth/demo/login`)
|
||||
.send({
|
||||
email: 'user@getunleash.io',
|
||||
@ -43,12 +48,30 @@ beforeAll(async () => {
|
||||
|
||||
await app.linkProjectToEnvironment('default', 'development');
|
||||
|
||||
await stores.accessStore.addPermissionsToRole(
|
||||
body.rootRole,
|
||||
[
|
||||
{ name: UPDATE_FEATURE_ENVIRONMENT },
|
||||
{ name: CREATE_FEATURE_STRATEGY },
|
||||
],
|
||||
'development',
|
||||
);
|
||||
|
||||
await stores.environmentStore.create({
|
||||
name: 'production',
|
||||
type: 'production',
|
||||
});
|
||||
|
||||
await app.linkProjectToEnvironment('default', 'production');
|
||||
|
||||
await stores.accessStore.addPermissionsToRole(
|
||||
body.rootRole,
|
||||
[
|
||||
{ name: UPDATE_FEATURE_ENVIRONMENT },
|
||||
{ name: CREATE_FEATURE_STRATEGY },
|
||||
],
|
||||
'production',
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
|
@ -208,6 +208,23 @@ export class FrontendApiService {
|
||||
);
|
||||
}
|
||||
|
||||
async setFrontendCorsSettings(
|
||||
value: FrontendSettings['frontendApiOrigins'],
|
||||
auditUser: IAuditUser,
|
||||
): Promise<void> {
|
||||
const error = validateOrigins(value);
|
||||
if (error) {
|
||||
throw new BadDataError(error);
|
||||
}
|
||||
const settings = (await this.getFrontendSettings(false)) || {};
|
||||
await this.services.settingService.insert(
|
||||
frontendSettingsKey,
|
||||
{ ...settings, frontendApiOrigins: value },
|
||||
auditUser,
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
async fetchFrontendSettings(): Promise<FrontendSettings> {
|
||||
try {
|
||||
this.cachedFrontendSettings =
|
||||
|
@ -805,6 +805,11 @@ describe('Managing Project access', () => {
|
||||
mode: 'open' as const,
|
||||
defaultStickiness: 'clientId',
|
||||
};
|
||||
await db.stores.environmentStore.create({
|
||||
name: 'production',
|
||||
type: 'production',
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const auditUser = extractAuditInfoFromUser(user);
|
||||
await projectService.createProject(project, user, auditUser);
|
||||
|
27
src/lib/openapi/spec/client-features-delta-schema.test.ts
Normal file
27
src/lib/openapi/spec/client-features-delta-schema.test.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import { validateSchema } from '../validate';
|
||||
import type { ClientFeaturesDeltaSchema } from './client-features-delta-schema';
|
||||
|
||||
test('clientFeaturesDeltaSchema all fields', () => {
|
||||
const data: ClientFeaturesDeltaSchema = {
|
||||
revisionId: 6,
|
||||
updated: [
|
||||
{
|
||||
impressionData: false,
|
||||
enabled: false,
|
||||
name: 'base_feature',
|
||||
description: null,
|
||||
project: 'default',
|
||||
stale: false,
|
||||
type: 'release',
|
||||
variants: [],
|
||||
strategies: [],
|
||||
},
|
||||
],
|
||||
removed: [],
|
||||
segments: [],
|
||||
};
|
||||
|
||||
expect(
|
||||
validateSchema('#/components/schemas/clientFeaturesDeltaSchema', data),
|
||||
).toBeUndefined();
|
||||
});
|
@ -177,6 +177,7 @@ export * from './search-features-schema';
|
||||
export * from './segment-schema';
|
||||
export * from './segment-strategies-schema';
|
||||
export * from './segments-schema';
|
||||
export * from './set-cors-schema';
|
||||
export * from './set-strategy-sort-order-schema';
|
||||
export * from './set-ui-config-schema';
|
||||
export * from './sort-order-schema';
|
||||
|
20
src/lib/openapi/spec/set-cors-schema.ts
Normal file
20
src/lib/openapi/spec/set-cors-schema.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import type { FromSchema } from 'json-schema-to-ts';
|
||||
|
||||
export const setCorsSchema = {
|
||||
$id: '#/components/schemas/setCorsSchema',
|
||||
type: 'object',
|
||||
additionalProperties: false,
|
||||
description: 'Unleash CORS configuration.',
|
||||
properties: {
|
||||
frontendApiOrigins: {
|
||||
description:
|
||||
'The list of origins that the front-end API should accept requests from.',
|
||||
example: ['*'],
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
},
|
||||
},
|
||||
components: {},
|
||||
} as const;
|
||||
|
||||
export type SetCorsSchema = FromSchema<typeof setCorsSchema>;
|
@ -19,6 +19,11 @@ const uiConfig = {
|
||||
async function getSetup() {
|
||||
const base = `/random${Math.round(Math.random() * 1000)}`;
|
||||
const config = createTestConfig({
|
||||
experimental: {
|
||||
flags: {
|
||||
granularAdminPermissions: true,
|
||||
},
|
||||
},
|
||||
server: { baseUriPath: base },
|
||||
ui: uiConfig,
|
||||
});
|
||||
@ -56,3 +61,26 @@ test('should get ui config', async () => {
|
||||
expect(body.segmentValuesLimit).toEqual(DEFAULT_SEGMENT_VALUES_LIMIT);
|
||||
expect(body.strategySegmentsLimit).toEqual(DEFAULT_STRATEGY_SEGMENTS_LIMIT);
|
||||
});
|
||||
|
||||
test('should update CORS settings', async () => {
|
||||
const { body } = await request
|
||||
.get(`${base}/api/admin/ui-config`)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200);
|
||||
|
||||
expect(body.frontendApiOrigins).toEqual(['*']);
|
||||
|
||||
await request
|
||||
.post(`${base}/api/admin/ui-config/cors`)
|
||||
.send({
|
||||
frontendApiOrigins: ['https://example.com'],
|
||||
})
|
||||
.expect(204);
|
||||
|
||||
const { body: updatedBody } = await request
|
||||
.get(`${base}/api/admin/ui-config`)
|
||||
.expect('Content-Type', /json/)
|
||||
.expect(200);
|
||||
|
||||
expect(updatedBody.frontendApiOrigins).toEqual(['https://example.com']);
|
||||
});
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
type SimpleAuthSettings,
|
||||
simpleAuthSettingsKey,
|
||||
} from '../../types/settings/simple-auth-settings';
|
||||
import { ADMIN, NONE } from '../../types/permissions';
|
||||
import { ADMIN, NONE, UPDATE_CORS } from '../../types/permissions';
|
||||
import { createResponseSchema } from '../../openapi/util/create-response-schema';
|
||||
import {
|
||||
uiConfigSchema,
|
||||
@ -22,6 +22,7 @@ import { emptyResponse } from '../../openapi/util/standard-responses';
|
||||
import type { IAuthRequest } from '../unleash-types';
|
||||
import NotFoundError from '../../error/notfound-error';
|
||||
import type { SetUiConfigSchema } from '../../openapi/spec/set-ui-config-schema';
|
||||
import type { SetCorsSchema } from '../../openapi/spec/set-cors-schema';
|
||||
import { createRequestSchema } from '../../openapi/util/create-request-schema';
|
||||
import type { FrontendApiService, SessionService } from '../../services';
|
||||
import type MaintenanceService from '../../features/maintenance/maintenance-service';
|
||||
@ -99,6 +100,7 @@ class ConfigController extends Controller {
|
||||
],
|
||||
});
|
||||
|
||||
// TODO: deprecate when removing `granularAdminPermissions` flag
|
||||
this.route({
|
||||
method: 'post',
|
||||
path: '',
|
||||
@ -116,6 +118,24 @@ class ConfigController extends Controller {
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
this.route({
|
||||
method: 'post',
|
||||
path: '/cors',
|
||||
handler: this.setCors,
|
||||
permission: [ADMIN, UPDATE_CORS],
|
||||
middleware: [
|
||||
openApiService.validPath({
|
||||
tags: ['Admin UI'],
|
||||
summary: 'Sets allowed CORS origins',
|
||||
description:
|
||||
'Sets Cross-Origin Resource Sharing headers for Frontend SDK API.',
|
||||
operationId: 'setCors',
|
||||
requestBody: createRequestSchema('setCorsSchema'),
|
||||
responses: { 204: emptyResponse },
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async getUiConfig(
|
||||
@ -197,6 +217,30 @@ class ConfigController extends Controller {
|
||||
|
||||
throw new NotFoundError();
|
||||
}
|
||||
|
||||
async setCors(
|
||||
req: IAuthRequest<void, void, SetCorsSchema>,
|
||||
res: Response<string>,
|
||||
): Promise<void> {
|
||||
const granularAdminPermissions = this.flagResolver.isEnabled(
|
||||
'granularAdminPermissions',
|
||||
);
|
||||
|
||||
if (!granularAdminPermissions) {
|
||||
throw new NotFoundError();
|
||||
}
|
||||
|
||||
if (req.body.frontendApiOrigins) {
|
||||
await this.frontendApiService.setFrontendCorsSettings(
|
||||
req.body.frontendApiOrigins,
|
||||
req.audit,
|
||||
);
|
||||
res.sendStatus(204);
|
||||
return;
|
||||
}
|
||||
|
||||
throw new NotFoundError();
|
||||
}
|
||||
}
|
||||
|
||||
export default ConfigController;
|
||||
|
@ -200,9 +200,10 @@ export const createServices = (
|
||||
? new FeatureLifecycleReadModel(db, config.flagResolver)
|
||||
: new FakeFeatureLifecycleReadModel();
|
||||
|
||||
const contextService = db
|
||||
const transactionalContextService = db
|
||||
? withTransactional(createContextService(config), db)
|
||||
: withFakeTransactional(createFakeContextService(config));
|
||||
const contextService = transactionalContextService;
|
||||
const emailService = new EmailService(config);
|
||||
const featureTypeService = new FeatureTypeService(
|
||||
stores,
|
||||
@ -434,6 +435,7 @@ export const createServices = (
|
||||
clientInstanceService,
|
||||
clientMetricsServiceV2,
|
||||
contextService,
|
||||
transactionalContextService,
|
||||
versionService,
|
||||
apiTokenService,
|
||||
emailService,
|
||||
|
@ -56,12 +56,12 @@ export type IFlagKey =
|
||||
| 'showUserDeviceCount'
|
||||
| 'deleteStaleUserSessions'
|
||||
| 'memorizeStats'
|
||||
| 'licensedUsers'
|
||||
| 'granularAdminPermissions'
|
||||
| 'streaming'
|
||||
| 'etagVariant'
|
||||
| 'oidcRedirect'
|
||||
| 'deltaApi';
|
||||
| 'deltaApi'
|
||||
| 'newHostedAuthHandler';
|
||||
|
||||
export type IFlags = Partial<{ [key in IFlagKey]: boolean | Variant }>;
|
||||
|
||||
@ -265,10 +265,6 @@ const flags: IFlags = {
|
||||
process.env.UNLEASH_EXPERIMENTAL_FLAG_OVERVIEW_REDESIGN,
|
||||
false,
|
||||
),
|
||||
licensedUsers: parseEnvVarBoolean(
|
||||
process.env.UNLEASH_EXPERIMENTAL_FLAG_LICENSED_USERS,
|
||||
false,
|
||||
),
|
||||
granularAdminPermissions: parseEnvVarBoolean(
|
||||
process.env.UNLEASH_EXPERIMENTAL_GRANULAR_ADMIN_PERMISSIONS,
|
||||
false,
|
||||
@ -290,6 +286,10 @@ const flags: IFlags = {
|
||||
process.env.UNLEASH_EXPERIMENTAL_DELTA_API,
|
||||
false,
|
||||
),
|
||||
newHostedAuthHandler: parseEnvVarBoolean(
|
||||
process.env.UNLEASH_EXPERIMENTAL_NEW_HOSTED_AUTH_HANDLER,
|
||||
false,
|
||||
),
|
||||
};
|
||||
|
||||
export const defaultExperimentalOptions: IExperimentalOptions = {
|
||||
|
@ -54,7 +54,13 @@ export interface IVersionOption {
|
||||
export enum IAuthType {
|
||||
OPEN_SOURCE = 'open-source',
|
||||
DEMO = 'demo',
|
||||
/**
|
||||
* Self-hosted by the customer. Should eventually be renamed to better reflect this.
|
||||
*/
|
||||
ENTERPRISE = 'enterprise',
|
||||
/**
|
||||
* Hosted by Unleash.
|
||||
*/
|
||||
HOSTED = 'hosted',
|
||||
CUSTOM = 'custom',
|
||||
NONE = 'none',
|
||||
|
@ -41,8 +41,10 @@ export const CREATE_TAG_TYPE = 'CREATE_TAG_TYPE';
|
||||
export const UPDATE_TAG_TYPE = 'UPDATE_TAG_TYPE';
|
||||
export const DELETE_TAG_TYPE = 'DELETE_TAG_TYPE';
|
||||
|
||||
export const READ_LOGS = 'READ_LOGS';
|
||||
export const UPDATE_MAINTENANCE_MODE = 'UPDATE_MAINTENANCE_MODE';
|
||||
export const UPDATE_INSTANCE_BANNERS = 'UPDATE_INSTANCE_BANNERS';
|
||||
export const UPDATE_CORS = 'UPDATE_CORS';
|
||||
export const UPDATE_AUTH_CONFIGURATION = 'UPDATE_AUTH_CONFIGURATION';
|
||||
|
||||
// Project
|
||||
@ -147,7 +149,12 @@ export const ROOT_PERMISSION_CATEGORIES = [
|
||||
},
|
||||
{
|
||||
label: 'Instance maintenance',
|
||||
permissions: [UPDATE_MAINTENANCE_MODE, UPDATE_INSTANCE_BANNERS],
|
||||
permissions: [
|
||||
READ_LOGS,
|
||||
UPDATE_MAINTENANCE_MODE,
|
||||
UPDATE_INSTANCE_BANNERS,
|
||||
UPDATE_CORS,
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Authentication',
|
||||
@ -162,4 +169,5 @@ export const MAINTENANCE_MODE_PERMISSIONS = [
|
||||
READ_CLIENT_API_TOKEN,
|
||||
READ_FRONTEND_API_TOKEN,
|
||||
UPDATE_MAINTENANCE_MODE,
|
||||
READ_LOGS,
|
||||
];
|
||||
|
@ -69,6 +69,7 @@ export interface IUnleashServices {
|
||||
clientInstanceService: ClientInstanceService;
|
||||
clientMetricsServiceV2: ClientMetricsServiceV2;
|
||||
contextService: ContextService;
|
||||
transactionalContextService: WithTransactional<ContextService>;
|
||||
emailService: EmailService;
|
||||
environmentService: EnvironmentService;
|
||||
transactionalEnvironmentService: WithTransactional<EnvironmentService>;
|
||||
|
@ -0,0 +1,20 @@
|
||||
exports.up = function (db, cb) {
|
||||
db.runSql(
|
||||
`
|
||||
UPDATE role_permission SET environment = null where environment = '';
|
||||
DELETE FROM role_permission WHERE environment IS NOT NULL AND environment NOT IN (SELECT name FROM environments);
|
||||
ALTER TABLE role_permission ADD CONSTRAINT fk_role_permission_environment FOREIGN KEY (environment) REFERENCES environments(name) ON DELETE CASCADE;
|
||||
`,
|
||||
cb,
|
||||
);
|
||||
};
|
||||
|
||||
exports.down = function (db, cb) {
|
||||
db.runSql(
|
||||
`
|
||||
ALTER TABLE role_permission
|
||||
DROP CONSTRAINT IF EXISTS fk_role_permission_environment;
|
||||
`,
|
||||
cb,
|
||||
);
|
||||
};
|
13
src/migrations/20250102150900-add-permission-read-logs.js
Normal file
13
src/migrations/20250102150900-add-permission-read-logs.js
Normal file
@ -0,0 +1,13 @@
|
||||
'use strict';
|
||||
|
||||
exports.up = function (db, cb) {
|
||||
db.runSql(`
|
||||
INSERT INTO permissions (permission, display_name, type) VALUES ('READ_LOGS', 'Read instance logs and login history', 'root');
|
||||
`, cb);
|
||||
}
|
||||
|
||||
exports.down = function (db, cb) {
|
||||
db.runSql(`
|
||||
DELETE FROM permissions WHERE permission IN ('READ_LOGS');
|
||||
`, cb);
|
||||
}
|
@ -53,7 +53,6 @@ process.nextTick(async () => {
|
||||
releasePlans: false,
|
||||
showUserDeviceCount: true,
|
||||
flagOverviewRedesign: false,
|
||||
licensedUsers: true,
|
||||
granularAdminPermissions: true,
|
||||
deltaApi: true,
|
||||
},
|
||||
|
@ -21,9 +21,19 @@ delete process.env.DATABASE_URL;
|
||||
// because of db-migrate bug (https://github.com/Unleash/unleash/issues/171)
|
||||
process.setMaxListeners(0);
|
||||
|
||||
async function getDefaultEnvRolePermissions(knex) {
|
||||
return knex.table('role_permission').whereIn('environment', ['default']);
|
||||
}
|
||||
|
||||
async function restoreRolePermissions(knex, rolePermissions) {
|
||||
await knex.table('role_permission').insert(rolePermissions);
|
||||
}
|
||||
|
||||
async function resetDatabase(knex) {
|
||||
return Promise.all([
|
||||
knex.table('environments').del(),
|
||||
knex
|
||||
.table('environments')
|
||||
.del(), // deletes role permissions transitively
|
||||
knex.table('strategies').del(),
|
||||
knex.table('features').del(),
|
||||
knex.table('client_applications').del(),
|
||||
@ -110,15 +120,20 @@ export default async function init(
|
||||
const testDb = createDb(config);
|
||||
const stores = await createStores(config, testDb);
|
||||
stores.eventStore.setMaxListeners(0);
|
||||
const defaultRolePermissions = await getDefaultEnvRolePermissions(testDb);
|
||||
await resetDatabase(testDb);
|
||||
await setupDatabase(stores);
|
||||
await restoreRolePermissions(testDb, defaultRolePermissions);
|
||||
|
||||
return {
|
||||
rawDatabase: testDb,
|
||||
stores,
|
||||
reset: async () => {
|
||||
const defaultRolePermissions =
|
||||
await getDefaultEnvRolePermissions(testDb);
|
||||
await resetDatabase(testDb);
|
||||
await setupDatabase(stores);
|
||||
await restoreRolePermissions(testDb, defaultRolePermissions);
|
||||
},
|
||||
destroy: async () => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
|
@ -94,8 +94,6 @@ const createRole = async (rolePermissions: PermissionRef[]) => {
|
||||
|
||||
const hasCommonProjectAccess = async (user, projectName, condition) => {
|
||||
const defaultEnv = 'default';
|
||||
const developmentEnv = 'development';
|
||||
const productionEnv = 'production';
|
||||
|
||||
const {
|
||||
CREATE_FEATURE,
|
||||
@ -155,70 +153,6 @@ const hasCommonProjectAccess = async (user, projectName, condition) => {
|
||||
defaultEnv,
|
||||
),
|
||||
).toBe(condition);
|
||||
expect(
|
||||
await accessService.hasPermission(
|
||||
user,
|
||||
CREATE_FEATURE_STRATEGY,
|
||||
projectName,
|
||||
developmentEnv,
|
||||
),
|
||||
).toBe(condition);
|
||||
expect(
|
||||
await accessService.hasPermission(
|
||||
user,
|
||||
UPDATE_FEATURE_STRATEGY,
|
||||
projectName,
|
||||
developmentEnv,
|
||||
),
|
||||
).toBe(condition);
|
||||
expect(
|
||||
await accessService.hasPermission(
|
||||
user,
|
||||
DELETE_FEATURE_STRATEGY,
|
||||
projectName,
|
||||
developmentEnv,
|
||||
),
|
||||
).toBe(condition);
|
||||
expect(
|
||||
await accessService.hasPermission(
|
||||
user,
|
||||
UPDATE_FEATURE_ENVIRONMENT,
|
||||
projectName,
|
||||
developmentEnv,
|
||||
),
|
||||
).toBe(condition);
|
||||
expect(
|
||||
await accessService.hasPermission(
|
||||
user,
|
||||
CREATE_FEATURE_STRATEGY,
|
||||
projectName,
|
||||
productionEnv,
|
||||
),
|
||||
).toBe(condition);
|
||||
expect(
|
||||
await accessService.hasPermission(
|
||||
user,
|
||||
UPDATE_FEATURE_STRATEGY,
|
||||
projectName,
|
||||
productionEnv,
|
||||
),
|
||||
).toBe(condition);
|
||||
expect(
|
||||
await accessService.hasPermission(
|
||||
user,
|
||||
DELETE_FEATURE_STRATEGY,
|
||||
projectName,
|
||||
productionEnv,
|
||||
),
|
||||
).toBe(condition);
|
||||
expect(
|
||||
await accessService.hasPermission(
|
||||
user,
|
||||
UPDATE_FEATURE_ENVIRONMENT,
|
||||
projectName,
|
||||
productionEnv,
|
||||
),
|
||||
).toBe(condition);
|
||||
};
|
||||
|
||||
const hasFullProjectAccess = async (user, projectName: string, condition) => {
|
||||
@ -378,7 +312,7 @@ test('should remove CREATE_FEATURE on default environment', async () => {
|
||||
await accessService.addPermissionToRole(
|
||||
editRole.id,
|
||||
permissions.CREATE_FEATURE,
|
||||
'*',
|
||||
'default',
|
||||
);
|
||||
|
||||
// TODO: to validate the remove works, we should make sure that we had permission before removing it
|
||||
@ -637,7 +571,7 @@ test('should support permission with "ALL" environment requirement', async () =>
|
||||
await accessStore.addPermissionsToRole(
|
||||
customRole.id,
|
||||
[{ name: CREATE_FEATURE_STRATEGY }],
|
||||
'production',
|
||||
'default',
|
||||
);
|
||||
await accessStore.addUserToRole(user.id, customRole.id, ALL_PROJECTS);
|
||||
|
||||
@ -645,7 +579,7 @@ test('should support permission with "ALL" environment requirement', async () =>
|
||||
user,
|
||||
CREATE_FEATURE_STRATEGY,
|
||||
'default',
|
||||
'production',
|
||||
'default',
|
||||
);
|
||||
|
||||
expect(hasAccess).toBe(true);
|
||||
@ -667,7 +601,7 @@ test('Should have access to create a strategy in an environment', async () => {
|
||||
user,
|
||||
CREATE_FEATURE_STRATEGY,
|
||||
'default',
|
||||
'development',
|
||||
'default',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
@ -693,7 +627,7 @@ test('Should have access to edit a strategy in an environment', async () => {
|
||||
user,
|
||||
UPDATE_FEATURE_STRATEGY,
|
||||
'default',
|
||||
'development',
|
||||
'default',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
@ -706,7 +640,7 @@ test('Should have access to delete a strategy in an environment', async () => {
|
||||
user,
|
||||
DELETE_FEATURE_STRATEGY,
|
||||
'default',
|
||||
'development',
|
||||
'default',
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
10
yarn.lock
10
yarn.lock
@ -9229,9 +9229,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"unleash-client@npm:^6.3.3":
|
||||
version: 6.3.3
|
||||
resolution: "unleash-client@npm:6.3.3"
|
||||
"unleash-client@npm:^6.4.0":
|
||||
version: 6.4.0
|
||||
resolution: "unleash-client@npm:6.4.0"
|
||||
dependencies:
|
||||
http-proxy-agent: "npm:^7.0.2"
|
||||
https-proxy-agent: "npm:^7.0.5"
|
||||
@ -9241,7 +9241,7 @@ __metadata:
|
||||
murmurhash3js: "npm:^3.0.1"
|
||||
proxy-from-env: "npm:^1.1.0"
|
||||
semver: "npm:^7.6.2"
|
||||
checksum: 10c0/047f3b63aa1cadde15abc39a4f627f77c297aaa15d11928d93d86815f88b7d72811c931be4a7fcd44887cc12bdb5ad32f674dbe19b62665ece4a86dac77224bb
|
||||
checksum: 10c0/df9647b903d21537f1d4d1fbebc4bd1451e8e1f6090f17bdab1eba67158bd98794e535c7850be1472f839cd3f2b06398add097fc7a1a8c5c1ec936c6c0274427
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -9356,7 +9356,7 @@ __metadata:
|
||||
tsc-watch: "npm:6.2.1"
|
||||
type-is: "npm:^1.6.18"
|
||||
typescript: "npm:5.4.5"
|
||||
unleash-client: "npm:^6.3.3"
|
||||
unleash-client: "npm:^6.4.0"
|
||||
uuid: "npm:^9.0.0"
|
||||
wait-on: "npm:^7.2.0"
|
||||
languageName: unknown
|
||||
|
Loading…
Reference in New Issue
Block a user