mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
## Problem
While working on changes for bulk reassign https://github.com/elastic/kibana/issues/90437, I found that the server has a runtime error and returns a 500 if given an invalid or missing id.
<details><summary>server error stack trace</summary>
```
│ proc [kibana] server log [12:21:48.953] [error][fleet][plugins] TypeError: Cannot read property 'policy_revision_idx' of undefined
│ proc [kibana] at map (/Users/jfsiii/work/kibana/x-pack/plugins/fleet/server/services/agents/helpers.ts:15:34)
│ proc [kibana] at Array.map (<anonymous>)
│ proc [kibana] at getAgents (/Users/jfsiii/work/kibana/x-pack/plugins/fleet/server/services/agents/crud.ts:191:32)
│ proc [kibana] at runMicrotasks (<anonymous>)
│ proc [kibana] at processTicksAndRejections (internal/process/task_queues.js:93:5)
│ proc [kibana] at Object.reassignAgents (/Users/jfsiii/work/kibana/x-pack/plugins/fleet/server/services/agents/reassign.ts:91:9)
│ proc [kibana] at postBulkAgentsReassignHandler (/Users/jfsiii/work/kibana/x-pack/plugins/fleet/server/routes/agent/handlers.ts:314:21)
│ proc [kibana] at Router.handle (/Users/jfsiii/work/kibana/src/core/server/http/router/router.ts:272:30)
│ proc [kibana] at handler (/Users/jfsiii/work/kibana/src/core/server/http/router/router.ts:227:11)
│ proc [kibana] at exports.Manager.execute (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/toolkit.js:60:28)
│ proc [kibana] at Object.internals.handler (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/handler.js:46:20)
│ proc [kibana] at exports.execute (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/handler.js:31:20)
│ proc [kibana] at Request._lifecycle (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/request.js:370:32)
│ proc [kibana] at Request._execute (/Users/jfsiii/work/kibana/node_modules/@hapi/hapi/lib/request.js:279:9)
```
</details>
<details><summary>see test added in this PR fail on master</summary>
```
1) Fleet Endpoints
reassign agent(s)
bulk reassign agents
should allow to reassign multiple agents by id -- some invalid:
Error: expected 200 "OK", got 500 "Internal Server Error"
```
</details>
## Root cause
Debugging runtime error in `searchHitToAgent` found some TS type mismatches for the ES values being returned. Perhaps from one or more of the recent changes to ES client & Fleet Server. Based on `test:jest` and `test:ftr`, it appears the possible types are `GetResponse` or `SearchResponse`, instead of only an `ESSearchHit`.
https://github.com/elastic/kibana/pull/94632/files#diff-254d0f427979efc3b442f78762302eb28fb9c8857df68ea04f8d411e052f939cL11
While a `.search` result will include return matched values, a `.get` or `.mget` will return a row for each input and a `found: boolean`. e.g. `{ _id: "does-not-exist", found: false }`. The error occurs when [`searchHitToAgent`](1702cf98f0/x-pack/plugins/fleet/server/services/agents/helpers.ts (L11)
) is run on a get miss instead of a search hit.
## PR Changes
* Added a test to ensure it doesn't fail if invalid or missing IDs are given
* Moved the `bulk_reassign` tests to their own test section
* Filter out any missing results before calling `searchHitToAgent`, to match current behavior
* Consolidate repeated arguments into and code for getting agents into single [function](https://github.com/elastic/kibana/pull/94632/files#diff-f7377ed9ad56eaa8ea188b64e957e771ccc7a7652fd1eaf44251c25b930f8448R70-R87): and [TS type](https://github.com/elastic/kibana/pull/94632/files#diff-f7377ed9ad56eaa8ea188b64e957e771ccc7a7652fd1eaf44251c25b930f8448R61-R68)
* Rename some agent service functions to be more explicit (IMO) but behavior maintained. Same API names exported.
This moves toward the "one result (success or error) per given id" approach for https://github.com/elastic/kibana/issues/90437
### Checklist
- [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
# Conflicts:
# x-pack/plugins/fleet/server/services/agents/crud.ts
# x-pack/plugins/fleet/server/services/index.ts
349 lines
12 KiB
TypeScript
349 lines
12 KiB
TypeScript
/*
|
|
* 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 { Observable } from 'rxjs';
|
|
import { first } from 'rxjs/operators';
|
|
import {
|
|
CoreSetup,
|
|
CoreStart,
|
|
ElasticsearchServiceStart,
|
|
Logger,
|
|
AsyncPlugin,
|
|
PluginInitializerContext,
|
|
SavedObjectsServiceStart,
|
|
HttpServiceSetup,
|
|
SavedObjectsClientContract,
|
|
RequestHandlerContext,
|
|
KibanaRequest,
|
|
} from 'kibana/server';
|
|
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
|
|
|
|
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
|
|
import { LicensingPluginSetup, ILicense } from '../../licensing/server';
|
|
import {
|
|
EncryptedSavedObjectsPluginStart,
|
|
EncryptedSavedObjectsPluginSetup,
|
|
} from '../../encrypted_saved_objects/server';
|
|
import { SecurityPluginSetup, SecurityPluginStart } from '../../security/server';
|
|
import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server';
|
|
import {
|
|
EsAssetReference,
|
|
FleetConfigType,
|
|
NewPackagePolicy,
|
|
UpdatePackagePolicy,
|
|
} from '../common';
|
|
import { CloudSetup } from '../../cloud/server';
|
|
|
|
import {
|
|
PLUGIN_ID,
|
|
OUTPUT_SAVED_OBJECT_TYPE,
|
|
AGENT_POLICY_SAVED_OBJECT_TYPE,
|
|
PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
|
PACKAGES_SAVED_OBJECT_TYPE,
|
|
AGENT_SAVED_OBJECT_TYPE,
|
|
AGENT_EVENT_SAVED_OBJECT_TYPE,
|
|
ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE,
|
|
} from './constants';
|
|
import { registerSavedObjects, registerEncryptedSavedObjects } from './saved_objects';
|
|
import {
|
|
registerLimitedConcurrencyRoutes,
|
|
registerEPMRoutes,
|
|
registerPackagePolicyRoutes,
|
|
registerDataStreamRoutes,
|
|
registerAgentPolicyRoutes,
|
|
registerSetupRoutes,
|
|
registerAgentAPIRoutes,
|
|
registerElasticAgentRoutes,
|
|
registerEnrollmentApiKeyRoutes,
|
|
registerInstallScriptRoutes,
|
|
registerOutputRoutes,
|
|
registerSettingsRoutes,
|
|
registerAppRoutes,
|
|
} from './routes';
|
|
import {
|
|
appContextService,
|
|
licenseService,
|
|
ESIndexPatternSavedObjectService,
|
|
ESIndexPatternService,
|
|
AgentService,
|
|
AgentPolicyServiceInterface,
|
|
agentPolicyService,
|
|
packagePolicyService,
|
|
PackageService,
|
|
} from './services';
|
|
import {
|
|
getAgentStatusById,
|
|
authenticateAgentWithAccessToken,
|
|
getAgentsByKuery,
|
|
getAgentById,
|
|
} from './services/agents';
|
|
import { agentCheckinState } from './services/agents/checkin/state';
|
|
import { registerFleetUsageCollector } from './collectors/register';
|
|
import { getInstallation } from './services/epm/packages';
|
|
import { makeRouterEnforcingSuperuser } from './routes/security';
|
|
import { startFleetServerSetup } from './services/fleet_server';
|
|
import { FleetArtifactsClient } from './services/artifacts';
|
|
|
|
export interface FleetSetupDeps {
|
|
licensing: LicensingPluginSetup;
|
|
security?: SecurityPluginSetup;
|
|
features?: FeaturesPluginSetup;
|
|
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup;
|
|
cloud?: CloudSetup;
|
|
usageCollection?: UsageCollectionSetup;
|
|
}
|
|
|
|
export interface FleetStartDeps {
|
|
encryptedSavedObjects: EncryptedSavedObjectsPluginStart;
|
|
security?: SecurityPluginStart;
|
|
}
|
|
|
|
export interface FleetAppContext {
|
|
elasticsearch: ElasticsearchServiceStart;
|
|
encryptedSavedObjectsStart?: EncryptedSavedObjectsPluginStart;
|
|
encryptedSavedObjectsSetup?: EncryptedSavedObjectsPluginSetup;
|
|
security?: SecurityPluginStart;
|
|
config$?: Observable<FleetConfigType>;
|
|
savedObjects: SavedObjectsServiceStart;
|
|
isProductionMode: PluginInitializerContext['env']['mode']['prod'];
|
|
kibanaVersion: PluginInitializerContext['env']['packageInfo']['version'];
|
|
kibanaBranch: PluginInitializerContext['env']['packageInfo']['branch'];
|
|
cloud?: CloudSetup;
|
|
logger?: Logger;
|
|
httpSetup?: HttpServiceSetup;
|
|
}
|
|
|
|
export type FleetSetupContract = void;
|
|
|
|
const allSavedObjectTypes = [
|
|
OUTPUT_SAVED_OBJECT_TYPE,
|
|
AGENT_POLICY_SAVED_OBJECT_TYPE,
|
|
PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
|
PACKAGES_SAVED_OBJECT_TYPE,
|
|
AGENT_SAVED_OBJECT_TYPE,
|
|
AGENT_EVENT_SAVED_OBJECT_TYPE,
|
|
ENROLLMENT_API_KEYS_SAVED_OBJECT_TYPE,
|
|
];
|
|
|
|
/**
|
|
* Callbacks supported by the Fleet plugin
|
|
*/
|
|
export type ExternalCallback =
|
|
| [
|
|
'packagePolicyCreate',
|
|
(
|
|
newPackagePolicy: NewPackagePolicy,
|
|
context: RequestHandlerContext,
|
|
request: KibanaRequest
|
|
) => Promise<NewPackagePolicy>
|
|
]
|
|
| [
|
|
'packagePolicyUpdate',
|
|
(
|
|
newPackagePolicy: UpdatePackagePolicy,
|
|
context: RequestHandlerContext,
|
|
request: KibanaRequest
|
|
) => Promise<UpdatePackagePolicy>
|
|
];
|
|
|
|
export type ExternalCallbacksStorage = Map<ExternalCallback[0], Set<ExternalCallback[1]>>;
|
|
|
|
/**
|
|
* Describes public Fleet plugin contract returned at the `startup` stage.
|
|
*/
|
|
export interface FleetStartContract {
|
|
esIndexPatternService: ESIndexPatternService;
|
|
packageService: PackageService;
|
|
agentService: AgentService;
|
|
/**
|
|
* Services for Fleet's package policies
|
|
*/
|
|
packagePolicyService: typeof packagePolicyService;
|
|
agentPolicyService: AgentPolicyServiceInterface;
|
|
/**
|
|
* Register callbacks for inclusion in fleet API processing
|
|
* @param args
|
|
*/
|
|
registerExternalCallback: (...args: ExternalCallback) => void;
|
|
|
|
/**
|
|
* Create a Fleet Artifact Client instance
|
|
* @param packageName
|
|
*/
|
|
createArtifactsClient: (packageName: string) => FleetArtifactsClient;
|
|
}
|
|
|
|
export class FleetPlugin
|
|
implements AsyncPlugin<FleetSetupContract, FleetStartContract, FleetSetupDeps, FleetStartDeps> {
|
|
private licensing$!: Observable<ILicense>;
|
|
private config$: Observable<FleetConfigType>;
|
|
private cloud: CloudSetup | undefined;
|
|
private logger: Logger | undefined;
|
|
|
|
private isProductionMode: FleetAppContext['isProductionMode'];
|
|
private kibanaVersion: FleetAppContext['kibanaVersion'];
|
|
private kibanaBranch: FleetAppContext['kibanaBranch'];
|
|
private httpSetup: HttpServiceSetup | undefined;
|
|
private encryptedSavedObjectsSetup: EncryptedSavedObjectsPluginSetup | undefined;
|
|
|
|
constructor(private readonly initializerContext: PluginInitializerContext) {
|
|
this.config$ = this.initializerContext.config.create<FleetConfigType>();
|
|
this.isProductionMode = this.initializerContext.env.mode.prod;
|
|
this.kibanaVersion = this.initializerContext.env.packageInfo.version;
|
|
this.kibanaBranch = this.initializerContext.env.packageInfo.branch;
|
|
this.logger = this.initializerContext.logger.get();
|
|
}
|
|
|
|
public async setup(core: CoreSetup, deps: FleetSetupDeps) {
|
|
this.httpSetup = core.http;
|
|
this.licensing$ = deps.licensing.license$;
|
|
this.encryptedSavedObjectsSetup = deps.encryptedSavedObjects;
|
|
this.cloud = deps.cloud;
|
|
|
|
registerSavedObjects(core.savedObjects, deps.encryptedSavedObjects);
|
|
registerEncryptedSavedObjects(deps.encryptedSavedObjects);
|
|
|
|
// Register feature
|
|
// TODO: Flesh out privileges
|
|
if (deps.features) {
|
|
deps.features.registerKibanaFeature({
|
|
id: PLUGIN_ID,
|
|
name: 'Fleet',
|
|
category: DEFAULT_APP_CATEGORIES.management,
|
|
app: [PLUGIN_ID, 'kibana'],
|
|
catalogue: ['fleet'],
|
|
privileges: {
|
|
all: {
|
|
api: [`${PLUGIN_ID}-read`, `${PLUGIN_ID}-all`],
|
|
app: [PLUGIN_ID, 'kibana'],
|
|
catalogue: ['fleet'],
|
|
savedObject: {
|
|
all: allSavedObjectTypes,
|
|
read: [],
|
|
},
|
|
ui: ['show', 'read', 'write'],
|
|
},
|
|
read: {
|
|
api: [`${PLUGIN_ID}-read`],
|
|
app: [PLUGIN_ID, 'kibana'],
|
|
catalogue: ['fleet'], // TODO: check if this is actually available to read user
|
|
savedObject: {
|
|
all: [],
|
|
read: allSavedObjectTypes,
|
|
},
|
|
ui: ['show', 'read'],
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
const router = core.http.createRouter();
|
|
|
|
const config = await this.config$.pipe(first()).toPromise();
|
|
|
|
// Register usage collection
|
|
registerFleetUsageCollector(core, config, deps.usageCollection);
|
|
|
|
// Always register app routes for permissions checking
|
|
registerAppRoutes(router);
|
|
// For all the routes we enforce the user to have role superuser
|
|
const routerSuperuserOnly = makeRouterEnforcingSuperuser(router);
|
|
// Register rest of routes only if security is enabled
|
|
if (deps.security) {
|
|
registerSetupRoutes(routerSuperuserOnly, config);
|
|
registerAgentPolicyRoutes(routerSuperuserOnly);
|
|
registerPackagePolicyRoutes(routerSuperuserOnly);
|
|
registerOutputRoutes(routerSuperuserOnly);
|
|
registerSettingsRoutes(routerSuperuserOnly);
|
|
registerDataStreamRoutes(routerSuperuserOnly);
|
|
registerEPMRoutes(routerSuperuserOnly);
|
|
|
|
// Conditional config routes
|
|
if (config.agents.enabled) {
|
|
const isESOCanEncrypt = deps.encryptedSavedObjects.canEncrypt;
|
|
if (!isESOCanEncrypt) {
|
|
if (this.logger) {
|
|
this.logger.warn(
|
|
'Fleet APIs are disabled because the Encrypted Saved Objects plugin is missing encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in the kibana.yml or use the bin/kibana-encryption-keys command.'
|
|
);
|
|
}
|
|
} else {
|
|
// we currently only use this global interceptor if fleet is enabled
|
|
// since it would run this func on *every* req (other plugins, CSS, etc)
|
|
registerLimitedConcurrencyRoutes(core, config);
|
|
registerAgentAPIRoutes(routerSuperuserOnly, config);
|
|
registerEnrollmentApiKeyRoutes(routerSuperuserOnly);
|
|
registerInstallScriptRoutes({
|
|
router: routerSuperuserOnly,
|
|
basePath: core.http.basePath,
|
|
});
|
|
// Do not enforce superuser role for Elastic Agent routes
|
|
registerElasticAgentRoutes(router, config);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public async start(core: CoreStart, plugins: FleetStartDeps): Promise<FleetStartContract> {
|
|
await appContextService.start({
|
|
elasticsearch: core.elasticsearch,
|
|
encryptedSavedObjectsStart: plugins.encryptedSavedObjects,
|
|
encryptedSavedObjectsSetup: this.encryptedSavedObjectsSetup,
|
|
security: plugins.security,
|
|
config$: this.config$,
|
|
savedObjects: core.savedObjects,
|
|
isProductionMode: this.isProductionMode,
|
|
kibanaVersion: this.kibanaVersion,
|
|
kibanaBranch: this.kibanaBranch,
|
|
httpSetup: this.httpSetup,
|
|
cloud: this.cloud,
|
|
logger: this.logger,
|
|
});
|
|
licenseService.start(this.licensing$);
|
|
agentCheckinState.start();
|
|
|
|
startFleetServerSetup();
|
|
|
|
return {
|
|
esIndexPatternService: new ESIndexPatternSavedObjectService(),
|
|
packageService: {
|
|
getInstalledEsAssetReferences: async (
|
|
savedObjectsClient: SavedObjectsClientContract,
|
|
pkgName: string
|
|
): Promise<EsAssetReference[]> => {
|
|
const installation = await getInstallation({ savedObjectsClient, pkgName });
|
|
return installation?.installed_es || [];
|
|
},
|
|
},
|
|
agentService: {
|
|
getAgent: getAgentById,
|
|
listAgents: getAgentsByKuery,
|
|
getAgentStatusById,
|
|
authenticateAgentWithAccessToken,
|
|
},
|
|
agentPolicyService: {
|
|
get: agentPolicyService.get,
|
|
list: agentPolicyService.list,
|
|
getDefaultAgentPolicyId: agentPolicyService.getDefaultAgentPolicyId,
|
|
getFullAgentPolicy: agentPolicyService.getFullAgentPolicy,
|
|
},
|
|
packagePolicyService,
|
|
registerExternalCallback: (type: ExternalCallback[0], callback: ExternalCallback[1]) => {
|
|
return appContextService.addExternalCallback(type, callback);
|
|
},
|
|
createArtifactsClient(packageName: string) {
|
|
return new FleetArtifactsClient(core.elasticsearch.client.asInternalUser, packageName);
|
|
},
|
|
};
|
|
}
|
|
|
|
public async stop() {
|
|
appContextService.stop();
|
|
licenseService.stop();
|
|
agentCheckinState.stop();
|
|
}
|
|
}
|