diff --git a/config/serverless.yml b/config/serverless.yml index 3b0587d45cae4..e18049e8517c6 100644 --- a/config/serverless.yml +++ b/config/serverless.yml @@ -50,6 +50,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 c2d4fd7002637..a9acd07e3aa26 100644 --- a/x-pack/test_serverless/shared/config.base.ts +++ b/x-pack/test_serverless/shared/config.base.ts @@ -23,6 +23,16 @@ 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, @@ -31,6 +41,15 @@ export default async () => { serverArgs: [ // HTTP SSL requires setup for Kibana to trust ESS certs, disable for now 'xpack.security.http.ssl.enabled=false', + + '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', ], }, @@ -42,7 +61,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 @@ -67,6 +86,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", ] }