mirror of
https://github.com/elastic/kibana.git
synced 2025-06-28 03:01:21 -04:00
Make xpack.security.authc.saml.realm
mandatory and completely remove xpack.security.authProviders
and xpack.security.public
. (#38657)
This commit is contained in:
parent
120b060687
commit
ffb0b06fa3
11 changed files with 75 additions and 140 deletions
|
@ -42,4 +42,24 @@ for example, `logstash-*`.
|
||||||
|
|
||||||
*Impact:* To restore the previous behavior, in kibana.yml set `logging.timezone: UTC`.
|
*Impact:* To restore the previous behavior, in kibana.yml set `logging.timezone: UTC`.
|
||||||
|
|
||||||
|
[float]
|
||||||
|
==== `xpack.security.authProviders` is no longer valid
|
||||||
|
*Details:* The deprecated `xpack.security.authProviders` setting in the `kibana.yml` file has been removed.
|
||||||
|
|
||||||
|
*Impact:* Use `xpack.security.authc.providers` instead.
|
||||||
|
|
||||||
|
[float]
|
||||||
|
==== `xpack.security.authc.saml.realm` is now mandatory when using the SAML authentication provider
|
||||||
|
*Details:* Previously Kibana was choosing the appropriate Elasticsearch SAML realm automatically using the `Assertion Consumer Service`
|
||||||
|
URL that it derived from the actual server address. Starting in 8.0.0, the Elasticsearch SAML realm name that Kibana will use should be
|
||||||
|
specified explicitly.
|
||||||
|
|
||||||
|
*Impact:* Always define `xpack.security.authc.saml.realm` when using the SAML authentication provider.
|
||||||
|
|
||||||
|
[float]
|
||||||
|
==== `xpack.security.public` is no longer valid
|
||||||
|
*Details:* The deprecated `xpack.security.public` setting in the `kibana.yml` file has been removed.
|
||||||
|
|
||||||
|
*Impact:* Define `xpack.security.authc.saml.realm` when using the SAML authentication provider instead.
|
||||||
|
|
||||||
// end::notable-breaking-changes[]
|
// end::notable-breaking-changes[]
|
|
@ -136,7 +136,6 @@ kibana_vars=(
|
||||||
xpack.reporting.queue.timeout
|
xpack.reporting.queue.timeout
|
||||||
xpack.reporting.roles.allow
|
xpack.reporting.roles.allow
|
||||||
xpack.searchprofiler.enabled
|
xpack.searchprofiler.enabled
|
||||||
xpack.security.authProviders
|
|
||||||
xpack.security.authc.providers
|
xpack.security.authc.providers
|
||||||
xpack.security.cookieName
|
xpack.security.cookieName
|
||||||
xpack.security.enabled
|
xpack.security.enabled
|
||||||
|
|
|
@ -8,8 +8,6 @@ exports[`config schema authc oidc realm returns a validation error when authc.pr
|
||||||
|
|
||||||
exports[`config schema authc oidc realm returns a validation error when authc.providers is "['oidc']" and realm is unspecified 2`] = `[ValidationError: child "authc" fails because [child "oidc" fails because [child "realm" fails because ["realm" is required]]]]`;
|
exports[`config schema authc oidc realm returns a validation error when authc.providers is "['oidc']" and realm is unspecified 2`] = `[ValidationError: child "authc" fails because [child "oidc" fails because [child "realm" fails because ["realm" is required]]]]`;
|
||||||
|
|
||||||
exports[`config schema authc saml \`realm\` is not allowed if saml provider is not enabled 1`] = `[ValidationError: child "authc" fails because [child "saml" fails because ["saml" is not allowed]]]`;
|
|
||||||
|
|
||||||
exports[`config schema with context {"dist":false} produces correct config 1`] = `
|
exports[`config schema with context {"dist":false} produces correct config 1`] = `
|
||||||
Object {
|
Object {
|
||||||
"audit": Object {
|
"audit": Object {
|
||||||
|
@ -28,7 +26,6 @@ Object {
|
||||||
"cookieName": "sid",
|
"cookieName": "sid",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
"encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
"public": Object {},
|
|
||||||
"secureCookies": false,
|
"secureCookies": false,
|
||||||
"sessionTimeout": null,
|
"sessionTimeout": null,
|
||||||
}
|
}
|
||||||
|
@ -51,7 +48,6 @@ Object {
|
||||||
},
|
},
|
||||||
"cookieName": "sid",
|
"cookieName": "sid",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"public": Object {},
|
|
||||||
"secureCookies": false,
|
"secureCookies": false,
|
||||||
"sessionTimeout": null,
|
"sessionTimeout": null,
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
import { get, has } from 'lodash';
|
|
||||||
import { getUserProvider } from './server/lib/get_user';
|
import { getUserProvider } from './server/lib/get_user';
|
||||||
import { initAuthenticateApi } from './server/routes/api/v1/authenticate';
|
import { initAuthenticateApi } from './server/routes/api/v1/authenticate';
|
||||||
import { initUsersApi } from './server/routes/api/v1/users';
|
import { initUsersApi } from './server/routes/api/v1/users';
|
||||||
|
@ -59,11 +58,6 @@ export const security = (kibana) => new kibana.Plugin({
|
||||||
}),
|
}),
|
||||||
sessionTimeout: Joi.number().allow(null).default(null),
|
sessionTimeout: Joi.number().allow(null).default(null),
|
||||||
secureCookies: Joi.boolean().default(false),
|
secureCookies: Joi.boolean().default(false),
|
||||||
public: Joi.object({
|
|
||||||
protocol: Joi.string().valid(['http', 'https']),
|
|
||||||
hostname: Joi.string().hostname(),
|
|
||||||
port: Joi.number().integer().min(0).max(65535)
|
|
||||||
}).default(),
|
|
||||||
authorization: Joi.object({
|
authorization: Joi.object({
|
||||||
legacyFallback: Joi.object({
|
legacyFallback: Joi.object({
|
||||||
enabled: Joi.boolean().default(true) // deprecated
|
enabled: Joi.boolean().default(true) // deprecated
|
||||||
|
@ -75,26 +69,14 @@ export const security = (kibana) => new kibana.Plugin({
|
||||||
authc: Joi.object({
|
authc: Joi.object({
|
||||||
providers: Joi.array().items(Joi.string()).default(['basic']),
|
providers: Joi.array().items(Joi.string()).default(['basic']),
|
||||||
oidc: providerOptionsSchema('oidc', Joi.object({ realm: Joi.string().required() }).required()),
|
oidc: providerOptionsSchema('oidc', Joi.object({ realm: Joi.string().required() }).required()),
|
||||||
saml: providerOptionsSchema('saml', Joi.object({ realm: Joi.string() })),
|
saml: providerOptionsSchema('saml', Joi.object({ realm: Joi.string().required() }).required()),
|
||||||
}).default()
|
}).default()
|
||||||
}).default();
|
}).default();
|
||||||
},
|
},
|
||||||
|
|
||||||
deprecations: function ({ unused, rename }) {
|
deprecations: function ({ unused }) {
|
||||||
return [
|
return [
|
||||||
unused('authorization.legacyFallback.enabled'),
|
unused('authorization.legacyFallback.enabled'),
|
||||||
rename('authProviders', 'authc.providers'),
|
|
||||||
(settings, log) => {
|
|
||||||
const hasSAMLProvider = get(settings, 'authc.providers', []).includes('saml');
|
|
||||||
if (hasSAMLProvider && !get(settings, 'authc.saml.realm')) {
|
|
||||||
log('Config key "authc.saml.realm" will become mandatory when using the SAML authentication provider in the next major version.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (has(settings, 'public')) {
|
|
||||||
log('Config key "public" is deprecated and will be removed in the next major version. ' +
|
|
||||||
'Specify "authc.saml.realm" instead.');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -78,24 +78,19 @@ describe('config schema', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('saml', () => {
|
describe('saml', () => {
|
||||||
it('`realm` is optional', async () => {
|
it('fails if authc.providers includes `saml`, but `saml.realm` is not specified', async () => {
|
||||||
const schema = await getConfigSchema(security);
|
const schema = await getConfigSchema(security);
|
||||||
|
|
||||||
let validationResult = schema.validate({
|
expect(schema.validate({ authc: { providers: ['saml'] } }).error).toMatchInlineSnapshot(
|
||||||
authc: { providers: ['saml'] },
|
`[ValidationError: child "authc" fails because [child "saml" fails because ["saml" is required]]]`
|
||||||
});
|
);
|
||||||
|
expect(
|
||||||
|
schema.validate({ authc: { providers: ['saml'], saml: {} } }).error
|
||||||
|
).toMatchInlineSnapshot(
|
||||||
|
`[ValidationError: child "authc" fails because [child "saml" fails because [child "realm" fails because ["realm" is required]]]]`
|
||||||
|
);
|
||||||
|
|
||||||
expect(validationResult.error).toBeNull();
|
const validationResult = schema.validate({
|
||||||
expect(validationResult.value.authc.saml).toBeUndefined();
|
|
||||||
|
|
||||||
validationResult = schema.validate({
|
|
||||||
authc: { providers: ['saml'], saml: {} },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(validationResult.error).toBeNull();
|
|
||||||
expect(validationResult.value.authc.saml.realm).toBeUndefined();
|
|
||||||
|
|
||||||
validationResult = schema.validate({
|
|
||||||
authc: { providers: ['saml'], saml: { realm: 'realm-1' } },
|
authc: { providers: ['saml'], saml: { realm: 'realm-1' } },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -105,12 +100,16 @@ describe('config schema', () => {
|
||||||
|
|
||||||
it('`realm` is not allowed if saml provider is not enabled', async () => {
|
it('`realm` is not allowed if saml provider is not enabled', async () => {
|
||||||
const schema = await getConfigSchema(security);
|
const schema = await getConfigSchema(security);
|
||||||
expect(schema.validate({
|
expect(
|
||||||
|
schema.validate({
|
||||||
authc: {
|
authc: {
|
||||||
providers: ['basic'],
|
providers: ['basic'],
|
||||||
saml: { realm: 'realm-1' },
|
saml: { realm: 'realm-1' },
|
||||||
},
|
},
|
||||||
}).error).toMatchSnapshot();
|
}).error
|
||||||
|
).toMatchInlineSnapshot(
|
||||||
|
`[ValidationError: child "authc" fails because [child "saml" fails because ["saml" is not allowed]]]`
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -60,13 +60,7 @@ function getProviderOptions(server: Legacy.Server) {
|
||||||
return {
|
return {
|
||||||
client: getClient(server),
|
client: getClient(server),
|
||||||
log: server.log.bind(server),
|
log: server.log.bind(server),
|
||||||
|
|
||||||
protocol: server.info.protocol,
|
|
||||||
hostname: config.get<string>('server.host'),
|
|
||||||
port: config.get<number>('server.port'),
|
|
||||||
basePath: config.get<string>('server.basePath'),
|
basePath: config.get<string>('server.basePath'),
|
||||||
|
|
||||||
...config.get('xpack.security.public'),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,9 +11,6 @@ export function mockAuthenticationProviderOptions(
|
||||||
providerOptions: Partial<AuthenticationProviderOptions> = {}
|
providerOptions: Partial<AuthenticationProviderOptions> = {}
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
hostname: 'test-hostname',
|
|
||||||
port: 1234,
|
|
||||||
protocol: 'test-protocol',
|
|
||||||
client: { callWithRequest: stub(), callWithInternalUser: stub() },
|
client: { callWithRequest: stub(), callWithInternalUser: stub() },
|
||||||
log: stub(),
|
log: stub(),
|
||||||
basePath: '/base-path',
|
basePath: '/base-path',
|
||||||
|
|
|
@ -20,9 +20,6 @@ export interface RequestWithLoginAttempt extends Legacy.Request {
|
||||||
* Represents available provider options.
|
* Represents available provider options.
|
||||||
*/
|
*/
|
||||||
export interface AuthenticationProviderOptions {
|
export interface AuthenticationProviderOptions {
|
||||||
protocol: string;
|
|
||||||
hostname: string;
|
|
||||||
port: number;
|
|
||||||
basePath: string;
|
basePath: string;
|
||||||
client: Legacy.Plugins.elasticsearch.Cluster;
|
client: Legacy.Plugins.elasticsearch.Cluster;
|
||||||
log: (tags: string[], message: string) => void;
|
log: (tags: string[], message: string) => void;
|
||||||
|
|
|
@ -22,7 +22,21 @@ describe('SAMLAuthenticationProvider', () => {
|
||||||
callWithRequest = providerOptions.client.callWithRequest as sinon.SinonStub;
|
callWithRequest = providerOptions.client.callWithRequest as sinon.SinonStub;
|
||||||
callWithInternalUser = providerOptions.client.callWithInternalUser as sinon.SinonStub;
|
callWithInternalUser = providerOptions.client.callWithInternalUser as sinon.SinonStub;
|
||||||
|
|
||||||
provider = new SAMLAuthenticationProvider(providerOptions);
|
provider = new SAMLAuthenticationProvider(providerOptions, { realm: 'test-realm' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws if `realm` option is not specified', () => {
|
||||||
|
const providerOptions = mockAuthenticationProviderOptions({ basePath: '/test-base-path' });
|
||||||
|
|
||||||
|
expect(() => new SAMLAuthenticationProvider(providerOptions)).toThrowError(
|
||||||
|
'Realm name must be specified'
|
||||||
|
);
|
||||||
|
expect(() => new SAMLAuthenticationProvider(providerOptions, {})).toThrowError(
|
||||||
|
'Realm name must be specified'
|
||||||
|
);
|
||||||
|
expect(() => new SAMLAuthenticationProvider(providerOptions, { realm: '' })).toThrowError(
|
||||||
|
'Realm name must be specified'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('`authenticate` method', () => {
|
describe('`authenticate` method', () => {
|
||||||
|
@ -73,36 +87,6 @@ describe('SAMLAuthenticationProvider', () => {
|
||||||
|
|
||||||
const authenticationResult = await provider.authenticate(request, null);
|
const authenticationResult = await provider.authenticate(request, null);
|
||||||
|
|
||||||
sinon.assert.calledWithExactly(callWithInternalUser, 'shield.samlPrepare', {
|
|
||||||
body: { acs: `test-protocol://test-hostname:1234/test-base-path/api/security/v1/saml` },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(authenticationResult.redirected()).toBe(true);
|
|
||||||
expect(authenticationResult.redirectURL).toBe(
|
|
||||||
'https://idp-host/path/login?SAMLRequest=some%20request%20'
|
|
||||||
);
|
|
||||||
expect(authenticationResult.state).toEqual({
|
|
||||||
requestId: 'some-request-id',
|
|
||||||
nextURL: `/s/foo/some-path`,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('uses `realm` name instead of `acs` if it is specified for SAML prepare request.', async () => {
|
|
||||||
const request = requestFixture({ path: '/some-path', basePath: '/s/foo' });
|
|
||||||
|
|
||||||
// Create new provider instance with additional `realm` option.
|
|
||||||
const providerOptions = mockAuthenticationProviderOptions({ basePath: '/test-base-path' });
|
|
||||||
callWithRequest = providerOptions.client.callWithRequest as sinon.SinonStub;
|
|
||||||
callWithInternalUser = providerOptions.client.callWithInternalUser as sinon.SinonStub;
|
|
||||||
provider = new SAMLAuthenticationProvider(providerOptions, { realm: 'test-realm' });
|
|
||||||
|
|
||||||
callWithInternalUser.withArgs('shield.samlPrepare').resolves({
|
|
||||||
id: 'some-request-id',
|
|
||||||
redirect: 'https://idp-host/path/login?SAMLRequest=some%20request%20',
|
|
||||||
});
|
|
||||||
|
|
||||||
const authenticationResult = await provider.authenticate(request, null);
|
|
||||||
|
|
||||||
sinon.assert.calledWithExactly(callWithInternalUser, 'shield.samlPrepare', {
|
sinon.assert.calledWithExactly(callWithInternalUser, 'shield.samlPrepare', {
|
||||||
body: { realm: 'test-realm' },
|
body: { realm: 'test-realm' },
|
||||||
});
|
});
|
||||||
|
@ -126,7 +110,7 @@ describe('SAMLAuthenticationProvider', () => {
|
||||||
const authenticationResult = await provider.authenticate(request, null);
|
const authenticationResult = await provider.authenticate(request, null);
|
||||||
|
|
||||||
sinon.assert.calledWithExactly(callWithInternalUser, 'shield.samlPrepare', {
|
sinon.assert.calledWithExactly(callWithInternalUser, 'shield.samlPrepare', {
|
||||||
body: { acs: `test-protocol://test-hostname:1234/test-base-path/api/security/v1/saml` },
|
body: { realm: 'test-realm' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(authenticationResult.failed()).toBe(true);
|
expect(authenticationResult.failed()).toBe(true);
|
||||||
|
@ -392,7 +376,7 @@ describe('SAMLAuthenticationProvider', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
sinon.assert.calledWithExactly(callWithInternalUser, 'shield.samlPrepare', {
|
sinon.assert.calledWithExactly(callWithInternalUser, 'shield.samlPrepare', {
|
||||||
body: { acs: `test-protocol://test-hostname:1234/test-base-path/api/security/v1/saml` },
|
body: { realm: 'test-realm' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(authenticationResult.redirected()).toBe(true);
|
expect(authenticationResult.redirected()).toBe(true);
|
||||||
|
@ -432,7 +416,7 @@ describe('SAMLAuthenticationProvider', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
sinon.assert.calledWithExactly(callWithInternalUser, 'shield.samlPrepare', {
|
sinon.assert.calledWithExactly(callWithInternalUser, 'shield.samlPrepare', {
|
||||||
body: { acs: `test-protocol://test-hostname:1234/test-base-path/api/security/v1/saml` },
|
body: { realm: 'test-realm' },
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(authenticationResult.redirected()).toBe(true);
|
expect(authenticationResult.redirected()).toBe(true);
|
||||||
|
@ -759,7 +743,7 @@ describe('SAMLAuthenticationProvider', () => {
|
||||||
sinon.assert.calledWithExactly(callWithInternalUser, 'shield.samlInvalidate', {
|
sinon.assert.calledWithExactly(callWithInternalUser, 'shield.samlInvalidate', {
|
||||||
body: {
|
body: {
|
||||||
queryString: 'SAMLRequest=xxx%20yyy',
|
queryString: 'SAMLRequest=xxx%20yyy',
|
||||||
acs: 'test-protocol://test-hostname:1234/test-base-path/api/security/v1/saml',
|
realm: 'test-realm',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -844,7 +828,7 @@ describe('SAMLAuthenticationProvider', () => {
|
||||||
sinon.assert.calledWithExactly(callWithInternalUser, 'shield.samlInvalidate', {
|
sinon.assert.calledWithExactly(callWithInternalUser, 'shield.samlInvalidate', {
|
||||||
body: {
|
body: {
|
||||||
queryString: 'SAMLRequest=xxx%20yyy',
|
queryString: 'SAMLRequest=xxx%20yyy',
|
||||||
acs: 'test-protocol://test-hostname:1234/test-base-path/api/security/v1/saml',
|
realm: 'test-realm',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -863,7 +847,7 @@ describe('SAMLAuthenticationProvider', () => {
|
||||||
sinon.assert.calledWithExactly(callWithInternalUser, 'shield.samlInvalidate', {
|
sinon.assert.calledWithExactly(callWithInternalUser, 'shield.samlInvalidate', {
|
||||||
body: {
|
body: {
|
||||||
queryString: 'SAMLRequest=xxx%20yyy',
|
queryString: 'SAMLRequest=xxx%20yyy',
|
||||||
acs: 'test-protocol://test-hostname:1234/test-base-path/api/security/v1/saml',
|
realm: 'test-realm',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -871,28 +855,6 @@ describe('SAMLAuthenticationProvider', () => {
|
||||||
expect(authenticationResult.redirectURL).toBe('/logged_out');
|
expect(authenticationResult.redirectURL).toBe('/logged_out');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses `realm` name instead of `acs` if it is specified for SAML invalidate request.', async () => {
|
|
||||||
const request = requestFixture({ search: '?SAMLRequest=xxx%20yyy' });
|
|
||||||
|
|
||||||
// Create new provider instance with additional `realm` option.
|
|
||||||
const providerOptions = mockAuthenticationProviderOptions({ basePath: '/test-base-path' });
|
|
||||||
callWithRequest = providerOptions.client.callWithRequest as sinon.SinonStub;
|
|
||||||
callWithInternalUser = providerOptions.client.callWithInternalUser as sinon.SinonStub;
|
|
||||||
provider = new SAMLAuthenticationProvider(providerOptions, { realm: 'test-realm' });
|
|
||||||
|
|
||||||
callWithInternalUser.withArgs('shield.samlInvalidate').resolves({ redirect: undefined });
|
|
||||||
|
|
||||||
const authenticationResult = await provider.deauthenticate(request);
|
|
||||||
|
|
||||||
sinon.assert.calledOnce(callWithInternalUser);
|
|
||||||
sinon.assert.calledWithExactly(callWithInternalUser, 'shield.samlInvalidate', {
|
|
||||||
body: { queryString: 'SAMLRequest=xxx%20yyy', realm: 'test-realm' },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(authenticationResult.redirected()).toBe(true);
|
|
||||||
expect(authenticationResult.redirectURL).toBe('/logged_out');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('redirects to /logged_out if `redirect` field in SAML invalidate response is not defined.', async () => {
|
it('redirects to /logged_out if `redirect` field in SAML invalidate response is not defined.', async () => {
|
||||||
const request = requestFixture({ search: '?SAMLRequest=xxx%20yyy' });
|
const request = requestFixture({ search: '?SAMLRequest=xxx%20yyy' });
|
||||||
|
|
||||||
|
@ -904,7 +866,7 @@ describe('SAMLAuthenticationProvider', () => {
|
||||||
sinon.assert.calledWithExactly(callWithInternalUser, 'shield.samlInvalidate', {
|
sinon.assert.calledWithExactly(callWithInternalUser, 'shield.samlInvalidate', {
|
||||||
body: {
|
body: {
|
||||||
queryString: 'SAMLRequest=xxx%20yyy',
|
queryString: 'SAMLRequest=xxx%20yyy',
|
||||||
acs: 'test-protocol://test-hostname:1234/test-base-path/api/security/v1/saml',
|
realm: 'test-realm',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -100,17 +100,21 @@ function isSAMLRequestQuery(query: any): query is SAMLRequestQuery {
|
||||||
*/
|
*/
|
||||||
export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
|
export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
|
||||||
/**
|
/**
|
||||||
* Optionally specifies Elasticsearch SAML realm name that Kibana should use. If not specified
|
* Specifies Elasticsearch SAML realm name that Kibana should use.
|
||||||
* Kibana ACS URL is used for realm matching instead.
|
|
||||||
*/
|
*/
|
||||||
private readonly realm?: string;
|
private readonly realm: string;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected readonly options: Readonly<AuthenticationProviderOptions>,
|
protected readonly options: Readonly<AuthenticationProviderOptions>,
|
||||||
samlOptions?: Readonly<{ realm?: string }>
|
samlOptions?: Readonly<{ realm?: string }>
|
||||||
) {
|
) {
|
||||||
super(options);
|
super(options);
|
||||||
this.realm = samlOptions && samlOptions.realm;
|
|
||||||
|
if (!samlOptions || !samlOptions.realm) {
|
||||||
|
throw new Error('Realm name must be specified');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.realm = samlOptions.realm;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -505,14 +509,11 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Prefer realm name if it's specified, otherwise fallback to ACS.
|
|
||||||
const preparePayload = this.realm ? { realm: this.realm } : { acs: this.getACS() };
|
|
||||||
|
|
||||||
// This operation should be performed on behalf of the user with a privilege that normal
|
// This operation should be performed on behalf of the user with a privilege that normal
|
||||||
// user usually doesn't have `cluster:admin/xpack/security/saml/prepare`.
|
// user usually doesn't have `cluster:admin/xpack/security/saml/prepare`.
|
||||||
const { id: requestId, redirect } = await this.options.client.callWithInternalUser(
|
const { id: requestId, redirect } = await this.options.client.callWithInternalUser(
|
||||||
'shield.samlPrepare',
|
'shield.samlPrepare',
|
||||||
{ body: preparePayload }
|
{ body: { realm: this.realm } }
|
||||||
);
|
);
|
||||||
|
|
||||||
this.debug('Redirecting to Identity Provider with SAML request.');
|
this.debug('Redirecting to Identity Provider with SAML request.');
|
||||||
|
@ -598,16 +599,13 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
|
||||||
private async performIdPInitiatedSingleLogout(request: Legacy.Request) {
|
private async performIdPInitiatedSingleLogout(request: Legacy.Request) {
|
||||||
this.debug('Single logout has been initiated by the Identity Provider.');
|
this.debug('Single logout has been initiated by the Identity Provider.');
|
||||||
|
|
||||||
// Prefer realm name if it's specified, otherwise fallback to ACS.
|
|
||||||
const invalidatePayload = this.realm ? { realm: this.realm } : { acs: this.getACS() };
|
|
||||||
|
|
||||||
// This operation should be performed on behalf of the user with a privilege that normal
|
// This operation should be performed on behalf of the user with a privilege that normal
|
||||||
// user usually doesn't have `cluster:admin/xpack/security/saml/invalidate`.
|
// user usually doesn't have `cluster:admin/xpack/security/saml/invalidate`.
|
||||||
const { redirect } = await this.options.client.callWithInternalUser('shield.samlInvalidate', {
|
const { redirect } = await this.options.client.callWithInternalUser('shield.samlInvalidate', {
|
||||||
// Elasticsearch expects `queryString` without leading `?`, so we should strip it with `slice`.
|
// Elasticsearch expects `queryString` without leading `?`, so we should strip it with `slice`.
|
||||||
body: {
|
body: {
|
||||||
queryString: request.url.search ? request.url.search.slice(1) : '',
|
queryString: request.url.search ? request.url.search.slice(1) : '',
|
||||||
...invalidatePayload,
|
realm: this.realm,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -616,16 +614,6 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
|
||||||
return redirect;
|
return redirect;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructs and returns Kibana's Assertion consumer service URL.
|
|
||||||
*/
|
|
||||||
private getACS() {
|
|
||||||
return (
|
|
||||||
`${this.options.protocol}://${this.options.hostname}:${this.options.port}` +
|
|
||||||
`${this.options.basePath}/api/security/v1/saml`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logs message with `debug` level and saml/security related tags.
|
* Logs message with `debug` level and saml/security related tags.
|
||||||
* @param message Message to log.
|
* @param message Message to log.
|
||||||
|
|
|
@ -48,6 +48,7 @@ export default async function ({ readConfigFile }) {
|
||||||
'--optimize.enabled=false',
|
'--optimize.enabled=false',
|
||||||
'--server.xsrf.whitelist=[\"/api/security/v1/saml\"]',
|
'--server.xsrf.whitelist=[\"/api/security/v1/saml\"]',
|
||||||
`--xpack.security.authc.providers=${JSON.stringify(['saml', 'basic'])}`,
|
`--xpack.security.authc.providers=${JSON.stringify(['saml', 'basic'])}`,
|
||||||
|
'--xpack.security.authc.saml.realm=saml1',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue