Skip to content

Commit

Permalink
[EEM] Disable authorization checks on endpoints (#198695)
Browse files Browse the repository at this point in the history
Disable authorization checks on all entity manager endpoints.

Also makes two notable changes to the endpoints/EntityClient behaviour:
- previously the EntityClient accepted a `IScopedClusterClient` and
abstracted usage of asInternalUser/asCurrentUser in its methods which
may result in unwanted behavior for consumers. It now only accepts an
`ElasticsearchClient` that is preauthenticated by the consumers
- added permissions verifications to custom definition endpoints

---------

Co-authored-by: Kevin Lacabane <[email protected]>
Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
3 people authored Nov 14, 2024
1 parent 35c2a9e commit 94d7df3
Show file tree
Hide file tree
Showing 19 changed files with 209 additions and 131 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { KibanaRequest } from '@kbn/core-http-server';
import { getFakeKibanaRequest } from '@kbn/security-plugin/server/authentication/api_keys/fake_kibana_request';
import { EntityManagerServerSetup } from '../../../types';
import { canManageEntityDefinition, entityDefinitionRuntimePrivileges } from '../privileges';
import { BUILT_IN_ALLOWED_INDICES } from '../../entities/built_in/constants';

export interface EntityDiscoveryAPIKey {
id: string;
Expand Down Expand Up @@ -45,7 +46,7 @@ export const checkIfEntityDiscoveryAPIKeyIsValid = async (

server.logger.debug('validating API key has runtime privileges for entity discovery');

return canManageEntityDefinition(esClient);
return canManageEntityDefinition(esClient, BUILT_IN_ALLOWED_INDICES);
};

export const generateEntityDiscoveryAPIKey = async (
Expand Down
22 changes: 13 additions & 9 deletions x-pack/plugins/entity_manager/server/lib/auth/privileges.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,18 @@ import { ENTITY_INTERNAL_INDICES_PATTERN } from '../../../common/constants_entit
import { SO_ENTITY_DEFINITION_TYPE, SO_ENTITY_DISCOVERY_API_KEY_TYPE } from '../../saved_objects';
import { BUILT_IN_ALLOWED_INDICES } from '../entities/built_in/constants';

export const canManageEntityDefinition = async (client: ElasticsearchClient) => {
export const canManageEntityDefinition = async (
client: ElasticsearchClient,
sourceIndices: string[]
) => {
const { has_all_requested: hasAllRequested } = await client.security.hasPrivileges({
body: entityDefinitionRuntimePrivileges,
body: entityDefinitionRuntimePrivileges(sourceIndices),
});

return hasAllRequested;
};

const canDeleteEntityDefinition = async (client: ElasticsearchClient) => {
export const canDeleteEntityDefinition = async (client: ElasticsearchClient) => {
const { has_all_requested: hasAllRequested } = await client.security.hasPrivileges({
body: entityDefinitionDeletionPrivileges,
});
Expand All @@ -43,9 +46,10 @@ const canDeleteAPIKey = async (client: ElasticsearchClient) => {
};

export const canEnableEntityDiscovery = async (client: ElasticsearchClient) => {
return Promise.all([canManageAPIKey(client), canManageEntityDefinition(client)]).then((results) =>
results.every(Boolean)
);
return Promise.all([
canManageAPIKey(client),
canManageEntityDefinition(client, BUILT_IN_ALLOWED_INDICES),
]).then((results) => results.every(Boolean));
};

export const canDisableEntityDiscovery = async (client: ElasticsearchClient) => {
Expand All @@ -54,15 +58,15 @@ export const canDisableEntityDiscovery = async (client: ElasticsearchClient) =>
);
};

export const entityDefinitionRuntimePrivileges = {
export const entityDefinitionRuntimePrivileges = (sourceIndices: string[]) => ({
cluster: ['manage_transform', 'manage_ingest_pipelines', 'manage_index_templates'],
index: [
{
names: [ENTITY_INTERNAL_INDICES_PATTERN],
privileges: ['create_index', 'delete_index', 'index', 'create_doc', 'auto_configure', 'read'],
},
{
names: [...BUILT_IN_ALLOWED_INDICES, ENTITY_INTERNAL_INDICES_PATTERN],
names: [...sourceIndices, ENTITY_INTERNAL_INDICES_PATTERN],
privileges: ['read', 'view_index_metadata'],
},
],
Expand All @@ -73,7 +77,7 @@ export const entityDefinitionRuntimePrivileges = {
resources: ['*'],
},
],
};
});

export const entityDefinitionDeletionPrivileges = {
cluster: ['manage_transform', 'manage_ingest_pipelines', 'manage_index_templates'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
* 2.0.
*/

import { compact, forEach, reduce } from 'lodash';
import { compact } from 'lodash';
import { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server';
import { EntityDefinition } from '@kbn/entities-schema';
import { NodesIngestTotal } from '@elastic/elasticsearch/lib/api/types';
import { SO_ENTITY_DEFINITION_TYPE } from '../../saved_objects';
import { BUILT_IN_ID_PREFIX } from './built_in';
import { EntityDefinitionState, EntityDefinitionWithState } from './types';
Expand Down Expand Up @@ -144,33 +143,14 @@ async function getIngestPipelineState({
.filter(({ type }) => type === 'ingest_pipeline')
.map(({ id }) => id);

const [ingestPipelines, ingestPipelinesStats] = await Promise.all([
esClient.ingest.getPipeline({ id: ingestPipelineIds.join(',') }, { ignore: [404] }),
esClient.nodes.stats({
metric: 'ingest',
filter_path: ingestPipelineIds.map((id) => `nodes.*.ingest.pipelines.${id}`),
}),
]);

const ingestStatsByPipeline = reduce(
ingestPipelinesStats.nodes,
(pipelines, { ingest }) => {
forEach(ingest?.pipelines, (value: NodesIngestTotal, key: string) => {
if (!pipelines[key]) {
pipelines[key] = { count: 0, failed: 0 };
}
pipelines[key].count += value.count ?? 0;
pipelines[key].failed += value.failed ?? 0;
});
return pipelines;
},
{} as Record<string, { count: number; failed: number }>
const ingestPipelines = await esClient.ingest.getPipeline(
{ id: ingestPipelineIds.join(',') },
{ ignore: [404] }
);

return ingestPipelineIds.map((id) => ({
id,
installed: !!ingestPipelines[id],
stats: ingestStatsByPipeline[id],
}));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ describe('install_entity_definition', () => {
describe('installBuiltInEntityDefinitions', () => {
it('should install definition when not found', async () => {
const builtInDefinitions = [mockEntityDefinition];
const clusterClient = elasticsearchClientMock.createScopedClusterClient();
const esClient = elasticsearchClientMock.createElasticsearchClient();
const soClient = savedObjectsClientMock.create();
soClient.find.mockResolvedValue({ saved_objects: [], total: 0, page: 1, per_page: 10 });
soClient.update.mockResolvedValue({
Expand All @@ -271,19 +271,18 @@ describe('install_entity_definition', () => {
});

await installBuiltInEntityDefinitions({
clusterClient,
esClient,
soClient,
definitions: builtInDefinitions,
logger: loggerMock.create(),
});

assertHasCreatedDefinition(mockEntityDefinition, soClient, clusterClient.asSecondaryAuthUser);
assertHasCreatedDefinition(mockEntityDefinition, soClient, esClient);
});

it('should reinstall when partial state found', async () => {
const builtInDefinitions = [mockEntityDefinition];
const clusterClient = elasticsearchClientMock.createScopedClusterClient();
const esClient = clusterClient.asInternalUser;
const esClient = elasticsearchClientMock.createElasticsearchClient();
// mock partially installed definition
esClient.ingest.getPipeline.mockResolvedValue({});
esClient.transform.getTransformStats.mockResolvedValue({ transforms: [], count: 0 });
Expand Down Expand Up @@ -315,27 +314,22 @@ describe('install_entity_definition', () => {
});

await installBuiltInEntityDefinitions({
clusterClient,
esClient,
soClient,
definitions: builtInDefinitions,
logger: loggerMock.create(),
});

assertHasDeletedTransforms(mockEntityDefinition, clusterClient.asSecondaryAuthUser);
assertHasUpgradedDefinition(
mockEntityDefinition,
soClient,
clusterClient.asSecondaryAuthUser
);
assertHasDeletedTransforms(mockEntityDefinition, esClient);
assertHasUpgradedDefinition(mockEntityDefinition, soClient, esClient);
});

it('should reinstall when outdated version', async () => {
const updatedDefinition = {
...mockEntityDefinition,
version: semver.inc(mockEntityDefinition.version, 'major') ?? '0.0.0',
};
const clusterClient = elasticsearchClientMock.createScopedClusterClient();
const esClient = clusterClient.asInternalUser;
const esClient = elasticsearchClientMock.createElasticsearchClient();
esClient.transform.getTransformStats.mockResolvedValue({ transforms: [], count: 0 });
const soClient = savedObjectsClientMock.create();

Expand Down Expand Up @@ -365,23 +359,22 @@ describe('install_entity_definition', () => {
});

await installBuiltInEntityDefinitions({
clusterClient,
esClient,
soClient,
definitions: [updatedDefinition],
logger: loggerMock.create(),
});

assertHasDeletedTransforms(mockEntityDefinition, clusterClient.asSecondaryAuthUser);
assertHasUpgradedDefinition(updatedDefinition, soClient, clusterClient.asSecondaryAuthUser);
assertHasDeletedTransforms(mockEntityDefinition, esClient);
assertHasUpgradedDefinition(updatedDefinition, soClient, esClient);
});

it('should reinstall when stale upgrade', async () => {
const updatedDefinition = {
...mockEntityDefinition,
version: semver.inc(mockEntityDefinition.version, 'major') ?? '0.0.0',
};
const clusterClient = elasticsearchClientMock.createScopedClusterClient();
const esClient = clusterClient.asInternalUser;
const esClient = elasticsearchClientMock.createElasticsearchClient();
esClient.transform.getTransformStats.mockResolvedValue({ transforms: [], count: 0 });
const soClient = savedObjectsClientMock.create();

Expand Down Expand Up @@ -413,19 +406,18 @@ describe('install_entity_definition', () => {
});

await installBuiltInEntityDefinitions({
clusterClient,
esClient,
soClient,
definitions: [updatedDefinition],
logger: loggerMock.create(),
});

assertHasDeletedTransforms(mockEntityDefinition, clusterClient.asSecondaryAuthUser);
assertHasUpgradedDefinition(updatedDefinition, soClient, clusterClient.asSecondaryAuthUser);
assertHasDeletedTransforms(mockEntityDefinition, esClient);
assertHasUpgradedDefinition(updatedDefinition, soClient, esClient);
});

it('should reinstall when failed installation', async () => {
const clusterClient = elasticsearchClientMock.createScopedClusterClient();
const esClient = clusterClient.asInternalUser;
const esClient = elasticsearchClientMock.createElasticsearchClient();
esClient.transform.getTransformStats.mockResolvedValue({ transforms: [], count: 0 });
const soClient = savedObjectsClientMock.create();

Expand Down Expand Up @@ -456,18 +448,14 @@ describe('install_entity_definition', () => {
});

await installBuiltInEntityDefinitions({
clusterClient,
esClient,
soClient,
definitions: [mockEntityDefinition],
logger: loggerMock.create(),
});

assertHasDeletedTransforms(mockEntityDefinition, clusterClient.asSecondaryAuthUser);
assertHasUpgradedDefinition(
mockEntityDefinition,
soClient,
clusterClient.asSecondaryAuthUser
);
assertHasDeletedTransforms(mockEntityDefinition, esClient);
assertHasUpgradedDefinition(mockEntityDefinition, soClient, esClient);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/

import semver from 'semver';
import { ElasticsearchClient, IScopedClusterClient } from '@kbn/core-elasticsearch-server';
import { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
import { EntityDefinition, EntityDefinitionUpdate } from '@kbn/entities-schema';
import { Logger } from '@kbn/logging';
Expand Down Expand Up @@ -88,12 +88,12 @@ export async function installEntityDefinition({
}

export async function installBuiltInEntityDefinitions({
clusterClient,
esClient,
soClient,
logger,
definitions,
}: Omit<InstallDefinitionParams, 'definition' | 'esClient'> & {
clusterClient: IScopedClusterClient;
esClient: ElasticsearchClient;
definitions: EntityDefinition[];
}): Promise<EntityDefinition[]> {
if (definitions.length === 0) return [];
Expand All @@ -102,18 +102,18 @@ export async function installBuiltInEntityDefinitions({
const installPromises = definitions.map(async (builtInDefinition) => {
const installedDefinition = await findEntityDefinitionById({
soClient,
esClient: clusterClient.asInternalUser,
esClient,
id: builtInDefinition.id,
includeState: true,
});

if (!installedDefinition) {
// clean data from previous installation
await deleteIndices(clusterClient.asCurrentUser, builtInDefinition, logger);
await deleteIndices(esClient, builtInDefinition, logger);

return await installEntityDefinition({
definition: builtInDefinition,
esClient: clusterClient.asSecondaryAuthUser,
esClient,
soClient,
logger,
});
Expand All @@ -134,7 +134,7 @@ export async function installBuiltInEntityDefinitions({
);
return await reinstallEntityDefinition({
soClient,
clusterClient,
esClient,
logger,
definition: installedDefinition,
definitionUpdate: builtInDefinition,
Expand Down Expand Up @@ -174,14 +174,14 @@ async function install({

// stop and delete the current transforms and reinstall all the components
export async function reinstallEntityDefinition({
clusterClient,
esClient,
soClient,
definition,
definitionUpdate,
logger,
deleteData = false,
}: Omit<InstallDefinitionParams, 'esClient'> & {
clusterClient: IScopedClusterClient;
esClient: ElasticsearchClient;
definitionUpdate: EntityDefinitionUpdate;
deleteData?: boolean;
}): Promise<EntityDefinition> {
Expand All @@ -202,16 +202,16 @@ export async function reinstallEntityDefinition({
});

logger.debug(`Deleting transforms for definition [${definition.id}] v${definition.version}`);
await stopAndDeleteTransforms(clusterClient.asSecondaryAuthUser, definition, logger);
await stopAndDeleteTransforms(esClient, definition, logger);

if (deleteData) {
await deleteIndices(clusterClient.asCurrentUser, definition, logger);
await deleteIndices(esClient, definition, logger);
}

return await install({
soClient,
logger,
esClient: clusterClient.asSecondaryAuthUser,
esClient,
definition: updatedDefinition,
});
} catch (err) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,18 @@ export async function upgradeBuiltInEntityDefinitions({
);
}

const { clusterClient, soClient } = getClientsFromAPIKey({ apiKey, server });
const { esClient, soClient } = getClientsFromAPIKey({ apiKey, server });

logger.debug(`Starting built-in definitions upgrade`);
const upgradedDefinitions = await installBuiltInEntityDefinitions({
clusterClient,
esClient,
soClient,
definitions,
logger,
});

await Promise.all(
upgradedDefinitions.map((definition) =>
startTransforms(clusterClient.asSecondaryAuthUser, definition, logger)
)
upgradedDefinitions.map((definition) => startTransforms(esClient, definition, logger))
);

return { success: true, definitions: upgradedDefinitions };
Expand Down
Loading

0 comments on commit 94d7df3

Please sign in to comment.