From 66907bfd8ef33c3e134ada07a606c5e8d9789b02 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Thu, 19 Dec 2024 14:33:54 +0100 Subject: [PATCH] feat: only display oss included projects/environments when install is oss (#8896) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trying again, now with a tested function for resolvingIsOss. Still want to test this on a pro instance in sandbox before we deploy this to our customers to avoid what happened Friday. --------- Co-authored-by: Gastón Fournier --- .../__snapshots__/create-config.test.ts.snap | 1 + src/lib/create-config.test.ts | 35 +++++++++- src/lib/create-config.ts | 20 ++++++ src/lib/db/feature-environment-store.ts | 31 ++++++++- src/lib/db/index.ts | 14 ++-- .../features/access/createAccessService.ts | 2 +- .../api-tokens/createApiTokenService.ts | 2 +- .../client-feature-toggle-store.ts | 10 +-- .../createClientFeatureToggleService.ts | 13 ++-- .../createExportImportService.ts | 9 +-- .../createFeatureLifecycle.ts | 4 +- .../createFeatureToggleService.ts | 14 ++-- .../createInstanceStatsService.ts | 9 +-- .../createEnvironmentService.ts | 11 +-- .../project-environments/environment-store.ts | 37 ++++++++-- .../createProjectInsightsService.ts | 7 +- .../createProjectStatusService.ts | 7 +- .../features/project/createProjectService.ts | 11 +-- src/lib/features/project/project-store.ts | 16 ++++- src/lib/middleware/rbac-middleware.ts | 20 +++++- src/lib/types/option.ts | 2 + .../e2e/api/admin/environment-oss.e2e.test.ts | 68 +++++++++++++++++++ 22 files changed, 252 insertions(+), 91 deletions(-) create mode 100644 src/test/e2e/api/admin/environment-oss.e2e.test.ts diff --git a/src/lib/__snapshots__/create-config.test.ts.snap b/src/lib/__snapshots__/create-config.test.ts.snap index d056cc87f375..6248cd1cfb8c 100644 --- a/src/lib/__snapshots__/create-config.test.ts.snap +++ b/src/lib/__snapshots__/create-config.test.ts.snap @@ -85,6 +85,7 @@ exports[`should create default config 1`] = ` }, "inlineSegmentConstraints": true, "isEnterprise": false, + "isOss": false, "listen": { "host": undefined, "port": 4242, diff --git a/src/lib/create-config.test.ts b/src/lib/create-config.test.ts index e69eb7b500df..940eedd1e37b 100644 --- a/src/lib/create-config.test.ts +++ b/src/lib/create-config.test.ts @@ -1,4 +1,4 @@ -import { createConfig } from './create-config'; +import { createConfig, resolveIsOss } from './create-config'; import { ApiTokenType } from './types/models/api-token'; test('should create default config', async () => { @@ -498,3 +498,36 @@ test('Config with enterpriseVersion set and not pro environment should set isEnt }); expect(config.isEnterprise).toBe(true); }); + +describe('isOSS', () => { + test('Config with pro environment should set isOss to false regardless of pro casing', async () => { + const isOss = resolveIsOss(false, false, 'Pro'); + expect(isOss).toBe(false); + const lowerCase = resolveIsOss(false, false, 'pro'); + expect(lowerCase).toBe(false); + const strangeCase = resolveIsOss(false, false, 'PrO'); + expect(strangeCase).toBe(false); + }); + test('Config with enterpriseVersion set should set isOss to false', async () => { + const isOss = resolveIsOss(true, false, 'Enterprise'); + expect(isOss).toBe(false); + }); + test('Config with no enterprise version and any other environment than pro should have isOss as true', async () => { + const isOss = resolveIsOss(false, false, 'my oss environment'); + expect(isOss).toBe(true); + }); + test('Config with enterprise false and isOss option set to false should return false in test mode', async () => { + const isOss = resolveIsOss(false, false, 'my environment', true); + expect(isOss).toBe(false); + }); + test('Config with isOss option set to true should return true when test environment is active', async () => { + let isOss = resolveIsOss(false, true, 'Pro', true); + expect(isOss).toBe(true); + + isOss = resolveIsOss(true, true, 'Pro', true); + expect(isOss).toBe(true); + + isOss = resolveIsOss(false, true, 'some environment', true); + expect(isOss).toBe(true); + }); +}); diff --git a/src/lib/create-config.ts b/src/lib/create-config.ts index bfd59d732458..25194c47217a 100644 --- a/src/lib/create-config.ts +++ b/src/lib/create-config.ts @@ -496,6 +496,17 @@ const parseFrontendApiOrigins = (options: IUnleashOptions): string[] => { return frontendApiOrigins; }; +export function resolveIsOss( + isEnterprise: boolean, + isOssOption?: boolean, + uiEnvironment?: string, + testEnvironmentActive: boolean = false, +): boolean { + return testEnvironmentActive + ? (isOssOption ?? false) + : !isEnterprise && uiEnvironment?.toLowerCase() !== 'pro'; +} + export function createConfig(options: IUnleashOptions): IUnleashConfig { let extraDbOptions = {}; @@ -620,6 +631,13 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig { Boolean(options.enterpriseVersion) && ui.environment?.toLowerCase() !== 'pro'; + const isTest = process.env.NODE_ENV === 'test'; + const isOss = resolveIsOss( + isEnterprise, + options.isOss, + ui.environment, + isTest, + ); const metricsRateLimiting = loadMetricsRateLimitingConfig(options); const rateLimiting = loadRateLimitingConfig(options); @@ -760,6 +778,7 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig { publicFolder: options.publicFolder, disableScheduler: options.disableScheduler, isEnterprise: isEnterprise, + isOss: isOss, metricsRateLimiting, rateLimiting, feedbackUriPath, @@ -771,5 +790,6 @@ export function createConfig(options: IUnleashOptions): IUnleashConfig { module.exports = { createConfig, + resolveIsOss, authTypeFromString, }; diff --git a/src/lib/db/feature-environment-store.ts b/src/lib/db/feature-environment-store.ts index 6b3cb7392539..3bb80159bff9 100644 --- a/src/lib/db/feature-environment-store.ts +++ b/src/lib/db/feature-environment-store.ts @@ -3,13 +3,14 @@ import type { FeatureEnvironmentKey, IFeatureEnvironmentStore, } from '../types/stores/feature-environment-store'; -import type { Logger, LogProvider } from '../logger'; +import type { Logger } from '../logger'; import metricsHelper from '../util/metrics-helper'; import { DB_TIME } from '../metric-events'; import type { IFeatureEnvironment, IVariant } from '../types/model'; import NotFoundError from '../error/notfound-error'; import { v4 as uuidv4 } from 'uuid'; import type { Db } from './db'; +import type { IUnleashConfig } from '../types'; const T = { featureEnvs: 'feature_environments', @@ -36,7 +37,12 @@ export class FeatureEnvironmentStore implements IFeatureEnvironmentStore { private readonly timer: Function; - constructor(db: Db, eventBus: EventEmitter, getLogger: LogProvider) { + private readonly isOss: boolean; + constructor( + db: Db, + eventBus: EventEmitter, + { getLogger, isOss }: Pick, + ) { this.db = db; this.logger = getLogger('feature-environment-store.ts'); this.timer = (action) => @@ -44,6 +50,7 @@ export class FeatureEnvironmentStore implements IFeatureEnvironmentStore { store: 'feature-environments', action, }); + this.isOss = isOss; } async delete({ @@ -96,11 +103,30 @@ export class FeatureEnvironmentStore implements IFeatureEnvironmentStore { ); } + addOssFilterIfNeeded(queryBuilder) { + if (this.isOss) { + return queryBuilder + .join( + 'environments', + 'environments.name', + '=', + `${T.featureEnvs}.environment`, + ) + .whereIn('environments.name', [ + 'default', + 'development', + 'production', + ]); + } + return queryBuilder; + } + async getAll(query?: Object): Promise { let rows = this.db(T.featureEnvs); if (query) { rows = rows.where(query); } + this.addOssFilterIfNeeded(rows); return (await rows).map((r) => ({ enabled: r.enabled, featureName: r.feature_name, @@ -119,6 +145,7 @@ export class FeatureEnvironmentStore implements IFeatureEnvironmentStore { if (environment) { rows = rows.where({ environment }); } + this.addOssFilterIfNeeded(rows); return (await rows).map((r) => ({ enabled: r.enabled, featureName: r.feature_name, diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index 72ddcd4b497b..3722d91d46fd 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -94,12 +94,7 @@ export const createStores = ( settingStore: new SettingStore(db, getLogger), userStore: new UserStore(db, getLogger, config.flagResolver), accountStore: new AccountStore(db, getLogger), - projectStore: new ProjectStore( - db, - eventBus, - getLogger, - config.flagResolver, - ), + projectStore: new ProjectStore(db, eventBus, config), tagStore: new TagStore(db, eventBus, getLogger), tagTypeStore: new TagTypeStore(db, eventBus, getLogger), addonStore: new AddonStore(db, eventBus, getLogger), @@ -122,15 +117,14 @@ export const createStores = ( clientFeatureToggleStore: new FeatureToggleClientStore( db, eventBus, - getLogger, - config.flagResolver, + config, ), - environmentStore: new EnvironmentStore(db, eventBus, getLogger), + environmentStore: new EnvironmentStore(db, eventBus, config), featureTagStore: new FeatureTagStore(db, eventBus, getLogger), featureEnvironmentStore: new FeatureEnvironmentStore( db, eventBus, - getLogger, + config, ), userSplashStore: new UserSplashStore(db, eventBus, getLogger), roleStore: new RoleStore(db, eventBus, getLogger), diff --git a/src/lib/features/access/createAccessService.ts b/src/lib/features/access/createAccessService.ts index 26791fc82fac..c381c0ed278c 100644 --- a/src/lib/features/access/createAccessService.ts +++ b/src/lib/features/access/createAccessService.ts @@ -25,7 +25,7 @@ export const createAccessService = ( const groupStore = new GroupStore(db); const accountStore = new AccountStore(db, getLogger); const roleStore = new RoleStore(db, eventBus, getLogger); - const environmentStore = new EnvironmentStore(db, eventBus, getLogger); + const environmentStore = new EnvironmentStore(db, eventBus, config); const accessStore = new AccessStore(db, eventBus, getLogger); const eventService = createEventsService(db, config); const groupService = new GroupService( diff --git a/src/lib/features/api-tokens/createApiTokenService.ts b/src/lib/features/api-tokens/createApiTokenService.ts index 18b236c628c8..0dec7de7a457 100644 --- a/src/lib/features/api-tokens/createApiTokenService.ts +++ b/src/lib/features/api-tokens/createApiTokenService.ts @@ -21,7 +21,7 @@ export const createApiTokenService = ( getLogger, config.flagResolver, ); - const environmentStore = new EnvironmentStore(db, eventBus, getLogger); + const environmentStore = new EnvironmentStore(db, eventBus, config); const eventService = createEventsService(db, config); return new ApiTokenService( diff --git a/src/lib/features/client-feature-toggles/client-feature-toggle-store.ts b/src/lib/features/client-feature-toggles/client-feature-toggle-store.ts index b93d94daeeda..435dae0d74f7 100644 --- a/src/lib/features/client-feature-toggles/client-feature-toggle-store.ts +++ b/src/lib/features/client-feature-toggles/client-feature-toggle-store.ts @@ -1,7 +1,7 @@ import { Knex } from 'knex'; import metricsHelper from '../../util/metrics-helper'; import { DB_TIME } from '../../metric-events'; -import type { Logger, LogProvider } from '../../logger'; +import type { Logger } from '../../logger'; import type { IFeatureToggleClient, IFeatureToggleClientStore, @@ -9,6 +9,7 @@ import type { IFlagResolver, IStrategyConfig, ITag, + IUnleashConfig, PartialDeep, } from '../../types'; import { @@ -49,8 +50,10 @@ export default class FeatureToggleClientStore constructor( db: Db, eventBus: EventEmitter, - getLogger: LogProvider, - flagResolver: IFlagResolver, + { + getLogger, + flagResolver, + }: Pick, ) { this.db = db; this.logger = getLogger('feature-toggle-client-store.ts'); @@ -72,7 +75,6 @@ export default class FeatureToggleClientStore const isPlayground = requestType === 'playground'; const environment = featureQuery?.environment || DEFAULT_ENV; const stopTimer = this.timer(`getAllBy${requestType}`); - let selectColumns = [ 'features.name as name', 'features.description as description', diff --git a/src/lib/features/client-feature-toggles/createClientFeatureToggleService.ts b/src/lib/features/client-feature-toggles/createClientFeatureToggleService.ts index f48d98e0d136..17be891a7a7d 100644 --- a/src/lib/features/client-feature-toggles/createClientFeatureToggleService.ts +++ b/src/lib/features/client-feature-toggles/createClientFeatureToggleService.ts @@ -11,13 +11,10 @@ export const createClientFeatureToggleService = ( db: Db, config: IUnleashConfig, ): ClientFeatureToggleService => { - const { getLogger, eventBus, flagResolver } = config; - const featureToggleClientStore = new FeatureToggleClientStore( db, - eventBus, - getLogger, - flagResolver, + config.eventBus, + config, ); const segmentReadModel = new SegmentReadModel(db); @@ -30,7 +27,7 @@ export const createClientFeatureToggleService = ( }, segmentReadModel, clientFeatureToggleCache, - { getLogger, flagResolver }, + config, ); return clientFeatureToggleService; @@ -39,8 +36,6 @@ export const createClientFeatureToggleService = ( export const createFakeClientFeatureToggleService = ( config: IUnleashConfig, ): ClientFeatureToggleService => { - const { getLogger, flagResolver } = config; - const fakeClientFeatureToggleStore = new FakeClientFeatureToggleStore(); const fakeSegmentReadModel = new FakeSegmentReadModel(); @@ -51,7 +46,7 @@ export const createFakeClientFeatureToggleService = ( }, fakeSegmentReadModel, null, - { getLogger, flagResolver }, + config, ); return clientFeatureToggleService; diff --git a/src/lib/features/export-import-toggles/createExportImportService.ts b/src/lib/features/export-import-toggles/createExportImportService.ts index b36b6e0fabf3..69bebf187066 100644 --- a/src/lib/features/export-import-toggles/createExportImportService.ts +++ b/src/lib/features/export-import-toggles/createExportImportService.ts @@ -150,12 +150,7 @@ export const deferredExportImportTogglesService = ( ); const tagStore = new TagStore(db, eventBus, getLogger); const tagTypeStore = new TagTypeStore(db, eventBus, getLogger); - const projectStore = new ProjectStore( - db, - eventBus, - getLogger, - flagResolver, - ); + const projectStore = new ProjectStore(db, eventBus, config); const featureTagStore = new FeatureTagStore(db, eventBus, getLogger); const strategyStore = new StrategyStore(db, getLogger); const contextFieldStore = new ContextFieldStore( @@ -172,7 +167,7 @@ export const deferredExportImportTogglesService = ( const featureEnvironmentStore = new FeatureEnvironmentStore( db, eventBus, - getLogger, + config, ); const eventStore = new EventStore(db, getLogger); const accessService = createAccessService(db, config); diff --git a/src/lib/features/feature-lifecycle/createFeatureLifecycle.ts b/src/lib/features/feature-lifecycle/createFeatureLifecycle.ts index a97b540b6043..ed907e9f894b 100644 --- a/src/lib/features/feature-lifecycle/createFeatureLifecycle.ts +++ b/src/lib/features/feature-lifecycle/createFeatureLifecycle.ts @@ -19,11 +19,11 @@ export const createFeatureLifecycleService = const { eventBus, getLogger } = config; const eventStore = new EventStore(db, getLogger); const featureLifecycleStore = new FeatureLifecycleStore(db); - const environmentStore = new EnvironmentStore(db, eventBus, getLogger); + const environmentStore = new EnvironmentStore(db, eventBus, config); const featureEnvironmentStore = new FeatureEnvironmentStore( db, eventBus, - getLogger, + config, ); const eventService = createEventsService(db, config); const featureLifecycleService = new FeatureLifecycleService( diff --git a/src/lib/features/feature-toggle/createFeatureToggleService.ts b/src/lib/features/feature-toggle/createFeatureToggleService.ts index d5d9d21bae87..b12eb3adfc17 100644 --- a/src/lib/features/feature-toggle/createFeatureToggleService.ts +++ b/src/lib/features/feature-toggle/createFeatureToggleService.ts @@ -80,19 +80,13 @@ export const createFeatureToggleService = ( const featureToggleClientStore = new FeatureToggleClientStore( db, eventBus, - getLogger, - flagResolver, - ); - const projectStore = new ProjectStore( - db, - eventBus, - getLogger, - flagResolver, + config, ); + const projectStore = new ProjectStore(db, eventBus, config); const featureEnvironmentStore = new FeatureEnvironmentStore( db, eventBus, - getLogger, + config, ); const contextFieldStore = new ContextFieldStore( db, @@ -105,7 +99,7 @@ export const createFeatureToggleService = ( const accessStore = new AccessStore(db, eventBus, getLogger); const featureTagStore = new FeatureTagStore(db, eventBus, getLogger); const roleStore = new RoleStore(db, eventBus, getLogger); - const environmentStore = new EnvironmentStore(db, eventBus, getLogger); + const environmentStore = new EnvironmentStore(db, eventBus, config); const eventService = createEventsService(db, config); const groupService = new GroupService( { groupStore, accountStore }, diff --git a/src/lib/features/instance-stats/createInstanceStatsService.ts b/src/lib/features/instance-stats/createInstanceStatsService.ts index f1718c149166..fb03d5ec8a1a 100644 --- a/src/lib/features/instance-stats/createInstanceStatsService.ts +++ b/src/lib/features/instance-stats/createInstanceStatsService.ts @@ -58,13 +58,8 @@ export const createInstanceStatsService = (db: Db, config: IUnleashConfig) => { flagResolver, ); const userStore = new UserStore(db, getLogger, flagResolver); - const projectStore = new ProjectStore( - db, - eventBus, - getLogger, - flagResolver, - ); - const environmentStore = new EnvironmentStore(db, eventBus, getLogger); + const projectStore = new ProjectStore(db, eventBus, config); + const environmentStore = new EnvironmentStore(db, eventBus, config); const strategyStore = new StrategyStore(db, getLogger); const contextFieldStore = new ContextFieldStore( db, diff --git a/src/lib/features/project-environments/createEnvironmentService.ts b/src/lib/features/project-environments/createEnvironmentService.ts index 9cbcb478c193..9a1dfbd6c626 100644 --- a/src/lib/features/project-environments/createEnvironmentService.ts +++ b/src/lib/features/project-environments/createEnvironmentService.ts @@ -21,21 +21,16 @@ export const createEnvironmentService = const featureEnvironmentStore = new FeatureEnvironmentStore( db, eventBus, - getLogger, - ); - const projectStore = new ProjectStore( - db, - eventBus, - getLogger, - flagResolver, + config, ); + const projectStore = new ProjectStore(db, eventBus, config); const featureStrategiesStore = new FeatureStrategiesStore( db, eventBus, getLogger, flagResolver, ); - const environmentStore = new EnvironmentStore(db, eventBus, getLogger); + const environmentStore = new EnvironmentStore(db, eventBus, config); const eventService = createEventsService(db, config); return new EnvironmentService( { diff --git a/src/lib/features/project-environments/environment-store.ts b/src/lib/features/project-environments/environment-store.ts index 6838d6eb4a81..3da9522b0018 100644 --- a/src/lib/features/project-environments/environment-store.ts +++ b/src/lib/features/project-environments/environment-store.ts @@ -1,6 +1,6 @@ import type EventEmitter from 'events'; import type { Db } from '../../db/db'; -import type { Logger, LogProvider } from '../../logger'; +import type { Logger } from '../../logger'; import metricsHelper from '../../util/metrics-helper'; import { DB_TIME } from '../../metric-events'; import type { @@ -12,6 +12,7 @@ import NotFoundError from '../../error/notfound-error'; import type { IEnvironmentStore } from './environment-store-type'; import { snakeCaseKeys } from '../../util/snakeCase'; import type { CreateFeatureStrategySchema } from '../../openapi'; +import type { IUnleashConfig } from '../../types'; interface IEnvironmentsTable { name: string; @@ -104,11 +105,18 @@ export default class EnvironmentStore implements IEnvironmentStore { private db: Db; + private isOss: boolean; + private timer: (string) => any; - constructor(db: Db, eventBus: EventEmitter, getLogger: LogProvider) { + constructor( + db: Db, + eventBus: EventEmitter, + { getLogger, isOss }: Pick, + ) { this.db = db; this.logger = getLogger('db/environment-store.ts'); + this.isOss = isOss; this.timer = (action) => metricsHelper.wrapTimer(eventBus, DB_TIME, { store: 'environment', @@ -148,9 +156,15 @@ export default class EnvironmentStore implements IEnvironmentStore { async get(key: string): Promise { const stopTimer = this.timer('get'); - const row = await this.db(TABLE) - .where({ name: key }) - .first(); + let keyQuery = this.db(TABLE).where({ name: key }); + if (this.isOss) { + keyQuery = keyQuery.whereIn('name', [ + 'default', + 'development', + 'production', + ]); + } + const row = await keyQuery.first(); stopTimer(); if (row) { return mapRow(row); @@ -169,6 +183,9 @@ export default class EnvironmentStore implements IEnvironmentStore { if (query) { qB = qB.where(query); } + if (this.isOss) { + qB = qB.whereIn('name', ['default', 'development', 'production']); + } const rows = await qB; stopTimer(); return rows.map(mapRow); @@ -196,6 +213,9 @@ export default class EnvironmentStore implements IEnvironmentStore { if (query) { qB = qB.where(query); } + if (this.isOss) { + qB = qB.whereIn('name', ['default', 'development', 'production']); + } const rows = await qB; stopTimer(); return rows.map(mapRowWithCounts); @@ -230,6 +250,13 @@ export default class EnvironmentStore implements IEnvironmentStore { if (query) { qB = qB.where(query); } + if (this.isOss) { + qB = qB.whereIn('environments.name', [ + 'default', + 'production', + 'development', + ]); + } const rows = await qB; stopTimer(); diff --git a/src/lib/features/project-insights/createProjectInsightsService.ts b/src/lib/features/project-insights/createProjectInsightsService.ts index 68b46ac3941f..f15c1d61ad07 100644 --- a/src/lib/features/project-insights/createProjectInsightsService.ts +++ b/src/lib/features/project-insights/createProjectInsightsService.ts @@ -16,12 +16,7 @@ export const createProjectInsightsService = ( config: IUnleashConfig, ): ProjectInsightsService => { const { eventBus, getLogger, flagResolver } = config; - const projectStore = new ProjectStore( - db, - eventBus, - getLogger, - flagResolver, - ); + const projectStore = new ProjectStore(db, eventBus, config); const featureToggleStore = new FeatureToggleStore( db, eventBus, diff --git a/src/lib/features/project-status/createProjectStatusService.ts b/src/lib/features/project-status/createProjectStatusService.ts index c8694775403d..165c641ce8cd 100644 --- a/src/lib/features/project-status/createProjectStatusService.ts +++ b/src/lib/features/project-status/createProjectStatusService.ts @@ -24,12 +24,7 @@ export const createProjectStatusService = ( config: IUnleashConfig, ): ProjectStatusService => { const eventStore = new EventStore(db, config.getLogger); - const projectStore = new ProjectStore( - db, - config.eventBus, - config.getLogger, - config.flagResolver, - ); + const projectStore = new ProjectStore(db, config.eventBus, config); const apiTokenStore = new ApiTokenStore( db, config.eventBus, diff --git a/src/lib/features/project/createProjectService.ts b/src/lib/features/project/createProjectService.ts index d4a42888eba6..a753ca34224f 100644 --- a/src/lib/features/project/createProjectService.ts +++ b/src/lib/features/project/createProjectService.ts @@ -63,12 +63,7 @@ export const createProjectService = ( ): ProjectService => { const { eventBus, getLogger, flagResolver } = config; const eventStore = new EventStore(db, getLogger); - const projectStore = new ProjectStore( - db, - eventBus, - getLogger, - flagResolver, - ); + const projectStore = new ProjectStore(db, eventBus, config); const projectOwnersReadModel = new ProjectOwnersReadModel(db); const projectFlagCreatorsReadModel = new ProjectFlagCreatorsReadModel(db); const groupStore = new GroupStore(db); @@ -79,11 +74,11 @@ export const createProjectService = ( flagResolver, ); const accountStore = new AccountStore(db, getLogger); - const environmentStore = new EnvironmentStore(db, eventBus, getLogger); + const environmentStore = new EnvironmentStore(db, eventBus, config); const featureEnvironmentStore = new FeatureEnvironmentStore( db, eventBus, - getLogger, + config, ); const projectStatsStore = new ProjectStatsStore(db, eventBus, getLogger); const accessService: AccessService = createAccessService(db, config); diff --git a/src/lib/features/project/project-store.ts b/src/lib/features/project/project-store.ts index 5df93193c63e..5011159825c4 100644 --- a/src/lib/features/project/project-store.ts +++ b/src/lib/features/project/project-store.ts @@ -1,4 +1,4 @@ -import type { Logger, LogProvider } from '../../logger'; +import type { Logger } from '../../logger'; import NotFoundError from '../../error/notfound-error'; import type { @@ -8,6 +8,7 @@ import type { IProjectApplication, IProjectApplications, IProjectUpdate, + IUnleashConfig, ProjectMode, } from '../../types'; import type { @@ -72,11 +73,16 @@ class ProjectStore implements IProjectStore { private timer: Function; + private isOss: boolean; + constructor( db: Db, eventBus: EventEmitter, - getLogger: LogProvider, - flagResolver: IFlagResolver, + { + getLogger, + flagResolver, + isOss, + }: Pick, ) { this.db = db; this.logger = getLogger('project-store.ts'); @@ -86,6 +92,7 @@ class ProjectStore implements IProjectStore { action, }); this.flagResolver = flagResolver; + this.isOss = isOss; } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types @@ -122,6 +129,9 @@ class ProjectStore implements IProjectStore { .orderBy('name', 'asc'); projects = projects.where(`${TABLE}.archived_at`, null); + if (this.isOss) { + projects = projects.where('id', 'default'); + } const rows = await projects; diff --git a/src/lib/middleware/rbac-middleware.ts b/src/lib/middleware/rbac-middleware.ts index 33ffd108b5a2..88e558f0e98e 100644 --- a/src/lib/middleware/rbac-middleware.ts +++ b/src/lib/middleware/rbac-middleware.ts @@ -33,7 +33,7 @@ export function findParam( } const rbacMiddleware = ( - config: Pick, + config: Pick, { featureToggleStore, segmentStore, @@ -98,6 +98,24 @@ const rbacMiddleware = ( ) { projectId = 'default'; } + if (config.isOss) { + if (projectId !== undefined && projectId !== 'default') { + logger.error( + 'OSS is only allowed to work with default project.', + ); + return false; + } + const ossEnvs = ['default', 'development', 'production']; + if ( + environment !== undefined && + !ossEnvs.includes(environment) + ) { + logger.error( + `OSS is only allowed to work with ${ossEnvs} environments.`, + ); + return false; + } + } // DELETE segment does not include information about the segment's project // This is needed to check if the user has the right permissions on a project level diff --git a/src/lib/types/option.ts b/src/lib/types/option.ts index 792cb22dc874..b4919ad34152 100644 --- a/src/lib/types/option.ts +++ b/src/lib/types/option.ts @@ -143,6 +143,7 @@ export interface IUnleashOptions { metricsRateLimiting?: Partial; dailyMetricsStorageDays?: number; rateLimiting?: Partial; + isOss?: boolean; resourceLimits?: Partial< Pick< ResourceLimitsSchema, @@ -273,6 +274,7 @@ export interface IUnleashConfig { publicFolder?: string; disableScheduler?: boolean; isEnterprise: boolean; + isOss: boolean; rateLimiting: IRateLimiting; feedbackUriPath?: string; openAIAPIKey?: string; diff --git a/src/test/e2e/api/admin/environment-oss.e2e.test.ts b/src/test/e2e/api/admin/environment-oss.e2e.test.ts new file mode 100644 index 000000000000..4de671b7d853 --- /dev/null +++ b/src/test/e2e/api/admin/environment-oss.e2e.test.ts @@ -0,0 +1,68 @@ +import { + type IUnleashTest, + setupAppWithCustomConfig, +} from '../../helpers/test-helper'; +import dbInit, { type ITestDb } from '../../helpers/database-init'; +import getLogger from '../../../fixtures/no-logger'; + +let app: IUnleashTest; +let db: ITestDb; + +beforeAll(async () => { + db = await dbInit('environment_api_is_oss_serial', getLogger, { + isOss: true, + }); + app = await setupAppWithCustomConfig( + db.stores, + { + experimental: { + flags: { + strictSchemaValidation: true, + }, + }, + isOss: true, + }, + db.rawDatabase, + ); + await db.stores.environmentStore.create({ + name: 'development', + type: 'development', + enabled: true, + }); + await db.stores.environmentStore.create({ + name: 'production', + type: 'production', + enabled: true, + }); + await db.stores.environmentStore.create({ + name: 'customenvironment', + type: 'production', + enabled: true, + }); + await db.stores.environmentStore.create({ + name: 'customenvironment2', + type: 'production', + enabled: true, + }); + await db.stores.environmentStore.create({ + name: 'customenvironment3', + type: 'production', + enabled: true, + }); +}); + +afterAll(async () => { + await app.destroy(); + await db.destroy(); +}); + +test('querying environments in OSS only returns environments that are included in oss', async () => { + await app.request + .get('/api/admin/environments') + .expect(200) + .expect((res) => { + expect(res.body.environments).toHaveLength(3); + const names = res.body.environments.map((env) => env.name); + expect(names).toEqual(['default', 'development', 'production']); + }); +});