From fe0ffab1da904945ec2fc6c7106c7e9a8f4eed72 Mon Sep 17 00:00:00 2001 From: Jeramy Soucy Date: Wed, 23 Aug 2023 06:34:45 -0400 Subject: [PATCH] Prepare the Security domain HTTP APIs for Serverless (#162087) Closes #161337 ## Summary Uses build flavor(see #161930) to disable specific Kibana security, spaces, and encrypted saved objects HTTP API routes in serverless (see details in #161337). HTTP APIs that will be public in serverless have been handled in #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 #161930~~ ~~blocked by #162149 for test implementation~~ --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Aleh Zasypkin Co-authored-by: Aleh Zasypkin --- config/serverless.yml | 3 + .../encrypted_saved_objects/server/plugin.ts | 30 ++- x-pack/plugins/security/server/plugin.ts | 1 + .../server/routes/authentication/common.ts | 46 ++-- .../server/routes/authentication/saml.ts | 7 +- .../server/routes/authorization/index.ts | 12 +- .../plugins/security/server/routes/index.ts | 23 +- .../server/routes/session_management/index.ts | 9 +- .../server/routes/views/index.test.ts | 55 ++++- .../security/server/routes/views/index.ts | 20 +- x-pack/plugins/spaces/server/plugin.ts | 25 +- .../routes/api/external/copy_to_space.test.ts | 2 +- .../routes/api/external/copy_to_space.ts | 6 +- .../server/routes/api/external/delete.test.ts | 2 +- .../server/routes/api/external/delete.ts | 4 +- .../disable_legacy_url_aliases.test.ts | 2 +- .../external/disable_legacy_url_aliases.ts | 4 +- .../server/routes/api/external/get.test.ts | 2 +- .../spaces/server/routes/api/external/get.ts | 4 +- .../routes/api/external/get_all.test.ts | 2 +- .../server/routes/api/external/get_all.ts | 4 +- .../external/get_shareable_references.test.ts | 2 +- .../api/external/get_shareable_references.ts | 4 +- .../server/routes/api/external/index.ts | 26 +- .../server/routes/api/external/post.test.ts | 2 +- .../spaces/server/routes/api/external/post.ts | 4 +- .../server/routes/api/external/put.test.ts | 2 +- .../spaces/server/routes/api/external/put.ts | 4 +- .../external/update_objects_spaces.test.ts | 2 +- .../api/external/update_objects_spaces.ts | 4 +- .../api/internal/get_active_space.test.ts | 2 +- .../routes/api/internal/get_active_space.ts | 4 +- .../server/routes/api/internal/index.ts | 2 +- .../spaces_client/spaces_client.test.ts | 18 +- x-pack/plugins/spaces/tsconfig.json | 1 + .../api_integration/services/index.ts | 2 + .../api_integration/services/saml_tools.ts | 42 ++++ .../services/svl_common_api.ts | 9 + .../common/encrypted_saved_objects.ts | 26 ++ .../test_suites/common/index.ts | 14 +- .../test_suites/common/security/anonymous.ts | 33 +++ .../test_suites/common/security/api_keys.ts | 212 ++++++++++++++++ .../common/security/authentication.ts | 201 ++++++++++++++++ .../common/security/authorization.ts | 110 +++++++++ .../test_suites/common/security/misc.ts | 58 +++++ .../response_headers.ts} | 4 +- .../common/security/role_mappings.ts | 55 +++++ .../test_suites/common/security/sessions.ts | 78 ++++++ .../common/security/user_profiles.ts | 48 ++++ .../test_suites/common/security/users.ts | 132 ++++++++++ .../test_suites/common/security/views.ts | 114 +++++++++ .../test_suites/common/security_users.ts | 27 --- .../test_suites/common/spaces.ts | 226 +++++++++++++++--- .../security/cypress/tasks/login.ts | 3 +- x-pack/test_serverless/shared/config.base.ts | 32 ++- x-pack/test_serverless/tsconfig.json | 1 + 56 files changed, 1581 insertions(+), 186 deletions(-) create mode 100644 x-pack/test_serverless/api_integration/services/saml_tools.ts create mode 100644 x-pack/test_serverless/api_integration/test_suites/common/encrypted_saved_objects.ts create mode 100644 x-pack/test_serverless/api_integration/test_suites/common/security/anonymous.ts create mode 100644 x-pack/test_serverless/api_integration/test_suites/common/security/api_keys.ts create mode 100644 x-pack/test_serverless/api_integration/test_suites/common/security/authentication.ts create mode 100644 x-pack/test_serverless/api_integration/test_suites/common/security/authorization.ts create mode 100644 x-pack/test_serverless/api_integration/test_suites/common/security/misc.ts rename x-pack/test_serverless/api_integration/test_suites/common/{security_response_headers.ts => security/response_headers.ts} (95%) create mode 100644 x-pack/test_serverless/api_integration/test_suites/common/security/role_mappings.ts create mode 100644 x-pack/test_serverless/api_integration/test_suites/common/security/sessions.ts create mode 100644 x-pack/test_serverless/api_integration/test_suites/common/security/user_profiles.ts create mode 100644 x-pack/test_serverless/api_integration/test_suites/common/security/users.ts create mode 100644 x-pack/test_serverless/api_integration/test_suites/common/security/views.ts delete mode 100644 x-pack/test_serverless/api_integration/test_suites/common/security_users.ts diff --git a/config/serverless.yml b/config/serverless.yml index e548146f39de5..a3c4990174642 100644 --- a/config/serverless.yml +++ b/config/serverless.yml @@ -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 diff --git a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts index 92a663fab64d4..7b7fed43f5181 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/plugin.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/plugin.ts @@ -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, diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 4f36c0bf508d0..d196dd9f41139 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -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({ diff --git a/x-pack/plugins/security/server/routes/authentication/common.ts b/x-pack/plugins/security/server/routes/authentication/common.ts index 4eeeed2998098..f359c320151e8 100644 --- a/x-pack/plugins/security/server/routes/authentication/common.ts +++ b/x-pack/plugins/security/server/routes/authentication/common.ts @@ -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, @@ -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) => { @@ -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', @@ -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(); + }) + ); + } } diff --git a/x-pack/plugins/security/server/routes/authentication/saml.ts b/x-pack/plugins/security/server/routes/authentication/saml.ts index 350f3527f3310..ddc31fbc88b89 100644 --- a/x-pack/plugins/security/server/routes/authentication/saml.ts +++ b/x-pack/plugins/security/server/routes/authentication/saml.ts @@ -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, diff --git a/x-pack/plugins/security/server/routes/authorization/index.ts b/x-pack/plugins/security/server/routes/authorization/index.ts index b3b29e950d721..0e4cc467f3b14 100644 --- a/x-pack/plugins/security/server/routes/authorization/index.ts +++ b/x-pack/plugins/security/server/routes/authorization/index.ts @@ -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); + } } diff --git a/x-pack/plugins/security/server/routes/index.ts b/x-pack/plugins/security/server/routes/index.ts index ba33ca319cd20..99739208e6b7a 100644 --- a/x-pack/plugins/security/server/routes/index.ts +++ b/x-pack/plugins/security/server/routes/index.ts @@ -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'; @@ -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 + } } diff --git a/x-pack/plugins/security/server/routes/session_management/index.ts b/x-pack/plugins/security/server/routes/session_management/index.ts index 041feea8a62fd..c095a77409975 100644 --- a/x-pack/plugins/security/server/routes/session_management/index.ts +++ b/x-pack/plugins/security/server/routes/session_management/index.ts @@ -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); + } } diff --git a/x-pack/plugins/security/server/routes/views/index.test.ts b/x-pack/plugins/security/server/routes/views/index.test.ts index 755a5a1202c73..f5f0296a39c19 100644 --- a/x-pack/plugins/security/server/routes/views/index.test.ts +++ b/x-pack/plugins/security/server/routes/views/index.test.ts @@ -12,6 +12,7 @@ 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); @@ -19,12 +20,12 @@ describe('View routes', () => { 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(` @@ -37,6 +38,7 @@ 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); @@ -44,19 +46,19 @@ describe('View routes', () => { 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", ] `); }); @@ -64,6 +66,7 @@ describe('View routes', () => { 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); @@ -71,19 +74,19 @@ describe('View routes', () => { 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", ] `); }); @@ -91,6 +94,7 @@ describe('View routes', () => { 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); @@ -98,19 +102,44 @@ describe('View routes', () => { 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", ] `); }); diff --git a/x-pack/plugins/security/server/routes/views/index.ts b/x-pack/plugins/security/server/routes/views/index.ts index f1efa4611dc58..c9fbb3b1bc363 100644 --- a/x-pack/plugins/security/server/routes/views/index.ts +++ b/x-pack/plugins/security/server/routes/views/index.ts @@ -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); } diff --git a/x-pack/plugins/spaces/server/plugin.ts b/x-pack/plugins/spaces/server/plugin.ts index cb8b42f343baa..893d3db22d709 100644 --- a/x-pack/plugins/spaces/server/plugin.ts +++ b/x-pack/plugins/spaces/server/plugin.ts @@ -103,7 +103,7 @@ export class SpacesPlugin private defaultSpaceService?: DefaultSpaceService; - constructor(initializerContext: PluginInitializerContext) { + constructor(private readonly initializerContext: PluginInitializerContext) { this.config$ = initializerContext.config.create(); this.log = initializerContext.logger.get(); this.spacesService = new SpacesService(); @@ -148,18 +148,21 @@ export class SpacesPlugin logger: this.log, }); - const externalRouter = core.http.createRouter(); - initExternalSpacesApi({ - externalRouter, - log: this.log, - getStartServices: core.getStartServices, - getSpacesService, - usageStatsServicePromise, - }); + const router = core.http.createRouter(); + + initExternalSpacesApi( + { + router, + log: this.log, + getStartServices: core.getStartServices, + getSpacesService, + usageStatsServicePromise, + }, + this.initializerContext.env.packageInfo.buildFlavor + ); - const internalRouter = core.http.createRouter(); initInternalSpacesApi({ - internalRouter, + router, getSpacesService, }); diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts index 4018cfa11b7b7..7722316799076 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts @@ -80,7 +80,7 @@ describe('copy to space', () => { }); initCopyToSpacesApi({ - externalRouter: router, + router, getStartServices: async () => [coreStart, {}, {}], log, getSpacesService: () => spacesServiceStart, diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts index 7faf03ea60b57..0c866de5bde66 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.ts @@ -24,10 +24,10 @@ const areObjectsUnique = (objects: SavedObjectIdentifier[]) => _.uniqBy(objects, (o: SavedObjectIdentifier) => `${o.type}:${o.id}`).length === objects.length; export function initCopyToSpacesApi(deps: ExternalRouteDeps) { - const { externalRouter, getSpacesService, usageStatsServicePromise, getStartServices } = deps; + const { router, getSpacesService, usageStatsServicePromise, getStartServices } = deps; const usageStatsClientPromise = usageStatsServicePromise.then(({ getClient }) => getClient()); - externalRouter.post( + router.post( { path: '/api/spaces/_copy_saved_objects', options: { @@ -137,7 +137,7 @@ export function initCopyToSpacesApi(deps: ExternalRouteDeps) { }) ); - externalRouter.post( + router.post( { path: '/api/spaces/_resolve_copy_saved_objects_errors', options: { diff --git a/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts b/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts index 02792389424db..09ed757a5df74 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/delete.test.ts @@ -62,7 +62,7 @@ describe('Spaces Public API', () => { }); initDeleteSpacesApi({ - externalRouter: router, + router, getStartServices: async () => [coreStart, {}, {}], log, getSpacesService: () => spacesServiceStart, diff --git a/x-pack/plugins/spaces/server/routes/api/external/delete.ts b/x-pack/plugins/spaces/server/routes/api/external/delete.ts index b2f0d71d34a21..63679f49847b7 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/delete.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/delete.ts @@ -15,9 +15,9 @@ import { wrapError } from '../../../lib/errors'; import { createLicensedRouteHandler } from '../../lib'; export function initDeleteSpacesApi(deps: ExternalRouteDeps) { - const { externalRouter, log, getSpacesService } = deps; + const { router, log, getSpacesService } = deps; - externalRouter.delete( + router.delete( { path: '/api/spaces/space/{id}', validate: { diff --git a/x-pack/plugins/spaces/server/routes/api/external/disable_legacy_url_aliases.test.ts b/x-pack/plugins/spaces/server/routes/api/external/disable_legacy_url_aliases.test.ts index 6af58c124be08..ed7c403182ab5 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/disable_legacy_url_aliases.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/disable_legacy_url_aliases.test.ts @@ -65,7 +65,7 @@ describe('_disable_legacy_url_aliases', () => { }); initDisableLegacyUrlAliasesApi({ - externalRouter: router, + router, getStartServices: async () => [coreStart, {}, {}], log, getSpacesService: () => spacesServiceStart, diff --git a/x-pack/plugins/spaces/server/routes/api/external/disable_legacy_url_aliases.ts b/x-pack/plugins/spaces/server/routes/api/external/disable_legacy_url_aliases.ts index ff523ea99c06b..725d0c87612fb 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/disable_legacy_url_aliases.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/disable_legacy_url_aliases.ts @@ -12,10 +12,10 @@ import { wrapError } from '../../../lib/errors'; import { createLicensedRouteHandler } from '../../lib'; export function initDisableLegacyUrlAliasesApi(deps: ExternalRouteDeps) { - const { externalRouter, getSpacesService, usageStatsServicePromise } = deps; + const { router, getSpacesService, usageStatsServicePromise } = deps; const usageStatsClientPromise = usageStatsServicePromise.then(({ getClient }) => getClient()); - externalRouter.post( + router.post( { path: '/api/spaces/_disable_legacy_url_aliases', validate: { diff --git a/x-pack/plugins/spaces/server/routes/api/external/get.test.ts b/x-pack/plugins/spaces/server/routes/api/external/get.test.ts index 43ac45ec3c4c5..34c1ed01e0ce4 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get.test.ts @@ -61,7 +61,7 @@ describe('GET space', () => { }); initGetSpaceApi({ - externalRouter: router, + router, getStartServices: async () => [coreStart, {}, {}], log, getSpacesService: () => spacesServiceStart, diff --git a/x-pack/plugins/spaces/server/routes/api/external/get.ts b/x-pack/plugins/spaces/server/routes/api/external/get.ts index a26cfe0211d16..ce89aac5fe186 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get.ts @@ -13,9 +13,9 @@ import { wrapError } from '../../../lib/errors'; import { createLicensedRouteHandler } from '../../lib'; export function initGetSpaceApi(deps: ExternalRouteDeps) { - const { externalRouter, getSpacesService } = deps; + const { router, getSpacesService } = deps; - externalRouter.get( + router.get( { path: '/api/spaces/space/{id}', validate: { diff --git a/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts b/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts index 8fa87bf5ffa42..93eab7a52b81e 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get_all.test.ts @@ -62,7 +62,7 @@ describe('GET /spaces/space', () => { }); initGetAllSpacesApi({ - externalRouter: router, + router, getStartServices: async () => [coreStart, {}, {}], log, getSpacesService: () => spacesServiceStart, diff --git a/x-pack/plugins/spaces/server/routes/api/external/get_all.ts b/x-pack/plugins/spaces/server/routes/api/external/get_all.ts index 06d629a194560..c1dc24caf151e 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get_all.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get_all.ts @@ -13,9 +13,9 @@ import { wrapError } from '../../../lib/errors'; import { createLicensedRouteHandler } from '../../lib'; export function initGetAllSpacesApi(deps: ExternalRouteDeps) { - const { externalRouter, log, getSpacesService } = deps; + const { router, log, getSpacesService } = deps; - externalRouter.get( + router.get( { path: '/api/spaces/space', validate: { diff --git a/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.test.ts b/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.test.ts index daa957c04d11f..c9ea0d71c11b7 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.test.ts @@ -61,7 +61,7 @@ describe('get shareable references', () => { spacesClientService: clientServiceStart, }); initGetShareableReferencesApi({ - externalRouter: router, + router, getStartServices: async () => [coreStart, {}, {}], log, getSpacesService: () => spacesServiceStart, diff --git a/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.ts b/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.ts index ec5b0ce82ece4..f9b18961fae59 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/get_shareable_references.ts @@ -12,9 +12,9 @@ import { wrapError } from '../../../lib/errors'; import { createLicensedRouteHandler } from '../../lib'; export function initGetShareableReferencesApi(deps: ExternalRouteDeps) { - const { externalRouter, getStartServices } = deps; + const { router, getStartServices } = deps; - externalRouter.post( + router.post( { path: '/api/spaces/_get_shareable_references', validate: { diff --git a/x-pack/plugins/spaces/server/routes/api/external/index.ts b/x-pack/plugins/spaces/server/routes/api/external/index.ts index 8716f63a5657f..290807f9b5f4d 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/index.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { BuildFlavor } from '@kbn/config/src/types'; import type { CoreSetup, Logger } from '@kbn/core/server'; import { initCopyToSpacesApi } from './copy_to_space'; @@ -21,21 +22,28 @@ import type { SpacesRouter } from '../../../types'; import type { UsageStatsServiceSetup } from '../../../usage_stats'; export interface ExternalRouteDeps { - externalRouter: SpacesRouter; + router: SpacesRouter; getStartServices: CoreSetup['getStartServices']; getSpacesService: () => SpacesServiceStart; usageStatsServicePromise: Promise; log: Logger; } -export function initExternalSpacesApi(deps: ExternalRouteDeps) { - initDeleteSpacesApi(deps); +export function initExternalSpacesApi(deps: ExternalRouteDeps, buildFlavor: BuildFlavor) { + // These two routes are always registered, internal in serverless by default initGetSpaceApi(deps); initGetAllSpacesApi(deps); - initPostSpacesApi(deps); - initPutSpacesApi(deps); - initCopyToSpacesApi(deps); - initUpdateObjectsSpacesApi(deps); - initGetShareableReferencesApi(deps); - initDisableLegacyUrlAliasesApi(deps); + + // In the serverless environment, Spaces are enabled but are effectively hidden from the user. We + // do not support more than 1 space: the default space. These HTTP APIs for creating, deleting, + // updating, and manipulating saved objects across multiple spaces are not needed. + if (buildFlavor !== 'serverless') { + initPutSpacesApi(deps); + initDeleteSpacesApi(deps); + initPostSpacesApi(deps); + initCopyToSpacesApi(deps); + initUpdateObjectsSpacesApi(deps); + initGetShareableReferencesApi(deps); + initDisableLegacyUrlAliasesApi(deps); + } } diff --git a/x-pack/plugins/spaces/server/routes/api/external/post.test.ts b/x-pack/plugins/spaces/server/routes/api/external/post.test.ts index 01c08eca85ec7..88c763f31c0be 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/post.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/post.test.ts @@ -62,7 +62,7 @@ describe('Spaces Public API', () => { }); initPostSpacesApi({ - externalRouter: router, + router, getStartServices: async () => [coreStart, {}, {}], log, getSpacesService: () => spacesServiceStart, diff --git a/x-pack/plugins/spaces/server/routes/api/external/post.ts b/x-pack/plugins/spaces/server/routes/api/external/post.ts index 3ea6da647b4f2..d8091a0140e00 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/post.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/post.ts @@ -15,9 +15,9 @@ import { spaceSchema } from '../../../lib/space_schema'; import { createLicensedRouteHandler } from '../../lib'; export function initPostSpacesApi(deps: ExternalRouteDeps) { - const { externalRouter, log, getSpacesService } = deps; + const { router, log, getSpacesService } = deps; - externalRouter.post( + router.post( { path: '/api/spaces/space', validate: { diff --git a/x-pack/plugins/spaces/server/routes/api/external/put.test.ts b/x-pack/plugins/spaces/server/routes/api/external/put.test.ts index 126d15268edf8..5e1e6077a758e 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/put.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/put.test.ts @@ -62,7 +62,7 @@ describe('PUT /api/spaces/space', () => { }); initPutSpacesApi({ - externalRouter: router, + router, getStartServices: async () => [coreStart, {}, {}], log, getSpacesService: () => spacesServiceStart, diff --git a/x-pack/plugins/spaces/server/routes/api/external/put.ts b/x-pack/plugins/spaces/server/routes/api/external/put.ts index fb9f818576580..753ec8e028925 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/put.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/put.ts @@ -15,9 +15,9 @@ import { spaceSchema } from '../../../lib/space_schema'; import { createLicensedRouteHandler } from '../../lib'; export function initPutSpacesApi(deps: ExternalRouteDeps) { - const { externalRouter, getSpacesService } = deps; + const { router, getSpacesService } = deps; - externalRouter.put( + router.put( { path: '/api/spaces/space/{id}', validate: { diff --git a/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.test.ts b/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.test.ts index 4c5fd44a6bb30..195db8148a378 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.test.ts @@ -62,7 +62,7 @@ describe('update_objects_spaces', () => { spacesClientService: clientServiceStart, }); initUpdateObjectsSpacesApi({ - externalRouter: router, + router, getStartServices: async () => [coreStart, {}, {}], log, getSpacesService: () => spacesServiceStart, diff --git a/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.ts b/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.ts index ea95557514d52..5e610c9693ab5 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/update_objects_spaces.ts @@ -14,7 +14,7 @@ import { SPACE_ID_REGEX } from '../../../lib/space_schema'; import { createLicensedRouteHandler } from '../../lib'; export function initUpdateObjectsSpacesApi(deps: ExternalRouteDeps) { - const { externalRouter, getStartServices } = deps; + const { router, getStartServices } = deps; const spacesSchema = schema.arrayOf( schema.string({ @@ -33,7 +33,7 @@ export function initUpdateObjectsSpacesApi(deps: ExternalRouteDeps) { } ); - externalRouter.post( + router.post( { path: '/api/spaces/_update_objects_spaces', validate: { diff --git a/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.test.ts b/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.test.ts index 172f1afec53cf..afa0f0a995944 100644 --- a/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.test.ts @@ -26,7 +26,7 @@ describe('GET /internal/spaces/_active_space', () => { }); initGetActiveSpaceApi({ - internalRouter: router, + router, getSpacesService: () => service.start({ basePath: coreStart.http.basePath, diff --git a/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.ts b/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.ts index feb6d8b59628b..2996e7dbc4ed1 100644 --- a/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.ts +++ b/x-pack/plugins/spaces/server/routes/api/internal/get_active_space.ts @@ -10,9 +10,9 @@ import { wrapError } from '../../../lib/errors'; import { createLicensedRouteHandler } from '../../lib'; export function initGetActiveSpaceApi(deps: InternalRouteDeps) { - const { internalRouter, getSpacesService } = deps; + const { router, getSpacesService } = deps; - internalRouter.get( + router.get( { path: '/internal/spaces/_active_space', validate: false, diff --git a/x-pack/plugins/spaces/server/routes/api/internal/index.ts b/x-pack/plugins/spaces/server/routes/api/internal/index.ts index 2f732bfcaf5ab..0cf8135d7b718 100644 --- a/x-pack/plugins/spaces/server/routes/api/internal/index.ts +++ b/x-pack/plugins/spaces/server/routes/api/internal/index.ts @@ -10,7 +10,7 @@ import type { SpacesServiceStart } from '../../../spaces_service/spaces_service' import type { SpacesRouter } from '../../../types'; export interface InternalRouteDeps { - internalRouter: SpacesRouter; + router: SpacesRouter; getSpacesService: () => SpacesServiceStart; } diff --git a/x-pack/plugins/spaces/server/spaces_client/spaces_client.test.ts b/x-pack/plugins/spaces/server/spaces_client/spaces_client.test.ts index e9305f07f1b0e..67c926229601b 100644 --- a/x-pack/plugins/spaces/server/spaces_client/spaces_client.test.ts +++ b/x-pack/plugins/spaces/server/spaces_client/spaces_client.test.ts @@ -18,7 +18,11 @@ const createMockDebugLogger = () => { }; const createMockConfig = ( - mockConfig: ConfigType = { enabled: true, maxSpaces: 1000, allowFeatureVisibility: true } + mockConfig: ConfigType = { + enabled: true, + maxSpaces: 1000, + allowFeatureVisibility: true, + } ) => { return ConfigSchema.validate(mockConfig, { serverless: !mockConfig.allowFeatureVisibility }); }; @@ -209,7 +213,11 @@ describe('#create', () => { total: maxSpaces - 1, } as any); - const mockConfig = createMockConfig({ enabled: true, maxSpaces, allowFeatureVisibility: true }); + const mockConfig = createMockConfig({ + enabled: true, + maxSpaces, + allowFeatureVisibility: true, + }); const client = new SpacesClient(mockDebugLogger, mockConfig, mockCallWithRequestRepository, []); @@ -235,7 +243,11 @@ describe('#create', () => { total: maxSpaces, } as any); - const mockConfig = createMockConfig({ enabled: true, maxSpaces, allowFeatureVisibility: true }); + const mockConfig = createMockConfig({ + enabled: true, + maxSpaces, + allowFeatureVisibility: true, + }); const client = new SpacesClient(mockDebugLogger, mockConfig, mockCallWithRequestRepository, []); diff --git a/x-pack/plugins/spaces/tsconfig.json b/x-pack/plugins/spaces/tsconfig.json index 43ea0f1c6c562..d59e30e3a0194 100644 --- a/x-pack/plugins/spaces/tsconfig.json +++ b/x-pack/plugins/spaces/tsconfig.json @@ -31,6 +31,7 @@ "@kbn/core-custom-branding-browser-mocks", "@kbn/core-custom-branding-common", "@kbn/shared-ux-link-redirect-app", + "@kbn/config", ], "exclude": [ "target/**/*", diff --git a/x-pack/test_serverless/api_integration/services/index.ts b/x-pack/test_serverless/api_integration/services/index.ts index 8102eeb9f4c1b..06e7c33fd7099 100644 --- a/x-pack/test_serverless/api_integration/services/index.ts +++ b/x-pack/test_serverless/api_integration/services/index.ts @@ -12,6 +12,7 @@ import { services as svlSharedServices } from '../../shared/services'; import { SvlCommonApiServiceProvider } from './svl_common_api'; import { AlertingApiProvider } from './alerting_api'; +import { SamlToolsProvider } from './saml_tools'; import { DataViewApiProvider } from './data_view_api'; export const services = { @@ -20,6 +21,7 @@ export const services = { svlCommonApi: SvlCommonApiServiceProvider, alertingApi: AlertingApiProvider, + samlTools: SamlToolsProvider, dataViewApi: DataViewApiProvider, }; diff --git a/x-pack/test_serverless/api_integration/services/saml_tools.ts b/x-pack/test_serverless/api_integration/services/saml_tools.ts new file mode 100644 index 0000000000000..4756109fc667d --- /dev/null +++ b/x-pack/test_serverless/api_integration/services/saml_tools.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +// eslint-disable-next-line @kbn/imports/no_boundary_crossing +import { getSAMLResponse } from '@kbn/security-api-integration-helpers/saml/saml_tools'; +import { kbnTestConfig } from '@kbn/test'; + +import { parse as parseCookie } from 'tough-cookie'; + +import { FtrProviderContext } from '../ftr_provider_context'; + +export function SamlToolsProvider({ getService }: FtrProviderContext) { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const randomness = getService('randomness'); + const svlCommonApi = getService('svlCommonApi'); + + function createSAMLResponse(options = {}) { + return getSAMLResponse({ + destination: `http://localhost:${kbnTestConfig.getPort()}/api/security/saml/callback`, + sessionIndex: String(randomness.naturalNumber()), + ...options, + }); + } + + return { + async login(username: string) { + const samlAuthenticationResponse = await supertestWithoutAuth + .post('/api/security/saml/callback') + .set(svlCommonApi.getCommonRequestHeader()) + .send({ SAMLResponse: await createSAMLResponse({ username }) }); + expect(samlAuthenticationResponse.status).to.equal(302); + expect(samlAuthenticationResponse.header.location).to.equal('/'); + const sessionCookie = parseCookie(samlAuthenticationResponse.header['set-cookie'][0])!; + return { Cookie: sessionCookie.cookieString() }; + }, + }; +} diff --git a/x-pack/test_serverless/api_integration/services/svl_common_api.ts b/x-pack/test_serverless/api_integration/services/svl_common_api.ts index 15b4f15f851fe..b23c8f70a3092 100644 --- a/x-pack/test_serverless/api_integration/services/svl_common_api.ts +++ b/x-pack/test_serverless/api_integration/services/svl_common_api.ts @@ -36,5 +36,14 @@ export function SvlCommonApiServiceProvider({}: FtrProviderContext) { )}'` ); }, + + assertApiNotFound(body: unknown, status: number) { + expect(body).to.eql({ + statusCode: 404, + error: 'Not Found', + message: 'Not Found', + }); + expect(status).to.eql(404); + }, }; } diff --git a/x-pack/test_serverless/api_integration/test_suites/common/encrypted_saved_objects.ts b/x-pack/test_serverless/api_integration/test_suites/common/encrypted_saved_objects.ts new file mode 100644 index 0000000000000..be5dc924c839d --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/encrypted_saved_objects.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const svlCommonApi = getService('svlCommonApi'); + const supertest = getService('supertest'); + + describe('encrypted saved objects', function () { + describe('route access', () => { + describe('disabled', () => { + it('rotate key', async () => { + const { body, status } = await supertest + .post('/api/encrypted_saved_objects/_rotate_key') + .set(svlCommonApi.getCommonRequestHeader()); + svlCommonApi.assertApiNotFound(body, status); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/index.ts b/x-pack/test_serverless/api_integration/test_suites/common/index.ts index 8296dd0d33c2c..3f6751b8e4d02 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/index.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/index.ts @@ -9,9 +9,19 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('serverless common API', function () { - loadTestFile(require.resolve('./security_users')); + loadTestFile(require.resolve('./encrypted_saved_objects')); + loadTestFile(require.resolve('./security/anonymous')); + loadTestFile(require.resolve('./security/api_keys')); + loadTestFile(require.resolve('./security/authentication')); + loadTestFile(require.resolve('./security/authorization')); + loadTestFile(require.resolve('./security/misc')); + loadTestFile(require.resolve('./security/response_headers')); + loadTestFile(require.resolve('./security/role_mappings')); + loadTestFile(require.resolve('./security/sessions')); + loadTestFile(require.resolve('./security/users')); + loadTestFile(require.resolve('./security/user_profiles')); + loadTestFile(require.resolve('./security/views')); loadTestFile(require.resolve('./spaces')); - loadTestFile(require.resolve('./security_response_headers')); loadTestFile(require.resolve('./rollups')); loadTestFile(require.resolve('./scripted_fields')); loadTestFile(require.resolve('./index_management')); diff --git a/x-pack/test_serverless/api_integration/test_suites/common/security/anonymous.ts b/x-pack/test_serverless/api_integration/test_suites/common/security/anonymous.ts new file mode 100644 index 0000000000000..0b08aa18ce128 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/security/anonymous.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const svlCommonApi = getService('svlCommonApi'); + const supertest = getService('supertest'); + + describe('security/anonymous', function () { + describe('route access', () => { + describe('disabled', () => { + it('get access capabilities', async () => { + const { body, status } = await supertest + .get('/internal/security/anonymous_access/capabilities') + .set(svlCommonApi.getCommonRequestHeader()); + svlCommonApi.assertApiNotFound(body, status); + }); + + it('get access state', async () => { + const { body, status } = await supertest + .get('/internal/security/anonymous_access/state') + .set(svlCommonApi.getCommonRequestHeader()); + svlCommonApi.assertApiNotFound(body, status); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/security/api_keys.ts b/x-pack/test_serverless/api_integration/test_suites/common/security/api_keys.ts new file mode 100644 index 0000000000000..256a0fcfe32a4 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/security/api_keys.ts @@ -0,0 +1,212 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from 'expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const svlCommonApi = getService('svlCommonApi'); + const supertest = getService('supertest'); + let roleMapping: { id: string; name: string; api_key: string; encoded: string }; + + describe('security/api_keys', function () { + describe('route access', () => { + describe('internal', () => { + before(async () => { + const { body, status } = await supertest + .post('/internal/security/api_key') + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + name: 'test', + metadata: {}, + role_descriptors: {}, + }); + expect(status).toBe(200); + roleMapping = body; + }); + + after(async () => { + const { body, status } = await supertest + .get('/internal/security/api_key?isAdmin=true') + .set(svlCommonApi.getInternalRequestHeader()); + + if (status === 200) { + await supertest + .post('/internal/security/api_key/invalidate') + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + apiKeys: body?.apiKeys, + isAdmin: true, + }); + } + }); + + it('create', async () => { + let body: unknown; + let status: number; + const requestBody = { + name: 'create_test', + metadata: {}, + role_descriptors: {}, + }; + + ({ body, status } = await supertest + .post('/internal/security/api_key') + .set(svlCommonApi.getCommonRequestHeader()) + .send(requestBody)); + // expect a rejection because we're not using the internal header + expect(body).toEqual({ + statusCode: 400, + error: 'Bad Request', + message: expect.stringContaining( + 'method [post] exists but is not available with the current configuration' + ), + }); + expect(status).toBe(400); + + ({ body, status } = await supertest + .post('/internal/security/api_key') + .set(svlCommonApi.getInternalRequestHeader()) + .send(requestBody)); + // expect success because we're using the internal header + expect(body).toEqual(expect.objectContaining({ name: 'create_test' })); + expect(status).toBe(200); + }); + + it('update', async () => { + let body: unknown; + let status: number; + const requestBody = { + id: roleMapping.id, + metadata: { test: 'value' }, + role_descriptors: {}, + }; + + ({ body, status } = await supertest + .put('/internal/security/api_key') + .set(svlCommonApi.getCommonRequestHeader()) + .send(requestBody)); + // expect a rejection because we're not using the internal header + expect(body).toEqual({ + statusCode: 400, + error: 'Bad Request', + message: expect.stringContaining( + 'method [put] exists but is not available with the current configuration' + ), + }); + expect(status).toBe(400); + + ({ body, status } = await supertest + .put('/internal/security/api_key') + .set(svlCommonApi.getInternalRequestHeader()) + .send(requestBody)); + // expect success because we're using the internal header + expect(body).toEqual(expect.objectContaining({ updated: true })); + expect(status).toBe(200); + }); + + it('get all', async () => { + let body: unknown; + let status: number; + + ({ body, status } = await supertest + .get('/internal/security/api_key?isAdmin=true') + .set(svlCommonApi.getCommonRequestHeader())); + // expect a rejection because we're not using the internal header + expect(body).toEqual({ + statusCode: 400, + error: 'Bad Request', + message: expect.stringContaining( + 'method [get] exists but is not available with the current configuration' + ), + }); + expect(status).toBe(400); + + ({ body, status } = await supertest + .get('/internal/security/api_key?isAdmin=true') + .set(svlCommonApi.getInternalRequestHeader())); + // expect success because we're using the internal header + expect(body).toEqual( + expect.objectContaining({ + apiKeys: expect.arrayContaining([expect.objectContaining({ id: roleMapping.id })]), + }) + ); + expect(status).toBe(200); + }); + + it('get enabled', async () => { + let body: unknown; + let status: number; + + ({ body, status } = await supertest + .get('/internal/security/api_key/_enabled') + .set(svlCommonApi.getCommonRequestHeader())); + // expect a rejection because we're not using the internal header + expect(body).toEqual({ + statusCode: 400, + error: 'Bad Request', + message: expect.stringContaining( + 'method [get] exists but is not available with the current configuration' + ), + }); + expect(status).toBe(400); + + ({ body, status } = await supertest + .get('/internal/security/api_key/_enabled') + .set(svlCommonApi.getInternalRequestHeader())); + // expect success because we're using the internal header + expect(body).toEqual({ apiKeysEnabled: true }); + expect(status).toBe(200); + }); + + it('invalidate', async () => { + let body: unknown; + let status: number; + const requestBody = { + apiKeys: [ + { + id: roleMapping.id, + name: roleMapping.name, + }, + ], + isAdmin: true, + }; + + ({ body, status } = await supertest + .post('/internal/security/api_key/invalidate') + .set(svlCommonApi.getCommonRequestHeader()) + .send(requestBody)); + // expect a rejection because we're not using the internal header + expect(body).toEqual({ + statusCode: 400, + error: 'Bad Request', + message: expect.stringContaining( + 'method [post] exists but is not available with the current configuration' + ), + }); + expect(status).toBe(400); + + ({ body, status } = await supertest + .post('/internal/security/api_key/invalidate') + .set(svlCommonApi.getInternalRequestHeader()) + .send(requestBody)); + // expect success because we're using the internal header + expect(body).toEqual({ + errors: [], + itemsInvalidated: [ + { + id: roleMapping.id, + name: roleMapping.name, + }, + ], + }); + expect(status).toBe(200); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/security/authentication.ts b/x-pack/test_serverless/api_integration/test_suites/common/security/authentication.ts new file mode 100644 index 0000000000000..590adbe267b45 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/security/authentication.ts @@ -0,0 +1,201 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from 'expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const svlCommonApi = getService('svlCommonApi'); + const supertest = getService('supertest'); + + describe('security/authentication', function () { + describe('route access', () => { + describe('disabled', () => { + // ToDo: uncomment when we disable login + // it('login', async () => { + // const { body, status } = await supertest + // .post('/internal/security/login') + // .set(svlCommonApi.getInternalRequestHeader()); + // svlCommonApi.assertApiNotFound(body, status); + // }); + + it('logout (deprecated)', async () => { + const { body, status } = await supertest + .get('/api/security/v1/logout') + .set(svlCommonApi.getInternalRequestHeader()); + svlCommonApi.assertApiNotFound(body, status); + }); + + it('get current user (deprecated)', async () => { + const { body, status } = await supertest + .get('/internal/security/v1/me') + .set(svlCommonApi.getInternalRequestHeader()); + svlCommonApi.assertApiNotFound(body, status); + }); + + it('acknowledge access agreement', async () => { + const { body, status } = await supertest + .post('/internal/security/access_agreement/acknowledge') + .set(svlCommonApi.getInternalRequestHeader()); + svlCommonApi.assertApiNotFound(body, status); + }); + + describe('OIDC', () => { + it('OIDC implicit', async () => { + const { body, status } = await supertest + .get('/api/security/oidc/implicit') + .set(svlCommonApi.getInternalRequestHeader()); + svlCommonApi.assertApiNotFound(body, status); + }); + + it('OIDC implicit (deprecated)', async () => { + const { body, status } = await supertest + .get('/api/security/v1/oidc/implicit') + .set(svlCommonApi.getInternalRequestHeader()); + svlCommonApi.assertApiNotFound(body, status); + }); + + it('OIDC implicit.js', async () => { + const { body, status } = await supertest + .get('/internal/security/oidc/implicit.js') + .set(svlCommonApi.getInternalRequestHeader()); + svlCommonApi.assertApiNotFound(body, status); + }); + + it('OIDC callback', async () => { + const { body, status } = await supertest + .get('/api/security/oidc/callback') + .set(svlCommonApi.getInternalRequestHeader()); + svlCommonApi.assertApiNotFound(body, status); + }); + + it('OIDC callback (deprecated)', async () => { + const { body, status } = await supertest + .get('/api/security/v1/oidc') + .set(svlCommonApi.getInternalRequestHeader()); + svlCommonApi.assertApiNotFound(body, status); + }); + + it('OIDC login', async () => { + const { body, status } = await supertest + .post('/api/security/oidc/initiate_login') + .set(svlCommonApi.getInternalRequestHeader()); + svlCommonApi.assertApiNotFound(body, status); + }); + + it('OIDC login (deprecated)', async () => { + const { body, status } = await supertest + .post('/api/security/v1/oidc') + .set(svlCommonApi.getInternalRequestHeader()); + svlCommonApi.assertApiNotFound(body, status); + }); + + it('OIDC 3rd party login', async () => { + const { body, status } = await supertest + .get('/api/security/oidc/initiate_login') + .set(svlCommonApi.getInternalRequestHeader()); + svlCommonApi.assertApiNotFound(body, status); + }); + }); + + it('SAML callback (deprecated)', async () => { + const { body, status } = await supertest + .post('/api/security/v1/saml') + .set(svlCommonApi.getInternalRequestHeader()); + svlCommonApi.assertApiNotFound(body, status); + }); + }); + + describe('internal', () => { + it('get current user', async () => { + let body: any; + let status: number; + + ({ body, status } = await supertest + .get('/internal/security/me') + .set(svlCommonApi.getCommonRequestHeader())); + // expect a rejection because we're not using the internal header + expect(body).toEqual({ + statusCode: 400, + error: 'Bad Request', + message: expect.stringContaining( + 'method [get] exists but is not available with the current configuration' + ), + }); + expect(status).toBe(400); + + ({ body, status } = await supertest + .get('/internal/security/me') + .set(svlCommonApi.getInternalRequestHeader())); + // expect success because we're using the internal header + expect(body).toEqual({ + authentication_provider: { name: '__http__', type: 'http' }, + authentication_realm: { name: 'reserved', type: 'reserved' }, + authentication_type: 'realm', + elastic_cloud_user: false, + email: null, + enabled: true, + full_name: null, + lookup_realm: { name: 'reserved', type: 'reserved' }, + metadata: { _reserved: true }, + roles: ['superuser'], + username: 'elastic', + }); + expect(status).toBe(200); + }); + + // ToDo: remove when we disable login + it('login', async () => { + let body: any; + let status: number; + + ({ body, status } = await supertest + .post('/internal/security/login') + .set(svlCommonApi.getCommonRequestHeader())); + // expect a rejection because we're not using the internal header + expect(body).toEqual({ + statusCode: 400, + error: 'Bad Request', + message: expect.stringContaining( + 'method [post] exists but is not available with the current configuration' + ), + }); + expect(status).toBe(400); + + ({ body, status } = await supertest + .post('/internal/security/login') + .set(svlCommonApi.getInternalRequestHeader())); + expect(status).not.toBe(404); + }); + }); + + describe('public', () => { + it('logout', async () => { + const { status } = await supertest.get('/api/security/logout'); + expect(status).toBe(302); + }); + + it('SAML callback', async () => { + const { body, status } = await supertest + .post('/api/security/saml/callback') + .set(svlCommonApi.getCommonRequestHeader()) + .send({ + SAMLResponse: '', + }); + + // Should fail with 401 (not 404) because there is no valid SAML response in the request body + expect(body).toEqual({ + error: 'Unauthorized', + message: 'Unauthorized', + statusCode: 401, + }); + expect(status).not.toBe(404); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/security/authorization.ts b/x-pack/test_serverless/api_integration/test_suites/common/security/authorization.ts new file mode 100644 index 0000000000000..d2f5db13da9bc --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/security/authorization.ts @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from 'expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const svlCommonApi = getService('svlCommonApi'); + const supertest = getService('supertest'); + + describe('security/authorization', function () { + describe('route access', () => { + describe('disabled', () => { + it('get all privileges', async () => { + const { body, status } = await supertest + .get('/api/security/privileges') + .set(svlCommonApi.getInternalRequestHeader()); + svlCommonApi.assertApiNotFound(body, status); + }); + + it('get built-in elasticsearch privileges', async () => { + const { body, status } = await supertest + .get('/internal/security/esPrivileges/builtin') + .set(svlCommonApi.getInternalRequestHeader()); + svlCommonApi.assertApiNotFound(body, status); + }); + + // ToDo: Uncomment when we disable role APIs + // it('create/update role', async () => { + // const { body, status } = await supertest + // .put('/api/security/role/test') + // .set(svlCommonApi.getInternalRequestHeader()); + // svlCommonApi.assertApiNotFound(body, status); + // }); + + // it('get role', async () => { + // const { body, status } = await supertest + // .get('/api/security/role/superuser') + // .set(svlCommonApi.getInternalRequestHeader()); + // svlCommonApi.assertApiNotFound(body, status); + // }); + + // it('get all roles', async () => { + // const { body, status } = await supertest + // .get('/api/security/role') + // .set(svlCommonApi.getInternalRequestHeader()); + // svlCommonApi.assertApiNotFound(body, status); + // }); + + // it('delete role', async () => { + // const { body, status } = await supertest + // .delete('/api/security/role/superuser') + // .set(svlCommonApi.getInternalRequestHeader()); + // svlCommonApi.assertApiNotFound(body, status); + // }); + + it('get shared saved object permissions', async () => { + const { body, status } = await supertest + .get('/internal/security/_share_saved_object_permissions') + .set(svlCommonApi.getInternalRequestHeader()); + svlCommonApi.assertApiNotFound(body, status); + }); + }); + + // ToDo: remove when we disable role APIs + describe('internal', () => { + it('create/update role', async () => { + const { status } = await supertest + .put('/api/security/role/test') + .set(svlCommonApi.getInternalRequestHeader()); + expect(status).not.toBe(404); + }); + + it('get role', async () => { + const { status } = await supertest + .get('/api/security/role/superuser') + .set(svlCommonApi.getInternalRequestHeader()); + expect(status).not.toBe(404); + }); + + it('get all roles', async () => { + const { status } = await supertest + .get('/api/security/role') + .set(svlCommonApi.getInternalRequestHeader()); + expect(status).not.toBe(404); + }); + + it('delete role', async () => { + const { status } = await supertest + .delete('/api/security/role/superuser') + .set(svlCommonApi.getInternalRequestHeader()); + expect(status).not.toBe(404); + }); + }); + + describe('public', () => { + it('reset session page', async () => { + const { status } = await supertest + .get('/internal/security/reset_session_page.js') + .set(svlCommonApi.getCommonRequestHeader()); + expect(status).toBe(200); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/security/misc.ts b/x-pack/test_serverless/api_integration/test_suites/common/security/misc.ts new file mode 100644 index 0000000000000..2b49b3a96ab20 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/security/misc.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from 'expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const svlCommonApi = getService('svlCommonApi'); + const supertest = getService('supertest'); + + describe('security/misc', function () { + describe('route access', () => { + describe('disabled', () => { + it('get index fields', async () => { + const { body, status } = await supertest + .get('/internal/security/fields/test') + .set(svlCommonApi.getInternalRequestHeader()) + .send({ params: 'params' }); + svlCommonApi.assertApiNotFound(body, status); + }); + + it('fix deprecated roles', async () => { + const { body, status } = await supertest + .post('/internal/security/deprecations/kibana_user_role/_fix_users') + .set(svlCommonApi.getInternalRequestHeader()); + svlCommonApi.assertApiNotFound(body, status); + }); + + it('fix deprecated role mappings', async () => { + const { body, status } = await supertest + .post('/internal/security/deprecations/kibana_user_role/_fix_role_mappings') + .set(svlCommonApi.getInternalRequestHeader()); + svlCommonApi.assertApiNotFound(body, status); + }); + + it('get security checkup state', async () => { + const { body, status } = await supertest + .get('/internal/security/security_checkup/state') + .set(svlCommonApi.getInternalRequestHeader()); + svlCommonApi.assertApiNotFound(body, status); + }); + }); + + describe('internal', () => { + it('get record auth type', async () => { + const { status } = await supertest + .post('/internal/security/analytics/_record_auth_type') + .set(svlCommonApi.getInternalRequestHeader()); + expect(status).toBe(200); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/security_response_headers.ts b/x-pack/test_serverless/api_integration/test_suites/common/security/response_headers.ts similarity index 95% rename from x-pack/test_serverless/api_integration/test_suites/common/security_response_headers.ts rename to x-pack/test_serverless/api_integration/test_suites/common/security/response_headers.ts index 01d1c1b147aa8..56a52914cf9a0 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/security_response_headers.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/security/response_headers.ts @@ -6,13 +6,13 @@ */ import expect from 'expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; +import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const svlCommonApi = getService('svlCommonApi'); const supertest = getService('supertest'); - describe('security response headers', function () { + describe('security/response_headers', function () { const defaultCSP = `script-src 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'; frame-ancestors 'self'`; const defaultCOOP = 'same-origin'; const defaultPermissionsPolicy = diff --git a/x-pack/test_serverless/api_integration/test_suites/common/security/role_mappings.ts b/x-pack/test_serverless/api_integration/test_suites/common/security/role_mappings.ts new file mode 100644 index 0000000000000..4d1f25cfd772f --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/security/role_mappings.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const svlCommonApi = getService('svlCommonApi'); + const supertest = getService('supertest'); + + describe('security/role_mappings', function () { + describe('route access', () => { + describe('disabled', () => { + it('create/update role mapping', async () => { + const { body, status } = await supertest + .post('/internal/security/role_mapping/test') + .set(svlCommonApi.getInternalRequestHeader()); + svlCommonApi.assertApiNotFound(body, status); + }); + + it('get role mapping', async () => { + const { body, status } = await supertest + .get('/internal/security/role_mapping/test') + .set(svlCommonApi.getInternalRequestHeader()); + svlCommonApi.assertApiNotFound(body, status); + }); + + it('get all role mappings', async () => { + const { body, status } = await supertest + .get('/internal/security/role_mapping') + .set(svlCommonApi.getInternalRequestHeader()); + svlCommonApi.assertApiNotFound(body, status); + }); + + it('delete role mapping', async () => { + // this test works because the message for a missing endpoint is different from a missing role mapping + const { body, status } = await supertest + .delete('/internal/security/role_mapping/test') + .set(svlCommonApi.getInternalRequestHeader()); + svlCommonApi.assertApiNotFound(body, status); + }); + + it('role mapping feature check', async () => { + const { body, status } = await supertest + .get('/internal/security/_check_role_mapping_features') + .set(svlCommonApi.getInternalRequestHeader()); + svlCommonApi.assertApiNotFound(body, status); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/security/sessions.ts b/x-pack/test_serverless/api_integration/test_suites/common/security/sessions.ts new file mode 100644 index 0000000000000..2148447fa736f --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/security/sessions.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from 'expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const svlCommonApi = getService('svlCommonApi'); + const supertest = getService('supertest'); + + describe('security/sessions', function () { + describe('route access', () => { + describe('disabled', () => { + it('invalidate', async () => { + const { body, status } = await supertest + .post('/api/security/session/_invalidate') + .set(svlCommonApi.getInternalRequestHeader()) + .send({ match: 'all' }); + svlCommonApi.assertApiNotFound(body, status); + }); + }); + + describe('internal', () => { + it('get session info', async () => { + let body: any; + let status: number; + + ({ body, status } = await supertest + .get('/internal/security/session') + .set(svlCommonApi.getCommonRequestHeader())); + // expect a rejection because we're not using the internal header + expect(body).toEqual({ + statusCode: 400, + error: 'Bad Request', + message: expect.stringContaining( + 'method [get] exists but is not available with the current configuration' + ), + }); + expect(status).toBe(400); + + ({ body, status } = await supertest + .get('/internal/security/session') + .set(svlCommonApi.getInternalRequestHeader())); + // expect 204 because there is no session + expect(status).toBe(204); + }); + + it('extend', async () => { + let body: any; + let status: number; + + ({ body, status } = await supertest + .post('/internal/security/session') + .set(svlCommonApi.getCommonRequestHeader())); + // expect a rejection because we're not using the internal header + expect(body).toEqual({ + statusCode: 400, + error: 'Bad Request', + message: expect.stringContaining( + 'method [post] exists but is not available with the current configuration' + ), + }); + expect(status).toBe(400); + + ({ body, status } = await supertest + .post('/internal/security/session') + .set(svlCommonApi.getInternalRequestHeader())); + // expect redirect + expect(status).toBe(302); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/security/user_profiles.ts b/x-pack/test_serverless/api_integration/test_suites/common/security/user_profiles.ts new file mode 100644 index 0000000000000..79555fd0953f6 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/security/user_profiles.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from 'expect'; +import { kibanaTestUser } from '@kbn/test'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const svlCommonApi = getService('svlCommonApi'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const samlTools = getService('samlTools'); + + describe('security/user_profiles', function () { + describe('route access', () => { + describe('internal', () => { + it('update', async () => { + const { status } = await supertestWithoutAuth + .post(`/internal/security/user_profile/_data`) + .set(svlCommonApi.getInternalRequestHeader()) + .set(await samlTools.login(kibanaTestUser.username)) + .send({ key: 'value' }); + expect(status).not.toBe(404); + }); + + it('get current', async () => { + const { status } = await supertestWithoutAuth + .get(`/internal/security/user_profile`) + .set(svlCommonApi.getInternalRequestHeader()) + .set(await samlTools.login(kibanaTestUser.username)); + expect(status).not.toBe(404); + }); + + it('bulk get', async () => { + const { status } = await supertestWithoutAuth + .get(`/internal/security/user_profile`) + .set(svlCommonApi.getInternalRequestHeader()) + .set(await samlTools.login(kibanaTestUser.username)) + .send({ uids: ['12345678'] }); + expect(status).not.toBe(404); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/security/users.ts b/x-pack/test_serverless/api_integration/test_suites/common/security/users.ts new file mode 100644 index 0000000000000..2feaa8878d1ef --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/security/users.ts @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from 'expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const svlCommonApi = getService('svlCommonApi'); + const supertest = getService('supertest'); + + describe('security/users', function () { + describe('route access', () => { + // ToDo: uncomment when we disable user APIs + // describe('disabled', () => { + // it('get', async () => { + // const { body, status } = await supertest + // .get('/internal/security/users/elastic') + // .set(svlCommonApi.getInternalRequestHeader()); + // svlCommonApi.assertApiNotFound(body, status); + // }); + + // it('get all', async () => { + // const { body, status } = await supertest + // .get('/internal/security/users') + // .set(svlCommonApi.getInternalRequestHeader()); + // svlCommonApi.assertApiNotFound(body, status); + // }); + + // it('create/update', async () => { + // const { body, status } = await supertest + // .post(`/internal/security/users/some_testuser`) + // .set(svlCommonApi.getInternalRequestHeader()) + // .send({ username: 'some_testuser', password: 'testpassword', roles: [] }); + // svlCommonApi.assertApiNotFound(body, status); + // }); + + // it('delete', async () => { + // const { body, status } = await supertest + // .delete(`/internal/security/users/elastic`) + // .set(svlCommonApi.getInternalRequestHeader()); + // svlCommonApi.assertApiNotFound(body, status); + // }); + + // it('disable', async () => { + // const { body, status } = await supertest + // .post(`/internal/security/users/elastic/_disable`) + // .set(svlCommonApi.getInternalRequestHeader()); + // svlCommonApi.assertApiNotFound(body, status); + // }); + + // it('enable', async () => { + // const { body, status } = await supertest + // .post(`/internal/security/users/elastic/_enable`) + // .set(svlCommonApi.getInternalRequestHeader()); + // svlCommonApi.assertApiNotFound(body, status); + // }); + + // it('set password', async () => { + // const { body, status } = await supertest + // .post(`/internal/security/users/{username}/password`) + // .set(svlCommonApi.getInternalRequestHeader()) + // .send({ + // password: 'old_pw', + // newPassword: 'new_pw', + // }); + // svlCommonApi.assertApiNotFound(body, status); + // }); + // }); + + // ToDo: remove when we disable user APIs + describe('internal', () => { + it('get', async () => { + const { status } = await supertest + .get('/internal/security/users/elastic') + .set(svlCommonApi.getInternalRequestHeader()); + expect(status).not.toBe(404); + }); + + it('get all', async () => { + const { status } = await supertest + .get('/internal/security/users') + .set(svlCommonApi.getInternalRequestHeader()); + expect(status).not.toBe(404); + }); + + it('create/update', async () => { + const { status } = await supertest + .post(`/internal/security/users/some_testuser`) + .set(svlCommonApi.getInternalRequestHeader()) + .send({ username: 'some_testuser', password: 'testpassword', roles: [] }); + expect(status).not.toBe(404); + }); + + it('delete', async () => { + const { status } = await supertest + .delete(`/internal/security/users/elastic`) + .set(svlCommonApi.getInternalRequestHeader()); + expect(status).not.toBe(404); + }); + + it('disable', async () => { + const { status } = await supertest + .post(`/internal/security/users/elastic/_disable`) + .set(svlCommonApi.getInternalRequestHeader()); + expect(status).not.toBe(404); + }); + + it('enable', async () => { + const { status } = await supertest + .post(`/internal/security/users/elastic/_enable`) + .set(svlCommonApi.getInternalRequestHeader()); + expect(status).not.toBe(404); + }); + + it('set password', async () => { + const { status } = await supertest + .post(`/internal/security/users/{username}/password`) + .set(svlCommonApi.getInternalRequestHeader()) + .send({ + password: 'old_pw', + newPassword: 'new_pw', + }); + expect(status).not.toBe(404); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/security/views.ts b/x-pack/test_serverless/api_integration/test_suites/common/security/views.ts new file mode 100644 index 0000000000000..ac1d994252c57 --- /dev/null +++ b/x-pack/test_serverless/api_integration/test_suites/common/security/views.ts @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from 'expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const svlCommonApi = getService('svlCommonApi'); + const supertest = getService('supertest'); + + describe('security/views', function () { + describe('route access', () => { + describe('disabled', () => { + // ToDo: uncomment these when we disable login routes + // it('login', async () => { + // const { body, status } = await supertest + // .get('/login') + // .set(svlCommonApi.getInternalRequestHeader()); + // svlCommonApi.assertApiNotFound(body, status); + // }); + + // it('get login state', async () => { + // const { body, status } = await supertest + // .get('/internal/security/login_state') + // .set(svlCommonApi.getInternalRequestHeader()); + // svlCommonApi.assertApiNotFound(body, status); + // }); + + it('access agreement', async () => { + const { body, status } = await supertest + .get('/security/access_agreement') + .set(svlCommonApi.getInternalRequestHeader()); + svlCommonApi.assertApiNotFound(body, status); + }); + + it('get access agreement state', async () => { + const { body, status } = await supertest + .get('/internal/security/access_agreement/state') + .set(svlCommonApi.getInternalRequestHeader()); + svlCommonApi.assertApiNotFound(body, status); + }); + }); + + describe('public', () => { + it('login', async () => { + const { status } = await supertest + .get('/login') + .set(svlCommonApi.getInternalRequestHeader()); + expect(status).toBe(302); + }); + + it('get login state', async () => { + const { status } = await supertest + .get('/internal/security/login_state') + .set(svlCommonApi.getInternalRequestHeader()); + expect(status).toBe(200); + }); + + it('capture URL', async () => { + const { status } = await supertest + .get('/internal/security/capture-url') + .set(svlCommonApi.getCommonRequestHeader()); + expect(status).toBe(200); + }); + + it('space selector', async () => { + const { status } = await supertest + .get('/spaces/space_selector') + .set(svlCommonApi.getCommonRequestHeader()); + expect(status).toBe(200); + }); + + it('enter space', async () => { + const { status } = await supertest + .get('/spaces/enter') + .set(svlCommonApi.getCommonRequestHeader()); + expect(status).toBe(302); + }); + + it('account', async () => { + const { status } = await supertest + .get('/security/account') + .set(svlCommonApi.getCommonRequestHeader()); + expect(status).toBe(200); + }); + + it('logged out', async () => { + const { status } = await supertest + .get('/security/logged_out') + .set(svlCommonApi.getCommonRequestHeader()); + expect(status).toBe(200); + }); + + it('logout', async () => { + const { status } = await supertest + .get('/logout') + .set(svlCommonApi.getCommonRequestHeader()); + expect(status).toBe(200); + }); + + it('overwritten session', async () => { + const { status } = await supertest + .get('/security/overwritten_session') + .set(svlCommonApi.getCommonRequestHeader()); + expect(status).toBe(200); + }); + }); + }); + }); +} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/security_users.ts b/x-pack/test_serverless/api_integration/test_suites/common/security_users.ts deleted file mode 100644 index 2c82e216505b9..0000000000000 --- a/x-pack/test_serverless/api_integration/test_suites/common/security_users.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ getService }: FtrProviderContext) { - const svlCommonApi = getService('svlCommonApi'); - const supertest = getService('supertest'); - - // Test should be unskipped when the API is disabled - // https://github.com/elastic/kibana/issues/161337 - describe.skip('security/users', function () { - it('rejects request to create user', async () => { - const { body, status } = await supertest - .post(`/internal/security/users/some_testuser`) - .set(svlCommonApi.getInternalRequestHeader()) - .send({ username: 'some_testuser', password: 'testpassword', roles: [] }); - - // in a non-serverless environment this would succeed with a 200 - svlCommonApi.assertResponseStatusCode(400, status, body); - }); - }); -} diff --git a/x-pack/test_serverless/api_integration/test_suites/common/spaces.ts b/x-pack/test_serverless/api_integration/test_suites/common/spaces.ts index fcb3dfe84fd17..78c2456f85ca1 100644 --- a/x-pack/test_serverless/api_integration/test_suites/common/spaces.ts +++ b/x-pack/test_serverless/api_integration/test_suites/common/spaces.ts @@ -13,44 +13,200 @@ export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); describe('spaces', function () { - it('rejects request to create a space', async () => { - const { body, status } = await supertest - .post('/api/spaces/space') - .set(svlCommonApi.getInternalRequestHeader()) - .send({ - id: 'custom', - name: 'Custom', - disabledFeatures: [], - }); - - // in a non-serverless environment this would succeed with a 200 - expect(body).toEqual({ - statusCode: 400, - error: 'Bad Request', - message: - 'Unable to create Space, this exceeds the maximum number of spaces set by the xpack.spaces.maxSpaces setting', + describe('route access', () => { + describe('disabled', () => { + it('#delete', async () => { + const { body, status } = await supertest + .delete('/api/spaces/space/default') + .set(svlCommonApi.getCommonRequestHeader()); + + // normally we'd get a 400 bad request if we tried to delete the default space + svlCommonApi.assertApiNotFound(body, status); + }); + + it('#create', async () => { + const { body, status } = await supertest + .post('/api/spaces/space') + .set(svlCommonApi.getCommonRequestHeader()) + .send({ + id: 'custom', + name: 'Custom', + disabledFeatures: [], + }); + + svlCommonApi.assertApiNotFound(body, status); + }); + + it('#update requires internal header', async () => { + const { body, status } = await supertest + .put('/api/spaces/space/default') + .set(svlCommonApi.getCommonRequestHeader()) + .send({ + id: 'default', + name: 'UPDATED!', + disabledFeatures: [], + }); + + svlCommonApi.assertApiNotFound(body, status); + }); + + it('#copyToSpace', async () => { + const { body, status } = await supertest + .post('/api/spaces/_copy_saved_objects') + .set(svlCommonApi.getCommonRequestHeader()); + + // without a request body we would normally a 400 bad request if the endpoint was registered + svlCommonApi.assertApiNotFound(body, status); + }); + + it('#resolveCopyToSpaceErrors', async () => { + const { body, status } = await supertest + .post('/api/spaces/_resolve_copy_saved_objects_errors') + .set(svlCommonApi.getCommonRequestHeader()); + + // without a request body we would normally a 400 bad request if the endpoint was registered + svlCommonApi.assertApiNotFound(body, status); + }); + + it('#updateObjectsSpaces', async () => { + const { body, status } = await supertest + .post('/api/spaces/_update_objects_spaces') + .set(svlCommonApi.getCommonRequestHeader()); + + // without a request body we would normally a 400 bad request if the endpoint was registered + svlCommonApi.assertApiNotFound(body, status); + }); + + it('#getShareableReferences', async () => { + const { body, status } = await supertest + .post('/api/spaces/_get_shareable_references') + .set(svlCommonApi.getCommonRequestHeader()); + + // without a request body we would normally a 400 bad request if the endpoint was registered + svlCommonApi.assertApiNotFound(body, status); + }); + + it('#disableLegacyUrlAliases', async () => { + const { body, status } = await supertest + .post('/api/spaces/_disable_legacy_url_aliases') + .set(svlCommonApi.getCommonRequestHeader()); + + // without a request body we would normally a 400 bad request if the endpoint was registered + svlCommonApi.assertApiNotFound(body, status); + }); }); - expect(status).toBe(400); - }); - it('rejects request to update a space with disabledFeatures', async () => { - const { body, status } = await supertest - .put('/api/spaces/space/default') - .set(svlCommonApi.getInternalRequestHeader()) - .send({ - id: 'custom', - name: 'Custom', - disabledFeatures: ['some-feature'], - }); - - // in a non-serverless environment this would succeed with a 200 - expect(body).toEqual({ - statusCode: 400, - error: 'Bad Request', - message: - 'Unable to update Space, the disabledFeatures array must be empty when xpack.spaces.allowFeatureVisibility setting is disabled', + describe('internal', () => { + it('#get requires internal header', async () => { + let body: any; + let status: number; + + ({ body, status } = await supertest + .get('/api/spaces/space/default') + .set(svlCommonApi.getCommonRequestHeader())); + // expect a rejection because we're not using the internal header + expect(body).toEqual({ + statusCode: 400, + error: 'Bad Request', + message: expect.stringContaining( + 'method [get] exists but is not available with the current configuration' + ), + }); + expect(status).toBe(400); + + ({ body, status } = await supertest + .get('/api/spaces/space/default') + .set(svlCommonApi.getInternalRequestHeader())); + // expect success because we're using the internal header + expect(body).toEqual( + expect.objectContaining({ + id: 'default', + }) + ); + expect(status).toBe(200); + }); + + it('#getAll requires internal header', async () => { + let body: any; + let status: number; + + ({ body, status } = await supertest + .get('/api/spaces/space') + .set(svlCommonApi.getCommonRequestHeader())); + // expect a rejection because we're not using the internal header + expect(body).toEqual({ + statusCode: 400, + error: 'Bad Request', + message: expect.stringContaining( + 'method [get] exists but is not available with the current configuration' + ), + }); + expect(status).toBe(400); + + ({ body, status } = await supertest + .get('/api/spaces/space') + .set(svlCommonApi.getInternalRequestHeader())); + // expect success because we're using the internal header + expect(body).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 'default', + }), + ]) + ); + expect(status).toBe(200); + }); + + it('#getActiveSpace requires internal header', async () => { + let body: any; + let status: number; + + ({ body, status } = await supertest + .get('/internal/spaces/_active_space') + .set(svlCommonApi.getCommonRequestHeader())); + // expect a rejection because we're not using the internal header + expect(body).toEqual({ + statusCode: 400, + error: 'Bad Request', + message: expect.stringContaining( + 'method [get] exists but is not available with the current configuration' + ), + }); + expect(status).toBe(400); + + ({ body, status } = await supertest + .get('/internal/spaces/_active_space') + .set(svlCommonApi.getInternalRequestHeader())); + // expect success because we're using the internal header + expect(body).toEqual( + expect.objectContaining({ + id: 'default', + }) + ); + expect(status).toBe(200); + }); }); - expect(status).toBe(400); }); + + // TODO: Re-enable test-suite once users can create and update spaces in the Serverless offering. + // it('rejects request to update a space with disabledFeatures', async () => { + // const { body, status } = await supertest + // .put('/api/spaces/space/default') + // .set(svlCommonApi.getInternalRequestHeader()) + // .send({ + // id: 'custom', + // name: 'Custom', + // disabledFeatures: ['some-feature'], + // }); + // + // // in a non-serverless environment this would succeed with a 200 + // expect(body).toEqual({ + // statusCode: 400, + // error: 'Bad Request', + // message: + // 'Unable to update Space, the disabledFeatures array must be empty when xpack.spaces.allowFeatureVisibility setting is disabled', + // }); + // expect(status).toBe(400); + // }); }); } diff --git a/x-pack/test_serverless/functional/test_suites/security/cypress/tasks/login.ts b/x-pack/test_serverless/functional/test_suites/security/cypress/tasks/login.ts index 7b9d571d8c7c9..a8cd016ed6159 100644 --- a/x-pack/test_serverless/functional/test_suites/security/cypress/tasks/login.ts +++ b/x-pack/test_serverless/functional/test_suites/security/cypress/tasks/login.ts @@ -6,7 +6,6 @@ */ import { request } from '@kbn/security-solution-plugin/public/management/cypress/tasks/common'; -import { isLocalhost } from '@kbn/security-solution-plugin/scripts/endpoint/common/is_localhost'; import type { ServerlessRoleName } from '../../../../../shared/lib'; import { STANDARD_HTTP_HEADERS } from '../../../../../shared/lib/security/default_http_headers'; @@ -32,7 +31,7 @@ const sendApiLoginRequest = ( url: url.toString(), body: { providerType: 'basic', - providerName: isLocalhost(url.hostname) ? 'basic' : 'cloud-basic', + providerName: 'basic', currentURL: '/', params: { username, diff --git a/x-pack/test_serverless/shared/config.base.ts b/x-pack/test_serverless/shared/config.base.ts index 3cd2f21310809..d9d34a1e14198 100644 --- a/x-pack/test_serverless/shared/config.base.ts +++ b/x-pack/test_serverless/shared/config.base.ts @@ -18,12 +18,32 @@ export default async () => { elasticsearch: esTestConfig.getUrlParts(), }; + // "Fake" SAML provider + const idpPath = resolve( + __dirname, + '../../test/security_api_integration/plugins/saml_provider/metadata.xml' + ); + const samlIdPPlugin = resolve( + __dirname, + '../../test/security_api_integration/plugins/saml_provider' + ); + return { servers, esTestCluster: { license: 'trial', from: 'snapshot', + serverArgs: [ + 'xpack.security.authc.token.enabled=true', + 'xpack.security.authc.realms.saml.cloud-saml-kibana.order=0', + `xpack.security.authc.realms.saml.cloud-saml-kibana.idp.metadata.path=${idpPath}`, + 'xpack.security.authc.realms.saml.cloud-saml-kibana.idp.entity_id=http://www.elastic.co/saml1', + `xpack.security.authc.realms.saml.cloud-saml-kibana.sp.entity_id=http://localhost:${servers.kibana.port}`, + `xpack.security.authc.realms.saml.cloud-saml-kibana.sp.logout=http://localhost:${servers.kibana.port}/logout`, + `xpack.security.authc.realms.saml.cloud-saml-kibana.sp.acs=http://localhost:${servers.kibana.port}/api/security/saml/callback`, + 'xpack.security.authc.realms.saml.cloud-saml-kibana.attributes.principal=urn:oid:0.0.7', + ], }, kbnTestServer: { @@ -34,7 +54,7 @@ export default async () => { sourceArgs: ['--no-base-path', '--env.name=development'], serverArgs: [ `--server.restrictInternalApis=true`, - `--server.port=${kbnTestConfig.getPort()}`, + `--server.port=${servers.kibana.port}`, '--status.allowAnonymous=true', // We shouldn't embed credentials into the URL since Kibana requests to Elasticsearch should // either include `kibanaServerTestUser` credentials, or credentials provided by the test @@ -60,6 +80,16 @@ export default async () => { appenders: ['deprecation'], }, ])}`, + // This ensures that we register the Security SAML API endpoints. + // In the real world the SAML config is injected by control plane. + // basic: { 'basic': { order: 0 } }, + `--plugin-path=${samlIdPPlugin}`, + '--xpack.cloud.id=ftr_fake_cloud_id', + '--xpack.security.authc.selector.enabled=false', + `--xpack.security.authc.providers=${JSON.stringify({ + basic: { basic: { order: 0 } }, + saml: { 'cloud-saml-kibana': { order: 1, realm: 'cloud-saml-kibana' } }, + })}`, '--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"', `--server.publicBaseUrl=${servers.kibana.protocol}://${servers.kibana.hostname}:${servers.kibana.port}`, ], diff --git a/x-pack/test_serverless/tsconfig.json b/x-pack/test_serverless/tsconfig.json index eb69a37884039..b986a6134525b 100644 --- a/x-pack/test_serverless/tsconfig.json +++ b/x-pack/test_serverless/tsconfig.json @@ -47,5 +47,6 @@ "@kbn/core-http-common", "@kbn/data-views-plugin", "@kbn/core-saved-objects-server", + "@kbn/security-api-integration-helpers", ] }