Merge pull request #7463 from Sarabadu/feat/accept-glob-pattern
[WIP]Add Glob pattern to Cors config
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
---
|
||||
|
||||
Add glob patterns support to config CORS options. It's possible to send patterns like:
|
||||
|
||||
```yaml
|
||||
backend:
|
||||
cors:
|
||||
origin:
|
||||
- https://*.my-domain.com
|
||||
- http://localhost:700[0-9]
|
||||
- https://sub-domain-+([0-9]).my-domain.com
|
||||
```
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { ConfigReader } from '@backstage/config';
|
||||
import { readCspOptions } from './config';
|
||||
import { readCorsOptions, readCspOptions } from './config';
|
||||
|
||||
describe('config', () => {
|
||||
describe('readCspOptions', () => {
|
||||
@@ -42,4 +42,67 @@ describe('config', () => {
|
||||
expect(() => readCspOptions(config)).toThrow(/wanted string-array/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readCorsOptions', () => {
|
||||
it('reads single string', () => {
|
||||
const mockCallback = jest.fn();
|
||||
const config = new ConfigReader({ cors: { origin: 'https://*.value*' } });
|
||||
const cors = readCorsOptions(config);
|
||||
expect(cors).toEqual(
|
||||
expect.objectContaining({
|
||||
origin: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
const origin = cors?.origin as Function;
|
||||
origin('https://a.value', mockCallback); // valid origin
|
||||
origin('http://a.value', mockCallback); // invalid origin
|
||||
origin(undefined, mockCallback); // when not origin needs to reject the call
|
||||
|
||||
expect(mockCallback.mock.calls[0][0]).toBe(null);
|
||||
expect(mockCallback.mock.calls[1][0]).toBe(null);
|
||||
|
||||
expect(mockCallback.mock.calls[0][1]).toBe(true);
|
||||
expect(mockCallback.mock.calls[1][1]).toBe(false);
|
||||
expect(mockCallback.mock.calls[2][1]).toBe(false);
|
||||
});
|
||||
|
||||
it('reads string array', () => {
|
||||
const mockCallback = jest.fn();
|
||||
const config = new ConfigReader({
|
||||
cors: {
|
||||
origin: ['http?(s)://*.value?(-+([0-9])).com', 'http://*.value'],
|
||||
},
|
||||
});
|
||||
const cors = readCorsOptions(config);
|
||||
expect(cors).toEqual(
|
||||
expect.objectContaining({
|
||||
origin: expect.any(Function),
|
||||
}),
|
||||
);
|
||||
const origin = cors?.origin as Function;
|
||||
origin('https://a.b.c.value-9.com', mockCallback);
|
||||
origin('http://a.value-999.com', mockCallback);
|
||||
origin('http://a.value', mockCallback);
|
||||
origin('http://a.valuex', mockCallback);
|
||||
|
||||
expect(mockCallback.mock.calls[0][0]).toBe(null);
|
||||
expect(mockCallback.mock.calls[1][0]).toBe(null);
|
||||
expect(mockCallback.mock.calls[2][0]).toBe(null);
|
||||
expect(mockCallback.mock.calls[3][0]).toBe(null);
|
||||
|
||||
expect(mockCallback.mock.calls[0][1]).toBe(true);
|
||||
expect(mockCallback.mock.calls[1][1]).toBe(true);
|
||||
expect(mockCallback.mock.calls[2][1]).toBe(true);
|
||||
expect(mockCallback.mock.calls[3][1]).toBe(false);
|
||||
});
|
||||
|
||||
it('reads undefined origin', () => {
|
||||
const config = new ConfigReader({
|
||||
cors: {},
|
||||
});
|
||||
const cors = readCorsOptions(config);
|
||||
expect(cors).toEqual(expect.objectContaining({}));
|
||||
expect(cors?.origin).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import { Config } from '@backstage/config';
|
||||
import { CorsOptions } from 'cors';
|
||||
import { Minimatch } from 'minimatch';
|
||||
|
||||
export type BaseOptions = {
|
||||
listenPort?: string | number;
|
||||
@@ -46,6 +47,13 @@ export type CertificateAttributes = {
|
||||
*/
|
||||
export type CspOptions = Record<string, string[]>;
|
||||
|
||||
type StaticOrigin = boolean | string | RegExp | (boolean | string | RegExp)[];
|
||||
|
||||
type CustomOrigin = (
|
||||
requestOrigin: string | undefined,
|
||||
callback: (err: Error | null, origin?: StaticOrigin) => void,
|
||||
) => void;
|
||||
|
||||
/**
|
||||
* Reads some base options out of a config object.
|
||||
*
|
||||
@@ -112,7 +120,7 @@ export function readCorsOptions(config: Config): CorsOptions | undefined {
|
||||
}
|
||||
|
||||
return removeUnknown({
|
||||
origin: getOptionalStringOrStrings(cc, 'origin'),
|
||||
origin: createCorsOriginMatcher(getOptionalStringOrStrings(cc, 'origin')),
|
||||
methods: getOptionalStringOrStrings(cc, 'methods'),
|
||||
allowedHeaders: getOptionalStringOrStrings(cc, 'allowedHeaders'),
|
||||
exposedHeaders: getOptionalStringOrStrings(cc, 'exposedHeaders'),
|
||||
@@ -207,16 +215,45 @@ function getOptionalStringOrStrings(
|
||||
key: string,
|
||||
): string | string[] | undefined {
|
||||
const value = config.getOptional(key);
|
||||
if (
|
||||
value === undefined ||
|
||||
typeof value === 'string' ||
|
||||
isStringArray(value)
|
||||
) {
|
||||
if (value === undefined || isStringOrStrings(value)) {
|
||||
return value;
|
||||
}
|
||||
throw new Error(`Expected string or array of strings, got ${typeof value}`);
|
||||
}
|
||||
|
||||
function createCorsOriginMatcher(
|
||||
originValue: string | string[] | undefined,
|
||||
): CustomOrigin | undefined {
|
||||
if (originValue === undefined) {
|
||||
return originValue;
|
||||
}
|
||||
|
||||
if (!isStringOrStrings(originValue)) {
|
||||
throw new Error(
|
||||
`Expected string or array of strings, got ${typeof originValue}`,
|
||||
);
|
||||
}
|
||||
|
||||
const allowedOrigin =
|
||||
typeof originValue === 'string' ? [originValue] : originValue;
|
||||
|
||||
const allowedOriginPatterns =
|
||||
allowedOrigin?.map(
|
||||
pattern => new Minimatch(pattern, { nocase: true, noglobstar: true }),
|
||||
) ?? [];
|
||||
|
||||
return (origin, callback) => {
|
||||
return callback(
|
||||
null,
|
||||
allowedOriginPatterns.some(pattern => pattern.match(origin ?? '')),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
function isStringOrStrings(value: any): value is string | string[] {
|
||||
return typeof value === 'string' || isStringArray(value);
|
||||
}
|
||||
|
||||
function isStringArray(value: any): value is string[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user