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
|
@ -42,4 +42,24 @@ for example, `logstash-*`.
|
|||
|
||||
*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[]
|
|
@ -136,7 +136,6 @@ kibana_vars=(
|
|||
xpack.reporting.queue.timeout
|
||||
xpack.reporting.roles.allow
|
||||
xpack.searchprofiler.enabled
|
||||
xpack.security.authProviders
|
||||
xpack.security.authc.providers
|
||||
xpack.security.cookieName
|
||||
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 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`] = `
|
||||
Object {
|
||||
"audit": Object {
|
||||
|
@ -28,7 +26,6 @@ Object {
|
|||
"cookieName": "sid",
|
||||
"enabled": true,
|
||||
"encryptionKey": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"public": Object {},
|
||||
"secureCookies": false,
|
||||
"sessionTimeout": null,
|
||||
}
|
||||
|
@ -51,7 +48,6 @@ Object {
|
|||
},
|
||||
"cookieName": "sid",
|
||||
"enabled": true,
|
||||
"public": Object {},
|
||||
"secureCookies": false,
|
||||
"sessionTimeout": null,
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
|
||||
import { resolve } from 'path';
|
||||
import { get, has } from 'lodash';
|
||||
import { getUserProvider } from './server/lib/get_user';
|
||||
import { initAuthenticateApi } from './server/routes/api/v1/authenticate';
|
||||
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),
|
||||
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({
|
||||
legacyFallback: Joi.object({
|
||||
enabled: Joi.boolean().default(true) // deprecated
|
||||
|
@ -75,26 +69,14 @@ export const security = (kibana) => new kibana.Plugin({
|
|||
authc: Joi.object({
|
||||
providers: Joi.array().items(Joi.string()).default(['basic']),
|
||||
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();
|
||||
},
|
||||
|
||||
deprecations: function ({ unused, rename }) {
|
||||
deprecations: function ({ unused }) {
|
||||
return [
|
||||
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', () => {
|
||||
it('`realm` is optional', async () => {
|
||||
it('fails if authc.providers includes `saml`, but `saml.realm` is not specified', async () => {
|
||||
const schema = await getConfigSchema(security);
|
||||
|
||||
let validationResult = schema.validate({
|
||||
authc: { providers: ['saml'] },
|
||||
});
|
||||
expect(schema.validate({ authc: { providers: ['saml'] } }).error).toMatchInlineSnapshot(
|
||||
`[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();
|
||||
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({
|
||||
const validationResult = schema.validate({
|
||||
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 () => {
|
||||
const schema = await getConfigSchema(security);
|
||||
expect(schema.validate({
|
||||
authc: {
|
||||
providers: ['basic'],
|
||||
saml: { realm: 'realm-1' },
|
||||
},
|
||||
}).error).toMatchSnapshot();
|
||||
expect(
|
||||
schema.validate({
|
||||
authc: {
|
||||
providers: ['basic'],
|
||||
saml: { realm: 'realm-1' },
|
||||
},
|
||||
}).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 {
|
||||
client: getClient(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'),
|
||||
|
||||
...config.get('xpack.security.public'),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -11,9 +11,6 @@ export function mockAuthenticationProviderOptions(
|
|||
providerOptions: Partial<AuthenticationProviderOptions> = {}
|
||||
) {
|
||||
return {
|
||||
hostname: 'test-hostname',
|
||||
port: 1234,
|
||||
protocol: 'test-protocol',
|
||||
client: { callWithRequest: stub(), callWithInternalUser: stub() },
|
||||
log: stub(),
|
||||
basePath: '/base-path',
|
||||
|
|
|
@ -20,9 +20,6 @@ export interface RequestWithLoginAttempt extends Legacy.Request {
|
|||
* Represents available provider options.
|
||||
*/
|
||||
export interface AuthenticationProviderOptions {
|
||||
protocol: string;
|
||||
hostname: string;
|
||||
port: number;
|
||||
basePath: string;
|
||||
client: Legacy.Plugins.elasticsearch.Cluster;
|
||||
log: (tags: string[], message: string) => void;
|
||||
|
|
|
@ -22,7 +22,21 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
callWithRequest = providerOptions.client.callWithRequest 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', () => {
|
||||
|
@ -73,36 +87,6 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
|
||||
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', {
|
||||
body: { realm: 'test-realm' },
|
||||
});
|
||||
|
@ -126,7 +110,7 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
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` },
|
||||
body: { realm: 'test-realm' },
|
||||
});
|
||||
|
||||
expect(authenticationResult.failed()).toBe(true);
|
||||
|
@ -392,7 +376,7 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
});
|
||||
|
||||
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);
|
||||
|
@ -432,7 +416,7 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
});
|
||||
|
||||
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);
|
||||
|
@ -759,7 +743,7 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
sinon.assert.calledWithExactly(callWithInternalUser, 'shield.samlInvalidate', {
|
||||
body: {
|
||||
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', {
|
||||
body: {
|
||||
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', {
|
||||
body: {
|
||||
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');
|
||||
});
|
||||
|
||||
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 () => {
|
||||
const request = requestFixture({ search: '?SAMLRequest=xxx%20yyy' });
|
||||
|
||||
|
@ -904,7 +866,7 @@ describe('SAMLAuthenticationProvider', () => {
|
|||
sinon.assert.calledWithExactly(callWithInternalUser, 'shield.samlInvalidate', {
|
||||
body: {
|
||||
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 {
|
||||
/**
|
||||
* Optionally specifies Elasticsearch SAML realm name that Kibana should use. If not specified
|
||||
* Kibana ACS URL is used for realm matching instead.
|
||||
* Specifies Elasticsearch SAML realm name that Kibana should use.
|
||||
*/
|
||||
private readonly realm?: string;
|
||||
private readonly realm: string;
|
||||
|
||||
constructor(
|
||||
protected readonly options: Readonly<AuthenticationProviderOptions>,
|
||||
samlOptions?: Readonly<{ realm?: string }>
|
||||
) {
|
||||
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 {
|
||||
// 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
|
||||
// user usually doesn't have `cluster:admin/xpack/security/saml/prepare`.
|
||||
const { id: requestId, redirect } = await this.options.client.callWithInternalUser(
|
||||
'shield.samlPrepare',
|
||||
{ body: preparePayload }
|
||||
{ body: { realm: this.realm } }
|
||||
);
|
||||
|
||||
this.debug('Redirecting to Identity Provider with SAML request.');
|
||||
|
@ -598,16 +599,13 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
|
|||
private async performIdPInitiatedSingleLogout(request: Legacy.Request) {
|
||||
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
|
||||
// user usually doesn't have `cluster:admin/xpack/security/saml/invalidate`.
|
||||
const { redirect } = await this.options.client.callWithInternalUser('shield.samlInvalidate', {
|
||||
// Elasticsearch expects `queryString` without leading `?`, so we should strip it with `slice`.
|
||||
body: {
|
||||
queryString: request.url.search ? request.url.search.slice(1) : '',
|
||||
...invalidatePayload,
|
||||
realm: this.realm,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -616,16 +614,6 @@ export class SAMLAuthenticationProvider extends BaseAuthenticationProvider {
|
|||
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.
|
||||
* @param message Message to log.
|
||||
|
|
|
@ -48,6 +48,7 @@ export default async function ({ readConfigFile }) {
|
|||
'--optimize.enabled=false',
|
||||
'--server.xsrf.whitelist=[\"/api/security/v1/saml\"]',
|
||||
`--xpack.security.authc.providers=${JSON.stringify(['saml', 'basic'])}`,
|
||||
'--xpack.security.authc.saml.realm=saml1',
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue