diff --git a/frontend/src/component/admin/cors/CorsForm.tsx b/frontend/src/component/admin/cors/CorsForm.tsx
index c3885e8821c5..0f8e2d7ca4a3 100644
--- a/frontend/src/component/admin/cors/CorsForm.tsx
+++ b/frontend/src/component/admin/cors/CorsForm.tsx
@@ -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();
- await setFrontendSettings(split);
+ 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' },
}}
/>
-
+
);
diff --git a/frontend/src/component/admin/cors/index.tsx b/frontend/src/component/admin/cors/index.tsx
index ceda3f629c8b..1f84bfa1dd42 100644
--- a/frontend/src/component/admin/cors/index.tsx
+++ b/frontend/src/component/admin/cors/index.tsx
@@ -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 = () => (
diff --git a/frontend/src/hooks/api/actions/useUiConfigApi/useUiConfigApi.ts b/frontend/src/hooks/api/actions/useUiConfigApi/useUiConfigApi.ts
index 9853704beff7..d80c89761fbd 100644
--- a/frontend/src/hooks/api/actions/useUiConfigApi/useUiConfigApi.ts
+++ b/frontend/src/hooks/api/actions/useUiConfigApi/useUiConfigApi.ts
@@ -5,6 +5,9 @@ export const useUiConfigApi = () => {
propagateErrors: true,
});
+ /**
+ * @deprecated remove when `granularAdminPermissions` flag is removed
+ */
const setFrontendSettings = async (
frontendApiOrigins: string[],
): Promise => {
@@ -19,8 +22,18 @@ export const useUiConfigApi = () => {
await makeRequest(req.caller, req.id);
};
+ const setCors = async (frontendApiOrigins: string[]): Promise => {
+ 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,
};
diff --git a/src/lib/features/frontend-api/frontend-api-service.ts b/src/lib/features/frontend-api/frontend-api-service.ts
index 81858f5b2f3b..98b1d3e60158 100644
--- a/src/lib/features/frontend-api/frontend-api-service.ts
+++ b/src/lib/features/frontend-api/frontend-api-service.ts
@@ -208,6 +208,23 @@ export class FrontendApiService {
);
}
+ async setFrontendCorsSettings(
+ value: FrontendSettings['frontendApiOrigins'],
+ auditUser: IAuditUser,
+ ): Promise {
+ const error = validateOrigins(value);
+ if (error) {
+ throw new BadDataError(error);
+ }
+ const settings = (await this.getFrontendSettings()) || {};
+ await this.services.settingService.insert(
+ frontendSettingsKey,
+ { ...settings, frontendApiOrigins: value },
+ auditUser,
+ false,
+ );
+ }
+
async fetchFrontendSettings(): Promise {
try {
this.cachedFrontendSettings =
diff --git a/src/lib/openapi/spec/index.ts b/src/lib/openapi/spec/index.ts
index db6f696b1051..f934b0d23b9c 100644
--- a/src/lib/openapi/spec/index.ts
+++ b/src/lib/openapi/spec/index.ts
@@ -180,6 +180,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';
diff --git a/src/lib/openapi/spec/set-cors-schema.ts b/src/lib/openapi/spec/set-cors-schema.ts
new file mode 100644
index 000000000000..6e268e85cd99
--- /dev/null
+++ b/src/lib/openapi/spec/set-cors-schema.ts
@@ -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 configuration settings affect the admin UI.',
+ 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;
diff --git a/src/lib/routes/admin-api/config.test.ts b/src/lib/routes/admin-api/config.test.ts
index 364712cf8eb1..e65da9941393 100644
--- a/src/lib/routes/admin-api/config.test.ts
+++ b/src/lib/routes/admin-api/config.test.ts
@@ -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']);
+});
diff --git a/src/lib/routes/admin-api/config.ts b/src/lib/routes/admin-api/config.ts
index 36c28be817ca..1b206a8509e5 100644
--- a/src/lib/routes/admin-api/config.ts
+++ b/src/lib/routes/admin-api/config.ts
@@ -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(
@@ -198,6 +218,30 @@ class ConfigController extends Controller {
throw new NotFoundError();
}
+
+ async setCors(
+ req: IAuthRequest,
+ res: Response,
+ ): Promise {
+ 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;
diff --git a/src/lib/types/permissions.ts b/src/lib/types/permissions.ts
index d179e93f8833..f0a60aee1ba0 100644
--- a/src/lib/types/permissions.ts
+++ b/src/lib/types/permissions.ts
@@ -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',