mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Migrate API keys functionality to a new Elasticsearch client. (#85029)
This commit is contained in:
parent
8b5c68ab63
commit
88e61a6651
58 changed files with 1225 additions and 1158 deletions
|
@ -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(
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -9,6 +9,7 @@ export {
|
|||
defaultIngestErrorHandler,
|
||||
ingestErrorToResponseOptions,
|
||||
isLegacyESClientError,
|
||||
isESClientError,
|
||||
} from './handlers';
|
||||
|
||||
export class IngestManagerError extends Error {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}`);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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(),
|
||||
}),
|
||||
};
|
|
@ -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',
|
||||
},
|
|
@ -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}`);
|
|
@ -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';
|
|
@ -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(),
|
||||
}),
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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;
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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>;
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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(),
|
||||
}),
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>),
|
||||
};
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()));
|
||||
|
|
|
@ -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 },
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 = () =>
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
||||
|
|
|
@ -93,5 +93,5 @@ export const initRoutes = (
|
|||
readTagsRoute(router);
|
||||
|
||||
// Privileges API to get the generic user privileges
|
||||
readPrivilegesRoute(router, security, usingEphemeralEncryptionKey);
|
||||
readPrivilegesRoute(router, usingEphemeralEncryptionKey);
|
||||
};
|
||||
|
|
|
@ -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() {}
|
||||
|
|
|
@ -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: {},
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue