1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-24 01:18:01 +02:00

feat: add CORS instance settings (#1957)

* feat: add CORS instance settings

* refactor: disallow arbitrary asterisks in CORS origins
This commit is contained in:
olav 2022-08-26 09:09:48 +02:00 committed by GitHub
parent f3e8f723a2
commit 42d64c8803
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 451 additions and 71 deletions

View File

@ -83,7 +83,9 @@ Object {
"isEnabled": [Function],
},
},
"frontendApiOrigins": Array [],
"frontendApiOrigins": Array [
"*",
],
"getLogger": [Function],
"import": Object {
"dropBeforeImport": false,

View File

@ -76,10 +76,7 @@ export default async function getApp(
// Support CORS preflight requests for the frontend endpoints.
// Preflight requests should not have Authorization headers,
// so this must be handled before the API token middleware.
app.options(
'/api/frontend*',
corsOriginMiddleware(config.frontendApiOrigins),
);
app.options('/api/frontend*', corsOriginMiddleware(services));
}
switch (config.authentication.type) {

View File

@ -403,3 +403,28 @@ test('Environment variables for client features caching takes priority over opti
expect(config.clientFeatureCaching.enabled).toBe(true);
expect(config.clientFeatureCaching.maxAge).toBe(120);
});
test('Environment variables for frontend CORS origins takes priority over options', async () => {
const create = (frontendApiOrigins?): string[] => {
return createConfig({
frontendApiOrigins,
}).frontendApiOrigins;
};
expect(create()).toEqual(['*']);
expect(create([])).toEqual([]);
expect(create(['*'])).toEqual(['*']);
expect(create(['https://example.com'])).toEqual(['https://example.com']);
expect(() => create(['a'])).toThrow('Invalid origin: a');
process.env.UNLEASH_FRONTEND_API_ORIGINS = '';
expect(create()).toEqual([]);
process.env.UNLEASH_FRONTEND_API_ORIGINS = '*';
expect(create()).toEqual(['*']);
process.env.UNLEASH_FRONTEND_API_ORIGINS = 'https://example.com, *';
expect(create()).toEqual(['https://example.com', '*']);
process.env.UNLEASH_FRONTEND_API_ORIGINS = 'b';
expect(() => create(['a'])).toThrow('Invalid origin: b');
delete process.env.UNLEASH_FRONTEND_API_ORIGINS;
expect(create()).toEqual(['*']);
});

View File

@ -43,6 +43,7 @@ import {
DEFAULT_STRATEGY_SEGMENTS_LIMIT,
} from './util/segments';
import FlagResolver from './util/flag-resolver';
import { validateOrigins } from './util/validateOrigin';
const safeToUpper = (s: string) => (s ? s.toUpperCase() : s);
@ -311,6 +312,20 @@ const parseCspEnvironmentVariables = (): ICspDomainConfig => {
};
};
const parseFrontendApiOrigins = (options: IUnleashOptions): string[] => {
const frontendApiOrigins = parseEnvVarStrings(
process.env.UNLEASH_FRONTEND_API_ORIGINS,
options.frontendApiOrigins || ['*'],
);
const error = validateOrigins(frontendApiOrigins);
if (error) {
throw new Error(error);
}
return frontendApiOrigins;
};
export function createConfig(options: IUnleashOptions): IUnleashConfig {
let extraDbOptions = {};
@ -420,10 +435,6 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
DEFAULT_STRATEGY_SEGMENTS_LIMIT,
);
const frontendApiOrigins =
options.frontendApiOrigins ||
parseEnvVarStrings(process.env.UNLEASH_FRONTEND_API_ORIGINS, []);
const clientFeatureCaching = loadClientCachingOptions(options);
return {
@ -449,7 +460,7 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
eventBus: new EventEmitter(),
environmentEnableOverrides,
additionalCspAllowedDomains,
frontendApiOrigins,
frontendApiOrigins: parseFrontendApiOrigins(options),
inlineSegmentConstraints,
segmentValuesLimit,
strategySegmentsLimit,

View File

@ -1,4 +1,21 @@
import { allowRequestOrigin } from './cors-origin-middleware';
import FakeSettingStore from '../../test/fixtures/fake-setting-store';
import SettingService from '../services/setting-service';
import { createTestConfig } from '../../test/config/test-config';
import FakeEventStore from '../../test/fixtures/fake-event-store';
import { randomId } from '../util/random-id';
import { frontendSettingsKey } from '../types/settings/frontend-settings';
const createSettingService = (frontendApiOrigins: string[]): SettingService => {
const config = createTestConfig({ frontendApiOrigins });
const stores = {
settingStore: new FakeSettingStore(),
eventStore: new FakeEventStore(),
};
return new SettingService(stores, config);
};
test('allowRequestOrigin', () => {
const dotCom = 'https://example.com';
@ -16,3 +33,54 @@ test('allowRequestOrigin', () => {
expect(allowRequestOrigin(dotCom, [dotOrg, '*'])).toEqual(true);
expect(allowRequestOrigin(dotCom, [dotCom, dotOrg, '*'])).toEqual(true);
});
test('corsOriginMiddleware origin validation', async () => {
const service = createSettingService([]);
const userName = randomId();
await expect(() =>
service.setFrontendSettings({ frontendApiOrigins: ['a'] }, userName),
).rejects.toThrow('Invalid origin: a');
});
test('corsOriginMiddleware without config', async () => {
const service = createSettingService([]);
const userName = randomId();
expect(await service.getFrontendSettings()).toEqual({
frontendApiOrigins: [],
});
await service.setFrontendSettings({ frontendApiOrigins: [] }, userName);
expect(await service.getFrontendSettings()).toEqual({
frontendApiOrigins: [],
});
await service.setFrontendSettings({ frontendApiOrigins: ['*'] }, userName);
expect(await service.getFrontendSettings()).toEqual({
frontendApiOrigins: ['*'],
});
await service.delete(frontendSettingsKey, userName);
expect(await service.getFrontendSettings()).toEqual({
frontendApiOrigins: [],
});
});
test('corsOriginMiddleware with config', async () => {
const service = createSettingService(['*']);
const userName = randomId();
expect(await service.getFrontendSettings()).toEqual({
frontendApiOrigins: ['*'],
});
await service.setFrontendSettings({ frontendApiOrigins: [] }, userName);
expect(await service.getFrontendSettings()).toEqual({
frontendApiOrigins: [],
});
await service.setFrontendSettings(
{ frontendApiOrigins: ['https://example.com', 'https://example.org'] },
userName,
);
expect(await service.getFrontendSettings()).toEqual({
frontendApiOrigins: ['https://example.com', 'https://example.org'],
});
await service.delete(frontendSettingsKey, userName);
expect(await service.getFrontendSettings()).toEqual({
frontendApiOrigins: ['*'],
});
});

View File

@ -1,25 +1,33 @@
import { RequestHandler } from 'express';
import cors from 'cors';
const ANY_ORIGIN = '*';
import { IUnleashServices } from '../types';
export const allowRequestOrigin = (
requestOrigin: string,
allowedOrigins: string[],
): boolean => {
return allowedOrigins.some((allowedOrigin) => {
return allowedOrigin === requestOrigin || allowedOrigin === ANY_ORIGIN;
return allowedOrigin === requestOrigin || allowedOrigin === '*';
});
};
// Check the request's Origin header against a list of allowed origins.
// The list may include '*', which `cors` does not support natively.
export const corsOriginMiddleware = (
allowedOrigins: string[],
): RequestHandler => {
return cors((req, callback) => {
export const corsOriginMiddleware = ({
settingService,
}: Pick<IUnleashServices, 'settingService'>): RequestHandler => {
return cors(async (req, callback) => {
try {
const { frontendApiOrigins = [] } =
await settingService.getFrontendSettings();
callback(null, {
origin: allowRequestOrigin(req.header('Origin'), allowedOrigins),
origin: allowRequestOrigin(
req.header('Origin'),
frontendApiOrigins,
),
});
} catch (error) {
callback(error);
}
});
};

View File

@ -110,6 +110,7 @@ import { proxyFeaturesSchema } from './spec/proxy-features-schema';
import { proxyFeatureSchema } from './spec/proxy-feature-schema';
import { proxyClientSchema } from './spec/proxy-client-schema';
import { proxyMetricsSchema } from './spec/proxy-metrics-schema';
import { setUiConfigSchema } from './spec/set-ui-config-schema';
// All schemas in `openapi/spec` should be listed here.
export const schemas = {
@ -187,6 +188,7 @@ export const schemas = {
searchEventsSchema,
segmentSchema,
setStrategySortOrderSchema,
setUiConfigSchema,
sortOrderSchema,
splashSchema,
stateSchema,

View File

@ -0,0 +1,24 @@
import { FromSchema } from 'json-schema-to-ts';
export const setUiConfigSchema = {
$id: '#/components/schemas/setUiConfigSchema',
type: 'object',
additionalProperties: false,
properties: {
frontendSettings: {
type: 'object',
additionalProperties: false,
required: ['frontendApiOrigins'],
properties: {
frontendApiOrigins: {
type: 'array',
additionalProperties: false,
items: { type: 'string' },
},
},
},
},
components: {},
} as const;
export type SetUiConfigSchema = FromSchema<typeof setUiConfigSchema>;

View File

@ -37,6 +37,12 @@ export const uiConfigSchema = {
strategySegmentsLimit: {
type: 'number',
},
frontendApiOrigins: {
type: 'array',
items: {
type: 'string',
},
},
flags: {
type: 'object',
additionalProperties: {

View File

@ -7,10 +7,10 @@ import Controller from '../controller';
import VersionService from '../../services/version-service';
import SettingService from '../../services/setting-service';
import {
simpleAuthKey,
simpleAuthSettingsKey,
SimpleAuthSettings,
} from '../../types/settings/simple-auth-settings';
import { NONE } from '../../types/permissions';
import { ADMIN, NONE } from '../../types/permissions';
import { createResponseSchema } from '../../openapi/util/create-response-schema';
import {
uiConfigSchema,
@ -18,6 +18,12 @@ import {
} from '../../openapi/spec/ui-config-schema';
import { OpenApiService } from '../../services/openapi-service';
import { EmailService } from '../../services/email-service';
import { emptyResponse } from '../../openapi/util/standard-responses';
import { IAuthRequest } from '../unleash-types';
import { extractUsername } from '../../util/extract-user';
import NotFoundError from '../../error/notfound-error';
import { SetUiConfigSchema } from '../../openapi/spec/set-ui-config-schema';
import { createRequestSchema } from '../../openapi/util/create-request-schema';
class ConfigController extends Controller {
private versionService: VersionService;
@ -52,26 +58,43 @@ class ConfigController extends Controller {
this.route({
method: 'get',
path: '',
handler: this.getUIConfig,
handler: this.getUiConfig,
permission: NONE,
middleware: [
openApiService.validPath({
tags: ['Admin UI'],
operationId: 'getUIConfig',
operationId: 'getUiConfig',
responses: {
200: createResponseSchema('uiConfigSchema'),
},
}),
],
});
this.route({
method: 'post',
path: '',
handler: this.setUiConfig,
permission: ADMIN,
middleware: [
openApiService.validPath({
tags: ['Admin UI'],
operationId: 'setUiConfig',
requestBody: createRequestSchema('setUiConfigSchema'),
responses: { 200: emptyResponse },
}),
],
});
}
async getUIConfig(
async getUiConfig(
req: AuthedRequest,
res: Response<UiConfigSchema>,
): Promise<void> {
const simpleAuthSettings =
await this.settingService.get<SimpleAuthSettings>(simpleAuthKey);
const [frontendSettings, simpleAuthSettings] = await Promise.all([
this.settingService.getFrontendSettings(),
this.settingService.get<SimpleAuthSettings>(simpleAuthSettingsKey),
]);
const disablePasswordAuth =
simpleAuthSettings?.disabled ||
@ -92,6 +115,7 @@ class ConfigController extends Controller {
authenticationType: this.config.authentication?.type,
segmentValuesLimit: this.config.segmentValuesLimit,
strategySegmentsLimit: this.config.strategySegmentsLimit,
frontendApiOrigins: frontendSettings.frontendApiOrigins,
versionInfo: this.versionService.getVersionInfo(),
disablePasswordAuth,
embedProxy: this.config.experimental.flags.embedProxy,
@ -104,5 +128,22 @@ class ConfigController extends Controller {
response,
);
}
async setUiConfig(
req: IAuthRequest<void, void, SetUiConfigSchema>,
res: Response<string>,
): Promise<void> {
if (req.body.frontendSettings) {
await this.settingService.setFrontendSettings(
req.body.frontendSettings,
extractUsername(req),
);
res.sendStatus(204);
return;
}
throw new NotFoundError();
}
}
export default ConfigController;

View File

@ -10,7 +10,7 @@ import ResetTokenService from '../../services/reset-token-service';
import { IAuthRequest } from '../unleash-types';
import SettingService from '../../services/setting-service';
import { IUser, SimpleAuthSettings } from '../../server-impl';
import { simpleAuthKey } from '../../types/settings/simple-auth-settings';
import { simpleAuthSettingsKey } from '../../types/settings/simple-auth-settings';
import { anonymise } from '../../util/anonymise';
import { OpenApiService } from '../../services/openapi-service';
import { createRequestSchema } from '../../openapi/util/create-request-schema';
@ -369,7 +369,9 @@ export default class UserAdminController extends Controller {
);
const passwordAuthSettings =
await this.settingService.get<SimpleAuthSettings>(simpleAuthKey);
await this.settingService.get<SimpleAuthSettings>(
simpleAuthSettingsKey,
);
let inviteLink: string;
if (!passwordAuthSettings?.disabled) {

View File

@ -2,9 +2,7 @@ import { Response, Request } from 'express';
import Controller from '../controller';
import { IUnleashConfig, IUnleashServices } from '../../types';
import { Logger } from '../../logger';
import { OpenApiService } from '../../services/openapi-service';
import { NONE } from '../../types/permissions';
import { ProxyService } from '../../services/proxy-service';
import ApiUser from '../../types/api-user';
import {
proxyFeaturesSchema,
@ -28,29 +26,25 @@ interface ApiUserRequest<
user: ApiUser;
}
type Services = Pick<
IUnleashServices,
'settingService' | 'proxyService' | 'openApiService'
>;
export default class ProxyController extends Controller {
private readonly logger: Logger;
private proxyService: ProxyService;
private services: Services;
private openApiService: OpenApiService;
constructor(
config: IUnleashConfig,
{
proxyService,
openApiService,
}: Pick<IUnleashServices, 'proxyService' | 'openApiService'>,
) {
constructor(config: IUnleashConfig, services: Services) {
super(config);
this.logger = config.getLogger('client-api/feature.js');
this.proxyService = proxyService;
this.openApiService = openApiService;
this.services = services;
if (config.frontendApiOrigins.length > 0) {
// Support CORS requests for the frontend endpoints.
// Preflight requests are handled in `app.ts`.
this.app.use(corsOriginMiddleware(config.frontendApiOrigins));
this.app.use(corsOriginMiddleware(services));
}
this.route({
@ -59,7 +53,7 @@ export default class ProxyController extends Controller {
handler: this.getProxyFeatures,
permission: NONE,
middleware: [
this.openApiService.validPath({
this.services.openApiService.validPath({
tags: ['Unstable'],
operationId: 'getFrontendFeatures',
responses: {
@ -89,7 +83,7 @@ export default class ProxyController extends Controller {
handler: this.registerProxyMetrics,
permission: NONE,
middleware: [
this.openApiService.validPath({
this.services.openApiService.validPath({
tags: ['Unstable'],
operationId: 'registerFrontendMetrics',
requestBody: createRequestSchema('proxyMetricsSchema'),
@ -104,7 +98,7 @@ export default class ProxyController extends Controller {
handler: ProxyController.registerProxyClient,
permission: NONE,
middleware: [
this.openApiService.validPath({
this.services.openApiService.validPath({
tags: ['Unstable'],
operationId: 'registerFrontendClient',
requestBody: createRequestSchema('proxyClientSchema'),
@ -141,11 +135,11 @@ export default class ProxyController extends Controller {
req: ApiUserRequest,
res: Response<ProxyFeaturesSchema>,
) {
const toggles = await this.proxyService.getProxyFeatures(
const toggles = await this.services.proxyService.getProxyFeatures(
req.user,
ProxyController.createContext(req),
);
this.openApiService.respondWithValidation(
this.services.openApiService.respondWithValidation(
200,
res,
proxyFeaturesSchema.$id,
@ -157,7 +151,7 @@ export default class ProxyController extends Controller {
req: ApiUserRequest<unknown, unknown, ProxyMetricsSchema>,
res: Response,
) {
await this.proxyService.registerProxyMetrics(
await this.services.proxyService.registerProxyMetrics(
req.user,
req.body,
req.ip,

View File

@ -8,31 +8,48 @@ import {
SettingDeletedEvent,
SettingUpdatedEvent,
} from '../types/events';
import { validateOrigins } from '../util/validateOrigin';
import {
FrontendSettings,
frontendSettingsKey,
} from '../types/settings/frontend-settings';
import BadDataError from '../error/bad-data-error';
export default class SettingService {
private config: IUnleashConfig;
private logger: Logger;
private settingStore: ISettingStore;
private eventStore: IEventStore;
// SettingService.getFrontendSettings is called on every request to the
// frontend API. Keep fetched settings in a cache for fewer DB queries.
private cache = new Map<string, unknown>();
constructor(
{
settingStore,
eventStore,
}: Pick<IUnleashStores, 'settingStore' | 'eventStore'>,
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
config: IUnleashConfig,
) {
this.logger = getLogger('services/setting-service.ts');
this.config = config;
this.logger = config.getLogger('services/setting-service.ts');
this.settingStore = settingStore;
this.eventStore = eventStore;
}
async get<T>(id: string): Promise<T> {
return this.settingStore.get(id);
async get<T>(id: string, defaultValue?: T): Promise<T> {
if (!this.cache.has(id)) {
this.cache.set(id, await this.settingStore.get(id));
}
return (this.cache.get(id) as T) || defaultValue;
}
async insert(id: string, value: object, createdBy: string): Promise<void> {
this.cache.delete(id);
const exists = await this.settingStore.exists(id);
if (exists) {
await this.settingStore.updateRow(id, value);
@ -54,6 +71,7 @@ export default class SettingService {
}
async delete(id: string, createdBy: string): Promise<void> {
this.cache.delete(id);
await this.settingStore.delete(id);
await this.eventStore.store(
new SettingDeletedEvent({
@ -64,6 +82,28 @@ export default class SettingService {
}),
);
}
async deleteAll(): Promise<void> {
this.cache.clear();
await this.settingStore.deleteAll();
}
async setFrontendSettings(
value: FrontendSettings,
createdBy: string,
): Promise<void> {
const error = validateOrigins(value.frontendApiOrigins);
if (error) {
throw new BadDataError(error);
}
await this.insert(frontendSettingsKey, value, createdBy);
}
async getFrontendSettings(): Promise<FrontendSettings> {
return this.get(frontendSettingsKey, {
frontendApiOrigins: this.config.frontendApiOrigins,
});
}
}
module.exports = SettingService;

View File

@ -22,7 +22,7 @@ import { IUserStore } from '../types/stores/user-store';
import { RoleName } from '../types/model';
import SettingService from './setting-service';
import { SimpleAuthSettings } from '../server-impl';
import { simpleAuthKey } from '../types/settings/simple-auth-settings';
import { simpleAuthSettingsKey } from '../types/settings/simple-auth-settings';
import DisabledError from '../error/disabled-error';
import PasswordMismatch from '../error/password-mismatch';
import BadDataError from '../error/bad-data-error';
@ -276,7 +276,7 @@ class UserService {
async loginUser(usernameOrEmail: string, password: string): Promise<IUser> {
const settings = await this.settingService.get<SimpleAuthSettings>(
simpleAuthKey,
simpleAuthSettingsKey,
);
if (settings?.disabled) {

View File

@ -0,0 +1,5 @@
import { IUnleashConfig } from '../option';
export const frontendSettingsKey = 'unleash.frontend';
export type FrontendSettings = Pick<IUnleashConfig, 'frontendApiOrigins'>;

View File

@ -1,4 +1,5 @@
export const simpleAuthKey = 'unleash.auth.simple';
export const simpleAuthSettingsKey = 'unleash.auth.simple';
export interface SimpleAuthSettings {
disabled: boolean;
}

View File

@ -29,9 +29,11 @@ test('parseEnvVarBoolean', () => {
});
test('parseEnvVarStringList', () => {
expect(parseEnvVarStrings(undefined, [])).toEqual([]);
expect(parseEnvVarStrings(undefined, ['a'])).toEqual(['a']);
expect(parseEnvVarStrings('', ['a'])).toEqual([]);
expect(parseEnvVarStrings('', [])).toEqual([]);
expect(parseEnvVarStrings(' ', [])).toEqual([]);
expect(parseEnvVarStrings('', ['*'])).toEqual(['*']);
expect(parseEnvVarStrings('a', ['*'])).toEqual(['a']);
expect(parseEnvVarStrings('a,b,c', [])).toEqual(['a', 'b', 'c']);
expect(parseEnvVarStrings('a,b,c', [])).toEqual(['a', 'b', 'c']);

View File

@ -20,10 +20,10 @@ export function parseEnvVarBoolean(
}
export function parseEnvVarStrings(
envVar: string,
envVar: string | undefined,
defaultVal: string[],
): string[] {
if (envVar) {
if (typeof envVar === 'string') {
return envVar
.split(',')
.map((item) => item.trim())

View File

@ -0,0 +1,24 @@
import { validateOrigin } from './validateOrigin';
test('validateOrigin', () => {
expect(validateOrigin(undefined)).toEqual(false);
expect(validateOrigin('')).toEqual(false);
expect(validateOrigin(' ')).toEqual(false);
expect(validateOrigin('a')).toEqual(false);
expect(validateOrigin('**')).toEqual(false);
expect(validateOrigin('localhost')).toEqual(false);
expect(validateOrigin('localhost:8080')).toEqual(false);
expect(validateOrigin('//localhost:8080')).toEqual(false);
expect(validateOrigin('http://localhost/')).toEqual(false);
expect(validateOrigin('http://localhost/a')).toEqual(false);
expect(validateOrigin('https://example.com/a')).toEqual(false);
expect(validateOrigin('https://example.com ')).toEqual(false);
expect(validateOrigin('https://*.example.com ')).toEqual(false);
expect(validateOrigin('*.example.com')).toEqual(false);
expect(validateOrigin('*')).toEqual(true);
expect(validateOrigin('http://localhost')).toEqual(true);
expect(validateOrigin('http://localhost:8080')).toEqual(true);
expect(validateOrigin('https://example.com')).toEqual(true);
expect(validateOrigin('https://example.com:8080')).toEqual(true);
});

View File

@ -0,0 +1,24 @@
export const validateOrigin = (origin: string): boolean => {
if (origin === '*') {
return true;
}
if (origin?.includes('*')) {
return false;
}
try {
const parsed = new URL(origin);
return parsed.origin && parsed.origin === origin;
} catch {
return false;
}
};
export const validateOrigins = (origins: string[]): string | undefined => {
for (const origin of origins) {
if (!validateOrigin(origin)) {
return `Invalid origin: ${origin}`;
}
}
};

View File

@ -1,10 +1,11 @@
import dbInit, { ITestDb } from '../../helpers/database-init';
import { setupApp } from '../../helpers/test-helper';
import { IUnleashTest, setupApp } from '../../helpers/test-helper';
import getLogger from '../../../fixtures/no-logger';
import { simpleAuthKey } from '../../../../lib/types/settings/simple-auth-settings';
import { simpleAuthSettingsKey } from '../../../../lib/types/settings/simple-auth-settings';
import { randomId } from '../../../../lib/util/random-id';
let db: ITestDb;
let app;
let app: IUnleashTest;
beforeAll(async () => {
db = await dbInit('config_api_serial', getLogger);
@ -16,24 +17,71 @@ afterAll(async () => {
await db.destroy();
});
beforeEach(async () => {
await app.services.settingService.deleteAll();
});
test('gets ui config fields', async () => {
const { body } = await app.request
.get('/api/admin/ui-config')
.expect('Content-Type', /json/)
.expect(200);
expect(body.unleashUrl).toBe('http://localhost:4242');
expect(body.version).toBeDefined();
expect(body.emailEnabled).toBe(false);
});
test('gets ui config with disablePasswordAuth', async () => {
await db.stores.settingStore.insert(simpleAuthKey, { disabled: true });
await db.stores.settingStore.insert(simpleAuthSettingsKey, {
disabled: true,
});
const { body } = await app.request
.get('/api/admin/ui-config')
.expect('Content-Type', /json/)
.expect(200);
expect(body.disablePasswordAuth).toBe(true);
});
test('gets ui config with frontendSettings', async () => {
const frontendApiOrigins = ['https://example.net'];
await app.services.settingService.setFrontendSettings(
{ frontendApiOrigins },
randomId(),
);
await app.request
.get('/api/admin/ui-config')
.expect('Content-Type', /json/)
.expect(200)
.expect((res) =>
expect(res.body.frontendApiOrigins).toEqual(frontendApiOrigins),
);
});
test('sets ui config with frontendSettings', async () => {
const frontendApiOrigins = ['https://example.org'];
await app.request
.get('/api/admin/ui-config')
.expect('Content-Type', /json/)
.expect(200)
.expect((res) => expect(res.body.frontendApiOrigins).toEqual(['*']));
await app.request
.post('/api/admin/ui-config')
.send({ frontendSettings: { frontendApiOrigins: [] } })
.expect(204);
await app.request
.get('/api/admin/ui-config')
.expect('Content-Type', /json/)
.expect(200)
.expect((res) => expect(res.body.frontendApiOrigins).toEqual([]));
await app.request
.post('/api/admin/ui-config')
.send({ frontendSettings: { frontendApiOrigins } })
.expect(204);
await app.request
.get('/api/admin/ui-config')
.expect('Content-Type', /json/)
.expect(200)
.expect((res) =>
expect(res.body.frontendApiOrigins).toEqual(frontendApiOrigins),
);
});

View File

@ -2498,6 +2498,28 @@ Object {
},
"type": "array",
},
"setUiConfigSchema": Object {
"additionalProperties": false,
"properties": Object {
"frontendSettings": Object {
"additionalProperties": false,
"properties": Object {
"frontendApiOrigins": Object {
"additionalProperties": false,
"items": Object {
"type": "string",
},
"type": "array",
},
},
"required": Array [
"frontendApiOrigins",
],
"type": "object",
},
},
"type": "object",
},
"sortOrderSchema": Object {
"additionalProperties": Object {
"type": "number",
@ -2829,6 +2851,12 @@ Object {
},
"type": "object",
},
"frontendApiOrigins": Object {
"items": Object {
"type": "string",
},
"type": "array",
},
"links": Object {
"items": Object {
"type": "object",
@ -6172,7 +6200,7 @@ If the provided project does not exist, the list of events will be empty.",
},
"/api/admin/ui-config": Object {
"get": Object {
"operationId": "getUIConfig",
"operationId": "getUiConfig",
"responses": Object {
"200": Object {
"content": Object {
@ -6189,6 +6217,28 @@ If the provided project does not exist, the list of events will be empty.",
"Admin UI",
],
},
"post": Object {
"operationId": "setUiConfig",
"requestBody": Object {
"content": Object {
"application/json": Object {
"schema": Object {
"$ref": "#/components/schemas/setUiConfigSchema",
},
},
},
"description": "setUiConfigSchema",
"required": true,
},
"responses": Object {
"200": Object {
"description": "This response has no body.",
},
},
"tags": Array [
"Admin UI",
],
},
},
"/api/admin/user": Object {
"get": Object {

View File

@ -11,9 +11,10 @@ import NotFoundError from '../../../lib/error/notfound-error';
import { IRole } from '../../../lib/types/stores/access-store';
import { RoleName } from '../../../lib/types/model';
import SettingService from '../../../lib/services/setting-service';
import { simpleAuthKey } from '../../../lib/types/settings/simple-auth-settings';
import { simpleAuthSettingsKey } from '../../../lib/types/settings/simple-auth-settings';
import { addDays, minutesToMilliseconds } from 'date-fns';
import { GroupService } from '../../../lib/services/group-service';
import { randomId } from '../../../lib/util/random-id';
let db;
let stores;
@ -22,6 +23,7 @@ let userStore: UserStore;
let adminRole: IRole;
let viewerRole: IRole;
let sessionService: SessionService;
let settingService: SettingService;
beforeAll(async () => {
db = await dbInit('user_service_serial', getLogger);
@ -32,7 +34,7 @@ beforeAll(async () => {
const resetTokenService = new ResetTokenService(stores, config);
const emailService = new EmailService(undefined, config.getLogger);
sessionService = new SessionService(stores, config);
const settingService = new SettingService(stores, config);
settingService = new SettingService(stores, config);
userService = new UserService(stores, config, {
accessService,
@ -101,7 +103,11 @@ test('should create user with password', async () => {
});
test('should not login user if simple auth is disabled', async () => {
await db.stores.settingStore.insert(simpleAuthKey, { disabled: true });
await settingService.insert(
simpleAuthSettingsKey,
{ disabled: true },
randomId(),
);
await userService.createUser({
username: 'test_no_pass',