feat(events/http): add HTTP endpoint-based event publisher
This plugin adds an event publisher which receives events via (an) HTTP endpoint(s) and can be used as destination at webhook subscriptions. Relates-to: #11082 Signed-off-by: Patrick Jungermann <Patrick.Jungermann@gmail.com>
This commit is contained in:
@@ -22,3 +22,4 @@
|
||||
|
||||
export { EventsBackend } from './service/EventsBackend';
|
||||
export { eventsPlugin } from './service/EventsPlugin';
|
||||
export { HttpPostIngressEventPublisher } from './service/http';
|
||||
|
||||
@@ -14,9 +14,12 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getVoidLogger } from '@backstage/backend-common';
|
||||
import { errorHandler, getVoidLogger } from '@backstage/backend-common';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import {
|
||||
configServiceRef,
|
||||
createBackendModule,
|
||||
httpRouterServiceRef,
|
||||
loggerServiceRef,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { startTestBackend } from '@backstage/backend-test-utils';
|
||||
@@ -26,13 +29,29 @@ import {
|
||||
TestEventPublisher,
|
||||
TestEventSubscriber,
|
||||
} from '@backstage/plugin-events-backend-test-utils';
|
||||
import express from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import request from 'supertest';
|
||||
import { eventsPlugin } from './EventsPlugin';
|
||||
|
||||
describe('eventPlugin', () => {
|
||||
it('should be initialized properly', async () => {
|
||||
const eventBroker = new TestEventBroker();
|
||||
const publisher = new TestEventPublisher();
|
||||
const subscriber = new TestEventSubscriber('sub', ['topicA']);
|
||||
const subscriber = new TestEventSubscriber('sub', ['fake']);
|
||||
|
||||
const config = new ConfigReader({
|
||||
events: {
|
||||
http: {
|
||||
topics: ['fake'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const httpRouter = Router();
|
||||
httpRouter.use(express.json());
|
||||
httpRouter.use(errorHandler());
|
||||
const app = express().use(httpRouter);
|
||||
|
||||
const testModule = createBackendModule({
|
||||
pluginId: 'events',
|
||||
@@ -53,12 +72,26 @@ describe('eventPlugin', () => {
|
||||
|
||||
await startTestBackend({
|
||||
extensionPoints: [],
|
||||
services: [[loggerServiceRef, getVoidLogger()]],
|
||||
services: [
|
||||
[configServiceRef, config],
|
||||
[httpRouterServiceRef, httpRouter],
|
||||
[loggerServiceRef, getVoidLogger()],
|
||||
],
|
||||
features: [eventsPlugin(), testModule()],
|
||||
});
|
||||
|
||||
expect(publisher.eventBroker).toBe(eventBroker);
|
||||
expect(eventBroker.subscribed.length).toEqual(1);
|
||||
expect(eventBroker.subscribed[0]).toBe(subscriber);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/http/fake')
|
||||
.timeout(100)
|
||||
.send({ test: 'fake' });
|
||||
expect(response.status).toBe(202);
|
||||
|
||||
expect(eventBroker.published.length).toEqual(1);
|
||||
expect(eventBroker.published[0].topic).toEqual('fake');
|
||||
expect(eventBroker.published[0].eventPayload).toEqual({ test: 'fake' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
configServiceRef,
|
||||
createBackendPlugin,
|
||||
httpRouterServiceRef,
|
||||
loggerServiceRef,
|
||||
loggerToWinstonLogger,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
@@ -25,11 +27,15 @@ import {
|
||||
EventSubscriber,
|
||||
eventsExtensionPoint,
|
||||
EventsExtensionPoint,
|
||||
HttpPostIngressOptions,
|
||||
} from '@backstage/plugin-events-node';
|
||||
import { InMemoryEventBroker } from './InMemoryEventBroker';
|
||||
import Router from 'express-promise-router';
|
||||
import { HttpPostIngressEventPublisher } from './http';
|
||||
|
||||
class EventsExtensionPointImpl implements EventsExtensionPoint {
|
||||
#eventBroker: EventBroker | undefined;
|
||||
#httpPostIngresses: HttpPostIngressOptions[] = [];
|
||||
#publishers: EventPublisher[] = [];
|
||||
#subscribers: EventSubscriber[] = [];
|
||||
|
||||
@@ -49,6 +55,10 @@ class EventsExtensionPointImpl implements EventsExtensionPoint {
|
||||
this.#subscribers.push(...subscribers.flat());
|
||||
}
|
||||
|
||||
addHttpPostIngress(options: HttpPostIngressOptions) {
|
||||
this.#httpPostIngresses.push(options);
|
||||
}
|
||||
|
||||
get eventBroker() {
|
||||
return this.#eventBroker;
|
||||
}
|
||||
@@ -60,6 +70,10 @@ class EventsExtensionPointImpl implements EventsExtensionPoint {
|
||||
get subscribers() {
|
||||
return this.#subscribers;
|
||||
}
|
||||
|
||||
get httpPostIngresses() {
|
||||
return this.#httpPostIngresses;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -75,18 +89,42 @@ export const eventsPlugin = createBackendPlugin({
|
||||
|
||||
env.registerInit({
|
||||
deps: {
|
||||
config: configServiceRef,
|
||||
httpRouter: httpRouterServiceRef,
|
||||
logger: loggerServiceRef,
|
||||
},
|
||||
async init({ logger }) {
|
||||
async init({ config, httpRouter, logger }) {
|
||||
const winstonLogger = loggerToWinstonLogger(logger);
|
||||
const eventsRouter = Router();
|
||||
const router = Router();
|
||||
eventsRouter.use('/http', router);
|
||||
|
||||
const ingresses = Object.fromEntries(
|
||||
extensionPoint.httpPostIngresses.map(ingress => [
|
||||
ingress.topic,
|
||||
ingress as Omit<HttpPostIngressOptions, 'topic'>,
|
||||
]),
|
||||
);
|
||||
|
||||
const http = HttpPostIngressEventPublisher.fromConfig({
|
||||
config,
|
||||
logger: winstonLogger,
|
||||
router,
|
||||
ingresses,
|
||||
});
|
||||
|
||||
if (!extensionPoint.eventBroker) {
|
||||
const winstonLogger = loggerToWinstonLogger(logger);
|
||||
extensionPoint.setEventBroker(new InMemoryEventBroker(winstonLogger));
|
||||
}
|
||||
|
||||
extensionPoint.eventBroker!.subscribe(extensionPoint.subscribers);
|
||||
extensionPoint.publishers.forEach(publisher =>
|
||||
publisher.setEventBroker(extensionPoint.eventBroker!),
|
||||
);
|
||||
[extensionPoint.publishers, http]
|
||||
.flat()
|
||||
.forEach(publisher =>
|
||||
publisher.setEventBroker(extensionPoint.eventBroker!),
|
||||
);
|
||||
|
||||
httpRouter.use(eventsRouter);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { errorHandler, getVoidLogger } from '@backstage/backend-common';
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { TestEventBroker } from '@backstage/plugin-events-backend-test-utils';
|
||||
import express from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import request from 'supertest';
|
||||
import { HttpPostIngressEventPublisher } from './HttpPostIngressEventPublisher';
|
||||
|
||||
describe('HttpPostIngressEventPublisher', () => {
|
||||
const logger = getVoidLogger();
|
||||
|
||||
it('should set up routes correctly', async () => {
|
||||
const config = new ConfigReader({
|
||||
events: {
|
||||
http: {
|
||||
topics: ['testA'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const router = Router();
|
||||
router.use(express.json());
|
||||
router.use(errorHandler());
|
||||
const app = express().use(router);
|
||||
|
||||
const publisher = HttpPostIngressEventPublisher.fromConfig({
|
||||
config,
|
||||
logger,
|
||||
router,
|
||||
ingresses: {
|
||||
testB: {},
|
||||
},
|
||||
});
|
||||
|
||||
const eventBroker = new TestEventBroker();
|
||||
await publisher.setEventBroker(eventBroker);
|
||||
|
||||
const notFoundResponse = await request(app)
|
||||
.post('/unknown')
|
||||
.timeout(100)
|
||||
.send({ test: 'data' });
|
||||
expect(notFoundResponse.status).toBe(404);
|
||||
|
||||
const response1 = await request(app)
|
||||
.post('/testA')
|
||||
.set('X-Custom-Header', 'test-value')
|
||||
.timeout(100)
|
||||
.send({ testA: 'data' });
|
||||
expect(response1.status).toBe(202);
|
||||
|
||||
const response2 = await request(app)
|
||||
.post('/testB')
|
||||
.set('X-Custom-Header', 'test-value')
|
||||
.timeout(100)
|
||||
.send({ testB: 'data' });
|
||||
expect(response2.status).toBe(202);
|
||||
|
||||
expect(eventBroker.published.length).toEqual(2);
|
||||
expect(eventBroker.published[0].topic).toEqual('testA');
|
||||
expect(eventBroker.published[0].eventPayload).toEqual({ testA: 'data' });
|
||||
expect(eventBroker.published[0].metadata).toEqual(
|
||||
expect.objectContaining({
|
||||
'content-type': 'application/json',
|
||||
'x-custom-header': 'test-value',
|
||||
}),
|
||||
);
|
||||
expect(eventBroker.published[1].topic).toEqual('testB');
|
||||
expect(eventBroker.published[1].eventPayload).toEqual({ testB: 'data' });
|
||||
expect(eventBroker.published[1].metadata).toEqual(
|
||||
expect.objectContaining({
|
||||
'content-type': 'application/json',
|
||||
'x-custom-header': 'test-value',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('with validator', async () => {
|
||||
const config = new ConfigReader({
|
||||
events: {
|
||||
http: {
|
||||
topics: ['testA'],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const router = Router();
|
||||
router.use(express.json());
|
||||
router.use(errorHandler());
|
||||
const app = express().use(router);
|
||||
|
||||
const publisher = HttpPostIngressEventPublisher.fromConfig({
|
||||
config,
|
||||
logger,
|
||||
router,
|
||||
ingresses: {
|
||||
testB: {
|
||||
validator: async (req, context) => {
|
||||
if (req.headers['x-test-signature'] === 'testB-signature') {
|
||||
return;
|
||||
}
|
||||
|
||||
context.reject({
|
||||
status: 400,
|
||||
payload: {
|
||||
message: 'wrong signature',
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
testC: {
|
||||
validator: async (req, context) => {
|
||||
if (req.headers['x-test-signature'] === 'testC-signature') {
|
||||
return;
|
||||
}
|
||||
|
||||
context.reject({
|
||||
status: 404,
|
||||
// payload: {},
|
||||
});
|
||||
},
|
||||
},
|
||||
testD: {
|
||||
validator: async (req, context) => {
|
||||
if (req.headers['x-test-signature'] === 'testD-signature') {
|
||||
return;
|
||||
}
|
||||
|
||||
context.reject({
|
||||
// status: 403,
|
||||
// payload: {},
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const eventBroker = new TestEventBroker();
|
||||
await publisher.setEventBroker(eventBroker);
|
||||
|
||||
const response1 = await request(app)
|
||||
.post('/testA')
|
||||
.timeout(100)
|
||||
.send({ test: 'data' });
|
||||
expect(response1.status).toBe(202);
|
||||
|
||||
const response2 = await request(app)
|
||||
.post('/testB')
|
||||
.timeout(100)
|
||||
.send({ test: 'data' });
|
||||
expect(response2.status).toBe(400);
|
||||
expect(response2.body).toEqual({ message: 'wrong signature' });
|
||||
|
||||
const response3 = await request(app)
|
||||
.post('/testB')
|
||||
.set('X-Test-Signature', 'wrong')
|
||||
.timeout(100)
|
||||
.send({ test: 'data' });
|
||||
expect(response3.status).toBe(400);
|
||||
expect(response3.body).toEqual({ message: 'wrong signature' });
|
||||
|
||||
const response4 = await request(app)
|
||||
.post('/testB')
|
||||
.set('X-Test-Signature', 'testB-signature')
|
||||
.timeout(100)
|
||||
.send({ test: 'data' });
|
||||
expect(response4.status).toBe(202);
|
||||
|
||||
const response5 = await request(app)
|
||||
.post('/testC')
|
||||
.timeout(100)
|
||||
.send({ test: 'data' });
|
||||
expect(response5.status).toBe(404);
|
||||
expect(response5.body).toEqual({});
|
||||
|
||||
const response6 = await request(app)
|
||||
.post('/testD')
|
||||
.timeout(100)
|
||||
.send({ test: 'data' });
|
||||
expect(response6.status).toBe(403);
|
||||
expect(response6.body).toEqual({});
|
||||
|
||||
expect(eventBroker.published.length).toEqual(2);
|
||||
expect(eventBroker.published[0].topic).toEqual('testA');
|
||||
expect(eventBroker.published[0].eventPayload).toEqual({ test: 'data' });
|
||||
expect(eventBroker.published[1].topic).toEqual('testB');
|
||||
expect(eventBroker.published[1].eventPayload).toEqual({ test: 'data' });
|
||||
expect(eventBroker.published[1].metadata).toEqual(
|
||||
expect.objectContaining({
|
||||
'x-test-signature': 'testB-signature',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('without configuration', async () => {
|
||||
const config = new ConfigReader({});
|
||||
|
||||
const router = Router();
|
||||
router.use(express.json());
|
||||
router.use(errorHandler());
|
||||
|
||||
expect(() =>
|
||||
HttpPostIngressEventPublisher.fromConfig({
|
||||
config,
|
||||
logger,
|
||||
router,
|
||||
}),
|
||||
).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { errorHandler } from '@backstage/backend-common';
|
||||
import { Config } from '@backstage/config';
|
||||
import {
|
||||
EventBroker,
|
||||
EventPublisher,
|
||||
HttpPostIngressOptions,
|
||||
RequestValidator,
|
||||
} from '@backstage/plugin-events-node';
|
||||
import express from 'express';
|
||||
import Router from 'express-promise-router';
|
||||
import { Logger } from 'winston';
|
||||
import { RequestValidationContextImpl } from './validation';
|
||||
|
||||
/**
|
||||
* Publishes events received from their origin (e.g., webhook events from an SCM system)
|
||||
* via HTTP POST endpoint and passes the request body as event payload to the registered subscribers.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
// TODO(pjungermann): add prom metrics? (see plugins/catalog-backend/src/util/metrics.ts, etc.)
|
||||
export class HttpPostIngressEventPublisher implements EventPublisher {
|
||||
private eventBroker?: EventBroker;
|
||||
|
||||
static fromConfig(env: {
|
||||
config: Config;
|
||||
ingresses?: { [topic: string]: Omit<HttpPostIngressOptions, 'topic'> };
|
||||
logger: Logger;
|
||||
router: express.Router;
|
||||
}): HttpPostIngressEventPublisher {
|
||||
const topics =
|
||||
env.config.getOptionalStringArray('events.http.topics') ?? [];
|
||||
|
||||
const ingresses = env.ingresses ?? {};
|
||||
topics.forEach(topic => {
|
||||
// don't overwrite topic settings
|
||||
// (e.g., added at the config as well as argument)
|
||||
if (!ingresses[topic]) {
|
||||
ingresses[topic] = {};
|
||||
}
|
||||
});
|
||||
|
||||
return new HttpPostIngressEventPublisher(env.logger, env.router, ingresses);
|
||||
}
|
||||
|
||||
private constructor(
|
||||
private logger: Logger,
|
||||
router: express.Router,
|
||||
ingresses: { [topic: string]: Omit<HttpPostIngressOptions, 'topic'> },
|
||||
) {
|
||||
router.use(this.createRouter(ingresses));
|
||||
}
|
||||
|
||||
async setEventBroker(eventBroker: EventBroker): Promise<void> {
|
||||
this.eventBroker = eventBroker;
|
||||
}
|
||||
|
||||
private createRouter(ingresses: {
|
||||
[topic: string]: Omit<HttpPostIngressOptions, 'topic'>;
|
||||
}): express.Router {
|
||||
const router = Router();
|
||||
router.use(express.json());
|
||||
|
||||
Object.keys(ingresses).forEach(topic =>
|
||||
this.addRouteForTopic(router, topic, ingresses[topic].validator),
|
||||
);
|
||||
|
||||
router.use(errorHandler());
|
||||
return router;
|
||||
}
|
||||
|
||||
private addRouteForTopic(
|
||||
router: express.Router,
|
||||
topic: string,
|
||||
validator?: RequestValidator,
|
||||
): void {
|
||||
const path = `/${topic}`;
|
||||
|
||||
router.post(path, async (request, response) => {
|
||||
const context = new RequestValidationContextImpl();
|
||||
await validator?.(request, context);
|
||||
if (context.wasRejected()) {
|
||||
response
|
||||
.status(context.rejectionDetails!.status)
|
||||
.json(context.rejectionDetails!.payload);
|
||||
return;
|
||||
}
|
||||
|
||||
const eventPayload = request.body;
|
||||
await this.eventBroker!.publish({
|
||||
topic,
|
||||
eventPayload,
|
||||
metadata: request.headers,
|
||||
});
|
||||
|
||||
response.status(202).json({ status: 'accepted' });
|
||||
});
|
||||
|
||||
// TODO(pjungermann): We don't really know the externally defined path prefix here,
|
||||
// however it is more useful for users to have it. Is there a better way?
|
||||
this.logger.info(`Registered /api/events/http${path} to receive events`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { HttpPostIngressEventPublisher } from './HttpPostIngressEventPublisher';
|
||||
export * from './validation';
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { RequestValidationContextImpl } from './RequestValidationContextImpl';
|
||||
|
||||
describe('RequestValidationContextImpl', () => {
|
||||
it('not rejected', () => {
|
||||
const context = new RequestValidationContextImpl();
|
||||
|
||||
expect(context.wasRejected()).toBe(false);
|
||||
expect(context.rejectionDetails).toBeUndefined();
|
||||
});
|
||||
|
||||
it('reject without details', () => {
|
||||
const context = new RequestValidationContextImpl();
|
||||
|
||||
context.reject();
|
||||
|
||||
expect(context.wasRejected()).toBe(true);
|
||||
expect(context.rejectionDetails).not.toBeUndefined();
|
||||
expect(context.rejectionDetails!.status).toBe(403);
|
||||
expect(context.rejectionDetails!.payload).toEqual({});
|
||||
});
|
||||
|
||||
it('reject with partial details', () => {
|
||||
const context = new RequestValidationContextImpl();
|
||||
|
||||
context.reject({ status: 404 });
|
||||
|
||||
expect(context.wasRejected()).toBe(true);
|
||||
expect(context.rejectionDetails).not.toBeUndefined();
|
||||
expect(context.rejectionDetails!.status).toBe(404);
|
||||
expect(context.rejectionDetails!.payload).toEqual({});
|
||||
});
|
||||
|
||||
it('reject with details', () => {
|
||||
const context = new RequestValidationContextImpl();
|
||||
|
||||
context.reject({
|
||||
status: 403,
|
||||
payload: { message: 'invalid signature' },
|
||||
});
|
||||
|
||||
expect(context.wasRejected()).toBe(true);
|
||||
expect(context.rejectionDetails).not.toBeUndefined();
|
||||
expect(context.rejectionDetails!.status).toBe(403);
|
||||
expect(context.rejectionDetails!.payload).toEqual({
|
||||
message: 'invalid signature',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2022 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
RequestRejectionDetails,
|
||||
RequestValidationContext,
|
||||
} from '@backstage/plugin-events-node';
|
||||
|
||||
export class RequestValidationContextImpl implements RequestValidationContext {
|
||||
#rejectionDetails: RequestRejectionDetails | undefined;
|
||||
|
||||
reject(details?: Partial<RequestRejectionDetails>): void {
|
||||
this.#rejectionDetails = {
|
||||
status: details?.status ?? 403,
|
||||
payload: details?.payload ?? {},
|
||||
};
|
||||
}
|
||||
|
||||
wasRejected(): boolean {
|
||||
return this.#rejectionDetails !== undefined;
|
||||
}
|
||||
|
||||
get rejectionDetails() {
|
||||
return this.#rejectionDetails;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { RequestValidationContextImpl } from './RequestValidationContextImpl';
|
||||
Reference in New Issue
Block a user