Skip to content

Commit

Permalink
Prepare the Security domain HTTP APIs for Serverless (elastic#162087)
Browse files Browse the repository at this point in the history
Closes elastic#161337

## Summary

Uses build flavor(see elastic#161930) to disable specific Kibana security,
spaces, and encrypted saved objects HTTP API routes in serverless (see
details in elastic#161337). HTTP APIs that will be public in serverless have
been handled in elastic#162523.

**IMPORTANT: This PR leaves login, user, and role routes enabled. The
primary reason for this is due to several testing mechanisms that rely
on basic authentication and custom roles (UI, Cypress). These tests will
be modified to use SAML authentication and serverless roles in the
immediate future. Once this occurs, we will disable these routes.**

### Testing
This PR also implements testing API access in serverless.
- The testing strategy for disabled routes in serverless is to verify a
`404 not found `response.
- The testing strategy for internal access routes in serverless is to
verify that without the internal request header
(`x-elastic-internal-origin`), a `400 bad request response` is received,
then verify that with the internal request header, a `200 ok response`
is received.
- The strategy for public routes in serverless is to verify a `200 ok`
or `203 redirect` is received.

~~blocked by elastic#161930~~
~~blocked by elastic#162149 for test implementation~~

---------

Co-authored-by: kibanamachine <[email protected]>
Co-authored-by: Aleh Zasypkin <[email protected]>
Co-authored-by: Aleh Zasypkin <[email protected]>
  • Loading branch information
4 people authored Aug 23, 2023
1 parent f4f286f commit fe0ffab
Show file tree
Hide file tree
Showing 56 changed files with 1,581 additions and 186 deletions.
3 changes: 3 additions & 0 deletions config/serverless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ xpack.cloud_integrations.data_migration.enabled: false
data.search.sessions.enabled: false
advanced_settings.enabled: false

# Disable the browser-side functionality that depends on SecurityCheckupGetStateRoutes
xpack.security.showInsecureClusterWarning: false

# Disable UI of security management plugins
xpack.security.ui.userManagementEnabled: false
xpack.security.ui.roleManagementEnabled: false
Expand Down
30 changes: 17 additions & 13 deletions x-pack/plugins/encrypted_saved_objects/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,19 +95,23 @@ export class EncryptedSavedObjectsPlugin
getStartServices: core.getStartServices,
});

defineRoutes({
router: core.http.createRouter(),
logger: this.initializerContext.logger.get('routes'),
encryptionKeyRotationService: Object.freeze(
new EncryptionKeyRotationService({
logger: this.logger.get('key-rotation-service'),
service,
getStartServices: core.getStartServices,
security: deps.security,
})
),
config,
});
// In the serverless environment, the encryption keys for saved objects is managed internally and never
// exposed to users and administrators, eliminating the need for any public Encrypted Saved Objects HTTP APIs
if (this.initializerContext.env.packageInfo.buildFlavor !== 'serverless') {
defineRoutes({
router: core.http.createRouter(),
logger: this.initializerContext.logger.get('routes'),
encryptionKeyRotationService: Object.freeze(
new EncryptionKeyRotationService({
logger: this.logger.get('key-rotation-service'),
service,
getStartServices: core.getStartServices,
security: deps.security,
})
),
config,
});
}

return {
canEncrypt,
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/security/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ export class SecurityPlugin
getAnonymousAccessService: this.getAnonymousAccess,
getUserProfileService: this.getUserProfileService,
analyticsService: this.analyticsService.setup({ analytics: core.analytics }),
buildFlavor: this.initializerContext.env.packageInfo.buildFlavor,
});

