Skip to content

Commit

Permalink
feat: read logs and update cors maintenance root-role permissions (#8996
Browse files Browse the repository at this point in the history
)

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>
  • Loading branch information
Tymek and gastonfournier authored Jan 8, 2025
1 parent cb77b10 commit dc4a760
Show file tree
Hide file tree
Showing 11 changed files with 149 additions and 11 deletions.
14 changes: 10 additions & 4 deletions frontend/src/component/admin/cors/CorsForm.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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) {
Expand Down Expand Up @@ -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>
);
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/component/admin/cors/index.tsx
Original file line number Diff line number Diff line change
@@ -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>
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/component/events/EventPage/EventPage.tsx
Original file line number Diff line number Diff line change
@@ -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 changes: 2 additions & 1 deletion frontend/src/component/loginHistory/LoginHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -13,7 +14,7 @@ export const LoginHistory = () => {

return (
<div>
<PermissionGuard permissions={ADMIN}>
<PermissionGuard permissions={[ADMIN, READ_LOGS]}>
<LoginHistoryTable />
</PermissionGuard>
</div>
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/hooks/api/actions/useUiConfigApi/useUiConfigApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ export const useUiConfigApi = () => {
propagateErrors: true,
});

/**
* @deprecated remove when `granularAdminPermissions` flag is removed
*/
const setFrontendSettings = async (
frontendApiOrigins: string[],
): Promise<void> => {
Expand All @@ -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,
};
Expand Down
17 changes: 17 additions & 0 deletions src/lib/features/frontend-api/frontend-api-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
1 change: 1 addition & 0 deletions src/lib/openapi/spec/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
20 changes: 20 additions & 0 deletions src/lib/openapi/spec/set-cors-schema.ts
Original file line number Diff line number Diff line change
@@ -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>;
28 changes: 28 additions & 0 deletions src/lib/routes/admin-api/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand Down Expand Up @@ -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']);
});
46 changes: 45 additions & 1 deletion src/lib/routes/admin-api/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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';
Expand Down Expand Up @@ -99,6 +100,7 @@ class ConfigController extends Controller {
],
});

// TODO: deprecate when removing `granularAdminPermissions` flag
this.route({
method: 'post',
path: '',
Expand All @@ -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(
Expand Down Expand Up @@ -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;
10 changes: 9 additions & 1 deletion src/lib/types/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand All @@ -162,4 +169,5 @@ export const MAINTENANCE_MODE_PERMISSIONS = [
READ_CLIENT_API_TOKEN,
READ_FRONTEND_API_TOKEN,
UPDATE_MAINTENANCE_MODE,
READ_LOGS,
];

0 comments on commit dc4a760

Please sign in to comment.