[Security in Core] Exposes apiKeys from core.security.authc (#186910)

## Summary
Expose `apiKeys` as a service under `core.security.authc.apiKeys`.

Closes https://github.com/elastic/kibana/issues/184764

### Details
PR introduces a new API Keys Service which is accessible under the
`authc` namespace in core.security. The service exposes the public API
that was already available on the server-side in the security plugin.

The service is initialized and registered with core using the
`delegate_api` - allowing access to the service within the core plugin
without the need for the `security` plugin.

Note: I had to move quite a few types/functions around to prevent
cyclical dependencies.

### Plugins and the APIs that use the current `apiKeys` function from
the security plugin
<details>
<summary> Expand for table with details </summary>

| Plugin | File | API used | Can be migrated |
|--------|--------|--------|--------|
| alerting | x-pack/plugins/alerting/plugin/server.ts |
areApiKeysEnabled() |  |
| | x-pack/plugins/alerting/server/rules_client_factory.ts |
grantAsInternalUser() |  |
| | x-pack/plugins/alerting/server/task.ts | invalidatedAsInternalUser()
|  |
| enterprise_search |
x-pack/plugins/enterprise_search/server/routes/enterprise_search/api_keys
| create() |  |
| |
x-pack/plugins/enterprise_search/server/lib/indices/create_api_key.ts |
create() |  |
| fleet | x-pack/plugins/fleet/server/routes/setup/handlers.ts |
areApiKeysEnabled() |  |
| | x-pack/plugins/fleet/server/services/api_keys/security |
invalidateAsInternalUser() |  |
| | x-pack/plugins/fleet/server/services/api_keys/transform_api_keys.ts
| grantAsInternalUser() |  |
| |
x-pack/plugins/fleet/server/services/setup/fleet_server_policies_enrollment_keys.ts
| areApiKeysEnabled() |  |
| |
x-pack/plugins/fleet/server/services/setup/fleet_server_policies_enrollment_keys.ts
| areAPIKeysEnabled() |  |
| |
x-pack/plugins/observability_solution/apm/server/routes/agent_keys/get_agent_keys_privileges.ts
| areAPIKeysEnabled() |  |
| observability_solution |
x-pack/plugins/observability_solution/entity_manager/server/lib/auth/api_key/api_key.ts
| areAPIKeysEnabled |  |
| | | validate |  |
| | | grantAsInternalUser |  |
| |
x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/disable.ts
| invalidateAsInternalUser |  |
| |
x-pack/plugins/observability_solution/entity_manager/server/routes/enablement/enable.ts
| invalidateAsInternalUser |  |
| |
x-pack/plugins/observability_solution/observability_onboarding/server/routes/flow/route.ts
| create |  |
| |
x-pack/plugins/observability_solution/synthetics/server/routes/synthetics_service/enablement.ts
| invalidateAsInternalUser |  |
| |
x-pack/plugins/observability_solution/synthetics/server/synthetics_service/get_api_key.ts
| validate |  |
| | | areAPIKeysEnabled |  |
| | | grantAsInternalUser |  |
| | | create |  |
| serverless_search |
x-pack/plugins/serverless_search/server/routes/api_key_routes.ts |
create |  |
| |
x-pack/plugins/transform/server/routes/api/reauthorize_transforms/route_handler_factory.ts
| grantAsInternalUser |  |
| |
x-pack/plugins/upgrade_assistant/server/lib/reindexing/credential_store.ts
| grantAsInternalUser |  |
| | | invalidateAsInternalUser |  |
| | | areAPIKeysEnabled() |  |
</details>

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Sid 2024-07-09 15:43:17 +02:00 committed by GitHub
parent f484acad11
commit ff9a48edbe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
31 changed files with 644 additions and 269 deletions

View file

@ -26,6 +26,16 @@ export class CoreSecurityRouteHandlerContext implements SecurityRequestHandlerCo
if (this.#authc == null) {
this.#authc = {
getCurrentUser: () => this.securityStart.authc.getCurrentUser(this.request),
apiKeys: {
areAPIKeysEnabled: () => this.securityStart.authc.apiKeys.areAPIKeysEnabled(),
create: (createParams) =>
this.securityStart.authc.apiKeys.create(this.request, createParams),
update: (updateParams) =>
this.securityStart.authc.apiKeys.update(this.request, updateParams),
validate: (apiKeyParams) => this.securityStart.authc.apiKeys.validate(apiKeyParams),
invalidate: (apiKeyParams) =>
this.securityStart.authc.apiKeys.invalidate(this.request, apiKeyParams),
},
};
}
return this.#authc;

View file

