mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[Fleet] Create package policy API Create Secrets (behind feature flag) (#156036)
## Summary The first part of secrets phase 1. This is not a fully working implementation of secrets just yet, hence why it is behind a feature flag. This just implements creating secrets: - on package policy creation, if a package has fields with `secrets: true` set, then their values are stored in the .secrets system index, a reference to the secret is stored on the package policy e.g { id : 1234 isSecretReference : true } - The compiled policy (returned from the get full agent policy API, or stored in the .fleet-policies index) shows secret values in the format `$co.elastic.secret{12345}` and includes a top level secret_references array with all secret IDs in it, allowing fleet server to look them up in one swoop. - This works for pakacge level vars, input level vars and stream level vars Part of https://github.com/elastic/kibana/issues/154715 How to test: ``` # clone the elasticsearch repo gh pr checkout 95625 ./gradlew run # now get a service token curl -XPOST -u elastic:password http://localhost:9200/_security/service/elastic/kibana/credential/token/token1 # paste the service token into your kibana config under # elasticsearch.serviceAccountToken: "<your_token>" # once kibana has started, we now need to run our own package registry to get a package with secrets in # replace /Users/markhopkin/dev with the path to your kibana docker run -p 8080:8080 -v /Users/markhopkin/dev/kibana/x-pack/test/fleet_api_integration/apis/fixtures/test_packages:/packages/test-packages -v /Users/markhopkin/dev/kibana/x-pack/test/fleet_api_integration/apis/fixtures/package_registry_config.yml:/package-registry/config.yml docker.elastic.co/package-registry/package-registry:main # once kibana has started successfully once and installed the fleet_server package, add this to your kibana config xpack.fleet.registryUrl: http://localhost:8080 # you can now see the 'secrets' package and create a package policy # after creating the package policy, check the .fleet-secrets index, the .fleet-policies index or the get package policy API to see how the secrets have been stored ``` ### Checklist Delete any items that are not applicable to this PR. - [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials - [ ] [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 - [ ] Any UI touched in this PR is usable by keyboard only (learn more about [keyboard accessibility](https://webaim.org/techniques/keyboard/)) - [ ] Any UI touched in this PR does not create any new axe failures (run axe in browser: [FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/), [Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US)) - [ ] If a plugin configuration key changed, check if it needs to be allowlisted in the cloud and added to the [docker list](https://github.com/elastic/kibana/blob/main/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker) - [ ] This renders correctly on smaller devices using a responsive layout. (You can test this [in your browser](https://www.browserstack.com/guide/responsive-testing-on-local-server)) - [ ] This was checked for [cross-browser compatibility](https://www.elastic.co/support/matrix#matrix_browsers) ### Risk Matrix Delete this section if it is not applicable to this PR. Before closing this PR, invite QA, stakeholders, and other developers to identify risks that should be tested prior to the change/feature release. When forming the risk matrix, consider some of the following examples and how they may potentially impact the change: | Risk | Probability | Severity | Mitigation/Notes | |---------------------------|-------------|----------|-------------------------| | Multiple Spaces—unexpected behavior in non-default Kibana Space. | Low | High | Integration tests will verify that all features are still supported in non-default Kibana Space and when user switches between spaces. | | Multiple nodes—Elasticsearch polling might have race conditions when multiple Kibana nodes are polling for the same tasks. | High | Low | Tasks are idempotent, so executing them multiple times will not result in logical error, but will degrade performance. To test for this case we add plenty of unit tests around this logic and document manual testing procedure. | | Code should gracefully handle cases when feature X or plugin Y are disabled. | Medium | High | Unit tests will verify that any feature flag or plugin combination still results in our service operational. | | [See more potential risk examples](https://github.com/elastic/kibana/blob/main/RISK_MATRIX.mdx) | ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
f59471bcdc
commit
53d3dcc537
38 changed files with 1406 additions and 46 deletions
|
@ -1656,6 +1656,13 @@
|
|||
"dynamic": false,
|
||||
"properties": {}
|
||||
},
|
||||
"secret_references": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "keyword"
|
||||
}
|
||||
}
|
||||
},
|
||||
"revision": {
|
||||
"type": "integer"
|
||||
},
|
||||
|
|
|
@ -105,7 +105,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
|
|||
"ingest-agent-policies": "d9906923f595f6f3163672351ed3a46472eb280e",
|
||||
"ingest-download-sources": "95a15b6589ef46e75aca8f7e534c493f99cc3ccd",
|
||||
"ingest-outputs": "f5adeb3f6abc732a6067137e170578dbf1f58c62",
|
||||
"ingest-package-policies": "6dc1c9b80a8dc95fbc9c6d9b73dfc56a098eb440",
|
||||
"ingest-package-policies": "b0b652adb1b26d056d8ed3c0303d0ad85c2c1ae9",
|
||||
"ingest_manager_settings": "fb75bff08a8de3435b23664b1191f9244a255701",
|
||||
"inventory-view": "6d47ef0b38166ecbd1c2fc7394599a4500db1ae4",
|
||||
"kql-telemetry": "92d6357aa3ce28727492f86a54783f802dc38893",
|
||||
|
|
|
@ -22,6 +22,7 @@ export * from './authz';
|
|||
export * from './file_storage';
|
||||
export * from './message_signing_keys';
|
||||
export * from './locators';
|
||||
export * from './secrets';
|
||||
export * from './uninstall_tokens';
|
||||
|
||||
// TODO: This is the default `index.max_result_window` ES setting, which dictates
|
||||
|
|
8
x-pack/plugins/fleet/common/constants/secrets.ts
Normal file
8
x-pack/plugins/fleet/common/constants/secrets.ts
Normal file
|
@ -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 SECRETS_INDEX = '.fleet-secrets';
|
|
@ -23,6 +23,7 @@ export const allowedExperimentalValues = Object.freeze({
|
|||
showExperimentalShipperOptions: false,
|
||||
fleetServerStandalone: false,
|
||||
agentTamperProtectionEnabled: false,
|
||||
secretsStorage: false,
|
||||
});
|
||||
|
||||
type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;
|
||||
|
|
|
@ -62,9 +62,13 @@ properties:
|
|||
agent:
|
||||
type: string
|
||||
nullable: true
|
||||
download_source_id:
|
||||
type: string
|
||||
nullable: true
|
||||
secret_references:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
required:
|
||||
- id
|
||||
- outputs
|
||||
|
|
|
@ -28,6 +28,17 @@ const DATA_STREAM_DATASET_VAR: RegistryVarsEntry = {
|
|||
show_user: true,
|
||||
};
|
||||
|
||||
export function packageHasNoPolicyTemplates(packageInfo: PackageInfo): boolean {
|
||||
return (
|
||||
!packageInfo.policy_templates ||
|
||||
packageInfo.policy_templates.length === 0 ||
|
||||
!packageInfo.policy_templates.find(
|
||||
(policyTemplate) =>
|
||||
isInputOnlyPolicyTemplate(policyTemplate) ||
|
||||
(policyTemplate.inputs && policyTemplate.inputs.length > 0)
|
||||
)
|
||||
);
|
||||
}
|
||||
export function isInputOnlyPolicyTemplate(
|
||||
policyTemplate: RegistryPolicyTemplate
|
||||
): policyTemplate is RegistryPolicyInputOnlyTemplate {
|
||||
|
|
|
@ -22,10 +22,10 @@ import type {
|
|||
import {
|
||||
isValidNamespace,
|
||||
doesPackageHaveIntegrations,
|
||||
isInputOnlyPolicyTemplate,
|
||||
getNormalizedInputs,
|
||||
getNormalizedDataStreams,
|
||||
} from '.';
|
||||
import { packageHasNoPolicyTemplates } from './policy_template';
|
||||
|
||||
type Errors = string[] | null;
|
||||
|
||||
|
@ -92,15 +92,7 @@ export const validatePackagePolicy = (
|
|||
}, {} as ValidationEntry);
|
||||
}
|
||||
|
||||
if (
|
||||
!packageInfo.policy_templates ||
|
||||
packageInfo.policy_templates.length === 0 ||
|
||||
!packageInfo.policy_templates.find(
|
||||
(policyTemplate) =>
|
||||
isInputOnlyPolicyTemplate(policyTemplate) ||
|
||||
(policyTemplate.inputs && policyTemplate.inputs.length > 0)
|
||||
)
|
||||
) {
|
||||
if (!packageInfo?.policy_templates?.length || packageHasNoPolicyTemplates(packageInfo)) {
|
||||
validationResults.inputs = {};
|
||||
return validationResults;
|
||||
}
|
||||
|
|
|
@ -43,6 +43,7 @@ export interface FleetConfigType {
|
|||
maxAgentPoliciesWithInactivityTimeout?: number;
|
||||
disableRegistryVersionCheck?: boolean;
|
||||
bundledPackageLocation?: string;
|
||||
testSecretsIndex?: string;
|
||||
};
|
||||
internal?: {
|
||||
disableILMPolicies: boolean;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import type { agentPolicyStatuses } from '../../constants';
|
||||
import type { MonitoringType, ValueOf } from '..';
|
||||
import type { MonitoringType, PolicySecretReference, ValueOf } from '..';
|
||||
|
||||
import type { PackagePolicy, PackagePolicyPackage } from './package_policy';
|
||||
import type { Output } from './output';
|
||||
|
@ -122,6 +122,7 @@ export interface FullAgentPolicy {
|
|||
signing_key: string;
|
||||
};
|
||||
};
|
||||
secret_references?: PolicySecretReference[];
|
||||
signed?: {
|
||||
data: string;
|
||||
signature: string;
|
||||
|
|
|
@ -396,6 +396,7 @@ export enum RegistryVarsEntryKeys {
|
|||
options = 'options',
|
||||
default = 'default',
|
||||
os = 'os',
|
||||
secret = 'secret',
|
||||
}
|
||||
|
||||
// EPR types this as `[]map[string]interface{}`
|
||||
|
@ -407,6 +408,7 @@ export interface RegistryVarsEntry {
|
|||
[RegistryVarsEntryKeys.description]?: string;
|
||||
[RegistryVarsEntryKeys.type]: RegistryVarType;
|
||||
[RegistryVarsEntryKeys.required]?: boolean;
|
||||
[RegistryVarsEntryKeys.secret]?: boolean;
|
||||
[RegistryVarsEntryKeys.show_user]?: boolean;
|
||||
[RegistryVarsEntryKeys.multi]?: boolean;
|
||||
[RegistryVarsEntryKeys.options]?: Array<{ value: string; text: string }>;
|
||||
|
|
|
@ -18,3 +18,4 @@ export * from './preconfiguration';
|
|||
export * from './download_sources';
|
||||
export * from './fleet_server_policy_config';
|
||||
export * from './fleet_proxy';
|
||||
export * from './secret';
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import type { RegistryRelease, ExperimentalDataStreamFeature } from './epm';
|
||||
import type { PolicySecretReference } from './secret';
|
||||
|
||||
export interface PackagePolicyPackage {
|
||||
name: string;
|
||||
|
@ -97,6 +98,7 @@ export interface PackagePolicy extends Omit<NewPackagePolicy, 'inputs'> {
|
|||
version?: string;
|
||||
agents?: number;
|
||||
revision: number;
|
||||
secret_references?: PolicySecretReference[];
|
||||
updated_at: string;
|
||||
updated_by: string;
|
||||
created_at: string;
|
||||
|
|
24
x-pack/plugins/fleet/common/types/models/secret.ts
Normal file
24
x-pack/plugins/fleet/common/types/models/secret.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 interface Secret {
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface SecretElasticDoc {
|
||||
value: string;
|
||||
}
|
||||
// this replaces a var value with a reference to a secret
|
||||
export interface VarSecretReference {
|
||||
id: string;
|
||||
isSecretRef: true;
|
||||
}
|
||||
// this is used in the top level secret_refs array on package and agent policies
|
||||
export interface PolicySecretReference {
|
||||
id: string;
|
||||
}
|
|
@ -133,6 +133,7 @@ export const config: PluginConfigDescriptor = {
|
|||
disableRegistryVersionCheck: schema.boolean({ defaultValue: false }),
|
||||
allowAgentUpgradeSourceUri: schema.boolean({ defaultValue: false }),
|
||||
bundledPackageLocation: schema.string({ defaultValue: DEFAULT_BUNDLED_PACKAGE_LOCATION }),
|
||||
testSecretsIndex: schema.maybe(schema.string()),
|
||||
}),
|
||||
packageVerification: schema.object({
|
||||
gpgKeyPath: schema.string({ defaultValue: DEFAULT_GPG_KEY_PATH }),
|
||||
|
|
|
@ -77,6 +77,8 @@ export {
|
|||
ENDPOINT_PRIVILEGES,
|
||||
// Message signing service
|
||||
MESSAGE_SIGNING_SERVICE_API_ROUTES,
|
||||
// secrets
|
||||
SECRETS_INDEX,
|
||||
} from '../../common/constants';
|
||||
|
||||
export {
|
||||
|
|
|
@ -201,6 +201,7 @@ const getSavedObjectTypes = (): { [key: string]: SavedObjectsType } => ({
|
|||
dynamic: false,
|
||||
properties: {},
|
||||
},
|
||||
secret_references: { properties: { id: { type: 'keyword' } } },
|
||||
revision: { type: 'integer' },
|
||||
updated_at: { type: 'date' },
|
||||
updated_by: { type: 'keyword' },
|
||||
|
|
|
@ -68,6 +68,7 @@ Object {
|
|||
},
|
||||
},
|
||||
"revision": 1,
|
||||
"secret_references": Array [],
|
||||
"signed": Object {
|
||||
"data": "",
|
||||
"signature": "",
|
||||
|
@ -143,6 +144,7 @@ Object {
|
|||
},
|
||||
},
|
||||
"revision": 1,
|
||||
"secret_references": Array [],
|
||||
"signed": Object {
|
||||
"data": "",
|
||||
"signature": "",
|
||||
|
@ -218,6 +220,7 @@ Object {
|
|||
},
|
||||
},
|
||||
"revision": 1,
|
||||
"secret_references": Array [],
|
||||
"signed": Object {
|
||||
"data": "",
|
||||
"signature": "",
|
||||
|
|
|
@ -106,6 +106,9 @@ export async function getFullAgentPolicy(
|
|||
packageInfoCache,
|
||||
getOutputIdForAgentPolicy(dataOutput)
|
||||
),
|
||||
secret_references: (agentPolicy?.package_policies || []).flatMap(
|
||||
(policy) => policy.secret_references || []
|
||||
),
|
||||
revision: agentPolicy.revision,
|
||||
agent: {
|
||||
download: {
|
||||
|
|
|
@ -9,6 +9,7 @@ import Handlebars from 'handlebars';
|
|||
import { safeLoad, safeDump } from 'js-yaml';
|
||||
|
||||
import type { PackagePolicyConfigRecord } from '../../../../common/types';
|
||||
import { toCompiledSecretRef } from '../../secrets';
|
||||
|
||||
const handlebars = Handlebars.create();
|
||||
|
||||
|
@ -23,7 +24,6 @@ export function compileTemplate(variables: PackagePolicyConfigRecord, templateSt
|
|||
}
|
||||
|
||||
compiledTemplate = replaceRootLevelYamlVariables(yamlValues, compiledTemplate);
|
||||
|
||||
const yamlFromCompiledTemplate = safeLoad(compiledTemplate, {});
|
||||
|
||||
// Hack to keep empty string ('') values around in the end yaml because
|
||||
|
@ -90,6 +90,8 @@ function buildTemplateVariables(variables: PackagePolicyConfigRecord, templateSt
|
|||
const yamlKeyPlaceholder = `##${key}##`;
|
||||
varPart[lastKeyPart] = recordEntry.value ? `"${yamlKeyPlaceholder}"` : null;
|
||||
yamlValues[yamlKeyPlaceholder] = recordEntry.value ? safeLoad(recordEntry.value) : null;
|
||||
} else if (recordEntry.value && recordEntry.value.isSecretRef) {
|
||||
varPart[lastKeyPart] = toCompiledSecretRef(recordEntry.value.id);
|
||||
} else {
|
||||
varPart[lastKeyPart] = recordEntry.value;
|
||||
}
|
||||
|
|
|
@ -172,6 +172,7 @@ jest.mock('./epm/packages', () => {
|
|||
return {
|
||||
getPackageInfo: jest.fn().mockImplementation(mockedGetPackageInfo),
|
||||
getInstallation: mockedGetInstallation,
|
||||
ensureInstalledPackage: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -256,6 +257,11 @@ describe('Package policy service', () => {
|
|||
enabled: true,
|
||||
policy_id: 'test',
|
||||
inputs: [],
|
||||
package: {
|
||||
name: 'test',
|
||||
title: 'Test',
|
||||
version: '0.0.1',
|
||||
},
|
||||
},
|
||||
// Skipping unique name verification just means we have to less mocking/setup
|
||||
{ id: 'test-package-policy', skipUniqueNameVerification: true }
|
||||
|
|
|
@ -67,6 +67,7 @@ import type {
|
|||
Installation,
|
||||
ExperimentalDataStreamFeature,
|
||||
DeletePackagePoliciesResponse,
|
||||
PolicySecretReference,
|
||||
} from '../../common/types';
|
||||
import { PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../constants';
|
||||
import {
|
||||
|
@ -112,6 +113,7 @@ import { updateDatastreamExperimentalFeatures } from './epm/packages/update';
|
|||
import type { PackagePolicyClient, PackagePolicyService } from './package_policy_service';
|
||||
import { installAssetsForInputPackagePolicy } from './epm/packages/install';
|
||||
import { auditLoggingService } from './audit_logging';
|
||||
import { extractAndWriteSecrets } from './secrets';
|
||||
|
||||
export type InputsOverride = Partial<NewPackagePolicyInput> & {
|
||||
vars?: Array<NewPackagePolicyInput['vars'] & { name: string }>;
|
||||
|
@ -148,10 +150,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
|
|||
context?: RequestHandlerContext,
|
||||
request?: KibanaRequest
|
||||
): Promise<PackagePolicy> {
|
||||
// Ensure an ID is provided, so we can include it in the audit logs below
|
||||
if (!options.id) {
|
||||
options.id = SavedObjectsUtils.generateId();
|
||||
}
|
||||
const packagePolicyId = options?.id || uuidv4();
|
||||
|
||||
let authorizationHeader = options.authorizationHeader;
|
||||
|
||||
|
@ -161,13 +160,13 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
|
|||
|
||||
auditLoggingService.writeCustomSoAuditLog({
|
||||
action: 'create',
|
||||
id: options.id,
|
||||
id: packagePolicyId,
|
||||
savedObjectType: PACKAGE_POLICY_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
|
||||
const logger = appContextService.getLogger();
|
||||
|
||||
const enrichedPackagePolicy = await packagePolicyService.runExternalCallbacks(
|
||||
let secretReferences: PolicySecretReference[] | undefined;
|
||||
let enrichedPackagePolicy = await packagePolicyService.runExternalCallbacks(
|
||||
'packagePolicyCreate',
|
||||
packagePolicy,
|
||||
soClient,
|
||||
|
@ -197,11 +196,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
|
|||
}
|
||||
|
||||
let elasticsearchPrivileges: NonNullable<PackagePolicy['elasticsearch']>['privileges'];
|
||||
// Add ids to stream
|
||||
const packagePolicyId = options?.id || uuidv4();
|
||||
let inputs: PackagePolicyInput[] = enrichedPackagePolicy.inputs.map((input) =>
|
||||
assignStreamIdToInput(packagePolicyId, input)
|
||||
);
|
||||
let inputs = getInputsWithStreamIds(enrichedPackagePolicy, packagePolicyId);
|
||||
|
||||
// Make sure the associated package is installed
|
||||
if (enrichedPackagePolicy.package?.name) {
|
||||
|
@ -244,6 +239,19 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
|
|||
}
|
||||
validatePackagePolicyOrThrow(enrichedPackagePolicy, pkgInfo);
|
||||
|
||||
const { secretsStorage: secretsStorageEnabled } = appContextService.getExperimentalFeatures();
|
||||
if (secretsStorageEnabled) {
|
||||
const secretsRes = await extractAndWriteSecrets({
|
||||
packagePolicy: enrichedPackagePolicy,
|
||||
packageInfo: pkgInfo,
|
||||
esClient,
|
||||
});
|
||||
|
||||
enrichedPackagePolicy = secretsRes.packagePolicy;
|
||||
secretReferences = secretsRes.secret_references;
|
||||
|
||||
inputs = getInputsWithStreamIds(enrichedPackagePolicy, packagePolicyId);
|
||||
}
|
||||
inputs = await _compilePackagePolicyInputs(pkgInfo, enrichedPackagePolicy.vars || {}, inputs);
|
||||
|
||||
elasticsearchPrivileges = pkgInfo.elasticsearch?.privileges;
|
||||
|
@ -270,6 +278,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
|
|||
: {}),
|
||||
inputs,
|
||||
...(elasticsearchPrivileges && { elasticsearch: { privileges: elasticsearchPrivileges } }),
|
||||
...(secretReferences?.length && { secret_references: secretReferences }),
|
||||
revision: 1,
|
||||
created_at: isoDate,
|
||||
created_by: options?.user?.username ?? 'system',
|
||||
|
@ -351,9 +360,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
|
|||
const packagePolicyId = packagePolicy.id ?? uuidv4();
|
||||
const agentPolicyId = packagePolicy.policy_id;
|
||||
|
||||
let inputs = packagePolicy.inputs.map((input) =>
|
||||
assignStreamIdToInput(packagePolicyId, input)
|
||||
);
|
||||
let inputs = getInputsWithStreamIds(packagePolicy, packagePolicyId);
|
||||
|
||||
const { id, ...pkgPolicyWithoutId } = packagePolicy;
|
||||
|
||||
|
@ -671,9 +678,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
|
|||
await requireUniqueName(soClient, enrichedPackagePolicy, id);
|
||||
}
|
||||
|
||||
let inputs = restOfPackagePolicy.inputs.map((input) =>
|
||||
assignStreamIdToInput(oldPackagePolicy.id, input)
|
||||
);
|
||||
let inputs = getInputsWithStreamIds(restOfPackagePolicy, oldPackagePolicy.id);
|
||||
|
||||
inputs = enforceFrozenInputs(oldPackagePolicy.inputs, inputs, options?.force);
|
||||
let elasticsearchPrivileges: NonNullable<PackagePolicy['elasticsearch']>['privileges'];
|
||||
|
@ -822,9 +827,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient {
|
|||
throw new PackagePolicyRestrictionRelatedError(`Cannot update package policy ${id}`);
|
||||
}
|
||||
|
||||
let inputs = restOfPackagePolicy.inputs.map((input) =>
|
||||
assignStreamIdToInput(oldPackagePolicy.id, input)
|
||||
);
|
||||
let inputs = getInputsWithStreamIds(restOfPackagePolicy, oldPackagePolicy.id);
|
||||
|
||||
inputs = enforceFrozenInputs(oldPackagePolicy.inputs, inputs, options?.force);
|
||||
let elasticsearchPrivileges: NonNullable<PackagePolicy['elasticsearch']>['privileges'];
|
||||
|
@ -1746,13 +1749,19 @@ function validatePackagePolicyOrThrow(packagePolicy: NewPackagePolicy, pkgInfo:
|
|||
}
|
||||
}
|
||||
|
||||
function assignStreamIdToInput(packagePolicyId: string, input: NewPackagePolicyInput) {
|
||||
return {
|
||||
...input,
|
||||
streams: input.streams.map((stream) => {
|
||||
return { ...stream, id: `${input.type}-${stream.data_stream.dataset}-${packagePolicyId}` };
|
||||
}),
|
||||
};
|
||||
function getInputsWithStreamIds(
|
||||
packagePolicy: NewPackagePolicy,
|
||||
packagePolicyId: string
|
||||
): PackagePolicy['inputs'] {
|
||||
return packagePolicy.inputs.map((input) => {
|
||||
return {
|
||||
...input,
|
||||
streams: input.streams.map((stream) => ({
|
||||
...stream,
|
||||
id: `${input.type}-${stream.data_stream.dataset}-${packagePolicyId}`,
|
||||
})),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function _compilePackagePolicyInputs(
|
||||
|
|
569
x-pack/plugins/fleet/server/services/secrets.test.ts
Normal file
569
x-pack/plugins/fleet/server/services/secrets.test.ts
Normal file
|
@ -0,0 +1,569 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
* 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 { NewPackagePolicy, PackageInfo } from '../types';
|
||||
|
||||
import { getPolicySecretPaths } from './secrets';
|
||||
|
||||
describe('getPolicySecretPaths', () => {
|
||||
describe('integration package with one policy template', () => {
|
||||
const mockIntegrationPackage = {
|
||||
name: 'mock-package',
|
||||
title: 'Mock package',
|
||||
version: '0[0].0',
|
||||
description: 'description',
|
||||
type: 'integration',
|
||||
status: 'not_installed',
|
||||
vars: [
|
||||
{ name: 'pkg-secret-1', type: 'text', secret: true },
|
||||
{ name: 'pkg-secret-2', type: 'text', secret: true },
|
||||
],
|
||||
data_streams: [
|
||||
{
|
||||
dataset: 'somedataset',
|
||||
streams: [
|
||||
{
|
||||
input: 'foo',
|
||||
title: 'Foo',
|
||||
vars: [
|
||||
{ name: 'stream-secret-1', type: 'text', secret: true },
|
||||
{ name: 'stream-secret-2', type: 'text', secret: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
policy_templates: [
|
||||
{
|
||||
name: 'pkgPolicy1',
|
||||
title: 'Package policy 1',
|
||||
description: 'test package policy',
|
||||
inputs: [
|
||||
{
|
||||
type: 'foo',
|
||||
title: 'Foo',
|
||||
vars: [
|
||||
{ default: 'foo-input-var-value', name: 'foo-input-var-name', type: 'text' },
|
||||
{
|
||||
name: 'input-secret-1',
|
||||
type: 'text',
|
||||
secret: true,
|
||||
},
|
||||
{
|
||||
name: 'input-secret-2',
|
||||
type: 'text',
|
||||
secret: true,
|
||||
},
|
||||
{ name: 'foo-input3-var-name', type: 'text', multi: true },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as unknown as PackageInfo;
|
||||
it('policy with package level secret vars', () => {
|
||||
const packagePolicy = {
|
||||
vars: {
|
||||
'pkg-secret-1': {
|
||||
value: 'pkg-secret-1-val',
|
||||
},
|
||||
'pkg-secret-2': {
|
||||
value: 'pkg-secret-2-val',
|
||||
},
|
||||
},
|
||||
inputs: [],
|
||||
} as unknown as NewPackagePolicy;
|
||||
|
||||
expect(getPolicySecretPaths(packagePolicy, mockIntegrationPackage)).toEqual([
|
||||
{
|
||||
path: 'vars.pkg-secret-1',
|
||||
value: {
|
||||
value: 'pkg-secret-1-val',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'vars.pkg-secret-2',
|
||||
value: {
|
||||
value: 'pkg-secret-2-val',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
it('policy with input level secret vars', () => {
|
||||
const packagePolicy = {
|
||||
inputs: [
|
||||
{
|
||||
type: 'foo',
|
||||
policy_template: 'pkgPolicy1',
|
||||
vars: {
|
||||
'input-secret-1': {
|
||||
value: 'input-secret-1-val',
|
||||
},
|
||||
'input-secret-2': {
|
||||
value: 'input-secret-2-val',
|
||||
},
|
||||
},
|
||||
streams: [],
|
||||
},
|
||||
],
|
||||
} as unknown as NewPackagePolicy;
|
||||
|
||||
expect(getPolicySecretPaths(packagePolicy, mockIntegrationPackage)).toEqual([
|
||||
{
|
||||
path: 'inputs[0].vars.input-secret-1',
|
||||
value: { value: 'input-secret-1-val' },
|
||||
},
|
||||
{
|
||||
path: 'inputs[0].vars.input-secret-2',
|
||||
value: { value: 'input-secret-2-val' },
|
||||
},
|
||||
]);
|
||||
});
|
||||
it('stream level secret vars', () => {
|
||||
const packagePolicy = {
|
||||
inputs: [
|
||||
{
|
||||
type: 'foo',
|
||||
policy_template: 'pkgPolicy1',
|
||||
streams: [
|
||||
{
|
||||
data_stream: {
|
||||
dataset: 'somedataset',
|
||||
type: 'logs',
|
||||
},
|
||||
vars: {
|
||||
'stream-secret-1': {
|
||||
value: 'stream-secret-1-value',
|
||||
},
|
||||
'stream-secret-2': {
|
||||
value: 'stream-secret-2-value',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as unknown as NewPackagePolicy;
|
||||
|
||||
expect(getPolicySecretPaths(packagePolicy, mockIntegrationPackage)).toEqual([
|
||||
{
|
||||
path: 'inputs[0].streams[0].vars.stream-secret-1',
|
||||
value: { value: 'stream-secret-1-value' },
|
||||
},
|
||||
{
|
||||
path: 'inputs[0].streams[0].vars.stream-secret-2',
|
||||
value: { value: 'stream-secret-2-value' },
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('integration package with multiple policy templates (e.g AWS)', () => {
|
||||
const miniAWsPackage = {
|
||||
name: 'aws',
|
||||
title: 'AWS',
|
||||
version: '0.5.3',
|
||||
release: 'beta',
|
||||
description: 'AWS Integration',
|
||||
type: 'integration',
|
||||
policy_templates: [
|
||||
{
|
||||
name: 'billing',
|
||||
title: 'AWS Billing',
|
||||
description: 'Collect AWS billing metrics',
|
||||
data_streams: ['billing'],
|
||||
inputs: [
|
||||
{
|
||||
type: 'aws/metrics',
|
||||
title: 'Collect billing metrics',
|
||||
description: 'Collect billing metrics',
|
||||
input_group: 'metrics',
|
||||
vars: [
|
||||
{
|
||||
name: 'password',
|
||||
type: 'text',
|
||||
secret: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'cloudtrail',
|
||||
title: 'AWS Cloudtrail',
|
||||
description: 'Collect logs from AWS Cloudtrail',
|
||||
data_streams: ['cloudtrail'],
|
||||
inputs: [
|
||||
{
|
||||
type: 's3',
|
||||
title: 'Collect logs from Cloudtrail service',
|
||||
description: 'Collecting Cloudtrail logs using S3 input',
|
||||
input_group: 'logs',
|
||||
vars: [
|
||||
{
|
||||
name: 'password',
|
||||
type: 'text',
|
||||
secret: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'httpjson',
|
||||
title: 'Collect logs from third-party REST API (experimental)',
|
||||
description: 'Collect logs from third-party REST API (experimental)',
|
||||
input_group: 'logs',
|
||||
vars: [
|
||||
{
|
||||
name: 'password',
|
||||
type: 'text',
|
||||
secret: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
vars: [
|
||||
{
|
||||
name: 'secret_access_key',
|
||||
type: 'text',
|
||||
title: 'Secret Access Key',
|
||||
multi: false,
|
||||
required: false,
|
||||
show_user: false,
|
||||
secret: true,
|
||||
},
|
||||
],
|
||||
data_streams: [
|
||||
{
|
||||
type: 'metrics',
|
||||
dataset: 'aws.billing',
|
||||
title: 'AWS billing metrics',
|
||||
release: 'beta',
|
||||
streams: [
|
||||
{
|
||||
input: 'aws/metrics',
|
||||
vars: [
|
||||
{
|
||||
name: 'password',
|
||||
type: 'text',
|
||||
secret: true,
|
||||
},
|
||||
],
|
||||
template_path: 'stream.yml.hbs',
|
||||
title: 'AWS Billing metrics',
|
||||
description: 'Collect AWS billing metrics',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
package: 'aws',
|
||||
path: 'billing',
|
||||
},
|
||||
{
|
||||
type: 'logs',
|
||||
dataset: 'aws.cloudtrail',
|
||||
title: 'AWS CloudTrail logs',
|
||||
release: 'beta',
|
||||
ingest_pipeline: 'default',
|
||||
streams: [
|
||||
{
|
||||
input: 's3',
|
||||
vars: [
|
||||
{
|
||||
name: 'password',
|
||||
type: 'text',
|
||||
secret: true,
|
||||
},
|
||||
],
|
||||
template_path: 's3.yml.hbs',
|
||||
},
|
||||
{
|
||||
input: 'httpjson',
|
||||
vars: [
|
||||
{
|
||||
name: 'username',
|
||||
type: 'text',
|
||||
title: 'Splunk REST API Username',
|
||||
multi: false,
|
||||
required: true,
|
||||
show_user: true,
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
type: 'password',
|
||||
title: 'Splunk REST API Password',
|
||||
multi: false,
|
||||
required: true,
|
||||
show_user: true,
|
||||
secret: true,
|
||||
},
|
||||
],
|
||||
template_path: 'httpjson.yml.hbs',
|
||||
},
|
||||
],
|
||||
package: 'aws',
|
||||
path: 'cloudtrail',
|
||||
},
|
||||
],
|
||||
} as PackageInfo;
|
||||
it('single policy with package + input + stream level secret var', () => {
|
||||
const policy = {
|
||||
vars: {
|
||||
secret_access_key: {
|
||||
value: 'my_secret_access_key',
|
||||
},
|
||||
},
|
||||
inputs: [
|
||||
{
|
||||
type: 'aws/metrics',
|
||||
policy_template: 'billing',
|
||||
enabled: true,
|
||||
vars: {
|
||||
password: { value: 'billing_input_password', type: 'text' },
|
||||
},
|
||||
streams: [
|
||||
{
|
||||
enabled: true,
|
||||
data_stream: { type: 'metrics', dataset: 'aws.billing' },
|
||||
vars: {
|
||||
password: { value: 'billing_stream_password', type: 'text' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(
|
||||
getPolicySecretPaths(
|
||||
policy as unknown as NewPackagePolicy,
|
||||
miniAWsPackage as unknown as PackageInfo
|
||||
)
|
||||
).toEqual([
|
||||
{
|
||||
path: 'vars.secret_access_key',
|
||||
value: {
|
||||
value: 'my_secret_access_key',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'inputs[0].vars.password',
|
||||
value: {
|
||||
type: 'text',
|
||||
value: 'billing_input_password',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'inputs[0].streams[0].vars.password',
|
||||
value: {
|
||||
type: 'text',
|
||||
value: 'billing_stream_password',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
it('double policy with package + input + stream level secret var', () => {
|
||||
const policy = {
|
||||
vars: {
|
||||
secret_access_key: {
|
||||
value: 'my_secret_access_key',
|
||||
},
|
||||
},
|
||||
inputs: [
|
||||
{
|
||||
type: 'httpjson',
|
||||
policy_template: 'cloudtrail',
|
||||
enabled: false,
|
||||
vars: {
|
||||
password: { value: 'cloudtrail_httpjson_input_password' },
|
||||
},
|
||||
streams: [
|
||||
{
|
||||
data_stream: { type: 'logs', dataset: 'aws.cloudtrail' },
|
||||
vars: {
|
||||
username: { value: 'hop_dev' },
|
||||
password: { value: 'cloudtrail_httpjson_stream_password' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 's3',
|
||||
policy_template: 'cloudtrail',
|
||||
enabled: true,
|
||||
vars: {
|
||||
password: { value: 'cloudtrail_s3_input_password' },
|
||||
},
|
||||
streams: [
|
||||
{
|
||||
enabled: true,
|
||||
data_stream: { type: 'logs', dataset: 'aws.cloudtrail' },
|
||||
vars: {
|
||||
password: { value: 'cloudtrail_s3_stream_password' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(
|
||||
getPolicySecretPaths(
|
||||
policy as unknown as NewPackagePolicy,
|
||||
miniAWsPackage as unknown as PackageInfo
|
||||
)
|
||||
).toEqual([
|
||||
{
|
||||
path: 'vars.secret_access_key',
|
||||
value: {
|
||||
value: 'my_secret_access_key',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'inputs[0].vars.password',
|
||||
value: {
|
||||
value: 'cloudtrail_httpjson_input_password',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'inputs[0].streams[0].vars.password',
|
||||
value: {
|
||||
value: 'cloudtrail_httpjson_stream_password',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'inputs[1].vars.password',
|
||||
value: {
|
||||
value: 'cloudtrail_s3_input_password',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'inputs[1].streams[0].vars.password',
|
||||
value: {
|
||||
value: 'cloudtrail_s3_stream_password',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('input package', () => {
|
||||
const mockInputPackage = {
|
||||
name: 'log',
|
||||
version: '2.0.0',
|
||||
description: 'Collect custom logs with Elastic Agent.',
|
||||
title: 'Custom Logs',
|
||||
format_version: '2.6.0',
|
||||
owner: {
|
||||
github: 'elastic/elastic-agent-data-plane',
|
||||
},
|
||||
type: 'input',
|
||||
categories: ['custom', 'custom_logs'],
|
||||
conditions: {},
|
||||
icons: [],
|
||||
policy_templates: [
|
||||
{
|
||||
name: 'logs',
|
||||
title: 'Custom log file',
|
||||
description: 'Collect your custom log files.',
|
||||
multiple: true,
|
||||
input: 'logfile',
|
||||
type: 'logs',
|
||||
template_path: 'input.yml.hbs',
|
||||
vars: [
|
||||
{
|
||||
name: 'paths',
|
||||
required: true,
|
||||
title: 'Log file path',
|
||||
description: 'Path to log files to be collected',
|
||||
type: 'text',
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
name: 'data_stream.dataset',
|
||||
required: true,
|
||||
title: 'Dataset name',
|
||||
description:
|
||||
"Set the name for your dataset. Changing the dataset will send the data to a different index. You can't use `-` in the name of a dataset and only valid characters for [Elasticsearch index names](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html).\n",
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
name: 'secret-1',
|
||||
type: 'text',
|
||||
secret: true,
|
||||
},
|
||||
{
|
||||
name: 'secret-2',
|
||||
type: 'text',
|
||||
secret: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
it('template level vars', () => {
|
||||
const policy = {
|
||||
inputs: [
|
||||
{
|
||||
type: 'logfile',
|
||||
policy_template: 'logs',
|
||||
enabled: true,
|
||||
streams: [
|
||||
{
|
||||
enabled: true,
|
||||
data_stream: {
|
||||
type: 'logs',
|
||||
dataset: 'log.logs',
|
||||
},
|
||||
vars: {
|
||||
paths: {
|
||||
value: ['/tmp/test.log'],
|
||||
},
|
||||
'data_stream.dataset': {
|
||||
value: 'hello',
|
||||
},
|
||||
'secret-1': {
|
||||
value: 'secret-1-value',
|
||||
},
|
||||
'secret-2': {
|
||||
value: 'secret-2-value',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(
|
||||
getPolicySecretPaths(
|
||||
policy as unknown as NewPackagePolicy,
|
||||
mockInputPackage as unknown as PackageInfo
|
||||
)
|
||||
).toEqual([
|
||||
{
|
||||
path: 'inputs[0].streams[0].vars.secret-1',
|
||||
value: {
|
||||
value: 'secret-1-value',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'inputs[0].streams[0].vars.secret-2',
|
||||
value: {
|
||||
value: 'secret-2-value',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
314
x-pack/plugins/fleet/server/services/secrets.ts
Normal file
314
x-pack/plugins/fleet/server/services/secrets.ts
Normal file
|
@ -0,0 +1,314 @@
|
|||
/*
|
||||
* 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/server';
|
||||
import type { BulkResponse, DeleteResponse } from '@elastic/elasticsearch/lib/api/types';
|
||||
|
||||
import { keyBy, partition } from 'lodash';
|
||||
import { set } from '@kbn/safer-lodash-set';
|
||||
|
||||
import { packageHasNoPolicyTemplates } from '../../common/services/policy_template';
|
||||
|
||||
import type {
|
||||
NewPackagePolicy,
|
||||
PackagePolicyConfigRecordEntry,
|
||||
RegistryStream,
|
||||
} from '../../common';
|
||||
|
||||
import {
|
||||
doesPackageHaveIntegrations,
|
||||
getNormalizedDataStreams,
|
||||
getNormalizedInputs,
|
||||
} from '../../common/services';
|
||||
|
||||
import type {
|
||||
PackageInfo,
|
||||
PackagePolicy,
|
||||
RegistryVarsEntry,
|
||||
Secret,
|
||||
VarSecretReference,
|
||||
PolicySecretReference,
|
||||
} from '../types';
|
||||
|
||||
import { FleetError } from '../errors';
|
||||
import { SECRETS_INDEX } from '../constants';
|
||||
|
||||
import { auditLoggingService } from './audit_logging';
|
||||
|
||||
import { appContextService } from './app_context';
|
||||
|
||||
interface SecretPath {
|
||||
path: string;
|
||||
value: PackagePolicyConfigRecordEntry;
|
||||
}
|
||||
|
||||
// This will be removed once the secrets index PR is merged into elasticsearch
|
||||
function getSecretsIndex() {
|
||||
const testIndex = appContextService.getConfig()?.developer?.testSecretsIndex;
|
||||
if (testIndex) {
|
||||
return testIndex;
|
||||
}
|
||||
return SECRETS_INDEX;
|
||||
}
|
||||
|
||||
export async function createSecrets(opts: {
|
||||
esClient: ElasticsearchClient;
|
||||
values: string[];
|
||||
}): Promise<Secret[]> {
|
||||
const { esClient, values } = opts;
|
||||
const logger = appContextService.getLogger();
|
||||
const body = values.flatMap((value) => [
|
||||
{
|
||||
create: { _index: getSecretsIndex() },
|
||||
},
|
||||
{ value },
|
||||
]);
|
||||
let res: BulkResponse;
|
||||
try {
|
||||
res = await esClient.bulk({
|
||||
body,
|
||||
});
|
||||
|
||||
const [errorItems, successItems] = partition(res.items, (a) => a.create?.error);
|
||||
|
||||
successItems.forEach((item) => {
|
||||
auditLoggingService.writeCustomAuditLog({
|
||||
message: `secret created: ${item.create!._id}`,
|
||||
event: {
|
||||
action: 'secret_create',
|
||||
category: ['database'],
|
||||
type: ['access'],
|
||||
outcome: 'success',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
if (errorItems.length) {
|
||||
throw new Error(JSON.stringify(errorItems));
|
||||
}
|
||||
|
||||
return res.items.map((item, i) => ({
|
||||
id: item.create!._id as string,
|
||||
value: values[i],
|
||||
}));
|
||||
} catch (e) {
|
||||
const msg = `Error creating secrets in ${getSecretsIndex()} index: ${e}`;
|
||||
logger.error(msg);
|
||||
throw new FleetError(msg);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteSecret(opts: {
|
||||
esClient: ElasticsearchClient;
|
||||
id: string;
|
||||
}): Promise<DeleteResponse['result']> {
|
||||
const { esClient, id } = opts;
|
||||
let res: DeleteResponse;
|
||||
try {
|
||||
res = await esClient.delete({
|
||||
index: getSecretsIndex(),
|
||||
id,
|
||||
});
|
||||
|
||||
auditLoggingService.writeCustomAuditLog({
|
||||
message: `secret deleted: ${id}`,
|
||||
event: {
|
||||
action: 'secret_delete',
|
||||
category: ['database'],
|
||||
type: ['access'],
|
||||
outcome: 'success',
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
const logger = appContextService.getLogger();
|
||||
const msg = `Error deleting secret '${id}' from ${getSecretsIndex()} index: ${e}`;
|
||||
logger.error(msg);
|
||||
throw new FleetError(msg);
|
||||
}
|
||||
|
||||
return res.result;
|
||||
}
|
||||
|
||||
export async function extractAndWriteSecrets(opts: {
|
||||
packagePolicy: NewPackagePolicy;
|
||||
packageInfo: PackageInfo;
|
||||
esClient: ElasticsearchClient;
|
||||
}): Promise<{ packagePolicy: NewPackagePolicy; secret_references: PolicySecretReference[] }> {
|
||||
const { packagePolicy, packageInfo, esClient } = opts;
|
||||
const secretPaths = getPolicySecretPaths(packagePolicy, packageInfo);
|
||||
|
||||
if (!secretPaths.length) {
|
||||
return { packagePolicy, secret_references: [] };
|
||||
}
|
||||
|
||||
const secrets = await createSecrets({
|
||||
esClient,
|
||||
values: secretPaths.map((secretPath) => secretPath.value.value),
|
||||
});
|
||||
|
||||
const policyWithSecretRefs = JSON.parse(JSON.stringify(packagePolicy));
|
||||
secretPaths.forEach((secretPath, i) => {
|
||||
set(policyWithSecretRefs, secretPath.path + '.value', toVarSecretRef(secrets[i].id));
|
||||
});
|
||||
|
||||
return {
|
||||
packagePolicy: policyWithSecretRefs,
|
||||
secret_references: secrets.map(({ id }) => ({ id })),
|
||||
};
|
||||
}
|
||||
|
||||
function isSecretVar(varDef: RegistryVarsEntry) {
|
||||
return varDef.secret === true;
|
||||
}
|
||||
|
||||
function containsSecretVar(vars?: RegistryVarsEntry[]) {
|
||||
return vars?.some(isSecretVar);
|
||||
}
|
||||
|
||||
// this is how secrets are stored on the package policy
|
||||
function toVarSecretRef(id: string): VarSecretReference {
|
||||
return { id, isSecretRef: true };
|
||||
}
|
||||
|
||||
// this is how IDs are inserted into compiled templates
|
||||
export function toCompiledSecretRef(id: string) {
|
||||
return `$co.elastic.secret{${id}}`;
|
||||
}
|
||||
|
||||
// Given a package policy and a package,
|
||||
// returns an array of lodash style paths to all secrets and their current values
|
||||
export function getPolicySecretPaths(
|
||||
packagePolicy: PackagePolicy | NewPackagePolicy,
|
||||
packageInfo: PackageInfo
|
||||
): SecretPath[] {
|
||||
const packageLevelVarPaths = _getPackageLevelSecretPaths(packagePolicy, packageInfo);
|
||||
|
||||
if (!packageInfo?.policy_templates?.length || packageHasNoPolicyTemplates(packageInfo)) {
|
||||
return packageLevelVarPaths;
|
||||
}
|
||||
|
||||
const inputSecretPaths = _getInputSecretPaths(packagePolicy, packageInfo);
|
||||
|
||||
return [...packageLevelVarPaths, ...inputSecretPaths];
|
||||
}
|
||||
|
||||
function _getPackageLevelSecretPaths(
|
||||
packagePolicy: NewPackagePolicy,
|
||||
packageInfo: PackageInfo
|
||||
): SecretPath[] {
|
||||
const packageSecretVars = packageInfo.vars?.filter(isSecretVar) || [];
|
||||
const packageSecretVarsByName = keyBy(packageSecretVars, 'name');
|
||||
const packageVars = Object.entries(packagePolicy.vars || {});
|
||||
|
||||
return packageVars.reduce((vars, [name, configEntry], i) => {
|
||||
if (packageSecretVarsByName[name]) {
|
||||
vars.push({
|
||||
value: configEntry,
|
||||
path: `vars.${name}`,
|
||||
});
|
||||
}
|
||||
return vars;
|
||||
}, [] as SecretPath[]);
|
||||
}
|
||||
|
||||
function _getInputSecretPaths(
|
||||
packagePolicy: NewPackagePolicy,
|
||||
packageInfo: PackageInfo
|
||||
): SecretPath[] {
|
||||
if (!packageInfo?.policy_templates?.length) return [];
|
||||
|
||||
const inputSecretVarDefsByPolicyTemplateAndType =
|
||||
_getInputSecretVarDefsByPolicyTemplateAndType(packageInfo);
|
||||
|
||||
const streamSecretVarDefsByDatasetAndInput =
|
||||
_getStreamSecretVarDefsByDatasetAndInput(packageInfo);
|
||||
|
||||
return packagePolicy.inputs.flatMap((input, inputIndex) => {
|
||||
if (!input.vars && !input.streams) {
|
||||
return [];
|
||||
}
|
||||
const currentInputVarPaths: SecretPath[] = [];
|
||||
const inputKey = doesPackageHaveIntegrations(packageInfo)
|
||||
? `${input.policy_template}-${input.type}`
|
||||
: input.type;
|
||||
const inputVars = Object.entries(input.vars || {});
|
||||
if (inputVars.length) {
|
||||
inputVars.forEach(([name, configEntry]) => {
|
||||
if (inputSecretVarDefsByPolicyTemplateAndType[inputKey]?.[name]) {
|
||||
currentInputVarPaths.push({
|
||||
path: `inputs[${inputIndex}].vars.${name}`,
|
||||
value: configEntry,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (input.streams.length) {
|
||||
input.streams.forEach((stream, streamIndex) => {
|
||||
const streamVarDefs =
|
||||
streamSecretVarDefsByDatasetAndInput[`${stream.data_stream.dataset}-${input.type}`];
|
||||
if (streamVarDefs && Object.keys(streamVarDefs).length) {
|
||||
Object.entries(stream.vars || {}).forEach(([name, configEntry]) => {
|
||||
if (streamVarDefs[name]) {
|
||||
currentInputVarPaths.push({
|
||||
path: `inputs[${inputIndex}].streams[${streamIndex}].vars.${name}`,
|
||||
value: configEntry,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return currentInputVarPaths;
|
||||
});
|
||||
}
|
||||
|
||||
// a map of all secret vars for each dataset and input combo
|
||||
function _getStreamSecretVarDefsByDatasetAndInput(packageInfo: PackageInfo) {
|
||||
const dataStreams = getNormalizedDataStreams(packageInfo);
|
||||
const streamsByDatasetAndInput = dataStreams.reduce<Record<string, RegistryStream>>(
|
||||
(streams, dataStream) => {
|
||||
dataStream.streams?.forEach((stream) => {
|
||||
streams[`${dataStream.dataset}-${stream.input}`] = stream;
|
||||
});
|
||||
return streams;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
return Object.entries(streamsByDatasetAndInput).reduce<
|
||||
Record<string, Record<string, RegistryVarsEntry>>
|
||||
>((varDefs, [path, stream]) => {
|
||||
if (stream.vars && containsSecretVar(stream.vars)) {
|
||||
const secretVars = stream.vars.filter(isSecretVar);
|
||||
varDefs[path] = keyBy(secretVars, 'name');
|
||||
}
|
||||
return varDefs;
|
||||
}, {});
|
||||
}
|
||||
|
||||
// a map of all secret vars for each policyTemplate and input type combo
|
||||
function _getInputSecretVarDefsByPolicyTemplateAndType(packageInfo: PackageInfo) {
|
||||
if (!packageInfo?.policy_templates?.length) return {};
|
||||
|
||||
const hasIntegrations = doesPackageHaveIntegrations(packageInfo);
|
||||
return packageInfo.policy_templates.reduce<Record<string, Record<string, RegistryVarsEntry>>>(
|
||||
(varDefs, policyTemplate) => {
|
||||
const inputs = getNormalizedInputs(policyTemplate);
|
||||
inputs.forEach((input) => {
|
||||
const varDefKey = hasIntegrations ? `${policyTemplate.name}-${input.type}` : input.type;
|
||||
const secretVars = input?.vars?.filter(isSecretVar);
|
||||
if (secretVars?.length) {
|
||||
varDefs[varDefKey] = keyBy(secretVars, 'name');
|
||||
}
|
||||
});
|
||||
return varDefs;
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
|
@ -91,6 +91,10 @@ export type {
|
|||
BulkInstallPackageInfo,
|
||||
PackageAssetReference,
|
||||
ExperimentalDataStreamFeature,
|
||||
Secret,
|
||||
SecretElasticDoc,
|
||||
VarSecretReference,
|
||||
PolicySecretReference,
|
||||
} from '../../common/types';
|
||||
export { ElasticsearchAssetType, KibanaAssetType, KibanaSavedObjectType } from '../../common/types';
|
||||
export { dataTypes } from '../../common/constants';
|
||||
|
|
|
@ -239,4 +239,11 @@ export const PackagePolicySchema = schema.object({
|
|||
compiled_input: schema.maybe(schema.any()),
|
||||
})
|
||||
),
|
||||
secret_references: schema.maybe(
|
||||
schema.arrayOf(
|
||||
schema.object({
|
||||
id: schema.string(),
|
||||
})
|
||||
)
|
||||
),
|
||||
});
|
||||
|
|
|
@ -95,5 +95,6 @@
|
|||
"@kbn/shared-ux-router",
|
||||
"@kbn/shared-ux-link-redirect-app",
|
||||
"@kbn/core-http-router-server-internal",
|
||||
"@kbn/safer-lodash-set",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
package_var_secret: {{package_var_secret}}
|
||||
input_var_secret: {{input_var_secret}}
|
|
@ -0,0 +1,4 @@
|
|||
config.version: "2"
|
||||
package_var_secret: {{package_var_secret}}
|
||||
input_var_secret: {{input_var_secret}}
|
||||
stream_var_secret: {{stream_var_secret}}
|
|
@ -0,0 +1,16 @@
|
|||
- name: data_stream.type
|
||||
type: constant_keyword
|
||||
description: >
|
||||
Data stream type.
|
||||
- name: data_stream.dataset
|
||||
type: constant_keyword
|
||||
description: >
|
||||
Data stream dataset.
|
||||
- name: data_stream.namespace
|
||||
type: constant_keyword
|
||||
description: >
|
||||
Data stream namespace.
|
||||
- name: '@timestamp'
|
||||
type: date
|
||||
description: >
|
||||
Event timestamp.
|
|
@ -0,0 +1,13 @@
|
|||
title: Test stream
|
||||
type: logs
|
||||
streams:
|
||||
- input: test_input
|
||||
title: test input
|
||||
vars:
|
||||
- name: stream_var_secret
|
||||
type: text
|
||||
title: Stream Var Secret
|
||||
multi: false
|
||||
required: true
|
||||
show_user: true
|
||||
secret: true
|
|
@ -0,0 +1,3 @@
|
|||
# secrets
|
||||
|
||||
This package has secrets
|
|
@ -0,0 +1,7 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path fill="#F04E98" d="M29,32.0001 L15.935,9.4321 C13.48,5.1941 7,6.9351 7,11.8321 L7,52.1681 C7,57.0651 13.48,58.8061 15.935,54.5671 L29,32.0001 Z"/>
|
||||
<path fill="#FA744E" d="M34.7773,32.0001 L33.3273,34.5051 L20.2613,57.0731 C19.8473,57.7871 19.3533,58.4271 18.8023,59.0001 L34.9273,59.0001 C38.7073,59.0001 42.2213,57.0601 44.2363,53.8611 L58.0003,32.0001 L34.7773,32.0001 Z"/>
|
||||
<path fill="#343741" d="M44.2363,10.1392 C42.2213,6.9402 38.7073,5.0002 34.9273,5.0002 L18.8023,5.0002 C19.3533,5.5732 19.8473,6.2122 20.2613,6.9272 L33.3273,29.4942 L34.7773,32.0002 L58.0003,32.0002 L44.2363,10.1392 Z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 750 B |
|
@ -0,0 +1,52 @@
|
|||
format_version: 1.0.0
|
||||
name: secrets
|
||||
title: Package with secrets
|
||||
description: This integration package has 3 secrets.
|
||||
version: 1.0.0
|
||||
categories: []
|
||||
# Options are experimental, beta, ga
|
||||
release: beta
|
||||
# The package type. The options for now are [integration, solution], more type might be added in the future.
|
||||
# The default type is integration and will be set if empty.
|
||||
type: integration
|
||||
license: basic
|
||||
owner:
|
||||
github: elastic/fleet
|
||||
|
||||
requirement:
|
||||
elasticsearch:
|
||||
versions: ">7.7.0"
|
||||
kibana:
|
||||
versions: ">7.7.0"
|
||||
|
||||
icons:
|
||||
- src: "/img/logo.svg"
|
||||
size: "16x16"
|
||||
type: "image/svg+xml"
|
||||
|
||||
vars:
|
||||
- name: package_var_secret
|
||||
type: text
|
||||
title: Package Var Secret
|
||||
multi: false
|
||||
required: true
|
||||
show_user: true
|
||||
secret: true
|
||||
policy_templates:
|
||||
- name: secrets
|
||||
title: This
|
||||
description: Test Package for Upgrading Package Policies
|
||||
inputs:
|
||||
- type: test_input
|
||||
title: Test Input
|
||||
description: Test Input
|
||||
enabled: true
|
||||
template_path: input.yml.hbs
|
||||
vars:
|
||||
- name: input_var_secret
|
||||
type: text
|
||||
title: Input Var Secret
|
||||
multi: false
|
||||
required: true
|
||||
show_user: true
|
||||
secret: true
|
|
@ -18,6 +18,19 @@ export default function ({ loadTestFile, getService }) {
|
|||
loadTestFile(require.resolve('./fleet_setup')); // ~ 6s
|
||||
|
||||
// Enrollment API keys
|
||||
loadTestFile(require.resolve('./enrollment_api_keys/crud'));
|
||||
|
||||
// Package policies
|
||||
loadTestFile(require.resolve('./policy_secrets'));
|
||||
loadTestFile(require.resolve('./package_policy/create'));
|
||||
loadTestFile(require.resolve('./package_policy/update'));
|
||||
loadTestFile(require.resolve('./package_policy/get'));
|
||||
loadTestFile(require.resolve('./package_policy/delete'));
|
||||
loadTestFile(require.resolve('./package_policy/upgrade'));
|
||||
loadTestFile(require.resolve('./package_policy/input_package_create_upgrade'));
|
||||
|
||||
// Agent policies
|
||||
loadTestFile(require.resolve('./agent_policy'));
|
||||
loadTestFile(require.resolve('./enrollment_api_keys/crud')); // ~ 20s
|
||||
|
||||
// Data Streams
|
||||
|
|
|
@ -703,7 +703,7 @@ export default function (providerContext: FtrProviderContext) {
|
|||
.post(`/api/fleet/package_policies`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({
|
||||
name: 'unverified_content-1',
|
||||
name: 'unverified_content_' + Date.now(),
|
||||
description: '',
|
||||
namespace: 'default',
|
||||
policy_id: agentPolicyId,
|
||||
|
@ -739,7 +739,7 @@ export default function (providerContext: FtrProviderContext) {
|
|||
.post(`/api/fleet/package_policies`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({
|
||||
name: 'unverified_content-1',
|
||||
name: 'unverified_content-' + Date.now(),
|
||||
description: '',
|
||||
namespace: 'default',
|
||||
policy_id: agentPolicyId,
|
||||
|
|
271
x-pack/test/fleet_api_integration/apis/policy_secrets.ts
Normal file
271
x-pack/test/fleet_api_integration/apis/policy_secrets.ts
Normal file
|
@ -0,0 +1,271 @@
|
|||
/*
|
||||
* 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 { Client } from '@elastic/elasticsearch';
|
||||
import expect from '@kbn/expect';
|
||||
import { FullAgentPolicy } from '@kbn/fleet-plugin/common';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { FtrProviderContext } from '../../api_integration/ftr_provider_context';
|
||||
import { skipIfNoDockerRegistry } from '../helpers';
|
||||
import { setupFleetAndAgents } from './agents/services';
|
||||
|
||||
const secretVar = (id: string) => `$co.elastic.secret{${id}}`;
|
||||
|
||||
const arrayIdsEqual = (a: Array<{ id: string }>, b: Array<{ id: string }>) => {
|
||||
if (a.length !== b.length) {
|
||||
return false;
|
||||
}
|
||||
return a.every(({ id }) => b.find(({ id: bid }) => bid === id));
|
||||
};
|
||||
|
||||
export default function (providerContext: FtrProviderContext) {
|
||||
describe('fleet policy secrets', () => {
|
||||
const { getService } = providerContext;
|
||||
|
||||
const es: Client = getService('es');
|
||||
const supertest = getService('supertest');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
|
||||
const getPackagePolicyById = async (id: string) => {
|
||||
const { body } = await supertest.get(`/api/fleet/package_policies/${id}`);
|
||||
return body.item;
|
||||
};
|
||||
|
||||
const maybeCreateSecretsIndex = async () => {
|
||||
// create mock .secrets index for testing
|
||||
if (await es.indices.exists({ index: '.fleet-test-secrets' })) {
|
||||
await es.indices.delete({ index: '.fleet-test-secrets' });
|
||||
}
|
||||
await es.indices.create({
|
||||
index: '.fleet-test-secrets',
|
||||
body: {
|
||||
mappings: {
|
||||
properties: {
|
||||
value: {
|
||||
type: 'keyword',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const getFullAgentPolicyById = async (id: string) => {
|
||||
const { body } = await supertest.get(`/api/fleet/agent_policies/${id}/full`).expect(200);
|
||||
return body.item;
|
||||
};
|
||||
|
||||
const getLatestPolicyRevision = async (id: string): Promise<{ data: FullAgentPolicy }> => {
|
||||
const res = await es.search({
|
||||
index: '.fleet-policies',
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
term: {
|
||||
policy_id: id,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
sort: [
|
||||
{
|
||||
revision_idx: {
|
||||
order: 'desc',
|
||||
},
|
||||
},
|
||||
],
|
||||
size: 1,
|
||||
},
|
||||
});
|
||||
return res.hits.hits[0]._source as any as { data: FullAgentPolicy };
|
||||
};
|
||||
let createdPackagePolicyId: string;
|
||||
let packageVarId: string;
|
||||
let inputVarId: string;
|
||||
let streamVarId: string;
|
||||
let expectedCompiledStream: any;
|
||||
let expectedCompiledInput: any;
|
||||
|
||||
function expectCompiledPolicyVars(policy: any) {
|
||||
expect(
|
||||
arrayIdsEqual(policy.secret_references, [
|
||||
{ id: packageVarId },
|
||||
{ id: streamVarId },
|
||||
{ id: inputVarId },
|
||||
])
|
||||
).to.eql(true);
|
||||
expect(policy.inputs[0].package_var_secret).to.eql(secretVar(packageVarId));
|
||||
expect(policy.inputs[0].input_var_secret).to.eql(secretVar(inputVarId));
|
||||
expect(policy.inputs[0].streams[0].package_var_secret).to.eql(secretVar(packageVarId));
|
||||
expect(policy.inputs[0].streams[0].input_var_secret).to.eql(secretVar(inputVarId));
|
||||
expect(policy.inputs[0].streams[0].stream_var_secret).to.eql(secretVar(streamVarId));
|
||||
}
|
||||
|
||||
skipIfNoDockerRegistry(providerContext);
|
||||
let agentPolicyId: string;
|
||||
before(async () => {
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
await getService('esArchiver').load(
|
||||
'x-pack/test/functional/es_archives/fleet/empty_fleet_server'
|
||||
);
|
||||
await maybeCreateSecretsIndex();
|
||||
});
|
||||
|
||||
setupFleetAndAgents(providerContext);
|
||||
|
||||
before(async () => {
|
||||
const { body: agentPolicyResponse } = await supertest
|
||||
.post(`/api/fleet/agent_policies`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({
|
||||
name: `Test policy ${uuidv4()}`,
|
||||
namespace: 'default',
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
agentPolicyId = agentPolicyResponse.item.id;
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
await getService('esArchiver').unload(
|
||||
'x-pack/test/functional/es_archives/fleet/empty_fleet_server'
|
||||
);
|
||||
});
|
||||
it('Should correctly create the policy with secrets', async () => {
|
||||
const { body: createResBody } = await supertest
|
||||
.post(`/api/fleet/package_policies`)
|
||||
.set('kbn-xsrf', 'xxxx')
|
||||
.send({
|
||||
name: `secrets-${Date.now()}`,
|
||||
description: '',
|
||||
namespace: 'default',
|
||||
policy_id: agentPolicyId,
|
||||
inputs: {
|
||||
'secrets-test_input': {
|
||||
enabled: true,
|
||||
vars: {
|
||||
input_var_secret: 'input_secret_val',
|
||||
},
|
||||
streams: {
|
||||
'secrets.log': {
|
||||
enabled: true,
|
||||
vars: {
|
||||
stream_var_secret: 'stream_secret_val',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
vars: {
|
||||
package_var_secret: 'package_secret_val',
|
||||
},
|
||||
package: {
|
||||
name: 'secrets',
|
||||
version: '1.0.0',
|
||||
},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const createdPackagePolicy = createResBody.item;
|
||||
createdPackagePolicyId = createdPackagePolicy.id;
|
||||
packageVarId = createdPackagePolicy.vars.package_var_secret.value.id;
|
||||
expect(packageVarId).to.be.an('string');
|
||||
inputVarId = createdPackagePolicy.inputs[0].vars.input_var_secret.value.id;
|
||||
expect(inputVarId).to.be.an('string');
|
||||
streamVarId = createdPackagePolicy.inputs[0].streams[0].vars.stream_var_secret.value.id;
|
||||
expect(streamVarId).to.be.an('string');
|
||||
|
||||
expect(
|
||||
arrayIdsEqual(createdPackagePolicy.secret_references, [
|
||||
{ id: packageVarId },
|
||||
{ id: streamVarId },
|
||||
{ id: inputVarId },
|
||||
])
|
||||
).to.eql(true);
|
||||
expectedCompiledStream = {
|
||||
'config.version': 2,
|
||||
package_var_secret: secretVar(packageVarId),
|
||||
input_var_secret: secretVar(inputVarId),
|
||||
stream_var_secret: secretVar(streamVarId),
|
||||
};
|
||||
expect(createdPackagePolicy.inputs[0].streams[0].compiled_stream).to.eql(
|
||||
expectedCompiledStream
|
||||
);
|
||||
|
||||
expectedCompiledInput = {
|
||||
package_var_secret: secretVar(packageVarId),
|
||||
input_var_secret: secretVar(inputVarId),
|
||||
};
|
||||
|
||||
expect(createdPackagePolicy.inputs[0].compiled_input).to.eql(expectedCompiledInput);
|
||||
|
||||
expect(createdPackagePolicy.vars.package_var_secret.value.isSecretRef).to.eql(true);
|
||||
expect(createdPackagePolicy.inputs[0].vars.input_var_secret.value.isSecretRef).to.eql(true);
|
||||
expect(
|
||||
createdPackagePolicy.inputs[0].streams[0].vars.stream_var_secret.value.isSecretRef
|
||||
).to.eql(true);
|
||||
});
|
||||
|
||||
it('should return the policy correctly from the get policies API', async () => {
|
||||
const packagePolicy = await getPackagePolicyById(createdPackagePolicyId);
|
||||
expect(
|
||||
arrayIdsEqual(packagePolicy.secret_references, [
|
||||
{ id: packageVarId },
|
||||
{ id: streamVarId },
|
||||
{ id: inputVarId },
|
||||
])
|
||||
).to.eql(true);
|
||||
expect(packagePolicy.inputs[0].streams[0].compiled_stream).to.eql(expectedCompiledStream);
|
||||
expect(packagePolicy.inputs[0].compiled_input).to.eql(expectedCompiledInput);
|
||||
expect(packagePolicy.vars.package_var_secret.value.isSecretRef).to.eql(true);
|
||||
expect(packagePolicy.vars.package_var_secret.value.id).eql(packageVarId);
|
||||
expect(packagePolicy.inputs[0].vars.input_var_secret.value.isSecretRef).to.eql(true);
|
||||
expect(packagePolicy.inputs[0].vars.input_var_secret.value.id).eql(inputVarId);
|
||||
expect(packagePolicy.inputs[0].streams[0].vars.stream_var_secret.value.isSecretRef).to.eql(
|
||||
true
|
||||
);
|
||||
expect(packagePolicy.inputs[0].streams[0].vars.stream_var_secret.value.id).eql(streamVarId);
|
||||
});
|
||||
|
||||
it('should have correctly created the secrets', async () => {
|
||||
const searchRes = await es.search({
|
||||
index: '.fleet-test-secrets',
|
||||
body: {
|
||||
query: {
|
||||
ids: {
|
||||
values: [packageVarId, inputVarId, streamVarId],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(searchRes.hits.hits.length).to.eql(3);
|
||||
|
||||
const secretValuesById = searchRes.hits.hits.reduce((acc: any, secret: any) => {
|
||||
acc[secret._id] = secret._source.value;
|
||||
return acc;
|
||||
}, {});
|
||||
expect(secretValuesById[packageVarId]).to.eql('package_secret_val');
|
||||
expect(secretValuesById[inputVarId]).to.eql('input_secret_val');
|
||||
expect(secretValuesById[streamVarId]).to.eql('stream_secret_val');
|
||||
});
|
||||
|
||||
it('should have written the secrets to the .fleet-policies index', async () => {
|
||||
const { data: policyDoc } = await getLatestPolicyRevision(agentPolicyId);
|
||||
expectCompiledPolicyVars(policyDoc);
|
||||
});
|
||||
|
||||
it('should return secret refs from agent policy API', async () => {
|
||||
const agentPolicy = await getFullAgentPolicyById(agentPolicyId);
|
||||
|
||||
expectCompiledPolicyVars(agentPolicy);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -70,6 +70,8 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
'./apis/fixtures/package_verification/signatures/fleet_test_key_public.asc'
|
||||
)}`,
|
||||
`--xpack.securitySolution.enableExperimental=${JSON.stringify(['endpointRbacEnabled'])}`,
|
||||
`--xpack.fleet.enableExperimental=${JSON.stringify(['secretsStorage'])}`,
|
||||
`--xpack.fleet.developer.testSecretsIndex=.fleet-test-secrets`,
|
||||
`--logging.loggers=${JSON.stringify([
|
||||
...getKibanaCliLoggers(xPackAPITestsConfig.get('kbnTestServer.serverArgs')),
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue