mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	Fix/remove settings cache (#2694)
In this PR we remove the general SettingService cache, as it will not work across multiple horizontal unleash instances, events are not published across. We also fix the CORS origin to: - Access-Control-Allow-Origin set to "*" if no Origin is configured - Access-Control-Allow-Origin set to "*" if any Origin is configured to "*" - - Access-Control-Allow-Origin set to array and have the "cors" middleware to return an exact match on the user provided Origin. Co-authored-by: Fredrik Oseberg <fredrik.no@gmail.com>
This commit is contained in:
		
							parent
							
								
									db7b39af2d
								
							
						
					
					
						commit
						09c87c755f
					
				@ -6,6 +6,7 @@ import { useUiConfigApi } from 'hooks/api/actions/useUiConfigApi/useUiConfigApi'
 | 
			
		||||
import useToast from 'hooks/useToast';
 | 
			
		||||
import { formatUnknownError } from 'utils/formatUnknownError';
 | 
			
		||||
import { useId } from 'hooks/useId';
 | 
			
		||||
import { fontSize } from '@mui/system';
 | 
			
		||||
 | 
			
		||||
interface ICorsFormProps {
 | 
			
		||||
    frontendApiOrigins: string[] | undefined;
 | 
			
		||||
@ -35,8 +36,23 @@ export const CorsForm = ({ frontendApiOrigins }: ICorsFormProps) => {
 | 
			
		||||
            <Box sx={{ display: 'grid', gap: 1 }}>
 | 
			
		||||
                <label htmlFor={inputFieldId}>
 | 
			
		||||
                    Which origins should be allowed to call the Frontend API?
 | 
			
		||||
                    Add only one origin per line.
 | 
			
		||||
                    Add only one origin per line. The CORS specification does
 | 
			
		||||
                    not support wildcard for subdomains, it needs to be a fully
 | 
			
		||||
                    qualified domain, including the protocol.
 | 
			
		||||
                    <br />
 | 
			
		||||
                    <br />
 | 
			
		||||
                    If you specify "*" it will be the chosen origin.
 | 
			
		||||
                    <br />
 | 
			
		||||
                    <br />
 | 
			
		||||
                    Example:
 | 
			
		||||
                </label>
 | 
			
		||||
 | 
			
		||||
                <code style={{ fontSize: '0.7em' }}>
 | 
			
		||||
                    https://www.example.com
 | 
			
		||||
                    <br />
 | 
			
		||||
                    https://www.example2.com
 | 
			
		||||
                </code>
 | 
			
		||||
 | 
			
		||||
                <TextField
 | 
			
		||||
                    id={inputFieldId}
 | 
			
		||||
                    aria-describedby={helpTextId}
 | 
			
		||||
 | 
			
		||||
@ -17,6 +17,15 @@ export const CorsHelpAlert = () => {
 | 
			
		||||
                An asterisk (<code>*</code>) may be used to allow API calls from
 | 
			
		||||
                any origin.
 | 
			
		||||
            </p>
 | 
			
		||||
            <br />
 | 
			
		||||
            <p>
 | 
			
		||||
                Be aware that changes here will take up to two minutes to be
 | 
			
		||||
                updated. In addition, there is a maxAge on the
 | 
			
		||||
                Access-Control-Allow-Origin header that will instruct browsers
 | 
			
		||||
                to cache this header for some time. The cache period is set to
 | 
			
		||||
                the maxium that the browser allows (2h for Chrome, 24h for
 | 
			
		||||
                Firefox).
 | 
			
		||||
            </p>
 | 
			
		||||
        </Alert>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@ -1,86 +1,152 @@
 | 
			
		||||
import { allowRequestOrigin } from './cors-origin-middleware';
 | 
			
		||||
import { resolveOrigin } 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';
 | 
			
		||||
import FakeProjectStore from '../../test/fixtures/fake-project-store';
 | 
			
		||||
import { ProxyService, SettingService } from '../../lib/services';
 | 
			
		||||
import { ISettingStore } from '../../lib/types';
 | 
			
		||||
import { frontendSettingsKey } from '../../lib/types/settings/frontend-settings';
 | 
			
		||||
import { minutesToMilliseconds } from 'date-fns';
 | 
			
		||||
 | 
			
		||||
const createSettingService = (frontendApiOrigins: string[]): SettingService => {
 | 
			
		||||
const createSettingService = (
 | 
			
		||||
    frontendApiOrigins: string[],
 | 
			
		||||
): { proxyService: ProxyService; settingStore: ISettingStore } => {
 | 
			
		||||
    const config = createTestConfig({ frontendApiOrigins });
 | 
			
		||||
 | 
			
		||||
    const stores = {
 | 
			
		||||
        settingStore: new FakeSettingStore(),
 | 
			
		||||
        eventStore: new FakeEventStore(),
 | 
			
		||||
        projectStore: new FakeProjectStore(),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return new SettingService(stores, config);
 | 
			
		||||
    const services = {
 | 
			
		||||
        settingService: new SettingService(stores, config),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        //@ts-ignore
 | 
			
		||||
        proxyService: new ProxyService(config, stores, services),
 | 
			
		||||
        settingStore: stores.settingStore,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
test('allowRequestOrigin', () => {
 | 
			
		||||
test('resolveOrigin', () => {
 | 
			
		||||
    const dotCom = 'https://example.com';
 | 
			
		||||
    const dotOrg = 'https://example.org';
 | 
			
		||||
 | 
			
		||||
    expect(allowRequestOrigin('', [])).toEqual(false);
 | 
			
		||||
    expect(allowRequestOrigin(dotCom, [])).toEqual(false);
 | 
			
		||||
    expect(allowRequestOrigin(dotCom, [dotOrg])).toEqual(false);
 | 
			
		||||
 | 
			
		||||
    expect(allowRequestOrigin(dotCom, [dotCom, dotOrg])).toEqual(true);
 | 
			
		||||
    expect(allowRequestOrigin(dotCom, [dotOrg, dotCom])).toEqual(true);
 | 
			
		||||
    expect(allowRequestOrigin(dotCom, [dotCom, dotCom])).toEqual(true);
 | 
			
		||||
 | 
			
		||||
    expect(allowRequestOrigin(dotCom, ['*'])).toEqual(true);
 | 
			
		||||
    expect(allowRequestOrigin(dotCom, [dotOrg, '*'])).toEqual(true);
 | 
			
		||||
    expect(allowRequestOrigin(dotCom, [dotCom, dotOrg, '*'])).toEqual(true);
 | 
			
		||||
    expect(resolveOrigin([])).toEqual('*');
 | 
			
		||||
    expect(resolveOrigin(['*'])).toEqual('*');
 | 
			
		||||
    expect(resolveOrigin([dotOrg])).toEqual([dotOrg]);
 | 
			
		||||
    expect(resolveOrigin([dotCom, dotOrg])).toEqual([dotCom, dotOrg]);
 | 
			
		||||
    expect(resolveOrigin([dotOrg, '*'])).toEqual('*');
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('corsOriginMiddleware origin validation', async () => {
 | 
			
		||||
    const service = createSettingService([]);
 | 
			
		||||
    const { proxyService } = createSettingService([]);
 | 
			
		||||
    const userName = randomId();
 | 
			
		||||
    await expect(() =>
 | 
			
		||||
        service.setFrontendSettings({ frontendApiOrigins: ['a'] }, userName),
 | 
			
		||||
        proxyService.setFrontendSettings(
 | 
			
		||||
            { frontendApiOrigins: ['a'] },
 | 
			
		||||
            userName,
 | 
			
		||||
        ),
 | 
			
		||||
    ).rejects.toThrow('Invalid origin: a');
 | 
			
		||||
    proxyService.destroy();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('corsOriginMiddleware without config', async () => {
 | 
			
		||||
    const service = createSettingService([]);
 | 
			
		||||
    const { proxyService, settingStore } = createSettingService([]);
 | 
			
		||||
    const userName = randomId();
 | 
			
		||||
    expect(await service.getFrontendSettings()).toEqual({
 | 
			
		||||
    expect(await proxyService.getFrontendSettings(false)).toEqual({
 | 
			
		||||
        frontendApiOrigins: [],
 | 
			
		||||
    });
 | 
			
		||||
    await service.setFrontendSettings({ frontendApiOrigins: [] }, userName);
 | 
			
		||||
    expect(await service.getFrontendSettings()).toEqual({
 | 
			
		||||
    await proxyService.setFrontendSettings(
 | 
			
		||||
        { frontendApiOrigins: [] },
 | 
			
		||||
        userName,
 | 
			
		||||
    );
 | 
			
		||||
    expect(await proxyService.getFrontendSettings(false)).toEqual({
 | 
			
		||||
        frontendApiOrigins: [],
 | 
			
		||||
    });
 | 
			
		||||
    await service.setFrontendSettings({ frontendApiOrigins: ['*'] }, userName);
 | 
			
		||||
    expect(await service.getFrontendSettings()).toEqual({
 | 
			
		||||
    await proxyService.setFrontendSettings(
 | 
			
		||||
        { frontendApiOrigins: ['*'] },
 | 
			
		||||
        userName,
 | 
			
		||||
    );
 | 
			
		||||
    expect(await proxyService.getFrontendSettings(false)).toEqual({
 | 
			
		||||
        frontendApiOrigins: ['*'],
 | 
			
		||||
    });
 | 
			
		||||
    await service.delete(frontendSettingsKey, userName);
 | 
			
		||||
    expect(await service.getFrontendSettings()).toEqual({
 | 
			
		||||
    await settingStore.delete(frontendSettingsKey);
 | 
			
		||||
    expect(await proxyService.getFrontendSettings(false)).toEqual({
 | 
			
		||||
        frontendApiOrigins: [],
 | 
			
		||||
    });
 | 
			
		||||
    proxyService.destroy();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('corsOriginMiddleware with config', async () => {
 | 
			
		||||
    const service = createSettingService(['*']);
 | 
			
		||||
    const { proxyService, settingStore } = createSettingService(['*']);
 | 
			
		||||
    const userName = randomId();
 | 
			
		||||
    expect(await service.getFrontendSettings()).toEqual({
 | 
			
		||||
    expect(await proxyService.getFrontendSettings(false)).toEqual({
 | 
			
		||||
        frontendApiOrigins: ['*'],
 | 
			
		||||
    });
 | 
			
		||||
    await service.setFrontendSettings({ frontendApiOrigins: [] }, userName);
 | 
			
		||||
    expect(await service.getFrontendSettings()).toEqual({
 | 
			
		||||
    await proxyService.setFrontendSettings(
 | 
			
		||||
        { frontendApiOrigins: [] },
 | 
			
		||||
        userName,
 | 
			
		||||
    );
 | 
			
		||||
    expect(await proxyService.getFrontendSettings(false)).toEqual({
 | 
			
		||||
        frontendApiOrigins: [],
 | 
			
		||||
    });
 | 
			
		||||
    await service.setFrontendSettings(
 | 
			
		||||
    await proxyService.setFrontendSettings(
 | 
			
		||||
        { frontendApiOrigins: ['https://example.com', 'https://example.org'] },
 | 
			
		||||
        userName,
 | 
			
		||||
    );
 | 
			
		||||
    expect(await service.getFrontendSettings()).toEqual({
 | 
			
		||||
    expect(await proxyService.getFrontendSettings(false)).toEqual({
 | 
			
		||||
        frontendApiOrigins: ['https://example.com', 'https://example.org'],
 | 
			
		||||
    });
 | 
			
		||||
    await service.delete(frontendSettingsKey, userName);
 | 
			
		||||
    expect(await service.getFrontendSettings()).toEqual({
 | 
			
		||||
    await settingStore.delete(frontendSettingsKey);
 | 
			
		||||
    expect(await proxyService.getFrontendSettings(false)).toEqual({
 | 
			
		||||
        frontendApiOrigins: ['*'],
 | 
			
		||||
    });
 | 
			
		||||
    proxyService.destroy();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test('corsOriginMiddleware with caching enabled', async () => {
 | 
			
		||||
    jest.useFakeTimers();
 | 
			
		||||
 | 
			
		||||
    const { proxyService } = createSettingService([]);
 | 
			
		||||
 | 
			
		||||
    const userName = randomId();
 | 
			
		||||
    expect(await proxyService.getFrontendSettings()).toEqual({
 | 
			
		||||
        frontendApiOrigins: [],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    //setting
 | 
			
		||||
    await proxyService.setFrontendSettings(
 | 
			
		||||
        { frontendApiOrigins: ['*'] },
 | 
			
		||||
        userName,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    //still get cached value
 | 
			
		||||
    expect(await proxyService.getFrontendSettings()).toEqual({
 | 
			
		||||
        frontendApiOrigins: [],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    jest.advanceTimersByTime(minutesToMilliseconds(2));
 | 
			
		||||
 | 
			
		||||
    jest.useRealTimers();
 | 
			
		||||
 | 
			
		||||
    /*
 | 
			
		||||
    This is needed because it is not enough to fake time to test the
 | 
			
		||||
    updated cache, we also need to make sure that all promises are 
 | 
			
		||||
    executed and completed, in the right order. 
 | 
			
		||||
    */
 | 
			
		||||
    await new Promise<void>((resolve) =>
 | 
			
		||||
        process.nextTick(async () => {
 | 
			
		||||
            const settings = await proxyService.getFrontendSettings();
 | 
			
		||||
 | 
			
		||||
            expect(settings).toEqual({
 | 
			
		||||
                frontendApiOrigins: ['*'],
 | 
			
		||||
            });
 | 
			
		||||
            resolve();
 | 
			
		||||
        }),
 | 
			
		||||
    );
 | 
			
		||||
    proxyService.destroy();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -2,32 +2,32 @@ import { RequestHandler } from 'express';
 | 
			
		||||
import cors from 'cors';
 | 
			
		||||
import { IUnleashConfig, IUnleashServices } from '../types';
 | 
			
		||||
 | 
			
		||||
export const allowRequestOrigin = (
 | 
			
		||||
    requestOrigin: string,
 | 
			
		||||
    allowedOrigins: string[],
 | 
			
		||||
): boolean => {
 | 
			
		||||
    return allowedOrigins.some((allowedOrigin) => {
 | 
			
		||||
        return allowedOrigin === requestOrigin || allowedOrigin === '*';
 | 
			
		||||
    });
 | 
			
		||||
export const resolveOrigin = (allowedOrigins: string[]): string | string[] => {
 | 
			
		||||
    if (allowedOrigins.length === 0) {
 | 
			
		||||
        return '*';
 | 
			
		||||
    }
 | 
			
		||||
    if (allowedOrigins.some((origin: string) => origin === '*')) {
 | 
			
		||||
        return '*';
 | 
			
		||||
    } else {
 | 
			
		||||
        return allowedOrigins;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// 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 = (
 | 
			
		||||
    { settingService }: Pick<IUnleashServices, 'settingService'>,
 | 
			
		||||
    { proxyService }: Pick<IUnleashServices, 'proxyService'>,
 | 
			
		||||
    config: IUnleashConfig,
 | 
			
		||||
): RequestHandler => {
 | 
			
		||||
    return cors(async (req, callback) => {
 | 
			
		||||
        try {
 | 
			
		||||
            const { frontendApiOrigins = [] } =
 | 
			
		||||
                await settingService.getFrontendSettings();
 | 
			
		||||
                await proxyService.getFrontendSettings();
 | 
			
		||||
            callback(null, {
 | 
			
		||||
                origin: allowRequestOrigin(
 | 
			
		||||
                    req.header('Origin'),
 | 
			
		||||
                    frontendApiOrigins,
 | 
			
		||||
                ),
 | 
			
		||||
                origin: resolveOrigin(frontendApiOrigins),
 | 
			
		||||
                maxAge: config.accessControlMaxAge,
 | 
			
		||||
                exposedHeaders: 'ETag',
 | 
			
		||||
                credentials: true,
 | 
			
		||||
            });
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
            callback(error);
 | 
			
		||||
 | 
			
		||||
@ -24,12 +24,15 @@ 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';
 | 
			
		||||
import { ProxyService } from 'lib/services';
 | 
			
		||||
 | 
			
		||||
class ConfigController extends Controller {
 | 
			
		||||
    private versionService: VersionService;
 | 
			
		||||
 | 
			
		||||
    private settingService: SettingService;
 | 
			
		||||
 | 
			
		||||
    private proxyService: ProxyService;
 | 
			
		||||
 | 
			
		||||
    private emailService: EmailService;
 | 
			
		||||
 | 
			
		||||
    private readonly openApiService: OpenApiService;
 | 
			
		||||
@ -41,12 +44,14 @@ class ConfigController extends Controller {
 | 
			
		||||
            settingService,
 | 
			
		||||
            emailService,
 | 
			
		||||
            openApiService,
 | 
			
		||||
            proxyService,
 | 
			
		||||
        }: Pick<
 | 
			
		||||
            IUnleashServices,
 | 
			
		||||
            | 'versionService'
 | 
			
		||||
            | 'settingService'
 | 
			
		||||
            | 'emailService'
 | 
			
		||||
            | 'openApiService'
 | 
			
		||||
            | 'proxyService'
 | 
			
		||||
        >,
 | 
			
		||||
    ) {
 | 
			
		||||
        super(config);
 | 
			
		||||
@ -54,6 +59,7 @@ class ConfigController extends Controller {
 | 
			
		||||
        this.settingService = settingService;
 | 
			
		||||
        this.emailService = emailService;
 | 
			
		||||
        this.openApiService = openApiService;
 | 
			
		||||
        this.proxyService = proxyService;
 | 
			
		||||
 | 
			
		||||
        this.route({
 | 
			
		||||
            method: 'get',
 | 
			
		||||
@ -92,7 +98,7 @@ class ConfigController extends Controller {
 | 
			
		||||
        res: Response<UiConfigSchema>,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        const [frontendSettings, simpleAuthSettings] = await Promise.all([
 | 
			
		||||
            this.settingService.getFrontendSettings(),
 | 
			
		||||
            this.proxyService.getFrontendSettings(false),
 | 
			
		||||
            this.settingService.get<SimpleAuthSettings>(simpleAuthSettingsKey),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
@ -133,7 +139,7 @@ class ConfigController extends Controller {
 | 
			
		||||
        res: Response<string>,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        if (req.body.frontendSettings) {
 | 
			
		||||
            await this.settingService.setFrontendSettings(
 | 
			
		||||
            await this.proxyService.setFrontendSettings(
 | 
			
		||||
                req.body.frontendSettings,
 | 
			
		||||
                extractUsername(req),
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
@ -55,6 +55,7 @@ async function createApp(
 | 
			
		||||
        metricsMonitor.stopMonitoring();
 | 
			
		||||
        stores.clientInstanceStore.destroy();
 | 
			
		||||
        services.clientMetricsServiceV2.destroy();
 | 
			
		||||
        services.proxyService.destroy();
 | 
			
		||||
        await db.destroy();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -109,6 +109,7 @@ export const createServices = (
 | 
			
		||||
        featureToggleServiceV2,
 | 
			
		||||
        clientMetricsServiceV2,
 | 
			
		||||
        segmentService,
 | 
			
		||||
        settingService,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const edgeService = new EdgeService(stores, config);
 | 
			
		||||
 | 
			
		||||
@ -10,16 +10,29 @@ import {
 | 
			
		||||
    UnleashEvents,
 | 
			
		||||
} from 'unleash-client';
 | 
			
		||||
import { ProxyRepository } from '../proxy';
 | 
			
		||||
import assert from 'assert';
 | 
			
		||||
import { ApiTokenType } from '../types/models/api-token';
 | 
			
		||||
import {
 | 
			
		||||
    FrontendSettings,
 | 
			
		||||
    frontendSettingsKey,
 | 
			
		||||
} from '../types/settings/frontend-settings';
 | 
			
		||||
import { validateOrigins } from '../util';
 | 
			
		||||
import { BadDataError } from '../error';
 | 
			
		||||
import assert from 'assert';
 | 
			
		||||
import { minutesToMilliseconds } from 'date-fns';
 | 
			
		||||
 | 
			
		||||
type Config = Pick<IUnleashConfig, 'getLogger' | 'frontendApi'>;
 | 
			
		||||
type Config = Pick<
 | 
			
		||||
    IUnleashConfig,
 | 
			
		||||
    'getLogger' | 'frontendApi' | 'frontendApiOrigins'
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
type Stores = Pick<IUnleashStores, 'projectStore' | 'eventStore'>;
 | 
			
		||||
 | 
			
		||||
type Services = Pick<
 | 
			
		||||
    IUnleashServices,
 | 
			
		||||
    'featureToggleServiceV2' | 'segmentService' | 'clientMetricsServiceV2'
 | 
			
		||||
    | 'featureToggleServiceV2'
 | 
			
		||||
    | 'segmentService'
 | 
			
		||||
    | 'clientMetricsServiceV2'
 | 
			
		||||
    | 'settingService'
 | 
			
		||||
>;
 | 
			
		||||
 | 
			
		||||
export class ProxyService {
 | 
			
		||||
@ -33,11 +46,20 @@ export class ProxyService {
 | 
			
		||||
 | 
			
		||||
    private readonly clients: Map<ApiUser['secret'], Unleash> = new Map();
 | 
			
		||||
 | 
			
		||||
    private cachedFrontendSettings?: FrontendSettings;
 | 
			
		||||
 | 
			
		||||
    private timer: NodeJS.Timeout;
 | 
			
		||||
 | 
			
		||||
    constructor(config: Config, stores: Stores, services: Services) {
 | 
			
		||||
        this.config = config;
 | 
			
		||||
        this.logger = config.getLogger('services/proxy-service.ts');
 | 
			
		||||
        this.stores = stores;
 | 
			
		||||
        this.services = services;
 | 
			
		||||
 | 
			
		||||
        this.timer = setInterval(
 | 
			
		||||
            () => this.fetchFrontendSettings(),
 | 
			
		||||
            minutesToMilliseconds(2),
 | 
			
		||||
        ).unref();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getProxyFeatures(
 | 
			
		||||
@ -138,4 +160,43 @@ export class ProxyService {
 | 
			
		||||
    private static assertExpectedTokenType({ type }: ApiUser) {
 | 
			
		||||
        assert(type === ApiTokenType.FRONTEND || type === ApiTokenType.ADMIN);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async setFrontendSettings(
 | 
			
		||||
        value: FrontendSettings,
 | 
			
		||||
        createdBy: string,
 | 
			
		||||
    ): Promise<void> {
 | 
			
		||||
        const error = validateOrigins(value.frontendApiOrigins);
 | 
			
		||||
        if (error) {
 | 
			
		||||
            throw new BadDataError(error);
 | 
			
		||||
        }
 | 
			
		||||
        await this.services.settingService.insert(
 | 
			
		||||
            frontendSettingsKey,
 | 
			
		||||
            value,
 | 
			
		||||
            createdBy,
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async fetchFrontendSettings(): Promise<FrontendSettings> {
 | 
			
		||||
        this.cachedFrontendSettings = await this.services.settingService.get(
 | 
			
		||||
            frontendSettingsKey,
 | 
			
		||||
            {
 | 
			
		||||
                frontendApiOrigins: this.config.frontendApiOrigins,
 | 
			
		||||
            },
 | 
			
		||||
        );
 | 
			
		||||
        return this.cachedFrontendSettings;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async getFrontendSettings(
 | 
			
		||||
        useCache: boolean = true,
 | 
			
		||||
    ): Promise<FrontendSettings> {
 | 
			
		||||
        if (useCache && this.cachedFrontendSettings) {
 | 
			
		||||
            return this.cachedFrontendSettings;
 | 
			
		||||
        }
 | 
			
		||||
        return this.fetchFrontendSettings();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    destroy(): void {
 | 
			
		||||
        clearInterval(this.timer);
 | 
			
		||||
        this.timer = null;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -8,12 +8,6 @@ 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;
 | 
			
		||||
@ -24,10 +18,6 @@ export default class SettingService {
 | 
			
		||||
 | 
			
		||||
    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,
 | 
			
		||||
@ -42,14 +32,11 @@ export default class SettingService {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    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;
 | 
			
		||||
        const value = await this.settingStore.get(id);
 | 
			
		||||
        return value || 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);
 | 
			
		||||
@ -71,7 +58,6 @@ 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({
 | 
			
		||||
@ -84,26 +70,6 @@ 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;
 | 
			
		||||
 | 
			
		||||
@ -44,7 +44,7 @@ test('gets ui config with disablePasswordAuth', async () => {
 | 
			
		||||
 | 
			
		||||
test('gets ui config with frontendSettings', async () => {
 | 
			
		||||
    const frontendApiOrigins = ['https://example.net'];
 | 
			
		||||
    await app.services.settingService.setFrontendSettings(
 | 
			
		||||
    await app.services.proxyService.setFrontendSettings(
 | 
			
		||||
        { frontendApiOrigins },
 | 
			
		||||
        randomId(),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
@ -47,6 +47,7 @@ async function createApp(
 | 
			
		||||
        services.clientInstanceService.destroy();
 | 
			
		||||
        services.clientMetricsServiceV2.destroy();
 | 
			
		||||
        services.apiTokenService.destroy();
 | 
			
		||||
        services.proxyService.destroy();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // TODO: use create from server-impl instead?
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user