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:
parent
f3e8f723a2
commit
42d64c8803
@ -83,7 +83,9 @@ Object {
|
|||||||
"isEnabled": [Function],
|
"isEnabled": [Function],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"frontendApiOrigins": Array [],
|
"frontendApiOrigins": Array [
|
||||||
|
"*",
|
||||||
|
],
|
||||||
"getLogger": [Function],
|
"getLogger": [Function],
|
||||||
"import": Object {
|
"import": Object {
|
||||||
"dropBeforeImport": false,
|
"dropBeforeImport": false,
|
||||||
|
@ -76,10 +76,7 @@ export default async function getApp(
|
|||||||
// Support CORS preflight requests for the frontend endpoints.
|
// Support CORS preflight requests for the frontend endpoints.
|
||||||
// Preflight requests should not have Authorization headers,
|
// Preflight requests should not have Authorization headers,
|
||||||
// so this must be handled before the API token middleware.
|
// so this must be handled before the API token middleware.
|
||||||
app.options(
|
app.options('/api/frontend*', corsOriginMiddleware(services));
|
||||||
'/api/frontend*',
|
|
||||||
corsOriginMiddleware(config.frontendApiOrigins),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (config.authentication.type) {
|
switch (config.authentication.type) {
|
||||||
|
@ -403,3 +403,28 @@ test('Environment variables for client features caching takes priority over opti
|
|||||||
expect(config.clientFeatureCaching.enabled).toBe(true);
|
expect(config.clientFeatureCaching.enabled).toBe(true);
|
||||||
expect(config.clientFeatureCaching.maxAge).toBe(120);
|
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(['*']);
|
||||||
|
});
|
||||||
|
@ -43,6 +43,7 @@ import {
|
|||||||
DEFAULT_STRATEGY_SEGMENTS_LIMIT,
|
DEFAULT_STRATEGY_SEGMENTS_LIMIT,
|
||||||
} from './util/segments';
|
} from './util/segments';
|
||||||
import FlagResolver from './util/flag-resolver';
|
import FlagResolver from './util/flag-resolver';
|
||||||
|
import { validateOrigins } from './util/validateOrigin';
|
||||||
|
|
||||||
const safeToUpper = (s: string) => (s ? s.toUpperCase() : s);
|
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 {
|
export function createConfig(options: IUnleashOptions): IUnleashConfig {
|
||||||
let extraDbOptions = {};
|
let extraDbOptions = {};
|
||||||
|
|
||||||
@ -420,10 +435,6 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
|
|||||||
DEFAULT_STRATEGY_SEGMENTS_LIMIT,
|
DEFAULT_STRATEGY_SEGMENTS_LIMIT,
|
||||||
);
|
);
|
||||||
|
|
||||||
const frontendApiOrigins =
|
|
||||||
options.frontendApiOrigins ||
|
|
||||||
parseEnvVarStrings(process.env.UNLEASH_FRONTEND_API_ORIGINS, []);
|
|
||||||
|
|
||||||
const clientFeatureCaching = loadClientCachingOptions(options);
|
const clientFeatureCaching = loadClientCachingOptions(options);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -449,7 +460,7 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig {
|
|||||||
eventBus: new EventEmitter(),
|
eventBus: new EventEmitter(),
|
||||||
environmentEnableOverrides,
|
environmentEnableOverrides,
|
||||||
additionalCspAllowedDomains,
|
additionalCspAllowedDomains,
|
||||||
frontendApiOrigins,
|
frontendApiOrigins: parseFrontendApiOrigins(options),
|
||||||
inlineSegmentConstraints,
|
inlineSegmentConstraints,
|
||||||
segmentValuesLimit,
|
segmentValuesLimit,
|
||||||
strategySegmentsLimit,
|
strategySegmentsLimit,
|
||||||
|
@ -1,4 +1,21 @@
|
|||||||
import { allowRequestOrigin } from './cors-origin-middleware';
|
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', () => {
|
test('allowRequestOrigin', () => {
|
||||||
const dotCom = 'https://example.com';
|
const dotCom = 'https://example.com';
|
||||||
@ -16,3 +33,54 @@ test('allowRequestOrigin', () => {
|
|||||||
expect(allowRequestOrigin(dotCom, [dotOrg, '*'])).toEqual(true);
|
expect(allowRequestOrigin(dotCom, [dotOrg, '*'])).toEqual(true);
|
||||||
expect(allowRequestOrigin(dotCom, [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: ['*'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -1,25 +1,33 @@
|
|||||||
import { RequestHandler } from 'express';
|
import { RequestHandler } from 'express';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
|
import { IUnleashServices } from '../types';
|
||||||
const ANY_ORIGIN = '*';
|
|
||||||
|
|
||||||
export const allowRequestOrigin = (
|
export const allowRequestOrigin = (
|
||||||
requestOrigin: string,
|
requestOrigin: string,
|
||||||
allowedOrigins: string[],
|
allowedOrigins: string[],
|
||||||
): boolean => {
|
): boolean => {
|
||||||
return allowedOrigins.some((allowedOrigin) => {
|
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.
|
// Check the request's Origin header against a list of allowed origins.
|
||||||
// The list may include '*', which `cors` does not support natively.
|
// The list may include '*', which `cors` does not support natively.
|
||||||
export const corsOriginMiddleware = (
|
export const corsOriginMiddleware = ({
|
||||||
allowedOrigins: string[],
|
settingService,
|
||||||
): RequestHandler => {
|
}: Pick<IUnleashServices, 'settingService'>): RequestHandler => {
|
||||||
return cors((req, callback) => {
|
return cors(async (req, callback) => {
|
||||||
|
try {
|
||||||
|
const { frontendApiOrigins = [] } =
|
||||||
|
await settingService.getFrontendSettings();
|
||||||
callback(null, {
|
callback(null, {
|
||||||
origin: allowRequestOrigin(req.header('Origin'), allowedOrigins),
|
origin: allowRequestOrigin(
|
||||||
|
req.header('Origin'),
|
||||||
|
frontendApiOrigins,
|
||||||
|
),
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
callback(error);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -110,6 +110,7 @@ import { proxyFeaturesSchema } from './spec/proxy-features-schema';
|
|||||||
import { proxyFeatureSchema } from './spec/proxy-feature-schema';
|
import { proxyFeatureSchema } from './spec/proxy-feature-schema';
|
||||||
import { proxyClientSchema } from './spec/proxy-client-schema';
|
import { proxyClientSchema } from './spec/proxy-client-schema';
|
||||||
import { proxyMetricsSchema } from './spec/proxy-metrics-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.
|
// All schemas in `openapi/spec` should be listed here.
|
||||||
export const schemas = {
|
export const schemas = {
|
||||||
@ -187,6 +188,7 @@ export const schemas = {
|
|||||||
searchEventsSchema,
|
searchEventsSchema,
|
||||||
segmentSchema,
|
segmentSchema,
|
||||||
setStrategySortOrderSchema,
|
setStrategySortOrderSchema,
|
||||||
|
setUiConfigSchema,
|
||||||
sortOrderSchema,
|
sortOrderSchema,
|
||||||
splashSchema,
|
splashSchema,
|
||||||
stateSchema,
|
stateSchema,
|
||||||
|
24
src/lib/openapi/spec/set-ui-config-schema.ts
Normal file
24
src/lib/openapi/spec/set-ui-config-schema.ts
Normal 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>;
|
@ -37,6 +37,12 @@ export const uiConfigSchema = {
|
|||||||
strategySegmentsLimit: {
|
strategySegmentsLimit: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
},
|
},
|
||||||
|
frontendApiOrigins: {
|
||||||
|
type: 'array',
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
flags: {
|
flags: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
additionalProperties: {
|
additionalProperties: {
|
||||||
|
@ -7,10 +7,10 @@ import Controller from '../controller';
|
|||||||
import VersionService from '../../services/version-service';
|
import VersionService from '../../services/version-service';
|
||||||
import SettingService from '../../services/setting-service';
|
import SettingService from '../../services/setting-service';
|
||||||
import {
|
import {
|
||||||
simpleAuthKey,
|
simpleAuthSettingsKey,
|
||||||
SimpleAuthSettings,
|
SimpleAuthSettings,
|
||||||
} from '../../types/settings/simple-auth-settings';
|
} 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 { createResponseSchema } from '../../openapi/util/create-response-schema';
|
||||||
import {
|
import {
|
||||||
uiConfigSchema,
|
uiConfigSchema,
|
||||||
@ -18,6 +18,12 @@ import {
|
|||||||
} from '../../openapi/spec/ui-config-schema';
|
} from '../../openapi/spec/ui-config-schema';
|
||||||
import { OpenApiService } from '../../services/openapi-service';
|
import { OpenApiService } from '../../services/openapi-service';
|
||||||
import { EmailService } from '../../services/email-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 {
|
class ConfigController extends Controller {
|
||||||
private versionService: VersionService;
|
private versionService: VersionService;
|
||||||
@ -52,26 +58,43 @@ class ConfigController extends Controller {
|
|||||||
this.route({
|
this.route({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
path: '',
|
path: '',
|
||||||
handler: this.getUIConfig,
|
handler: this.getUiConfig,
|
||||||
permission: NONE,
|
permission: NONE,
|
||||||
middleware: [
|
middleware: [
|
||||||
openApiService.validPath({
|
openApiService.validPath({
|
||||||
tags: ['Admin UI'],
|
tags: ['Admin UI'],
|
||||||
operationId: 'getUIConfig',
|
operationId: 'getUiConfig',
|
||||||
responses: {
|
responses: {
|
||||||
200: createResponseSchema('uiConfigSchema'),
|
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,
|
req: AuthedRequest,
|
||||||
res: Response<UiConfigSchema>,
|
res: Response<UiConfigSchema>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const simpleAuthSettings =
|
const [frontendSettings, simpleAuthSettings] = await Promise.all([
|
||||||
await this.settingService.get<SimpleAuthSettings>(simpleAuthKey);
|
this.settingService.getFrontendSettings(),
|
||||||
|
this.settingService.get<SimpleAuthSettings>(simpleAuthSettingsKey),
|
||||||
|
]);
|
||||||
|
|
||||||
const disablePasswordAuth =
|
const disablePasswordAuth =
|
||||||
simpleAuthSettings?.disabled ||
|
simpleAuthSettings?.disabled ||
|
||||||
@ -92,6 +115,7 @@ class ConfigController extends Controller {
|
|||||||
authenticationType: this.config.authentication?.type,
|
authenticationType: this.config.authentication?.type,
|
||||||
segmentValuesLimit: this.config.segmentValuesLimit,
|
segmentValuesLimit: this.config.segmentValuesLimit,
|
||||||
strategySegmentsLimit: this.config.strategySegmentsLimit,
|
strategySegmentsLimit: this.config.strategySegmentsLimit,
|
||||||
|
frontendApiOrigins: frontendSettings.frontendApiOrigins,
|
||||||
versionInfo: this.versionService.getVersionInfo(),
|
versionInfo: this.versionService.getVersionInfo(),
|
||||||
disablePasswordAuth,
|
disablePasswordAuth,
|
||||||
embedProxy: this.config.experimental.flags.embedProxy,
|
embedProxy: this.config.experimental.flags.embedProxy,
|
||||||
@ -104,5 +128,22 @@ class ConfigController extends Controller {
|
|||||||
response,
|
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;
|
export default ConfigController;
|
||||||
|
@ -10,7 +10,7 @@ import ResetTokenService from '../../services/reset-token-service';
|
|||||||
import { IAuthRequest } from '../unleash-types';
|
import { IAuthRequest } from '../unleash-types';
|
||||||
import SettingService from '../../services/setting-service';
|
import SettingService from '../../services/setting-service';
|
||||||
import { IUser, SimpleAuthSettings } from '../../server-impl';
|
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 { anonymise } from '../../util/anonymise';
|
||||||
import { OpenApiService } from '../../services/openapi-service';
|
import { OpenApiService } from '../../services/openapi-service';
|
||||||
import { createRequestSchema } from '../../openapi/util/create-request-schema';
|
import { createRequestSchema } from '../../openapi/util/create-request-schema';
|
||||||
@ -369,7 +369,9 @@ export default class UserAdminController extends Controller {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const passwordAuthSettings =
|
const passwordAuthSettings =
|
||||||
await this.settingService.get<SimpleAuthSettings>(simpleAuthKey);
|
await this.settingService.get<SimpleAuthSettings>(
|
||||||
|
simpleAuthSettingsKey,
|
||||||
|
);
|
||||||
|
|
||||||
let inviteLink: string;
|
let inviteLink: string;
|
||||||
if (!passwordAuthSettings?.disabled) {
|
if (!passwordAuthSettings?.disabled) {
|
||||||
|
@ -2,9 +2,7 @@ import { Response, Request } from 'express';
|
|||||||
import Controller from '../controller';
|
import Controller from '../controller';
|
||||||
import { IUnleashConfig, IUnleashServices } from '../../types';
|
import { IUnleashConfig, IUnleashServices } from '../../types';
|
||||||
import { Logger } from '../../logger';
|
import { Logger } from '../../logger';
|
||||||
import { OpenApiService } from '../../services/openapi-service';
|
|
||||||
import { NONE } from '../../types/permissions';
|
import { NONE } from '../../types/permissions';
|
||||||
import { ProxyService } from '../../services/proxy-service';
|
|
||||||
import ApiUser from '../../types/api-user';
|
import ApiUser from '../../types/api-user';
|
||||||
import {
|
import {
|
||||||
proxyFeaturesSchema,
|
proxyFeaturesSchema,
|
||||||
@ -28,29 +26,25 @@ interface ApiUserRequest<
|
|||||||
user: ApiUser;
|
user: ApiUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Services = Pick<
|
||||||
|
IUnleashServices,
|
||||||
|
'settingService' | 'proxyService' | 'openApiService'
|
||||||
|
>;
|
||||||
|
|
||||||
export default class ProxyController extends Controller {
|
export default class ProxyController extends Controller {
|
||||||
private readonly logger: Logger;
|
private readonly logger: Logger;
|
||||||
|
|
||||||
private proxyService: ProxyService;
|
private services: Services;
|
||||||
|
|
||||||
private openApiService: OpenApiService;
|
constructor(config: IUnleashConfig, services: Services) {
|
||||||
|
|
||||||
constructor(
|
|
||||||
config: IUnleashConfig,
|
|
||||||
{
|
|
||||||
proxyService,
|
|
||||||
openApiService,
|
|
||||||
}: Pick<IUnleashServices, 'proxyService' | 'openApiService'>,
|
|
||||||
) {
|
|
||||||
super(config);
|
super(config);
|
||||||
this.logger = config.getLogger('client-api/feature.js');
|
this.logger = config.getLogger('client-api/feature.js');
|
||||||
this.proxyService = proxyService;
|
this.services = services;
|
||||||
this.openApiService = openApiService;
|
|
||||||
|
|
||||||
if (config.frontendApiOrigins.length > 0) {
|
if (config.frontendApiOrigins.length > 0) {
|
||||||
// Support CORS requests for the frontend endpoints.
|
// Support CORS requests for the frontend endpoints.
|
||||||
// Preflight requests are handled in `app.ts`.
|
// Preflight requests are handled in `app.ts`.
|
||||||
this.app.use(corsOriginMiddleware(config.frontendApiOrigins));
|
this.app.use(corsOriginMiddleware(services));
|
||||||
}
|
}
|
||||||
|
|
||||||
this.route({
|
this.route({
|
||||||
@ -59,7 +53,7 @@ export default class ProxyController extends Controller {
|
|||||||
handler: this.getProxyFeatures,
|
handler: this.getProxyFeatures,
|
||||||
permission: NONE,
|
permission: NONE,
|
||||||
middleware: [
|
middleware: [
|
||||||
this.openApiService.validPath({
|
this.services.openApiService.validPath({
|
||||||
tags: ['Unstable'],
|
tags: ['Unstable'],
|
||||||
operationId: 'getFrontendFeatures',
|
operationId: 'getFrontendFeatures',
|
||||||
responses: {
|
responses: {
|
||||||
@ -89,7 +83,7 @@ export default class ProxyController extends Controller {
|
|||||||
handler: this.registerProxyMetrics,
|
handler: this.registerProxyMetrics,
|
||||||
permission: NONE,
|
permission: NONE,
|
||||||
middleware: [
|
middleware: [
|
||||||
this.openApiService.validPath({
|
this.services.openApiService.validPath({
|
||||||
tags: ['Unstable'],
|
tags: ['Unstable'],
|
||||||
operationId: 'registerFrontendMetrics',
|
operationId: 'registerFrontendMetrics',
|
||||||
requestBody: createRequestSchema('proxyMetricsSchema'),
|
requestBody: createRequestSchema('proxyMetricsSchema'),
|
||||||
@ -104,7 +98,7 @@ export default class ProxyController extends Controller {
|
|||||||
handler: ProxyController.registerProxyClient,
|
handler: ProxyController.registerProxyClient,
|
||||||
permission: NONE,
|
permission: NONE,
|
||||||
middleware: [
|
middleware: [
|
||||||
this.openApiService.validPath({
|
this.services.openApiService.validPath({
|
||||||
tags: ['Unstable'],
|
tags: ['Unstable'],
|
||||||
operationId: 'registerFrontendClient',
|
operationId: 'registerFrontendClient',
|
||||||
requestBody: createRequestSchema('proxyClientSchema'),
|
requestBody: createRequestSchema('proxyClientSchema'),
|
||||||
@ -141,11 +135,11 @@ export default class ProxyController extends Controller {
|
|||||||
req: ApiUserRequest,
|
req: ApiUserRequest,
|
||||||
res: Response<ProxyFeaturesSchema>,
|
res: Response<ProxyFeaturesSchema>,
|
||||||
) {
|
) {
|
||||||
const toggles = await this.proxyService.getProxyFeatures(
|
const toggles = await this.services.proxyService.getProxyFeatures(
|
||||||
req.user,
|
req.user,
|
||||||
ProxyController.createContext(req),
|
ProxyController.createContext(req),
|
||||||
);
|
);
|
||||||
this.openApiService.respondWithValidation(
|
this.services.openApiService.respondWithValidation(
|
||||||
200,
|
200,
|
||||||
res,
|
res,
|
||||||
proxyFeaturesSchema.$id,
|
proxyFeaturesSchema.$id,
|
||||||
@ -157,7 +151,7 @@ export default class ProxyController extends Controller {
|
|||||||
req: ApiUserRequest<unknown, unknown, ProxyMetricsSchema>,
|
req: ApiUserRequest<unknown, unknown, ProxyMetricsSchema>,
|
||||||
res: Response,
|
res: Response,
|
||||||
) {
|
) {
|
||||||
await this.proxyService.registerProxyMetrics(
|
await this.services.proxyService.registerProxyMetrics(
|
||||||
req.user,
|
req.user,
|
||||||
req.body,
|
req.body,
|
||||||
req.ip,
|
req.ip,
|
||||||
|
@ -8,31 +8,48 @@ import {
|
|||||||
SettingDeletedEvent,
|
SettingDeletedEvent,
|
||||||
SettingUpdatedEvent,
|
SettingUpdatedEvent,
|
||||||
} from '../types/events';
|
} 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 {
|
export default class SettingService {
|
||||||
|
private config: IUnleashConfig;
|
||||||
|
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
private settingStore: ISettingStore;
|
private settingStore: ISettingStore;
|
||||||
|
|
||||||
private eventStore: IEventStore;
|
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(
|
constructor(
|
||||||
{
|
{
|
||||||
settingStore,
|
settingStore,
|
||||||
eventStore,
|
eventStore,
|
||||||
}: Pick<IUnleashStores, '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.settingStore = settingStore;
|
||||||
this.eventStore = eventStore;
|
this.eventStore = eventStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
async get<T>(id: string): Promise<T> {
|
async get<T>(id: string, defaultValue?: T): Promise<T> {
|
||||||
return this.settingStore.get(id);
|
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> {
|
async insert(id: string, value: object, createdBy: string): Promise<void> {
|
||||||
|
this.cache.delete(id);
|
||||||
const exists = await this.settingStore.exists(id);
|
const exists = await this.settingStore.exists(id);
|
||||||
if (exists) {
|
if (exists) {
|
||||||
await this.settingStore.updateRow(id, value);
|
await this.settingStore.updateRow(id, value);
|
||||||
@ -54,6 +71,7 @@ export default class SettingService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async delete(id: string, createdBy: string): Promise<void> {
|
async delete(id: string, createdBy: string): Promise<void> {
|
||||||
|
this.cache.delete(id);
|
||||||
await this.settingStore.delete(id);
|
await this.settingStore.delete(id);
|
||||||
await this.eventStore.store(
|
await this.eventStore.store(
|
||||||
new SettingDeletedEvent({
|
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;
|
module.exports = SettingService;
|
||||||
|
@ -22,7 +22,7 @@ import { IUserStore } from '../types/stores/user-store';
|
|||||||
import { RoleName } from '../types/model';
|
import { RoleName } from '../types/model';
|
||||||
import SettingService from './setting-service';
|
import SettingService from './setting-service';
|
||||||
import { SimpleAuthSettings } from '../server-impl';
|
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 DisabledError from '../error/disabled-error';
|
||||||
import PasswordMismatch from '../error/password-mismatch';
|
import PasswordMismatch from '../error/password-mismatch';
|
||||||
import BadDataError from '../error/bad-data-error';
|
import BadDataError from '../error/bad-data-error';
|
||||||
@ -276,7 +276,7 @@ class UserService {
|
|||||||
|
|
||||||
async loginUser(usernameOrEmail: string, password: string): Promise<IUser> {
|
async loginUser(usernameOrEmail: string, password: string): Promise<IUser> {
|
||||||
const settings = await this.settingService.get<SimpleAuthSettings>(
|
const settings = await this.settingService.get<SimpleAuthSettings>(
|
||||||
simpleAuthKey,
|
simpleAuthSettingsKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (settings?.disabled) {
|
if (settings?.disabled) {
|
||||||
|
5
src/lib/types/settings/frontend-settings.ts
Normal file
5
src/lib/types/settings/frontend-settings.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import { IUnleashConfig } from '../option';
|
||||||
|
|
||||||
|
export const frontendSettingsKey = 'unleash.frontend';
|
||||||
|
|
||||||
|
export type FrontendSettings = Pick<IUnleashConfig, 'frontendApiOrigins'>;
|
@ -1,4 +1,5 @@
|
|||||||
export const simpleAuthKey = 'unleash.auth.simple';
|
export const simpleAuthSettingsKey = 'unleash.auth.simple';
|
||||||
|
|
||||||
export interface SimpleAuthSettings {
|
export interface SimpleAuthSettings {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
}
|
}
|
||||||
|
@ -29,9 +29,11 @@ test('parseEnvVarBoolean', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('parseEnvVarStringList', () => {
|
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(' ', [])).toEqual([]);
|
||||||
expect(parseEnvVarStrings('', ['*'])).toEqual(['*']);
|
|
||||||
expect(parseEnvVarStrings('a', ['*'])).toEqual(['a']);
|
expect(parseEnvVarStrings('a', ['*'])).toEqual(['a']);
|
||||||
expect(parseEnvVarStrings('a,b,c', [])).toEqual(['a', 'b', 'c']);
|
expect(parseEnvVarStrings('a,b,c', [])).toEqual(['a', 'b', 'c']);
|
||||||
expect(parseEnvVarStrings('a,b,c', [])).toEqual(['a', 'b', 'c']);
|
expect(parseEnvVarStrings('a,b,c', [])).toEqual(['a', 'b', 'c']);
|
||||||
|
@ -20,10 +20,10 @@ export function parseEnvVarBoolean(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function parseEnvVarStrings(
|
export function parseEnvVarStrings(
|
||||||
envVar: string,
|
envVar: string | undefined,
|
||||||
defaultVal: string[],
|
defaultVal: string[],
|
||||||
): string[] {
|
): string[] {
|
||||||
if (envVar) {
|
if (typeof envVar === 'string') {
|
||||||
return envVar
|
return envVar
|
||||||
.split(',')
|
.split(',')
|
||||||
.map((item) => item.trim())
|
.map((item) => item.trim())
|
||||||
|
24
src/lib/util/validateOrigin.test.ts
Normal file
24
src/lib/util/validateOrigin.test.ts
Normal 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);
|
||||||
|
});
|
24
src/lib/util/validateOrigin.ts
Normal file
24
src/lib/util/validateOrigin.ts
Normal 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}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
@ -1,10 +1,11 @@
|
|||||||
import dbInit, { ITestDb } from '../../helpers/database-init';
|
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 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 db: ITestDb;
|
||||||
let app;
|
let app: IUnleashTest;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('config_api_serial', getLogger);
|
db = await dbInit('config_api_serial', getLogger);
|
||||||
@ -16,24 +17,71 @@ afterAll(async () => {
|
|||||||
await db.destroy();
|
await db.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await app.services.settingService.deleteAll();
|
||||||
|
});
|
||||||
|
|
||||||
test('gets ui config fields', async () => {
|
test('gets ui config fields', async () => {
|
||||||
const { body } = await app.request
|
const { body } = await app.request
|
||||||
.get('/api/admin/ui-config')
|
.get('/api/admin/ui-config')
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(body.unleashUrl).toBe('http://localhost:4242');
|
expect(body.unleashUrl).toBe('http://localhost:4242');
|
||||||
expect(body.version).toBeDefined();
|
expect(body.version).toBeDefined();
|
||||||
expect(body.emailEnabled).toBe(false);
|
expect(body.emailEnabled).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('gets ui config with disablePasswordAuth', async () => {
|
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
|
const { body } = await app.request
|
||||||
.get('/api/admin/ui-config')
|
.get('/api/admin/ui-config')
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
expect(body.disablePasswordAuth).toBe(true);
|
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),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
@ -2498,6 +2498,28 @@ Object {
|
|||||||
},
|
},
|
||||||
"type": "array",
|
"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 {
|
"sortOrderSchema": Object {
|
||||||
"additionalProperties": Object {
|
"additionalProperties": Object {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
@ -2829,6 +2851,12 @@ Object {
|
|||||||
},
|
},
|
||||||
"type": "object",
|
"type": "object",
|
||||||
},
|
},
|
||||||
|
"frontendApiOrigins": Object {
|
||||||
|
"items": Object {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
},
|
||||||
"links": Object {
|
"links": Object {
|
||||||
"items": Object {
|
"items": Object {
|
||||||
"type": "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 {
|
"/api/admin/ui-config": Object {
|
||||||
"get": Object {
|
"get": Object {
|
||||||
"operationId": "getUIConfig",
|
"operationId": "getUiConfig",
|
||||||
"responses": Object {
|
"responses": Object {
|
||||||
"200": Object {
|
"200": Object {
|
||||||
"content": Object {
|
"content": Object {
|
||||||
@ -6189,6 +6217,28 @@ If the provided project does not exist, the list of events will be empty.",
|
|||||||
"Admin UI",
|
"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 {
|
"/api/admin/user": Object {
|
||||||
"get": Object {
|
"get": Object {
|
||||||
|
@ -11,9 +11,10 @@ import NotFoundError from '../../../lib/error/notfound-error';
|
|||||||
import { IRole } from '../../../lib/types/stores/access-store';
|
import { IRole } from '../../../lib/types/stores/access-store';
|
||||||
import { RoleName } from '../../../lib/types/model';
|
import { RoleName } from '../../../lib/types/model';
|
||||||
import SettingService from '../../../lib/services/setting-service';
|
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 { addDays, minutesToMilliseconds } from 'date-fns';
|
||||||
import { GroupService } from '../../../lib/services/group-service';
|
import { GroupService } from '../../../lib/services/group-service';
|
||||||
|
import { randomId } from '../../../lib/util/random-id';
|
||||||
|
|
||||||
let db;
|
let db;
|
||||||
let stores;
|
let stores;
|
||||||
@ -22,6 +23,7 @@ let userStore: UserStore;
|
|||||||
let adminRole: IRole;
|
let adminRole: IRole;
|
||||||
let viewerRole: IRole;
|
let viewerRole: IRole;
|
||||||
let sessionService: SessionService;
|
let sessionService: SessionService;
|
||||||
|
let settingService: SettingService;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('user_service_serial', getLogger);
|
db = await dbInit('user_service_serial', getLogger);
|
||||||
@ -32,7 +34,7 @@ beforeAll(async () => {
|
|||||||
const resetTokenService = new ResetTokenService(stores, config);
|
const resetTokenService = new ResetTokenService(stores, config);
|
||||||
const emailService = new EmailService(undefined, config.getLogger);
|
const emailService = new EmailService(undefined, config.getLogger);
|
||||||
sessionService = new SessionService(stores, config);
|
sessionService = new SessionService(stores, config);
|
||||||
const settingService = new SettingService(stores, config);
|
settingService = new SettingService(stores, config);
|
||||||
|
|
||||||
userService = new UserService(stores, config, {
|
userService = new UserService(stores, config, {
|
||||||
accessService,
|
accessService,
|
||||||
@ -101,7 +103,11 @@ test('should create user with password', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('should not login user if simple auth is disabled', 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({
|
await userService.createUser({
|
||||||
username: 'test_no_pass',
|
username: 'test_no_pass',
|
||||||
|
Loading…
Reference in New Issue
Block a user