mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Fleet] add uninstall token service (#154610)
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
ba6f36757e
commit
cb7c0f4a6b
19 changed files with 1207 additions and 6 deletions
|
@ -937,14 +937,14 @@
|
|||
"description": {
|
||||
"type": "text"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {}
|
||||
},
|
||||
"title": {
|
||||
"type": "text"
|
||||
},
|
||||
"version": {
|
||||
"type": "integer"
|
||||
},
|
||||
"kibanaSavedObjectMeta": {
|
||||
"properties": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -1864,6 +1864,20 @@
|
|||
"dynamic": false,
|
||||
"properties": {}
|
||||
},
|
||||
"fleet-uninstall-tokens": {
|
||||
"dynamic": false,
|
||||
"properties": {
|
||||
"created_at": {
|
||||
"type": "date"
|
||||
},
|
||||
"policy_id": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"token_plain": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"osquery-manager-usage-metric": {
|
||||
"properties": {
|
||||
"count": {
|
||||
|
|
|
@ -95,6 +95,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
|
|||
"fleet-message-signing-keys": "0c6da6a680807e568540b2aa263ae52331ba66db",
|
||||
"fleet-preconfiguration-deletion-record": "3afad160748b430427086985a3445fd8697566d5",
|
||||
"fleet-proxy": "94d0a902a0fd22578d7d3a20873b95d902e25245",
|
||||
"fleet-uninstall-tokens": "404109e8624f6585e4837a2d01f6e405e0c36d9d",
|
||||
"graph-workspace": "565642a208fe7413b487aea979b5b153e4e74abe",
|
||||
"guided-onboarding-guide-state": "3257825ae840309cb676d64b347107db7b76f30a",
|
||||
"guided-onboarding-plugin-state": "2d3ef3069ca8e981cafe8647c0c4a4c20739db10",
|
||||
|
|
|
@ -202,6 +202,7 @@ describe('split .kibana index into multiple system indices', () => {
|
|||
"fleet-message-signing-keys",
|
||||
"fleet-preconfiguration-deletion-record",
|
||||
"fleet-proxy",
|
||||
"fleet-uninstall-tokens",
|
||||
"graph-workspace",
|
||||
"guided-onboarding-guide-state",
|
||||
"guided-onboarding-plugin-state",
|
||||
|
|
|
@ -62,6 +62,7 @@ const previouslyRegisteredTypes = [
|
|||
'fleet-message-signing-keys',
|
||||
'fleet-preconfiguration-deletion-record',
|
||||
'fleet-proxy',
|
||||
'fleet-uninstall-tokens',
|
||||
'graph-workspace',
|
||||
'guided-setup-state',
|
||||
'guided-onboarding-guide-state',
|
||||
|
|
|
@ -22,6 +22,7 @@ export * from './authz';
|
|||
export * from './file_storage';
|
||||
export * from './message_signing_keys';
|
||||
export * from './locators';
|
||||
export * from './uninstall_tokens';
|
||||
|
||||
// TODO: This is the default `index.max_result_window` ES setting, which dictates
|
||||
// the maximum amount of results allowed to be returned from a search. It's possible
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const UNINSTALL_TOKENS_SAVED_OBJECT_TYPE = 'fleet-uninstall-tokens';
|
|
@ -27,6 +27,7 @@ export {
|
|||
PRECONFIGURATION_DELETION_RECORD_SAVED_OBJECT_TYPE,
|
||||
ASSETS_SAVED_OBJECT_TYPE,
|
||||
MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE,
|
||||
UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
// Fleet server index
|
||||
FLEET_SERVER_SERVERS_INDEX,
|
||||
FLEET_SERVER_ARTIFACTS_INDEX,
|
||||
|
|
|
@ -13,7 +13,7 @@ const meta = getESAssetMetadata();
|
|||
|
||||
export const FLEET_INSTALL_FORMAT_VERSION = '1.0.0';
|
||||
|
||||
export const FLEET_AGENT_POLICIES_SCHEMA_VERSION = '1.1.0';
|
||||
export const FLEET_AGENT_POLICIES_SCHEMA_VERSION = '1.1.1';
|
||||
|
||||
export const FLEET_FINAL_PIPELINE_ID = '.fleet_final_pipeline-1';
|
||||
|
||||
|
|
|
@ -47,6 +47,7 @@ export {
|
|||
ASSETS_SAVED_OBJECT_TYPE,
|
||||
GLOBAL_SETTINGS_SAVED_OBJECT_TYPE,
|
||||
MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE,
|
||||
UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
// Defaults
|
||||
DEFAULT_OUTPUT,
|
||||
DEFAULT_OUTPUT_ID,
|
||||
|
|
|
@ -76,6 +76,7 @@ export const createAppContextStartContractMock = (
|
|||
telemetryEventsSender: createMockTelemetryEventsSender(),
|
||||
bulkActionsResolver: {} as any,
|
||||
messageSigningService: createMessageSigningServiceMock(),
|
||||
uninstallTokenService: createUninstallTokenServiceMock(),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -171,3 +172,18 @@ export function createMessageSigningServiceMock() {
|
|||
rotateKeyPair: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
export function createUninstallTokenServiceMock() {
|
||||
return {
|
||||
getTokenForPolicyId: jest.fn(),
|
||||
getTokensForPolicyIds: jest.fn(),
|
||||
getAllTokens: jest.fn(),
|
||||
getHashedTokenForPolicyId: jest.fn(),
|
||||
getHashedTokensForPolicyIds: jest.fn(),
|
||||
getAllHashedTokens: jest.fn(),
|
||||
generateTokenForPolicyId: jest.fn(),
|
||||
generateTokensForPolicyIds: jest.fn(),
|
||||
generateTokensForAllPolicies: jest.fn(),
|
||||
encryptTokens: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -58,7 +58,11 @@ import type { FleetConfigType } from '../common/types';
|
|||
import type { FleetAuthz } from '../common';
|
||||
import type { ExperimentalFeatures } from '../common/experimental_features';
|
||||
|
||||
import { MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE, INTEGRATIONS_PLUGIN_ID } from '../common';
|
||||
import {
|
||||
MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE,
|
||||
INTEGRATIONS_PLUGIN_ID,
|
||||
UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
} from '../common';
|
||||
import { parseExperimentalConfigValue } from '../common/experimental_features';
|
||||
|
||||
import type { MessageSigningServiceInterface } from './services/security';
|
||||
|
@ -116,6 +120,10 @@ import { PackagePolicyServiceImpl } from './services/package_policy';
|
|||
import { registerFleetUsageLogger, startFleetUsageLogger } from './services/fleet_usage_logger';
|
||||
import { CheckDeletedFilesTask } from './tasks/check_deleted_files_task';
|
||||
import { getRequestStore } from './services/request_store';
|
||||
import {
|
||||
UninstallTokenService,
|
||||
type UninstallTokenServiceInterface,
|
||||
} from './services/security/uninstall_token_service';
|
||||
|
||||
export interface FleetSetupDeps {
|
||||
security: SecurityPluginSetup;
|
||||
|
@ -160,6 +168,7 @@ export interface FleetAppContext {
|
|||
bulkActionsResolver: BulkActionsResolver;
|
||||
messageSigningService: MessageSigningServiceInterface;
|
||||
auditLogger?: AuditLogger;
|
||||
uninstallTokenService: UninstallTokenServiceInterface;
|
||||
}
|
||||
|
||||
export type FleetSetupContract = void;
|
||||
|
@ -209,6 +218,7 @@ export interface FleetStartContract {
|
|||
createArtifactsClient: (packageName: string) => FleetArtifactsClient;
|
||||
|
||||
messageSigningService: MessageSigningServiceInterface;
|
||||
uninstallTokenService: UninstallTokenServiceInterface;
|
||||
}
|
||||
|
||||
export class FleetPlugin
|
||||
|
@ -441,6 +451,11 @@ export class FleetPlugin
|
|||
includedHiddenTypes: [MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE],
|
||||
})
|
||||
);
|
||||
const uninstallTokenService = new UninstallTokenService(
|
||||
plugins.encryptedSavedObjects.getClient({
|
||||
includedHiddenTypes: [UNINSTALL_TOKENS_SAVED_OBJECT_TYPE],
|
||||
})
|
||||
);
|
||||
|
||||
appContextService.start({
|
||||
elasticsearch: core.elasticsearch,
|
||||
|
@ -465,6 +480,7 @@ export class FleetPlugin
|
|||
telemetryEventsSender: this.telemetryEventsSender,
|
||||
bulkActionsResolver: this.bulkActionsResolver!,
|
||||
messageSigningService,
|
||||
uninstallTokenService,
|
||||
});
|
||||
licenseService.start(plugins.licensing.license$);
|
||||
|
||||
|
@ -552,6 +568,7 @@ export class FleetPlugin
|
|||
return new FleetArtifactsClient(core.elasticsearch.client.asInternalUser, packageName);
|
||||
},
|
||||
messageSigningService,
|
||||
uninstallTokenService,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import {
|
|||
FLEET_PROXY_SAVED_OBJECT_TYPE,
|
||||
MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE,
|
||||
INGEST_SAVED_OBJECT_INDEX,
|
||||
UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
} from '../constants';
|
||||
|
||||
import {
|
||||
|
@ -392,6 +393,22 @@ const getSavedObjectTypes = (): { [key: string]: SavedObjectsType } => ({
|
|||
properties: {},
|
||||
},
|
||||
},
|
||||
[UNINSTALL_TOKENS_SAVED_OBJECT_TYPE]: {
|
||||
name: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
hidden: true,
|
||||
namespaceType: 'agnostic',
|
||||
management: {
|
||||
importableAndExportable: false,
|
||||
},
|
||||
mappings: {
|
||||
dynamic: false,
|
||||
properties: {
|
||||
created_at: { type: 'date' },
|
||||
policy_id: { type: 'keyword' },
|
||||
token_plain: { type: 'keyword' },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export function registerSavedObjects(savedObjects: SavedObjectsServiceSetup) {
|
||||
|
@ -427,4 +444,8 @@ export function registerEncryptedSavedObjects(
|
|||
type: MESSAGE_SIGNING_KEYS_SAVED_OBJECT_TYPE,
|
||||
attributesToEncrypt: new Set(['passphrase']),
|
||||
});
|
||||
encryptedSavedObjects.registerType({
|
||||
type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
attributesToEncrypt: new Set(['token']),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -188,10 +188,14 @@ export async function getFullAgentPolicy(
|
|||
const messageSigningService = appContextService.getMessageSigningService();
|
||||
if (messageSigningService && fullAgentPolicy.agent) {
|
||||
const publicKey = await messageSigningService.getPublicKey();
|
||||
const tokenHash =
|
||||
(await appContextService
|
||||
.getUninstallTokenService()
|
||||
?.getHashedTokenForPolicyId(fullAgentPolicy.id)) ?? '';
|
||||
|
||||
fullAgentPolicy.agent.protection = {
|
||||
enabled: false,
|
||||
uninstall_token_hash: '',
|
||||
uninstall_token_hash: tokenHash,
|
||||
signing_key: publicKey,
|
||||
};
|
||||
|
||||
|
|
|
@ -246,6 +246,7 @@ class AgentPolicyService {
|
|||
options
|
||||
);
|
||||
|
||||
await appContextService.getUninstallTokenService()?.generateTokenForPolicyId(newSo.id);
|
||||
await this.triggerAgentPolicyUpdatedEvent(soClient, esClient, 'created', newSo.id);
|
||||
|
||||
return { id: newSo.id, ...newSo.attributes };
|
||||
|
|
|
@ -46,6 +46,7 @@ import type { TelemetryEventsSender } from '../telemetry/sender';
|
|||
import type { MessageSigningServiceInterface } from '..';
|
||||
|
||||
import type { BulkActionsResolver } from './agents';
|
||||
import type { UninstallTokenServiceInterface } from './security/uninstall_token_service';
|
||||
|
||||
class AppContextService {
|
||||
private encryptedSavedObjects: EncryptedSavedObjectsClient | undefined;
|
||||
|
@ -69,6 +70,7 @@ class AppContextService {
|
|||
private savedObjectsTagging: SavedObjectTaggingStart | undefined;
|
||||
private bulkActionsResolver: BulkActionsResolver | undefined;
|
||||
private messageSigningService: MessageSigningServiceInterface | undefined;
|
||||
private uninstallTokenService: UninstallTokenServiceInterface | undefined;
|
||||
|
||||
public start(appContext: FleetAppContext) {
|
||||
this.data = appContext.data;
|
||||
|
@ -89,6 +91,7 @@ class AppContextService {
|
|||
this.savedObjectsTagging = appContext.savedObjectsTagging;
|
||||
this.bulkActionsResolver = appContext.bulkActionsResolver;
|
||||
this.messageSigningService = appContext.messageSigningService;
|
||||
this.uninstallTokenService = appContext.uninstallTokenService;
|
||||
|
||||
if (appContext.config$) {
|
||||
this.config$ = appContext.config$;
|
||||
|
@ -254,6 +257,10 @@ class AppContextService {
|
|||
public getMessageSigningService() {
|
||||
return this.messageSigningService;
|
||||
}
|
||||
|
||||
public getUninstallTokenService() {
|
||||
return this.uninstallTokenService;
|
||||
}
|
||||
}
|
||||
|
||||
export const appContextService = new AppContextService();
|
||||
|
|
|
@ -270,6 +270,9 @@ jest.mock('./app_context', () => ({
|
|||
},
|
||||
}
|
||||
),
|
||||
getUninstallTokenService: () => ({
|
||||
generateTokenForPolicyId: jest.fn(),
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
|
|
|
@ -0,0 +1,677 @@
|
|||
/*
|
||||
* 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 { createHash } from 'crypto';
|
||||
|
||||
import type { KibanaRequest } from '@kbn/core-http-server';
|
||||
import type { SavedObjectsClientContract } from '@kbn/core/server';
|
||||
import type { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server';
|
||||
import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
|
||||
|
||||
import { UNINSTALL_TOKENS_SAVED_OBJECT_TYPE } from '../../../constants';
|
||||
import { createAppContextStartContractMock, type MockedFleetAppContext } from '../../../mocks';
|
||||
import { appContextService } from '../../app_context';
|
||||
import { agentPolicyService } from '../../agent_policy';
|
||||
|
||||
import { UninstallTokenService, type UninstallTokenServiceInterface } from '.';
|
||||
|
||||
describe('UninstallTokenService', () => {
|
||||
let soClientMock: jest.Mocked<SavedObjectsClientContract>;
|
||||
let esoClientMock: jest.Mocked<EncryptedSavedObjectsClient>;
|
||||
let mockContext: MockedFleetAppContext;
|
||||
let mockBuckets: any[] = [];
|
||||
let uninstallTokenService: UninstallTokenServiceInterface;
|
||||
|
||||
function getDefaultSO(encrypted: boolean = true) {
|
||||
return encrypted
|
||||
? {
|
||||
id: 'test-so-id',
|
||||
attributes: {
|
||||
policy_id: 'test-policy-id',
|
||||
token: 'test-token',
|
||||
},
|
||||
}
|
||||
: {
|
||||
id: 'test-so-id',
|
||||
attributes: {
|
||||
policy_id: 'test-policy-id',
|
||||
token_plain: 'test-token-plain',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getDefaultSO2(encrypted: boolean = true) {
|
||||
return encrypted
|
||||
? {
|
||||
id: 'test-so-id-two',
|
||||
attributes: {
|
||||
policy_id: 'test-policy-id-two',
|
||||
token: 'test-token-two',
|
||||
},
|
||||
}
|
||||
: {
|
||||
id: 'test-so-id-two',
|
||||
attributes: {
|
||||
policy_id: 'test-policy-id-two',
|
||||
token_plain: 'test-token-plain-two',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getDefaultBuckets(encrypted: boolean = true) {
|
||||
const defaultSO = getDefaultSO(encrypted);
|
||||
const defaultSO2 = getDefaultSO2(encrypted);
|
||||
return [
|
||||
{
|
||||
key: 'test-policy-id',
|
||||
latest: {
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_id: defaultSO.id,
|
||||
...defaultSO,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'test-policy-id-two',
|
||||
latest: {
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
_id: defaultSO2.id,
|
||||
...defaultSO2,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function mockFind(encrypted: boolean = true, savedObjects?: any[]) {
|
||||
soClientMock.find = jest.fn().mockResolvedValue({
|
||||
saved_objects:
|
||||
savedObjects ?? getDefaultBuckets(encrypted).map((bucket) => ({ id: bucket.key })),
|
||||
});
|
||||
}
|
||||
|
||||
function mockCreatePointInTimeFinder(encrypted: boolean = true, buckets?: any[]) {
|
||||
mockBuckets = buckets ?? getDefaultBuckets(encrypted);
|
||||
|
||||
soClientMock.createPointInTimeFinder = jest.fn().mockReturnValue({
|
||||
close: jest.fn(),
|
||||
find: function* asyncGenerator() {
|
||||
yield {
|
||||
aggregations: {
|
||||
by_policy_id: {
|
||||
buckets: mockBuckets,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function mockCreatePointInTimeFinderAsInternalUser(savedObjects?: any[]) {
|
||||
esoClientMock.createPointInTimeFinderDecryptedAsInternalUser = jest.fn().mockResolvedValue({
|
||||
close: jest.fn(),
|
||||
find: function* asyncGenerator() {
|
||||
yield {
|
||||
saved_objects: savedObjects ?? mockBuckets.map(({ latest }) => latest.hits.hits[0]),
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function setupMocks(canEncrypt: boolean = true) {
|
||||
mockContext = createAppContextStartContractMock();
|
||||
mockContext.encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup({
|
||||
canEncrypt,
|
||||
});
|
||||
appContextService.start(mockContext);
|
||||
esoClientMock =
|
||||
mockContext.encryptedSavedObjectsStart!.getClient() as jest.Mocked<EncryptedSavedObjectsClient>;
|
||||
soClientMock = appContextService
|
||||
.getSavedObjects()
|
||||
.getScopedClient({} as unknown as KibanaRequest) as jest.Mocked<SavedObjectsClientContract>;
|
||||
agentPolicyService.deployPolicies = jest.fn();
|
||||
|
||||
uninstallTokenService = new UninstallTokenService(esoClientMock);
|
||||
mockFind(canEncrypt);
|
||||
mockCreatePointInTimeFinder(canEncrypt);
|
||||
mockCreatePointInTimeFinderAsInternalUser();
|
||||
}
|
||||
|
||||
function hashToken(token?: string): string {
|
||||
if (!token) return '';
|
||||
return createHash('sha256').update(token).digest('base64');
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
mockBuckets = [];
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('with encryption key configured', () => {
|
||||
beforeEach(() => {
|
||||
setupMocks();
|
||||
});
|
||||
|
||||
describe('get uninstall tokens', () => {
|
||||
it('can correctly getTokenForPolicyId', async () => {
|
||||
const so = getDefaultSO();
|
||||
const token = await uninstallTokenService.getTokenForPolicyId(so.attributes.policy_id);
|
||||
expect(token).toBe(so.attributes.token);
|
||||
});
|
||||
|
||||
it('can correctly getTokensForPolicyIds', async () => {
|
||||
const so = getDefaultSO();
|
||||
const so2 = getDefaultSO2();
|
||||
|
||||
const tokensMap = await uninstallTokenService.getTokensForPolicyIds([
|
||||
so.attributes.policy_id,
|
||||
so2.attributes.policy_id,
|
||||
]);
|
||||
expect(tokensMap).toEqual({
|
||||
[so.attributes.policy_id]: so.attributes.token,
|
||||
[so2.attributes.policy_id]: so2.attributes.token,
|
||||
});
|
||||
});
|
||||
|
||||
it('can correctly getAllTokens', async () => {
|
||||
const so = getDefaultSO();
|
||||
const so2 = getDefaultSO2();
|
||||
|
||||
const tokensMap = await uninstallTokenService.getAllTokens();
|
||||
expect(tokensMap).toEqual({
|
||||
[so.attributes.policy_id]: so.attributes.token,
|
||||
[so2.attributes.policy_id]: so2.attributes.token,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('get hashed uninstall tokens', () => {
|
||||
it('can correctly getHashedTokenForPolicyId', async () => {
|
||||
const so = getDefaultSO();
|
||||
|
||||
const token = await uninstallTokenService.getHashedTokenForPolicyId(
|
||||
so.attributes.policy_id
|
||||
);
|
||||
expect(token).toBe(hashToken(so.attributes.token));
|
||||
});
|
||||
|
||||
it('can correctly getHashedTokensForPolicyIds', async () => {
|
||||
const so = getDefaultSO();
|
||||
const so2 = getDefaultSO2();
|
||||
|
||||
const tokensMap = await uninstallTokenService.getHashedTokensForPolicyIds([
|
||||
so.attributes.policy_id,
|
||||
so2.attributes.policy_id,
|
||||
]);
|
||||
expect(tokensMap).toEqual({
|
||||
[so.attributes.policy_id]: hashToken(so.attributes.token),
|
||||
[so2.attributes.policy_id]: hashToken(so2.attributes.token),
|
||||
});
|
||||
});
|
||||
|
||||
it('can correctly getAllHashedTokens', async () => {
|
||||
const so = getDefaultSO();
|
||||
const so2 = getDefaultSO2();
|
||||
|
||||
const tokensMap = await uninstallTokenService.getAllHashedTokens();
|
||||
expect(tokensMap).toEqual({
|
||||
[so.attributes.policy_id]: hashToken(so.attributes.token),
|
||||
[so2.attributes.policy_id]: hashToken(so2.attributes.token),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('token generation', () => {
|
||||
describe('existing token', () => {
|
||||
describe('force = false', () => {
|
||||
it('does not create new token when calling generateTokenForPolicyId', async () => {
|
||||
const so = getDefaultSO();
|
||||
await uninstallTokenService.generateTokenForPolicyId(so.attributes.policy_id);
|
||||
expect(soClientMock.bulkCreate).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('does not create new token when calling generateTokensForPolicyIds', async () => {
|
||||
const so = getDefaultSO();
|
||||
await uninstallTokenService.generateTokensForPolicyIds([so.attributes.policy_id]);
|
||||
expect(soClientMock.bulkCreate).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('does not create new token when calling generateTokensForAllPolicies', async () => {
|
||||
await uninstallTokenService.generateTokensForAllPolicies();
|
||||
expect(soClientMock.bulkCreate).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('force = true', () => {
|
||||
it('creates a new token when calling generateTokenForPolicyId', async () => {
|
||||
const so = getDefaultSO();
|
||||
await uninstallTokenService.generateTokenForPolicyId(so.attributes.policy_id, true);
|
||||
expect(soClientMock.bulkCreate).toBeCalledWith([
|
||||
{
|
||||
type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
attributes: {
|
||||
policy_id: so.attributes.policy_id,
|
||||
token: expect.any(String),
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(agentPolicyService.deployPolicies).toBeCalledWith(soClientMock, [
|
||||
so.attributes.policy_id,
|
||||
]);
|
||||
});
|
||||
|
||||
it('creates a new token when calling generateTokensForPolicyIds', async () => {
|
||||
const so = getDefaultSO();
|
||||
const so2 = getDefaultSO2();
|
||||
|
||||
await uninstallTokenService.generateTokensForPolicyIds(
|
||||
[so.attributes.policy_id, so2.attributes.policy_id],
|
||||
true
|
||||
);
|
||||
expect(soClientMock.bulkCreate).toBeCalledWith([
|
||||
{
|
||||
type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
attributes: {
|
||||
policy_id: so.attributes.policy_id,
|
||||
token: expect.any(String),
|
||||
},
|
||||
},
|
||||
{
|
||||
type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
attributes: {
|
||||
policy_id: so2.attributes.policy_id,
|
||||
token: expect.any(String),
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(agentPolicyService.deployPolicies).toBeCalledWith(soClientMock, [
|
||||
so.attributes.policy_id,
|
||||
so2.attributes.policy_id,
|
||||
]);
|
||||
});
|
||||
|
||||
it('creates a new token when calling generateTokensForAllPolicies', async () => {
|
||||
const so = getDefaultSO();
|
||||
const so2 = getDefaultSO2();
|
||||
|
||||
await uninstallTokenService.generateTokensForAllPolicies(true);
|
||||
expect(soClientMock.bulkCreate).toBeCalledWith([
|
||||
{
|
||||
type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
attributes: {
|
||||
policy_id: so.attributes.policy_id,
|
||||
token: expect.any(String),
|
||||
},
|
||||
},
|
||||
{
|
||||
type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
attributes: {
|
||||
policy_id: so2.attributes.policy_id,
|
||||
token: expect.any(String),
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(agentPolicyService.deployPolicies).toBeCalledWith(soClientMock, [
|
||||
so.attributes.policy_id,
|
||||
so2.attributes.policy_id,
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('no existing token', () => {
|
||||
beforeEach(() => {
|
||||
mockCreatePointInTimeFinder(true, []);
|
||||
mockCreatePointInTimeFinderAsInternalUser([]);
|
||||
});
|
||||
|
||||
it('creates a new token when calling generateTokenForPolicyId', async () => {
|
||||
const so = getDefaultSO();
|
||||
await uninstallTokenService.generateTokenForPolicyId(so.attributes.policy_id);
|
||||
expect(soClientMock.bulkCreate).toBeCalledWith([
|
||||
{
|
||||
type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
attributes: {
|
||||
policy_id: so.attributes.policy_id,
|
||||
token: expect.any(String),
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('creates a new token when calling generateTokensForPolicyIds', async () => {
|
||||
const so = getDefaultSO();
|
||||
const so2 = getDefaultSO2();
|
||||
|
||||
await uninstallTokenService.generateTokensForPolicyIds([
|
||||
so.attributes.policy_id,
|
||||
so2.attributes.policy_id,
|
||||
]);
|
||||
expect(soClientMock.bulkCreate).toBeCalledWith([
|
||||
{
|
||||
type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
attributes: {
|
||||
policy_id: so.attributes.policy_id,
|
||||
token: expect.any(String),
|
||||
},
|
||||
},
|
||||
{
|
||||
type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
attributes: {
|
||||
policy_id: so2.attributes.policy_id,
|
||||
token: expect.any(String),
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('creates a new token when calling generateTokensForAllPolicies', async () => {
|
||||
const so = getDefaultSO();
|
||||
const so2 = getDefaultSO2();
|
||||
|
||||
await uninstallTokenService.generateTokensForAllPolicies();
|
||||
expect(soClientMock.bulkCreate).toBeCalledWith([
|
||||
{
|
||||
type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
attributes: {
|
||||
policy_id: so.attributes.policy_id,
|
||||
token: expect.any(String),
|
||||
},
|
||||
},
|
||||
{
|
||||
type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
attributes: {
|
||||
policy_id: so2.attributes.policy_id,
|
||||
token: expect.any(String),
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with encryption key NOT configured', () => {
|
||||
beforeEach(() => {
|
||||
setupMocks(false);
|
||||
});
|
||||
|
||||
describe('get uninstall tokens', () => {
|
||||
it('can correctly getTokenForPolicyId', async () => {
|
||||
const so = getDefaultSO(false);
|
||||
const token = await uninstallTokenService.getTokenForPolicyId(so.attributes.policy_id);
|
||||
expect(token).toBe(so.attributes.token_plain);
|
||||
});
|
||||
|
||||
it('can correctly getTokensForPolicyIds', async () => {
|
||||
const so = getDefaultSO(false);
|
||||
const so2 = getDefaultSO2(false);
|
||||
|
||||
const tokensMap = await uninstallTokenService.getTokensForPolicyIds([
|
||||
so.attributes.policy_id,
|
||||
so2.attributes.policy_id,
|
||||
]);
|
||||
expect(tokensMap).toEqual({
|
||||
[so.attributes.policy_id]: so.attributes.token_plain,
|
||||
[so2.attributes.policy_id]: so2.attributes.token_plain,
|
||||
});
|
||||
});
|
||||
|
||||
it('can correctly getAllTokens', async () => {
|
||||
const so = getDefaultSO(false);
|
||||
const so2 = getDefaultSO2(false);
|
||||
|
||||
const tokensMap = await uninstallTokenService.getAllTokens();
|
||||
expect(tokensMap).toEqual({
|
||||
[so.attributes.policy_id]: so.attributes.token_plain,
|
||||
[so2.attributes.policy_id]: so2.attributes.token_plain,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('get hashed uninstall tokens', () => {
|
||||
it('can correctly getHashedTokenForPolicyId', async () => {
|
||||
const so = getDefaultSO(false);
|
||||
|
||||
const token = await uninstallTokenService.getHashedTokenForPolicyId(
|
||||
so.attributes.policy_id
|
||||
);
|
||||
expect(token).toBe(hashToken(so.attributes.token_plain));
|
||||
});
|
||||
|
||||
it('can correctly getHashedTokensForPolicyIds', async () => {
|
||||
const so = getDefaultSO(false);
|
||||
const so2 = getDefaultSO2(false);
|
||||
|
||||
const tokensMap = await uninstallTokenService.getHashedTokensForPolicyIds([
|
||||
so.attributes.policy_id,
|
||||
so2.attributes.policy_id,
|
||||
]);
|
||||
expect(tokensMap).toEqual({
|
||||
[so.attributes.policy_id]: hashToken(so.attributes.token_plain),
|
||||
[so2.attributes.policy_id]: hashToken(so2.attributes.token_plain),
|
||||
});
|
||||
});
|
||||
|
||||
it('can correctly getAllHashedTokens', async () => {
|
||||
const so = getDefaultSO(false);
|
||||
const so2 = getDefaultSO2(false);
|
||||
|
||||
const tokensMap = await uninstallTokenService.getAllHashedTokens();
|
||||
expect(tokensMap).toEqual({
|
||||
[so.attributes.policy_id]: hashToken(so.attributes.token_plain),
|
||||
[so2.attributes.policy_id]: hashToken(so2.attributes.token_plain),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('token generation', () => {
|
||||
describe('existing token', () => {
|
||||
describe('force = false', () => {
|
||||
it('does not create new token when calling generateTokenForPolicyId', async () => {
|
||||
const so = getDefaultSO();
|
||||
await uninstallTokenService.generateTokenForPolicyId(so.attributes.policy_id);
|
||||
expect(soClientMock.bulkCreate).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('does not create new token when calling generateTokensForPolicyIds', async () => {
|
||||
const so = getDefaultSO();
|
||||
await uninstallTokenService.generateTokensForPolicyIds([so.attributes.policy_id]);
|
||||
expect(soClientMock.bulkCreate).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('does not create new token when calling generateTokensForAllPolicies', async () => {
|
||||
await uninstallTokenService.generateTokensForAllPolicies();
|
||||
expect(soClientMock.bulkCreate).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('force = true', () => {
|
||||
it('creates a new token when calling generateTokenForPolicyId', async () => {
|
||||
const so = getDefaultSO();
|
||||
await uninstallTokenService.generateTokenForPolicyId(so.attributes.policy_id, true);
|
||||
expect(soClientMock.bulkCreate).toBeCalledWith([
|
||||
{
|
||||
type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
attributes: {
|
||||
policy_id: so.attributes.policy_id,
|
||||
token_plain: expect.any(String),
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(agentPolicyService.deployPolicies).toBeCalledWith(soClientMock, [
|
||||
so.attributes.policy_id,
|
||||
]);
|
||||
});
|
||||
|
||||
it('creates a new token when calling generateTokensForPolicyIds', async () => {
|
||||
const so = getDefaultSO();
|
||||
const so2 = getDefaultSO2();
|
||||
|
||||
await uninstallTokenService.generateTokensForPolicyIds(
|
||||
[so.attributes.policy_id, so2.attributes.policy_id],
|
||||
true
|
||||
);
|
||||
expect(soClientMock.bulkCreate).toBeCalledWith([
|
||||
{
|
||||
type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
attributes: {
|
||||
policy_id: so.attributes.policy_id,
|
||||
token_plain: expect.any(String),
|
||||
},
|
||||
},
|
||||
{
|
||||
type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
attributes: {
|
||||
policy_id: so2.attributes.policy_id,
|
||||
token_plain: expect.any(String),
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(agentPolicyService.deployPolicies).toBeCalledWith(soClientMock, [
|
||||
so.attributes.policy_id,
|
||||
so2.attributes.policy_id,
|
||||
]);
|
||||
});
|
||||
|
||||
it('creates a new token when calling generateTokensForAllPolicies', async () => {
|
||||
const so = getDefaultSO();
|
||||
const so2 = getDefaultSO2();
|
||||
|
||||
await uninstallTokenService.generateTokensForAllPolicies(true);
|
||||
expect(soClientMock.bulkCreate).toBeCalledWith([
|
||||
{
|
||||
type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
attributes: {
|
||||
policy_id: so.attributes.policy_id,
|
||||
token_plain: expect.any(String),
|
||||
},
|
||||
},
|
||||
{
|
||||
type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
attributes: {
|
||||
policy_id: so2.attributes.policy_id,
|
||||
token_plain: expect.any(String),
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(agentPolicyService.deployPolicies).toBeCalledWith(soClientMock, [
|
||||
so.attributes.policy_id,
|
||||
so2.attributes.policy_id,
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('no existing token', () => {
|
||||
beforeEach(() => {
|
||||
mockCreatePointInTimeFinder(false, []);
|
||||
mockCreatePointInTimeFinderAsInternalUser([]);
|
||||
});
|
||||
|
||||
it('creates a new token when calling generateTokenForPolicyId', async () => {
|
||||
const so = getDefaultSO();
|
||||
await uninstallTokenService.generateTokenForPolicyId(so.attributes.policy_id);
|
||||
expect(soClientMock.bulkCreate).toBeCalledWith([
|
||||
{
|
||||
type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
attributes: {
|
||||
policy_id: so.attributes.policy_id,
|
||||
token_plain: expect.any(String),
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('creates a new token when calling generateTokensForPolicyIds', async () => {
|
||||
const so = getDefaultSO();
|
||||
const so2 = getDefaultSO2();
|
||||
|
||||
await uninstallTokenService.generateTokensForPolicyIds([
|
||||
so.attributes.policy_id,
|
||||
so2.attributes.policy_id,
|
||||
]);
|
||||
expect(soClientMock.bulkCreate).toBeCalledWith([
|
||||
{
|
||||
type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
attributes: {
|
||||
policy_id: so.attributes.policy_id,
|
||||
token_plain: expect.any(String),
|
||||
},
|
||||
},
|
||||
{
|
||||
type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
attributes: {
|
||||
policy_id: so2.attributes.policy_id,
|
||||
token_plain: expect.any(String),
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('creates a new token when calling generateTokensForAllPolicies', async () => {
|
||||
const so = getDefaultSO();
|
||||
const so2 = getDefaultSO2();
|
||||
|
||||
await uninstallTokenService.generateTokensForAllPolicies();
|
||||
expect(soClientMock.bulkCreate).toBeCalledWith([
|
||||
{
|
||||
type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
attributes: {
|
||||
policy_id: so.attributes.policy_id,
|
||||
token_plain: expect.any(String),
|
||||
},
|
||||
},
|
||||
{
|
||||
type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
attributes: {
|
||||
policy_id: so2.attributes.policy_id,
|
||||
token_plain: expect.any(String),
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can encryptTokens', async () => {
|
||||
const so = getDefaultSO(false);
|
||||
const so2 = getDefaultSO2(false);
|
||||
|
||||
mockContext!.encryptedSavedObjectsSetup!.canEncrypt = true;
|
||||
mockFind(false, [so, so2]);
|
||||
await uninstallTokenService.encryptTokens();
|
||||
|
||||
expect(soClientMock.bulkUpdate).toBeCalledWith([
|
||||
{
|
||||
id: so.id,
|
||||
attributes: {
|
||||
policy_id: so.attributes.policy_id,
|
||||
token: so.attributes.token_plain,
|
||||
token_plain: '',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: so2.id,
|
||||
attributes: {
|
||||
policy_id: so2.attributes.policy_id,
|
||||
token: so2.attributes.token_plain,
|
||||
token_plain: '',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,414 @@
|
|||
/*
|
||||
* 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 { randomBytes, createHash } from 'crypto';
|
||||
|
||||
import { chunk } from 'lodash';
|
||||
|
||||
import type {
|
||||
SavedObjectsClientContract,
|
||||
SavedObjectsCreatePointInTimeFinderOptions,
|
||||
SavedObjectsBulkUpdateObject,
|
||||
SavedObjectsFindResult,
|
||||
} from '@kbn/core-saved-objects-api-server';
|
||||
import type {
|
||||
AggregationsMultiBucketAggregateBase,
|
||||
AggregationsTopHitsAggregate,
|
||||
} from '@elastic/elasticsearch/lib/api/types';
|
||||
import type { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server';
|
||||
import type { KibanaRequest } from '@kbn/core-http-server';
|
||||
import { SECURITY_EXTENSION_ID } from '@kbn/core-saved-objects-server';
|
||||
import { asyncForEach } from '@kbn/std';
|
||||
|
||||
import { UNINSTALL_TOKENS_SAVED_OBJECT_TYPE, SO_SEARCH_LIMIT } from '../../../constants';
|
||||
import { appContextService } from '../../app_context';
|
||||
import { agentPolicyService } from '../../agent_policy';
|
||||
|
||||
interface UninstallTokenSOAttributes {
|
||||
policy_id: string;
|
||||
token: string;
|
||||
token_plain: string;
|
||||
}
|
||||
|
||||
interface UninstallTokenSOAggregationBucket {
|
||||
key: string;
|
||||
latest: AggregationsTopHitsAggregate;
|
||||
}
|
||||
|
||||
interface UninstallTokenSOAggregation {
|
||||
by_policy_id: AggregationsMultiBucketAggregateBase<UninstallTokenSOAggregationBucket>;
|
||||
}
|
||||
|
||||
export interface UninstallTokenServiceInterface {
|
||||
getTokenForPolicyId(policyId: string): Promise<string>;
|
||||
getTokensForPolicyIds(policyIds: string[]): Promise<Record<string, string>>;
|
||||
getAllTokens(): Promise<Record<string, string>>;
|
||||
getHashedTokenForPolicyId(policyId: string): Promise<string>;
|
||||
getHashedTokensForPolicyIds(policyIds?: string[]): Promise<Record<string, string>>;
|
||||
getAllHashedTokens(): Promise<Record<string, string>>;
|
||||
generateTokenForPolicyId(policyId: string, force?: boolean): Promise<string>;
|
||||
generateTokensForPolicyIds(policyIds: string[], force?: boolean): Promise<Record<string, string>>;
|
||||
generateTokensForAllPolicies(force?: boolean): Promise<Record<string, string>>;
|
||||
encryptTokens(): Promise<void>;
|
||||
}
|
||||
|
||||
export class UninstallTokenService implements UninstallTokenServiceInterface {
|
||||
private _soClient: SavedObjectsClientContract | undefined;
|
||||
|
||||
constructor(private esoClient: EncryptedSavedObjectsClient) {}
|
||||
|
||||
/**
|
||||
* gets uninstall token for given policy id
|
||||
*
|
||||
* @param policyId agent policy id
|
||||
* @returns token
|
||||
*/
|
||||
public async getTokenForPolicyId(policyId: string): Promise<string> {
|
||||
return (await this.getTokensForPolicyIds([policyId]))[policyId];
|
||||
}
|
||||
|
||||
/**
|
||||
* gets uninstall tokens for given policy ids
|
||||
*
|
||||
* @param policyIds agent policy ids
|
||||
* @returns Record<policyId, token>
|
||||
*/
|
||||
public async getTokensForPolicyIds(policyIds: string[]): Promise<Record<string, string>> {
|
||||
let filter = policyIds
|
||||
.map((policyId) => `${UNINSTALL_TOKENS_SAVED_OBJECT_TYPE}.attributes.policy_id: ${policyId}`)
|
||||
.join(' or ');
|
||||
const bucketSize = 10000;
|
||||
const query: SavedObjectsCreatePointInTimeFinderOptions = {
|
||||
type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
perPage: 0,
|
||||
filter,
|
||||
aggs: {
|
||||
by_policy_id: {
|
||||
terms: {
|
||||
field: `${UNINSTALL_TOKENS_SAVED_OBJECT_TYPE}.attributes.policy_id`,
|
||||
size: bucketSize,
|
||||
},
|
||||
aggs: {
|
||||
latest: {
|
||||
top_hits: {
|
||||
size: 1,
|
||||
sort: [{ [`${UNINSTALL_TOKENS_SAVED_OBJECT_TYPE}.created_at`]: { order: 'desc' } }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
// encrypted saved objects doesn't decrypt aggregation values so we get
|
||||
// the ids first from saved objects to use with encrypted saved objects
|
||||
const idFinder = this.soClient.createPointInTimeFinder<
|
||||
UninstallTokenSOAttributes,
|
||||
UninstallTokenSOAggregation
|
||||
>(query);
|
||||
|
||||
let aggResults: UninstallTokenSOAggregationBucket[] = [];
|
||||
for await (const result of idFinder.find()) {
|
||||
if (
|
||||
!result?.aggregations?.by_policy_id.buckets ||
|
||||
!Array.isArray(result?.aggregations?.by_policy_id.buckets)
|
||||
) {
|
||||
break;
|
||||
}
|
||||
aggResults = result.aggregations.by_policy_id.buckets;
|
||||
break;
|
||||
}
|
||||
|
||||
filter = aggResults
|
||||
.reduce((acc, { latest }) => {
|
||||
const id = latest?.hits?.hits?.at(0)?._id;
|
||||
if (!id) return acc;
|
||||
const filterStr = `${UNINSTALL_TOKENS_SAVED_OBJECT_TYPE}.id: "${id}"`;
|
||||
return [...acc, filterStr];
|
||||
}, [] as string[])
|
||||
.join(' or ');
|
||||
|
||||
const tokensFinder =
|
||||
await this.esoClient.createPointInTimeFinderDecryptedAsInternalUser<UninstallTokenSOAttributes>(
|
||||
{
|
||||
type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
perPage: SO_SEARCH_LIMIT,
|
||||
filter,
|
||||
}
|
||||
);
|
||||
let tokenObjects: Array<SavedObjectsFindResult<UninstallTokenSOAttributes>> = [];
|
||||
for await (const result of tokensFinder.find()) {
|
||||
tokenObjects = [...tokenObjects, ...result.saved_objects];
|
||||
}
|
||||
tokensFinder.close();
|
||||
|
||||
const tokensMap = tokenObjects.reduce((acc, { attributes }) => {
|
||||
const policyId = attributes.policy_id;
|
||||
const token = attributes.token || attributes.token_plain;
|
||||
if (!policyId || !token) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
return {
|
||||
...acc,
|
||||
[policyId]: token,
|
||||
};
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
return tokensMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* gets uninstall tokens for all policies
|
||||
*
|
||||
* @returns Record<policyId, token>
|
||||
*/
|
||||
public async getAllTokens(): Promise<Record<string, string>> {
|
||||
const policyIds = await this.getAllPolicyIds();
|
||||
return this.getTokensForPolicyIds(policyIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* get hashed uninstall token for given policy id
|
||||
*
|
||||
* @param policyId agent policy id
|
||||
* @returns hashedToken
|
||||
*/
|
||||
public async getHashedTokenForPolicyId(policyId: string): Promise<string> {
|
||||
return (await this.getHashedTokensForPolicyIds([policyId]))[policyId];
|
||||
}
|
||||
|
||||
/**
|
||||
* get hashed uninstall tokens for given policy ids
|
||||
*
|
||||
* @param policyIds agent policy ids
|
||||
* @returns Record<policyId, hashedToken>
|
||||
*/
|
||||
public async getHashedTokensForPolicyIds(policyIds: string[]): Promise<Record<string, string>> {
|
||||
const tokensMap = await this.getTokensForPolicyIds(policyIds);
|
||||
return Object.entries(tokensMap).reduce((acc, [policyId, token]) => {
|
||||
if (!policyId || !token) {
|
||||
return acc;
|
||||
}
|
||||
return { ...acc, [policyId]: this.hashToken(token) };
|
||||
}, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* get hashed uninstall token for all policies
|
||||
*
|
||||
* @returns Record<policyId, hashedToken>
|
||||
*/
|
||||
public async getAllHashedTokens(): Promise<Record<string, string>> {
|
||||
const policyIds = await this.getAllPolicyIds();
|
||||
return this.getHashedTokensForPolicyIds(policyIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* generate uninstall token for given policy id
|
||||
* will not create a new token if one already exists for a given policy unless force: true is used
|
||||
*
|
||||
* @param policyId agent policy id
|
||||
* @param force generate a new token even if one already exists
|
||||
* @returns hashedToken
|
||||
*/
|
||||
public async generateTokenForPolicyId(policyId: string, force: boolean = false): Promise<string> {
|
||||
return (await this.generateTokensForPolicyIds([policyId], force))[policyId];
|
||||
}
|
||||
|
||||
/**
|
||||
* generate uninstall tokens for given policy ids
|
||||
* will not create a new token if one already exists for a given policy unless force: true is used
|
||||
*
|
||||
* @param policyIds agent policy ids
|
||||
* @param force generate a new token even if one already exists
|
||||
* @returns Record<policyId, hashedToken>
|
||||
*/
|
||||
public async generateTokensForPolicyIds(
|
||||
policyIds: string[],
|
||||
force: boolean = false
|
||||
): Promise<Record<string, string>> {
|
||||
if (!policyIds.length) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const existingTokens = force ? {} : await this.getTokensForPolicyIds(policyIds);
|
||||
const missingTokenPolicyIds = force
|
||||
? policyIds
|
||||
: policyIds.filter((policyId) => !existingTokens[policyId]);
|
||||
|
||||
const newTokensMap = missingTokenPolicyIds.reduce((acc, policyId) => {
|
||||
const token = this.generateToken();
|
||||
return {
|
||||
...acc,
|
||||
[policyId]: token,
|
||||
};
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
await this.persistTokens(missingTokenPolicyIds, newTokensMap);
|
||||
if (force) {
|
||||
const config = appContextService.getConfig();
|
||||
const batchSize = config?.setup?.agentPolicySchemaUpgradeBatchSize ?? 100;
|
||||
asyncForEach(
|
||||
chunk(policyIds, batchSize),
|
||||
async (policyIdsBatch) =>
|
||||
await agentPolicyService.deployPolicies(this.soClient, policyIdsBatch)
|
||||
);
|
||||
}
|
||||
|
||||
const tokensMap = {
|
||||
...existingTokens,
|
||||
...newTokensMap,
|
||||
};
|
||||
|
||||
return Object.entries(tokensMap).reduce(
|
||||
(acc, [policyId, token]) => ({
|
||||
...acc,
|
||||
[policyId]: this.hashToken(token),
|
||||
}),
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* generate uninstall tokens all policies
|
||||
* will not create a new token if one already exists for a given policy unless force: true is used
|
||||
*
|
||||
* @param force generate a new token even if one already exists
|
||||
* @returns Record<policyId, hashedToken>
|
||||
*/
|
||||
public async generateTokensForAllPolicies(
|
||||
force: boolean = false
|
||||
): Promise<Record<string, string>> {
|
||||
const policyIds = await this.getAllPolicyIds();
|
||||
return this.generateTokensForPolicyIds(policyIds, force);
|
||||
}
|
||||
|
||||
/**
|
||||
* if encryption is available, checks for any plain text uninstall tokens and encrypts them
|
||||
*/
|
||||
public async encryptTokens(): Promise<void> {
|
||||
if (!this.isEncryptionAvailable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { saved_objects: unencryptedTokenObjects } =
|
||||
await this.soClient.find<UninstallTokenSOAttributes>({
|
||||
type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
filter: `${UNINSTALL_TOKENS_SAVED_OBJECT_TYPE}.attributes.token_plain:* AND (NOT ${UNINSTALL_TOKENS_SAVED_OBJECT_TYPE}.attributes.token_plain: "")`,
|
||||
});
|
||||
|
||||
if (!unencryptedTokenObjects.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bulkUpdateObjects: Array<SavedObjectsBulkUpdateObject<UninstallTokenSOAttributes>> = [];
|
||||
for (const unencryptedTokenObject of unencryptedTokenObjects) {
|
||||
bulkUpdateObjects.push({
|
||||
...unencryptedTokenObject,
|
||||
attributes: {
|
||||
...unencryptedTokenObject.attributes,
|
||||
token: unencryptedTokenObject.attributes.token_plain,
|
||||
token_plain: '',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await this.soClient.bulkUpdate(bulkUpdateObjects);
|
||||
}
|
||||
|
||||
private async getPolicyIdsBatch(
|
||||
batchSize: number = SO_SEARCH_LIMIT,
|
||||
page: number = 1
|
||||
): Promise<string[]> {
|
||||
return (
|
||||
await agentPolicyService.list(this.soClient, { page, perPage: batchSize, fields: ['id'] })
|
||||
).items.map((policy) => policy.id);
|
||||
}
|
||||
|
||||
private async getAllPolicyIds(): Promise<string[]> {
|
||||
const batchSize = SO_SEARCH_LIMIT;
|
||||
let policyIdsBatch = await this.getPolicyIdsBatch(batchSize);
|
||||
let policyIds = policyIdsBatch;
|
||||
let page = 2;
|
||||
|
||||
while (policyIdsBatch.length === batchSize) {
|
||||
policyIdsBatch = await this.getPolicyIdsBatch(batchSize, page);
|
||||
policyIds = [...policyIds, ...policyIdsBatch];
|
||||
page++;
|
||||
}
|
||||
|
||||
return policyIds;
|
||||
}
|
||||
|
||||
private async persistTokens(
|
||||
policyIds: string[],
|
||||
tokensMap: Record<string, string>
|
||||
): Promise<void> {
|
||||
if (!policyIds.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = appContextService.getConfig();
|
||||
const batchSize = config?.setup?.agentPolicySchemaUpgradeBatchSize ?? 100;
|
||||
|
||||
asyncForEach(chunk(policyIds, batchSize), async (policyIdsBatch) => {
|
||||
await this.soClient.bulkCreate<Partial<UninstallTokenSOAttributes>>(
|
||||
policyIdsBatch.map((policyId) => ({
|
||||
type: UNINSTALL_TOKENS_SAVED_OBJECT_TYPE,
|
||||
attributes: this.isEncryptionAvailable
|
||||
? {
|
||||
policy_id: policyId,
|
||||
token: tokensMap[policyId],
|
||||
}
|
||||
: {
|
||||
policy_id: policyId,
|
||||
token_plain: tokensMap[policyId],
|
||||
},
|
||||
}))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private generateToken(): string {
|
||||
return randomBytes(32).toString('hex');
|
||||
}
|
||||
|
||||
private hashToken(token: string): string {
|
||||
if (!token) {
|
||||
return '';
|
||||
}
|
||||
const hash = createHash('sha256');
|
||||
hash.update(token);
|
||||
return hash.digest('base64');
|
||||
}
|
||||
|
||||
private get soClient() {
|
||||
if (this._soClient) {
|
||||
return this._soClient;
|
||||
}
|
||||
|
||||
const fakeRequest = {
|
||||
headers: {},
|
||||
getBasePath: () => '',
|
||||
path: '/',
|
||||
route: { settings: {} },
|
||||
url: { href: {} },
|
||||
raw: { req: { url: '/' } },
|
||||
} as unknown as KibanaRequest;
|
||||
|
||||
this._soClient = appContextService.getSavedObjects().getScopedClient(fakeRequest, {
|
||||
excludedExtensions: [SECURITY_EXTENSION_ID],
|
||||
includedHiddenTypes: [UNINSTALL_TOKENS_SAVED_OBJECT_TYPE],
|
||||
});
|
||||
|
||||
return this._soClient;
|
||||
}
|
||||
|
||||
private get isEncryptionAvailable(): boolean {
|
||||
return appContextService.getEncryptedSavedObjectsSetup()?.canEncrypt ?? false;
|
||||
}
|
||||
}
|
|
@ -177,6 +177,19 @@ async function createSetupSideEffects(
|
|||
}
|
||||
await appContextService.getMessageSigningService()?.generateKeyPair();
|
||||
|
||||
logger.debug('Generating Agent uninstall tokens');
|
||||
if (!appContextService.getEncryptedSavedObjectsSetup()?.canEncrypt) {
|
||||
logger.warn(
|
||||
'xpack.encryptedSavedObjects.encryptionKey is not configured, agent uninstall tokens are being stored in plain text'
|
||||
);
|
||||
}
|
||||
await appContextService.getUninstallTokenService()?.generateTokensForAllPolicies();
|
||||
|
||||
if (appContextService.getEncryptedSavedObjectsSetup()?.canEncrypt) {
|
||||
logger.debug('Checking for and encrypting plain text uninstall tokens');
|
||||
await appContextService.getUninstallTokenService()?.encryptTokens();
|
||||
}
|
||||
|
||||
logger.debug('Upgrade Agent policy schema version');
|
||||
await upgradeAgentPolicySchemaVersion(soClient);
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue