Migrate API keys functionality to a new Elasticsearch client. (#85029)

This commit is contained in:
Aleh Zasypkin 2020-12-09 20:43:24 +01:00 committed by GitHub
parent 8b5c68ab63
commit 88e61a6651
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
58 changed files with 1225 additions and 1158 deletions

View file

@ -33,6 +33,7 @@ const savedObjectsService = savedObjectsServiceMock.createInternalStartContract(
const features = featuresPluginMock.createStart();
const securityPluginSetup = securityMock.createSetup();
const securityPluginStart = securityMock.createStart();
const alertsClientFactoryParams: jest.Mocked<AlertsClientFactoryOpts> = {
logger: loggingSystemMock.create().get(),
taskManager: taskManagerMock.createStart(),
@ -77,7 +78,7 @@ beforeEach(() => {
test('creates an alerts client with proper constructor arguments when security is enabled', async () => {
const factory = new AlertsClientFactory();
factory.initialize({ securityPluginSetup, ...alertsClientFactoryParams });
factory.initialize({ securityPluginSetup, securityPluginStart, ...alertsClientFactoryParams });
const request = KibanaRequest.from(fakeRequest);
const { AlertsAuthorizationAuditLogger } = jest.requireMock('./authorization/audit_logger');
@ -98,7 +99,7 @@ test('creates an alerts client with proper constructor arguments when security i
const { AlertsAuthorization } = jest.requireMock('./authorization/alerts_authorization');
expect(AlertsAuthorization).toHaveBeenCalledWith({
request,
authorization: securityPluginSetup.authz,
authorization: securityPluginStart.authz,
alertTypeRegistry: alertsClientFactoryParams.alertTypeRegistry,
features: alertsClientFactoryParams.features,
auditLogger: expect.any(AlertsAuthorizationAuditLogger),
@ -188,11 +189,12 @@ test('getUserName() returns a name when security is enabled', async () => {
factory.initialize({
...alertsClientFactoryParams,
securityPluginSetup,
securityPluginStart,
});
factory.create(KibanaRequest.from(fakeRequest), savedObjectsService);
const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0];
securityPluginSetup.authc.getCurrentUser.mockReturnValueOnce(({
securityPluginStart.authc.getCurrentUser.mockReturnValueOnce(({
username: 'bob',
} as unknown) as AuthenticatedUser);
const userNameResult = await constructorCall.getUserName();
@ -225,7 +227,7 @@ test('createAPIKey() returns { apiKeysEnabled: false } when security is enabled
factory.create(KibanaRequest.from(fakeRequest), savedObjectsService);
const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0];
securityPluginSetup.authc.grantAPIKeyAsInternalUser.mockResolvedValueOnce(null);
securityPluginStart.authc.apiKeys.grantAsInternalUser.mockResolvedValueOnce(null);
const createAPIKeyResult = await constructorCall.createAPIKey();
expect(createAPIKeyResult).toEqual({ apiKeysEnabled: false });
});
@ -235,11 +237,12 @@ test('createAPIKey() returns an API key when security is enabled', async () => {
factory.initialize({
...alertsClientFactoryParams,
securityPluginSetup,
securityPluginStart,
});
factory.create(KibanaRequest.from(fakeRequest), savedObjectsService);
const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0];
securityPluginSetup.authc.grantAPIKeyAsInternalUser.mockResolvedValueOnce({
securityPluginStart.authc.apiKeys.grantAsInternalUser.mockResolvedValueOnce({
api_key: '123',
id: 'abc',
name: '',
@ -256,11 +259,12 @@ test('createAPIKey() throws when security plugin createAPIKey throws an error',
factory.initialize({
...alertsClientFactoryParams,
securityPluginSetup,
securityPluginStart,
});
factory.create(KibanaRequest.from(fakeRequest), savedObjectsService);
const constructorCall = jest.requireMock('./alerts_client').AlertsClient.mock.calls[0][0];
securityPluginSetup.authc.grantAPIKeyAsInternalUser.mockRejectedValueOnce(
securityPluginStart.authc.apiKeys.grantAsInternalUser.mockRejectedValueOnce(
new Error('TLS disabled')
);
await expect(constructorCall.createAPIKey()).rejects.toThrowErrorMatchingInlineSnapshot(

View file

@ -14,7 +14,7 @@ import { PluginStartContract as ActionsPluginStartContract } from '../../actions
import { AlertsClient } from './alerts_client';
import { ALERTS_FEATURE_ID } from '../common';
import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from './types';
import { SecurityPluginSetup } from '../../security/server';
import { SecurityPluginSetup, SecurityPluginStart } from '../../security/server';
import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/server';
import { TaskManagerStartContract } from '../../task_manager/server';
import { PluginStartContract as FeaturesPluginStart } from '../../features/server';
@ -28,6 +28,7 @@ export interface AlertsClientFactoryOpts {
taskManager: TaskManagerStartContract;
alertTypeRegistry: AlertTypeRegistry;
securityPluginSetup?: SecurityPluginSetup;
securityPluginStart?: SecurityPluginStart;
getSpaceId: (request: KibanaRequest) => string | undefined;
getSpace: (request: KibanaRequest) => Promise<Space | undefined>;
spaceIdToNamespace: SpaceIdToNamespaceFunction;
@ -44,6 +45,7 @@ export class AlertsClientFactory {
private taskManager!: TaskManagerStartContract;
private alertTypeRegistry!: AlertTypeRegistry;
private securityPluginSetup?: SecurityPluginSetup;
private securityPluginStart?: SecurityPluginStart;
private getSpaceId!: (request: KibanaRequest) => string | undefined;
private getSpace!: (request: KibanaRequest) => Promise<Space | undefined>;
private spaceIdToNamespace!: SpaceIdToNamespaceFunction;
@ -64,6 +66,7 @@ export class AlertsClientFactory {
this.taskManager = options.taskManager;
this.alertTypeRegistry = options.alertTypeRegistry;
this.securityPluginSetup = options.securityPluginSetup;
this.securityPluginStart = options.securityPluginStart;
this.spaceIdToNamespace = options.spaceIdToNamespace;
this.encryptedSavedObjectsClient = options.encryptedSavedObjectsClient;
this.actions = options.actions;
@ -73,10 +76,10 @@ export class AlertsClientFactory {
}
public create(request: KibanaRequest, savedObjects: SavedObjectsServiceStart): AlertsClient {
const { securityPluginSetup, actions, eventLog, features } = this;
const { securityPluginSetup, securityPluginStart, actions, eventLog, features } = this;
const spaceId = this.getSpaceId(request);
const authorization = new AlertsAuthorization({
authorization: securityPluginSetup?.authz,
authorization: securityPluginStart?.authz,
request,
getSpace: this.getSpace,
alertTypeRegistry: this.alertTypeRegistry,
@ -102,25 +105,22 @@ export class AlertsClientFactory {
encryptedSavedObjectsClient: this.encryptedSavedObjectsClient,
auditLogger: securityPluginSetup?.audit.asScoped(request),
async getUserName() {
if (!securityPluginSetup) {
if (!securityPluginStart) {
return null;
}
const user = await securityPluginSetup.authc.getCurrentUser(request);
const user = await securityPluginStart.authc.getCurrentUser(request);
return user ? user.username : null;
},
async createAPIKey(name: string) {
if (!securityPluginSetup) {
if (!securityPluginStart) {
return { apiKeysEnabled: false };
}
// Create an API key using the new grant API - in this case the Kibana system user is creating the
// API key for the user, instead of having the user create it themselves, which requires api_key
// privileges
const createAPIKeyResult = await securityPluginSetup.authc.grantAPIKeyAsInternalUser(
const createAPIKeyResult = await securityPluginStart.authc.apiKeys.grantAsInternalUser(
request,
{
name,
role_descriptors: {},
}
{ name, role_descriptors: {} }
);
if (!createAPIKeyResult) {
return { apiKeysEnabled: false };

View file

@ -12,7 +12,7 @@ import {
SavedObjectsClientContract,
} from 'kibana/server';
import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server';
import { InvalidateAPIKeyParams, SecurityPluginSetup } from '../../../security/server';
import { InvalidateAPIKeyParams, SecurityPluginStart } from '../../../security/server';
import {
RunContext,
TaskManagerSetupContract,
@ -29,12 +29,12 @@ export const TASK_ID = `Alerts-${TASK_TYPE}`;
const invalidateAPIKey = async (
params: InvalidateAPIKeyParams,
securityPluginSetup?: SecurityPluginSetup
securityPluginStart?: SecurityPluginStart
): Promise<InvalidateAPIKeyResult> => {
if (!securityPluginSetup) {
if (!securityPluginStart) {
return { apiKeysEnabled: false };
}
const invalidateAPIKeyResult = await securityPluginSetup.authc.invalidateAPIKeyAsInternalUser(
const invalidateAPIKeyResult = await securityPluginStart.authc.apiKeys.invalidateAsInternalUser(
params
);
// Null when Elasticsearch security is disabled
@ -51,16 +51,9 @@ export function initializeApiKeyInvalidator(
logger: Logger,
coreStartServices: Promise<[CoreStart, AlertingPluginsStart, unknown]>,
taskManager: TaskManagerSetupContract,
config: Promise<AlertsConfig>,
securityPluginSetup?: SecurityPluginSetup
config: Promise<AlertsConfig>
) {
registerApiKeyInvalitorTaskDefinition(
logger,
coreStartServices,
taskManager,
config,
securityPluginSetup
);
registerApiKeyInvalidatorTaskDefinition(logger, coreStartServices, taskManager, config);
}
export async function scheduleApiKeyInvalidatorTask(
@ -84,17 +77,16 @@ export async function scheduleApiKeyInvalidatorTask(
}
}
function registerApiKeyInvalitorTaskDefinition(
function registerApiKeyInvalidatorTaskDefinition(
logger: Logger,
coreStartServices: Promise<[CoreStart, AlertingPluginsStart, unknown]>,
taskManager: TaskManagerSetupContract,
config: Promise<AlertsConfig>,
securityPluginSetup?: SecurityPluginSetup
config: Promise<AlertsConfig>
) {
taskManager.registerTaskDefinitions({
[TASK_TYPE]: {
title: 'Invalidate alert API Keys',
createTaskRunner: taskRunner(logger, coreStartServices, config, securityPluginSetup),
createTaskRunner: taskRunner(logger, coreStartServices, config),
},
});
}
@ -120,8 +112,7 @@ function getFakeKibanaRequest(basePath: string) {
function taskRunner(
logger: Logger,
coreStartServices: Promise<[CoreStart, AlertingPluginsStart, unknown]>,
config: Promise<AlertsConfig>,
securityPluginSetup?: SecurityPluginSetup
config: Promise<AlertsConfig>
) {
return ({ taskInstance }: RunContext) => {
const { state } = taskInstance;
@ -130,7 +121,10 @@ function taskRunner(
let totalInvalidated = 0;
const configResult = await config;
try {
const [{ savedObjects, http }, { encryptedSavedObjects }] = await coreStartServices;
const [
{ savedObjects, http },
{ encryptedSavedObjects, security },
] = await coreStartServices;
const savedObjectsClient = savedObjects.getScopedClient(
getFakeKibanaRequest(http.basePath.serverBasePath),
{
@ -160,7 +154,7 @@ function taskRunner(
savedObjectsClient,
apiKeysToInvalidate,
encryptedSavedObjectsClient,
securityPluginSetup
security
);
hasApiKeysPendingInvalidation = apiKeysToInvalidate.total > PAGE_SIZE;
@ -197,7 +191,7 @@ async function invalidateApiKeys(
savedObjectsClient: SavedObjectsClientContract,
apiKeysToInvalidate: SavedObjectsFindResponse<InvalidatePendingApiKey>,
encryptedSavedObjectsClient: EncryptedSavedObjectsClient,
securityPluginSetup?: SecurityPluginSetup
securityPluginStart?: SecurityPluginStart
) {
let totalInvalidated = 0;
await Promise.all(
@ -207,7 +201,7 @@ async function invalidateApiKeys(
apiKeyObj.id
);
const apiKeyId = decryptedApiKey.attributes.apiKeyId;
const response = await invalidateAPIKey({ id: apiKeyId }, securityPluginSetup);
const response = await invalidateAPIKey({ id: apiKeyId }, securityPluginStart);
if (response.apiKeysEnabled === true && response.result.error_count > 0) {
logger.error(`Failed to invalidate API Key [id="${apiKeyObj.attributes.apiKeyId}"]`);
} else {

View file

@ -8,7 +8,7 @@ import { first, map } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { combineLatest } from 'rxjs';
import { SecurityPluginSetup } from '../../security/server';
import { SecurityPluginSetup, SecurityPluginStart } from '../../security/server';
import {
EncryptedSavedObjectsPluginSetup,
EncryptedSavedObjectsPluginStart,
@ -115,6 +115,7 @@ export interface AlertingPluginsStart {
features: FeaturesPluginStart;
eventLog: IEventLogClientService;
spaces?: SpacesPluginStart;
security?: SecurityPluginStart;
}
export class AlertingPlugin {
@ -203,8 +204,7 @@ export class AlertingPlugin {
this.logger,
core.getStartServices(),
plugins.taskManager,
this.config,
this.security
this.config
);
core.getStartServices().then(async ([, startPlugins]) => {
@ -279,6 +279,7 @@ export class AlertingPlugin {
logger,
taskManager: plugins.taskManager,
securityPluginSetup: security,
securityPluginStart: plugins.security,
encryptedSavedObjectsClient,
spaceIdToNamespace,
getSpaceId(request: KibanaRequest) {

View file

@ -12,6 +12,7 @@ import {
KibanaResponseFactory,
} from 'src/core/server';
import { errors as LegacyESErrors } from 'elasticsearch';
import { ResponseError } from '@elastic/elasticsearch/lib/errors';
import { appContextService } from '../services';
import {
IngestManagerError,
@ -51,6 +52,10 @@ export const isLegacyESClientError = (error: any): error is LegacyESClientError
return error instanceof LegacyESErrors._Abstract;
};
export function isESClientError(error: unknown): error is ResponseError {
return error instanceof ResponseError;
}
const getHTTPResponseCode = (error: IngestManagerError): number => {
if (error instanceof RegistryError) {
return 502; // Bad Gateway

View file

@ -9,6 +9,7 @@ export {
defaultIngestErrorHandler,
ingestErrorToResponseOptions,
isLegacyESClientError,
isESClientError,
} from './handlers';
export class IngestManagerError extends Error {

View file

@ -15,7 +15,7 @@ export const createAppContextStartContractMock = (): FleetAppContext => {
return {
encryptedSavedObjectsStart: encryptedSavedObjectsMock.createStart(),
savedObjects: savedObjectsServiceMock.createStartContract(),
security: securityMock.createSetup(),
security: securityMock.createStart(),
logger: loggingSystemMock.create().get(),
isProductionMode: true,
kibanaVersion: '8.0.0',

View file

@ -24,7 +24,7 @@ import {
EncryptedSavedObjectsPluginStart,
EncryptedSavedObjectsPluginSetup,
} from '../../encrypted_saved_objects/server';
import { SecurityPluginSetup } from '../../security/server';
import { SecurityPluginSetup, SecurityPluginStart } from '../../security/server';
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
import {
PLUGIN_ID,
@ -85,12 +85,15 @@ export interface FleetSetupDeps {
usageCollection?: UsageCollectionSetup;
}
export type FleetStartDeps = object;
export interface FleetStartDeps {
encryptedSavedObjects: EncryptedSavedObjectsPluginStart;
security?: SecurityPluginStart;
}
export interface FleetAppContext {
encryptedSavedObjectsStart: EncryptedSavedObjectsPluginStart;
encryptedSavedObjectsSetup?: EncryptedSavedObjectsPluginSetup;
security?: SecurityPluginSetup;
security?: SecurityPluginStart;
config$?: Observable<FleetConfigType>;
savedObjects: SavedObjectsServiceStart;
isProductionMode: PluginInitializerContext['env']['mode']['prod'];
@ -150,7 +153,6 @@ export class FleetPlugin
implements Plugin<FleetSetupContract, FleetStartContract, FleetSetupDeps, FleetStartDeps> {
private licensing$!: Observable<ILicense>;
private config$: Observable<FleetConfigType>;
private security: SecurityPluginSetup | undefined;
private cloud: CloudSetup | undefined;
private logger: Logger | undefined;
@ -171,9 +173,6 @@ export class FleetPlugin
public async setup(core: CoreSetup, deps: FleetSetupDeps) {
this.httpSetup = core.http;
this.licensing$ = deps.licensing.license$;
if (deps.security) {
this.security = deps.security;
}
this.encryptedSavedObjectsSetup = deps.encryptedSavedObjects;
this.cloud = deps.cloud;
@ -226,7 +225,7 @@ export class FleetPlugin
// For all the routes we enforce the user to have role superuser
const routerSuperuserOnly = makeRouterEnforcingSuperuser(router);
// Register rest of routes only if security is enabled
if (this.security) {
if (deps.security) {
registerSetupRoutes(routerSuperuserOnly, config);
registerAgentPolicyRoutes(routerSuperuserOnly);
registerPackagePolicyRoutes(routerSuperuserOnly);
@ -262,16 +261,11 @@ export class FleetPlugin
}
}
public async start(
core: CoreStart,
plugins: {
encryptedSavedObjects: EncryptedSavedObjectsPluginStart;
}
): Promise<FleetStartContract> {
public async start(core: CoreStart, plugins: FleetStartDeps): Promise<FleetStartContract> {
await appContextService.start({
encryptedSavedObjectsStart: plugins.encryptedSavedObjects,
encryptedSavedObjectsSetup: this.encryptedSavedObjectsSetup,
security: this.security,
security: plugins.security,
config$: this.config$,
savedObjects: core.savedObjects,
isProductionMode: this.isProductionMode,

View file

@ -15,7 +15,9 @@ export const getFleetStatusHandler: RequestHandler = async (context, request, re
const soClient = context.core.savedObjects.client;
try {
const isAdminUserSetup = (await outputService.getAdminUser(soClient)) !== null;
const isApiKeysEnabled = await appContextService.getSecurity().authc.areAPIKeysEnabled();
const isApiKeysEnabled = await appContextService
.getSecurity()
.authc.apiKeys.areAPIKeysEnabled();
const isTLSEnabled = appContextService.getHttpSetup().getServerInfo().protocol === 'https';
const isProductionMode = appContextService.getIsProductionMode();
const isCloud = appContextService.getCloud()?.isCloudEnabled ?? false;

View file

@ -6,7 +6,7 @@
import type { Request } from '@hapi/hapi';
import { KibanaRequest, SavedObjectsClientContract } from '../../../../../../src/core/server';
import { FleetAdminUserInvalidError, isLegacyESClientError } from '../../errors';
import { FleetAdminUserInvalidError, isESClientError } from '../../errors';
import { CallESAsCurrentUser } from '../../types';
import { appContextService } from '../app_context';
import { outputService } from '../output';
@ -37,14 +37,14 @@ export async function createAPIKey(
}
try {
const key = await security.authc.createAPIKey(request, {
const key = await security.authc.apiKeys.create(request, {
name,
role_descriptors: roleDescriptors,
});
return key;
} catch (err) {
if (isLegacyESClientError(err) && err.statusCode === 401) {
if (isESClientError(err) && err.statusCode === 401) {
// Clear Fleet admin user cache as the user is probably not valid anymore
outputService.invalidateCache();
throw new FleetAdminUserInvalidError(`Fleet Admin user is invalid: ${err.message}`);
@ -87,13 +87,13 @@ export async function invalidateAPIKey(soClient: SavedObjectsClientContract, id:
}
try {
const res = await security.authc.invalidateAPIKey(request, {
const res = await security.authc.apiKeys.invalidate(request, {
id,
});
return res;
} catch (err) {
if (isLegacyESClientError(err) && err.statusCode === 401) {
if (isESClientError(err) && err.statusCode === 401) {
// Clear Fleet admin user cache as the user is probably not valid anymore
outputService.invalidateCache();
throw new FleetAdminUserInvalidError(`Fleet Admin user is invalid: ${err.message}`);

View file

@ -11,7 +11,7 @@ import {
EncryptedSavedObjectsPluginSetup,
} from '../../../encrypted_saved_objects/server';
import packageJSON from '../../../../../package.json';
import { SecurityPluginSetup } from '../../../security/server';
import { SecurityPluginStart } from '../../../security/server';
import { FleetConfigType } from '../../common';
import { ExternalCallback, ExternalCallbacksStorage, FleetAppContext } from '../plugin';
import { CloudSetup } from '../../../cloud/server';
@ -19,7 +19,7 @@ import { CloudSetup } from '../../../cloud/server';
class AppContextService {
private encryptedSavedObjects: EncryptedSavedObjectsClient | undefined;
private encryptedSavedObjectsSetup: EncryptedSavedObjectsPluginSetup | undefined;
private security: SecurityPluginSetup | undefined;
private security: SecurityPluginStart | undefined;
private config$?: Observable<FleetConfigType>;
private configSubject$?: BehaviorSubject<FleetConfigType>;
private savedObjects: SavedObjectsServiceStart | undefined;

View file

@ -23,13 +23,13 @@ describe('get_space_id', () => {
});
test('it returns "default" as the space id given a space id of "default"', () => {
const spaces = spacesServiceMock.createSetupContract();
const spaces = spacesServiceMock.createStartContract();
const space = getSpaceId({ request, spaces });
expect(space).toEqual('default');
});
test('it returns "another-space" as the space id given a space id of "another-space"', () => {
const spaces = spacesServiceMock.createSetupContract('another-space');
const spaces = spacesServiceMock.createStartContract('another-space');
const space = getSpaceId({ request, spaces });
expect(space).toEqual('another-space');
});

View file

@ -6,12 +6,12 @@
import { KibanaRequest } from 'kibana/server';
import { SpacesServiceSetup } from '../../spaces/server';
import { SpacesServiceStart } from '../../spaces/server';
export const getSpaceId = ({
spaces,
request,
}: {
spaces: SpacesServiceSetup | undefined | null;
spaces: SpacesServiceStart | undefined | null;
request: KibanaRequest;
}): string => spaces?.getSpaceId(request) ?? 'default';

View file

@ -23,42 +23,42 @@ describe('get_user', () => {
});
test('it returns "bob" as the user given a security request with "bob"', () => {
const security = securityMock.createSetup();
const security = securityMock.createStart();
security.authc.getCurrentUser = jest.fn().mockReturnValue({ username: 'bob' });
const user = getUser({ request, security });
expect(user).toEqual('bob');
});
test('it returns "alice" as the user given a security request with "alice"', () => {
const security = securityMock.createSetup();
const security = securityMock.createStart();
security.authc.getCurrentUser = jest.fn().mockReturnValue({ username: 'alice' });
const user = getUser({ request, security });
expect(user).toEqual('alice');
});
test('it returns "elastic" as the user given null as the current user', () => {
const security = securityMock.createSetup();
const security = securityMock.createStart();
security.authc.getCurrentUser = jest.fn().mockReturnValue(null);
const user = getUser({ request, security });
expect(user).toEqual('elastic');
});
test('it returns "elastic" as the user given undefined as the current user', () => {
const security = securityMock.createSetup();
const security = securityMock.createStart();
security.authc.getCurrentUser = jest.fn().mockReturnValue(undefined);
const user = getUser({ request, security });
expect(user).toEqual('elastic');
});
test('it returns "elastic" as the user given undefined as the plugin', () => {
const security = securityMock.createSetup();
const security = securityMock.createStart();
security.authc.getCurrentUser = jest.fn().mockReturnValue(undefined);
const user = getUser({ request, security: undefined });
expect(user).toEqual('elastic');
});
test('it returns "elastic" as the user given null as the plugin', () => {
const security = securityMock.createSetup();
const security = securityMock.createStart();
security.authc.getCurrentUser = jest.fn().mockReturnValue(undefined);
const user = getUser({ request, security: null });
expect(user).toEqual('elastic');

View file

@ -6,10 +6,10 @@
import { KibanaRequest } from 'kibana/server';
import { SecurityPluginSetup } from '../../security/server';
import { SecurityPluginStart } from '../../security/server';
export interface GetUserOptions {
security: SecurityPluginSetup | null | undefined;
security: SecurityPluginStart | null | undefined;
request: KibanaRequest;
}

View file

@ -6,20 +6,20 @@
import { first } from 'rxjs/operators';
import { Logger, Plugin, PluginInitializerContext } from 'kibana/server';
import { CoreSetup } from 'src/core/server';
import type { CoreSetup, CoreStart } from 'src/core/server';
import { SecurityPluginSetup } from '../../security/server';
import { SpacesServiceSetup } from '../../spaces/server';
import type { SecurityPluginStart } from '../../security/server';
import type { SpacesServiceStart } from '../../spaces/server';
import { ConfigType } from './config';
import { initRoutes } from './routes/init_routes';
import { ListClient } from './services/lists/list_client';
import {
import type {
ContextProvider,
ContextProviderReturn,
ListPluginSetup,
ListsPluginStart,
PluginsSetup,
PluginsStart,
} from './types';
import { createConfig$ } from './create_config';
import { getSpaceId } from './get_space_id';
@ -28,27 +28,25 @@ import { initSavedObjects } from './saved_objects';
import { ExceptionListClient } from './services/exception_lists/exception_list_client';
export class ListPlugin
implements Plugin<Promise<ListPluginSetup>, ListsPluginStart, PluginsSetup> {
implements Plugin<Promise<ListPluginSetup>, ListsPluginStart, {}, PluginsStart> {
private readonly logger: Logger;
private spaces: SpacesServiceSetup | undefined | null;
private spaces: SpacesServiceStart | undefined | null;
private config: ConfigType | undefined | null;
private security: SecurityPluginSetup | undefined | null;
private security: SecurityPluginStart | undefined | null;
constructor(private readonly initializerContext: PluginInitializerContext) {
this.logger = this.initializerContext.logger.get();
}
public async setup(core: CoreSetup, plugins: PluginsSetup): Promise<ListPluginSetup> {
public async setup(core: CoreSetup): Promise<ListPluginSetup> {
const config = await createConfig$(this.initializerContext).pipe(first()).toPromise();
this.spaces = plugins.spaces?.spacesService;
this.config = config;
this.security = plugins.security;
initSavedObjects(core.savedObjects);
core.http.registerRouteHandlerContext('lists', this.createRouteHandlerContext());
const router = core.http.createRouter();
initRoutes(router, config, plugins.security);
initRoutes(router, config);
return {
getExceptionListClient: (savedObjectsClient, user): ExceptionListClient => {
@ -68,8 +66,10 @@ export class ListPlugin
};
}
public start(): void {
public start(core: CoreStart, plugins: PluginsStart): void {
this.logger.debug('Starting plugin');
this.security = plugins.security;
this.spaces = plugins.spaces?.spacesService;
}
public stop(): void {

View file

@ -6,7 +6,6 @@
import { IRouter } from 'kibana/server';
import { SecurityPluginSetup } from '../../../security/server';
import { ConfigType } from '../config';
import {
@ -46,11 +45,7 @@ import {
updateListRoute,
} from '.';
export const initRoutes = (
router: IRouter,
config: ConfigType,
security: SecurityPluginSetup | null | undefined
): void => {
export const initRoutes = (router: IRouter, config: ConfigType): void => {
// lists
createListRoute(router);
readListRoute(router);
@ -58,7 +53,7 @@ export const initRoutes = (
deleteListRoute(router);
patchListRoute(router);
findListRoute(router);
readPrivilegesRoute(router, security);
readPrivilegesRoute(router);
// list items
createListItemRoute(router);

View file

@ -7,16 +7,12 @@
import { IRouter } from 'kibana/server';
import { merge } from 'lodash/fp';
import { SecurityPluginSetup } from '../../../security/server';
import { LIST_PRIVILEGES_URL } from '../../common/constants';
import { buildSiemResponse, readPrivileges, transformError } from '../siem_server_deps';
import { getListClient } from './utils';
export const readPrivilegesRoute = (
router: IRouter,
security: SecurityPluginSetup | null | undefined
): void => {
export const readPrivilegesRoute = (router: IRouter): void => {
router.get(
{
options: {
@ -44,7 +40,7 @@ export const readPrivilegesRoute = (
lists: clusterPrivilegesLists,
},
{
is_authenticated: security?.authc.isAuthenticated(request) ?? false,
is_authenticated: request.auth.isAuthenticated ?? false,
}
);
return response.ok({ body: privileges });

View file

@ -11,17 +11,17 @@ import {
SavedObjectsClientContract,
} from 'kibana/server';
import { SecurityPluginSetup } from '../../security/server';
import { SpacesPluginSetup } from '../../spaces/server';
import type { SecurityPluginStart } from '../../security/server';
import type { SpacesPluginStart } from '../../spaces/server';
import { ListClient } from './services/lists/list_client';
import { ExceptionListClient } from './services/exception_lists/exception_list_client';
export type ContextProvider = IContextProvider<RequestHandler<unknown, unknown, unknown>, 'lists'>;
export type ListsPluginStart = void;
export interface PluginsSetup {
security: SecurityPluginSetup | undefined | null;
spaces: SpacesPluginSetup | undefined | null;
export interface PluginsStart {
security: SecurityPluginStart | undefined | null;
spaces: SpacesPluginStart | undefined | null;
}
export type GetListClientType = (

View file

@ -0,0 +1,18 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import type { PublicMethodsOf } from '@kbn/utility-types';
import type { APIKeys } from '.';
export const apiKeysMock = {
create: (): jest.Mocked<PublicMethodsOf<APIKeys>> => ({
areAPIKeysEnabled: jest.fn(),
create: jest.fn(),
grantAsInternalUser: jest.fn(),
invalidate: jest.fn(),
invalidateAsInternalUser: jest.fn(),
}),
};

View file

@ -4,31 +4,31 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { ILegacyClusterClient, ILegacyScopedClusterClient } from '../../../../../src/core/server';
import { SecurityLicense } from '../../common/licensing';
import type { SecurityLicense } from '../../../common/licensing';
import { APIKeys } from './api_keys';
import {
httpServerMock,
loggingSystemMock,
elasticsearchServiceMock,
} from '../../../../../src/core/server/mocks';
import { licenseMock } from '../../common/licensing/index.mock';
} from '../../../../../../src/core/server/mocks';
import { licenseMock } from '../../../common/licensing/index.mock';
import { securityMock } from '../../mocks';
const encodeToBase64 = (str: string) => Buffer.from(str).toString('base64');
describe('API Keys', () => {
let apiKeys: APIKeys;
let mockClusterClient: jest.Mocked<ILegacyClusterClient>;
let mockScopedClusterClient: jest.Mocked<ILegacyScopedClusterClient>;
let mockClusterClient: ReturnType<typeof elasticsearchServiceMock.createClusterClient>;
let mockScopedClusterClient: ReturnType<
typeof elasticsearchServiceMock.createScopedClusterClient
>;
let mockLicense: jest.Mocked<SecurityLicense>;
beforeEach(() => {
mockClusterClient = elasticsearchServiceMock.createLegacyClusterClient();
mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient();
mockClusterClient.asScoped.mockReturnValue(
(mockScopedClusterClient as unknown) as jest.Mocked<ILegacyScopedClusterClient>
);
mockClusterClient = elasticsearchServiceMock.createClusterClient();
mockScopedClusterClient = elasticsearchServiceMock.createScopedClusterClient();
mockClusterClient.asScoped.mockReturnValue(mockScopedClusterClient);
mockLicense = licenseMock.create();
mockLicense.isEnabled.mockReturnValue(true);
@ -46,9 +46,10 @@ describe('API Keys', () => {
const result = await apiKeys.areAPIKeysEnabled();
expect(result).toEqual(false);
expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled();
expect(mockScopedClusterClient.callAsInternalUser).not.toHaveBeenCalled();
expect(mockClusterClient.callAsInternalUser).not.toHaveBeenCalled();
expect(mockClusterClient.asInternalUser.security.invalidateApiKey).not.toHaveBeenCalled();
expect(
mockScopedClusterClient.asCurrentUser.security.invalidateApiKey
).not.toHaveBeenCalled();
});
it('returns false when the exception metadata indicates api keys are disabled', async () => {
@ -57,17 +58,19 @@ describe('API Keys', () => {
(error as any).body = {
error: { 'disabled.feature': 'api_keys' },
};
mockClusterClient.callAsInternalUser.mockRejectedValue(error);
mockClusterClient.asInternalUser.security.invalidateApiKey.mockRejectedValue(error);
const result = await apiKeys.areAPIKeysEnabled();
expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1);
expect(mockClusterClient.asInternalUser.security.invalidateApiKey).toHaveBeenCalledTimes(1);
expect(result).toEqual(false);
});
it('returns true when the operation completes without error', async () => {
mockLicense.isEnabled.mockReturnValue(true);
mockClusterClient.callAsInternalUser.mockResolvedValue({});
mockClusterClient.asInternalUser.security.invalidateApiKey.mockResolvedValue(
securityMock.createApiResponse({ body: {} })
);
const result = await apiKeys.areAPIKeysEnabled();
expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1);
expect(mockClusterClient.asInternalUser.security.invalidateApiKey).toHaveBeenCalledTimes(1);
expect(result).toEqual(true);
});
@ -78,9 +81,9 @@ describe('API Keys', () => {
error: { 'disabled.feature': 'something_else' },
};
mockClusterClient.callAsInternalUser.mockRejectedValue(error);
expect(apiKeys.areAPIKeysEnabled()).rejects.toThrowError(error);
expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1);
mockClusterClient.asInternalUser.security.invalidateApiKey.mockRejectedValue(error);
await expect(apiKeys.areAPIKeysEnabled()).rejects.toThrowError(error);
expect(mockClusterClient.asInternalUser.security.invalidateApiKey).toHaveBeenCalledTimes(1);
});
it('throws the original error when exception metadata does not contain `disabled.feature`', async () => {
@ -88,27 +91,29 @@ describe('API Keys', () => {
const error = new Error();
(error as any).body = {};
mockClusterClient.callAsInternalUser.mockRejectedValue(error);
expect(apiKeys.areAPIKeysEnabled()).rejects.toThrowError(error);
expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1);
mockClusterClient.asInternalUser.security.invalidateApiKey.mockRejectedValue(error);
await expect(apiKeys.areAPIKeysEnabled()).rejects.toThrowError(error);
expect(mockClusterClient.asInternalUser.security.invalidateApiKey).toHaveBeenCalledTimes(1);
});
it('throws the original error when exception contains no metadata', async () => {
mockLicense.isEnabled.mockReturnValue(true);
const error = new Error();
mockClusterClient.callAsInternalUser.mockRejectedValue(error);
expect(apiKeys.areAPIKeysEnabled()).rejects.toThrowError(error);
expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledTimes(1);
mockClusterClient.asInternalUser.security.invalidateApiKey.mockRejectedValue(error);
await expect(apiKeys.areAPIKeysEnabled()).rejects.toThrowError(error);
expect(mockClusterClient.asInternalUser.security.invalidateApiKey).toHaveBeenCalledTimes(1);
});
it('calls callCluster with proper parameters', async () => {
it('calls `invalidateApiKey` with proper parameters', async () => {
mockLicense.isEnabled.mockReturnValue(true);
mockClusterClient.callAsInternalUser.mockResolvedValueOnce({});
mockClusterClient.asInternalUser.security.invalidateApiKey.mockResolvedValueOnce(
securityMock.createApiResponse({ body: {} })
);
const result = await apiKeys.areAPIKeysEnabled();
expect(result).toEqual(true);
expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('shield.invalidateAPIKey', {
expect(mockClusterClient.asInternalUser.security.invalidateApiKey).toHaveBeenCalledWith({
body: {
id: 'kibana-api-key-service-test',
},
@ -124,17 +129,22 @@ describe('API Keys', () => {
role_descriptors: {},
});
expect(result).toBeNull();
expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled();
expect(mockScopedClusterClient.asCurrentUser.security.createApiKey).not.toHaveBeenCalled();
});
it('calls callCluster with proper parameters', async () => {
it('calls `createApiKey` with proper parameters', async () => {
mockLicense.isEnabled.mockReturnValue(true);
mockScopedClusterClient.callAsCurrentUser.mockResolvedValueOnce({
id: '123',
name: 'key-name',
expiration: '1d',
api_key: 'abc123',
});
mockScopedClusterClient.asCurrentUser.security.createApiKey.mockResolvedValueOnce(
securityMock.createApiResponse({
body: {
id: '123',
name: 'key-name',
expiration: '1d',
api_key: 'abc123',
},
})
);
const result = await apiKeys.create(httpServerMock.createKibanaRequest(), {
name: 'key-name',
role_descriptors: { foo: true },
@ -146,16 +156,13 @@ describe('API Keys', () => {
id: '123',
name: 'key-name',
});
expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith(
'shield.createAPIKey',
{
body: {
name: 'key-name',
role_descriptors: { foo: true },
expiration: '1d',
},
}
);
expect(mockScopedClusterClient.asCurrentUser.security.createApiKey).toHaveBeenCalledWith({
body: {
name: 'key-name',
role_descriptors: { foo: true },
expiration: '1d',
},
});
});
});
@ -168,17 +175,21 @@ describe('API Keys', () => {
});
expect(result).toBeNull();
expect(mockClusterClient.callAsInternalUser).not.toHaveBeenCalled();
expect(mockClusterClient.asInternalUser.security.grantApiKey).not.toHaveBeenCalled();
});
it('calls callAsInternalUser with proper parameters for the Basic scheme', async () => {
it('calls `grantApiKey` with proper parameters for the Basic scheme', async () => {
mockLicense.isEnabled.mockReturnValue(true);
mockClusterClient.callAsInternalUser.mockResolvedValueOnce({
id: '123',
name: 'key-name',
api_key: 'abc123',
expires: '1d',
});
mockClusterClient.asInternalUser.security.grantApiKey.mockResolvedValueOnce(
securityMock.createApiResponse({
body: {
id: '123',
name: 'key-name',
api_key: 'abc123',
expires: '1d',
},
})
);
const result = await apiKeys.grantAsInternalUser(
httpServerMock.createKibanaRequest({
headers: {
@ -197,7 +208,7 @@ describe('API Keys', () => {
name: 'key-name',
expires: '1d',
});
expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('shield.grantAPIKey', {
expect(mockClusterClient.asInternalUser.security.grantApiKey).toHaveBeenCalledWith({
body: {
api_key: {
name: 'test_api_key',
@ -211,13 +222,17 @@ describe('API Keys', () => {
});
});
it('calls callAsInternalUser with proper parameters for the Bearer scheme', async () => {
it('calls `grantApiKey` with proper parameters for the Bearer scheme', async () => {
mockLicense.isEnabled.mockReturnValue(true);
mockClusterClient.callAsInternalUser.mockResolvedValueOnce({
id: '123',
name: 'key-name',
api_key: 'abc123',
});
mockClusterClient.asInternalUser.security.grantApiKey.mockResolvedValueOnce(
securityMock.createApiResponse({
body: {
id: '123',
name: 'key-name',
api_key: 'abc123',
},
})
);
const result = await apiKeys.grantAsInternalUser(
httpServerMock.createKibanaRequest({
headers: {
@ -235,7 +250,7 @@ describe('API Keys', () => {
id: '123',
name: 'key-name',
});
expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('shield.grantAPIKey', {
expect(mockClusterClient.asInternalUser.security.grantApiKey).toHaveBeenCalledWith({
body: {
api_key: {
name: 'test_api_key',
@ -266,7 +281,7 @@ describe('API Keys', () => {
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Unsupported scheme \\"Digest\\" for granting API Key"`
);
expect(mockClusterClient.callAsInternalUser).not.toHaveBeenCalled();
expect(mockClusterClient.asInternalUser.security.grantApiKey).not.toHaveBeenCalled();
});
});
@ -277,16 +292,22 @@ describe('API Keys', () => {
id: '123',
});
expect(result).toBeNull();
expect(mockScopedClusterClient.callAsCurrentUser).not.toHaveBeenCalled();
expect(
mockScopedClusterClient.asCurrentUser.security.invalidateApiKey
).not.toHaveBeenCalled();
});
it('calls callCluster with proper parameters', async () => {
mockLicense.isEnabled.mockReturnValue(true);
mockScopedClusterClient.callAsCurrentUser.mockResolvedValueOnce({
invalidated_api_keys: ['api-key-id-1'],
previously_invalidated_api_keys: [],
error_count: 0,
});
mockScopedClusterClient.asCurrentUser.security.invalidateApiKey.mockResolvedValueOnce(
securityMock.createApiResponse({
body: {
invalidated_api_keys: ['api-key-id-1'],
previously_invalidated_api_keys: [],
error_count: 0,
},
})
);
const result = await apiKeys.invalidate(httpServerMock.createKibanaRequest(), {
id: '123',
});
@ -295,23 +316,24 @@ describe('API Keys', () => {
previously_invalidated_api_keys: [],
error_count: 0,
});
expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith(
'shield.invalidateAPIKey',
{
body: {
id: '123',
},
}
);
expect(mockScopedClusterClient.asCurrentUser.security.invalidateApiKey).toHaveBeenCalledWith({
body: {
id: '123',
},
});
});
it(`Only passes id as a parameter`, async () => {
mockLicense.isEnabled.mockReturnValue(true);
mockScopedClusterClient.callAsCurrentUser.mockResolvedValueOnce({
invalidated_api_keys: ['api-key-id-1'],
previously_invalidated_api_keys: [],
error_count: 0,
});
mockScopedClusterClient.asCurrentUser.security.invalidateApiKey.mockResolvedValueOnce(
securityMock.createApiResponse({
body: {
invalidated_api_keys: ['api-key-id-1'],
previously_invalidated_api_keys: [],
error_count: 0,
},
})
);
const result = await apiKeys.invalidate(httpServerMock.createKibanaRequest(), {
id: '123',
name: 'abc',
@ -321,14 +343,11 @@ describe('API Keys', () => {
previously_invalidated_api_keys: [],
error_count: 0,
});
expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith(
'shield.invalidateAPIKey',
{
body: {
id: '123',
},
}
);
expect(mockScopedClusterClient.asCurrentUser.security.invalidateApiKey).toHaveBeenCalledWith({
body: {
id: '123',
},
});
});
});
@ -337,23 +356,27 @@ describe('API Keys', () => {
mockLicense.isEnabled.mockReturnValue(false);
const result = await apiKeys.invalidateAsInternalUser({ id: '123' });
expect(result).toBeNull();
expect(mockClusterClient.callAsInternalUser).not.toHaveBeenCalled();
expect(mockClusterClient.asInternalUser.security.invalidateApiKey).not.toHaveBeenCalled();
});
it('calls callCluster with proper parameters', async () => {
mockLicense.isEnabled.mockReturnValue(true);
mockClusterClient.callAsInternalUser.mockResolvedValueOnce({
invalidated_api_keys: ['api-key-id-1'],
previously_invalidated_api_keys: [],
error_count: 0,
});
mockClusterClient.asInternalUser.security.invalidateApiKey.mockResolvedValueOnce(
securityMock.createApiResponse({
body: {
invalidated_api_keys: ['api-key-id-1'],
previously_invalidated_api_keys: [],
error_count: 0,
},
})
);
const result = await apiKeys.invalidateAsInternalUser({ id: '123' });
expect(result).toEqual({
invalidated_api_keys: ['api-key-id-1'],
previously_invalidated_api_keys: [],
error_count: 0,
});
expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('shield.invalidateAPIKey', {
expect(mockClusterClient.asInternalUser.security.invalidateApiKey).toHaveBeenCalledWith({
body: {
id: '123',
},
@ -362,11 +385,15 @@ describe('API Keys', () => {
it('Only passes id as a parameter', async () => {
mockLicense.isEnabled.mockReturnValue(true);
mockClusterClient.callAsInternalUser.mockResolvedValueOnce({
invalidated_api_keys: ['api-key-id-1'],
previously_invalidated_api_keys: [],
error_count: 0,
});
mockClusterClient.asInternalUser.security.invalidateApiKey.mockResolvedValueOnce(
securityMock.createApiResponse({
body: {
invalidated_api_keys: ['api-key-id-1'],
previously_invalidated_api_keys: [],
error_count: 0,
},
})
);
const result = await apiKeys.invalidateAsInternalUser({
id: '123',
name: 'abc',
@ -376,7 +403,7 @@ describe('API Keys', () => {
previously_invalidated_api_keys: [],
error_count: 0,
});
expect(mockClusterClient.callAsInternalUser).toHaveBeenCalledWith('shield.invalidateAPIKey', {
expect(mockClusterClient.asInternalUser.security.invalidateApiKey).toHaveBeenCalledWith({
body: {
id: '123',
},

View file

@ -4,10 +4,12 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { ILegacyClusterClient, KibanaRequest, Logger } from '../../../../../src/core/server';
import { SecurityLicense } from '../../common/licensing';
import { HTTPAuthorizationHeader } from './http_authentication';
import { BasicHTTPAuthorizationHeaderCredentials } from './http_authentication';
import type { IClusterClient, KibanaRequest, Logger } from '../../../../../../src/core/server';
import type { SecurityLicense } from '../../../common/licensing';
import {
HTTPAuthorizationHeader,
BasicHTTPAuthorizationHeaderCredentials,
} from '../http_authentication';
/**
* Represents the options to create an APIKey class instance that will be
@ -15,7 +17,7 @@ import { BasicHTTPAuthorizationHeaderCredentials } from './http_authentication';
*/
export interface ConstructorOptions {
logger: Logger;
clusterClient: ILegacyClusterClient;
clusterClient: IClusterClient;
license: SecurityLicense;
}
@ -117,7 +119,7 @@ export interface InvalidateAPIKeyResult {
*/
export class APIKeys {
private readonly logger: Logger;
private readonly clusterClient: ILegacyClusterClient;
private readonly clusterClient: IClusterClient;
private readonly license: SecurityLicense;
constructor({ logger, clusterClient, license }: ConstructorOptions) {
@ -141,11 +143,7 @@ export class APIKeys {
);
try {
await this.clusterClient.callAsInternalUser('shield.invalidateAPIKey', {
body: {
id,
},
});
await this.clusterClient.asInternalUser.security.invalidateApiKey({ body: { id } });
return true;
} catch (e) {
if (this.doesErrorIndicateAPIKeysAreDisabled(e)) {
@ -171,11 +169,13 @@ export class APIKeys {
this.logger.debug('Trying to create an API key');
// User needs `manage_api_key` privilege to use this API
let result: CreateAPIKeyResult;
let result;
try {
result = (await this.clusterClient
.asScoped(request)
.callAsCurrentUser('shield.createAPIKey', { body: params })) as CreateAPIKeyResult;
result = (
await this.clusterClient
.asScoped(request)
.asCurrentUser.security.createApiKey<CreateAPIKeyResult>({ body: params })
).body;
this.logger.debug('API key was created successfully');
} catch (e) {
this.logger.error(`Failed to create API key: ${e.message}`);
@ -188,6 +188,7 @@ export class APIKeys {
/**
* Tries to grant an API key for the current user.
* @param request Request instance.
* @param createParams Create operation parameters.
*/
async grantAsInternalUser(request: KibanaRequest, createParams: CreateAPIKeyParams) {
if (!this.license.isEnabled()) {
@ -204,11 +205,13 @@ export class APIKeys {
const params = this.getGrantParams(createParams, authorizationHeader);
// User needs `manage_api_key` or `grant_api_key` privilege to use this API
let result: GrantAPIKeyResult;
let result;
try {
result = (await this.clusterClient.callAsInternalUser('shield.grantAPIKey', {
body: params,
})) as GrantAPIKeyResult;
result = (
await this.clusterClient.asInternalUser.security.grantApiKey<GrantAPIKeyResult>({
body: params,
})
).body;
this.logger.debug('API key was granted successfully');
} catch (e) {
this.logger.error(`Failed to grant API key: ${e.message}`);
@ -230,16 +233,16 @@ export class APIKeys {
this.logger.debug('Trying to invalidate an API key as current user');
let result: InvalidateAPIKeyResult;
let result;
try {
// User needs `manage_api_key` privilege to use this API
result = await this.clusterClient
.asScoped(request)
.callAsCurrentUser('shield.invalidateAPIKey', {
body: {
id: params.id,
},
});
result = (
await this.clusterClient
.asScoped(request)
.asCurrentUser.security.invalidateApiKey<InvalidateAPIKeyResult>({
body: { id: params.id },
})
).body;
this.logger.debug('API key was invalidated successfully as current user');
} catch (e) {
this.logger.error(`Failed to invalidate API key as current user: ${e.message}`);
@ -260,14 +263,14 @@ export class APIKeys {
this.logger.debug('Trying to invalidate an API key');
let result: InvalidateAPIKeyResult;
let result;
try {
// Internal user needs `cluster:admin/xpack/security/api_key/invalidate` privilege to use this API
result = await this.clusterClient.callAsInternalUser('shield.invalidateAPIKey', {
body: {
id: params.id,
},
});
result = (
await this.clusterClient.asInternalUser.security.invalidateApiKey<InvalidateAPIKeyResult>({
body: { id: params.id },
})
).body;
this.logger.debug('API key was invalidated successfully');
} catch (e) {
this.logger.error(`Failed to invalidate API key: ${e.message}`);

View file

@ -0,0 +1,14 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export {
APIKeys,
CreateAPIKeyResult,
InvalidateAPIKeyResult,
CreateAPIKeyParams,
InvalidateAPIKeyParams,
GrantAPIKeyResult,
} from './api_keys';

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;
* you may not use this file except in compliance with the Elastic License.
*/
import type { DeeplyMockedKeys } from '@kbn/utility-types/jest';
import type {
AuthenticationServiceSetup,
AuthenticationServiceStart,
} from './authentication_service';
import { apiKeysMock } from './api_keys/api_keys.mock';
export const authenticationServiceMock = {
createSetup: (): jest.Mocked<AuthenticationServiceSetup> => ({
getCurrentUser: jest.fn(),
}),
createStart: (): DeeplyMockedKeys<AuthenticationServiceStart> => ({
apiKeys: apiKeysMock.create(),
login: jest.fn(),
logout: jest.fn(),
getCurrentUser: jest.fn(),
acknowledgeAccessAgreement: jest.fn(),
}),
};

View file

@ -0,0 +1,366 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
jest.mock('./authenticator');
import Boom from '@hapi/boom';
import type { PublicMethodsOf } from '@kbn/utility-types';
import {
loggingSystemMock,
coreMock,
httpServerMock,
httpServiceMock,
elasticsearchServiceMock,
} from '../../../../../src/core/server/mocks';
import { licenseMock } from '../../common/licensing/index.mock';
import { mockAuthenticatedUser } from '../../common/model/authenticated_user.mock';
import { auditServiceMock, securityAuditLoggerMock } from '../audit/index.mock';
import { securityFeatureUsageServiceMock } from '../feature_usage/index.mock';
import { sessionMock } from '../session_management/session.mock';
import type {
AuthenticationHandler,
AuthToolkit,
ILegacyClusterClient,
KibanaRequest,
Logger,
LoggerFactory,
LegacyScopedClusterClient,
HttpServiceSetup,
HttpServiceStart,
} from '../../../../../src/core/server';
import type { AuthenticatedUser } from '../../common/model';
import type { SecurityLicense } from '../../common/licensing';
import type { AuditServiceSetup, SecurityAuditLogger } from '../audit';
import type { SecurityFeatureUsageServiceStart } from '../feature_usage';
import type { Session } from '../session_management';
import { ConfigSchema, ConfigType, createConfig } from '../config';
import { AuthenticationResult } from './authentication_result';
import { AuthenticationService } from './authentication_service';
describe('AuthenticationService', () => {
let service: AuthenticationService;
let logger: jest.Mocked<Logger>;
let mockSetupAuthenticationParams: {
legacyAuditLogger: jest.Mocked<SecurityAuditLogger>;
audit: jest.Mocked<AuditServiceSetup>;
config: ConfigType;
loggers: LoggerFactory;
http: jest.Mocked<HttpServiceSetup>;
clusterClient: jest.Mocked<ILegacyClusterClient>;
license: jest.Mocked<SecurityLicense>;
getFeatureUsageService: () => jest.Mocked<SecurityFeatureUsageServiceStart>;
session: jest.Mocked<PublicMethodsOf<Session>>;
};
let mockScopedClusterClient: jest.Mocked<PublicMethodsOf<LegacyScopedClusterClient>>;
beforeEach(() => {
logger = loggingSystemMock.createLogger();
mockSetupAuthenticationParams = {
legacyAuditLogger: securityAuditLoggerMock.create(),
audit: auditServiceMock.create(),
http: coreMock.createSetup().http,
config: createConfig(
ConfigSchema.validate({
encryptionKey: 'ab'.repeat(16),
secureCookies: true,
cookieName: 'my-sid-cookie',
}),
loggingSystemMock.create().get(),
{ isTLSEnabled: false }
),
clusterClient: elasticsearchServiceMock.createLegacyClusterClient(),
license: licenseMock.create(),
loggers: loggingSystemMock.create(),
getFeatureUsageService: jest
.fn()
.mockReturnValue(securityFeatureUsageServiceMock.createStartContract()),
session: sessionMock.create(),
};
mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient();
mockSetupAuthenticationParams.clusterClient.asScoped.mockReturnValue(
(mockScopedClusterClient as unknown) as jest.Mocked<LegacyScopedClusterClient>
);
service = new AuthenticationService(logger);
});
afterEach(() => jest.clearAllMocks());
describe('#setup()', () => {
it('properly registers auth handler', () => {
service.setup(mockSetupAuthenticationParams);
expect(mockSetupAuthenticationParams.http.registerAuth).toHaveBeenCalledTimes(1);
expect(mockSetupAuthenticationParams.http.registerAuth).toHaveBeenCalledWith(
expect.any(Function)
);
});
describe('authentication handler', () => {
let authHandler: AuthenticationHandler;
let authenticate: jest.SpyInstance<Promise<AuthenticationResult>, [KibanaRequest]>;
let mockAuthToolkit: jest.Mocked<AuthToolkit>;
beforeEach(() => {
mockAuthToolkit = httpServiceMock.createAuthToolkit();
service.setup(mockSetupAuthenticationParams);
expect(mockSetupAuthenticationParams.http.registerAuth).toHaveBeenCalledTimes(1);
expect(mockSetupAuthenticationParams.http.registerAuth).toHaveBeenCalledWith(
expect.any(Function)
);
authHandler = mockSetupAuthenticationParams.http.registerAuth.mock.calls[0][0];
authenticate = jest.requireMock('./authenticator').Authenticator.mock.instances[0]
.authenticate;
});
it('replies with no credentials when security is disabled in elasticsearch', async () => {
const mockRequest = httpServerMock.createKibanaRequest();
const mockResponse = httpServerMock.createLifecycleResponseFactory();
mockSetupAuthenticationParams.license.isEnabled.mockReturnValue(false);
await authHandler(mockRequest, mockResponse, mockAuthToolkit);
expect(mockAuthToolkit.authenticated).toHaveBeenCalledTimes(1);
expect(mockAuthToolkit.authenticated).toHaveBeenCalledWith();
expect(mockAuthToolkit.redirected).not.toHaveBeenCalled();
expect(mockResponse.internalError).not.toHaveBeenCalled();
expect(authenticate).not.toHaveBeenCalled();
});
it('continues request with credentials on success', async () => {
const mockRequest = httpServerMock.createKibanaRequest();
const mockResponse = httpServerMock.createLifecycleResponseFactory();
const mockUser = mockAuthenticatedUser();
const mockAuthHeaders = { authorization: 'Basic xxx' };
authenticate.mockResolvedValue(
AuthenticationResult.succeeded(mockUser, { authHeaders: mockAuthHeaders })
);
await authHandler(mockRequest, mockResponse, mockAuthToolkit);
expect(mockAuthToolkit.authenticated).toHaveBeenCalledTimes(1);
expect(mockAuthToolkit.authenticated).toHaveBeenCalledWith({
state: mockUser,
requestHeaders: mockAuthHeaders,
});
expect(mockAuthToolkit.redirected).not.toHaveBeenCalled();
expect(mockResponse.internalError).not.toHaveBeenCalled();
expect(authenticate).toHaveBeenCalledTimes(1);
expect(authenticate).toHaveBeenCalledWith(mockRequest);
});
it('returns authentication response headers on success if any', async () => {
const mockRequest = httpServerMock.createKibanaRequest();
const mockResponse = httpServerMock.createLifecycleResponseFactory();
const mockUser = mockAuthenticatedUser();
const mockAuthHeaders = { authorization: 'Basic xxx' };
const mockAuthResponseHeaders = { 'WWW-Authenticate': 'Negotiate' };
authenticate.mockResolvedValue(
AuthenticationResult.succeeded(mockUser, {
authHeaders: mockAuthHeaders,
authResponseHeaders: mockAuthResponseHeaders,
})
);
await authHandler(mockRequest, mockResponse, mockAuthToolkit);
expect(mockAuthToolkit.authenticated).toHaveBeenCalledTimes(1);
expect(mockAuthToolkit.authenticated).toHaveBeenCalledWith({
state: mockUser,
requestHeaders: mockAuthHeaders,
responseHeaders: mockAuthResponseHeaders,
});
expect(mockAuthToolkit.redirected).not.toHaveBeenCalled();
expect(mockResponse.internalError).not.toHaveBeenCalled();
expect(authenticate).toHaveBeenCalledTimes(1);
expect(authenticate).toHaveBeenCalledWith(mockRequest);
});
it('redirects user if redirection is requested by the authenticator preserving authentication response headers if any', async () => {
const mockResponse = httpServerMock.createLifecycleResponseFactory();
authenticate.mockResolvedValue(
AuthenticationResult.redirectTo('/some/url', {
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
})
);
await authHandler(httpServerMock.createKibanaRequest(), mockResponse, mockAuthToolkit);
expect(mockAuthToolkit.redirected).toHaveBeenCalledTimes(1);
expect(mockAuthToolkit.redirected).toHaveBeenCalledWith({
location: '/some/url',
'WWW-Authenticate': 'Negotiate',
});
expect(mockAuthToolkit.authenticated).not.toHaveBeenCalled();
expect(mockResponse.internalError).not.toHaveBeenCalled();
});
it('rejects with `Internal Server Error` and log error when `authenticate` throws unhandled exception', async () => {
const mockResponse = httpServerMock.createLifecycleResponseFactory();
const failureReason = new Error('something went wrong');
authenticate.mockRejectedValue(failureReason);
await authHandler(httpServerMock.createKibanaRequest(), mockResponse, mockAuthToolkit);
expect(mockResponse.internalError).toHaveBeenCalledTimes(1);
const [[error]] = mockResponse.internalError.mock.calls;
expect(error).toBeUndefined();
expect(mockAuthToolkit.authenticated).not.toHaveBeenCalled();
expect(mockAuthToolkit.redirected).not.toHaveBeenCalled();
expect(logger.error).toHaveBeenCalledWith(failureReason);
});
it('rejects with original `badRequest` error when `authenticate` fails to authenticate user', async () => {
const mockResponse = httpServerMock.createLifecycleResponseFactory();
const esError = Boom.badRequest('some message');
authenticate.mockResolvedValue(AuthenticationResult.failed(esError));
await authHandler(httpServerMock.createKibanaRequest(), mockResponse, mockAuthToolkit);
expect(mockResponse.customError).toHaveBeenCalledTimes(1);
const [[response]] = mockResponse.customError.mock.calls;
expect(response.body).toBe(esError);
expect(mockAuthToolkit.authenticated).not.toHaveBeenCalled();
expect(mockAuthToolkit.redirected).not.toHaveBeenCalled();
});
it('includes `WWW-Authenticate` header if `authenticate` fails to authenticate user and provides challenges', async () => {
const mockResponse = httpServerMock.createLifecycleResponseFactory();
const originalError = Boom.unauthorized('some message');
originalError.output.headers['WWW-Authenticate'] = [
'Basic realm="Access to prod", charset="UTF-8"',
'Basic',
'Negotiate',
] as any;
authenticate.mockResolvedValue(
AuthenticationResult.failed(originalError, {
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
})
);
await authHandler(httpServerMock.createKibanaRequest(), mockResponse, mockAuthToolkit);
expect(mockResponse.customError).toHaveBeenCalledTimes(1);
const [[options]] = mockResponse.customError.mock.calls;
expect(options.body).toBe(originalError);
expect(options!.headers).toEqual({ 'WWW-Authenticate': 'Negotiate' });
expect(mockAuthToolkit.authenticated).not.toHaveBeenCalled();
expect(mockAuthToolkit.redirected).not.toHaveBeenCalled();
});
it('returns `notHandled` when authentication can not be handled', async () => {
const mockResponse = httpServerMock.createLifecycleResponseFactory();
authenticate.mockResolvedValue(AuthenticationResult.notHandled());
await authHandler(httpServerMock.createKibanaRequest(), mockResponse, mockAuthToolkit);
expect(mockAuthToolkit.notHandled).toHaveBeenCalledTimes(1);
expect(mockAuthToolkit.authenticated).not.toHaveBeenCalled();
expect(mockAuthToolkit.redirected).not.toHaveBeenCalled();
});
});
describe('getCurrentUser()', () => {
let getCurrentUser: (r: KibanaRequest) => AuthenticatedUser | null;
beforeEach(async () => {
getCurrentUser = service.setup(mockSetupAuthenticationParams).getCurrentUser;
});
it('returns `null` if Security is disabled', () => {
mockSetupAuthenticationParams.license.isEnabled.mockReturnValue(false);
expect(getCurrentUser(httpServerMock.createKibanaRequest())).toBe(null);
});
it('returns user from the auth state.', () => {
const mockUser = mockAuthenticatedUser();
const mockAuthGet = mockSetupAuthenticationParams.http.auth.get as jest.Mock;
mockAuthGet.mockReturnValue({ state: mockUser });
const mockRequest = httpServerMock.createKibanaRequest();
expect(getCurrentUser(mockRequest)).toBe(mockUser);
expect(mockAuthGet).toHaveBeenCalledTimes(1);
expect(mockAuthGet).toHaveBeenCalledWith(mockRequest);
});
it('returns null if auth state is not available.', () => {
const mockAuthGet = mockSetupAuthenticationParams.http.auth.get as jest.Mock;
mockAuthGet.mockReturnValue({});
const mockRequest = httpServerMock.createKibanaRequest();
expect(getCurrentUser(mockRequest)).toBeNull();
expect(mockAuthGet).toHaveBeenCalledTimes(1);
expect(mockAuthGet).toHaveBeenCalledWith(mockRequest);
});
});
});
describe('#start()', () => {
let mockStartAuthenticationParams: {
http: jest.Mocked<HttpServiceStart>;
clusterClient: ReturnType<typeof elasticsearchServiceMock.createClusterClient>;
};
beforeEach(() => {
const coreStart = coreMock.createStart();
mockStartAuthenticationParams = {
http: coreStart.http,
clusterClient: elasticsearchServiceMock.createClusterClient(),
};
service.setup(mockSetupAuthenticationParams);
});
describe('getCurrentUser()', () => {
let getCurrentUser: (r: KibanaRequest) => AuthenticatedUser | null;
beforeEach(async () => {
getCurrentUser = (await service.start(mockStartAuthenticationParams)).getCurrentUser;
});
it('returns `null` if Security is disabled', () => {
mockSetupAuthenticationParams.license.isEnabled.mockReturnValue(false);
expect(getCurrentUser(httpServerMock.createKibanaRequest())).toBe(null);
});
it('returns user from the auth state.', () => {
const mockUser = mockAuthenticatedUser();
const mockAuthGet = mockStartAuthenticationParams.http.auth.get as jest.Mock;
mockAuthGet.mockReturnValue({ state: mockUser });
const mockRequest = httpServerMock.createKibanaRequest();
expect(getCurrentUser(mockRequest)).toBe(mockUser);
expect(mockAuthGet).toHaveBeenCalledTimes(1);
expect(mockAuthGet).toHaveBeenCalledWith(mockRequest);
});
it('returns null if auth state is not available.', () => {
const mockAuthGet = mockStartAuthenticationParams.http.auth.get as jest.Mock;
mockAuthGet.mockReturnValue({});
const mockRequest = httpServerMock.createKibanaRequest();
expect(getCurrentUser(mockRequest)).toBeNull();
expect(mockAuthGet).toHaveBeenCalledTimes(1);
expect(mockAuthGet).toHaveBeenCalledWith(mockRequest);
});
});
});
});

View file

@ -0,0 +1,205 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import type { PublicMethodsOf } from '@kbn/utility-types';
import type {
LoggerFactory,
KibanaRequest,
Logger,
HttpServiceSetup,
IClusterClient,
ILegacyClusterClient,
HttpServiceStart,
} from '../../../../../src/core/server';
import type { SecurityLicense } from '../../common/licensing';
import type { AuthenticatedUser } from '../../common/model';
import type { AuditServiceSetup, SecurityAuditLogger } from '../audit';
import type { ConfigType } from '../config';
import type { SecurityFeatureUsageServiceStart } from '../feature_usage';
import type { Session } from '../session_management';
import type { DeauthenticationResult } from './deauthentication_result';
import type { AuthenticationResult } from './authentication_result';
import { getErrorStatusCode } from '../errors';
import { APIKeys } from './api_keys';
import { Authenticator, ProviderLoginAttempt } from './authenticator';
interface AuthenticationServiceSetupParams {
legacyAuditLogger: SecurityAuditLogger;
audit: AuditServiceSetup;
getFeatureUsageService: () => SecurityFeatureUsageServiceStart;
http: HttpServiceSetup;
clusterClient: ILegacyClusterClient;
config: ConfigType;
license: SecurityLicense;
loggers: LoggerFactory;
session: PublicMethodsOf<Session>;
}
interface AuthenticationServiceStartParams {
http: HttpServiceStart;
clusterClient: IClusterClient;
}
export interface AuthenticationServiceSetup {
/**
* @deprecated use `getCurrentUser` from the start contract instead
*/
getCurrentUser: (request: KibanaRequest) => AuthenticatedUser | null;
}
export interface AuthenticationServiceStart {
apiKeys: Pick<
APIKeys,
| 'areAPIKeysEnabled'
| 'create'
| 'invalidate'
| 'grantAsInternalUser'
| 'invalidateAsInternalUser'
>;
login: (request: KibanaRequest, attempt: ProviderLoginAttempt) => Promise<AuthenticationResult>;
logout: (request: KibanaRequest) => Promise<DeauthenticationResult>;
acknowledgeAccessAgreement: (request: KibanaRequest) => Promise<void>;
getCurrentUser: (request: KibanaRequest) => AuthenticatedUser | null;
}
export class AuthenticationService {
private license!: SecurityLicense;
private authenticator!: Authenticator;
constructor(private readonly logger: Logger) {}
setup({
legacyAuditLogger: auditLogger,
audit,
getFeatureUsageService,
http,
clusterClient,
config,
license,
loggers,
session,
}: AuthenticationServiceSetupParams): AuthenticationServiceSetup {
this.license = license;
const getCurrentUser = (request: KibanaRequest) => {
if (!license.isEnabled()) {
return null;
}
return http.auth.get<AuthenticatedUser>(request).state ?? null;
};
this.authenticator = new Authenticator({
legacyAuditLogger: auditLogger,
audit,
loggers,
clusterClient,
basePath: http.basePath,
config: { authc: config.authc },
getCurrentUser,
getFeatureUsageService,
license,
session,
});
http.registerAuth(async (request, response, t) => {
// If security is disabled continue with no user credentials and delete the client cookie as well.
if (!license.isEnabled()) {
return t.authenticated();
}
let authenticationResult;
try {
authenticationResult = await this.authenticator.authenticate(request);
} catch (err) {
this.logger.error(err);
return response.internalError();
}
if (authenticationResult.succeeded()) {
return t.authenticated({
state: authenticationResult.user,
requestHeaders: authenticationResult.authHeaders,
responseHeaders: authenticationResult.authResponseHeaders,
});
}
if (authenticationResult.redirected()) {
// Some authentication mechanisms may require user to be redirected to another location to
// initiate or complete authentication flow. It can be Kibana own login page for basic
// authentication (username and password) or arbitrary external page managed by 3rd party
// Identity Provider for SSO authentication mechanisms. Authentication provider is the one who
// decides what location user should be redirected to.
return t.redirected({
location: authenticationResult.redirectURL!,
...(authenticationResult.authResponseHeaders || {}),
});
}
if (authenticationResult.failed()) {
this.logger.info(`Authentication attempt failed: ${authenticationResult.error!.message}`);
const error = authenticationResult.error!;
// proxy Elasticsearch "native" errors
const statusCode = getErrorStatusCode(error);
if (typeof statusCode === 'number') {
return response.customError({
body: error,
statusCode,
headers: authenticationResult.authResponseHeaders,
});
}
return response.unauthorized({
headers: authenticationResult.authResponseHeaders,
});
}
this.logger.debug('Could not handle authentication attempt');
return t.notHandled();
});
this.logger.debug('Successfully registered core authentication handler.');
return {
getCurrentUser,
};
}
start({ clusterClient, http }: AuthenticationServiceStartParams): AuthenticationServiceStart {
const apiKeys = new APIKeys({
clusterClient,
logger: this.logger.get('api-key'),
license: this.license,
});
return {
apiKeys: {
areAPIKeysEnabled: apiKeys.areAPIKeysEnabled.bind(apiKeys),
create: apiKeys.create.bind(apiKeys),
grantAsInternalUser: apiKeys.grantAsInternalUser.bind(apiKeys),
invalidate: apiKeys.invalidate.bind(apiKeys),
invalidateAsInternalUser: apiKeys.invalidateAsInternalUser.bind(apiKeys),
},
login: this.authenticator.login.bind(this.authenticator),
logout: this.authenticator.logout.bind(this.authenticator),
acknowledgeAccessAgreement: this.authenticator.acknowledgeAccessAgreement.bind(
this.authenticator
),
/**
* Retrieves currently authenticated user associated with the specified request.
* @param request
*/
getCurrentUser: (request: KibanaRequest) => {
if (!this.license.isEnabled()) {
return null;
}
return http.auth.get<AuthenticatedUser>(request).state ?? null;
},
};
}
}

View file

@ -198,9 +198,7 @@ describe('Authenticator', () => {
afterEach(() => jest.resetAllMocks());
it('enabled by default', () => {
const authenticator = new Authenticator(getMockOptions());
expect(authenticator.isProviderTypeEnabled('basic')).toBe(true);
expect(authenticator.isProviderTypeEnabled('http')).toBe(true);
new Authenticator(getMockOptions());
expect(
jest.requireMock('./providers/http').HTTPAuthenticationProvider
@ -210,14 +208,11 @@ describe('Authenticator', () => {
});
it('includes all required schemes if `autoSchemesEnabled` is enabled', () => {
const authenticator = new Authenticator(
new Authenticator(
getMockOptions({
providers: { basic: { basic1: { order: 0 } }, kerberos: { kerberos1: { order: 1 } } },
})
);
expect(authenticator.isProviderTypeEnabled('basic')).toBe(true);
expect(authenticator.isProviderTypeEnabled('kerberos')).toBe(true);
expect(authenticator.isProviderTypeEnabled('http')).toBe(true);
expect(
jest.requireMock('./providers/http').HTTPAuthenticationProvider
@ -227,15 +222,12 @@ describe('Authenticator', () => {
});
it('does not include additional schemes if `autoSchemesEnabled` is disabled', () => {
const authenticator = new Authenticator(
new Authenticator(
getMockOptions({
providers: { basic: { basic1: { order: 0 } }, kerberos: { kerberos1: { order: 1 } } },
http: { autoSchemesEnabled: false },
})
);
expect(authenticator.isProviderTypeEnabled('basic')).toBe(true);
expect(authenticator.isProviderTypeEnabled('kerberos')).toBe(true);
expect(authenticator.isProviderTypeEnabled('http')).toBe(true);
expect(
jest.requireMock('./providers/http').HTTPAuthenticationProvider
@ -243,14 +235,12 @@ describe('Authenticator', () => {
});
it('disabled if explicitly disabled', () => {
const authenticator = new Authenticator(
new Authenticator(
getMockOptions({
providers: { basic: { basic1: { order: 0 } } },
http: { enabled: false },
})
);
expect(authenticator.isProviderTypeEnabled('basic')).toBe(true);
expect(authenticator.isProviderTypeEnabled('http')).toBe(false);
expect(
jest.requireMock('./providers/http').HTTPAuthenticationProvider
@ -1864,27 +1854,6 @@ describe('Authenticator', () => {
});
});
describe('`isProviderEnabled` method', () => {
it('returns `true` only if specified provider is enabled', () => {
let authenticator = new Authenticator(
getMockOptions({ providers: { basic: { basic1: { order: 0 } } } })
);
expect(authenticator.isProviderTypeEnabled('basic')).toBe(true);
expect(authenticator.isProviderTypeEnabled('saml')).toBe(false);
authenticator = new Authenticator(
getMockOptions({
providers: {
basic: { basic1: { order: 0 } },
saml: { saml1: { order: 1, realm: 'test' } },
},
})
);
expect(authenticator.isProviderTypeEnabled('basic')).toBe(true);
expect(authenticator.isProviderTypeEnabled('saml')).toBe(true);
});
});
describe('`acknowledgeAccessAgreement` method', () => {
let authenticator: Authenticator;
let mockOptions: ReturnType<typeof getMockOptions>;

View file

@ -421,14 +421,6 @@ export class Authenticator {
return DeauthenticationResult.notHandled();
}
/**
* Checks whether specified provider type is currently enabled.
* @param providerType Type of the provider (`basic`, `saml`, `pki` etc.).
*/
isProviderTypeEnabled(providerType: string) {
return [...this.providers.values()].some((provider) => provider.type === providerType);
}
/**
* Acknowledges access agreement on behalf of the currently authenticated user.
* @param request Request instance.

View file

@ -1,23 +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;
* you may not use this file except in compliance with the Elastic License.
*/
import { Authentication } from '.';
export const authenticationMock = {
create: (): jest.Mocked<Authentication> => ({
login: jest.fn(),
logout: jest.fn(),
isProviderTypeEnabled: jest.fn(),
areAPIKeysEnabled: jest.fn(),
createAPIKey: jest.fn(),
getCurrentUser: jest.fn(),
grantAPIKeyAsInternalUser: jest.fn(),
invalidateAPIKey: jest.fn(),
invalidateAPIKeyAsInternalUser: jest.fn(),
isAuthenticated: jest.fn(),
acknowledgeAccessAgreement: jest.fn(),
}),
};

View file

@ -1,442 +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;
* you may not use this file except in compliance with the Elastic License.
*/
jest.mock('./api_keys');
jest.mock('./authenticator');
import Boom from '@hapi/boom';
import type { PublicMethodsOf } from '@kbn/utility-types';
import {
loggingSystemMock,
coreMock,
httpServerMock,
httpServiceMock,
elasticsearchServiceMock,
} from '../../../../../src/core/server/mocks';
import { licenseMock } from '../../common/licensing/index.mock';
import { mockAuthenticatedUser } from '../../common/model/authenticated_user.mock';
import { auditServiceMock, securityAuditLoggerMock } from '../audit/index.mock';
import { securityFeatureUsageServiceMock } from '../feature_usage/index.mock';
import { sessionMock } from '../session_management/session.mock';
import {
AuthenticationHandler,
AuthToolkit,
ILegacyClusterClient,
KibanaRequest,
LoggerFactory,
LegacyScopedClusterClient,
HttpServiceSetup,
} from '../../../../../src/core/server';
import { AuthenticatedUser } from '../../common/model';
import { ConfigSchema, ConfigType, createConfig } from '../config';
import { AuthenticationResult } from './authentication_result';
import { Authentication, setupAuthentication } from '.';
import {
CreateAPIKeyResult,
CreateAPIKeyParams,
InvalidateAPIKeyResult,
InvalidateAPIKeyParams,
} from './api_keys';
import { SecurityLicense } from '../../common/licensing';
import { AuditServiceSetup, SecurityAuditLogger } from '../audit';
import { SecurityFeatureUsageServiceStart } from '../feature_usage';
import { Session } from '../session_management';
describe('setupAuthentication()', () => {
let mockSetupAuthenticationParams: {
legacyAuditLogger: jest.Mocked<SecurityAuditLogger>;
audit: jest.Mocked<AuditServiceSetup>;
config: ConfigType;
loggers: LoggerFactory;
http: jest.Mocked<HttpServiceSetup>;
clusterClient: jest.Mocked<ILegacyClusterClient>;
license: jest.Mocked<SecurityLicense>;
getFeatureUsageService: () => jest.Mocked<SecurityFeatureUsageServiceStart>;
session: jest.Mocked<PublicMethodsOf<Session>>;
};
let mockScopedClusterClient: jest.Mocked<PublicMethodsOf<LegacyScopedClusterClient>>;
beforeEach(() => {
mockSetupAuthenticationParams = {
legacyAuditLogger: securityAuditLoggerMock.create(),
audit: auditServiceMock.create(),
http: coreMock.createSetup().http,
config: createConfig(
ConfigSchema.validate({
encryptionKey: 'ab'.repeat(16),
secureCookies: true,
cookieName: 'my-sid-cookie',
}),
loggingSystemMock.create().get(),
{ isTLSEnabled: false }
),
clusterClient: elasticsearchServiceMock.createLegacyClusterClient(),
license: licenseMock.create(),
loggers: loggingSystemMock.create(),
getFeatureUsageService: jest
.fn()
.mockReturnValue(securityFeatureUsageServiceMock.createStartContract()),
session: sessionMock.create(),
};
mockScopedClusterClient = elasticsearchServiceMock.createLegacyScopedClusterClient();
mockSetupAuthenticationParams.clusterClient.asScoped.mockReturnValue(
(mockScopedClusterClient as unknown) as jest.Mocked<LegacyScopedClusterClient>
);
});
afterEach(() => jest.clearAllMocks());
it('properly registers auth handler', async () => {
await setupAuthentication(mockSetupAuthenticationParams);
expect(mockSetupAuthenticationParams.http.registerAuth).toHaveBeenCalledTimes(1);
expect(mockSetupAuthenticationParams.http.registerAuth).toHaveBeenCalledWith(
expect.any(Function)
);
});
describe('authentication handler', () => {
let authHandler: AuthenticationHandler;
let authenticate: jest.SpyInstance<Promise<AuthenticationResult>, [KibanaRequest]>;
let mockAuthToolkit: jest.Mocked<AuthToolkit>;
beforeEach(async () => {
mockAuthToolkit = httpServiceMock.createAuthToolkit();
await setupAuthentication(mockSetupAuthenticationParams);
expect(mockSetupAuthenticationParams.http.registerAuth).toHaveBeenCalledTimes(1);
expect(mockSetupAuthenticationParams.http.registerAuth).toHaveBeenCalledWith(
expect.any(Function)
);
authHandler = mockSetupAuthenticationParams.http.registerAuth.mock.calls[0][0];
authenticate = jest.requireMock('./authenticator').Authenticator.mock.instances[0]
.authenticate;
});
it('replies with no credentials when security is disabled in elasticsearch', async () => {
const mockRequest = httpServerMock.createKibanaRequest();
const mockResponse = httpServerMock.createLifecycleResponseFactory();
mockSetupAuthenticationParams.license.isEnabled.mockReturnValue(false);
await authHandler(mockRequest, mockResponse, mockAuthToolkit);
expect(mockAuthToolkit.authenticated).toHaveBeenCalledTimes(1);
expect(mockAuthToolkit.authenticated).toHaveBeenCalledWith();
expect(mockAuthToolkit.redirected).not.toHaveBeenCalled();
expect(mockResponse.internalError).not.toHaveBeenCalled();
expect(authenticate).not.toHaveBeenCalled();
});
it('continues request with credentials on success', async () => {
const mockRequest = httpServerMock.createKibanaRequest();
const mockResponse = httpServerMock.createLifecycleResponseFactory();
const mockUser = mockAuthenticatedUser();
const mockAuthHeaders = { authorization: 'Basic xxx' };
authenticate.mockResolvedValue(
AuthenticationResult.succeeded(mockUser, { authHeaders: mockAuthHeaders })
);
await authHandler(mockRequest, mockResponse, mockAuthToolkit);
expect(mockAuthToolkit.authenticated).toHaveBeenCalledTimes(1);
expect(mockAuthToolkit.authenticated).toHaveBeenCalledWith({
state: mockUser,
requestHeaders: mockAuthHeaders,
});
expect(mockAuthToolkit.redirected).not.toHaveBeenCalled();
expect(mockResponse.internalError).not.toHaveBeenCalled();
expect(authenticate).toHaveBeenCalledTimes(1);
expect(authenticate).toHaveBeenCalledWith(mockRequest);
});
it('returns authentication response headers on success if any', async () => {
const mockRequest = httpServerMock.createKibanaRequest();
const mockResponse = httpServerMock.createLifecycleResponseFactory();
const mockUser = mockAuthenticatedUser();
const mockAuthHeaders = { authorization: 'Basic xxx' };
const mockAuthResponseHeaders = { 'WWW-Authenticate': 'Negotiate' };
authenticate.mockResolvedValue(
AuthenticationResult.succeeded(mockUser, {
authHeaders: mockAuthHeaders,
authResponseHeaders: mockAuthResponseHeaders,
})
);
await authHandler(mockRequest, mockResponse, mockAuthToolkit);
expect(mockAuthToolkit.authenticated).toHaveBeenCalledTimes(1);
expect(mockAuthToolkit.authenticated).toHaveBeenCalledWith({
state: mockUser,
requestHeaders: mockAuthHeaders,
responseHeaders: mockAuthResponseHeaders,
});
expect(mockAuthToolkit.redirected).not.toHaveBeenCalled();
expect(mockResponse.internalError).not.toHaveBeenCalled();
expect(authenticate).toHaveBeenCalledTimes(1);
expect(authenticate).toHaveBeenCalledWith(mockRequest);
});
it('redirects user if redirection is requested by the authenticator preserving authentication response headers if any', async () => {
const mockResponse = httpServerMock.createLifecycleResponseFactory();
authenticate.mockResolvedValue(
AuthenticationResult.redirectTo('/some/url', {
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
})
);
await authHandler(httpServerMock.createKibanaRequest(), mockResponse, mockAuthToolkit);
expect(mockAuthToolkit.redirected).toHaveBeenCalledTimes(1);
expect(mockAuthToolkit.redirected).toHaveBeenCalledWith({
location: '/some/url',
'WWW-Authenticate': 'Negotiate',
});
expect(mockAuthToolkit.authenticated).not.toHaveBeenCalled();
expect(mockResponse.internalError).not.toHaveBeenCalled();
});
it('rejects with `Internal Server Error` and log error when `authenticate` throws unhandled exception', async () => {
const mockResponse = httpServerMock.createLifecycleResponseFactory();
authenticate.mockRejectedValue(new Error('something went wrong'));
await authHandler(httpServerMock.createKibanaRequest(), mockResponse, mockAuthToolkit);
expect(mockResponse.internalError).toHaveBeenCalledTimes(1);
const [[error]] = mockResponse.internalError.mock.calls;
expect(error).toBeUndefined();
expect(mockAuthToolkit.authenticated).not.toHaveBeenCalled();
expect(mockAuthToolkit.redirected).not.toHaveBeenCalled();
expect(loggingSystemMock.collect(mockSetupAuthenticationParams.loggers).error)
.toMatchInlineSnapshot(`
Array [
Array [
[Error: something went wrong],
],
]
`);
});
it('rejects with original `badRequest` error when `authenticate` fails to authenticate user', async () => {
const mockResponse = httpServerMock.createLifecycleResponseFactory();
const esError = Boom.badRequest('some message');
authenticate.mockResolvedValue(AuthenticationResult.failed(esError));
await authHandler(httpServerMock.createKibanaRequest(), mockResponse, mockAuthToolkit);
expect(mockResponse.customError).toHaveBeenCalledTimes(1);
const [[response]] = mockResponse.customError.mock.calls;
expect(response.body).toBe(esError);
expect(mockAuthToolkit.authenticated).not.toHaveBeenCalled();
expect(mockAuthToolkit.redirected).not.toHaveBeenCalled();
});
it('includes `WWW-Authenticate` header if `authenticate` fails to authenticate user and provides challenges', async () => {
const mockResponse = httpServerMock.createLifecycleResponseFactory();
const originalError = Boom.unauthorized('some message');
originalError.output.headers['WWW-Authenticate'] = [
'Basic realm="Access to prod", charset="UTF-8"',
'Basic',
'Negotiate',
] as any;
authenticate.mockResolvedValue(
AuthenticationResult.failed(originalError, {
authResponseHeaders: { 'WWW-Authenticate': 'Negotiate' },
})
);
await authHandler(httpServerMock.createKibanaRequest(), mockResponse, mockAuthToolkit);
expect(mockResponse.customError).toHaveBeenCalledTimes(1);
const [[options]] = mockResponse.customError.mock.calls;
expect(options.body).toBe(originalError);
expect(options!.headers).toEqual({ 'WWW-Authenticate': 'Negotiate' });
expect(mockAuthToolkit.authenticated).not.toHaveBeenCalled();
expect(mockAuthToolkit.redirected).not.toHaveBeenCalled();
});
it('returns `notHandled` when authentication can not be handled', async () => {
const mockResponse = httpServerMock.createLifecycleResponseFactory();
authenticate.mockResolvedValue(AuthenticationResult.notHandled());
await authHandler(httpServerMock.createKibanaRequest(), mockResponse, mockAuthToolkit);
expect(mockAuthToolkit.notHandled).toHaveBeenCalledTimes(1);
expect(mockAuthToolkit.authenticated).not.toHaveBeenCalled();
expect(mockAuthToolkit.redirected).not.toHaveBeenCalled();
});
});
describe('getCurrentUser()', () => {
let getCurrentUser: (r: KibanaRequest) => AuthenticatedUser | null;
beforeEach(async () => {
getCurrentUser = (await setupAuthentication(mockSetupAuthenticationParams)).getCurrentUser;
});
it('returns `null` if Security is disabled', () => {
mockSetupAuthenticationParams.license.isEnabled.mockReturnValue(false);
expect(getCurrentUser(httpServerMock.createKibanaRequest())).toBe(null);
});
it('returns user from the auth state.', () => {
const mockUser = mockAuthenticatedUser();
const mockAuthGet = mockSetupAuthenticationParams.http.auth.get as jest.Mock;
mockAuthGet.mockReturnValue({ state: mockUser });
const mockRequest = httpServerMock.createKibanaRequest();
expect(getCurrentUser(mockRequest)).toBe(mockUser);
expect(mockAuthGet).toHaveBeenCalledTimes(1);
expect(mockAuthGet).toHaveBeenCalledWith(mockRequest);
});
it('returns null if auth state is not available.', () => {
const mockAuthGet = mockSetupAuthenticationParams.http.auth.get as jest.Mock;
mockAuthGet.mockReturnValue({});
const mockRequest = httpServerMock.createKibanaRequest();
expect(getCurrentUser(mockRequest)).toBeNull();
expect(mockAuthGet).toHaveBeenCalledTimes(1);
expect(mockAuthGet).toHaveBeenCalledWith(mockRequest);
});
});
describe('isAuthenticated()', () => {
let isAuthenticated: (r: KibanaRequest) => boolean;
beforeEach(async () => {
isAuthenticated = (await setupAuthentication(mockSetupAuthenticationParams)).isAuthenticated;
});
it('returns `true` if request is authenticated', () => {
const mockIsAuthenticated = mockSetupAuthenticationParams.http.auth
.isAuthenticated as jest.Mock;
mockIsAuthenticated.mockReturnValue(true);
const mockRequest = httpServerMock.createKibanaRequest();
expect(isAuthenticated(mockRequest)).toBe(true);
expect(mockIsAuthenticated).toHaveBeenCalledTimes(1);
expect(mockIsAuthenticated).toHaveBeenCalledWith(mockRequest);
});
it('returns `false` if request is not authenticated', () => {
const mockIsAuthenticated = mockSetupAuthenticationParams.http.auth
.isAuthenticated as jest.Mock;
mockIsAuthenticated.mockReturnValue(false);
const mockRequest = httpServerMock.createKibanaRequest();
expect(isAuthenticated(mockRequest)).toBe(false);
expect(mockIsAuthenticated).toHaveBeenCalledTimes(1);
expect(mockIsAuthenticated).toHaveBeenCalledWith(mockRequest);
});
});
describe('createAPIKey()', () => {
let createAPIKey: (
request: KibanaRequest,
params: CreateAPIKeyParams
) => Promise<CreateAPIKeyResult | null>;
beforeEach(async () => {
createAPIKey = (await setupAuthentication(mockSetupAuthenticationParams)).createAPIKey;
});
it('calls createAPIKey with given arguments', async () => {
const request = httpServerMock.createKibanaRequest();
const apiKeysInstance = jest.requireMock('./api_keys').APIKeys.mock.instances[0];
const params = {
name: 'my-key',
role_descriptors: {},
expiration: '1d',
};
apiKeysInstance.create.mockResolvedValueOnce({ success: true });
await expect(createAPIKey(request, params)).resolves.toEqual({
success: true,
});
expect(apiKeysInstance.create).toHaveBeenCalledWith(request, params);
});
});
describe('grantAPIKeyAsInternalUser()', () => {
let grantAPIKeyAsInternalUser: (
request: KibanaRequest,
params: CreateAPIKeyParams
) => Promise<CreateAPIKeyResult | null>;
beforeEach(async () => {
grantAPIKeyAsInternalUser = (await setupAuthentication(mockSetupAuthenticationParams))
.grantAPIKeyAsInternalUser;
});
it('calls grantAsInternalUser', async () => {
const request = httpServerMock.createKibanaRequest();
const apiKeysInstance = jest.requireMock('./api_keys').APIKeys.mock.instances[0];
apiKeysInstance.grantAsInternalUser.mockResolvedValueOnce({ api_key: 'foo' });
const createParams = { name: 'test_key', role_descriptors: {} };
await expect(grantAPIKeyAsInternalUser(request, createParams)).resolves.toEqual({
api_key: 'foo',
});
expect(apiKeysInstance.grantAsInternalUser).toHaveBeenCalledWith(request, createParams);
});
});
describe('invalidateAPIKey()', () => {
let invalidateAPIKey: (
request: KibanaRequest,
params: InvalidateAPIKeyParams
) => Promise<InvalidateAPIKeyResult | null>;
beforeEach(async () => {
invalidateAPIKey = (await setupAuthentication(mockSetupAuthenticationParams))
.invalidateAPIKey;
});
it('calls invalidateAPIKey with given arguments', async () => {
const request = httpServerMock.createKibanaRequest();
const apiKeysInstance = jest.requireMock('./api_keys').APIKeys.mock.instances[0];
const params = {
id: '123',
};
apiKeysInstance.invalidate.mockResolvedValueOnce({ success: true });
await expect(invalidateAPIKey(request, params)).resolves.toEqual({
success: true,
});
expect(apiKeysInstance.invalidate).toHaveBeenCalledWith(request, params);
});
});
describe('invalidateAPIKeyAsInternalUser()', () => {
let invalidateAPIKeyAsInternalUser: Authentication['invalidateAPIKeyAsInternalUser'];
beforeEach(async () => {
invalidateAPIKeyAsInternalUser = (await setupAuthentication(mockSetupAuthenticationParams))
.invalidateAPIKeyAsInternalUser;
});
it('calls invalidateAPIKeyAsInternalUser with given arguments', async () => {
const apiKeysInstance = jest.requireMock('./api_keys').APIKeys.mock.instances[0];
const params = {
id: '123',
};
apiKeysInstance.invalidateAsInternalUser.mockResolvedValueOnce({ success: true });
await expect(invalidateAPIKeyAsInternalUser(params)).resolves.toEqual({
success: true,
});
expect(apiKeysInstance.invalidateAsInternalUser).toHaveBeenCalledWith(params);
});
});
});

View file

@ -3,25 +3,13 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import type { PublicMethodsOf, UnwrapPromise } from '@kbn/utility-types';
import {
ILegacyClusterClient,
KibanaRequest,
LoggerFactory,
HttpServiceSetup,
} from '../../../../../src/core/server';
import { SecurityLicense } from '../../common/licensing';
import { AuthenticatedUser } from '../../common/model';
import { SecurityAuditLogger, AuditServiceSetup } from '../audit';
import { ConfigType } from '../config';
import { getErrorStatusCode } from '../errors';
import { SecurityFeatureUsageServiceStart } from '../feature_usage';
import { Session } from '../session_management';
import { Authenticator } from './authenticator';
import { APIKeys, CreateAPIKeyParams, InvalidateAPIKeyParams } from './api_keys';
export { canRedirectRequest } from './can_redirect_request';
export { Authenticator, ProviderLoginAttempt } from './authenticator';
export {
AuthenticationService,
AuthenticationServiceSetup,
AuthenticationServiceStart,
} from './authentication_service';
export { AuthenticationResult } from './authentication_result';
export { DeauthenticationResult } from './deauthentication_result';
export {
@ -33,149 +21,13 @@ export {
OIDCAuthenticationProvider,
} from './providers';
export {
BasicHTTPAuthorizationHeaderCredentials,
HTTPAuthorizationHeader,
} from './http_authentication';
export type {
CreateAPIKeyResult,
InvalidateAPIKeyResult,
CreateAPIKeyParams,
InvalidateAPIKeyParams,
GrantAPIKeyResult,
} from './api_keys';
export {
BasicHTTPAuthorizationHeaderCredentials,
HTTPAuthorizationHeader,
} from './http_authentication';
interface SetupAuthenticationParams {
legacyAuditLogger: SecurityAuditLogger;
audit: AuditServiceSetup;
getFeatureUsageService: () => SecurityFeatureUsageServiceStart;
http: HttpServiceSetup;
clusterClient: ILegacyClusterClient;
config: ConfigType;
license: SecurityLicense;
loggers: LoggerFactory;
session: PublicMethodsOf<Session>;
}
export type Authentication = UnwrapPromise<ReturnType<typeof setupAuthentication>>;
export async function setupAuthentication({
legacyAuditLogger: auditLogger,
audit,
getFeatureUsageService,
http,
clusterClient,
config,
license,
loggers,
session,
}: SetupAuthenticationParams) {
const authLogger = loggers.get('authentication');
/**
* Retrieves currently authenticated user associated with the specified request.
* @param request
*/
const getCurrentUser = (request: KibanaRequest) => {
if (!license.isEnabled()) {
return null;
}
return (http.auth.get(request).state ?? null) as AuthenticatedUser | null;
};
const authenticator = new Authenticator({
legacyAuditLogger: auditLogger,
audit,
loggers,
clusterClient,
basePath: http.basePath,
config: { authc: config.authc },
getCurrentUser,
getFeatureUsageService,
license,
session,
});
authLogger.debug('Successfully initialized authenticator.');
http.registerAuth(async (request, response, t) => {
// If security is disabled continue with no user credentials and delete the client cookie as well.
if (!license.isEnabled()) {
return t.authenticated();
}
let authenticationResult;
try {
authenticationResult = await authenticator.authenticate(request);
} catch (err) {
authLogger.error(err);
return response.internalError();
}
if (authenticationResult.succeeded()) {
return t.authenticated({
state: authenticationResult.user,
requestHeaders: authenticationResult.authHeaders,
responseHeaders: authenticationResult.authResponseHeaders,
});
}
if (authenticationResult.redirected()) {
// Some authentication mechanisms may require user to be redirected to another location to
// initiate or complete authentication flow. It can be Kibana own login page for basic
// authentication (username and password) or arbitrary external page managed by 3rd party
// Identity Provider for SSO authentication mechanisms. Authentication provider is the one who
// decides what location user should be redirected to.
return t.redirected({
location: authenticationResult.redirectURL!,
...(authenticationResult.authResponseHeaders || {}),
});
}
if (authenticationResult.failed()) {
authLogger.info(`Authentication attempt failed: ${authenticationResult.error!.message}`);
const error = authenticationResult.error!;
// proxy Elasticsearch "native" errors
const statusCode = getErrorStatusCode(error);
if (typeof statusCode === 'number') {
return response.customError({
body: error,
statusCode,
headers: authenticationResult.authResponseHeaders,
});
}
return response.unauthorized({
headers: authenticationResult.authResponseHeaders,
});
}
authLogger.debug('Could not handle authentication attempt');
return t.notHandled();
});
authLogger.debug('Successfully registered core authentication handler.');
const apiKeys = new APIKeys({
clusterClient,
logger: loggers.get('api-key'),
license,
});
return {
login: authenticator.login.bind(authenticator),
logout: authenticator.logout.bind(authenticator),
isProviderTypeEnabled: authenticator.isProviderTypeEnabled.bind(authenticator),
acknowledgeAccessAgreement: authenticator.acknowledgeAccessAgreement.bind(authenticator),
getCurrentUser,
areAPIKeysEnabled: () => apiKeys.areAPIKeysEnabled(),
createAPIKey: (request: KibanaRequest, params: CreateAPIKeyParams) =>
apiKeys.create(request, params),
grantAPIKeyAsInternalUser: (request: KibanaRequest, params: CreateAPIKeyParams) =>
apiKeys.grantAsInternalUser(request, params),
invalidateAPIKey: (request: KibanaRequest, params: InvalidateAPIKeyParams) =>
apiKeys.invalidate(request, params),
invalidateAPIKeyAsInternalUser: (params: InvalidateAPIKeyParams) =>
apiKeys.invalidateAsInternalUser(params),
isAuthenticated: (request: KibanaRequest) => http.auth.isAuthenticated(request),
};
}

View file

@ -196,63 +196,6 @@ export function elasticsearchClientPlugin(Client: any, config: unknown, componen
},
});
/**
* Creates an API key in Elasticsearch for the current user.
*
* @param {string} name A name for this API key
* @param {object} role_descriptors Role descriptors for this API key, if not
* provided then permissions of authenticated user are applied.
* @param {string} [expiration] Optional expiration for the API key being generated. If expiration
* is not provided then the API keys do not expire.
*
* @returns {{id: string, name: string, api_key: string, expiration?: number}}
*/
shield.createAPIKey = ca({
method: 'POST',
needBody: true,
url: {
fmt: '/_security/api_key',
},
});
/**
* Grants an API key in Elasticsearch for the current user.
*
* @param {string} type The type of grant, either "password" or "access_token"
* @param {string} username Required when using the "password" type
* @param {string} password Required when using the "password" type
* @param {string} access_token Required when using the "access_token" type
*
* @returns {{api_key: string}}
*/
shield.grantAPIKey = ca({
method: 'POST',
needBody: true,
url: {
fmt: '/_security/api_key/grant',
},
});
/**
* Invalidates an API key in Elasticsearch.
*
* @param {string} [id] An API key id.
* @param {string} [name] An API key name.
* @param {string} [realm_name] The name of an authentication realm.
* @param {string} [username] The username of a user.
*
* NOTE: While all parameters are optional, at least one of them is required.
*
* @returns {{invalidated_api_keys: string[], previously_invalidated_api_keys: string[], error_count: number, error_details?: object[]}}
*/
shield.invalidateAPIKey = ca({
method: 'DELETE',
needBody: true,
url: {
fmt: '/_security/api_key',
},
});
/**
* Gets an access token in exchange to the certificate chain for the target subject distinguished name.
*

View file

@ -4,28 +4,28 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { TypeOf } from '@kbn/config-schema';
import { RecursiveReadonly } from '@kbn/utility-types';
import {
import type { TypeOf } from '@kbn/config-schema';
import type { RecursiveReadonly } from '@kbn/utility-types';
import type {
PluginConfigDescriptor,
PluginInitializer,
PluginInitializerContext,
} from '../../../../src/core/server';
import { ConfigSchema } from './config';
import { Plugin, SecurityPluginSetup, PluginSetupDependencies } from './plugin';
import {
Plugin,
SecurityPluginSetup,
SecurityPluginStart,
PluginSetupDependencies,
} from './plugin';
// These exports are part of public Security plugin contract, any change in signature of exported
// functions or removal of exports should be considered as a breaking change.
export {
Authentication,
AuthenticationResult,
DeauthenticationResult,
export type {
CreateAPIKeyResult,
InvalidateAPIKeyParams,
InvalidateAPIKeyResult,
GrantAPIKeyResult,
SAMLLogin,
OIDCLogin,
} from './authentication';
export {
LegacyAuditLogger,
@ -35,8 +35,8 @@ export {
EventType,
EventOutcome,
} from './audit';
export { SecurityPluginSetup };
export { AuthenticatedUser } from '../common/model';
export type { SecurityPluginSetup, SecurityPluginStart };
export type { AuthenticatedUser } from '../common/model';
export const config: PluginConfigDescriptor<TypeOf<typeof ConfigSchema>> = {
schema: ConfigSchema,
@ -93,6 +93,6 @@ export const config: PluginConfigDescriptor<TypeOf<typeof ConfigSchema>> = {
};
export const plugin: PluginInitializer<
RecursiveReadonly<SecurityPluginSetup>,
void,
RecursiveReadonly<SecurityPluginStart>,
PluginSetupDependencies
> = (initializerContext: PluginInitializerContext) => new Plugin(initializerContext);

View file

@ -4,7 +4,8 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { authenticationMock } from './authentication/index.mock';
import type { ApiResponse } from '@elastic/elasticsearch';
import { authenticationServiceMock } from './authentication/authentication_service.mock';
import { authorizationMock } from './authorization/index.mock';
import { licenseMock } from '../common/licensing/index.mock';
import { auditServiceMock } from './audit/index.mock';
@ -13,7 +14,7 @@ function createSetupMock() {
const mockAuthz = authorizationMock.create();
return {
audit: auditServiceMock.create(),
authc: authenticationMock.create(),
authc: authenticationServiceMock.createSetup(),
authz: {
actions: mockAuthz.actions,
checkPrivilegesWithRequest: mockAuthz.checkPrivilegesWithRequest,
@ -25,6 +26,38 @@ function createSetupMock() {
};
}
function createStartMock() {
const mockAuthz = authorizationMock.create();
const mockAuthc = authenticationServiceMock.createStart();
return {
authc: {
apiKeys: mockAuthc.apiKeys,
getCurrentUser: mockAuthc.getCurrentUser,
},
authz: {
actions: mockAuthz.actions,
checkPrivilegesWithRequest: mockAuthz.checkPrivilegesWithRequest,
checkPrivilegesDynamicallyWithRequest: mockAuthz.checkPrivilegesDynamicallyWithRequest,
mode: mockAuthz.mode,
},
};
}
function createApiResponseMock<TResponse, TContext>(
apiResponse: Pick<ApiResponse<TResponse, TContext>, 'body'> &
Partial<Omit<ApiResponse<TResponse, TContext>, 'body'>>
): ApiResponse<TResponse, TContext> {
return {
statusCode: null,
headers: null,
warnings: null,
meta: {} as any,
...apiResponse,
};
}
export const securityMock = {
createSetup: createSetupMock,
createStart: createStartMock,
createApiResponse: createApiResponseMock,
};

View file

@ -8,17 +8,20 @@ import { of } from 'rxjs';
import { ByteSizeValue } from '@kbn/config-schema';
import { ILegacyCustomClusterClient } from '../../../../src/core/server';
import { ConfigSchema } from './config';
import { Plugin, PluginSetupDependencies } from './plugin';
import { Plugin, PluginSetupDependencies, PluginStartDependencies } from './plugin';
import { coreMock, elasticsearchServiceMock } from '../../../../src/core/server/mocks';
import { featuresPluginMock } from '../../features/server/mocks';
import { taskManagerMock } from '../../task_manager/server/mocks';
import { licensingMock } from '../../licensing/server/mocks';
describe('Security Plugin', () => {
let plugin: Plugin;
let mockCoreSetup: ReturnType<typeof coreMock.createSetup>;
let mockCoreStart: ReturnType<typeof coreMock.createStart>;
let mockClusterClient: jest.Mocked<ILegacyCustomClusterClient>;
let mockDependencies: PluginSetupDependencies;
let mockSetupDependencies: PluginSetupDependencies;
let mockStartDependencies: PluginStartDependencies;
beforeEach(() => {
plugin = new Plugin(
coreMock.createPluginInitializerContext(
@ -43,29 +46,34 @@ describe('Security Plugin', () => {
mockClusterClient = elasticsearchServiceMock.createLegacyCustomClusterClient();
mockCoreSetup.elasticsearch.legacy.createClient.mockReturnValue(mockClusterClient);
mockDependencies = ({
mockSetupDependencies = ({
licensing: { license$: of({}), featureUsage: { register: jest.fn() } },
features: featuresPluginMock.createSetup(),
taskManager: taskManagerMock.createSetup(),
} as unknown) as PluginSetupDependencies;
mockCoreStart = coreMock.createStart();
const mockFeaturesStart = featuresPluginMock.createStart();
mockFeaturesStart.getKibanaFeatures.mockReturnValue([]);
mockStartDependencies = {
features: mockFeaturesStart,
licensing: licensingMock.createStart(),
taskManager: taskManagerMock.createStart(),
};
});
describe('setup()', () => {
it('exposes proper contract', async () => {
await expect(plugin.setup(mockCoreSetup, mockDependencies)).resolves.toMatchInlineSnapshot(`
await expect(plugin.setup(mockCoreSetup, mockSetupDependencies)).resolves
.toMatchInlineSnapshot(`
Object {
"audit": Object {
"asScoped": [Function],
"getLogger": [Function],
},
"authc": Object {
"areAPIKeysEnabled": [Function],
"createAPIKey": [Function],
"getCurrentUser": [Function],
"grantAPIKeyAsInternalUser": [Function],
"invalidateAPIKey": [Function],
"invalidateAPIKeyAsInternalUser": [Function],
"isAuthenticated": [Function],
},
"authz": Object {
"actions": Actions {
@ -119,8 +127,58 @@ describe('Security Plugin', () => {
});
});
describe('start()', () => {
it('exposes proper contract', async () => {
await plugin.setup(mockCoreSetup, mockSetupDependencies);
expect(plugin.start(mockCoreStart, mockStartDependencies)).toMatchInlineSnapshot(`
Object {
"authc": Object {
"apiKeys": Object {
"areAPIKeysEnabled": [Function],
"create": [Function],
"grantAsInternalUser": [Function],
"invalidate": [Function],
"invalidateAsInternalUser": [Function],
},
"getCurrentUser": [Function],
},
"authz": Object {
"actions": Actions {
"alerting": AlertingActions {
"prefix": "alerting:version:",
},
"api": ApiActions {
"prefix": "api:version:",
},
"app": AppActions {
"prefix": "app:version:",
},
"login": "login:",
"savedObject": SavedObjectActions {
"prefix": "saved_object:version:",
},
"space": SpaceActions {
"prefix": "space:version:",
},
"ui": UIActions {
"prefix": "ui:version:",
},
"version": "version:version",
"versionNumber": "version",
},
"checkPrivilegesDynamicallyWithRequest": [Function],
"checkPrivilegesWithRequest": [Function],
"mode": Object {
"useRbacForRequest": [Function],
},
},
}
`);
});
});
describe('stop()', () => {
beforeEach(async () => await plugin.setup(mockCoreSetup, mockDependencies));
beforeEach(async () => await plugin.setup(mockCoreSetup, mockSetupDependencies));
it('close does not throw', async () => {
await plugin.stop();

View file

@ -7,7 +7,6 @@
import { combineLatest } from 'rxjs';
import { first, map } from 'rxjs/operators';
import { TypeOf } from '@kbn/config-schema';
import { deepFreeze } from '@kbn/std';
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { SecurityOssPluginSetup } from 'src/plugins/security_oss/server';
import {
@ -25,7 +24,11 @@ import {
import { LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server';
import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server';
import { Authentication, setupAuthentication } from './authentication';
import {
AuthenticationService,
AuthenticationServiceSetup,
AuthenticationServiceStart,
} from './authentication';
import { AuthorizationService, AuthorizationServiceSetup } from './authorization';
import { ConfigSchema, createConfig } from './config';
import { defineRoutes } from './routes';
@ -53,16 +56,13 @@ export type FeaturesService = Pick<
* Describes public Security plugin contract returned at the `setup` stage.
*/
export interface SecurityPluginSetup {
authc: Pick<
Authentication,
| 'isAuthenticated'
| 'getCurrentUser'
| 'areAPIKeysEnabled'
| 'createAPIKey'
| 'invalidateAPIKey'
| 'grantAPIKeyAsInternalUser'
| 'invalidateAPIKeyAsInternalUser'
>;
/**
* @deprecated Use `authc` methods from the `SecurityServiceStart` contract instead.
*/
authc: Pick<AuthenticationServiceSetup, 'getCurrentUser'>;
/**
* @deprecated Use `authz` methods from the `SecurityServiceStart` contract instead.
*/
authz: Pick<
AuthorizationServiceSetup,
'actions' | 'checkPrivilegesDynamicallyWithRequest' | 'checkPrivilegesWithRequest' | 'mode'
@ -71,6 +71,17 @@ export interface SecurityPluginSetup {
audit: AuditServiceSetup;
}
/**
* Describes public Security plugin contract returned at the `start` stage.
*/
export interface SecurityPluginStart {
authc: Pick<AuthenticationServiceStart, 'apiKeys' | 'getCurrentUser'>;
authz: Pick<
AuthorizationServiceSetup,
'actions' | 'checkPrivilegesDynamicallyWithRequest' | 'checkPrivilegesWithRequest' | 'mode'
>;
}
export interface PluginSetupDependencies {
features: FeaturesPluginSetup;
licensing: LicensingPluginSetup;
@ -93,7 +104,8 @@ export interface PluginStartDependencies {
export class Plugin {
private readonly logger: Logger;
private securityLicenseService?: SecurityLicenseService;
private authc?: Authentication;
private authenticationStart?: AuthenticationServiceStart;
private authorizationSetup?: AuthorizationServiceSetup;
private readonly featureUsageService = new SecurityFeatureUsageService();
private featureUsageServiceStart?: SecurityFeatureUsageServiceStart;
@ -112,6 +124,9 @@ export class Plugin {
private readonly sessionManagementService = new SessionManagementService(
this.initializerContext.logger.get('session')
);
private readonly authenticationService = new AuthenticationService(
this.initializerContext.logger.get('authentication')
);
constructor(private readonly initializerContext: PluginInitializerContext) {
this.logger = this.initializerContext.logger.get();
@ -179,7 +194,7 @@ export class Plugin {
logging: core.logging,
http: core.http,
getSpaceId: (request) => spaces?.spacesService.getSpaceId(request),
getCurrentUser: (request) => this.authc?.getCurrentUser(request),
getCurrentUser: (request) => authenticationSetup.getCurrentUser(request),
});
const legacyAuditLogger = new SecurityAuditLogger(audit.getLogger());
@ -191,7 +206,7 @@ export class Plugin {
taskManager,
});
this.authc = await setupAuthentication({
const authenticationSetup = this.authenticationService.setup({
legacyAuditLogger,
audit,
getFeatureUsageService: this.getFeatureUsageService,
@ -203,7 +218,7 @@ export class Plugin {
session,
});
const authz = this.authorizationService.setup({
this.authorizationSetup = this.authorizationService.setup({
http: core.http,
capabilities: core.capabilities,
getClusterClient: () =>
@ -215,19 +230,19 @@ export class Plugin {
buildNumber: this.initializerContext.env.packageInfo.buildNum,
getSpacesService: () => spaces?.spacesService,
features,
getCurrentUser: this.authc.getCurrentUser,
getCurrentUser: authenticationSetup.getCurrentUser,
});
setupSpacesClient({
spaces,
audit,
authz,
authz: this.authorizationSetup,
});
setupSavedObjects({
legacyAuditLogger,
audit,
authz,
authz: this.authorizationSetup,
savedObjects: core.savedObjects,
getSpacesService: () => spaces?.spacesService,
});
@ -238,36 +253,35 @@ export class Plugin {
httpResources: core.http.resources,
logger: this.initializerContext.logger.get('routes'),
config,
authc: this.authc,
authz,
authz: this.authorizationSetup,
license,
session,
getFeatures: () =>
startServicesPromise.then((services) => services.features.getKibanaFeatures()),
getFeatureUsageService: this.getFeatureUsageService,
getAuthenticationService: () => {
if (!this.authenticationStart) {
throw new Error('Authentication service is not started!');
}
return this.authenticationStart;
},
});
return deepFreeze<SecurityPluginSetup>({
return Object.freeze<SecurityPluginSetup>({
audit: {
asScoped: audit.asScoped,
getLogger: audit.getLogger,
},
authc: {
isAuthenticated: this.authc.isAuthenticated,
getCurrentUser: this.authc.getCurrentUser,
areAPIKeysEnabled: this.authc.areAPIKeysEnabled,
createAPIKey: this.authc.createAPIKey,
invalidateAPIKey: this.authc.invalidateAPIKey,
grantAPIKeyAsInternalUser: this.authc.grantAPIKeyAsInternalUser,
invalidateAPIKeyAsInternalUser: this.authc.invalidateAPIKeyAsInternalUser,
},
authc: { getCurrentUser: authenticationSetup.getCurrentUser },
authz: {
actions: authz.actions,
checkPrivilegesWithRequest: authz.checkPrivilegesWithRequest,
checkPrivilegesDynamicallyWithRequest: authz.checkPrivilegesDynamicallyWithRequest,
mode: authz.mode,
actions: this.authorizationSetup.actions,
checkPrivilegesWithRequest: this.authorizationSetup.checkPrivilegesWithRequest,
checkPrivilegesDynamicallyWithRequest: this.authorizationSetup
.checkPrivilegesDynamicallyWithRequest,
mode: this.authorizationSetup.mode,
},
license,
@ -281,13 +295,29 @@ export class Plugin {
featureUsage: licensing.featureUsage,
});
const clusterClient = core.elasticsearch.client;
const { watchOnlineStatus$ } = this.elasticsearchService.start();
this.sessionManagementService.start({ online$: watchOnlineStatus$(), taskManager });
this.authorizationService.start({
features,
clusterClient: core.elasticsearch.client,
online$: watchOnlineStatus$(),
this.authenticationStart = this.authenticationService.start({
http: core.http,
clusterClient,
});
this.authorizationService.start({ features, clusterClient, online$: watchOnlineStatus$() });
return Object.freeze<SecurityPluginStart>({
authc: {
apiKeys: this.authenticationStart.apiKeys,
getCurrentUser: this.authenticationStart.getCurrentUser,
},
authz: {
actions: this.authorizationSetup!.actions,
checkPrivilegesWithRequest: this.authorizationSetup!.checkPrivilegesWithRequest,
checkPrivilegesDynamicallyWithRequest: this.authorizationSetup!
.checkPrivilegesDynamicallyWithRequest,
mode: this.authorizationSetup!.mode,
},
});
}

View file

@ -5,6 +5,7 @@
*/
import Boom from '@hapi/boom';
import type { DeeplyMockedKeys } from '@kbn/utility-types/jest';
import {
kibanaResponseFactory,
RequestHandler,
@ -14,8 +15,9 @@ import {
import { httpServerMock } from '../../../../../../src/core/server/mocks';
import { routeDefinitionParamsMock } from '../index.mock';
import type { AuthenticationServiceStart } from '../../authentication';
import { defineEnabledApiKeysRoutes } from './enabled';
import { Authentication } from '../../authentication';
import { authenticationServiceMock } from '../../authentication/authentication_service.mock';
describe('API keys enabled', () => {
function getMockContext(
@ -27,10 +29,11 @@ describe('API keys enabled', () => {
}
let routeHandler: RequestHandler<any, any, any, any>;
let authc: jest.Mocked<Authentication>;
let authc: DeeplyMockedKeys<AuthenticationServiceStart>;
beforeEach(() => {
authc = authenticationServiceMock.createStart();
const mockRouteDefinitionParams = routeDefinitionParamsMock.create();
authc = mockRouteDefinitionParams.authc;
mockRouteDefinitionParams.getAuthenticationService.mockReturnValue(authc);
defineEnabledApiKeysRoutes(mockRouteDefinitionParams);
@ -56,7 +59,7 @@ describe('API keys enabled', () => {
test('returns error from cluster client', async () => {
const error = Boom.notAcceptable('test not acceptable message');
authc.areAPIKeysEnabled.mockRejectedValue(error);
authc.apiKeys.areAPIKeysEnabled.mockRejectedValue(error);
const response = await routeHandler(
getMockContext(),
@ -71,7 +74,7 @@ describe('API keys enabled', () => {
describe('success', () => {
test('returns true if API Keys are enabled', async () => {
authc.areAPIKeysEnabled.mockResolvedValue(true);
authc.apiKeys.areAPIKeysEnabled.mockResolvedValue(true);
const response = await routeHandler(
getMockContext(),
@ -84,7 +87,7 @@ describe('API keys enabled', () => {
});
test('returns false if API Keys are disabled', async () => {
authc.areAPIKeysEnabled.mockResolvedValue(false);
authc.apiKeys.areAPIKeysEnabled.mockResolvedValue(false);
const response = await routeHandler(
getMockContext(),

View file

@ -8,7 +8,10 @@ import { wrapIntoCustomErrorResponse } from '../../errors';
import { createLicensedRouteHandler } from '../licensed_route_handler';
import { RouteDefinitionParams } from '..';
export function defineEnabledApiKeysRoutes({ router, authc }: RouteDefinitionParams) {
export function defineEnabledApiKeysRoutes({
router,
getAuthenticationService,
}: RouteDefinitionParams) {
router.get(
{
path: '/internal/security/api_key/_enabled',
@ -16,7 +19,7 @@ export function defineEnabledApiKeysRoutes({ router, authc }: RouteDefinitionPar
},
createLicensedRouteHandler(async (context, request, response) => {
try {
const apiKeysEnabled = await authc.areAPIKeysEnabled();
const apiKeysEnabled = await getAuthenticationService().apiKeys.areAPIKeysEnabled();
return response.ok({ body: { apiKeysEnabled } });
} catch (error) {

View file

@ -11,6 +11,7 @@ import { kibanaResponseFactory } from '../../../../../../src/core/server';
import { coreMock, httpServerMock } from '../../../../../../src/core/server/mocks';
import { routeDefinitionParamsMock } from '../index.mock';
import { defineCheckPrivilegesRoutes } from './privileges';
import { authenticationServiceMock } from '../../authentication/authentication_service.mock';
interface TestOptions {
licenseCheckResult?: LicenseCheck;
@ -36,7 +37,9 @@ describe('Check API keys privileges', () => {
licensing: { license: { check: jest.fn().mockReturnValue(licenseCheckResult) } } as any,
};
mockRouteDefinitionParams.authc.areAPIKeysEnabled.mockResolvedValue(areAPIKeysEnabled);
const authc = authenticationServiceMock.createStart();
authc.apiKeys.areAPIKeysEnabled.mockResolvedValue(areAPIKeysEnabled);
mockRouteDefinitionParams.getAuthenticationService.mockReturnValue(authc);
if (apiResponse) {
mockContext.core.elasticsearch.client.asCurrentUser.security.hasPrivileges.mockImplementation(

View file

@ -8,7 +8,10 @@ import { wrapIntoCustomErrorResponse } from '../../errors';
import { createLicensedRouteHandler } from '../licensed_route_handler';
import { RouteDefinitionParams } from '..';
export function defineCheckPrivilegesRoutes({ router, authc }: RouteDefinitionParams) {
export function defineCheckPrivilegesRoutes({
router,
getAuthenticationService,
}: RouteDefinitionParams) {
router.get(
{
path: '/internal/security/api_key/privileges',
@ -37,7 +40,7 @@ export function defineCheckPrivilegesRoutes({ router, authc }: RouteDefinitionPa
}>({
body: { cluster: ['manage_security', 'manage_api_key', 'manage_own_api_key'] },
}),
authc.areAPIKeysEnabled(),
getAuthenticationService().apiKeys.areAPIKeysEnabled(),
]);
const isAdmin = manageSecurity || manageApiKey;

View file

@ -5,6 +5,7 @@
*/
import { Type } from '@kbn/config-schema';
import type { DeeplyMockedKeys } from '@kbn/utility-types/jest';
import {
IRouter,
kibanaResponseFactory,
@ -12,10 +13,10 @@ import {
RequestHandlerContext,
RouteConfig,
} from '../../../../../../src/core/server';
import { SecurityLicense, SecurityLicenseFeatures } from '../../../common/licensing';
import type { SecurityLicense, SecurityLicenseFeatures } from '../../../common/licensing';
import {
Authentication,
AuthenticationResult,
AuthenticationServiceStart,
DeauthenticationResult,
OIDCLogin,
SAMLLogin,
@ -25,17 +26,19 @@ import { defineCommonRoutes } from './common';
import { httpServerMock } from '../../../../../../src/core/server/mocks';
import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock';
import { routeDefinitionParamsMock } from '../index.mock';
import { authenticationServiceMock } from '../../authentication/authentication_service.mock';
describe('Common authentication routes', () => {
let router: jest.Mocked<IRouter>;
let authc: jest.Mocked<Authentication>;
let authc: DeeplyMockedKeys<AuthenticationServiceStart>;
let license: jest.Mocked<SecurityLicense>;
let mockContext: RequestHandlerContext;
beforeEach(() => {
const routeParamsMock = routeDefinitionParamsMock.create();
router = routeParamsMock.router;
authc = routeParamsMock.authc;
license = routeParamsMock.license;
authc = authenticationServiceMock.createStart();
routeParamsMock.getAuthenticationService.mockReturnValue(authc);
mockContext = ({
licensing: {

View file

@ -24,7 +24,7 @@ import { RouteDefinitionParams } from '..';
*/
export function defineCommonRoutes({
router,
authc,
getAuthenticationService,
basePath,
license,
logger,
@ -55,7 +55,7 @@ export function defineCommonRoutes({
}
try {
const deauthenticationResult = await authc.logout(request);
const deauthenticationResult = await getAuthenticationService().logout(request);
if (deauthenticationResult.failed()) {
return response.customError(wrapIntoCustomErrorResponse(deauthenticationResult.error));
}
@ -82,7 +82,7 @@ export function defineCommonRoutes({
);
}
return response.ok({ body: authc.getCurrentUser(request)! });
return response.ok({ body: getAuthenticationService().getCurrentUser(request)! });
})
);
}
@ -142,7 +142,7 @@ export function defineCommonRoutes({
const redirectURL = parseNext(currentURL, basePath.serverBasePath);
try {
const authenticationResult = await authc.login(request, {
const authenticationResult = await getAuthenticationService().login(request, {
provider: { name: providerName },
redirectURL,
value: getLoginAttemptForProviderType(providerType, redirectURL, params),
@ -178,7 +178,7 @@ export function defineCommonRoutes({
}
try {
await authc.acknowledgeAccessAgreement(request);
await getAuthenticationService().acknowledgeAccessAgreement(request);
} catch (err) {
logger.error(err);
return response.internalError();

View file

@ -12,11 +12,11 @@ import { RouteDefinitionParams } from '..';
export function defineAuthenticationRoutes(params: RouteDefinitionParams) {
defineCommonRoutes(params);
if (params.authc.isProviderTypeEnabled('saml')) {
if (params.config.authc.sortedProviders.some(({ type }) => type === 'saml')) {
defineSAMLRoutes(params);
}
if (params.authc.isProviderTypeEnabled('oidc')) {
if (params.config.authc.sortedProviders.some(({ type }) => type === 'oidc')) {
defineOIDCRoutes(params);
}
}

View file

@ -23,7 +23,7 @@ export function defineOIDCRoutes({
router,
httpResources,
logger,
authc,
getAuthenticationService,
basePath,
}: RouteDefinitionParams) {
// Generate two identical routes with new and deprecated URL and issue a warning if route with deprecated URL is ever used.
@ -241,7 +241,7 @@ export function defineOIDCRoutes({
try {
// We handle the fact that the user might get redirected to Kibana while already having a session
// Return an error notifying the user they are already logged in.
const authenticationResult = await authc.login(request, {
const authenticationResult = await getAuthenticationService().login(request, {
provider: { type: OIDCAuthenticationProvider.type },
value: loginAttempt,
});

View file

@ -5,21 +5,24 @@
*/
import { Type } from '@kbn/config-schema';
import { Authentication, AuthenticationResult, SAMLLogin } from '../../authentication';
import type { DeeplyMockedKeys } from '@kbn/utility-types/jest';
import { AuthenticationResult, AuthenticationServiceStart, SAMLLogin } from '../../authentication';
import { defineSAMLRoutes } from './saml';
import { IRouter, RequestHandler, RouteConfig } from '../../../../../../src/core/server';
import type { IRouter, RequestHandler, RouteConfig } from '../../../../../../src/core/server';
import { httpServerMock } from '../../../../../../src/core/server/mocks';
import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock';
import { routeDefinitionParamsMock } from '../index.mock';
import { authenticationServiceMock } from '../../authentication/authentication_service.mock';
describe('SAML authentication routes', () => {
let router: jest.Mocked<IRouter>;
let authc: jest.Mocked<Authentication>;
let authc: DeeplyMockedKeys<AuthenticationServiceStart>;
beforeEach(() => {
const routeParamsMock = routeDefinitionParamsMock.create();
router = routeParamsMock.router;
authc = routeParamsMock.authc;
authc = authenticationServiceMock.createStart();
routeParamsMock.getAuthenticationService.mockReturnValue(authc);
defineSAMLRoutes(routeParamsMock);
});

View file

@ -12,7 +12,11 @@ import { RouteDefinitionParams } from '..';
/**
* Defines routes required for SAML authentication.
*/
export function defineSAMLRoutes({ router, logger, authc }: RouteDefinitionParams) {
export function defineSAMLRoutes({
router,
logger,
getAuthenticationService,
}: RouteDefinitionParams) {
router.post(
{
path: '/api/security/saml/callback',
@ -27,7 +31,7 @@ export function defineSAMLRoutes({ router, logger, authc }: RouteDefinitionParam
async (context, request, response) => {
try {
// When authenticating using SAML we _expect_ to redirect to the Kibana target location.
const authenticationResult = await authc.login(request, {
const authenticationResult = await getAuthenticationService().login(request, {
provider: { type: SAMLAuthenticationProvider.type },
value: {
type: SAMLLogin.LoginWithSAMLResponse,

View file

@ -4,18 +4,18 @@
* you may not use this file except in compliance with the Elastic License.
*/
import type { DeeplyMockedKeys } from '@kbn/utility-types/jest';
import {
httpServiceMock,
loggingSystemMock,
httpResourcesMock,
} from '../../../../../src/core/server/mocks';
import { authenticationMock } from '../authentication/index.mock';
import { authorizationMock } from '../authorization/index.mock';
import { ConfigSchema, createConfig } from '../config';
import { licenseMock } from '../../common/licensing/index.mock';
import { authenticationServiceMock } from '../authentication/authentication_service.mock';
import { sessionMock } from '../session_management/session.mock';
import { RouteDefinitionParams } from '.';
import { DeeplyMockedKeys } from '@kbn/utility-types/jest';
import type { RouteDefinitionParams } from '.';
export const routeDefinitionParamsMock = {
create: (config: Record<string, unknown> = {}) =>
@ -27,12 +27,12 @@ export const routeDefinitionParamsMock = {
config: createConfig(ConfigSchema.validate(config), loggingSystemMock.create().get(), {
isTLSEnabled: false,
}),
authc: authenticationMock.create(),
authz: authorizationMock.create(),
license: licenseMock.create(),
httpResources: httpResourcesMock.createRegistrar(),
getFeatures: jest.fn(),
getFeatureUsageService: jest.fn(),
session: sessionMock.create(),
getAuthenticationService: jest.fn().mockReturnValue(authenticationServiceMock.createStart()),
} as unknown) as DeeplyMockedKeys<RouteDefinitionParams>),
};

View file

@ -4,12 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import type { PublicMethodsOf } from '@kbn/utility-types';
import { KibanaFeature } from '../../../features/server';
import { HttpResources, IBasePath, IRouter, Logger } from '../../../../../src/core/server';
import { SecurityLicense } from '../../common/licensing';
import { Authentication } from '../authentication';
import { AuthorizationServiceSetup } from '../authorization';
import { ConfigType } from '../config';
import type { KibanaFeature } from '../../../features/server';
import type { HttpResources, IBasePath, IRouter, Logger } from '../../../../../src/core/server';
import type { SecurityLicense } from '../../common/licensing';
import type { AuthenticationServiceStart } from '../authentication';
import type { AuthorizationServiceSetup } from '../authorization';
import type { ConfigType } from '../config';
import type { SecurityFeatureUsageServiceStart } from '../feature_usage';
import type { Session } from '../session_management';
import { defineAuthenticationRoutes } from './authentication';
import { defineAuthorizationRoutes } from './authorization';
@ -19,8 +21,6 @@ import { defineUsersRoutes } from './users';
import { defineRoleMappingRoutes } from './role_mapping';
import { defineSessionManagementRoutes } from './session_management';
import { defineViewRoutes } from './views';
import { SecurityFeatureUsageServiceStart } from '../feature_usage';
import { Session } from '../session_management';
/**
* Describes parameters used to define HTTP routes.
@ -31,12 +31,12 @@ export interface RouteDefinitionParams {
httpResources: HttpResources;
logger: Logger;
config: ConfigType;
authc: Authentication;
authz: AuthorizationServiceSetup;
session: PublicMethodsOf<Session>;
license: SecurityLicense;
getFeatures: () => Promise<KibanaFeature[]>;
getFeatureUsageService: () => SecurityFeatureUsageServiceStart;
getAuthenticationService: () => AuthenticationServiceStart;
}
export function defineRoutes(params: RouteDefinitionParams) {

View file

@ -16,7 +16,7 @@ import {
RequestHandlerContext,
RouteConfig,
} from '../../../../../../src/core/server';
import { Authentication, AuthenticationResult } from '../../authentication';
import { AuthenticationResult, AuthenticationServiceStart } from '../../authentication';
import { Session } from '../../session_management';
import { defineChangeUserPasswordRoutes } from './change_password';
@ -24,10 +24,11 @@ import { coreMock, httpServerMock } from '../../../../../../src/core/server/mock
import { mockAuthenticatedUser } from '../../../common/model/authenticated_user.mock';
import { sessionMock } from '../../session_management/session.mock';
import { routeDefinitionParamsMock } from '../index.mock';
import { authenticationServiceMock } from '../../authentication/authentication_service.mock';
describe('Change password', () => {
let router: jest.Mocked<IRouter>;
let authc: jest.Mocked<Authentication>;
let authc: DeeplyMockedKeys<AuthenticationServiceStart>;
let session: jest.Mocked<PublicMethodsOf<Session>>;
let routeHandler: RequestHandler<any, any, any>;
let routeConfig: RouteConfig<any, any, any, any>;
@ -48,8 +49,9 @@ describe('Change password', () => {
beforeEach(() => {
const routeParamsMock = routeDefinitionParamsMock.create();
router = routeParamsMock.router;
authc = routeParamsMock.authc;
session = routeParamsMock.session;
authc = authenticationServiceMock.createStart();
routeParamsMock.getAuthenticationService.mockReturnValue(authc);
authc.getCurrentUser.mockReturnValue(mockAuthenticatedUser(mockAuthenticatedUser()));
authc.login.mockResolvedValue(AuthenticationResult.succeeded(mockAuthenticatedUser()));

View file

@ -14,7 +14,11 @@ import {
} from '../../authentication';
import { RouteDefinitionParams } from '..';
export function defineChangeUserPasswordRoutes({ authc, session, router }: RouteDefinitionParams) {
export function defineChangeUserPasswordRoutes({
getAuthenticationService,
session,
router,
}: RouteDefinitionParams) {
router.post(
{
path: '/internal/security/users/{username}/password',
@ -30,7 +34,7 @@ export function defineChangeUserPasswordRoutes({ authc, session, router }: Route
const { username } = request.params;
const { password: currentPassword, newPassword } = request.body;
const currentUser = authc.getCurrentUser(request);
const currentUser = getAuthenticationService().getCurrentUser(request);
const isUserChangingOwnPassword =
currentUser && currentUser.username === username && canUserChangePassword(currentUser);
const currentSession = isUserChangingOwnPassword ? await session.get(request) : null;
@ -74,7 +78,7 @@ export function defineChangeUserPasswordRoutes({ authc, session, router }: Route
// session and in such cases we shouldn't create a new one.
if (isUserChangingOwnPassword && currentSession) {
try {
const authenticationResult = await authc.login(request, {
const authenticationResult = await getAuthenticationService().login(request, {
provider: { name: currentSession.provider.name },
value: { username, password: newPassword },
});

View file

@ -10,10 +10,9 @@ import { routeDefinitionParamsMock } from '../index.mock';
describe('View routes', () => {
it('does not register Login routes if both `basic` and `token` providers are disabled', () => {
const routeParamsMock = routeDefinitionParamsMock.create();
routeParamsMock.authc.isProviderTypeEnabled.mockImplementation(
(provider) => provider !== 'basic' && provider !== 'token'
);
const routeParamsMock = routeDefinitionParamsMock.create({
authc: { providers: { pki: { pki1: { order: 0 } } } },
});
defineViewRoutes(routeParamsMock);
@ -36,10 +35,9 @@ describe('View routes', () => {
});
it('registers Login routes if `basic` provider is enabled', () => {
const routeParamsMock = routeDefinitionParamsMock.create();
routeParamsMock.authc.isProviderTypeEnabled.mockImplementation(
(provider) => provider !== 'token'
);
const routeParamsMock = routeDefinitionParamsMock.create({
authc: { providers: { basic: { basic1: { order: 0 } } } },
});
defineViewRoutes(routeParamsMock);
@ -64,10 +62,9 @@ describe('View routes', () => {
});
it('registers Login routes if `token` provider is enabled', () => {
const routeParamsMock = routeDefinitionParamsMock.create();
routeParamsMock.authc.isProviderTypeEnabled.mockImplementation(
(provider) => provider !== 'basic'
);
const routeParamsMock = routeDefinitionParamsMock.create({
authc: { providers: { token: { token1: { order: 0 } } } },
});
defineViewRoutes(routeParamsMock);
@ -93,9 +90,8 @@ 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 } },
authc: { selector: { enabled: true }, providers: { pki: { pki1: { order: 0 } } } },
});
routeParamsMock.authc.isProviderTypeEnabled.mockReturnValue(false);
defineViewRoutes(routeParamsMock);

View file

@ -16,8 +16,7 @@ import { RouteDefinitionParams } from '..';
export function defineViewRoutes(params: RouteDefinitionParams) {
if (
params.config.authc.selector.enabled ||
params.authc.isProviderTypeEnabled('basic') ||
params.authc.isProviderTypeEnabled('token')
params.config.authc.sortedProviders.some(({ type }) => type === 'basic' || type === 'token')
) {
defineLoginRoutes(params);
}

View file

@ -129,10 +129,11 @@ export const getDeleteAsPostBulkRequest = () =>
body: [{ rule_id: 'rule-1' }],
});
export const getPrivilegeRequest = () =>
export const getPrivilegeRequest = (options: { auth?: { isAuthenticated: boolean } } = {}) =>
requestMock.create({
method: 'get',
path: DETECTION_ENGINE_PRIVILEGES_URL,
...options,
});
export const addPrepackagedRulesRequest = () =>

View file

@ -4,7 +4,6 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { securityMock } from '../../../../../../security/server/mocks';
import { readPrivilegesRoute } from './read_privileges_route';
import { serverMock, requestContextMock } from '../__mocks__';
import { getPrivilegeRequest, getMockPrivilegesResult } from '../__mocks__/request_responses';
@ -12,26 +11,29 @@ import { getPrivilegeRequest, getMockPrivilegesResult } from '../__mocks__/reque
describe('read_privileges route', () => {
let server: ReturnType<typeof serverMock.create>;
let { clients, context } = requestContextMock.createTools();
let mockSecurity: ReturnType<typeof securityMock.createSetup>;
beforeEach(() => {
server = serverMock.create();
({ clients, context } = requestContextMock.createTools());
mockSecurity = securityMock.createSetup();
mockSecurity.authc.isAuthenticated.mockReturnValue(false);
clients.clusterClient.callAsCurrentUser.mockResolvedValue(getMockPrivilegesResult());
readPrivilegesRoute(server.router, mockSecurity, false);
readPrivilegesRoute(server.router, false);
});
describe('normal status codes', () => {
test('returns 200 when doing a normal request', async () => {
const response = await server.inject(getPrivilegeRequest(), context);
const response = await server.inject(
getPrivilegeRequest({ auth: { isAuthenticated: false } }),
context
);
expect(response.status).toEqual(200);
});
test('returns the payload when doing a normal request', async () => {
const response = await server.inject(getPrivilegeRequest(), context);
const response = await server.inject(
getPrivilegeRequest({ auth: { isAuthenticated: false } }),
context
);
const expectedBody = {
...getMockPrivilegesResult(),
is_authenticated: false,
@ -42,14 +44,16 @@ describe('read_privileges route', () => {
});
test('is authenticated when security says so', async () => {
mockSecurity.authc.isAuthenticated.mockReturnValue(true);
const expectedBody = {
...getMockPrivilegesResult(),
is_authenticated: true,
has_encryption_key: true,
};
const response = await server.inject(getPrivilegeRequest(), context);
const response = await server.inject(
getPrivilegeRequest({ auth: { isAuthenticated: true } }),
context
);
expect(response.status).toEqual(200);
expect(response.body).toEqual(expectedBody);
});
@ -58,38 +62,22 @@ describe('read_privileges route', () => {
clients.clusterClient.callAsCurrentUser.mockImplementation(() => {
throw new Error('Test error');
});
const response = await server.inject(getPrivilegeRequest(), context);
const response = await server.inject(
getPrivilegeRequest({ auth: { isAuthenticated: false } }),
context
);
expect(response.status).toEqual(500);
expect(response.body).toEqual({ message: 'Test error', status_code: 500 });
});
it('returns 404 if siem client is unavailable', async () => {
const { securitySolution, ...contextWithoutSecuritySolution } = context;
const response = await server.inject(getPrivilegeRequest(), contextWithoutSecuritySolution);
const response = await server.inject(
getPrivilegeRequest({ auth: { isAuthenticated: false } }),
contextWithoutSecuritySolution
);
expect(response.status).toEqual(404);
expect(response.body).toEqual({ message: 'Not Found', status_code: 404 });
});
});
describe('when security plugin is disabled', () => {
beforeEach(() => {
server = serverMock.create();
({ clients, context } = requestContextMock.createTools());
clients.clusterClient.callAsCurrentUser.mockResolvedValue(getMockPrivilegesResult());
readPrivilegesRoute(server.router, undefined, false);
});
it('returns unauthenticated', async () => {
const expectedBody = {
...getMockPrivilegesResult(),
is_authenticated: false,
has_encryption_key: true,
};
const response = await server.inject(getPrivilegeRequest(), context);
expect(response.status).toEqual(200);
expect(response.body).toEqual(expectedBody);
});
});
});

View file

@ -8,15 +8,10 @@ import { merge } from 'lodash/fp';
import { IRouter } from '../../../../../../../../src/core/server';
import { DETECTION_ENGINE_PRIVILEGES_URL } from '../../../../../common/constants';
import { SetupPlugins } from '../../../../plugin';
import { buildSiemResponse, transformError } from '../utils';
import { readPrivileges } from '../../privileges/read_privileges';
export const readPrivilegesRoute = (
router: IRouter,
security: SetupPlugins['security'],
usingEphemeralEncryptionKey: boolean
) => {
export const readPrivilegesRoute = (router: IRouter, usingEphemeralEncryptionKey: boolean) => {
router.get(
{
path: DETECTION_ENGINE_PRIVILEGES_URL,
@ -39,7 +34,7 @@ export const readPrivilegesRoute = (
const index = siemClient.getSignalsIndex();
const clusterPrivileges = await readPrivileges(clusterClient.callAsCurrentUser, index);
const privileges = merge(clusterPrivileges, {
is_authenticated: security?.authc.isAuthenticated(request) ?? false,
is_authenticated: request.auth.isAuthenticated ?? false,
has_encryption_key: !usingEphemeralEncryptionKey,
});

View file

@ -93,5 +93,5 @@ export const initRoutes = (
readTagsRoute(router);
// Privileges API to get the generic user privileges
readPrivilegesRoute(router, security, usingEphemeralEncryptionKey);
readPrivilegesRoute(router, usingEphemeralEncryptionKey);
};

View file

@ -12,26 +12,23 @@ import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../..
import { defineAlertTypes } from './alert_types';
import { defineActionTypes } from './action_types';
import { defineRoutes } from './routes';
import { SpacesPluginSetup } from '../../../../../../../plugins/spaces/server';
import { SecurityPluginSetup } from '../../../../../../../plugins/security/server';
import { SpacesPluginStart } from '../../../../../../../plugins/spaces/server';
import { SecurityPluginStart } from '../../../../../../../plugins/security/server';
export interface FixtureSetupDeps {
features: FeaturesPluginSetup;
actions: ActionsPluginSetup;
alerts: AlertingPluginSetup;
spaces?: SpacesPluginSetup;
security?: SecurityPluginSetup;
}
export interface FixtureStartDeps {
encryptedSavedObjects: EncryptedSavedObjectsPluginStart;
security?: SecurityPluginStart;
spaces?: SpacesPluginStart;
}
export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, FixtureStartDeps> {
public setup(
core: CoreSetup<FixtureStartDeps>,
{ features, actions, alerts, spaces, security }: FixtureSetupDeps
) {
public setup(core: CoreSetup<FixtureStartDeps>, { features, actions, alerts }: FixtureSetupDeps) {
features.registerKibanaFeature({
id: 'alertsFixture',
name: 'Alerts',
@ -108,7 +105,7 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
defineActionTypes(core, { actions });
defineAlertTypes(core, { alerts });
defineRoutes(core, { spaces, security });
defineRoutes(core);
}
public start() {}

View file

@ -14,12 +14,9 @@ import { schema } from '@kbn/config-schema';
import { InvalidatePendingApiKey } from '../../../../../../../plugins/alerts/server/types';
import { RawAlert } from '../../../../../../../plugins/alerts/server/types';
import { TaskInstance } from '../../../../../../../plugins/task_manager/server';
import { FixtureSetupDeps, FixtureStartDeps } from './plugin';
import { FixtureStartDeps } from './plugin';
export function defineRoutes(
core: CoreSetup<FixtureStartDeps>,
{ spaces, security }: Partial<FixtureSetupDeps>
) {
export function defineRoutes(core: CoreSetup<FixtureStartDeps>) {
const router = core.http.createRouter();
router.put(
{
@ -40,13 +37,16 @@ export function defineRoutes(
): Promise<IKibanaResponse<any>> => {
const { id } = req.params;
const [
{ savedObjects },
{ encryptedSavedObjects, security, spaces },
] = await core.getStartServices();
if (!security) {
return res.ok({
body: {},
});
}
const [{ savedObjects }, { encryptedSavedObjects }] = await core.getStartServices();
const encryptedSavedObjectsWithAlerts = await encryptedSavedObjects.getClient({
includedHiddenTypes: ['alert'],
});
@ -70,7 +70,7 @@ export function defineRoutes(
// Create an API key using the new grant API - in this case the Kibana system user is creating the
// API key for the user, instead of having the user create it themselves, which requires api_key
// privileges
const createAPIKeyResult = await security.authc.grantAPIKeyAsInternalUser(req, {
const createAPIKeyResult = await security.authc.apiKeys.grantAsInternalUser(req, {
name: `alert:migrated-to-7.10:${user.username}`,
role_descriptors: {},
});