= memo(({ packageInfo, theme$ }: Prop
+
+ {numTransformAssets > 0 ? (
+ <>
+
+
+ >
+ ) : null}
!!c),
+ isReauthorizationRequired,
isUnverified,
isUpdateAvailable,
};
diff --git a/x-pack/plugins/fleet/public/components/custom_assets_accordion.tsx b/x-pack/plugins/fleet/public/components/custom_assets_accordion.tsx
index 1194cd496c1dc..7c8ac417e066d 100644
--- a/x-pack/plugins/fleet/public/components/custom_assets_accordion.tsx
+++ b/x-pack/plugins/fleet/public/components/custom_assets_accordion.tsx
@@ -5,7 +5,7 @@
* 2.0.
*/
-import React from 'react';
+import React, { Fragment } from 'react';
import type { FunctionComponent } from 'react';
import {
EuiAccordion,
@@ -62,7 +62,7 @@ export const CustomAssetsAccordion: FunctionComponent
{views.map((view, index) => (
- <>
+
@@ -78,7 +78,7 @@ export const CustomAssetsAccordion: FunctionComponent
{index + 1 < views.length && }
- >
+
))}
>
diff --git a/x-pack/plugins/fleet/public/components/transform_install_as_current_user_callout.tsx b/x-pack/plugins/fleet/public/components/transform_install_as_current_user_callout.tsx
new file mode 100644
index 0000000000000..bc2af74ccbb96
--- /dev/null
+++ b/x-pack/plugins/fleet/public/components/transform_install_as_current_user_callout.tsx
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+
+import { EuiCallOut } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n-react';
+import React from 'react';
+import { uniqBy } from 'lodash';
+
+import type { PackageInfo } from '../../common';
+
+export const getNumTransformAssets = (assets?: PackageInfo['assets']) => {
+ if (
+ !assets ||
+ !(Array.isArray(assets.elasticsearch?.transform) && assets.elasticsearch?.transform?.length > 0)
+ ) {
+ return 0;
+ }
+
+ return uniqBy(assets.elasticsearch?.transform, 'file').length;
+};
+export const TransformInstallWithCurrentUserPermissionCallout: React.FunctionComponent<{
+ count: number;
+}> = ({ count }) => {
+ return (
+
+
+
+ );
+};
diff --git a/x-pack/plugins/fleet/public/hooks/use_kibana_link.ts b/x-pack/plugins/fleet/public/hooks/use_kibana_link.ts
index 7ab1b17b77caa..739fd078fe4db 100644
--- a/x-pack/plugins/fleet/public/hooks/use_kibana_link.ts
+++ b/x-pack/plugins/fleet/public/hooks/use_kibana_link.ts
@@ -32,7 +32,7 @@ export const getHrefToObjectInKibanaApp = ({
id,
http,
}: {
- type: KibanaAssetType;
+ type: KibanaAssetType | undefined;
id: string;
http: HttpStart;
}): undefined | string => {
diff --git a/x-pack/plugins/fleet/public/hooks/use_request/epm.ts b/x-pack/plugins/fleet/public/hooks/use_request/epm.ts
index 1a50be67099a5..f050820508d98 100644
--- a/x-pack/plugins/fleet/public/hooks/use_request/epm.ts
+++ b/x-pack/plugins/fleet/public/hooks/use_request/epm.ts
@@ -224,6 +224,18 @@ export const sendRemovePackage = (pkgName: string, pkgVersion: string, force: bo
});
};
+export const sendRequestReauthorizeTransforms = (
+ pkgName: string,
+ pkgVersion: string,
+ transforms: Array<{ transformId: string }>
+) => {
+ return sendRequest({
+ path: epmRouteService.getReauthorizeTransformsPath(pkgName, pkgVersion),
+ method: 'post',
+ body: { transforms },
+ });
+};
+
interface UpdatePackageArgs {
pkgName: string;
pkgVersion: string;
diff --git a/x-pack/plugins/fleet/public/services/has_deferred_installations.test.ts b/x-pack/plugins/fleet/public/services/has_deferred_installations.test.ts
new file mode 100644
index 0000000000000..f4f39ed7115d1
--- /dev/null
+++ b/x-pack/plugins/fleet/public/services/has_deferred_installations.test.ts
@@ -0,0 +1,99 @@
+/*
+ * 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.
+ */
+
+import type { EsAssetReference } from '../../common/types';
+import type { PackageInfo } from '../types';
+
+import { ElasticsearchAssetType } from '../../common/types';
+
+import { hasDeferredInstallations } from './has_deferred_installations';
+
+import { ExperimentalFeaturesService } from '.';
+
+const mockGet = jest.spyOn(ExperimentalFeaturesService, 'get');
+
+const createPackage = ({
+ installedEs = [],
+}: {
+ installedEs?: EsAssetReference[];
+} = {}): PackageInfo => ({
+ name: 'test-package',
+ description: 'Test Package',
+ title: 'Test Package',
+ version: '0.0.1',
+ latestVersion: '0.0.1',
+ release: 'experimental',
+ format_version: '1.0.0',
+ owner: { github: 'elastic/fleet' },
+ policy_templates: [],
+ // @ts-ignore
+ assets: {},
+ savedObject: {
+ id: '1234',
+ type: 'epm-package',
+ references: [],
+ attributes: {
+ installed_kibana: [],
+ installed_es: installedEs ?? [],
+ es_index_patterns: {},
+ name: 'test-package',
+ version: '0.0.1',
+ install_status: 'installed',
+ install_version: '0.0.1',
+ install_started_at: new Date().toString(),
+ install_source: 'registry',
+ verification_status: 'verified',
+ verification_key_id: '',
+ },
+ },
+});
+
+describe('isPackageUnverified', () => {
+ describe('When experimental feature is disabled', () => {
+ beforeEach(() => {
+ // @ts-ignore don't want to define all experimental features here
+ mockGet.mockReturnValue({
+ packageVerification: false,
+ } as ReturnType);
+ });
+
+ it('Should return false for a package with no saved object', () => {
+ const noSoPkg = createPackage();
+ // @ts-ignore we know pkg has savedObject but ts doesn't
+ delete noSoPkg.savedObject;
+ expect(hasDeferredInstallations(noSoPkg)).toEqual(false);
+ });
+
+ it('Should return true for a package with at least one asset deferred', () => {
+ const pkgWithDeferredInstallations = createPackage({
+ installedEs: [
+ { id: '', type: ElasticsearchAssetType.ingestPipeline },
+ { id: '', type: ElasticsearchAssetType.transform, deferred: true },
+ ],
+ });
+ // @ts-ignore we know pkg has savedObject but ts doesn't
+ expect(hasDeferredInstallations(pkgWithDeferredInstallations)).toEqual(true);
+ });
+
+ it('Should return false for a package that has no asset deferred', () => {
+ const pkgWithoutDeferredInstallations = createPackage({
+ installedEs: [
+ { id: '', type: ElasticsearchAssetType.ingestPipeline },
+ { id: '', type: ElasticsearchAssetType.transform, deferred: false },
+ ],
+ });
+ expect(hasDeferredInstallations(pkgWithoutDeferredInstallations)).toEqual(false);
+ });
+
+ it('Should return false for a package that has no asset', () => {
+ const pkgWithoutDeferredInstallations = createPackage({
+ installedEs: [],
+ });
+ expect(hasDeferredInstallations(pkgWithoutDeferredInstallations)).toEqual(false);
+ });
+ });
+});
diff --git a/x-pack/plugins/fleet/public/services/has_deferred_installations.ts b/x-pack/plugins/fleet/public/services/has_deferred_installations.ts
new file mode 100644
index 0000000000000..8026ca0ae39a0
--- /dev/null
+++ b/x-pack/plugins/fleet/public/services/has_deferred_installations.ts
@@ -0,0 +1,17 @@
+/*
+ * 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.
+ */
+
+import type { PackageInfo, PackageListItem } from '../../common';
+
+export const getDeferredInstallationsCnt = (pkg?: PackageInfo | PackageListItem | null): number => {
+ return pkg && 'savedObject' in pkg && pkg.savedObject
+ ? pkg.savedObject.attributes?.installed_es?.filter((d) => d.deferred).length
+ : 0;
+};
+
+export const hasDeferredInstallations = (pkg?: PackageInfo | PackageListItem | null): boolean =>
+ getDeferredInstallationsCnt(pkg) > 0;
diff --git a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts
index a31ba8fa5cb70..9601052fc415c 100644
--- a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts
+++ b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts
@@ -15,6 +15,8 @@ import type {
import pMap from 'p-map';
import { safeDump } from 'js-yaml';
+import { HTTPAuthorizationHeader } from '../../../common/http_authorization_header';
+
import { fullAgentPolicyToYaml } from '../../../common/services';
import { appContextService, agentPolicyService } from '../../services';
import { getAgentsByKuery } from '../../services/agents';
@@ -175,6 +177,8 @@ export const createAgentPolicyHandler: FleetRequestHandler<
const monitoringEnabled = request.body.monitoring_enabled;
const { has_fleet_server: hasFleetServer, ...newPolicy } = request.body;
const spaceId = fleetContext.spaceId;
+ const authorizationHeader = HTTPAuthorizationHeader.parseFromRequest(request, user?.username);
+
try {
const body: CreateAgentPolicyResponse = {
item: await createAgentPolicyWithPackages({
@@ -186,6 +190,7 @@ export const createAgentPolicyHandler: FleetRequestHandler<
monitoringEnabled,
spaceId,
user,
+ authorizationHeader,
}),
};
diff --git a/x-pack/plugins/fleet/server/routes/epm/handlers.ts b/x-pack/plugins/fleet/server/routes/epm/handlers.ts
index 8bacf87284271..0f2232593fd5e 100644
--- a/x-pack/plugins/fleet/server/routes/epm/handlers.ts
+++ b/x-pack/plugins/fleet/server/routes/epm/handlers.ts
@@ -12,6 +12,11 @@ import mime from 'mime-types';
import semverValid from 'semver/functions/valid';
import type { ResponseHeaders, KnownHeaders, HttpResponseOptions } from '@kbn/core/server';
+import { HTTPAuthorizationHeader } from '../../../common/http_authorization_header';
+
+import { generateTransformSecondaryAuthHeaders } from '../../services/api_keys/transform_api_keys';
+import { handleTransformReauthorizeAndStart } from '../../services/epm/elasticsearch/transform/reauthorize';
+
import type {
GetInfoResponse,
InstallPackageResponse,
@@ -54,12 +59,13 @@ import {
} from '../../services/epm/packages';
import type { BulkInstallResponse } from '../../services/epm/packages';
import { defaultFleetErrorHandler, fleetErrorToResponseOptions, FleetError } from '../../errors';
-import { checkAllowedPackages, licenseService } from '../../services';
+import { appContextService, checkAllowedPackages, licenseService } from '../../services';
import { getArchiveEntry } from '../../services/epm/archive/cache';
import { getAsset } from '../../services/epm/archive/storage';
import { getPackageUsageStats } from '../../services/epm/packages/get';
import { updatePackage } from '../../services/epm/packages/update';
import { getGpgKeyIdOrUndefined } from '../../services/epm/packages/package_verification';
+import type { ReauthorizeTransformRequestSchema } from '../../types';
const CACHE_CONTROL_10_MINUTES_HEADER: HttpResponseOptions['headers'] = {
'cache-control': 'max-age=600',
@@ -282,8 +288,12 @@ export const installPackageFromRegistryHandler: FleetRequestHandler<
const fleetContext = await context.fleet;
const savedObjectsClient = fleetContext.internalSoClient;
const esClient = coreContext.elasticsearch.client.asInternalUser;
+ const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined;
+
const { pkgName, pkgVersion } = request.params;
+ const authorizationHeader = HTTPAuthorizationHeader.parseFromRequest(request, user?.username);
+
const spaceId = fleetContext.spaceId;
const res = await installPackage({
installSource: 'registry',
@@ -294,6 +304,7 @@ export const installPackageFromRegistryHandler: FleetRequestHandler<
force: request.body?.force,
ignoreConstraints: request.body?.ignore_constraints,
prerelease: request.query?.prerelease,
+ authorizationHeader,
});
if (!res.error) {
@@ -334,6 +345,7 @@ export const bulkInstallPackagesFromRegistryHandler: FleetRequestHandler<
const savedObjectsClient = fleetContext.internalSoClient;
const esClient = coreContext.elasticsearch.client.asInternalUser;
const spaceId = fleetContext.spaceId;
+
const bulkInstalledResponses = await bulkInstallPackages({
savedObjectsClient,
esClient,
@@ -361,6 +373,7 @@ export const installPackageByUploadHandler: FleetRequestHandler<
body: { message: 'Requires Enterprise license' },
});
}
+
const coreContext = await context.core;
const fleetContext = await context.fleet;
const savedObjectsClient = fleetContext.internalSoClient;
@@ -368,6 +381,10 @@ export const installPackageByUploadHandler: FleetRequestHandler<
const contentType = request.headers['content-type'] as string; // from types it could also be string[] or undefined but this is checked later
const archiveBuffer = Buffer.from(request.body);
const spaceId = fleetContext.spaceId;
+ const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined;
+
+ const authorizationHeader = HTTPAuthorizationHeader.parseFromRequest(request, user?.username);
+
const res = await installPackage({
installSource: 'upload',
savedObjectsClient,
@@ -375,6 +392,7 @@ export const installPackageByUploadHandler: FleetRequestHandler<
archiveBuffer,
spaceId,
contentType,
+ authorizationHeader,
});
if (!res.error) {
const body: InstallPackageResponse = {
@@ -432,3 +450,60 @@ export const getVerificationKeyIdHandler: FleetRequestHandler = async (
return defaultFleetErrorHandler({ error, response });
}
};
+
+/**
+ * Create transform and optionally start transform
+ * Note that we want to add the current user's roles/permissions to the es-secondary-auth with a API Key.
+ * If API Key has insufficient permissions, it should still create the transforms but not start it
+ * Instead of failing, we need to allow package to continue installing other assets
+ * and prompt for users to authorize the transforms with the appropriate permissions after package is done installing
+ */
+export const reauthorizeTransformsHandler: FleetRequestHandler<
+ TypeOf,
+ TypeOf,
+ TypeOf
+> = async (context, request, response) => {
+ const coreContext = await context.core;
+ const savedObjectsClient = (await context.fleet).internalSoClient;
+
+ const esClient = coreContext.elasticsearch.client.asInternalUser;
+ const { pkgName, pkgVersion } = request.params;
+ const { transforms } = request.body;
+
+ let username;
+ try {
+ const user = await appContextService.getSecurity()?.authc.getCurrentUser(request);
+ if (user) {
+ username = user.username;
+ }
+ } catch (e) {
+ // User might not have permission to get username, or security is not enabled, and that's okay.
+ }
+
+ try {
+ const logger = appContextService.getLogger();
+ const authorizationHeader = HTTPAuthorizationHeader.parseFromRequest(request, username);
+ const secondaryAuth = await generateTransformSecondaryAuthHeaders({
+ authorizationHeader,
+ logger,
+ username,
+ pkgName,
+ pkgVersion,
+ });
+
+ const resp = await handleTransformReauthorizeAndStart({
+ esClient,
+ savedObjectsClient,
+ logger,
+ pkgName,
+ pkgVersion,
+ transforms,
+ secondaryAuth,
+ username,
+ });
+
+ return response.ok({ body: resp });
+ } catch (error) {
+ return defaultFleetErrorHandler({ error, response });
+ }
+};
diff --git a/x-pack/plugins/fleet/server/routes/epm/index.ts b/x-pack/plugins/fleet/server/routes/epm/index.ts
index 5c2a34cd90551..b707a8b80e582 100644
--- a/x-pack/plugins/fleet/server/routes/epm/index.ts
+++ b/x-pack/plugins/fleet/server/routes/epm/index.ts
@@ -39,6 +39,7 @@ import {
GetStatsRequestSchema,
UpdatePackageRequestSchema,
UpdatePackageRequestSchemaDeprecated,
+ ReauthorizeTransformRequestSchema,
} from '../../types';
import {
@@ -54,6 +55,7 @@ import {
getStatsHandler,
updatePackageHandler,
getVerificationKeyIdHandler,
+ reauthorizeTransformsHandler,
} from './handlers';
const MAX_FILE_SIZE_BYTES = 104857600; // 100MB
@@ -294,4 +296,26 @@ export const registerRoutes = (router: FleetAuthzRouter) => {
return resp;
}
);
+
+ // Update transforms with es-secondary-authorization headers,
+ // append authorized_by to transform's _meta, and start transforms
+ router.post(
+ {
+ path: EPM_API_ROUTES.REAUTHORIZE_TRANSFORMS,
+ validate: ReauthorizeTransformRequestSchema,
+ fleetAuthz: {
+ integrations: { installPackages: true },
+ packagePrivileges: {
+ transform: {
+ actions: {
+ canStartStopTransform: {
+ executePackageAction: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ reauthorizeTransformsHandler
+ );
};
diff --git a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts
index 51b4056843d25..9edfad74b7c5b 100644
--- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts
+++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts
@@ -13,6 +13,8 @@ import type { RequestHandler } from '@kbn/core/server';
import { groupBy, keyBy } from 'lodash';
+import { HTTPAuthorizationHeader } from '../../../common/http_authorization_header';
+
import { populatePackagePolicyAssignedAgentsCount } from '../../services/package_policies/populate_package_policy_assigned_agents_count';
import {
@@ -219,6 +221,8 @@ export const createPackagePolicyHandler: FleetRequestHandler<
const esClient = coreContext.elasticsearch.client.asInternalUser;
const user = appContextService.getSecurity()?.authc.getCurrentUser(request) || undefined;
const { force, package: pkg, ...newPolicy } = request.body;
+ const authorizationHeader = HTTPAuthorizationHeader.parseFromRequest(request, user?.username);
+
if ('output_id' in newPolicy) {
// TODO Remove deprecated APIs https://github.com/elastic/kibana/issues/121485
delete newPolicy.output_id;
@@ -248,6 +252,7 @@ export const createPackagePolicyHandler: FleetRequestHandler<
}
// Create package policy
+
const packagePolicy = await fleetContext.packagePolicyService.asCurrentUser.create(
soClient,
esClient,
@@ -256,6 +261,7 @@ export const createPackagePolicyHandler: FleetRequestHandler<
user,
force,
spaceId,
+ authorizationHeader,
},
context,
request
diff --git a/x-pack/plugins/fleet/server/saved_objects/index.ts b/x-pack/plugins/fleet/server/saved_objects/index.ts
index 8f0db94d1d31c..763c6d6c69136 100644
--- a/x-pack/plugins/fleet/server/saved_objects/index.ts
+++ b/x-pack/plugins/fleet/server/saved_objects/index.ts
@@ -243,6 +243,7 @@ const getSavedObjectTypes = (): { [key: string]: SavedObjectsType } => ({
id: { type: 'keyword' },
type: { type: 'keyword' },
version: { type: 'keyword' },
+ deferred: { type: 'boolean' },
},
},
installed_kibana: {
diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts
index 96b8fd55e3b84..aade7b382a7d3 100644
--- a/x-pack/plugins/fleet/server/services/agent_policy.ts
+++ b/x-pack/plugins/fleet/server/services/agent_policy.ts
@@ -22,6 +22,8 @@ import type { BulkResponseItem } from '@elastic/elasticsearch/lib/api/typesWithB
import { DEFAULT_SPACE_ID } from '@kbn/spaces-plugin/common/constants';
+import type { HTTPAuthorizationHeader } from '../../common/http_authorization_header';
+
import {
AGENT_POLICY_SAVED_OBJECT_TYPE,
AGENTS_PREFIX,
@@ -209,7 +211,11 @@ class AgentPolicyService {
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
agentPolicy: NewAgentPolicy,
- options: { id?: string; user?: AuthenticatedUser } = {}
+ options: {
+ id?: string;
+ user?: AuthenticatedUser;
+ authorizationHeader?: HTTPAuthorizationHeader | null;
+ } = {}
): Promise {
// Ensure an ID is provided, so we can include it in the audit logs below
if (!options.id) {
@@ -444,7 +450,12 @@ class AgentPolicyService {
esClient: ElasticsearchClient,
id: string,
agentPolicy: Partial,
- options?: { user?: AuthenticatedUser; force?: boolean; spaceId?: string }
+ options?: {
+ user?: AuthenticatedUser;
+ force?: boolean;
+ spaceId?: string;
+ authorizationHeader?: HTTPAuthorizationHeader | null;
+ }
): Promise {
if (agentPolicy.name) {
await this.requireUniqueName(soClient, {
@@ -479,6 +490,7 @@ class AgentPolicyService {
esClient,
packagesToInstall,
spaceId: options?.spaceId || DEFAULT_SPACE_ID,
+ authorizationHeader: options?.authorizationHeader,
});
}
diff --git a/x-pack/plugins/fleet/server/services/agent_policy_create.ts b/x-pack/plugins/fleet/server/services/agent_policy_create.ts
index 1f7aeb2bc985f..11cb82123a347 100644
--- a/x-pack/plugins/fleet/server/services/agent_policy_create.ts
+++ b/x-pack/plugins/fleet/server/services/agent_policy_create.ts
@@ -9,6 +9,8 @@ import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/
import type { AuthenticatedUser } from '@kbn/security-plugin/common/model';
+import type { HTTPAuthorizationHeader } from '../../common/http_authorization_header';
+
import {
FLEET_ELASTIC_AGENT_PACKAGE,
FLEET_SERVER_PACKAGE,
@@ -48,7 +50,11 @@ async function createPackagePolicy(
esClient: ElasticsearchClient,
agentPolicy: AgentPolicy,
packageToInstall: string,
- options: { spaceId: string; user: AuthenticatedUser | undefined }
+ options: {
+ spaceId: string;
+ user: AuthenticatedUser | undefined;
+ authorizationHeader?: HTTPAuthorizationHeader | null;
+ }
) {
const newPackagePolicy = await packagePolicyService
.buildPackagePolicyFromPackage(soClient, packageToInstall)
@@ -71,6 +77,7 @@ async function createPackagePolicy(
spaceId: options.spaceId,
user: options.user,
bumpRevision: false,
+ authorizationHeader: options.authorizationHeader,
});
}
@@ -83,6 +90,7 @@ interface CreateAgentPolicyParams {
monitoringEnabled?: string[];
spaceId: string;
user?: AuthenticatedUser;
+ authorizationHeader?: HTTPAuthorizationHeader | null;
}
export async function createAgentPolicyWithPackages({
@@ -94,6 +102,7 @@ export async function createAgentPolicyWithPackages({
monitoringEnabled,
spaceId,
user,
+ authorizationHeader,
}: CreateAgentPolicyParams) {
let agentPolicyId = newPolicy.id;
const packagesToInstall = [];
@@ -118,6 +127,7 @@ export async function createAgentPolicyWithPackages({
esClient,
packagesToInstall,
spaceId,
+ authorizationHeader,
});
}
@@ -126,6 +136,7 @@ export async function createAgentPolicyWithPackages({
const agentPolicy = await agentPolicyService.create(soClient, esClient, policy, {
user,
id: agentPolicyId,
+ authorizationHeader,
});
// Create the fleet server package policy and add it to agent policy.
@@ -133,6 +144,7 @@ export async function createAgentPolicyWithPackages({
await createPackagePolicy(soClient, esClient, agentPolicy, FLEET_SERVER_PACKAGE, {
spaceId,
user,
+ authorizationHeader,
});
}
@@ -141,6 +153,7 @@ export async function createAgentPolicyWithPackages({
await createPackagePolicy(soClient, esClient, agentPolicy, FLEET_SYSTEM_PACKAGE, {
spaceId,
user,
+ authorizationHeader,
});
}
diff --git a/x-pack/plugins/fleet/server/services/api_keys/transform_api_keys.ts b/x-pack/plugins/fleet/server/services/api_keys/transform_api_keys.ts
new file mode 100644
index 0000000000000..90d873289fa88
--- /dev/null
+++ b/x-pack/plugins/fleet/server/services/api_keys/transform_api_keys.ts
@@ -0,0 +1,124 @@
+/*
+ * 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.
+ */
+
+import type { CreateAPIKeyParams } from '@kbn/security-plugin/server';
+import type { FakeRawRequest, Headers } from '@kbn/core-http-server';
+import { CoreKibanaRequest } from '@kbn/core-http-router-server-internal';
+
+import type { Logger } from '@kbn/logging';
+
+import { appContextService } from '..';
+
+import type { HTTPAuthorizationHeader } from '../../../common/http_authorization_header';
+
+import type {
+ TransformAPIKey,
+ SecondaryAuthorizationHeader,
+} from '../../../common/types/models/transform_api_key';
+
+export function isTransformApiKey(arg: any): arg is TransformAPIKey {
+ return (
+ arg &&
+ arg.hasOwnProperty('api_key') &&
+ arg.hasOwnProperty('encoded') &&
+ typeof arg.encoded === 'string'
+ );
+}
+
+function createKibanaRequestFromAuth(authorizationHeader: HTTPAuthorizationHeader) {
+ const requestHeaders: Headers = {
+ authorization: authorizationHeader.toString(),
+ };
+ const fakeRawRequest: FakeRawRequest = {
+ headers: requestHeaders,
+ path: '/',
+ };
+
+ // Since we're using API keys and accessing elasticsearch can only be done
+ // via a request, we're faking one with the proper authorization headers.
+ const fakeRequest = CoreKibanaRequest.from(fakeRawRequest);
+
+ return fakeRequest;
+}
+
+/** This function generates a new API based on current Kibana's user request.headers.authorization
+ * then formats it into a es-secondary-authorization header object
+ * @param authorizationHeader:
+ * @param createParams
+ */
+export async function generateTransformSecondaryAuthHeaders({
+ authorizationHeader,
+ createParams,
+ logger,
+ username,
+ pkgName,
+ pkgVersion,
+}: {
+ authorizationHeader: HTTPAuthorizationHeader | null | undefined;
+ logger: Logger;
+ createParams?: CreateAPIKeyParams;
+ username?: string;
+ pkgName?: string;
+ pkgVersion?: string;
+}): Promise {
+ if (!authorizationHeader) {
+ return;
+ }
+
+ const fakeKibanaRequest = createKibanaRequestFromAuth(authorizationHeader);
+
+ const user = username ?? authorizationHeader.getUsername();
+
+ const name = pkgName
+ ? `${pkgName}${pkgVersion ? '-' + pkgVersion : ''}-transform${user ? '-by-' + user : ''}`
+ : `fleet-transform-api-key`;
+
+ const security = appContextService.getSecurity();
+
+ // If security is not enabled or available, we can't generate api key
+ // but that's ok, cause all the index and transform commands should work
+ if (!security) return;
+
+ try {
+ const apiKeyWithCurrentUserPermission = await security?.authc.apiKeys.grantAsInternalUser(
+ fakeKibanaRequest,
+ createParams ?? {
+ name,
+ metadata: {
+ managed_by: 'fleet',
+ managed: true,
+ type: 'transform',
+ },
+ role_descriptors: {},
+ }
+ );
+
+ logger.debug(`Created api_key name: ${name}`);
+ let encodedApiKey: TransformAPIKey['encoded'] | null = null;
+
+ // Property 'encoded' does exist in the resp coming back from request
+ // and is required to use in authentication headers
+ // It's just not defined in returned GrantAPIKeyResult type
+ if (isTransformApiKey(apiKeyWithCurrentUserPermission)) {
+ encodedApiKey = apiKeyWithCurrentUserPermission.encoded;
+ }
+
+ const secondaryAuth =
+ encodedApiKey !== null
+ ? {
+ headers: {
+ 'es-secondary-authorization': `ApiKey ${encodedApiKey}`,
+ },
+ }
+ : undefined;
+
+ return secondaryAuth;
+ } catch (e) {
+ logger.debug(`Failed to create api_key: ${name} because ${e}`);
+ return undefined;
+ }
+}
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/index/update_settings.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/index/update_settings.ts
index a381f19a3d9fa..766d03f6a776c 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/index/update_settings.ts
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/index/update_settings.ts
@@ -9,6 +9,8 @@ import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
import type { IndicesIndexSettings } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
+import { appContextService } from '../../..';
+
import { retryTransientEsErrors } from '../retry';
export async function updateIndexSettings(
@@ -16,6 +18,8 @@ export async function updateIndexSettings(
index: string,
settings: IndicesIndexSettings
): Promise {
+ const logger = appContextService.getLogger();
+
if (index) {
try {
await retryTransientEsErrors(() =>
@@ -25,7 +29,8 @@ export async function updateIndexSettings(
})
);
} catch (err) {
- throw new Error(`could not update index settings for ${index}`);
+ // No need to throw error and block installation process
+ logger.debug(`Could not update index settings for ${index} because ${err}`);
}
}
}
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts
index dc775d6f52e01..78469e7654504 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts
@@ -11,6 +11,12 @@ import { safeLoad } from 'js-yaml';
import { isPopulatedObject } from '@kbn/ml-is-populated-object';
import { uniqBy } from 'lodash';
+import type { HTTPAuthorizationHeader } from '../../../../../common/http_authorization_header';
+
+import type { SecondaryAuthorizationHeader } from '../../../../../common/types/models/transform_api_key';
+
+import { generateTransformSecondaryAuthHeaders } from '../../../api_keys/transform_api_keys';
+
import {
PACKAGE_TEMPLATE_SUFFIX,
USER_SETTINGS_TEMPLATE_SUFFIX,
@@ -36,7 +42,8 @@ import { getInstallation } from '../../packages';
import { retryTransientEsErrors } from '../retry';
import { deleteTransforms } from './remove';
-import { getAsset, TRANSFORM_DEST_IDX_ALIAS_LATEST_SFX } from './common';
+import { getAsset } from './common';
+import { getDestinationIndexAliases } from './transform_utils';
const DEFAULT_TRANSFORM_TEMPLATES_PRIORITY = 250;
enum TRANSFORM_SPECS_TYPES {
@@ -58,6 +65,7 @@ interface TransformInstallation extends TransformModuleBase {
content: any;
transformVersion?: string;
installationOrder?: number;
+ runAsKibanaSystem?: boolean;
}
const installLegacyTransformsAssets = async (
@@ -137,7 +145,8 @@ const processTransformAssetsPerModule = (
installablePackage: InstallablePackage,
installNameSuffix: string,
transformPaths: string[],
- previousInstalledTransformEsAssets: EsAssetReference[] = []
+ previousInstalledTransformEsAssets: EsAssetReference[] = [],
+ username?: string
) => {
const transformsSpecifications = new Map();
const destinationIndexTemplates: DestinationIndexTemplateInstallation[] = [];
@@ -195,10 +204,6 @@ const processTransformAssetsPerModule = (
const installationOrder =
isFinite(content._meta?.order) && content._meta?.order >= 0 ? content._meta?.order : 0;
const transformVersion = content._meta?.fleet_transform_version ?? '0.1.0';
- // The “all” alias for the transform destination indices will be adjusted to include the new transform destination index as well as everything it previously included
- const allIndexAliasName = `${content.dest.index}.all`;
- // The “latest” alias for the transform destination indices will point solely to the new transform destination index
- const latestIndexAliasName = `${content.dest.index}.latest`;
transformsSpecifications
.get(transformModuleId)
@@ -206,24 +211,30 @@ const processTransformAssetsPerModule = (
// Create two aliases associated with the destination index
// for better handling during upgrades
- const alias = {
- [allIndexAliasName]: {},
- [latestIndexAliasName]: {},
- };
+ const aliases = getDestinationIndexAliases(content.dest.aliases);
+ const aliasNames = aliases.map((a) => a.alias);
+ // Override yml settings with alia format for transform's dest.aliases
+ content.dest.aliases = aliases;
- const versionedIndexName = `${content.dest.index}-${installNameSuffix}`;
- content.dest.index = versionedIndexName;
indicesToAddRefs.push({
- id: versionedIndexName,
+ id: content.dest.index,
type: ElasticsearchAssetType.index,
});
+
+ // If run_as_kibana_system is not set, or is set to true, then run as kibana_system user
+ // else, run with user's secondary credentials
+ const runAsKibanaSystem = content._meta?.run_as_kibana_system !== false;
+
transformsSpecifications.get(transformModuleId)?.set('destinationIndex', content.dest);
- transformsSpecifications.get(transformModuleId)?.set('destinationIndexAlias', alias);
+ transformsSpecifications.get(transformModuleId)?.set('destinationIndexAlias', aliases);
transformsSpecifications.get(transformModuleId)?.set('transform', content);
transformsSpecifications.get(transformModuleId)?.set('transformVersion', transformVersion);
+
content._meta = {
...(content._meta ?? {}),
...getESAssetMetadata({ packageName: installablePackage.name }),
+ ...(username ? { installed_by: username } : {}),
+ run_as_kibana_system: runAsKibanaSystem,
};
const installationName = getTransformAssetNameForInstallation(
@@ -236,13 +247,14 @@ const processTransformAssetsPerModule = (
const currentTransformSameAsPrev =
previousInstalledTransformEsAssets.find((t) => t.id === installationName) !== undefined;
if (previousInstalledTransformEsAssets.length === 0) {
- aliasesRefs.push(allIndexAliasName, latestIndexAliasName);
+ aliasesRefs.push(...aliasNames);
transforms.push({
transformModuleId,
installationName,
installationOrder,
transformVersion,
content,
+ runAsKibanaSystem,
});
transformsSpecifications.get(transformModuleId)?.set('transformVersionChanged', true);
} else {
@@ -277,9 +289,12 @@ const processTransformAssetsPerModule = (
installationOrder,
transformVersion,
content,
+ runAsKibanaSystem,
});
transformsSpecifications.get(transformModuleId)?.set('transformVersionChanged', true);
- aliasesRefs.push(allIndexAliasName, latestIndexAliasName);
+ if (aliasNames.length > 0) {
+ aliasesRefs.push(...aliasNames);
+ }
} else {
transformsSpecifications.get(transformModuleId)?.set('transformVersionChanged', false);
}
@@ -371,9 +386,12 @@ const installTransformsAssets = async (
savedObjectsClient: SavedObjectsClientContract,
logger: Logger,
esReferences: EsAssetReference[] = [],
- previousInstalledTransformEsAssets: EsAssetReference[] = []
+ previousInstalledTransformEsAssets: EsAssetReference[] = [],
+ authorizationHeader?: HTTPAuthorizationHeader | null
) => {
let installedTransforms: EsAssetReference[] = [];
+ const username = authorizationHeader?.getUsername();
+
if (transformPaths.length > 0) {
const {
indicesToAddRefs,
@@ -383,23 +401,27 @@ const installTransformsAssets = async (
transforms,
destinationIndexTemplates,
transformsSpecifications,
- aliasesRefs,
transformsToRemove,
transformsToRemoveWithDestIndex,
} = processTransformAssetsPerModule(
installablePackage,
installNameSuffix,
transformPaths,
- previousInstalledTransformEsAssets
+ previousInstalledTransformEsAssets,
+ username
);
- // ensure the .latest alias points to only the latest
- // by removing any associate of old destination indices
- await Promise.all(
- aliasesRefs
- .filter((a) => a.endsWith(TRANSFORM_DEST_IDX_ALIAS_LATEST_SFX))
- .map((alias) => deleteAliasFromIndices({ esClient, logger, alias }))
- );
+ // By default, for internal Elastic packages that touch system indices, we want to run as internal user
+ // so we set runAsKibanaSystem: true by default (e.g. when run_as_kibana_system set to true/not defined in yml file).
+ // If package should be installed as the logged in user, set run_as_kibana_system: false,
+ // and pass es-secondary-authorization in header when creating the transforms.
+ const secondaryAuth = await generateTransformSecondaryAuthHeaders({
+ authorizationHeader,
+ logger,
+ pkgName: installablePackage.name,
+ pkgVersion: installablePackage.version,
+ username,
+ });
// delete all previous transform
await Promise.all([
@@ -407,13 +429,15 @@ const installTransformsAssets = async (
esClient,
transformsToRemoveWithDestIndex.map((asset) => asset.id),
// Delete destination indices if specified or if from old json schema
- true
+ true,
+ secondaryAuth
),
deleteTransforms(
esClient,
transformsToRemove.map((asset) => asset.id),
// Else, keep destination indices by default
- false
+ false,
+ secondaryAuth
),
]);
@@ -492,58 +516,6 @@ const installTransformsAssets = async (
.filter((p) => p !== undefined)
);
- // create destination indices
- await Promise.all(
- transforms.map(async (transform) => {
- const index = transform.content.dest.index;
-
- const aliases = transformsSpecifications
- .get(transform.transformModuleId)
- ?.get('destinationIndexAlias');
- try {
- const resp = await retryTransientEsErrors(
- () =>
- esClient.indices.create(
- {
- index,
- aliases,
- },
- { ignore: [400] }
- ),
- { logger }
- );
- logger.debug(`Created destination index: ${index}`);
-
- // If index already exists, we still need to update the destination index alias
- // to point '{destinationIndexName}.latest' to the versioned index
- // @ts-ignore status is a valid field of resp
- if (resp.status === 400 && aliases) {
- await retryTransientEsErrors(
- () =>
- esClient.indices.updateAliases({
- body: {
- actions: Object.keys(aliases).map((alias) => ({ add: { index, alias } })),
- },
- }),
- { logger }
- );
- logger.debug(`Created aliases for destination index: ${index}`);
- }
- } catch (err) {
- logger.error(
- `Error creating destination index: ${JSON.stringify({
- index,
- aliases: transformsSpecifications
- .get(transform.transformModuleId)
- ?.get('destinationIndexAlias'),
- })} with error ${err}`
- );
-
- throw new Error(err.message);
- }
- })
- );
-
// If the transforms have specific installation order, install & optionally start transforms sequentially
const shouldInstallSequentially =
uniqBy(transforms, 'installationOrder').length === transforms.length;
@@ -555,6 +527,7 @@ const installTransformsAssets = async (
logger,
transform,
startTransform: transformsSpecifications.get(transform.transformModuleId)?.get('start'),
+ secondaryAuth: transform.runAsKibanaSystem !== false ? undefined : secondaryAuth,
});
installedTransforms.push(installTransform);
}
@@ -566,22 +539,42 @@ const installTransformsAssets = async (
logger,
transform,
startTransform: transformsSpecifications.get(transform.transformModuleId)?.get('start'),
+ secondaryAuth: transform.runAsKibanaSystem !== false ? undefined : secondaryAuth,
});
});
installedTransforms = await Promise.all(transformsPromises).then((results) => results.flat());
}
+
+ // If user does not have sufficient permissions to start the transforms,
+ // we need to mark them as deferred installations without blocking full package installation
+ // so that they can be updated/re-authorized later
+
+ if (installedTransforms.length > 0) {
+ // get and save refs associated with the transforms before installing
+ esReferences = await updateEsAssetReferences(
+ savedObjectsClient,
+ installablePackage.name,
+ esReferences,
+ {
+ assetsToRemove: installedTransforms,
+ assetsToAdd: installedTransforms,
+ }
+ );
+ }
}
return { installedTransforms, esReferences };
};
+
export const installTransforms = async (
installablePackage: InstallablePackage,
paths: string[],
esClient: ElasticsearchClient,
savedObjectsClient: SavedObjectsClientContract,
logger: Logger,
- esReferences?: EsAssetReference[]
+ esReferences?: EsAssetReference[],
+ authorizationHeader?: HTTPAuthorizationHeader | null
) => {
const transformPaths = paths.filter((path) => isTransform(path));
@@ -628,7 +621,8 @@ export const installTransforms = async (
savedObjectsClient,
logger,
esReferences,
- previousInstalledTransformEsAssets
+ previousInstalledTransformEsAssets,
+ authorizationHeader
);
};
@@ -637,65 +631,59 @@ export const isTransform = (path: string) => {
return !path.endsWith('/') && pathParts.type === ElasticsearchAssetType.transform;
};
-async function deleteAliasFromIndices({
- esClient,
- logger,
- alias,
-}: {
- esClient: ElasticsearchClient;
- logger: Logger;
- alias: string;
-}) {
- try {
- const resp = await esClient.indices.getAlias({ name: alias });
- const indicesMatchingAlias = Object.keys(resp);
- logger.debug(`Deleting alias: '${alias}' matching indices ${indicesMatchingAlias}`);
-
- if (indicesMatchingAlias.length > 0) {
- await retryTransientEsErrors(
- () =>
- // defer validation on put if the source index is not available
- esClient.indices.deleteAlias(
- { index: indicesMatchingAlias, name: alias },
- { ignore: [404] }
- ),
- { logger }
- );
- logger.debug(`Deleted alias: '${alias}' matching indices ${indicesMatchingAlias}`);
- }
- } catch (err) {
- logger.error(`Error deleting alias: ${alias}`);
- }
+interface TransformEsAssetReference extends EsAssetReference {
+ version?: string;
}
+/**
+ * Create transform and optionally start transform
+ * Note that we want to add the current user's roles/permissions to the es-secondary-auth with a API Key.
+ * If API Key has insufficient permissions, it should still create the transforms but not start it
+ * Instead of failing, we need to allow package to continue installing other assets
+ * and prompt for users to authorize the transforms with the appropriate permissions after package is done installing
+ */
async function handleTransformInstall({
esClient,
logger,
transform,
startTransform,
+ secondaryAuth,
}: {
esClient: ElasticsearchClient;
logger: Logger;
transform: TransformInstallation;
startTransform?: boolean;
-}): Promise {
+ secondaryAuth?: SecondaryAuthorizationHeader;
+}): Promise {
+ let isUnauthorizedAPIKey = false;
try {
await retryTransientEsErrors(
() =>
- // defer validation on put if the source index is not available
- esClient.transform.putTransform({
- transform_id: transform.installationName,
- defer_validation: true,
- body: transform.content,
- }),
+ // defer_validation: true on put if the source index is not available
+ // but will check if API Key has sufficient permission
+ esClient.transform.putTransform(
+ {
+ transform_id: transform.installationName,
+ defer_validation: true,
+ body: transform.content,
+ },
+ // add '{ headers: { es-secondary-authorization: 'ApiKey {encodedApiKey}' } }'
+ secondaryAuth ? { ...secondaryAuth } : undefined
+ ),
{ logger }
);
logger.debug(`Created transform: ${transform.installationName}`);
} catch (err) {
- // swallow the error if the transform already exists.
+ const isResponseError = err instanceof errors.ResponseError;
+ isUnauthorizedAPIKey =
+ isResponseError &&
+ err?.body?.error?.type === 'security_exception' &&
+ err?.body?.error?.reason?.includes('unauthorized for API key');
+
const isAlreadyExistError =
- err instanceof errors.ResponseError &&
- err?.body?.error?.type === 'resource_already_exists_exception';
- if (!isAlreadyExistError) {
+ isResponseError && err?.body?.error?.type === 'resource_already_exists_exception';
+
+ // swallow the error if the transform already exists or if API key has insufficient permissions
+ if (!isUnauthorizedAPIKey && !isAlreadyExistError) {
throw err;
}
}
@@ -703,18 +691,71 @@ async function handleTransformInstall({
// start transform by default if not set in yml file
// else, respect the setting
if (startTransform === undefined || startTransform === true) {
- await retryTransientEsErrors(
- () =>
- esClient.transform.startTransform(
- { transform_id: transform.installationName },
- { ignore: [409] }
- ),
- { logger, additionalResponseStatuses: [400] }
- );
- logger.debug(`Started transform: ${transform.installationName}`);
+ try {
+ await retryTransientEsErrors(
+ () =>
+ esClient.transform.startTransform(
+ { transform_id: transform.installationName },
+ { ignore: [409] }
+ ),
+ { logger, additionalResponseStatuses: [400] }
+ );
+ logger.debug(`Started transform: ${transform.installationName}`);
+ } catch (err) {
+ const isResponseError = err instanceof errors.ResponseError;
+ isUnauthorizedAPIKey =
+ isResponseError &&
+ // if transform was created with insufficient permission,
+ // _start will yield an error
+ err?.body?.error?.type === 'security_exception' &&
+ err?.body?.error?.reason?.includes('lacks the required permissions');
+
+ // swallow the error if the transform can't be started if API key has insufficient permissions
+ if (!isUnauthorizedAPIKey) {
+ throw err;
+ }
+ }
+ } else {
+ // if transform was not set to start automatically in yml config,
+ // we need to check using _stats if the transform had insufficient permissions
+ try {
+ const transformStats = await retryTransientEsErrors(
+ () =>
+ esClient.transform.getTransformStats(
+ { transform_id: transform.installationName },
+ { ignore: [409] }
+ ),
+ { logger, additionalResponseStatuses: [400] }
+ );
+ if (Array.isArray(transformStats.transforms) && transformStats.transforms.length === 1) {
+ // @ts-expect-error TransformGetTransformStatsTransformStats should have 'health'
+ const transformHealth = transformStats.transforms[0].health;
+ if (
+ transformHealth.status === 'red' &&
+ Array.isArray(transformHealth.issues) &&
+ transformHealth.issues.find(
+ (i: { issue: string }) => i.issue === 'Privileges check failed'
+ )
+ ) {
+ isUnauthorizedAPIKey = true;
+ }
+ }
+ } catch (err) {
+ logger.debug(
+ `Error getting transform stats for transform: ${transform.installationName} cause ${err}`
+ );
+ }
}
- return { id: transform.installationName, type: ElasticsearchAssetType.transform };
+ return {
+ id: transform.installationName,
+ type: ElasticsearchAssetType.transform,
+ // If isUnauthorizedAPIKey: true (due to insufficient user permission at transform creation)
+ // that means the transform is created but not started.
+ // Note in saved object this is a deferred installation so user can later reauthorize
+ deferred: isUnauthorizedAPIKey,
+ version: transform.transformVersion,
+ };
}
const getLegacyTransformNameForInstallation = (
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/reauthorize.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/reauthorize.ts
new file mode 100644
index 0000000000000..50308554ce5ac
--- /dev/null
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/reauthorize.ts
@@ -0,0 +1,174 @@
+/*
+ * 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.
+ */
+
+import type { ElasticsearchClient } from '@kbn/core-elasticsearch-server';
+import type { Logger } from '@kbn/logging';
+import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
+
+import { sortBy, uniqBy } from 'lodash';
+
+import type { SecondaryAuthorizationHeader } from '../../../../../common/types/models/transform_api_key';
+import { updateEsAssetReferences } from '../../packages/install';
+import type { Installation } from '../../../../../common';
+import { ElasticsearchAssetType, PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common';
+
+import { retryTransientEsErrors } from '../retry';
+
+interface FleetTransformMetadata {
+ fleet_transform_version?: string;
+ order?: number;
+ package?: { name: string };
+ managed?: boolean;
+ managed_by?: string;
+ installed_by?: string;
+ last_authorized_by?: string;
+ transformId: string;
+}
+
+async function reauthorizeAndStartTransform({
+ esClient,
+ logger,
+ transformId,
+ secondaryAuth,
+ meta,
+}: {
+ esClient: ElasticsearchClient;
+ logger: Logger;
+ transformId: string;
+ secondaryAuth?: SecondaryAuthorizationHeader;
+ shouldInstallSequentially?: boolean;
+ meta?: object;
+}): Promise<{ transformId: string; success: boolean; error: null | any }> {
+ try {
+ await retryTransientEsErrors(
+ () =>
+ esClient.transform.updateTransform(
+ {
+ transform_id: transformId,
+ body: { _meta: meta },
+ },
+ { ...(secondaryAuth ? secondaryAuth : {}) }
+ ),
+ { logger, additionalResponseStatuses: [400] }
+ );
+
+ logger.debug(`Updated transform: ${transformId}`);
+ } catch (err) {
+ logger.error(`Failed to update transform: ${transformId} because ${err}`);
+ return { transformId, success: false, error: err };
+ }
+
+ try {
+ const startedTransform = await retryTransientEsErrors(
+ () => esClient.transform.startTransform({ transform_id: transformId }, { ignore: [409] }),
+ { logger, additionalResponseStatuses: [400] }
+ );
+ logger.debug(`Started transform: ${transformId}`);
+ return { transformId, success: startedTransform.acknowledged, error: null };
+ } catch (err) {
+ logger.error(`Failed to start transform: ${transformId} because ${err}`);
+ return { transformId, success: false, error: err };
+ }
+}
+export async function handleTransformReauthorizeAndStart({
+ esClient,
+ savedObjectsClient,
+ logger,
+ pkgName,
+ pkgVersion,
+ transforms,
+ secondaryAuth,
+ username,
+}: {
+ esClient: ElasticsearchClient;
+ savedObjectsClient: SavedObjectsClientContract;
+ logger: Logger;
+ transforms: Array<{ transformId: string }>;
+ pkgName: string;
+ pkgVersion?: string;
+ secondaryAuth?: SecondaryAuthorizationHeader;
+ username?: string;
+}) {
+ if (!secondaryAuth) {
+ throw Error(
+ 'A valid secondary authorization with sufficient `manage_transform` permission is needed to re-authorize and start transforms. ' +
+ 'This could be because security is not enabled, or API key cannot be generated.'
+ );
+ }
+
+ const transformInfos = await Promise.all(
+ transforms.map(({ transformId }) =>
+ retryTransientEsErrors(
+ () =>
+ esClient.transform.getTransform(
+ {
+ transform_id: transformId,
+ },
+ { ...(secondaryAuth ? secondaryAuth : {}) }
+ ),
+ { logger, additionalResponseStatuses: [400] }
+ )
+ )
+ );
+ const transformsMetadata: FleetTransformMetadata[] = transformInfos.flat().map((t) => {
+ const transform = t.transforms?.[0];
+ return { ...transform._meta, transformId: transform?.id };
+ });
+
+ const shouldInstallSequentially =
+ uniqBy(transformsMetadata, 'order').length === transforms.length;
+
+ let authorizedTransforms = [];
+
+ if (shouldInstallSequentially) {
+ const sortedTransformsMetadata = sortBy(transformsMetadata, [
+ (t) => t.package?.name,
+ (t) => t.fleet_transform_version,
+ (t) => t.order,
+ ]);
+
+ for (const { transformId, ...meta } of sortedTransformsMetadata) {
+ const authorizedTransform = await reauthorizeAndStartTransform({
+ esClient,
+ logger,
+ transformId,
+ secondaryAuth,
+ meta: { ...meta, last_authorized_by: username },
+ });
+
+ authorizedTransforms.push(authorizedTransform);
+ }
+ } else {
+ // Else, create & start all the transforms at once for speed
+ const transformsPromises = transformsMetadata.map(async ({ transformId, ...meta }) => {
+ return await reauthorizeAndStartTransform({
+ esClient,
+ logger,
+ transformId,
+ secondaryAuth,
+ meta: { ...meta, last_authorized_by: username },
+ });
+ });
+
+ authorizedTransforms = await Promise.all(transformsPromises).then((results) => results.flat());
+ }
+
+ const so = await savedObjectsClient.get(PACKAGES_SAVED_OBJECT_TYPE, pkgName);
+ const esReferences = so.attributes.installed_es ?? [];
+
+ const successfullyAuthorizedTransforms = authorizedTransforms.filter((t) => t.success);
+ const authorizedTransformsRefs = successfullyAuthorizedTransforms.map((t) => ({
+ type: ElasticsearchAssetType.transform,
+ id: t.transformId,
+ version: pkgVersion,
+ }));
+ await updateEsAssetReferences(savedObjectsClient, pkgName, esReferences, {
+ assetsToRemove: authorizedTransformsRefs,
+ assetsToAdd: authorizedTransformsRefs,
+ });
+ return authorizedTransforms;
+}
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/remove.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/remove.ts
index 77996674f402e..5de22e050483a 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/remove.ts
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/remove.ts
@@ -7,6 +7,7 @@
import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server';
+import type { SecondaryAuthorizationHeader } from '../../../../../common/types/models/transform_api_key';
import { ElasticsearchAssetType } from '../../../../types';
import type { EsAssetReference } from '../../../../types';
import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common/constants';
@@ -24,7 +25,8 @@ export const stopTransforms = async (transformIds: string[], esClient: Elasticse
export const deleteTransforms = async (
esClient: ElasticsearchClient,
transformIds: string[],
- deleteDestinationIndices = false
+ deleteDestinationIndices = false,
+ secondaryAuth?: SecondaryAuthorizationHeader
) => {
const logger = appContextService.getLogger();
if (transformIds.length) {
@@ -41,7 +43,7 @@ export const deleteTransforms = async (
await stopTransforms([transformId], esClient);
await esClient.transform.deleteTransform(
{ force: true, transform_id: transformId },
- { ignore: [404] }
+ { ...(secondaryAuth ? secondaryAuth : {}), ignore: [404] }
);
logger.info(`Deleted: ${transformId}`);
if (deleteDestinationIndices && transformResponse?.transforms) {
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform_utils.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform_utils.test.ts
new file mode 100644
index 0000000000000..3449af4da8eaf
--- /dev/null
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform_utils.test.ts
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ */
+
+import { getDestinationIndexAliases } from './transform_utils';
+
+describe('test transform_utils', () => {
+ describe('getDestinationIndexAliases()', function () {
+ test('return transform alias settings when input is an object', () => {
+ const aliasSettings = {
+ '.alerts-security.host-risk-score-latest.latest': { move_on_creation: true },
+ '.alerts-security.host-risk-score-latest.all': { move_on_creation: false },
+ };
+ expect(getDestinationIndexAliases(aliasSettings)).toStrictEqual([
+ { alias: '.alerts-security.host-risk-score-latest.latest', move_on_creation: true },
+ { alias: '.alerts-security.host-risk-score-latest.all', move_on_creation: false },
+ ]);
+ });
+
+ test('return transform alias settings when input is an array', () => {
+ const aliasSettings = [
+ '.alerts-security.host-risk-score-latest.latest',
+ '.alerts-security.host-risk-score-latest.all',
+ ];
+ expect(getDestinationIndexAliases(aliasSettings)).toStrictEqual([
+ { alias: '.alerts-security.host-risk-score-latest.latest', move_on_creation: true },
+ { alias: '.alerts-security.host-risk-score-latest.all', move_on_creation: false },
+ ]);
+ });
+
+ test('return transform alias settings when input is a string', () => {
+ expect(
+ getDestinationIndexAliases('.alerts-security.host-risk-score-latest.latest')
+ ).toStrictEqual([
+ { alias: '.alerts-security.host-risk-score-latest.latest', move_on_creation: true },
+ ]);
+
+ expect(
+ getDestinationIndexAliases('.alerts-security.host-risk-score-latest.all')
+ ).toStrictEqual([
+ { alias: '.alerts-security.host-risk-score-latest.all', move_on_creation: false },
+ ]);
+ });
+
+ test('return empty array when input is invalid', () => {
+ expect(getDestinationIndexAliases(undefined)).toStrictEqual([]);
+
+ expect(getDestinationIndexAliases({})).toStrictEqual([]);
+ });
+ });
+});
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform_utils.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform_utils.ts
new file mode 100644
index 0000000000000..eacb8cfbf3c94
--- /dev/null
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transform_utils.ts
@@ -0,0 +1,45 @@
+/*
+ * 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.
+ */
+
+import { isPopulatedObject } from '@kbn/ml-is-populated-object';
+
+interface TransformAliasSetting {
+ alias: string;
+ // When move_on_creation: true, all the other indices are removed from the alias,
+ // ensuring that the alias points at only one index (i.e.: the destination index of the current transform).
+ move_on_creation?: boolean;
+}
+
+export const getDestinationIndexAliases = (aliasSettings: unknown): TransformAliasSetting[] => {
+ let aliases: TransformAliasSetting[] = [];
+
+ if (!aliasSettings) return aliases;
+
+ // If in form of
+ if (isPopulatedObject(aliasSettings)) {
+ Object.keys(aliasSettings).forEach((alias) => {
+ if (aliasSettings.hasOwnProperty(alias) && typeof alias === 'string') {
+ const moveOnCreation = aliasSettings[alias].move_on_creation === true;
+ aliases.push({ alias, move_on_creation: moveOnCreation });
+ }
+ });
+ }
+ if (Array.isArray(aliasSettings)) {
+ aliases = aliasSettings.reduce((acc, alias) => {
+ if (typeof alias === 'string') {
+ acc.push({ alias, move_on_creation: alias.endsWith('.latest') ? true : false });
+ }
+ return acc;
+ }, []);
+ }
+ if (typeof aliasSettings === 'string') {
+ aliases = [
+ { alias: aliasSettings, move_on_creation: aliasSettings.endsWith('.latest') ? true : false },
+ ];
+ }
+ return aliases;
+};
diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transforms.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transforms.test.ts
index 77537f200628d..6180e37c4373d 100644
--- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transforms.test.ts
+++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/transforms.test.ts
@@ -5,25 +5,14 @@
* 2.0.
*/
-// eslint-disable-next-line import/order
-import { createAppContextStartContractMock } from '../../../../mocks';
-
-jest.mock('../../packages/get', () => {
- return { getInstallation: jest.fn(), getInstallationObject: jest.fn() };
-});
-
-jest.mock('./common', () => {
- return {
- getAsset: jest.fn(),
- };
-});
-
import type { SavedObject, SavedObjectsClientContract } from '@kbn/core/server';
import { loggerMock } from '@kbn/logging-mocks';
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks';
+import { HTTPAuthorizationHeader } from '../../../../../common/http_authorization_header';
+
import { getInstallation, getInstallationObject } from '../../packages';
import type { Installation, RegistryPackage } from '../../../../types';
import { ElasticsearchAssetType } from '../../../../types';
@@ -33,15 +22,31 @@ import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../constants';
import { getESAssetMetadata } from '../meta';
+import { createAppContextStartContractMock } from '../../../../mocks';
+
import { installTransforms } from './install';
import { getAsset } from './common';
+jest.mock('../../packages/get', () => {
+ return { getInstallation: jest.fn(), getInstallationObject: jest.fn() };
+});
+
+jest.mock('./common', () => {
+ return {
+ getAsset: jest.fn(),
+ };
+});
+
const meta = getESAssetMetadata({ packageName: 'endpoint' });
describe('test transform install', () => {
let esClient: ReturnType;
let savedObjectsClient: jest.Mocked;
+ const authorizationHeader = new HTTPAuthorizationHeader(
+ 'Basic',
+ 'bW9uaXRvcmluZ191c2VyOm1scWFfYWRtaW4='
+ );
const getYamlTestData = (
autoStart: boolean | undefined = undefined,
transformVersion: string = '0.1.0'
@@ -113,7 +118,8 @@ _meta:
body: {
description: 'Merges latest endpoint and Agent metadata documents.',
dest: {
- index: '.metrics-endpoint.metadata_united_default-0.16.0-dev.0',
+ index: '.metrics-endpoint.metadata_united_default',
+ aliases: [],
},
frequency: '1s',
pivot: {
@@ -145,7 +151,7 @@ _meta:
field: 'updated_at',
},
},
- _meta: { fleet_transform_version: transformVersion, ...meta },
+ _meta: { fleet_transform_version: transformVersion, ...meta, run_as_kibana_system: true },
},
},
};
@@ -336,7 +342,7 @@ _meta:
'logs-endpoint.metadata_current-template@package',
'logs-endpoint.metadata_current-template@custom',
],
- index_patterns: ['.metrics-endpoint.metadata_united_default-0.16.0-dev.0'],
+ index_patterns: ['.metrics-endpoint.metadata_united_default'],
priority: 250,
template: { mappings: undefined, settings: undefined },
},
@@ -346,19 +352,8 @@ _meta:
],
]);
- // Destination index is created before transform is created
- expect(esClient.indices.create.mock.calls).toEqual([
- [
- {
- aliases: {
- '.metrics-endpoint.metadata_united_default.all': {},
- '.metrics-endpoint.metadata_united_default.latest': {},
- },
- index: '.metrics-endpoint.metadata_united_default-0.16.0-dev.0',
- },
- { ignore: [400] },
- ],
- ]);
+ // Destination index is not created before transform is created
+ expect(esClient.indices.create.mock.calls).toEqual([]);
expect(esClient.transform.putTransform.mock.calls).toEqual([[expectedData.TRANSFORM]]);
expect(esClient.transform.startTransform.mock.calls).toEqual([
@@ -382,7 +377,47 @@ _meta:
type: ElasticsearchAssetType.ingestPipeline,
},
{
- id: '.metrics-endpoint.metadata_united_default-0.16.0-dev.0',
+ id: '.metrics-endpoint.metadata_united_default',
+ type: ElasticsearchAssetType.index,
+ },
+ {
+ id: 'logs-endpoint.metadata_current-template',
+ type: ElasticsearchAssetType.indexTemplate,
+ version: '0.2.0',
+ },
+ {
+ id: 'logs-endpoint.metadata_current-template@custom',
+ type: ElasticsearchAssetType.componentTemplate,
+ version: '0.2.0',
+ },
+ {
+ id: 'logs-endpoint.metadata_current-template@package',
+ type: ElasticsearchAssetType.componentTemplate,
+ version: '0.2.0',
+ },
+ {
+ id: 'logs-endpoint.metadata_current-default-0.2.0',
+ type: ElasticsearchAssetType.transform,
+ version: '0.2.0',
+ },
+ ],
+ },
+ {
+ refresh: false,
+ },
+ ],
+ // After transforms are installed, es asset reference needs to be updated if they are deferred or not
+ [
+ 'epm-packages',
+ 'endpoint',
+ {
+ installed_es: [
+ {
+ id: 'metrics-endpoint.policy-0.16.0-dev.0',
+ type: ElasticsearchAssetType.ingestPipeline,
+ },
+ {
+ id: '.metrics-endpoint.metadata_united_default',
type: ElasticsearchAssetType.index,
},
{
@@ -401,6 +436,8 @@ _meta:
version: '0.2.0',
},
{
+ // After transforms are installed, es asset reference needs to be updated if they are deferred or not
+ deferred: false,
id: 'logs-endpoint.metadata_current-default-0.2.0',
type: ElasticsearchAssetType.transform,
version: '0.2.0',
@@ -588,7 +625,7 @@ _meta:
'logs-endpoint.metadata_current-template@package',
'logs-endpoint.metadata_current-template@custom',
],
- index_patterns: ['.metrics-endpoint.metadata_united_default-0.16.0-dev.0'],
+ index_patterns: ['.metrics-endpoint.metadata_united_default'],
priority: 250,
template: { mappings: undefined, settings: undefined },
},
@@ -598,19 +635,8 @@ _meta:
],
]);
- // Destination index is created before transform is created
- expect(esClient.indices.create.mock.calls).toEqual([
- [
- {
- aliases: {
- '.metrics-endpoint.metadata_united_default.all': {},
- '.metrics-endpoint.metadata_united_default.latest': {},
- },
- index: '.metrics-endpoint.metadata_united_default-0.16.0-dev.0',
- },
- { ignore: [400] },
- ],
- ]);
+ // Destination index is not created before transform is created
+ expect(esClient.indices.create.mock.calls).toEqual([]);
expect(esClient.transform.putTransform.mock.calls).toEqual([[expectedData.TRANSFORM]]);
expect(esClient.transform.startTransform.mock.calls).toEqual([
@@ -634,7 +660,46 @@ _meta:
type: ElasticsearchAssetType.ingestPipeline,
},
{
- id: '.metrics-endpoint.metadata_united_default-0.16.0-dev.0',
+ id: '.metrics-endpoint.metadata_united_default',
+ type: ElasticsearchAssetType.index,
+ },
+ {
+ id: 'logs-endpoint.metadata_current-template',
+ type: ElasticsearchAssetType.indexTemplate,
+ version: '0.2.0',
+ },
+ {
+ id: 'logs-endpoint.metadata_current-template@custom',
+ type: ElasticsearchAssetType.componentTemplate,
+ version: '0.2.0',
+ },
+ {
+ id: 'logs-endpoint.metadata_current-template@package',
+ type: ElasticsearchAssetType.componentTemplate,
+ version: '0.2.0',
+ },
+ {
+ id: 'logs-endpoint.metadata_current-default-0.2.0',
+ type: ElasticsearchAssetType.transform,
+ version: '0.2.0',
+ },
+ ],
+ },
+ {
+ refresh: false,
+ },
+ ],
+ [
+ 'epm-packages',
+ 'endpoint',
+ {
+ installed_es: [
+ {
+ id: 'metrics-endpoint.policy-0.1.0-dev.0',
+ type: ElasticsearchAssetType.ingestPipeline,
+ },
+ {
+ id: '.metrics-endpoint.metadata_united_default',
type: ElasticsearchAssetType.index,
},
{
@@ -653,6 +718,8 @@ _meta:
version: '0.2.0',
},
{
+ // After transforms are installed, es asset reference needs to be updated if they are deferred or not
+ deferred: false,
id: 'logs-endpoint.metadata_current-default-0.2.0',
type: ElasticsearchAssetType.transform,
version: '0.2.0',
@@ -809,7 +876,7 @@ _meta:
'logs-endpoint.metadata_current-template@package',
'logs-endpoint.metadata_current-template@custom',
],
- index_patterns: ['.metrics-endpoint.metadata_united_default-0.16.0-dev.0'],
+ index_patterns: ['.metrics-endpoint.metadata_united_default'],
priority: 250,
template: { mappings: undefined, settings: undefined },
},
@@ -819,19 +886,8 @@ _meta:
],
]);
- // Destination index is created before transform is created
- expect(esClient.indices.create.mock.calls).toEqual([
- [
- {
- aliases: {
- '.metrics-endpoint.metadata_united_default.all': {},
- '.metrics-endpoint.metadata_united_default.latest': {},
- },
- index: '.metrics-endpoint.metadata_united_default-0.16.0-dev.0',
- },
- { ignore: [400] },
- ],
- ]);
+ // Destination index is not created before transform is created
+ expect(esClient.indices.create.mock.calls).toEqual([]);
expect(esClient.transform.putTransform.mock.calls).toEqual([[expectedData.TRANSFORM]]);
expect(esClient.transform.startTransform.mock.calls).toEqual([
@@ -855,7 +911,46 @@ _meta:
type: ElasticsearchAssetType.ingestPipeline,
},
{
- id: '.metrics-endpoint.metadata_united_default-0.16.0-dev.0',
+ id: '.metrics-endpoint.metadata_united_default',
+ type: ElasticsearchAssetType.index,
+ },
+ {
+ id: 'logs-endpoint.metadata_current-template',
+ type: ElasticsearchAssetType.indexTemplate,
+ version: '0.2.0',
+ },
+ {
+ id: 'logs-endpoint.metadata_current-template@custom',
+ type: ElasticsearchAssetType.componentTemplate,
+ version: '0.2.0',
+ },
+ {
+ id: 'logs-endpoint.metadata_current-template@package',
+ type: ElasticsearchAssetType.componentTemplate,
+ version: '0.2.0',
+ },
+ {
+ id: 'logs-endpoint.metadata_current-default-0.2.0',
+ type: ElasticsearchAssetType.transform,
+ version: '0.2.0',
+ },
+ ],
+ },
+ {
+ refresh: false,
+ },
+ ],
+ [
+ 'epm-packages',
+ 'endpoint',
+ {
+ installed_es: [
+ {
+ id: 'metrics-endpoint.policy-0.16.0-dev.0',
+ type: ElasticsearchAssetType.ingestPipeline,
+ },
+ {
+ id: '.metrics-endpoint.metadata_united_default',
type: ElasticsearchAssetType.index,
},
{
@@ -874,6 +969,7 @@ _meta:
version: '0.2.0',
},
{
+ deferred: false,
id: 'logs-endpoint.metadata_current-default-0.2.0',
type: ElasticsearchAssetType.transform,
version: '0.2.0',
@@ -931,7 +1027,8 @@ _meta:
esClient,
savedObjectsClient,
loggerMock.create(),
- previousInstallation.installed_es
+ previousInstallation.installed_es,
+ authorizationHeader
);
expect(esClient.transform.putTransform.mock.calls).toEqual([[expectedData.TRANSFORM]]);
@@ -1026,43 +1123,11 @@ _meta:
previousInstallation.installed_es
);
- expect(esClient.indices.create.mock.calls).toEqual([
- [
- {
- index: '.metrics-endpoint.metadata_united_default-0.16.0-dev.0',
- aliases: {
- '.metrics-endpoint.metadata_united_default.all': {},
- '.metrics-endpoint.metadata_united_default.latest': {},
- },
- },
- { ignore: [400] },
- ],
- ]);
+ expect(esClient.indices.create.mock.calls).toEqual([]);
// If downgrading to and older version, and destination index already exists
// aliases should still be updated to point .latest to this index
- expect(esClient.indices.updateAliases.mock.calls).toEqual([
- [
- {
- body: {
- actions: [
- {
- add: {
- index: '.metrics-endpoint.metadata_united_default-0.16.0-dev.0',
- alias: '.metrics-endpoint.metadata_united_default.all',
- },
- },
- {
- add: {
- index: '.metrics-endpoint.metadata_united_default-0.16.0-dev.0',
- alias: '.metrics-endpoint.metadata_united_default.latest',
- },
- },
- ],
- },
- },
- ],
- ]);
+ expect(esClient.indices.updateAliases.mock.calls).toEqual([]);
expect(esClient.transform.deleteTransform.mock.calls).toEqual([
[
diff --git a/x-pack/plugins/fleet/server/services/epm/package_service.test.ts b/x-pack/plugins/fleet/server/services/epm/package_service.test.ts
index aa6f8c81111f5..7f0493d1ee66f 100644
--- a/x-pack/plugins/fleet/server/services/epm/package_service.test.ts
+++ b/x-pack/plugins/fleet/server/services/epm/package_service.test.ts
@@ -131,7 +131,17 @@ function getTest(
method: mocks.packageClient.reinstallEsAssets.bind(mocks.packageClient),
args: [pkg, paths],
spy: jest.spyOn(epmTransformsInstall, 'installTransforms'),
- spyArgs: [pkg, paths, mocks.esClient, mocks.soClient, mocks.logger],
+ spyArgs: [
+ pkg,
+ paths,
+ mocks.esClient,
+ mocks.soClient,
+ mocks.logger,
+ // Undefined es references
+ undefined,
+ // Undefined secondary authorization
+ undefined,
+ ],
spyResponse: {
installedTransforms: [
{
diff --git a/x-pack/plugins/fleet/server/services/epm/package_service.ts b/x-pack/plugins/fleet/server/services/epm/package_service.ts
index 4e820df7a99fc..02a12dd9e77aa 100644
--- a/x-pack/plugins/fleet/server/services/epm/package_service.ts
+++ b/x-pack/plugins/fleet/server/services/epm/package_service.ts
@@ -14,6 +14,8 @@ import type {
Logger,
} from '@kbn/core/server';
+import { HTTPAuthorizationHeader } from '../../../common/http_authorization_header';
+
import type { PackageList } from '../../../common';
import type {
@@ -96,7 +98,8 @@ export class PackageServiceImpl implements PackageService {
this.internalEsClient,
this.internalSoClient,
this.logger,
- preflightCheck
+ preflightCheck,
+ request
);
}
@@ -106,13 +109,23 @@ export class PackageServiceImpl implements PackageService {
}
class PackageClientImpl implements PackageClient {
+ private authorizationHeader?: HTTPAuthorizationHeader | null = undefined;
+
constructor(
private readonly internalEsClient: ElasticsearchClient,
private readonly internalSoClient: SavedObjectsClientContract,
private readonly logger: Logger,
- private readonly preflightCheck?: () => void | Promise
+ private readonly preflightCheck?: () => void | Promise,
+ private readonly request?: KibanaRequest
) {}
+ private getAuthorizationHeader() {
+ if (this.request) {
+ this.authorizationHeader = HTTPAuthorizationHeader.parseFromRequest(this.request);
+ return this.authorizationHeader;
+ }
+ }
+
public async getInstallation(pkgName: string) {
await this.#runPreflight();
return getInstallation({
@@ -127,6 +140,7 @@ class PackageClientImpl implements PackageClient {
spaceId?: string;
}): Promise {
await this.#runPreflight();
+
return ensureInstalledPackage({
...options,
esClient: this.internalEsClient,
@@ -193,12 +207,16 @@ class PackageClientImpl implements PackageClient {
}
async #reinstallTransforms(packageInfo: InstallablePackage, paths: string[]) {
+ const authorizationHeader = await this.getAuthorizationHeader();
+
const { installedTransforms } = await installTransforms(
packageInfo,
paths,
this.internalEsClient,
this.internalSoClient,
- this.logger
+ this.logger,
+ undefined,
+ authorizationHeader
);
return installedTransforms;
}
diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts
index 68c981a308f82..6ea2749b0823a 100644
--- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts
+++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts
@@ -16,6 +16,8 @@ import { SavedObjectsErrorHelpers } from '@kbn/core/server';
import type { IAssignmentService, ITagsClient } from '@kbn/saved-objects-tagging-plugin/server';
+import type { HTTPAuthorizationHeader } from '../../../../common/http_authorization_header';
+
import { getNormalizedDataStreams } from '../../../../common/services';
import {
@@ -74,6 +76,7 @@ export async function _installPackage({
installSource,
spaceId,
verificationResult,
+ authorizationHeader,
}: {
savedObjectsClient: SavedObjectsClientContract;
savedObjectsImporter: Pick;
@@ -88,6 +91,7 @@ export async function _installPackage({
installSource: InstallSource;
spaceId: string;
verificationResult?: PackageVerificationResult;
+ authorizationHeader?: HTTPAuthorizationHeader | null;
}): Promise {
const { name: pkgName, version: pkgVersion, title: pkgTitle } = packageInfo;
@@ -245,7 +249,15 @@ export async function _installPackage({
);
({ esReferences } = await withPackageSpan('Install transforms', () =>
- installTransforms(packageInfo, paths, esClient, savedObjectsClient, logger, esReferences)
+ installTransforms(
+ packageInfo,
+ paths,
+ esClient,
+ savedObjectsClient,
+ logger,
+ esReferences,
+ authorizationHeader
+ )
));
// If this is an update or retrying an update, delete the previous version's pipelines
diff --git a/x-pack/plugins/fleet/server/services/epm/packages/bulk_install_packages.ts b/x-pack/plugins/fleet/server/services/epm/packages/bulk_install_packages.ts
index 659d8d1a1c5db..a95560fbd9cbb 100644
--- a/x-pack/plugins/fleet/server/services/epm/packages/bulk_install_packages.ts
+++ b/x-pack/plugins/fleet/server/services/epm/packages/bulk_install_packages.ts
@@ -7,6 +7,8 @@
import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server';
+import type { HTTPAuthorizationHeader } from '../../../../common/http_authorization_header';
+
import { appContextService } from '../../app_context';
import * as Registry from '../registry';
@@ -23,6 +25,7 @@ interface BulkInstallPackagesParams {
spaceId: string;
preferredSource?: 'registry' | 'bundled';
prerelease?: boolean;
+ authorizationHeader?: HTTPAuthorizationHeader | null;
}
export async function bulkInstallPackages({
@@ -32,6 +35,7 @@ export async function bulkInstallPackages({
spaceId,
force,
prerelease,
+ authorizationHeader,
}: BulkInstallPackagesParams): Promise {
const logger = appContextService.getLogger();
@@ -94,6 +98,7 @@ export async function bulkInstallPackages({
spaceId,
force,
prerelease,
+ authorizationHeader,
});
if (installResult.error) {
diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts
index af56737f7f8b5..866a24f74ec25 100644
--- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts
+++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts
@@ -25,6 +25,8 @@ import { uniqBy } from 'lodash';
import type { LicenseType } from '@kbn/licensing-plugin/server';
+import type { HTTPAuthorizationHeader } from '../../../../common/http_authorization_header';
+
import { isPackagePrerelease, getNormalizedDataStreams } from '../../../../common/services';
import { FLEET_INSTALL_FORMAT_VERSION } from '../../../constants/fleet_es_assets';
@@ -120,6 +122,7 @@ export async function ensureInstalledPackage(options: {
pkgVersion?: string;
spaceId?: string;
force?: boolean;
+ authorizationHeader?: HTTPAuthorizationHeader | null;
}): Promise {
const {
savedObjectsClient,
@@ -128,6 +131,7 @@ export async function ensureInstalledPackage(options: {
pkgVersion,
force = false,
spaceId = DEFAULT_SPACE_ID,
+ authorizationHeader,
} = options;
// If pkgVersion isn't specified, find the latest package version
@@ -152,6 +156,7 @@ export async function ensureInstalledPackage(options: {
esClient,
neverIgnoreVerificationError: !force,
force: true, // Always force outdated packages to be installed if a later version isn't installed
+ authorizationHeader,
});
if (installResult.error) {
@@ -188,6 +193,7 @@ export async function handleInstallPackageFailure({
installedPkg,
esClient,
spaceId,
+ authorizationHeader,
}: {
savedObjectsClient: SavedObjectsClientContract;
error: FleetError | Boom.Boom | Error;
@@ -196,6 +202,7 @@ export async function handleInstallPackageFailure({
installedPkg: SavedObject | undefined;
esClient: ElasticsearchClient;
spaceId: string;
+ authorizationHeader?: HTTPAuthorizationHeader | null;
}) {
if (error instanceof FleetError) {
return;
@@ -232,6 +239,7 @@ export async function handleInstallPackageFailure({
esClient,
spaceId,
force: true,
+ authorizationHeader,
});
}
} catch (e) {
@@ -255,6 +263,7 @@ interface InstallRegistryPackageParams {
neverIgnoreVerificationError?: boolean;
ignoreConstraints?: boolean;
prerelease?: boolean;
+ authorizationHeader?: HTTPAuthorizationHeader | null;
}
interface InstallUploadedArchiveParams {
savedObjectsClient: SavedObjectsClientContract;
@@ -263,6 +272,7 @@ interface InstallUploadedArchiveParams {
contentType: string;
spaceId: string;
version?: string;
+ authorizationHeader?: HTTPAuthorizationHeader | null;
}
function getTelemetryEvent(pkgName: string, pkgVersion: string): PackageUpdateEvent {
@@ -290,6 +300,7 @@ async function installPackageFromRegistry({
pkgkey,
esClient,
spaceId,
+ authorizationHeader,
force = false,
ignoreConstraints = false,
neverIgnoreVerificationError = false,
@@ -366,6 +377,7 @@ async function installPackageFromRegistry({
packageInfo,
paths,
verificationResult,
+ authorizationHeader,
});
} catch (e) {
sendEvent({
@@ -400,6 +412,7 @@ async function installPackageCommon(options: {
paths: string[];
verificationResult?: PackageVerificationResult;
telemetryEvent?: PackageUpdateEvent;
+ authorizationHeader?: HTTPAuthorizationHeader | null;
}): Promise {
const {
pkgName,
@@ -414,6 +427,7 @@ async function installPackageCommon(options: {
packageInfo,
paths,
verificationResult,
+ authorizationHeader,
} = options;
let { telemetryEvent } = options;
const logger = appContextService.getLogger();
@@ -496,6 +510,7 @@ async function installPackageCommon(options: {
spaceId,
verificationResult,
installSource,
+ authorizationHeader,
})
.then(async (assets) => {
await removeOldAssets({
@@ -519,6 +534,7 @@ async function installPackageCommon(options: {
installedPkg,
spaceId,
esClient,
+ authorizationHeader,
});
sendEvent({
...telemetryEvent!,
@@ -548,6 +564,7 @@ async function installPackageByUpload({
contentType,
spaceId,
version,
+ authorizationHeader,
}: InstallUploadedArchiveParams): Promise {
// if an error happens during getInstallType, report that we don't know
let installType: InstallType = 'unknown';
@@ -595,6 +612,7 @@ async function installPackageByUpload({
force: true, // upload has implicit force
packageInfo,
paths,
+ authorizationHeader,
});
} catch (e) {
return {
@@ -622,6 +640,8 @@ export async function installPackage(args: InstallPackageParams): Promise;
diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts
index 06c206ee77dc8..350909d3de0a7 100644
--- a/x-pack/plugins/fleet/server/services/preconfiguration.ts
+++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts
@@ -80,7 +80,6 @@ export async function ensurePreconfiguredPackagesAndPolicies(
const packagesToInstall = packages.map((pkg) =>
pkg.version === PRECONFIGURATION_LATEST_KEYWORD ? pkg.name : pkg
);
-
// Preinstall packages specified in Kibana config
const preconfiguredPackages = await bulkInstallPackages({
savedObjectsClient: soClient,
diff --git a/x-pack/plugins/fleet/server/services/security/security.ts b/x-pack/plugins/fleet/server/services/security/security.ts
index 0f72aeabfd17e..715d8d966484f 100644
--- a/x-pack/plugins/fleet/server/services/security/security.ts
+++ b/x-pack/plugins/fleet/server/services/security/security.ts
@@ -9,6 +9,8 @@ import { pick } from 'lodash';
import type { KibanaRequest } from '@kbn/core/server';
+import { TRANSFORM_PLUGIN_ID } from '../../../common/constants/plugin';
+
import type { FleetAuthz } from '../../../common';
import { INTEGRATIONS_PLUGIN_ID } from '../../../common';
import {
@@ -36,6 +38,7 @@ export function checkSuperuser(req: KibanaRequest) {
const security = appContextService.getSecurity();
const user = security.authc.getCurrentUser(req);
+
if (!user) {
return false;
}
@@ -79,6 +82,10 @@ export async function getAuthzFromRequest(req: KibanaRequest): Promise
Date: Thu, 20 Apr 2023 14:13:51 -0400
Subject: [PATCH 36/60] feat(slo): Add overview and alerts tab on slo details
page (#155413)
---
.../slo_details/components/header_title.tsx | 7 --
.../slo_details/components/slo_details.tsx | 116 +++++++++++++++---
.../pages/slo_details/slo_details.test.tsx | 27 ++--
3 files changed, 112 insertions(+), 38 deletions(-)
diff --git a/x-pack/plugins/observability/public/pages/slo_details/components/header_title.tsx b/x-pack/plugins/observability/public/pages/slo_details/components/header_title.tsx
index 94e7bcc8acf2c..e7d19c8b7f18a 100644
--- a/x-pack/plugins/observability/public/pages/slo_details/components/header_title.tsx
+++ b/x-pack/plugins/observability/public/pages/slo_details/components/header_title.tsx
@@ -9,9 +9,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
import React from 'react';
-import { useFetchActiveAlerts } from '../../../hooks/slo/use_fetch_active_alerts';
import { SloStatusBadge } from '../../../components/slo/slo_status_badge';
-import { SloActiveAlertsBadge } from '../../../components/slo/slo_status_badge/slo_active_alerts_badge';
export interface Props {
slo: SLOWithSummaryResponse | undefined;
@@ -21,10 +19,6 @@ export interface Props {
export function HeaderTitle(props: Props) {
const { isLoading, slo } = props;
- const { data: activeAlerts } = useFetchActiveAlerts({
- sloIds: !!slo ? [slo.id] : [],
- });
-
if (isLoading) {
return ;
}
@@ -38,7 +32,6 @@ export function HeaderTitle(props: Props) {
{slo.name}
-