@ -15,6 +15,16 @@ describe('convertSecurityApi', () => {
const source: CoreSecurityDelegateContract = {
authc: {
getCurrentUser: jest.fn(),
apiKeys: {
areAPIKeysEnabled: jest.fn(),
areCrossClusterAPIKeysEnabled: jest.fn(),
validate: jest.fn(),
invalidate: jest.fn(),
invalidateAsInternalUser: jest.fn(),
grantAsInternalUser: jest.fn(),
create: jest.fn(),
update: jest.fn(),
},
},
audit: {
asScoped: jest.fn().mockReturnValue(createAuditLoggerMock.create()),
@ -23,6 +33,7 @@ describe('convertSecurityApi', () => {
};
const output = convertSecurityApi(source);
expect(output.authc.getCurrentUser).toBe(source.authc.getCurrentUser);
expect(output.authc.apiKeys).toBe(source.authc.apiKeys);
expect(output.audit.asScoped).toBe(source.audit.asScoped);
expect(output.audit.withoutRequest).toBe(source.audit.withoutRequest);
});

View file

@ -23,6 +23,15 @@ describe('getDefaultSecurityImplementation', () => {
});
});
describe('authc.apiKeys', () => {
it('returns stub object', async () => {
const { apiKeys } = implementation.authc;
const areAPIKeysEnabled = await apiKeys.areAPIKeysEnabled();
expect(areAPIKeysEnabled).toBe(false);
});
});
describe('audit.asScoped', () => {
it('returns null', async () => {
const logger = implementation.audit.asScoped({} as any);

View file

@ -8,10 +8,23 @@
import type { CoreSecurityDelegateContract } from '@kbn/core-security-server';
const API_KEYS_DISABLED_ERROR = new Error('API keys are disabled');
const REJECT_WHEN_API_KEYS_DISABLED = () => Promise.reject(API_KEYS_DISABLED_ERROR);
export const getDefaultSecurityImplementation = (): CoreSecurityDelegateContract => {
return {
authc: {
getCurrentUser: () => null,
apiKeys: {
areAPIKeysEnabled: () => Promise.resolve(false),
areCrossClusterAPIKeysEnabled: () => Promise.resolve(false),
create: REJECT_WHEN_API_KEYS_DISABLED,
update: REJECT_WHEN_API_KEYS_DISABLED,
grantAsInternalUser: REJECT_WHEN_API_KEYS_DISABLED,
validate: REJECT_WHEN_API_KEYS_DISABLED,
invalidate: REJECT_WHEN_API_KEYS_DISABLED,
invalidateAsInternalUser: REJECT_WHEN_API_KEYS_DISABLED,
},
},
audit: {
asScoped: () => {

View file

@ -9,3 +9,4 @@
export { securityServiceMock } from './src/security_service.mock';
export type { InternalSecurityStartMock, SecurityStartMock } from './src/security_service.mock';
export { auditLoggerMock } from './src/audit.mock';
export { apiKeysMock } from './src/api_keys.mock';

View file

@ -1,16 +1,15 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { PublicMethodsOf } from '@kbn/utility-types';
import type { APIKeys } from './api_keys';
import type { APIKeysService } from '@kbn/core-security-server';
export const apiKeysMock = {
create: (): jest.Mocked<PublicMethodsOf<APIKeys>> => ({
create: (): jest.MockedObjectDeep<APIKeysService> => ({
areAPIKeysEnabled: jest.fn(),
areCrossClusterAPIKeysEnabled: jest.fn(),
create: jest.fn(),

View file

@ -15,6 +15,7 @@ import type {
InternalSecurityServiceSetup,
InternalSecurityServiceStart,
} from '@kbn/core-security-server-internal';
import { apiKeysMock } from './api_keys.mock';
import { auditServiceMock, type MockedAuditService } from './audit.mock';
import { mockAuthenticatedUser, MockAuthenticatedUserProps } from '@kbn/core-security-common/mocks';
@ -35,6 +36,7 @@ const createStartMock = (): SecurityStartMock => {
const mock = {
authc: {
getCurrentUser: jest.fn(),
apiKeys: apiKeysMock.create(),
},
audit: auditServiceMock.create(),
};
@ -61,6 +63,7 @@ const createInternalStartMock = (): InternalSecurityStartMock => {
const mock = {
authc: {
getCurrentUser: jest.fn(),
apiKeys: apiKeysMock.create(),
},
audit: auditServiceMock.create(),
};
@ -82,6 +85,13 @@ const createRequestHandlerContextMock = () => {
const mock: jest.MockedObjectDeep<SecurityRequestHandlerContext> = {
authc: {
getCurrentUser: jest.fn(),
apiKeys: {
areAPIKeysEnabled: jest.fn(),
create: jest.fn(),
update: jest.fn(),
validate: jest.fn(),
invalidate: jest.fn(),
},
},
audit: {
logger: {

View file

@ -26,4 +26,26 @@ export type {
AuditRequest,
} from './src/audit_logging/audit_events';
export type { AuditLogger } from './src/audit_logging/audit_logger';
export type {
APIKeysServiceWithContext,
APIKeysService,
CreateAPIKeyParams,
CreateAPIKeyResult,
InvalidateAPIKeyResult,
InvalidateAPIKeysParams,
ValidateAPIKeyParams,
CreateRestAPIKeyParams,
CreateRestAPIKeyWithKibanaPrivilegesParams,
CreateCrossClusterAPIKeyParams,
GrantAPIKeyResult,
UpdateAPIKeyParams,
UpdateAPIKeyResult,
UpdateCrossClusterAPIKeyParams,
UpdateRestAPIKeyParams,
UpdateRestAPIKeyWithKibanaPrivilegesParams,
} from './src/authentication/api_keys';
export type { KibanaPrivilegesType, ElasticsearchPrivilegesType } from './src/roles';
export { isCreateRestAPIKeyParams } from './src/authentication/api_keys';
export type { CoreFipsService } from './src/fips';

View file

@ -8,6 +8,7 @@
import type { KibanaRequest } from '@kbn/core-http-server';
import type { AuthenticatedUser } from '@kbn/core-security-common';
import type { APIKeysService } from './authentication/api_keys';
/**
* Core's authentication service
@ -22,4 +23,5 @@ export interface CoreAuthenticationService {
* @param request The request to retrieve the authenticated user for.
*/
getCurrentUser(request: KibanaRequest): AuthenticatedUser | null;
apiKeys: APIKeysService;
}

View file

@ -0,0 +1,268 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type { estypes } from '@elastic/elasticsearch';
import type { KibanaRequest } from '@kbn/core-http-server';
import { ElasticsearchPrivilegesType, KibanaPrivilegesType } from '../../roles';
/**
* Interface for managing API keys in Elasticsearch, including creation,
* validation, and invalidation of API keys,
* as well as checking the status of API key features.
*/
export interface APIKeys {
/**
* Determines if API Keys are enabled in Elasticsearch.
*/
areAPIKeysEnabled(): Promise<boolean>;
/**
* Determines if Cross-Cluster API Keys are enabled in Elasticsearch.
*/
areCrossClusterAPIKeysEnabled(): Promise<boolean>;
/**
* Tries to create an API key for the current user.
*
* Returns newly created API key or `null` if API keys are disabled.
*
* User needs `manage_api_key` privilege to create REST API keys and `manage_security` for Cross-Cluster API keys.
*
* @param request Request instance.
* @param createParams The params to create an API key
*/
create(
request: KibanaRequest,
createParams: CreateAPIKeyParams
): Promise<CreateAPIKeyResult | null>;
/**
* Attempts update an API key with the provided 'role_descriptors' and 'metadata'
*
* Returns `updated`, `true` if the update was successful, `false` if there was nothing to update
*
* User needs `manage_api_key` privilege to update REST API keys and `manage_security` for cross-cluster API keys.
*
* @param request Request instance.
* @param updateParams The params to edit an API key
*/
update(
request: KibanaRequest,
updateParams: UpdateAPIKeyParams
): Promise<UpdateAPIKeyResult | null>;
/**
* Tries to grant an API key for the current user.
* @param request Request instance.
* @param createParams Create operation parameters.
*/
grantAsInternalUser(
request: KibanaRequest,
createParams: CreateRestAPIKeyParams | CreateRestAPIKeyWithKibanaPrivilegesParams
): Promise<GrantAPIKeyResult | null>;
/**
* Tries to validate an API key.
* @param apiKeyPrams ValidateAPIKeyParams.
*/
validate(apiKeyPrams: ValidateAPIKeyParams): Promise<boolean>;
/**
* Tries to invalidate an API keys.
* @param request Request instance.
* @param params The params to invalidate an API keys.
*/
invalidate(
request: KibanaRequest,
params: InvalidateAPIKeysParams
): Promise<InvalidateAPIKeyResult | null>;
/**
* Tries to invalidate the API keys by using the internal user.
* @param params The params to invalidate the API keys.
*/
invalidateAsInternalUser(params: InvalidateAPIKeysParams): Promise<InvalidateAPIKeyResult | null>;
}
export type CreateAPIKeyParams =
| CreateRestAPIKeyParams
| CreateRestAPIKeyWithKibanaPrivilegesParams
| CreateCrossClusterAPIKeyParams;
/**
* Response of Kibana Create API key endpoint.
*/
export type CreateAPIKeyResult = estypes.SecurityCreateApiKeyResponse;
export interface CreateRestAPIKeyParams {
type?: 'rest';
expiration?: string;
name: string;
role_descriptors: Record<string, { [key: string]: any }>;
metadata?: { [key: string]: any };
}
export interface CreateRestAPIKeyWithKibanaPrivilegesParams {
type?: 'rest';
expiration?: string;
name: string;
metadata?: { [key: string]: any };
kibana_role_descriptors: Record<
string,
{
elasticsearch: ElasticsearchPrivilegesType & { [key: string]: unknown };
kibana: KibanaPrivilegesType;
}
>;
}
export interface CreateCrossClusterAPIKeyParams {
type: 'cross_cluster';
expiration?: string;
name: string;
metadata?: { [key: string]: any };
access: {
search?: Array<{
names: string[];
query?: unknown;
field_security?: unknown;
allow_restricted_indices?: boolean;
}>;
replication?: Array<{
names: string[];
}>;
};
}
export interface GrantAPIKeyResult {
/**
* Unique id for this API key
*/
id: string;
/**
* Name for this API key
*/
name: string;
/**
* Generated API key
*/
api_key: string;
}
/**
* Represents the parameters for validating API Key credentials.
*/
export interface ValidateAPIKeyParams {
/**
* Unique id for this API key
*/
id: string;
/**
* Generated API Key (secret)
*/
api_key: string;
}
/**
* Represents the params for invalidating multiple API keys
*/
export interface InvalidateAPIKeysParams {
/**
* List of unique API key IDs
*/
ids: string[];
}
/**
* The return value when invalidating an API key in Elasticsearch.
*/
export interface InvalidateAPIKeyResult {
/**
* The IDs of the API keys that were invalidated as part of the request.
*/
invalidated_api_keys: string[];
/**
* The IDs of the API keys that were already invalidated.
*/
previously_invalidated_api_keys: string[];
/**
* The number of errors that were encountered when invalidating the API keys.
*/
error_count: number;
/**
* Details about these errors. This field is not present in the response when error_count is 0.
*/
error_details?: Array<{
type?: string;
reason?: string;
caused_by?: {
type?: string;
reason?: string;
};
}>;
}
/**
* Response of Kibana Update API key endpoint.
*/
export type UpdateAPIKeyResult = estypes.SecurityUpdateApiKeyResponse;
/**
* Request body of Kibana Update API key endpoint.
*/
export type UpdateAPIKeyParams =
| UpdateRestAPIKeyParams
| UpdateCrossClusterAPIKeyParams
| UpdateRestAPIKeyWithKibanaPrivilegesParams;
export interface UpdateRestAPIKeyParams {
id: string;
type?: 'rest';
expiration?: string;
role_descriptors: Record<string, { [key: string]: unknown }>;
metadata?: { [key: string]: any };
}
export interface UpdateCrossClusterAPIKeyParams {
id: string;
type: 'cross_cluster';
expiration?: string;
metadata?: { [key: string]: any };
access: {
search?: Array<{
names: string[];
query?: unknown;
field_security?: unknown;
allow_restricted_indices?: boolean;
}>;
replication?: Array<{
names: string[];
}>;
};
}
export interface UpdateRestAPIKeyWithKibanaPrivilegesParams {
id: string;
type?: 'rest';
expiration?: string;
metadata?: { [key: string]: any };
kibana_role_descriptors: Record<
string,
{
elasticsearch: ElasticsearchPrivilegesType & { [key: string]: unknown };
kibana: KibanaPrivilegesType;
}
>;
}
export function isCreateRestAPIKeyParams(params: any): params is CreateRestAPIKeyParams {
return 'role_descriptors' in params;
}

View file

@ -0,0 +1,64 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import type {
CreateAPIKeyParams,
CreateAPIKeyResult,
UpdateAPIKeyParams,
UpdateAPIKeyResult,
ValidateAPIKeyParams,
InvalidateAPIKeyResult,
InvalidateAPIKeysParams,
} from './api_keys';
/**
* Public API Keys service exposed through core context to manage
* API keys in Elasticsearch, including creation,
* validation, and invalidation of API keys,
* as well as checking the status of API key features.
*/
export interface APIKeysServiceWithContext {
/**
* Determines if API Keys are enabled in Elasticsearch.
*/
areAPIKeysEnabled(): Promise<boolean>;
/**
* Tries to create an API key for the current user.
*
* Returns newly created API key or `null` if API keys are disabled.
*
* User needs `manage_api_key` privilege to create REST API keys and `manage_security` for Cross-Cluster API keys.
*
* @param createParams The params to create an API key
*/
create(createParams: CreateAPIKeyParams): Promise<CreateAPIKeyResult | null>;
/**
* Attempts update an API key with the provided 'role_descriptors' and 'metadata'
*
* Returns `updated`, `true` if the update was successful, `false` if there was nothing to update
*
* User needs `manage_api_key` privilege to update REST API keys and `manage_security` for cross-cluster API keys.
*
* @param updateParams The params to edit an API key
*/
update(updateParams: UpdateAPIKeyParams): Promise<UpdateAPIKeyResult | null>;
/**
* Tries to validate an API key.
* @param apiKeyPrams ValidateAPIKeyParams.
*/
validate(apiKeyPrams: ValidateAPIKeyParams): Promise<boolean>;
/**
* Tries to invalidate an API keys.
* @param params The params to invalidate an API keys.
*/
invalidate(params: InvalidateAPIKeysParams): Promise<InvalidateAPIKeyResult | null>;
}

View file

@ -0,0 +1,27 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export type {
APIKeys as APIKeysService,
CreateAPIKeyParams,
CreateAPIKeyResult,
InvalidateAPIKeyResult,
InvalidateAPIKeysParams,
ValidateAPIKeyParams,
CreateRestAPIKeyParams,
CreateRestAPIKeyWithKibanaPrivilegesParams,
CreateCrossClusterAPIKeyParams,
GrantAPIKeyResult,
UpdateAPIKeyParams,
UpdateAPIKeyResult,
UpdateCrossClusterAPIKeyParams,
UpdateRestAPIKeyParams,
UpdateRestAPIKeyWithKibanaPrivilegesParams,
} from './api_keys';
export type { APIKeysServiceWithContext } from './api_keys_context';
export { isCreateRestAPIKeyParams } from './api_keys';

View file

@ -7,7 +7,9 @@
*/
import type { AuthenticatedUser } from '@kbn/core-security-common';
import { AuditLogger } from './audit_logging/audit_logger';
import type { APIKeysServiceWithContext } from './authentication/api_keys';
export interface SecurityRequestHandlerContext {
authc: AuthcRequestHandlerContext;
@ -16,6 +18,7 @@ export interface SecurityRequestHandlerContext {
export interface AuthcRequestHandlerContext {
getCurrentUser(): AuthenticatedUser | null;
apiKeys: APIKeysServiceWithContext;
}
export interface AuditRequestHandlerContext {

View file

@ -0,0 +1,9 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export type { ElasticsearchPrivilegesType, KibanaPrivilegesType } from './schema';

View file

@ -0,0 +1,42 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
/**
* Type representing Elasticsearch specific portion of the role definition.
*/
export interface ElasticsearchPrivilegesType {
cluster?: string[];
remote_cluster?: Array<{
privileges: string[];
clusters: string[];
}>;
indices?: Array<{
names: string[];
field_security?: Record<'grant' | 'except', string[]>;
privileges: string[];
query?: string;
allow_restricted_indices?: boolean;
}>;
remote_indices?: Array<{
clusters: string[];
names: string[];
field_security?: Record<'grant' | 'except', string[]>;
privileges: string[];
query?: string;
allow_restricted_indices?: boolean;
}>;
run_as?: string[];
}
/**
* Type representing Kibana specific portion of the role definition.
*/
export type KibanaPrivilegesType = Array<{
spaces: string[];
base?: string[];
feature?: Record<string, string[]>;
}>;

View file

@ -14,15 +14,6 @@ export type {
AuditLogger,
} from './src/audit';
export type {
CreateAPIKeyParams,
CreateAPIKeyResult,
CreateRestAPIKeyParams,
GrantAPIKeyResult,
InvalidateAPIKeysParams,
ValidateAPIKeyParams,
CreateRestAPIKeyWithKibanaPrivilegesParams,
CreateCrossClusterAPIKeyParams,
InvalidateAPIKeyResult,
APIKeys,
AuthenticationServiceStart,
UpdateAPIKeyParams,
@ -39,7 +30,6 @@ export type {
CheckPrivilegesWithRequest,
CheckSavedObjectsPrivilegesWithRequest,
CheckPrivilegesDynamicallyWithRequest,
KibanaPrivilegesType,
SavedObjectActions,
UIActions,
CheckPrivilegesPayload,
@ -51,7 +41,6 @@ export type {
CheckPrivilegesOptions,
CheckUserProfilesPrivilegesPayload,
CheckUserProfilesPrivilegesResponse,
ElasticsearchPrivilegesType,
CasesActions,
CheckPrivileges,
AlertingActions,
@ -72,11 +61,30 @@ export type {
} from './src/user_profile';
export {
restApiKeySchema,
getRestApiKeyWithKibanaPrivilegesSchema,
getUpdateRestApiKeyWithKibanaPrivilegesSchema,
crossClusterApiKeySchema,
updateRestApiKeySchema,
updateCrossClusterApiKeySchema,
} from './src/authentication';
export { GLOBAL_RESOURCE, elasticsearchRoleSchema, getKibanaRoleSchema } from './src/authorization';
export type {
ElasticsearchPrivilegesType,
KibanaPrivilegesType,
APIKeysService,
CreateAPIKeyParams,
CreateAPIKeyResult,
InvalidateAPIKeyResult,
InvalidateAPIKeysParams,
ValidateAPIKeyParams,
CreateRestAPIKeyParams,
CreateRestAPIKeyWithKibanaPrivilegesParams,
CreateCrossClusterAPIKeyParams,
GrantAPIKeyResult,
} from '@kbn/core-security-server';
export { isCreateRestAPIKeyParams } from '@kbn/core-security-server';
export {
restApiKeySchema,
crossClusterApiKeySchema,
getRestApiKeyWithKibanaPrivilegesSchema,
} from './src/authentication';
export { getKibanaRoleSchema, elasticsearchRoleSchema, GLOBAL_RESOURCE } from './src/authorization';

View file

@ -5,153 +5,9 @@
* 2.0.
*/
import type { estypes } from '@elastic/elasticsearch';
import type { KibanaRequest } from '@kbn/core/server';
import { schema, TypeOf } from '@kbn/config-schema';
import { schema } from '@kbn/config-schema';
import { getKibanaRoleSchema, elasticsearchRoleSchema } from '../../authorization';
export interface APIKeys {
/**
* Determines if API Keys are enabled in Elasticsearch.
*/
areAPIKeysEnabled(): Promise<boolean>;
/**
* Determines if Cross-Cluster API Keys are enabled in Elasticsearch.
*/
areCrossClusterAPIKeysEnabled(): Promise<boolean>;
/**
* Tries to create an API key for the current user.
*
* Returns newly created API key or `null` if API keys are disabled.
*
* User needs `manage_api_key` privilege to create REST API keys and `manage_security` for Cross-Cluster API keys.
*
* @param request Request instance.
* @param createParams The params to create an API key
*/
create(
request: KibanaRequest,
createParams: CreateAPIKeyParams
): Promise<CreateAPIKeyResult | null>;
/**
* Tries to grant an API key for the current user.
* @param request Request instance.
* @param createParams Create operation parameters.
*/
grantAsInternalUser(
request: KibanaRequest,
createParams: CreateRestAPIKeyParams | CreateRestAPIKeyWithKibanaPrivilegesParams
): Promise<GrantAPIKeyResult | null>;
/**
* Tries to validate an API key.
* @param apiKeyPrams ValidateAPIKeyParams.
*/
validate(apiKeyPrams: ValidateAPIKeyParams): Promise<boolean>;
/**
* Tries to invalidate an API keys.
* @param request Request instance.
* @param params The params to invalidate an API keys.
*/
invalidate(
request: KibanaRequest,
params: InvalidateAPIKeysParams
): Promise<InvalidateAPIKeyResult | null>;
/**
* Tries to invalidate the API keys by using the internal user.
* @param params The params to invalidate the API keys.
*/
invalidateAsInternalUser(params: InvalidateAPIKeysParams): Promise<InvalidateAPIKeyResult | null>;
}
export type CreateAPIKeyParams =
| CreateRestAPIKeyParams
| CreateRestAPIKeyWithKibanaPrivilegesParams
| CreateCrossClusterAPIKeyParams;
/**
* Response of Kibana Create API key endpoint.
*/
export type CreateAPIKeyResult = estypes.SecurityCreateApiKeyResponse;
export type CreateRestAPIKeyParams = TypeOf<typeof restApiKeySchema>;
export type CreateRestAPIKeyWithKibanaPrivilegesParams = TypeOf<
ReturnType<typeof getRestApiKeyWithKibanaPrivilegesSchema>
>;
export type CreateCrossClusterAPIKeyParams = TypeOf<typeof crossClusterApiKeySchema>;
export interface GrantAPIKeyResult {
/**
* Unique id for this API key
*/
id: string;
/**
* Name for this API key
*/
name: string;
/**
* Generated API key
*/
api_key: string;
}
/**
* Represents the parameters for validating API Key credentials.
*/
export interface ValidateAPIKeyParams {
/**
* Unique id for this API key
*/
id: string;
/**
* Generated API Key (secret)
*/
api_key: string;
}
/**
* Represents the params for invalidating multiple API keys
*/
export interface InvalidateAPIKeysParams {
ids: string[];
}
/**
* The return value when invalidating an API key in Elasticsearch.
*/
export interface InvalidateAPIKeyResult {
/**
* The IDs of the API keys that were invalidated as part of the request.
*/
invalidated_api_keys: string[];
/**
* The IDs of the API keys that were already invalidated.
*/
previously_invalidated_api_keys: string[];
/**
* The number of errors that were encountered when invalidating the API keys.
*/
error_count: number;
/**
* Details about these errors. This field is not present in the response when error_count is 0.
*/
error_details?: Array<{
type?: string;
reason?: string;
caused_by?: {
type?: string;
reason?: string;
};
}>;
}
export const restApiKeySchema = schema.object({
type: schema.maybe(schema.literal('rest')),
name: schema.string(),
@ -165,8 +21,11 @@ export const restApiKeySchema = schema.object({
export const getRestApiKeyWithKibanaPrivilegesSchema = (
getBasePrivilegeNames: Parameters<typeof getKibanaRoleSchema>[0]
) =>
restApiKeySchema.extends({
role_descriptors: null,
schema.object({
type: schema.maybe(schema.literal('rest')),
name: schema.string(),
expiration: schema.maybe(schema.string()),
metadata: schema.maybe(schema.object({}, { unknowns: 'allow' })),
kibana_role_descriptors: schema.recordOf(
schema.string(),
schema.object({
@ -176,9 +35,11 @@ export const getRestApiKeyWithKibanaPrivilegesSchema = (
),
});
export const crossClusterApiKeySchema = restApiKeySchema.extends({
export const crossClusterApiKeySchema = schema.object({
type: schema.literal('cross_cluster'),
role_descriptors: null,
name: schema.string(),
expiration: schema.maybe(schema.string()),
metadata: schema.maybe(schema.object({}, { unknowns: 'allow' })),
access: schema.object(
{
search: schema.maybe(
@ -203,41 +64,52 @@ export const crossClusterApiKeySchema = restApiKeySchema.extends({
),
});
/**
* Response of Kibana Update API key endpoint.
*/
export type UpdateAPIKeyResult = estypes.SecurityUpdateApiKeyResponse;
/**
* Request body of Kibana Update API key endpoint.
*/
export type UpdateAPIKeyParams =
| UpdateRestAPIKeyParams
| UpdateCrossClusterAPIKeyParams
| UpdateRestAPIKeyWithKibanaPrivilegesParams;
export const updateRestApiKeySchema = restApiKeySchema.extends({
name: null,
export const updateRestApiKeySchema = schema.object({
id: schema.string(),
type: schema.maybe(schema.literal('rest')),
expiration: schema.maybe(schema.string()),
role_descriptors: schema.recordOf(schema.string(), schema.object({}, { unknowns: 'allow' }), {
defaultValue: {},
}),
metadata: schema.maybe(schema.object({}, { unknowns: 'allow' })),
});
export const updateCrossClusterApiKeySchema = crossClusterApiKeySchema.extends({
name: null,
export const updateCrossClusterApiKeySchema = schema.object({
id: schema.string(),
type: schema.literal('cross_cluster'),
expiration: schema.maybe(schema.string()),
metadata: schema.maybe(schema.object({}, { unknowns: 'allow' })),
access: schema.object(
{
search: schema.maybe(
schema.arrayOf(
schema.object({
names: schema.arrayOf(schema.string()),
query: schema.maybe(schema.any()),
field_security: schema.maybe(schema.any()),
allow_restricted_indices: schema.maybe(schema.boolean()),
})
)
),
replication: schema.maybe(
schema.arrayOf(
schema.object({
names: schema.arrayOf(schema.string()),
})
)
),
},
{ unknowns: 'allow' }
),
});
export type UpdateRestAPIKeyParams = TypeOf<typeof updateRestApiKeySchema>;
export type UpdateCrossClusterAPIKeyParams = TypeOf<typeof updateCrossClusterApiKeySchema>;
export type UpdateRestAPIKeyWithKibanaPrivilegesParams = TypeOf<
ReturnType<typeof getUpdateRestApiKeyWithKibanaPrivilegesSchema>
>;
export const getUpdateRestApiKeyWithKibanaPrivilegesSchema = (
getBasePrivilegeNames: Parameters<typeof getKibanaRoleSchema>[0]
) =>
restApiKeySchema.extends({
role_descriptors: null,
name: null,
schema.object({
type: schema.maybe(schema.literal('rest')),
expiration: schema.maybe(schema.string()),
metadata: schema.maybe(schema.object({}, { unknowns: 'allow' })),
id: schema.string(),
kibana_role_descriptors: schema.recordOf(
schema.string(),

View file

@ -5,23 +5,6 @@
* 2.0.
*/
export type {
CreateAPIKeyParams,
CreateAPIKeyResult,
InvalidateAPIKeyResult,
InvalidateAPIKeysParams,
ValidateAPIKeyParams,
CreateRestAPIKeyParams,
CreateRestAPIKeyWithKibanaPrivilegesParams,
CreateCrossClusterAPIKeyParams,
GrantAPIKeyResult,
APIKeys,
UpdateAPIKeyParams,
UpdateAPIKeyResult,
UpdateCrossClusterAPIKeyParams,
UpdateRestAPIKeyParams,
UpdateRestAPIKeyWithKibanaPrivilegesParams,
} from './api_keys';
export {
crossClusterApiKeySchema,
getRestApiKeyWithKibanaPrivilegesSchema,

View file

@ -6,14 +6,13 @@
*/
import type { KibanaRequest } from '@kbn/core/server';
import type { AuthenticatedUser } from '@kbn/core-security-common';
import type { APIKeys } from './api_keys';
import type { AuthenticatedUser } from '@kbn/security-plugin-types-common';
import type { APIKeysService } from '@kbn/core-security-server';
/**
* Authentication services available on the security plugin's start contract.
*/
export interface AuthenticationServiceStart {
apiKeys: APIKeys;
apiKeys: APIKeysService;
getCurrentUser: (request: KibanaRequest) => AuthenticatedUser | null;
}

View file

@ -5,29 +5,22 @@
* 2.0.
*/
export type { AuthenticationServiceStart } from './authentication_service';
export type {
CreateAPIKeyParams,
CreateAPIKeyResult,
CreateRestAPIKeyParams,
CreateRestAPIKeyWithKibanaPrivilegesParams,
CreateCrossClusterAPIKeyParams,
InvalidateAPIKeyResult,
InvalidateAPIKeysParams,
ValidateAPIKeyParams,
APIKeys,
GrantAPIKeyResult,
APIKeysService as APIKeys,
UpdateAPIKeyParams,
UpdateAPIKeyResult,
UpdateCrossClusterAPIKeyParams,
UpdateRestAPIKeyParams,
UpdateRestAPIKeyWithKibanaPrivilegesParams,
} from './api_keys';
export type { AuthenticationServiceStart } from './authentication_service';
} from '@kbn/core-security-server';
export {
restApiKeySchema,
crossClusterApiKeySchema,
getRestApiKeyWithKibanaPrivilegesSchema,
getUpdateRestApiKeyWithKibanaPrivilegesSchema,
crossClusterApiKeySchema,
restApiKeySchema,
updateRestApiKeySchema,
updateCrossClusterApiKeySchema,
} from './api_keys';

View file

@ -42,7 +42,6 @@ export type {
PrivilegeDeprecationsRolesByFeatureIdResponse,
} from './deprecations';
export type { AuthorizationMode } from './mode';
export type { ElasticsearchPrivilegesType, KibanaPrivilegesType } from './role_schema';
export { GLOBAL_RESOURCE } from './constants';
export { elasticsearchRoleSchema, getKibanaRoleSchema } from './role_schema';

View file

@ -10,11 +10,10 @@
"target/**/*"
],
"kbn_references": [
"@kbn/config-schema",
"@kbn/core",
"@kbn/security-plugin-types-common",
"@kbn/core-user-profile-server",
"@kbn/core-security-server",
"@kbn/core-security-common"
"@kbn/config-schema",
]
}

View file

@ -30,6 +30,7 @@ describe('API Keys', () => {
>;
let mockLicense: jest.Mocked<SecurityLicense>;
let logger: Logger;
const roleDescriptors: { [key: string]: any } = { foo: true };
beforeEach(() => {
mockValidateKibanaPrivileges.mockReset().mockReturnValue({ validationErrors: [] });
@ -239,9 +240,10 @@ describe('API Keys', () => {
});
const result = await apiKeys.create(httpServerMock.createKibanaRequest(), {
name: 'key-name',
role_descriptors: { foo: true },
role_descriptors: roleDescriptors,
expiration: '1d',
});
expect(result).toEqual({
api_key: 'abc123',
expiration: '1d',
@ -253,7 +255,7 @@ describe('API Keys', () => {
expect(mockScopedClusterClient.asCurrentUser.security.createApiKey).toHaveBeenCalledWith({
body: {
name: 'key-name',
role_descriptors: { foo: true },
role_descriptors: roleDescriptors,
expiration: '1d',
},
});
@ -343,7 +345,7 @@ describe('API Keys', () => {
const result = await apiKeys.update(httpServerMock.createKibanaRequest(), {
id: 'test_id',
role_descriptors: { foo: true },
role_descriptors: roleDescriptors,
metadata: {},
});
@ -370,7 +372,7 @@ describe('API Keys', () => {
const result = await apiKeys.update(httpServerMock.createKibanaRequest(), {
id: 'test_id',
role_descriptors: { foo: true },
role_descriptors: roleDescriptors,
metadata: {},
});
@ -473,7 +475,7 @@ describe('API Keys', () => {
}),
{
name: 'test_api_key',
role_descriptors: { foo: true },
role_descriptors: roleDescriptors,
expiration: '1d',
}
);
@ -512,7 +514,7 @@ describe('API Keys', () => {
}),
{
name: 'test_api_key',
role_descriptors: { foo: true },
role_descriptors: roleDescriptors,
expiration: '1d',
}
);
@ -527,7 +529,7 @@ describe('API Keys', () => {
body: {
api_key: {
name: 'test_api_key',
role_descriptors: { foo: true },
role_descriptors: roleDescriptors,
expiration: '1d',
},
grant_type: 'access_token',
@ -553,7 +555,7 @@ describe('API Keys', () => {
}),
{
name: 'test_api_key',
role_descriptors: { foo: true },
role_descriptors: roleDescriptors,
expiration: '1d',
}
);
@ -592,7 +594,7 @@ describe('API Keys', () => {
}),
{
name: 'test_api_key',
role_descriptors: { foo: true },
role_descriptors: roleDescriptors,
expiration: '1d',
}
)

View file

@ -20,6 +20,7 @@ import type {
InvalidateAPIKeysParams,
ValidateAPIKeyParams,
} from '@kbn/security-plugin-types-server';
import { isCreateRestAPIKeyParams } from '@kbn/security-plugin-types-server';
import { getFakeKibanaRequest } from './fake_kibana_request';
import type { SecurityLicense } from '../../../common';
@ -96,7 +97,6 @@ export class APIKeys implements APIKeysType {
this.logger.debug(
`Testing if API Keys are enabled by attempting to invalidate a non-existant key: ${id}`
);
try {
await this.clusterClient.asInternalUser.security.invalidateApiKey({
body: {
@ -125,7 +125,6 @@ export class APIKeys implements APIKeysType {
this.logger.debug(
`Testing if cross-cluster API Keys are enabled by attempting to update a non-existant key: ${id}`
);
try {
await this.clusterClient.asInternalUser.transport.request({
method: 'PUT',
@ -155,13 +154,13 @@ export class APIKeys implements APIKeysType {
if (!this.license.isEnabled()) {
return null;
}
const { type, expiration, name, metadata } = createParams;
const scopedClusterClient = this.clusterClient.asScoped(request);
this.logger.debug('Trying to create an API key');
let result: CreateAPIKeyResult;
try {
if (type === 'cross_cluster') {
result = await scopedClusterClient.asCurrentUser.transport.request<CreateAPIKeyResult>({
@ -175,13 +174,13 @@ export class APIKeys implements APIKeysType {
name,
expiration,
metadata,
role_descriptors:
'role_descriptors' in createParams
? createParams.role_descriptors
: this.parseRoleDescriptorsWithKibanaPrivileges(
createParams.kibana_role_descriptors,
false
),
role_descriptors: isCreateRestAPIKeyParams(createParams)
? createParams.role_descriptors
: this.parseRoleDescriptorsWithKibanaPrivileges(
createParams.kibana_role_descriptors,
this.kibanaFeatures,
false
),
},
});
}
@ -234,6 +233,7 @@ export class APIKeys implements APIKeysType {
? updateParams.role_descriptors
: this.parseRoleDescriptorsWithKibanaPrivileges(
updateParams.kibana_role_descriptors,
this.kibanaFeatures,
true
),
});
@ -279,12 +279,12 @@ export class APIKeys implements APIKeysType {
);
const { expiration, metadata, name } = createParams;
const roleDescriptors =
'role_descriptors' in createParams
? createParams.role_descriptors
: this.parseRoleDescriptorsWithKibanaPrivileges(
createParams.kibana_role_descriptors,
this.kibanaFeatures,
false
);
@ -293,7 +293,6 @@ export class APIKeys implements APIKeysType {
authorizationHeader,
clientAuthorizationHeader
);
// User needs `manage_api_key` or `grant_api_key` privilege to use this API
let result: GrantAPIKeyResult;
try {
@ -318,7 +317,6 @@ export class APIKeys implements APIKeysType {
}
this.logger.debug(`Trying to invalidate ${params.ids.length} an API key as current user`);
let result: InvalidateAPIKeyResult;
try {
// User needs `manage_api_key` privilege to use this API
@ -354,6 +352,7 @@ export class APIKeys implements APIKeysType {
this.logger.debug(`Trying to invalidate ${params.ids.length} API keys`);
let result: InvalidateAPIKeyResult;
try {
// Internal user needs `cluster:admin/xpack/security/api_key/invalidate` privilege to use this API
result = await this.clusterClient.asInternalUser.security.invalidateApiKey({
@ -384,7 +383,6 @@ export class APIKeys implements APIKeysType {
const fakeRequest = getFakeKibanaRequest(apiKeyPrams);
this.logger.debug(`Trying to validate an API key`);
try {
await this.clusterClient.asScoped(fakeRequest).asCurrentUser.security.authenticate();
this.logger.debug(`API key was validated successfully`);
@ -445,6 +443,7 @@ export class APIKeys implements APIKeysType {
private parseRoleDescriptorsWithKibanaPrivileges(
kibanaRoleDescriptors: CreateRestAPIKeyWithKibanaPrivilegesParams['kibana_role_descriptors'],
features: KibanaFeature[],
isEdit: boolean
) {
const roleDescriptors = Object.create(null);
@ -452,10 +451,7 @@ export class APIKeys implements APIKeysType {
const allValidationErrors: string[] = [];
if (kibanaRoleDescriptors) {
Object.entries(kibanaRoleDescriptors).forEach(([roleKey, roleDescriptor]) => {
const { validationErrors } = validateKibanaPrivileges(
this.kibanaFeatures,
roleDescriptor.kibana
);
const { validationErrors } = validateKibanaPrivileges(features, roleDescriptor.kibana);
allValidationErrors.push(...validationErrors);
const applications = transformPrivilegesToElasticsearchPrivileges(

View file

@ -5,9 +5,9 @@
* 2.0.
*/
import { apiKeysMock } from '@kbn/core-security-server-mocks';
import type { DeeplyMockedKeys } from '@kbn/utility-types-jest';
import { apiKeysMock } from './api_keys/api_keys.mock';
import type { InternalAuthenticationServiceStart } from './authentication_service';
export const authenticationServiceMock = {

View file

@ -348,7 +348,6 @@ export class AuthenticationService {
applicationName,
kibanaFeatures,
});
/**
* Retrieves server protocol name/host name/port and merges it with `xpack.security.public` config
* to construct a server base URL (deprecated, used by the SAML provider only).

View file

@ -65,6 +65,21 @@ describe('buildSecurityApi', () => {
expect(auditService.asScoped(request).log).toHaveBeenCalledWith({ message: 'an event' });
});
});
describe('authc.apiKeys', () => {
it('properly delegates to the service', async () => {
await authc.apiKeys.areAPIKeysEnabled();
expect(authc.apiKeys.areAPIKeysEnabled).toHaveBeenCalledTimes(1);
});
it('returns the result from the service', async () => {
authc.apiKeys.areAPIKeysEnabled.mockReturnValue(Promise.resolve(false));
const areAPIKeysEnabled = await authc.apiKeys.areAPIKeysEnabled();
expect(areAPIKeysEnabled).toBe(false);
});
});
});
describe('buildUserProfileApi', () => {

View file

@ -24,6 +24,17 @@ export const buildSecurityApi = ({
getCurrentUser: (request) => {
return getAuthc().getCurrentUser(request);
},
apiKeys: {
areAPIKeysEnabled: () => getAuthc().apiKeys.areAPIKeysEnabled(),
areCrossClusterAPIKeysEnabled: () => getAuthc().apiKeys.areAPIKeysEnabled(),
grantAsInternalUser: (request, createParams) =>
getAuthc().apiKeys.grantAsInternalUser(request, createParams),
create: (request, createParams) => getAuthc().apiKeys.create(request, createParams),
update: (request, updateParams) => getAuthc().apiKeys.update(request, updateParams),
validate: (apiKeyParams) => getAuthc().apiKeys.validate(apiKeyParams),
invalidate: (request, params) => getAuthc().apiKeys.invalidate(request, params),
invalidateAsInternalUser: (params) => getAuthc().apiKeys.invalidateAsInternalUser(params),
},
},
audit: {
asScoped(request) {

View file

@ -7,7 +7,7 @@
import type { TransportResult } from '@elastic/elasticsearch';
import { securityServiceMock } from '@kbn/core-security-server-mocks';
import { apiKeysMock, securityServiceMock } from '@kbn/core-security-server-mocks';
import { auditServiceMock } from './audit/mocks';
import { authenticationServiceMock } from './authentication/authentication_service.mock';
@ -19,7 +19,10 @@ function createSetupMock() {
const mockAuthz = authorizationMock.create();
return {
audit: auditServiceMock.create(),
authc: { getCurrentUser: jest.fn() },
authc: {
getCurrentUser: jest.fn(),
apiKeys: apiKeysMock.create(),
},
authz: {
actions: mockAuthz.actions,
checkPrivilegesWithRequest: mockAuthz.checkPrivilegesWithRequest,

View file

@ -77,7 +77,9 @@ export interface SecurityPluginSetup extends SecurityPluginSetupWithoutDeprecate
/**
* @deprecated Use `authc` methods from the `SecurityServiceStart` contract instead.
*/
authc: { getCurrentUser: (request: KibanaRequest) => AuthenticatedUser | null };
authc: {
getCurrentUser: (request: KibanaRequest) => AuthenticatedUser | null;
};
/**
* @deprecated Use `authz` methods from the `SecurityServiceStart` contract instead.
*/
@ -110,6 +112,7 @@ export class SecurityPlugin
private readonly logger: Logger;
private authorizationSetup?: AuthorizationServiceSetupInternal;
private auditSetup?: AuditServiceSetup;
private configSubscription?: Subscription;
private config?: ConfigType;
@ -189,6 +192,7 @@ export class SecurityPlugin
this.initializerContext.logger.get('authentication')
);
this.auditService = new AuditService(this.initializerContext.logger.get('audit'));
this.elasticsearchService = new ElasticsearchService(
this.initializerContext.logger.get('elasticsearch')
);
@ -340,7 +344,9 @@ export class SecurityPlugin
return Object.freeze<SecurityPluginSetup>({
audit: this.auditSetup,
authc: { getCurrentUser: (request) => this.getAuthentication().getCurrentUser(request) },
authc: {
getCurrentUser: (request) => this.getAuthentication().getCurrentUser(request),
},
authz: {
actions: this.authorizationSetup.actions,
checkPrivilegesWithRequest: this.authorizationSetup.checkPrivilegesWithRequest,
@ -421,8 +427,8 @@ export class SecurityPlugin
return Object.freeze<SecurityPluginStart>({
authc: {
apiKeys: this.authenticationStart.apiKeys,
getCurrentUser: this.authenticationStart.getCurrentUser,
apiKeys: this.authenticationStart.apiKeys,
},
authz: {
actions: this.authorizationSetup!.actions,

View file

@ -83,7 +83,7 @@
"@kbn/core-user-profile-browser",
"@kbn/security-api-key-management",
"@kbn/security-form-components",
"@kbn/core-security-server-mocks",
"@kbn/core-security-server-mocks"
],
"exclude": [
"target/**/*",