return Object.freeze<SecurityPluginSetup>({
Expand Down
46 changes: 30 additions & 16 deletions x-pack/plugins/security/server/routes/authentication/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,14 @@ export function defineCommonRoutes({
basePath,
license,
logger,
buildFlavor,
}: RouteDefinitionParams) {
// Generate two identical routes with new and deprecated URL and issue a warning if route with deprecated URL is ever used.
for (const path of ['/api/security/logout', '/api/security/v1/logout']) {
// For a serverless build, do not register deprecated versioned routes
for (const path of [
'/api/security/logout',
...(buildFlavor !== 'serverless' ? ['/api/security/v1/logout'] : []),
]) {
router.get(
{
path,
Expand Down Expand Up @@ -79,7 +84,11 @@ export function defineCommonRoutes({
}

// Generate two identical routes with new and deprecated URL and issue a warning if route with deprecated URL is ever used.
for (const path of ['/internal/security/me', '/api/security/v1/me']) {
// For a serverless build, do not register deprecated versioned routes
for (const path of [
'/internal/security/me',
...(buildFlavor !== 'serverless' ? ['/api/security/v1/me'] : []),
]) {
router.get(
{ path, validate: false },
createLicensedRouteHandler((context, request, response) => {
Expand Down Expand Up @@ -123,6 +132,8 @@ export function defineCommonRoutes({
return undefined;
}

// Register the login route for serverless for the time being. Note: This route will move into the buildFlavor !== 'serverless' block below. See next line.
// ToDo: In the serverless environment, we do not support API login - the only valid authentication methodology (or maybe just method or mechanism?) is SAML
router.post(
{
path: '/internal/security/login',
Expand Down Expand Up @@ -169,20 +180,23 @@ export function defineCommonRoutes({
})
);

router.post(
{ path: '/internal/security/access_agreement/acknowledge', validate: false },
createLicensedRouteHandler(async (context, request, response) => {
// If license doesn't allow access agreement we shouldn't handle request.
if (!license.getFeatures().allowAccessAgreement) {
logger.warn(`Attempted to acknowledge access agreement when license doesn't allow it.`);
return response.forbidden({
body: { message: `Current license doesn't support access agreement.` },
});
}
if (buildFlavor !== 'serverless') {
// In the serverless offering, the access agreement functionality isn't available.
router.post(
{ path: '/internal/security/access_agreement/acknowledge', validate: false },
createLicensedRouteHandler(async (context, request, response) => {
// If license doesn't allow access agreement we shouldn't handle request.
if (!license.getFeatures().allowAccessAgreement) {
logger.warn(`Attempted to acknowledge access agreement when license doesn't allow it.`);
return response.forbidden({
body: { message: `Current license doesn't support access agreement.` },
});
}

await getAuthenticationService().acknowledgeAccessAgreement(request);
await getAuthenticationService().acknowledgeAccessAgreement(request);

return response.noContent();
})
);
return response.noContent();
})
);
}
}
7 changes: 6 additions & 1 deletion x-pack/plugins/security/server/routes/authentication/saml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,14 @@ export function defineSAMLRoutes({
getAuthenticationService,
basePath,
logger,
buildFlavor,
}: RouteDefinitionParams) {
// Generate two identical routes with new and deprecated URL and issue a warning if route with deprecated URL is ever used.
for (const path of ['/api/security/saml/callback', '/api/security/v1/saml']) {
// For a serverless build, do not register deprecated versioned routes
for (const path of [
'/api/security/saml/callback',
...(buildFlavor !== 'serverless' ? ['/api/security/v1/saml'] : []),
]) {
router.post(
{
path,
Expand Down
12 changes: 9 additions & 3 deletions x-pack/plugins/security/server/routes/authorization/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,14 @@ import { defineShareSavedObjectPermissionRoutes } from './spaces';
import type { RouteDefinitionParams } from '..';

export function defineAuthorizationRoutes(params: RouteDefinitionParams) {
defineRolesRoutes(params);
definePrivilegesRoutes(params);
// The reset session endpoint is registered with httpResources and should remain public in serverless
resetSessionPageRoutes(params);
defineShareSavedObjectPermissionRoutes(params);
defineRolesRoutes(params); // Temporarily allow role APIs (ToDo: move to non-serverless block below)

// In the serverless environment, roles, privileges, and permissions are managed internally and only
// exposed to users and administrators via control plane UI, eliminating the need for any public HTTP APIs.
if (params.buildFlavor !== 'serverless') {
definePrivilegesRoutes(params);
defineShareSavedObjectPermissionRoutes(params);
}
}
23 changes: 15 additions & 8 deletions x-pack/plugins/security/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import type { Observable } from 'rxjs';

import type { BuildFlavor } from '@kbn/config/src/types';
import type { HttpResources, IBasePath, Logger } from '@kbn/core/server';
import type { KibanaFeature } from '@kbn/features-plugin/server';
import type { PublicMethodsOf } from '@kbn/utility-types';
Expand Down Expand Up @@ -54,20 +55,26 @@ export interface RouteDefinitionParams {
getUserProfileService: () => UserProfileServiceStartInternal;
getAnonymousAccessService: () => AnonymousAccessServiceStart;
analyticsService: AnalyticsServiceSetup;
buildFlavor: BuildFlavor;
}

export function defineRoutes(params: RouteDefinitionParams) {
defineAnalyticsRoutes(params);
defineApiKeysRoutes(params);
defineAuthenticationRoutes(params);
defineAuthorizationRoutes(params);
defineSessionManagementRoutes(params);
defineApiKeysRoutes(params);
defineIndicesRoutes(params);
defineUsersRoutes(params);
defineUserProfileRoutes(params);
defineRoleMappingRoutes(params);
defineUsersRoutes(params); // Temporarily allow user APIs (ToDo: move to non-serverless block below)
defineViewRoutes(params);
defineDeprecationsRoutes(params);
defineAnonymousAccessRoutes(params);
defineSecurityCheckupGetStateRoutes(params);
defineAnalyticsRoutes(params);

// In the serverless environment...
if (params.buildFlavor !== 'serverless') {
defineAnonymousAccessRoutes(params); // anonymous access is disabled
defineDeprecationsRoutes(params); // deprecated kibana user roles are not applicable, these HTTP APIs are not needed
defineIndicesRoutes(params); // the ES privileges form used to help define roles (only consumer) is disabled, so there is no need for these HTTP APIs
defineRoleMappingRoutes(params); // role mappings are managed internally, based on configurations in control plane, these HTTP APIs are not needed
defineSecurityCheckupGetStateRoutes(params); // security checkup is not applicable, these HTTP APIs are not needed
// defineUsersRoutes(params); // the native realm is not enabled (there is only Elastic cloud SAML), no user HTTP API routes are needed
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,12 @@ import type { RouteDefinitionParams } from '..';
export function defineSessionManagementRoutes(params: RouteDefinitionParams) {
defineSessionInfoRoutes(params);
defineSessionExtendRoutes(params);
defineInvalidateSessionsRoutes(params);

// The invalidate session API was introduced to address situations where the session index
// could grow rapidly - when session timeouts are disabled, or with anonymous access.
// In the serverless environment, sessions timeouts are always be enabled, and there is no
// anonymous access. This eliminates the need for an invalidate session HTTP API.
if (params.buildFlavor !== 'serverless') {
defineInvalidateSessionsRoutes(params);
}
}
55 changes: 42 additions & 13 deletions x-pack/plugins/security/server/routes/views/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,20 @@ describe('View routes', () => {
it('does not register Login routes if both `basic` and `token` providers are disabled', () => {
const routeParamsMock = routeDefinitionParamsMock.create({
authc: { providers: { pki: { pki1: { order: 0 } } } },
accessAgreement: { message: 'some-message' },
});

defineViewRoutes(routeParamsMock);

expect(routeParamsMock.httpResources.register.mock.calls.map(([{ path }]) => path))
.toMatchInlineSnapshot(`
Array [
"/security/access_agreement",
"/security/account",
"/internal/security/capture-url",
"/security/logged_out",
"/logout",
"/security/overwritten_session",
"/internal/security/capture-url",
"/security/access_agreement",
]
`);
expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(`
Expand All @@ -37,80 +38,108 @@ describe('View routes', () => {
it('registers Login routes if `basic` provider is enabled', () => {
const routeParamsMock = routeDefinitionParamsMock.create({
authc: { providers: { basic: { basic1: { order: 0 } } } },
accessAgreement: { message: 'some-message' },
});

defineViewRoutes(routeParamsMock);

expect(routeParamsMock.httpResources.register.mock.calls.map(([{ path }]) => path))
.toMatchInlineSnapshot(`
Array [
"/login",
"/security/access_agreement",
"/security/account",
"/internal/security/capture-url",
"/security/logged_out",
"/logout",
"/security/overwritten_session",
"/internal/security/capture-url",
"/security/access_agreement",
"/login",
]
`);
expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(`
Array [
"/internal/security/login_state",
"/internal/security/access_agreement/state",
"/internal/security/login_state",
]
`);
});

it('registers Login routes if `token` provider is enabled', () => {
const routeParamsMock = routeDefinitionParamsMock.create({
authc: { providers: { token: { token1: { order: 0 } } } },
accessAgreement: { message: 'some-message' },
});

defineViewRoutes(routeParamsMock);

expect(routeParamsMock.httpResources.register.mock.calls.map(([{ path }]) => path))
.toMatchInlineSnapshot(`
Array [
"/login",
"/security/access_agreement",
"/security/account",
"/internal/security/capture-url",
"/security/logged_out",
"/logout",
"/security/overwritten_session",
"/internal/security/capture-url",
"/security/access_agreement",
"/login",
]
`);
expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(`
Array [
"/internal/security/login_state",
"/internal/security/access_agreement/state",
"/internal/security/login_state",
]
`);
});

it('registers Login routes if Login Selector is enabled even if both `token` and `basic` providers are not enabled', () => {
const routeParamsMock = routeDefinitionParamsMock.create({
authc: { selector: { enabled: true }, providers: { pki: { pki1: { order: 0 } } } },
accessAgreement: { message: 'some-message' },
});

defineViewRoutes(routeParamsMock);

expect(routeParamsMock.httpResources.register.mock.calls.map(([{ path }]) => path))
.toMatchInlineSnapshot(`
Array [
"/login",
"/security/access_agreement",
"/security/account",
"/internal/security/capture-url",
"/security/logged_out",
"/logout",
"/security/overwritten_session",
"/security/access_agreement",
"/login",
]
`);
expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(`
Array [
"/internal/security/access_agreement/state",
"/internal/security/login_state",
]
`);
});

it('does not register access agreement routes if access agreement is not enabled', () => {
const routeParamsMock = routeDefinitionParamsMock.create({
authc: { providers: { basic: { basic1: { order: 0 } } } },
});

defineViewRoutes(routeParamsMock);

expect(routeParamsMock.httpResources.register.mock.calls.map(([{ path }]) => path))
.toMatchInlineSnapshot(`
Array [
"/security/account",
"/internal/security/capture-url",
"/security/logged_out",
"/logout",
"/security/overwritten_session",
"/login",
]
`);
expect(routeParamsMock.router.get.mock.calls.map(([{ path }]) => path)).toMatchInlineSnapshot(`
Array [
"/internal/security/login_state",
"/internal/security/access_agreement/state",
]
`);
});
Expand Down
20 changes: 13 additions & 7 deletions x-pack/plugins/security/server/routes/views/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,23 @@ import { defineOverwrittenSessionRoutes } from './overwritten_session';
import type { RouteDefinitionParams } from '..';

export function defineViewRoutes(params: RouteDefinitionParams) {
defineAccountManagementRoutes(params);
defineCaptureURLRoutes(params);
defineLoggedOutRoutes(params);
defineLogoutRoutes(params);
defineOverwrittenSessionRoutes(params);

if (
params.config.accessAgreement?.message ||
params.config.authc.sortedProviders.some(({ hasAccessAgreement }) => hasAccessAgreement)
) {
defineAccessAgreementRoutes(params);
}

if (
params.config.authc.selector.enabled ||
params.config.authc.sortedProviders.some(({ type }) => type === 'basic' || type === 'token')
) {
defineLoginRoutes(params);
}

defineAccessAgreementRoutes(params);
defineAccountManagementRoutes(params);
defineLoggedOutRoutes(params);
defineLogoutRoutes(params);
defineOverwrittenSessionRoutes(params);
defineCaptureURLRoutes(params);
}
Loading

0 comments on commit fe0ffab

Please sign in to comment.