From f1b2c1d2c266a42b7a9acb56a1a2241bdbb8f3dd Mon Sep 17 00:00:00 2001 From: Crevil Date: Fri, 19 Mar 2021 14:18:42 +0100 Subject: [PATCH 1/6] Add readonly mode to catalog This change includes a new `mode` concept to the catalog backend. This gives us the handle to configure catalog in a `readonly` mode as discussed in #3348. The new `catalog.mode` field configures that catalog in either `readwrite` or `readonly`. - `readwrite` is the current mode that allows users to register components at startup through `catalog.locations` and with eg. plugin `catalog-import` - `readonly` is the new mode which, for now, disables the mutating `location` APIs Signed-off-by: Crevil --- .changeset/rich-birds-reply.md | 24 +++ packages/backend/src/plugins/catalog.ts | 1 + plugins/catalog-backend/config.d.ts | 16 ++ .../src/service/router.test.ts | 156 +++++++++++++++++- plugins/catalog-backend/src/service/router.ts | 26 ++- .../src/service/standaloneServer.ts | 1 + plugins/catalog-backend/src/service/util.ts | 9 +- 7 files changed, 230 insertions(+), 3 deletions(-) create mode 100644 .changeset/rich-birds-reply.md diff --git a/.changeset/rich-birds-reply.md b/.changeset/rich-birds-reply.md new file mode 100644 index 0000000000..8f82a02d46 --- /dev/null +++ b/.changeset/rich-birds-reply.md @@ -0,0 +1,24 @@ +--- +'example-backend': minor +'@backstage/plugin-catalog-backend': minor +--- + +Add `readonly` mode to catalog backend + +This change adds a `catalog.mode` field in `app-config.yaml` that can be used to configure the catalog in readonly mode which effectively disables the possibilty of adding new components to the catalog after startup. + +When in `readonly` mode only locations configured in `catalog.locations` are loaded and served. +By default the mode is `readwrite` which represents the current functionality where locations can be added at run-time. + +This change requires the config API in the router which requires a change to `createRouter`. + +```diff + return await createRouter({ + entitiesCatalog, + locationsCatalog, + higherOrderOperation, + locationAnalyzer, + logger: env.logger, ++ config: env.config, + }); +``` diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts index eaca42717c..076840944f 100644 --- a/packages/backend/src/plugins/catalog.ts +++ b/packages/backend/src/plugins/catalog.ts @@ -45,5 +45,6 @@ export default async function createPlugin( higherOrderOperation, locationAnalyzer, logger: env.logger, + config: env.config, }); } diff --git a/plugins/catalog-backend/config.d.ts b/plugins/catalog-backend/config.d.ts index 32ff44457c..aced6c3856 100644 --- a/plugins/catalog-backend/config.d.ts +++ b/plugins/catalog-backend/config.d.ts @@ -16,6 +16,8 @@ import { JsonValue } from '@backstage/config'; +export type Mode = 'readonly' | 'readwrite'; + export interface Config { /** * Configuration options for the catalog plugin. @@ -42,6 +44,20 @@ export interface Config { allow: Array; }>; + /** + * Mode defines the overall behaviour mode of the catalog. + * + * Setting the mode to 'readwrite' you allow users to register their own + * components. This is the default value. + * + * Setting the mode to 'readonly' configures catalog to only allow reads. + * This can be used in combination with static locations to only serve + * operator provided locations. Effectively this removes the ability to + * register new components to a running backstage instance. + * + */ + mode?: Mode; + /** * A set of static locations that the catalog shall always keep itself * up-to-date with. This is commonly used for large, permanent integrations diff --git a/plugins/catalog-backend/src/service/router.test.ts b/plugins/catalog-backend/src/service/router.test.ts index 74cf187548..72ce61a126 100644 --- a/plugins/catalog-backend/src/service/router.test.ts +++ b/plugins/catalog-backend/src/service/router.test.ts @@ -15,6 +15,7 @@ */ import { getVoidLogger } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; import { NotFoundError } from '@backstage/errors'; import type { Entity, LocationSpec } from '@backstage/catalog-model'; import express from 'express'; @@ -25,7 +26,7 @@ import { HigherOrderOperation } from '../ingestion/types'; import { createRouter } from './router'; import { basicEntityFilter } from './request'; -describe('createRouter', () => { +describe('createRouter readwrite mode', () => { let entitiesCatalog: jest.Mocked; let locationsCatalog: jest.Mocked; let higherOrderOperation: jest.Mocked; @@ -55,6 +56,7 @@ describe('createRouter', () => { locationsCatalog, higherOrderOperation, logger: getVoidLogger(), + config: new ConfigReader(undefined), }); app = express().use(router); }); @@ -353,3 +355,155 @@ describe('createRouter', () => { }); }); }); + +describe('createRouter readonly mode', () => { + let entitiesCatalog: jest.Mocked; + let locationsCatalog: jest.Mocked; + let higherOrderOperation: jest.Mocked; + let app: express.Express; + + beforeAll(async () => { + entitiesCatalog = { + entities: jest.fn(), + removeEntityByUid: jest.fn(), + batchAddOrUpdateEntities: jest.fn(), + }; + locationsCatalog = { + addLocation: jest.fn(), + removeLocation: jest.fn(), + locations: jest.fn(), + location: jest.fn(), + locationHistory: jest.fn(), + logUpdateSuccess: jest.fn(), + logUpdateFailure: jest.fn(), + }; + higherOrderOperation = { + addLocation: jest.fn(), + refreshAllLocations: jest.fn(), + }; + const router = await createRouter({ + entitiesCatalog, + locationsCatalog, + higherOrderOperation, + logger: getVoidLogger(), + config: new ConfigReader({ + catalog: { + mode: 'readonly', + }, + }), + }); + app = express().use(router); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('GET /entities', () => { + it('happy path: lists entities', async () => { + const entities: Entity[] = [ + { apiVersion: 'a', kind: 'b', metadata: { name: 'n' } }, + ]; + + entitiesCatalog.entities.mockResolvedValueOnce({ + entities: [entities[0]], + pageInfo: { hasNextPage: false }, + }); + + const response = await request(app).get('/entities'); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(entities); + }); + }); + + describe('POST /entities', () => { + it('is not allowed', async () => { + const entity: Entity = { + apiVersion: 'a', + kind: 'b', + metadata: { + name: 'c', + namespace: 'd', + }, + }; + + const response = await request(app) + .post('/entities') + .set('Content-Type', 'application/json') + .send(entity); + + expect(entitiesCatalog.batchAddOrUpdateEntities).not.toHaveBeenCalled(); + expect(response.status).toEqual(403); + expect(response.text).toMatch(/readwrite/); + }); + }); + + describe('DELETE /entities/by-uid/:uid', () => { + it('is not allowed', async () => { + const response = await request(app).delete('/entities/by-uid/apa'); + + expect(response.status).toEqual(403); + expect(response.text).toMatch(/readwrite/); + }); + }); + + describe('GET /locations', () => { + it('happy path: lists locations', async () => { + const locations: LocationResponse[] = [ + { + currentStatus: { timestamp: '', status: '', message: '' }, + data: { id: 'a', type: 'b', target: 'c' }, + }, + ]; + locationsCatalog.locations.mockResolvedValueOnce(locations); + + const response = await request(app).get('/locations'); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(locations); + }); + }); + + describe('POST /locations', () => { + it('is not allowed', async () => { + const spec: LocationSpec = { + type: 'b', + target: 'c', + }; + + const response = await request(app).post('/locations').send(spec); + + expect(higherOrderOperation.addLocation).not.toHaveBeenCalled(); + expect(response.status).toEqual(403); + expect(response.text).toMatch(/readwrite/); + }); + + it('supports dry run', async () => { + const spec: LocationSpec = { + type: 'b', + target: 'c', + }; + + higherOrderOperation.addLocation.mockResolvedValue({ + location: { id: 'a', ...spec }, + entities: [], + }); + + const response = await request(app) + .post('/locations?dryRun=true') + .send(spec); + + expect(higherOrderOperation.addLocation).toHaveBeenCalledTimes(1); + expect(higherOrderOperation.addLocation).toHaveBeenCalledWith(spec, { + dryRun: true, + }); + expect(response.status).toEqual(201); + expect(response.body).toEqual( + expect.objectContaining({ + location: { id: 'a', ...spec }, + }), + ); + }); + }); +}); diff --git a/plugins/catalog-backend/src/service/router.ts b/plugins/catalog-backend/src/service/router.ts index 048126aace..51b1c20012 100644 --- a/plugins/catalog-backend/src/service/router.ts +++ b/plugins/catalog-backend/src/service/router.ts @@ -20,6 +20,7 @@ import { analyzeLocationSchema, locationSpecSchema, } from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; import { NotFoundError } from '@backstage/errors'; import express from 'express'; import Router from 'express-promise-router'; @@ -27,13 +28,18 @@ import { Logger } from 'winston'; import yn from 'yn'; import { EntitiesCatalog, LocationsCatalog } from '../catalog'; import { HigherOrderOperation, LocationAnalyzer } from '../ingestion/types'; +import { Mode } from '../../config'; import { basicEntityFilter, parseEntityFilterParams, parseEntityPaginationParams, parseEntityTransformParams, } from './request'; -import { requireRequestBody, validateRequestBody } from './util'; +import { + requireReadWriteMode, + requireRequestBody, + validateRequestBody, +} from './util'; export interface RouterOptions { entitiesCatalog?: EntitiesCatalog; @@ -41,6 +47,7 @@ export interface RouterOptions { higherOrderOperation?: HigherOrderOperation; locationAnalyzer?: LocationAnalyzer; logger: Logger; + config: Config; } export async function createRouter( @@ -51,11 +58,17 @@ export async function createRouter( locationsCatalog, higherOrderOperation, locationAnalyzer, + config, + logger, } = options; const router = Router(); router.use(express.json()); + const mode: Mode = + (config.getOptionalString('catalog.mode') as Mode) || 'readwrite'; + logger.info(`Catalog is running in ${mode} mode`); + if (entitiesCatalog) { router .get('/entities', async (req, res) => { @@ -87,6 +100,8 @@ export async function createRouter( * It stays around in the service for the time being, but may be * removed or change semantics at any time without prior notice. */ + requireReadWriteMode(mode); + const body = await requireRequestBody(req); const [result] = await entitiesCatalog.batchAddOrUpdateEntities([ { entity: body as Entity, relations: [] }, @@ -107,6 +122,8 @@ export async function createRouter( res.status(200).json(entities[0]); }) .delete('/entities/by-uid/:uid', async (req, res) => { + requireReadWriteMode(mode); + const { uid } = req.params; await entitiesCatalog.removeEntityByUid(uid); res.status(204).end(); @@ -133,6 +150,11 @@ export async function createRouter( router.post('/locations', async (req, res) => { const input = await validateRequestBody(req, locationSpecSchema); const dryRun = yn(req.query.dryRun, { default: false }); + + // when in dryRun addLocation is effectively a read operation so when in + // dryRun we override mode to readwrite to allow the operation + requireReadWriteMode(dryRun ? 'readwrite' : mode); + const output = await higherOrderOperation.addLocation(input, { dryRun }); res.status(201).json(output); }); @@ -155,6 +177,8 @@ export async function createRouter( res.status(200).json(output); }) .delete('/locations/:id', async (req, res) => { + requireReadWriteMode(mode); + const { id } = req.params; await locationsCatalog.removeLocation(id); res.status(204).end(); diff --git a/plugins/catalog-backend/src/service/standaloneServer.ts b/plugins/catalog-backend/src/service/standaloneServer.ts index 73a33c8c8e..44a5c0f84d 100644 --- a/plugins/catalog-backend/src/service/standaloneServer.ts +++ b/plugins/catalog-backend/src/service/standaloneServer.ts @@ -61,6 +61,7 @@ export async function startStandaloneServer( locationsCatalog, higherOrderOperation, logger, + config, }); const service = createServiceBuilder(module) .enableCors({ origin: 'http://localhost:3000' }) diff --git a/plugins/catalog-backend/src/service/util.ts b/plugins/catalog-backend/src/service/util.ts index d806f630e8..60e8c48984 100644 --- a/plugins/catalog-backend/src/service/util.ts +++ b/plugins/catalog-backend/src/service/util.ts @@ -14,10 +14,11 @@ * limitations under the License. */ -import { InputError } from '@backstage/errors'; +import { InputError, NotAllowedError } from '@backstage/errors'; import { Request } from 'express'; import lodash from 'lodash'; import yup from 'yup'; +import { Mode } from '../../config'; export async function requireRequestBody(req: Request): Promise { const contentType = req.header('content-type'); @@ -54,3 +55,9 @@ export async function validateRequestBody( return (body as unknown) as T; } + +export function requireReadWriteMode(mode: Mode) { + if (mode !== 'readwrite') { + throw new NotAllowedError('This operation requires readwrite mode'); + } +} From bf2db41cecccc1d1c64e9c48d445e8f23bbb1028 Mon Sep 17 00:00:00 2001 From: Crevil Date: Sat, 20 Mar 2021 12:54:37 +0100 Subject: [PATCH 2/6] Fix typo in changelog Signed-off-by: Crevil --- .changeset/rich-birds-reply.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/rich-birds-reply.md b/.changeset/rich-birds-reply.md index 8f82a02d46..fb1337dace 100644 --- a/.changeset/rich-birds-reply.md +++ b/.changeset/rich-birds-reply.md @@ -5,7 +5,7 @@ Add `readonly` mode to catalog backend -This change adds a `catalog.mode` field in `app-config.yaml` that can be used to configure the catalog in readonly mode which effectively disables the possibilty of adding new components to the catalog after startup. +This change adds a `catalog.mode` field in `app-config.yaml` that can be used to configure the catalog in readonly mode which effectively disables the possibility of adding new components to the catalog after startup. When in `readonly` mode only locations configured in `catalog.locations` are loaded and served. By default the mode is `readwrite` which represents the current functionality where locations can be added at run-time. From d8e085b2df96fcdf833a1be5dce41e31b7dcdeac Mon Sep 17 00:00:00 2001 From: Crevil Date: Sat, 20 Mar 2021 13:08:50 +0100 Subject: [PATCH 3/6] Add readonly to vocab.txr Signed-off-by: Crevil --- .github/styles/vocab.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index cc20afaf57..e9cd572231 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -70,6 +70,7 @@ Protobuf Proxying Raghunandan Readme +readonly rebase Recharts Redash From 131ac14d5e1200fb885fc3f3f8fcfc09a97e74f8 Mon Sep 17 00:00:00 2001 From: Crevil Date: Sat, 20 Mar 2021 18:01:33 +0100 Subject: [PATCH 4/6] Add config to create-app template Signed-off-by: Crevil --- .../default-app/packages/backend/src/plugins/catalog.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/catalog.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/catalog.ts index 556b385af3..838228cdb4 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/catalog.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/catalog.ts @@ -27,5 +27,6 @@ export default async function createPlugin(env: PluginEnvironment): Promise Date: Thu, 25 Mar 2021 14:36:02 +0100 Subject: [PATCH 5/6] Change to readonly config value Signed-off-by: Crevil --- .changeset/rich-birds-reply.md | 5 ++-- plugins/catalog-backend/config.d.ts | 18 ++++++------- .../src/service/router.test.ts | 12 ++++----- plugins/catalog-backend/src/service/router.ts | 25 +++++++++++-------- plugins/catalog-backend/src/service/util.ts | 7 +++--- 5 files changed, 33 insertions(+), 34 deletions(-) diff --git a/.changeset/rich-birds-reply.md b/.changeset/rich-birds-reply.md index fb1337dace..6e4755ca86 100644 --- a/.changeset/rich-birds-reply.md +++ b/.changeset/rich-birds-reply.md @@ -1,14 +1,13 @@ --- -'example-backend': minor '@backstage/plugin-catalog-backend': minor --- Add `readonly` mode to catalog backend -This change adds a `catalog.mode` field in `app-config.yaml` that can be used to configure the catalog in readonly mode which effectively disables the possibility of adding new components to the catalog after startup. +This change adds a `catalog.readonly` field in `app-config.yaml` that can be used to configure the catalog in readonly mode which effectively disables the possibility of adding new components to the catalog after startup. When in `readonly` mode only locations configured in `catalog.locations` are loaded and served. -By default the mode is `readwrite` which represents the current functionality where locations can be added at run-time. +By default `readonly` is disabled which represents the current functionality where locations can be added at run-time. This change requires the config API in the router which requires a change to `createRouter`. diff --git a/plugins/catalog-backend/config.d.ts b/plugins/catalog-backend/config.d.ts index aced6c3856..564dd7511e 100644 --- a/plugins/catalog-backend/config.d.ts +++ b/plugins/catalog-backend/config.d.ts @@ -16,8 +16,6 @@ import { JsonValue } from '@backstage/config'; -export type Mode = 'readonly' | 'readwrite'; - export interface Config { /** * Configuration options for the catalog plugin. @@ -45,18 +43,18 @@ export interface Config { }>; /** - * Mode defines the overall behaviour mode of the catalog. + * Readonly defines whether the catalog allows writes after startup. * - * Setting the mode to 'readwrite' you allow users to register their own - * components. This is the default value. + * Setting 'readonly=false' allows users to register their own components. + * This is the default value. * - * Setting the mode to 'readonly' configures catalog to only allow reads. - * This can be used in combination with static locations to only serve - * operator provided locations. Effectively this removes the ability to - * register new components to a running backstage instance. + * Setting 'readonly=true' configures catalog to only allow reads. This can + * be used in combination with static locations to only serve operator + * provided locations. Effectively this removes the ability to register new + * components to a running backstage instance. * */ - mode?: Mode; + readonly?: boolean; /** * A set of static locations that the catalog shall always keep itself diff --git a/plugins/catalog-backend/src/service/router.test.ts b/plugins/catalog-backend/src/service/router.test.ts index 72ce61a126..eb072f04d2 100644 --- a/plugins/catalog-backend/src/service/router.test.ts +++ b/plugins/catalog-backend/src/service/router.test.ts @@ -26,7 +26,7 @@ import { HigherOrderOperation } from '../ingestion/types'; import { createRouter } from './router'; import { basicEntityFilter } from './request'; -describe('createRouter readwrite mode', () => { +describe('createRouter readonly disabled', () => { let entitiesCatalog: jest.Mocked; let locationsCatalog: jest.Mocked; let higherOrderOperation: jest.Mocked; @@ -356,7 +356,7 @@ describe('createRouter readwrite mode', () => { }); }); -describe('createRouter readonly mode', () => { +describe('createRouter readonly enabled', () => { let entitiesCatalog: jest.Mocked; let locationsCatalog: jest.Mocked; let higherOrderOperation: jest.Mocked; @@ -388,7 +388,7 @@ describe('createRouter readonly mode', () => { logger: getVoidLogger(), config: new ConfigReader({ catalog: { - mode: 'readonly', + readonly: true, }, }), }); @@ -435,7 +435,7 @@ describe('createRouter readonly mode', () => { expect(entitiesCatalog.batchAddOrUpdateEntities).not.toHaveBeenCalled(); expect(response.status).toEqual(403); - expect(response.text).toMatch(/readwrite/); + expect(response.text).toMatch(/not allowed in readonly/); }); }); @@ -444,7 +444,7 @@ describe('createRouter readonly mode', () => { const response = await request(app).delete('/entities/by-uid/apa'); expect(response.status).toEqual(403); - expect(response.text).toMatch(/readwrite/); + expect(response.text).toMatch(/not allowed in readonly/); }); }); @@ -476,7 +476,7 @@ describe('createRouter readonly mode', () => { expect(higherOrderOperation.addLocation).not.toHaveBeenCalled(); expect(response.status).toEqual(403); - expect(response.text).toMatch(/readwrite/); + expect(response.text).toMatch(/not allowed in readonly/); }); it('supports dry run', async () => { diff --git a/plugins/catalog-backend/src/service/router.ts b/plugins/catalog-backend/src/service/router.ts index 51b1c20012..4c93b27640 100644 --- a/plugins/catalog-backend/src/service/router.ts +++ b/plugins/catalog-backend/src/service/router.ts @@ -28,7 +28,6 @@ import { Logger } from 'winston'; import yn from 'yn'; import { EntitiesCatalog, LocationsCatalog } from '../catalog'; import { HigherOrderOperation, LocationAnalyzer } from '../ingestion/types'; -import { Mode } from '../../config'; import { basicEntityFilter, parseEntityFilterParams, @@ -36,7 +35,7 @@ import { parseEntityTransformParams, } from './request'; import { - requireReadWriteMode, + disallowReadonlyMode, requireRequestBody, validateRequestBody, } from './util'; @@ -65,9 +64,11 @@ export async function createRouter( const router = Router(); router.use(express.json()); - const mode: Mode = - (config.getOptionalString('catalog.mode') as Mode) || 'readwrite'; - logger.info(`Catalog is running in ${mode} mode`); + const readonlyEnabled = + config.getOptionalBoolean('catalog.readonly') || false; + if (readonlyEnabled) { + logger.info('Catalog is running in readonly mode'); + } if (entitiesCatalog) { router @@ -100,7 +101,7 @@ export async function createRouter( * It stays around in the service for the time being, but may be * removed or change semantics at any time without prior notice. */ - requireReadWriteMode(mode); + disallowReadonlyMode(readonlyEnabled); const body = await requireRequestBody(req); const [result] = await entitiesCatalog.batchAddOrUpdateEntities([ @@ -122,7 +123,7 @@ export async function createRouter( res.status(200).json(entities[0]); }) .delete('/entities/by-uid/:uid', async (req, res) => { - requireReadWriteMode(mode); + disallowReadonlyMode(readonlyEnabled); const { uid } = req.params; await entitiesCatalog.removeEntityByUid(uid); @@ -151,9 +152,11 @@ export async function createRouter( const input = await validateRequestBody(req, locationSpecSchema); const dryRun = yn(req.query.dryRun, { default: false }); - // when in dryRun addLocation is effectively a read operation so when in - // dryRun we override mode to readwrite to allow the operation - requireReadWriteMode(dryRun ? 'readwrite' : mode); + // when in dryRun addLocation is effectively a read operation so we don't + // need to disallow readonly + if (!dryRun) { + disallowReadonlyMode(readonlyEnabled); + } const output = await higherOrderOperation.addLocation(input, { dryRun }); res.status(201).json(output); @@ -177,7 +180,7 @@ export async function createRouter( res.status(200).json(output); }) .delete('/locations/:id', async (req, res) => { - requireReadWriteMode(mode); + disallowReadonlyMode(readonlyEnabled); const { id } = req.params; await locationsCatalog.removeLocation(id); diff --git a/plugins/catalog-backend/src/service/util.ts b/plugins/catalog-backend/src/service/util.ts index 60e8c48984..9cc5ba1b52 100644 --- a/plugins/catalog-backend/src/service/util.ts +++ b/plugins/catalog-backend/src/service/util.ts @@ -18,7 +18,6 @@ import { InputError, NotAllowedError } from '@backstage/errors'; import { Request } from 'express'; import lodash from 'lodash'; import yup from 'yup'; -import { Mode } from '../../config'; export async function requireRequestBody(req: Request): Promise { const contentType = req.header('content-type'); @@ -56,8 +55,8 @@ export async function validateRequestBody( return (body as unknown) as T; } -export function requireReadWriteMode(mode: Mode) { - if (mode !== 'readwrite') { - throw new NotAllowedError('This operation requires readwrite mode'); +export function disallowReadonlyMode(readonly: boolean) { + if (readonly) { + throw new NotAllowedError('This operation not allowed in readonly mode'); } } From d9638c3e8f85b17b73091491f3ad86e94923d1a4 Mon Sep 17 00:00:00 2001 From: Crevil Date: Thu, 25 Mar 2021 21:02:54 +0100 Subject: [PATCH 6/6] Allow DELETE /entities/by-uid/:uid in readonly mode Signed-off-by: Crevil --- plugins/catalog-backend/src/service/router.test.ts | 8 +++++--- plugins/catalog-backend/src/service/router.ts | 2 -- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/catalog-backend/src/service/router.test.ts b/plugins/catalog-backend/src/service/router.test.ts index eb072f04d2..756e17f858 100644 --- a/plugins/catalog-backend/src/service/router.test.ts +++ b/plugins/catalog-backend/src/service/router.test.ts @@ -440,11 +440,13 @@ describe('createRouter readonly enabled', () => { }); describe('DELETE /entities/by-uid/:uid', () => { - it('is not allowed', async () => { + // this delete is allowed as there is no other way to remove entities + it('is allowed', async () => { const response = await request(app).delete('/entities/by-uid/apa'); - expect(response.status).toEqual(403); - expect(response.text).toMatch(/not allowed in readonly/); + expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledTimes(1); + expect(entitiesCatalog.removeEntityByUid).toHaveBeenCalledWith('apa'); + expect(response.status).toEqual(204); }); }); diff --git a/plugins/catalog-backend/src/service/router.ts b/plugins/catalog-backend/src/service/router.ts index 4c93b27640..ae58255a4e 100644 --- a/plugins/catalog-backend/src/service/router.ts +++ b/plugins/catalog-backend/src/service/router.ts @@ -123,8 +123,6 @@ export async function createRouter( res.status(200).json(entities[0]); }) .delete('/entities/by-uid/:uid', async (req, res) => { - disallowReadonlyMode(readonlyEnabled); - const { uid } = req.params; await entitiesCatalog.removeEntityByUid(uid); res.status(204).end();