Skip to content

Commit 91c63cf

Browse files
mshustovlegregokobelb
committed
Add ability to specify CORS accepted origins (#84316)
* add settings * update abab package to version with types * add test case for CORS * add tests for cors config * fix jest tests * add deprecation message * tweak deprecation * make test runable on Cloud * add docs * fix type error * add test to throw on invalid URL * address comments * Update src/core/server/http/http_config.test.ts Co-authored-by: Larry Gregory <lgregorydev@gmail.com> * Update docs/setup/settings.asciidoc Co-authored-by: Brandon Kobel <brandon.kobel@gmail.com> * allow kbn-xsrf headers to be set on CORS request Co-authored-by: Larry Gregory <lgregorydev@gmail.com> Co-authored-by: Brandon Kobel <brandon.kobel@gmail.com> # Conflicts: # src/core/server/config/deprecation/core_deprecations.ts # x-pack/scripts/functional_tests.js
1 parent 0968676 commit 91c63cf

26 files changed

Lines changed: 452 additions & 16 deletions

docs/setup/settings.asciidoc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,15 @@ deprecation warning at startup. This setting cannot end in a slash (`/`).
450450
| [[server-compression]] `server.compression.enabled:`
451451
| Set to `false` to disable HTTP compression for all responses. *Default: `true`*
452452

453+
| `server.cors.enabled:`
454+
| experimental[] Set to `true` to allow cross-origin API calls. *Default:* `false`
455+
456+
| `server.cors.credentials:`
457+
| experimental[] Set to `true` to allow browser code to access response body whenever request performed with user credentials. *Default:* `false`
458+
459+
| `server.cors.origin:`
460+
| experimental[] List of origins permitted to access resources. You must specify explicit hostnames and not use `*` for `server.cors.origin` when `server.cors.credentials: true`. *Default:* "*"
461+
453462
| `server.compression.referrerWhitelist:`
454463
| Specifies an array of trusted hostnames, such as the {kib} host, or a reverse
455464
proxy sitting in front of it. This determines whether HTTP compression may be used for responses, based on the request `Referer` header.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -570,7 +570,7 @@
570570
"@typescript-eslint/parser": "^4.8.1",
571571
"@welldone-software/why-did-you-render": "^5.0.0",
572572
"@yarnpkg/lockfile": "^1.1.0",
573-
"abab": "^1.0.4",
573+
"abab": "^2.0.4",
574574
"angular-aria": "^1.8.0",
575575
"angular-mocks": "^1.7.9",
576576
"angular-recursion": "^1.0.5",

src/core/server/config/deprecation/core_deprecations.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,32 @@ describe('core deprecations', () => {
9494
});
9595
});
9696

97+
describe('server.cors', () => {
98+
it('renames server.cors to server.cors.enabled', () => {
99+
const { migrated } = applyCoreDeprecations({
100+
server: { cors: true },
101+
});
102+
expect(migrated.server.cors).toEqual({ enabled: true });
103+
});
104+
it('logs a warning message about server.cors renaming', () => {
105+
const { messages } = applyCoreDeprecations({
106+
server: { cors: true },
107+
});
108+
expect(messages).toMatchInlineSnapshot(`
109+
Array [
110+
"\\"server.cors\\" is deprecated and has been replaced by \\"server.cors.enabled\\"",
111+
]
112+
`);
113+
});
114+
it('does not log deprecation message when server.cors.enabled set', () => {
115+
const { migrated, messages } = applyCoreDeprecations({
116+
server: { cors: { enabled: true } },
117+
});
118+
expect(migrated.server.cors).toEqual({ enabled: true });
119+
expect(messages.length).toBe(0);
120+
});
121+
});
122+
97123
describe('rewriteBasePath', () => {
98124
it('logs a warning is server.basePath is set and server.rewriteBasePath is not', () => {
99125
const { messages } = applyCoreDeprecations({

src/core/server/config/deprecation/core_deprecations.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,17 @@ const rewriteBasePathDeprecation: ConfigDeprecation = (settings, fromPath, log)
5050
return settings;
5151
};
5252

53+
const rewriteCorsSettings: ConfigDeprecation = (settings, fromPath, log) => {
54+
const corsSettings = get(settings, 'server.cors');
55+
if (typeof get(settings, 'server.cors') === 'boolean') {
56+
log('"server.cors" is deprecated and has been replaced by "server.cors.enabled"');
57+
settings.server.cors = {
58+
enabled: corsSettings,
59+
};
60+
}
61+
return settings;
62+
};
63+
5364
const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, log) => {
5465
const NONCE_STRING = `{nonce}`;
5566
// Policies that should include the 'self' source
@@ -142,6 +153,7 @@ export const coreDeprecationProvider: ConfigDeprecationProvider = ({
142153
renameFromRoot('server.xsrf.whitelist', 'server.xsrf.allowlist'),
143154
unusedFromRoot('elasticsearch.preserveHost'),
144155
unusedFromRoot('elasticsearch.startupTimeout'),
156+
rewriteCorsSettings,
145157
configPathDeprecation,
146158
dataPathDeprecation,
147159
rewriteBasePathDeprecation,

src/core/server/http/__snapshots__/http_config.test.ts.snap

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/core/server/http/cookie_session_storage.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ configService.atPath.mockImplementation((path) => {
6868
allowFromAnyIp: true,
6969
ipAllowlist: [],
7070
},
71+
cors: {
72+
enabled: false,
73+
},
7174
} as any);
7275
}
7376
if (path === 'externalUrl') {

src/core/server/http/http_config.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,59 @@ describe('with compression', () => {
330330
});
331331
});
332332

333+
describe('cors', () => {
334+
describe('origin', () => {
335+
it('list cannot be empty', () => {
336+
expect(() =>
337+
config.schema.validate({
338+
cors: {
339+
origin: [],
340+
},
341+
})
342+
).toThrowErrorMatchingInlineSnapshot(`
343+
"[cors.origin]: types that failed validation:
344+
- [cors.origin.0]: expected value to equal [*]
345+
- [cors.origin.1]: array size is [0], but cannot be smaller than [1]"
346+
`);
347+
});
348+
349+
it('list of valid URLs', () => {
350+
const origin = ['http://127.0.0.1:3000', 'https://elastic.co'];
351+
expect(
352+
config.schema.validate({
353+
cors: { origin },
354+
}).cors.origin
355+
).toStrictEqual(origin);
356+
357+
expect(() =>
358+
config.schema.validate({
359+
cors: {
360+
origin: ['*://elastic.co/*'],
361+
},
362+
})
363+
).toThrow();
364+
});
365+
366+
it('can be configured as "*" wildcard', () => {
367+
expect(config.schema.validate({ cors: { origin: '*' } }).cors.origin).toBe('*');
368+
});
369+
});
370+
describe('credentials', () => {
371+
it('cannot use wildcard origin if "credentials: true"', () => {
372+
expect(
373+
() => config.schema.validate({ cors: { credentials: true, origin: '*' } }).cors.origin
374+
).toThrowErrorMatchingInlineSnapshot(
375+
`"[cors]: Cannot specify wildcard origin \\"*\\" with \\"credentials: true\\". Please provide a list of allowed origins."`
376+
);
377+
expect(
378+
() => config.schema.validate({ cors: { credentials: true } }).cors.origin
379+
).toThrowErrorMatchingInlineSnapshot(
380+
`"[cors]: Cannot specify wildcard origin \\"*\\" with \\"credentials: true\\". Please provide a list of allowed origins."`
381+
);
382+
});
383+
});
384+
});
385+
333386
describe('HttpConfig', () => {
334387
it('converts customResponseHeaders to strings or arrays of strings', () => {
335388
const httpSchema = config.schema;

src/core/server/http/http_config.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { SslConfig, sslSchema } from './ssl_config';
2727

2828
const validBasePathRegex = /^\/.*[^\/]$/;
2929
const uuidRegexp = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
30-
30+
const hostURISchema = schema.uri({ scheme: ['http', 'https'] });
3131
const match = (regex: RegExp, errorMsg: string) => (str: string) =>
3232
regex.test(str) ? undefined : errorMsg;
3333

@@ -45,7 +45,25 @@ export const config = {
4545
validate: match(validBasePathRegex, "must start with a slash, don't end with one"),
4646
})
4747
),
48-
cors: schema.boolean({ defaultValue: false }),
48+
cors: schema.object(
49+
{
50+
enabled: schema.boolean({ defaultValue: false }),
51+
credentials: schema.boolean({ defaultValue: false }),
52+
origin: schema.oneOf(
53+
[schema.literal('*'), schema.arrayOf(hostURISchema, { minSize: 1 })],
54+
{
55+
defaultValue: '*',
56+
}
57+
),
58+
},
59+
{
60+
validate(value) {
61+
if (value.credentials === true && value.origin === '*') {
62+
return 'Cannot specify wildcard origin "*" with "credentials: true". Please provide a list of allowed origins.';
63+
}
64+
},
65+
}
66+
),
4967
customResponseHeaders: schema.recordOf(schema.string(), schema.any(), {
5068
defaultValue: {},
5169
}),
@@ -148,7 +166,11 @@ export class HttpConfig {
148166
public keepaliveTimeout: number;
149167
public socketTimeout: number;
150168
public port: number;
151-
public cors: boolean | { origin: string[] };
169+
public cors: {
170+
enabled: boolean;
171+
credentials: boolean;
172+
origin: '*' | string[];
173+
};
152174
public customResponseHeaders: Record<string, string | string[]>;
153175
public maxPayload: ByteSizeValue;
154176
public basePath?: string;

src/core/server/http/http_server.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ beforeEach(() => {
7272
allowFromAnyIp: true,
7373
ipAllowlist: [],
7474
},
75+
cors: {
76+
enabled: false,
77+
},
7578
} as any;
7679

7780
configWithSSL = {

src/core/server/http/http_tools.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ describe('timeouts', () => {
102102
host: '127.0.0.1',
103103
maxPayload: new ByteSizeValue(1024),
104104
ssl: {},
105+
cors: {
106+
enabled: false,
107+
},
105108
compression: { enabled: true },
106109
requestId: {
107110
allowFromAnyIp: true,
@@ -187,6 +190,26 @@ describe('getServerOptions', () => {
187190
}
188191
`);
189192
});
193+
194+
it('properly configures CORS when cors enabled', () => {
195+
const httpConfig = new HttpConfig(
196+
config.schema.validate({
197+
cors: {
198+
enabled: true,
199+
credentials: false,
200+
origin: '*',
201+
},
202+
}),
203+
{} as any,
204+
{} as any
205+
);
206+
207+
expect(getServerOptions(httpConfig).routes?.cors).toEqual({
208+
credentials: false,
209+
origin: '*',
210+
headers: ['Accept', 'Authorization', 'Content-Type', 'If-None-Match', 'kbn-xsrf'],
211+
});
212+
});
190213
});
191214

192215
describe('getRequestId', () => {

0 commit comments

Comments
 (0)