diff --git a/scripts/docker-compose.yml b/scripts/docker-compose.yml new file mode 100644 index 0000000000..226b49bfd6 --- /dev/null +++ b/scripts/docker-compose.yml @@ -0,0 +1,19 @@ +version: '3.1' + +services: + postgres: + image: postgres:alpine3.15 + environment: + POSTGRES_USER: unleash_user + POSTGRES_PASSWORD: passord + POSTGRES_DB: unleash + ports: + - 5432:5432 + + pgadmin: + image: dpage/pgadmin4:6.8 + environment: + PGADMIN_DEFAULT_EMAIL: 'admin@admin.com' + PGADMIN_DEFAULT_PASSWORD: 'admin' + ports: + - 8080:80 diff --git a/src/lib/db/api-token-store.ts b/src/lib/db/api-token-store.ts index 18139fc4bb..47f35f145d 100644 --- a/src/lib/db/api-token-store.ts +++ b/src/lib/db/api-token-store.ts @@ -11,7 +11,7 @@ import { IApiTokenCreate, isAllProjects, } from '../types/models/api-token'; -import { ALL_PROJECTS } from '../../lib/services/access-service'; +import { ALL_PROJECTS } from '../util/constants'; const TABLE = 'api_tokens'; const API_LINK_TABLE = 'api_token_project'; diff --git a/src/lib/services/access-service.ts b/src/lib/services/access-service.ts index b499a79c7e..3511bf0691 100644 --- a/src/lib/services/access-service.ts +++ b/src/lib/services/access-service.ts @@ -24,13 +24,10 @@ import NameExistsError from '../error/name-exists-error'; import { IEnvironmentStore } from 'lib/types/stores/environment-store'; import RoleInUseError from '../error/role-in-use-error'; import { roleSchema } from '../schema/role-schema'; -import { CUSTOM_ROLE_TYPE } from '../util/constants'; +import { CUSTOM_ROLE_TYPE, ALL_PROJECTS, ALL_ENVS } from '../util/constants'; import { DEFAULT_PROJECT } from '../types/project'; import InvalidOperationError from '../error/invalid-operation-error'; -export const ALL_PROJECTS = '*'; -export const ALL_ENVS = '*'; - const { ADMIN } = permissions; const PROJECT_ADMIN = [ diff --git a/src/lib/services/state-service.ts b/src/lib/services/state-service.ts index 0a35f4888a..d0de790505 100644 --- a/src/lib/services/state-service.ts +++ b/src/lib/services/state-service.ts @@ -46,10 +46,11 @@ import { IFeatureStrategiesStore } from '../types/stores/feature-strategies-stor import { IEnvironmentStore } from '../types/stores/environment-store'; import { IFeatureEnvironmentStore } from '../types/stores/feature-environment-store'; import { IUnleashStores } from '../types/stores'; -import { DEFAULT_ENV } from '../util/constants'; +import { DEFAULT_ENV, ALL_ENVS } from '../util/constants'; import { GLOBAL_ENV } from '../types/environment'; import { ISegmentStore } from '../types/stores/segment-store'; import { PartialSome } from '../types/partial'; +import { IApiTokenStore } from 'lib/types/stores/api-token-store'; export interface IBackupOption { includeFeatureToggles: boolean; @@ -92,6 +93,8 @@ export default class StateService { private segmentStore: ISegmentStore; + private apiTokenStore: IApiTokenStore; + constructor( stores: IUnleashStores, { getLogger }: Pick, @@ -107,6 +110,7 @@ export default class StateService { this.featureTagStore = stores.featureTagStore; this.environmentStore = stores.environmentStore; this.segmentStore = stores.segmentStore; + this.apiTokenStore = stores.apiTokenStore; this.logger = getLogger('services/state-service.js'); } @@ -433,6 +437,15 @@ export default class StateService { data: env, })); await this.eventStore.batchStore(importedEnvironmentEvents); + + const apiTokens = await this.apiTokenStore.getAll(); + const envNames = importedEnvs.map((env) => env.name); + apiTokens + .filter((apiToken) => !(apiToken.environment === ALL_ENVS)) + .filter((apiToken) => !envNames.includes(apiToken.environment)) + .forEach((apiToken) => + this.apiTokenStore.delete(apiToken.secret), + ); } } diff --git a/src/lib/util/constants.ts b/src/lib/util/constants.ts index 1723f54a76..18ed7d2e48 100644 --- a/src/lib/util/constants.ts +++ b/src/lib/util/constants.ts @@ -1,5 +1,8 @@ export const DEFAULT_ENV = 'default'; +export const ALL_PROJECTS = '*'; +export const ALL_ENVS = '*'; + export const ROOT_PERMISSION_TYPE = 'root'; export const ENVIRONMENT_PERMISSION_TYPE = 'environment'; export const PROJECT_PERMISSION_TYPE = 'project'; diff --git a/src/migrations/20220528143630-dont-cascade-environment-deletion-to-apitokens.js b/src/migrations/20220528143630-dont-cascade-environment-deletion-to-apitokens.js new file mode 100644 index 0000000000..a28e99e12d --- /dev/null +++ b/src/migrations/20220528143630-dont-cascade-environment-deletion-to-apitokens.js @@ -0,0 +1,19 @@ +'use strict'; + +exports.up = function (db, cb) { + db.runSql( + ` + ALTER TABLE api_tokens DROP CONSTRAINT api_tokens_environment_fkey; + `, + cb, + ); +}; + +exports.down = function (db, cb) { + db.runSql( + ` + ALTER TABLE api_tokens ADD CONSTRAINT api_tokens_environment_fkey FOREIGN KEY(environment) REFERENCES environments(name) ON DELETE CASCADE; + `, + cb, + ); +}; diff --git a/src/test/e2e/api/admin/state.e2e.test.ts b/src/test/e2e/api/admin/state.e2e.test.ts index 2b48827eca..c6623c55b7 100644 --- a/src/test/e2e/api/admin/state.e2e.test.ts +++ b/src/test/e2e/api/admin/state.e2e.test.ts @@ -3,6 +3,7 @@ import { IUnleashTest, setupApp } from '../../helpers/test-helper'; import getLogger from '../../../fixtures/no-logger'; import { DEFAULT_ENV } from '../../../../lib/util/constants'; import { collectIds } from '../../../../lib/util/collect-ids'; +import { ApiTokenType } from '../../../../lib/types/models/api-token'; const importData = require('../../../examples/import.json'); @@ -337,3 +338,105 @@ test(`should import segments and connect them to feature strategies`, async () = expect(activeSegments.length).toEqual(1); expect(collectIds(activeSegments)).toEqual([1]); }); + +test(`should not delete api_tokens on import when drop-flag is set`, async () => { + const projectId = 'reimported-project'; + const environment = 'reimported-environment'; + const apiTokenName = 'not-dropped-token'; + const featureName = 'reimportedFeature'; + const userName = 'apiTokens-user'; + + await db.stores.environmentStore.create({ + name: environment, + type: 'test', + }); + await db.stores.projectStore.create({ + name: projectId, + id: projectId, + description: 'Project for export', + }); + await app.services.environmentService.addEnvironmentToProject( + environment, + projectId, + ); + await app.services.featureToggleServiceV2.createFeatureToggle( + projectId, + { + type: 'Release', + name: featureName, + description: 'Feature for export', + }, + userName, + ); + await app.services.featureToggleServiceV2.createStrategy( + { + name: 'default', + constraints: [ + { contextName: 'userId', operator: 'IN', values: ['123'] }, + ], + parameters: {}, + }, + { + projectId, + featureName, + environment, + }, + userName, + ); + await app.services.apiTokenService.createApiTokenWithProjects({ + username: apiTokenName, + type: ApiTokenType.CLIENT, + environment: environment, + projects: [projectId], + }); + + const data = await app.services.stateService.export({}); + await app.services.stateService.import({ + data, + dropBeforeImport: true, + keepExisting: false, + userName: userName, + }); + + const apiTokens = await app.services.apiTokenService.getAllTokens(); + + expect(apiTokens.length).toEqual(1); + expect(apiTokens[0].username).toBe(apiTokenName); +}); + +test(`should clean apitokens for not existing environment after import with drop`, async () => { + const projectId = 'not-reimported-project'; + const environment = 'not-reimported-environment'; + const apiTokenName = 'dropped-token'; + + await db.stores.environmentStore.create({ + name: environment, + type: 'test', + }); + await db.stores.projectStore.create({ + name: projectId, + id: projectId, + description: 'Project for export', + }); + await app.services.environmentService.addEnvironmentToProject( + environment, + projectId, + ); + await app.services.apiTokenService.createApiTokenWithProjects({ + username: apiTokenName, + type: ApiTokenType.CLIENT, + environment: environment, + projects: [projectId], + }); + + await app.request + .post('/api/admin/state/import?drop=true') + .attach('file', 'src/test/examples/v3-minimal.json') + .expect(202); + + const apiTokens = await app.services.apiTokenService.getAllTokens(); + + console.log(apiTokens); + + expect(apiTokens.length).toEqual(0); +}); diff --git a/src/test/e2e/services/access-service.e2e.test.ts b/src/test/e2e/services/access-service.e2e.test.ts index 3fd28e9852..61adfe395d 100644 --- a/src/test/e2e/services/access-service.e2e.test.ts +++ b/src/test/e2e/services/access-service.e2e.test.ts @@ -2,10 +2,7 @@ import dbInit, { ITestDb } from '../helpers/database-init'; import getLogger from '../../fixtures/no-logger'; // eslint-disable-next-line import/no-unresolved -import { - AccessService, - ALL_PROJECTS, -} from '../../../lib/services/access-service'; +import { AccessService } from '../../../lib/services/access-service'; import * as permissions from '../../../lib/types/permissions'; import { RoleName } from '../../../lib/types/model'; @@ -14,6 +11,7 @@ import FeatureToggleService from '../../../lib/services/feature-toggle-service'; import ProjectService from '../../../lib/services/project-service'; import { createTestConfig } from '../../config/test-config'; import { DEFAULT_PROJECT } from '../../../lib/types/project'; +import { ALL_PROJECTS } from '../../../lib/util/constants'; import { SegmentService } from '../../../lib/services/segment-service'; let db: ITestDb; diff --git a/src/test/examples/v3-minimal.json b/src/test/examples/v3-minimal.json new file mode 100644 index 0000000000..9c75cd6c8c --- /dev/null +++ b/src/test/examples/v3-minimal.json @@ -0,0 +1,36 @@ +{ + "version": 3, + "projects": [ + { + "id": "default", + "name": "Default", + "description": "Default project", + "createdAt": "2022-05-19T18:28:31.927Z", + "health": 100, + "updatedAt": "2022-05-19T20:28:32.736Z" + } + ], + "environments": [ + { + "name": "default", + "type": "production", + "sortOrder": 1, + "enabled": false, + "protected": true + }, + { + "name": "development", + "type": "development", + "sortOrder": 100, + "enabled": true, + "protected": false + }, + { + "name": "production", + "type": "production", + "sortOrder": 200, + "enabled": true, + "protected": false + } + ] +}