Prepare the Security domain HTTP APIs for Serverless (#162087)

Closes #161337

## Summary

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

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

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

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

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Aleh Zasypkin <aleh.zasypkin@gmail.com>
Co-authored-by: Aleh Zasypkin <aleh.zasypkin@elastic.co>
This commit is contained in:
Jeramy Soucy 2023-08-23 06:34:45 -04:00 committed by GitHub
parent f4f286f965
commit fe0ffab1da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 1578 additions and 183 deletions

View file

@ -48,6 +48,9 @@ xpack.cloud_integrations.data_migration.enabled: false
data.search.sessions.enabled: false
advanced_settings.enabled: false
# Disable the browser-side functionality that depends on SecurityCheckupGetStateRoutes
xpack.security.showInsecureClusterWarning: false
# Disable UI of security management plugins
xpack.security.ui.userManagementEnabled: false
xpack.security.ui.roleManagementEnabled: false

View file

@ -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,

View file

@ -359,6 +359,7 @@ export class SecurityPlugin
getAnonymousAccessService: this.getAnonymousAccess,
getUserProfileService: this.getUserProfileService,
analyticsService: this.analyticsService.setup({ analytics: core.analytics }),
buildFlavor: this.initializerContext.env.packageInfo.buildFlavor,
});
return Object.freeze<SecurityPluginSetup>({

View file

@ -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();
})
);
}
}

View file

@ -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,

View file

@ -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);
}
}

View file

@ -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
}
}

View file

@ -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);
}
}

View file

@ -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",
]
`);
});

View file

@ -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);
}

View file

@ -103,7 +103,7 @@ export class SpacesPlugin
private defaultSpaceService?: DefaultSpaceService;
constructor(initializerContext: PluginInitializerContext) {
constructor(private readonly initializerContext: PluginInitializerContext) {
this.config$ = initializerContext.config.create<ConfigType>();
this.log = initializerContext.logger.get();
this.spacesService = new SpacesService();
@ -148,18 +148,21 @@ export class SpacesPlugin
logger: this.log,
});
const externalRouter = core.http.createRouter<SpacesRequestHandlerContext>();
initExternalSpacesApi({
externalRouter,
log: this.log,
getStartServices: core.getStartServices,
getSpacesService,
usageStatsServicePromise,
});
const router = core.http.createRouter<SpacesRequestHandlerContext>();
initExternalSpacesApi(
{
router,
log: this.log,
getStartServices: core.getStartServices,
getSpacesService,
usageStatsServicePromise,
},
this.initializerContext.env.packageInfo.buildFlavor
);
const internalRouter = core.http.createRouter<SpacesRequestHandlerContext>();
initInternalSpacesApi({
internalRouter,
router,
getSpacesService,
});

View file

@ -80,7 +80,7 @@ describe('copy to space', () => {
});
initCopyToSpacesApi({
externalRouter: router,
router,
getStartServices: async () => [coreStart, {}, {}],
log,
getSpacesService: () => spacesServiceStart,

View file

@ -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: {

View file

@ -62,7 +62,7 @@ describe('Spaces Public API', () => {
});
initDeleteSpacesApi({
externalRouter: router,
router,
getStartServices: async () => [coreStart, {}, {}],
log,
getSpacesService: () => spacesServiceStart,

View file

@ -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: {

View file

@ -65,7 +65,7 @@ describe('_disable_legacy_url_aliases', () => {
});
initDisableLegacyUrlAliasesApi({
externalRouter: router,
router,
getStartServices: async () => [coreStart, {}, {}],
log,
getSpacesService: () => spacesServiceStart,

View file

@ -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: {

View file

@ -61,7 +61,7 @@ describe('GET space', () => {
});
initGetSpaceApi({
externalRouter: router,
router,
getStartServices: async () => [coreStart, {}, {}],
log,
getSpacesService: () => spacesServiceStart,

View file

@ -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: {

View file

@ -62,7 +62,7 @@ describe('GET /spaces/space', () => {
});
initGetAllSpacesApi({
externalRouter: router,
router,
getStartServices: async () => [coreStart, {}, {}],
log,
getSpacesService: () => spacesServiceStart,

View file

@ -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: {

View file

@ -61,7 +61,7 @@ describe('get shareable references', () => {
spacesClientService: clientServiceStart,
});
initGetShareableReferencesApi({
externalRouter: router,
router,
getStartServices: async () => [coreStart, {}, {}],
log,
getSpacesService: () => spacesServiceStart,

View file

@ -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: {

View file

@ -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<UsageStatsServiceSetup>;
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);
}
}

View file

@ -62,7 +62,7 @@ describe('Spaces Public API', () => {
});
initPostSpacesApi({
externalRouter: router,
router,
getStartServices: async () => [coreStart, {}, {}],
log,
getSpacesService: () => spacesServiceStart,

View file

@ -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: {

View file

@ -62,7 +62,7 @@ describe('PUT /api/spaces/space', () => {
});
initPutSpacesApi({
externalRouter: router,
router,
getStartServices: async () => [coreStart, {}, {}],
log,
getSpacesService: () => spacesServiceStart,

View file

@ -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: {

View file

@ -62,7 +62,7 @@ describe('update_objects_spaces', () => {
spacesClientService: clientServiceStart,
});
initUpdateObjectsSpacesApi({
externalRouter: router,
router,
getStartServices: async () => [coreStart, {}, {}],
log,
getSpacesService: () => spacesServiceStart,

View file

@ -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: {

View file

@ -26,7 +26,7 @@ describe('GET /internal/spaces/_active_space', () => {
});
initGetActiveSpaceApi({
internalRouter: router,
router,
getSpacesService: () =>
service.start({
basePath: coreStart.http.basePath,

View file

@ -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,

View file

@ -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;
}

View file

@ -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, []);

View file

@ -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/**/*",

View file

@ -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,
};

View file

@ -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() };
},
};
}

View file

@ -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);
},
};
}

View file

@ -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);
});
});
});
});
}

View file

@ -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'));

View file

@ -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);
});
});
});
});
}

View file

@ -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);
});
});
});
});
}

View file

@ -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);
});
});
});
});
}

View file

@ -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);
});
});
});
});
}

View file

@ -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);
});
});
});
});
}

View file

@ -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 =

View file

@ -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);
});
});
});
});
}

View file

@ -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);
});
});
});
});
}

View file

@ -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);
});
});
});
});
}

View file

@ -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);
});
});
});
});
}

View file

@ -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);
});
});
});
});
}

View file

@ -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);
});
});
}

View file

@ -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: [],
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);
});
// 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',
});
expect(status).toBe(400);
});
it('#create', async () => {
const { body, status } = await supertest
.post('/api/spaces/space')
.set(svlCommonApi.getCommonRequestHeader())
.send({
id: 'custom',
name: 'Custom',
disabledFeatures: [],
});
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'],
svlCommonApi.assertApiNotFound(body, status);
});
// 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',
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);
});
});
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);
// });
});
}

View file

@ -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,

View file

@ -18,12 +18,32 @@ export default async () => {
elasticsearch: esTestConfig.getUrlParts(),
};
// "Fake" SAML provider
const idpPath = resolve(
__dirname,
'../../test/security_api_integration/plugins/saml_provider/metadata.xml'
);
const samlIdPPlugin = resolve(
__dirname,
'../../test/security_api_integration/plugins/saml_provider'
);
return {
servers,
esTestCluster: {
license: 'trial',
from: 'snapshot',
serverArgs: [
'xpack.security.authc.token.enabled=true',
'xpack.security.authc.realms.saml.cloud-saml-kibana.order=0',
`xpack.security.authc.realms.saml.cloud-saml-kibana.idp.metadata.path=${idpPath}`,
'xpack.security.authc.realms.saml.cloud-saml-kibana.idp.entity_id=http://www.elastic.co/saml1',
`xpack.security.authc.realms.saml.cloud-saml-kibana.sp.entity_id=http://localhost:${servers.kibana.port}`,
`xpack.security.authc.realms.saml.cloud-saml-kibana.sp.logout=http://localhost:${servers.kibana.port}/logout`,
`xpack.security.authc.realms.saml.cloud-saml-kibana.sp.acs=http://localhost:${servers.kibana.port}/api/security/saml/callback`,
'xpack.security.authc.realms.saml.cloud-saml-kibana.attributes.principal=urn:oid:0.0.7',
],
},
kbnTestServer: {
@ -34,7 +54,7 @@ export default async () => {
sourceArgs: ['--no-base-path', '--env.name=development'],
serverArgs: [
`--server.restrictInternalApis=true`,
`--server.port=${kbnTestConfig.getPort()}`,
`--server.port=${servers.kibana.port}`,
'--status.allowAnonymous=true',
// We shouldn't embed credentials into the URL since Kibana requests to Elasticsearch should
// either include `kibanaServerTestUser` credentials, or credentials provided by the test
@ -60,6 +80,16 @@ export default async () => {
appenders: ['deprecation'],
},
])}`,
// This ensures that we register the Security SAML API endpoints.
// In the real world the SAML config is injected by control plane.
// basic: { 'basic': { order: 0 } },
`--plugin-path=${samlIdPPlugin}`,
'--xpack.cloud.id=ftr_fake_cloud_id',
'--xpack.security.authc.selector.enabled=false',
`--xpack.security.authc.providers=${JSON.stringify({
basic: { basic: { order: 0 } },
saml: { 'cloud-saml-kibana': { order: 1, realm: 'cloud-saml-kibana' } },
})}`,
'--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"',
`--server.publicBaseUrl=${servers.kibana.protocol}://${servers.kibana.hostname}:${servers.kibana.port}`,
],

View file

@ -47,5 +47,6 @@
"@kbn/core-http-common",
"@kbn/data-views-plugin",
"@kbn/core-saved-objects-server",
"@kbn/security-api-integration-helpers",
]
}