mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-04 01:18:20 +02:00
feat: read logs and update cors maintenance root-role permissions (#8996)
Additional granular permissions related to instance-level access. - CORS settings - Reading logs (both instance logs and login history) --------- Co-authored-by: Gastón Fournier <gaston@getunleash.io>
This commit is contained in:
parent
cb77b106bd
commit
dc4a760172
@ -1,4 +1,3 @@
|
|||||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
|
||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { TextField, Box } from '@mui/material';
|
import { TextField, Box } from '@mui/material';
|
||||||
@ -7,23 +6,30 @@ import { useUiConfigApi } from 'hooks/api/actions/useUiConfigApi/useUiConfigApi'
|
|||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
import { useId } from 'hooks/useId';
|
import { useId } from 'hooks/useId';
|
||||||
|
import { ADMIN, UPDATE_CORS } from '@server/types/permissions';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
|
||||||
interface ICorsFormProps {
|
interface ICorsFormProps {
|
||||||
frontendApiOrigins: string[] | undefined;
|
frontendApiOrigins: string[] | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CorsForm = ({ frontendApiOrigins }: ICorsFormProps) => {
|
export const CorsForm = ({ frontendApiOrigins }: ICorsFormProps) => {
|
||||||
const { setFrontendSettings } = useUiConfigApi();
|
const { setFrontendSettings, setCors } = useUiConfigApi();
|
||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
const [value, setValue] = useState(formatInputValue(frontendApiOrigins));
|
const [value, setValue] = useState(formatInputValue(frontendApiOrigins));
|
||||||
const inputFieldId = useId();
|
const inputFieldId = useId();
|
||||||
const helpTextId = useId();
|
const helpTextId = useId();
|
||||||
|
const isGranularPermissionsEnabled = useUiFlag('granularAdminPermissions');
|
||||||
|
|
||||||
const onSubmit = async (event: React.FormEvent) => {
|
const onSubmit = async (event: React.FormEvent) => {
|
||||||
try {
|
try {
|
||||||
const split = parseInputValue(value);
|
const split = parseInputValue(value);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
await setFrontendSettings(split);
|
if (isGranularPermissionsEnabled) {
|
||||||
|
await setCors(split);
|
||||||
|
} else {
|
||||||
|
await setFrontendSettings(split);
|
||||||
|
}
|
||||||
setValue(formatInputValue(split));
|
setValue(formatInputValue(split));
|
||||||
setToastData({ text: 'Settings saved', type: 'success' });
|
setToastData({ text: 'Settings saved', type: 'success' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -67,7 +73,7 @@ export const CorsForm = ({ frontendApiOrigins }: ICorsFormProps) => {
|
|||||||
style: { fontFamily: 'monospace', fontSize: '0.8em' },
|
style: { fontFamily: 'monospace', fontSize: '0.8em' },
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<UpdateButton permission={ADMIN} />
|
<UpdateButton permission={[ADMIN, UPDATE_CORS]} />
|
||||||
</Box>
|
</Box>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
|
import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
|
||||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
|
||||||
import { PageContent } from 'component/common/PageContent/PageContent';
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import { CorsHelpAlert } from 'component/admin/cors/CorsHelpAlert';
|
import { CorsHelpAlert } from 'component/admin/cors/CorsHelpAlert';
|
||||||
import { CorsForm } from 'component/admin/cors/CorsForm';
|
import { CorsForm } from 'component/admin/cors/CorsForm';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import { ADMIN, UPDATE_CORS } from '@server/types/permissions';
|
||||||
|
|
||||||
export const CorsAdmin = () => (
|
export const CorsAdmin = () => (
|
||||||
<div>
|
<div>
|
||||||
<PermissionGuard permissions={ADMIN}>
|
<PermissionGuard permissions={[ADMIN, UPDATE_CORS]}>
|
||||||
<CorsPage />
|
<CorsPage />
|
||||||
</PermissionGuard>
|
</PermissionGuard>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
|
||||||
import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
|
import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
|
||||||
import { EventLog } from 'component/events/EventLog/EventLog';
|
import { EventLog } from 'component/events/EventLog/EventLog';
|
||||||
|
import { READ_LOGS, ADMIN } from '@server/types/permissions';
|
||||||
|
|
||||||
export const EventPage = () => (
|
export const EventPage = () => (
|
||||||
<PermissionGuard permissions={ADMIN}>
|
<PermissionGuard permissions={[ADMIN, READ_LOGS]}>
|
||||||
<EventLog title='Event log' />
|
<EventLog title='Event log' />
|
||||||
</PermissionGuard>
|
</PermissionGuard>
|
||||||
);
|
);
|
||||||
|
@ -3,6 +3,7 @@ import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuar
|
|||||||
import { LoginHistoryTable } from './LoginHistoryTable/LoginHistoryTable';
|
import { LoginHistoryTable } from './LoginHistoryTable/LoginHistoryTable';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
|
import { PremiumFeature } from 'component/common/PremiumFeature/PremiumFeature';
|
||||||
|
import { READ_LOGS } from '@server/types/permissions';
|
||||||
|
|
||||||
export const LoginHistory = () => {
|
export const LoginHistory = () => {
|
||||||
const { isEnterprise } = useUiConfig();
|
const { isEnterprise } = useUiConfig();
|
||||||
@ -13,7 +14,7 @@ export const LoginHistory = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<PermissionGuard permissions={ADMIN}>
|
<PermissionGuard permissions={[ADMIN, READ_LOGS]}>
|
||||||
<LoginHistoryTable />
|
<LoginHistoryTable />
|
||||||
</PermissionGuard>
|
</PermissionGuard>
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,6 +5,9 @@ export const useUiConfigApi = () => {
|
|||||||
propagateErrors: true,
|
propagateErrors: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated remove when `granularAdminPermissions` flag is removed
|
||||||
|
*/
|
||||||
const setFrontendSettings = async (
|
const setFrontendSettings = async (
|
||||||
frontendApiOrigins: string[],
|
frontendApiOrigins: string[],
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
@ -19,8 +22,18 @@ export const useUiConfigApi = () => {
|
|||||||
await makeRequest(req.caller, req.id);
|
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 {
|
return {
|
||||||
setFrontendSettings,
|
setFrontendSettings,
|
||||||
|
setCors,
|
||||||
loading,
|
loading,
|
||||||
errors,
|
errors,
|
||||||
};
|
};
|
||||||
|
@ -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> {
|
async fetchFrontendSettings(): Promise<FrontendSettings> {
|
||||||
try {
|
try {
|
||||||
this.cachedFrontendSettings =
|
this.cachedFrontendSettings =
|
||||||
|
@ -177,6 +177,7 @@ export * from './search-features-schema';
|
|||||||
export * from './segment-schema';
|
export * from './segment-schema';
|
||||||
export * from './segment-strategies-schema';
|
export * from './segment-strategies-schema';
|
||||||
export * from './segments-schema';
|
export * from './segments-schema';
|
||||||
|
export * from './set-cors-schema';
|
||||||
export * from './set-strategy-sort-order-schema';
|
export * from './set-strategy-sort-order-schema';
|
||||||
export * from './set-ui-config-schema';
|
export * from './set-ui-config-schema';
|
||||||
export * from './sort-order-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() {
|
async function getSetup() {
|
||||||
const base = `/random${Math.round(Math.random() * 1000)}`;
|
const base = `/random${Math.round(Math.random() * 1000)}`;
|
||||||
const config = createTestConfig({
|
const config = createTestConfig({
|
||||||
|
experimental: {
|
||||||
|
flags: {
|
||||||
|
granularAdminPermissions: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
server: { baseUriPath: base },
|
server: { baseUriPath: base },
|
||||||
ui: uiConfig,
|
ui: uiConfig,
|
||||||
});
|
});
|
||||||
@ -56,3 +61,26 @@ test('should get ui config', async () => {
|
|||||||
expect(body.segmentValuesLimit).toEqual(DEFAULT_SEGMENT_VALUES_LIMIT);
|
expect(body.segmentValuesLimit).toEqual(DEFAULT_SEGMENT_VALUES_LIMIT);
|
||||||
expect(body.strategySegmentsLimit).toEqual(DEFAULT_STRATEGY_SEGMENTS_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,
|
type SimpleAuthSettings,
|
||||||
simpleAuthSettingsKey,
|
simpleAuthSettingsKey,
|
||||||
} from '../../types/settings/simple-auth-settings';
|
} 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 { createResponseSchema } from '../../openapi/util/create-response-schema';
|
||||||
import {
|
import {
|
||||||
uiConfigSchema,
|
uiConfigSchema,
|
||||||
@ -22,6 +22,7 @@ import { emptyResponse } from '../../openapi/util/standard-responses';
|
|||||||
import type { IAuthRequest } from '../unleash-types';
|
import type { IAuthRequest } from '../unleash-types';
|
||||||
import NotFoundError from '../../error/notfound-error';
|
import NotFoundError from '../../error/notfound-error';
|
||||||
import type { SetUiConfigSchema } from '../../openapi/spec/set-ui-config-schema';
|
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 { createRequestSchema } from '../../openapi/util/create-request-schema';
|
||||||
import type { FrontendApiService, SessionService } from '../../services';
|
import type { FrontendApiService, SessionService } from '../../services';
|
||||||
import type MaintenanceService from '../../features/maintenance/maintenance-service';
|
import type MaintenanceService from '../../features/maintenance/maintenance-service';
|
||||||
@ -99,6 +100,7 @@ class ConfigController extends Controller {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO: deprecate when removing `granularAdminPermissions` flag
|
||||||
this.route({
|
this.route({
|
||||||
method: 'post',
|
method: 'post',
|
||||||
path: '',
|
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(
|
async getUiConfig(
|
||||||
@ -197,6 +217,30 @@ class ConfigController extends Controller {
|
|||||||
|
|
||||||
throw new NotFoundError();
|
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;
|
export default ConfigController;
|
||||||
|
@ -41,8 +41,10 @@ export const CREATE_TAG_TYPE = 'CREATE_TAG_TYPE';
|
|||||||
export const UPDATE_TAG_TYPE = 'UPDATE_TAG_TYPE';
|
export const UPDATE_TAG_TYPE = 'UPDATE_TAG_TYPE';
|
||||||
export const DELETE_TAG_TYPE = 'DELETE_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_MAINTENANCE_MODE = 'UPDATE_MAINTENANCE_MODE';
|
||||||
export const UPDATE_INSTANCE_BANNERS = 'UPDATE_INSTANCE_BANNERS';
|
export const UPDATE_INSTANCE_BANNERS = 'UPDATE_INSTANCE_BANNERS';
|
||||||
|
export const UPDATE_CORS = 'UPDATE_CORS';
|
||||||
export const UPDATE_AUTH_CONFIGURATION = 'UPDATE_AUTH_CONFIGURATION';
|
export const UPDATE_AUTH_CONFIGURATION = 'UPDATE_AUTH_CONFIGURATION';
|
||||||
|
|
||||||
// Project
|
// Project
|
||||||
@ -147,7 +149,12 @@ export const ROOT_PERMISSION_CATEGORIES = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Instance maintenance',
|
label: 'Instance maintenance',
|
||||||
permissions: [UPDATE_MAINTENANCE_MODE, UPDATE_INSTANCE_BANNERS],
|
permissions: [
|
||||||
|
READ_LOGS,
|
||||||
|
UPDATE_MAINTENANCE_MODE,
|
||||||
|
UPDATE_INSTANCE_BANNERS,
|
||||||
|
UPDATE_CORS,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Authentication',
|
label: 'Authentication',
|
||||||
@ -162,4 +169,5 @@ export const MAINTENANCE_MODE_PERMISSIONS = [
|
|||||||
READ_CLIENT_API_TOKEN,
|
READ_CLIENT_API_TOKEN,
|
||||||
READ_FRONTEND_API_TOKEN,
|
READ_FRONTEND_API_TOKEN,
|
||||||
UPDATE_MAINTENANCE_MODE,
|
UPDATE_MAINTENANCE_MODE,
|
||||||
|
READ_LOGS,
|
||||||
];
|
];
|
||||||
|
Loading…
Reference in New Issue
Block a user