Skip to content

Commit

Permalink
[8.x] [EEM] Disable authorization checks on endpoints (#198695) (#200565
Browse files Browse the repository at this point in the history
)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[EEM] Disable authorization checks on endpoints
(#198695)](#198695)

<!--- Backport version: 9.4.3 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Milton
Hultgren","email":"[email protected]"},"sourceCommit":{"committedDate":"2024-11-14T08:32:57Z","message":"[EEM]
Disable authorization checks on endpoints (#198695)\n\nDisable
authorization checks on all entity manager endpoints.\r\n\r\nAlso makes
two notable changes to the endpoints/EntityClient behaviour:\r\n-
previously the EntityClient accepted a `IScopedClusterClient`
and\r\nabstracted usage of asInternalUser/asCurrentUser in its methods
which\r\nmay result in unwanted behavior for consumers. It now only
accepts an\r\n`ElasticsearchClient` that is preauthenticated by the
consumers\r\n- added permissions verifications to custom definition
endpoints\r\n\r\n---------\r\n\r\nCo-authored-by: Kevin Lacabane
<[email protected]>\r\nCo-authored-by: kibanamachine
<[email protected]>","sha":"94d7df3ae7ae6c482906ec8946e61a62e05b1960","branchLabelMapping":{"^v9.0.0$":"main","^v8.17.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","v9.0.0","backport:prev-minor"],"title":"[EEM]
Disable authorization checks on
endpoints","number":198695,"url":"https://github.com/elastic/kibana/pull/198695","mergeCommit":{"message":"[EEM]
Disable authorization checks on endpoints (#198695)\n\nDisable
authorization checks on all entity manager endpoints.\r\n\r\nAlso makes
two notable changes to the endpoints/EntityClient behaviour:\r\n-
previously the EntityClient accepted a `IScopedClusterClient`
and\r\nabstracted usage of asInternalUser/asCurrentUser in its methods
which\r\nmay result in unwanted behavior for consumers. It now only
accepts an\r\n`ElasticsearchClient` that is preauthenticated by the
consumers\r\n- added permissions verifications to custom definition
endpoints\r\n\r\n---------\r\n\r\nCo-authored-by: Kevin Lacabane
<[email protected]>\r\nCo-authored-by: kibanamachine
<[email protected]>","sha":"94d7df3ae7ae6c482906ec8946e61a62e05b1960"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","branchLabelMappingKey":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/198695","number":198695,"mergeCommit":{"message":"[EEM]
Disable authorization checks on endpoints (#198695)\n\nDisable
authorization checks on all entity manager endpoints.\r\n\r\nAlso makes
two notable changes to the endpoints/EntityClient behaviour:\r\n-
previously the EntityClient accepted a `IScopedClusterClient`
and\r\nabstracted usage of asInternalUser/asCurrentUser in its methods
which\r\nmay result in unwanted behavior for consumers. It now only
accepts an\r\n`ElasticsearchClient` that is preauthenticated by the
consumers\r\n- added permissions verifications to custom definition
endpoints\r\n\r\n---------\r\n\r\nCo-authored-by: Kevin Lacabane
<[email protected]>\r\nCo-authored-by: kibanamachine
<[email protected]>","sha":"94d7df3ae7ae6c482906ec8946e61a62e05b1960"}}]}]
BACKPORT-->

Co-authored-by: Milton Hultgren <[email protected]>
  • Loading branch information
kibanamachine and miltonhultgren authored Nov 18, 2024
1 parent 6ec3126 commit a813573
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 a813573

Please sign in to comment.