From 8759b03474d1b18f8493f00752b417f09361a791 Mon Sep 17 00:00:00 2001 From: Rickyanto Ang Date: Thu, 28 Sep 2023 14:02:23 -0700 Subject: [PATCH] [Cloud Security] [CIS GCP] GCP Organization option (#166983) ## Summary This PR is for adding the GCP Organization option as well as updating the Single option to include Project ID field. Still rough Changes: - Added GCP Organization Option - Project ID field now exist on Google Cloud Shell Single option as well as Organization Option - Organization ID field added to the form when user chose account_type : GCP Organization - Project ID are now optional (previously users aren't able to save the integration without filling in the Project ID) - Removed Beta tag for CIS GCP TODO: - Make sure previous installation using previous wont break because of the new fields and requirement (migration) - More tests - Clean up --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../public/common/constants.ts | 1 - .../fleet_extensions/gcp_credential_form.tsx | 166 ++++++++++++++---- .../components/fleet_extensions/mocks.ts | 2 + .../policy_template_form.test.tsx | 119 ++++++++++--- .../fleet_extensions/policy_template_form.tsx | 153 ++++++++++++---- .../post_install_google_cloud_shell_modal.tsx | 11 +- .../google_cloud_shell_instructions.tsx | 4 +- .../steps/compute_steps.tsx | 14 +- ..._google_cloud_shell_managed_agent_step.tsx | 3 + .../enrollment_instructions/manual/index.tsx | 10 +- .../components/google_cloud_shell_guide.tsx | 17 +- ...egration_details_from_agent_policy.test.ts | 101 +++++++++++ ..._integration_details_from_agent_policy.tsx | 71 ++++++++ ...ration_details_from_package_policy.test.ts | 80 +++++++++ ...ntegration_details_from_package_policy.tsx | 53 ++++++ x-pack/plugins/fleet/public/services/index.ts | 2 + 16 files changed, 702 insertions(+), 105 deletions(-) create mode 100644 x-pack/plugins/fleet/public/services/get_gcp_integration_details_from_agent_policy.test.ts create mode 100644 x-pack/plugins/fleet/public/services/get_gcp_integration_details_from_agent_policy.tsx create mode 100644 x-pack/plugins/fleet/public/services/get_gcp_integration_details_from_package_policy.test.ts create mode 100644 x-pack/plugins/fleet/public/services/get_gcp_integration_details_from_package_policy.tsx diff --git a/x-pack/plugins/cloud_security_posture/public/common/constants.ts b/x-pack/plugins/cloud_security_posture/public/common/constants.ts index eb6318e7c6727..bec25a70dbd1e 100644 --- a/x-pack/plugins/cloud_security_posture/public/common/constants.ts +++ b/x-pack/plugins/cloud_security_posture/public/common/constants.ts @@ -93,7 +93,6 @@ export const cloudPostureIntegrations: CloudPostureIntegrations = { defaultMessage: 'CIS GCP', }), icon: googleCloudLogo, - isBeta: true, }, // needs to be a function that disables/enabled based on integration version { diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/gcp_credential_form.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/gcp_credential_form.tsx index 18ac1247d4b1a..411d5298580a5 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/gcp_credential_form.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/gcp_credential_form.tsx @@ -25,6 +25,7 @@ import type { NewPackagePolicy } from '@kbn/fleet-plugin/public'; import { NewPackagePolicyInput, PackageInfo } from '@kbn/fleet-plugin/common'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; +import { GcpCredentialsType } from '../../../common/types'; import { CLOUDBEAT_GCP, SETUP_ACCESS_CLOUD_SHELL, @@ -39,10 +40,12 @@ import { import { MIN_VERSION_GCP_CIS } from '../../common/constants'; import { cspIntegrationDocsNavigation } from '../../common/navigation/constants'; import { ReadDocumentation } from './aws_credentials_form/aws_credentials_form'; +import { GCP_ORGANIZATION_ACCOUNT } from './policy_template_form'; export const CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS = { GOOGLE_CLOUD_SHELL_SETUP: 'google_cloud_shell_setup_test_id', PROJECT_ID: 'project_id_test_id', + ORGANIZATION_ID: 'organization_id_test_id', CREDENTIALS_TYPE: 'credentials_type_test_id', CREDENTIALS_FILE: 'credentials_file_test_id', CREDENTIALS_JSON: 'credentials_json_test_id', @@ -71,7 +74,21 @@ const GCPSetupInfoContent = () => ( ); -const GoogleCloudShellSetup = () => { +const GoogleCloudShellSetup = ({ + fields, + onChange, + input, +}: { + fields: Array; + onChange: (key: string, value: string) => void; + input: NewPackagePolicyInput; +}) => { + const accountType = input.streams?.[0]?.vars?.['gcp.account_type']?.value; + const getFieldById = (id: keyof GcpInputFields['fields']) => { + return fields.find((element) => element.id === id); + }; + const projectIdFields = getFieldById('gcp.project_id'); + const organizationIdFields = getFieldById('gcp.organization_id'); return ( <> { defaultMessage="Log into your Google Cloud Console" /> -
  • - -
  • + {accountType === GCP_ORGANIZATION_ACCOUNT ? ( +
  • + +
  • + ) : ( +
  • + +
  • + )} +
  • { + + {organizationIdFields && accountType === GCP_ORGANIZATION_ACCOUNT && ( + + onChange(organizationIdFields.id, event.target.value)} + /> + + )} + {projectIdFields && ( + + onChange(projectIdFields.id, event.target.value)} + /> + + )} + + ); }; @@ -137,6 +189,12 @@ interface GcpInputFields { export const gcpField: GcpInputFields = { fields: { + 'gcp.organization_id': { + label: i18n.translate('xpack.csp.gcpIntegration.organizationIdFieldLabel', { + defaultMessage: 'Organization ID', + }), + type: 'text', + }, 'gcp.project_id': { label: i18n.translate('xpack.csp.gcpIntegration.projectidFieldLabel', { defaultMessage: 'Project ID', @@ -190,17 +248,14 @@ const getSetupFormatOptions = (): Array<{ interface GcpFormProps { newPolicy: NewPackagePolicy; - input: Extract< - NewPackagePolicyPostureInput, - { type: 'cloudbeat/cis_aws' | 'cloudbeat/cis_eks' | 'cloudbeat/cis_gcp' } - >; + input: Extract; updatePolicy(updatedPolicy: NewPackagePolicy): void; packageInfo: PackageInfo; setIsValid: (isValid: boolean) => void; onChange: any; } -const getInputVarsFields = (input: NewPackagePolicyInput, fields: GcpFields) => +export const getInputVarsFields = (input: NewPackagePolicyInput, fields: GcpFields) => Object.entries(input.streams[0].vars || {}) .filter(([id]) => id in fields) .map(([id, inputVar]) => { @@ -290,6 +345,10 @@ const useCloudShellUrl = ({ }, [newPolicy?.vars?.cloud_shell_url, newPolicy, packageInfo, setupFormat]); }; +export const getGcpCredentialsType = ( + input: Extract +): GcpCredentialsType | undefined => input.streams[0].vars?.setup_access.value; + export const GcpCredentialsForm = ({ input, newPolicy, @@ -298,6 +357,12 @@ export const GcpCredentialsForm = ({ setIsValid, onChange, }: GcpFormProps) => { + /* Create a subset of properties from GcpField to use for hiding value of credentials json and credentials file when user switch from Manual to Cloud Shell, we wanna keep Project and Organization ID */ + const subsetOfGcpField = (({ ['gcp.credentials.file']: a, ['gcp.credentials.json']: b }) => ({ + 'gcp.credentials.file': a, + ['gcp.credentials.json']: b, + }))(gcpField.fields); + const fieldsToHide = getInputVarsFields(input, subsetOfGcpField); const fields = getInputVarsFields(input, gcpField.fields); const validSemantic = semverValid(packageInfo.version); const integrationVersionNumberOnly = semverCoerce(validSemantic) || ''; @@ -305,9 +370,20 @@ export const GcpCredentialsForm = ({ const fieldsSnapshot = useRef({}); const lastSetupAccessType = useRef(undefined); const setupFormat = getSetupFormatFromInput(input); - const getFieldById = (id: keyof GcpInputFields['fields']) => { - return fields.find((element) => element.id === id); - }; + const accountType = input.streams?.[0]?.vars?.['gcp.account_type']?.value; + const isOrganization = accountType === 'organization-account'; + // Integration is Invalid IF Version is not at least 1.5.0 OR Setup Access is manual but Project ID is empty + useEffect(() => { + const isInvalidPolicy = isInvalid; + + setIsValid(!isInvalidPolicy); + + onChange({ + isValid: !isInvalidPolicy, + updatedPolicy: newPolicy, + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [setupFormat, input.type]); useCloudShellUrl({ packageInfo, @@ -316,23 +392,23 @@ export const GcpCredentialsForm = ({ setupFormat, }); const onSetupFormatChange = (newSetupFormat: SetupFormatGCP) => { - if (newSetupFormat === SETUP_ACCESS_CLOUD_SHELL) { + if (newSetupFormat === 'google_cloud_shell') { // We need to store the current manual fields to restore them later fieldsSnapshot.current = Object.fromEntries( - fields.map((field) => [field.id, { value: field.value }]) + fieldsToHide.map((field) => [field.id, { value: field.value }]) ); // We need to store the last manual credentials type to restore it later - lastSetupAccessType.current = input.streams[0].vars?.setup_access?.value; + lastSetupAccessType.current = getGcpCredentialsType(input); updatePolicy( getPosturePolicy(newPolicy, input.type, { setup_access: { - value: SETUP_ACCESS_CLOUD_SHELL, + value: 'google_cloud_shell', type: 'text', }, // Clearing fields from previous setup format to prevent exposing credentials // when switching from manual to cloud formation - ...Object.fromEntries(fields.map((field) => [field.id, { value: undefined }])), + ...Object.fromEntries(fieldsToHide.map((field) => [field.id, { value: undefined }])), }) ); } else { @@ -340,7 +416,7 @@ export const GcpCredentialsForm = ({ getPosturePolicy(newPolicy, input.type, { setup_access: { // Restoring last manual credentials type - value: SETUP_ACCESS_MANUAL, + value: lastSetupAccessType.current || SETUP_ACCESS_MANUAL, type: 'text', }, // Restoring fields from manual setup format if any @@ -349,20 +425,6 @@ export const GcpCredentialsForm = ({ ); } }; - // Integration is Invalid IF Version is not at least 1.5.0 OR Setup Access is manual but Project ID is empty - useEffect(() => { - const isProjectIdEmpty = - setupFormat === SETUP_ACCESS_MANUAL && !getFieldById('gcp.project_id')?.value; - const isInvalidPolicy = isInvalid || isProjectIdEmpty; - - setIsValid(!isInvalidPolicy); - - onChange({ - isValid: !isInvalidPolicy, - updatedPolicy: newPolicy, - }); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [input, packageInfo, setupFormat]); if (isInvalid) { return ( @@ -385,19 +447,29 @@ export const GcpCredentialsForm = ({ size="s" options={getSetupFormatOptions()} idSelected={setupFormat} - onChange={onSetupFormatChange} + onChange={(idSelected: SetupFormatGCP) => + idSelected !== setupFormat && onSetupFormatChange(idSelected) + } /> - {setupFormat === SETUP_ACCESS_MANUAL ? ( - updatePolicy(getPosturePolicy(newPolicy, input.type, { [key]: { value } })) } + input={input} /> ) : ( - + + updatePolicy(getPosturePolicy(newPolicy, input.type, { [key]: { value } })) + } + isOrganization={isOrganization} + /> )} + @@ -408,13 +480,18 @@ export const GcpCredentialsForm = ({ const GcpInputVarFields = ({ fields, onChange, + isOrganization, }: { fields: Array; onChange: (key: string, value: string) => void; + isOrganization: boolean; }) => { const getFieldById = (id: keyof GcpInputFields['fields']) => { return fields.find((element) => element.id === id); }; + + const organizationIdFields = getFieldById('gcp.organization_id'); + const projectIdFields = getFieldById('gcp.project_id'); const credentialsTypeFields = getFieldById('gcp.credentials.type'); const credentialFilesFields = getFieldById('gcp.credentials.file'); @@ -428,6 +505,17 @@ const GcpInputVarFields = ({ return (
    + {organizationIdFields && isOrganization && ( + + onChange(organizationIdFields.id, event.target.value)} + /> + + )} {projectIdFields && ( ', () => { it(`renders Google Cloud Shell forms when Setup Access is set to Google Cloud Shell`, () => { let policy = getMockPolicyGCP(); policy = getPosturePolicy(policy, CLOUDBEAT_GCP, { - credentials_type: { value: 'credentials-file' }, + 'gcp.account_type': { value: GCP_ORGANIZATION_ACCOUNT }, setup_access: { value: 'google_cloud_shell' }, }); @@ -1028,31 +1030,6 @@ describe('', () => { ).toBeInTheDocument(); }); - it(`project ID is required for Manual users`, () => { - let policy = getMockPolicyGCP(); - policy = getPosturePolicy(policy, CLOUDBEAT_GCP, { - 'gcp.project_id': { value: undefined }, - setup_access: { value: 'manual' }, - }); - - const { rerender } = render( - - ); - expect(onChange).toHaveBeenCalledWith({ - isValid: false, - updatedPolicy: policy, - }); - policy = getPosturePolicy(policy, CLOUDBEAT_GCP, { - 'gcp.project_id': { value: '' }, - setup_access: { value: 'manual' }, - }); - rerender(); - expect(onChange).toHaveBeenCalledWith({ - isValid: false, - updatedPolicy: policy, - }); - }); - it(`renders ${CLOUDBEAT_GCP} Credentials File fields`, () => { let policy = getMockPolicyGCP(); policy = getPosturePolicy(policy, CLOUDBEAT_GCP, { @@ -1136,6 +1113,96 @@ describe('', () => { updatedPolicy: policy, }); }); + + it(`${CLOUDBEAT_GCP} form do not displays upgrade message for supported versions and gcp organization option is enabled`, () => { + let policy = getMockPolicyGCP(); + policy = getPosturePolicy(policy, CLOUDBEAT_GCP, { + 'gcp.credentials.type': { value: 'manual' }, + 'gcp.account_type': { value: GCP_ORGANIZATION_ACCOUNT }, + }); + + const { queryByText, getByLabelText } = render( + + ); + + expect( + queryByText( + 'GCP Organization not supported in current integration version. Please upgrade to the latest version to enable GCP Organizations integration.' + ) + ).not.toBeInTheDocument(); + expect(getByLabelText('GCP Organization')).toBeEnabled(); + }); + + it(`renders ${CLOUDBEAT_GCP} Organization fields when account type is Organization and Setup Access is Google Cloud Shell`, () => { + let policy = getMockPolicyGCP(); + policy = getPosturePolicy(policy, CLOUDBEAT_GCP, { + 'gcp.account_type': { value: GCP_ORGANIZATION_ACCOUNT }, + setup_access: { value: 'google_cloud_shell' }, + }); + + const { getByLabelText, getByTestId } = render( + + ); + + expect(getByTestId(CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.ORGANIZATION_ID)).toBeInTheDocument(); + + expect(getByLabelText('Organization ID')).toBeInTheDocument(); + }); + + it(`renders ${CLOUDBEAT_GCP} Organization fields when account type is Organization and Setup Access is manual`, () => { + let policy = getMockPolicyGCP(); + policy = getPosturePolicy(policy, CLOUDBEAT_GCP, { + 'gcp.account_type': { value: GCP_ORGANIZATION_ACCOUNT }, + setup_access: { value: 'manual' }, + }); + + const { getByLabelText, getByTestId } = render( + + ); + + expect(getByTestId(CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.ORGANIZATION_ID)).toBeInTheDocument(); + + expect(getByLabelText('Organization ID')).toBeInTheDocument(); + }); + + it(`Should not render ${CLOUDBEAT_GCP} Organization fields when account type is Single`, () => { + let policy = getMockPolicyGCP(); + policy = getPosturePolicy(policy, CLOUDBEAT_GCP, { + 'gcp.account_type': { value: GCP_SINGLE_ACCOUNT }, + setup_access: { value: 'google_cloud_shell' }, + }); + + const { queryByLabelText, queryByTestId } = render( + + ); + + expect(queryByTestId(CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.ORGANIZATION_ID)).toBeNull(); + + expect(queryByLabelText('Organization ID')).toBeNull(); + }); + + it(`updates ${CLOUDBEAT_GCP} organization id`, () => { + let policy = getMockPolicyGCP(); + policy = getPosturePolicy(policy, CLOUDBEAT_GCP, { + 'gcp.account_type': { value: GCP_ORGANIZATION_ACCOUNT }, + setup_access: { value: 'manual' }, + }); + + const { getByTestId } = render( + + ); + + userEvent.type(getByTestId(CIS_GCP_INPUT_FIELDS_TEST_SUBJECTS.ORGANIZATION_ID), 'c'); + + policy = getPosturePolicy(policy, CLOUDBEAT_GCP, { + 'gcp.organization_id': { value: 'c' }, + }); + + expect(onChange).toHaveBeenCalledWith({ + isValid: true, + updatedPolicy: policy, + }); + }); }); describe('Azure Credentials input fields', () => { diff --git a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx index 67a617decac76..306cc6da445fd 100644 --- a/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx +++ b/x-pack/plugins/cloud_security_posture/public/components/fleet_extensions/policy_template_form.tsx @@ -4,9 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; +import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import semverCompare from 'semver/functions/compare'; import semverValid from 'semver/functions/valid'; +import semverCoerce from 'semver/functions/coerce'; +import semverLt from 'semver/functions/lt'; import { EuiCallOut, EuiFieldText, @@ -54,6 +56,7 @@ import { PolicyTemplateVarsForm, } from './policy_template_selectors'; import { usePackagePolicyList } from '../../common/api/use_package_policy_list'; +import { gcpField, getInputVarsFields } from './gcp_credential_form'; const DEFAULT_INPUT_TYPE = { kspm: CLOUDBEAT_VANILLA, @@ -82,13 +85,14 @@ interface IntegrationInfoFieldsProps { export const AWS_SINGLE_ACCOUNT = 'single-account'; export const AWS_ORGANIZATION_ACCOUNT = 'organization-account'; -export const GCP_SINGLE_ACCOUNT = 'single-account-gcp'; -export const GCP_ORGANIZATION_ACCOUNT = 'organization-account-gcp'; +export const GCP_SINGLE_ACCOUNT = 'single-account'; +export const GCP_ORGANIZATION_ACCOUNT = 'organization-account'; export const AZURE_SINGLE_ACCOUNT = 'single-account-azure'; export const AZURE_ORGANIZATION_ACCOUNT = 'organization-account-azure'; type AwsAccountType = typeof AWS_SINGLE_ACCOUNT | typeof AWS_ORGANIZATION_ACCOUNT; type AzureAccountType = typeof AZURE_SINGLE_ACCOUNT | typeof AZURE_ORGANIZATION_ACCOUNT; +type GcpAccountType = typeof GCP_SINGLE_ACCOUNT | typeof GCP_ORGANIZATION_ACCOUNT; const getAwsAccountTypeOptions = (isAwsOrgDisabled: boolean): CspRadioGroupProps['options'] => [ { @@ -111,19 +115,18 @@ const getAwsAccountTypeOptions = (isAwsOrgDisabled: boolean): CspRadioGroupProps }, ]; -const getGcpAccountTypeOptions = (): CspRadioGroupProps['options'] => [ +const getGcpAccountTypeOptions = (isGcpOrgDisabled: boolean): CspRadioGroupProps['options'] => [ { id: GCP_ORGANIZATION_ACCOUNT, label: i18n.translate('xpack.csp.fleetIntegration.gcpAccountType.gcpOrganizationLabel', { defaultMessage: 'GCP Organization', }), - disabled: true, - tooltip: i18n.translate( - 'xpack.csp.fleetIntegration.gcpAccountType.gcpOrganizationDisabledTooltip', - { - defaultMessage: 'Coming Soon', - } - ), + disabled: isGcpOrgDisabled, + tooltip: isGcpOrgDisabled + ? i18n.translate('xpack.csp.fleetIntegration.gcpAccountType.gcpOrganizationDisabledTooltip', { + defaultMessage: 'Supported from integration version 1.6.0 and above', + }) + : undefined, }, { id: GCP_SINGLE_ACCOUNT, @@ -258,6 +261,12 @@ const AwsAccountTypeSelect = ({ ); }; +const getGcpAccountType = ( + input: Extract +): GcpAccountType | undefined => input.streams[0].vars?.['gcp.account_type']?.value; + +const GCP_ORG_MINIMUM_PACKAGE_VERSION = '1.6.0'; + const GcpAccountTypeSelect = ({ input, newPolicy, @@ -269,6 +278,71 @@ const GcpAccountTypeSelect = ({ updatePolicy: (updatedPolicy: NewPackagePolicy) => void; packageInfo: PackageInfo; }) => { + // This will disable the gcp org option for any version below 1.6.0 which introduced support for account_type. https://github.com/elastic/integrations/pull/6682 + const validSemantic = semverValid(packageInfo.version); + const integrationVersionNumberOnly = semverCoerce(validSemantic) || ''; + const isGcpOrgDisabled = semverLt(integrationVersionNumberOnly, GCP_ORG_MINIMUM_PACKAGE_VERSION); + + const gcpAccountTypeOptions = useMemo( + () => getGcpAccountTypeOptions(isGcpOrgDisabled), + [isGcpOrgDisabled] + ); + /* Create a subset of properties from GcpField to use for hiding value of Organization ID when switching account type from Organization to Single */ + const subsetOfGcpField = (({ ['gcp.organization_id']: a }) => ({ 'gcp.organization_id': a }))( + gcpField.fields + ); + const fieldsToHide = getInputVarsFields(input, subsetOfGcpField); + const fieldsSnapshot = useRef({}); + const lastSetupAccessType = useRef(undefined); + const onSetupFormatChange = (newSetupFormat: string) => { + if (newSetupFormat === 'single-account') { + // We need to store the current manual fields to restore them later + fieldsSnapshot.current = Object.fromEntries( + fieldsToHide.map((field) => [field.id, { value: field.value }]) + ); + // We need to store the last manual credentials type to restore it later + lastSetupAccessType.current = input.streams[0].vars?.['gcp.account_type'].value; + + updatePolicy( + getPosturePolicy(newPolicy, input.type, { + 'gcp.account_type': { + value: 'single-account', + type: 'text', + }, + // Clearing fields from previous setup format to prevent exposing credentials + // when switching from manual to cloud formation + ...Object.fromEntries(fieldsToHide.map((field) => [field.id, { value: undefined }])), + }) + ); + } else { + updatePolicy( + getPosturePolicy(newPolicy, input.type, { + 'gcp.account_type': { + // Restoring last manual credentials type + value: lastSetupAccessType.current || 'organization-account', + type: 'text', + }, + // Restoring fields from manual setup format if any + ...fieldsSnapshot.current, + }) + ); + } + }; + + useEffect(() => { + if (!getGcpAccountType(input)) { + updatePolicy( + getPosturePolicy(newPolicy, input.type, { + 'gcp.account_type': { + value: isGcpOrgDisabled ? GCP_SINGLE_ACCOUNT : GCP_ORGANIZATION_ACCOUNT, + type: 'text', + }, + }) + ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [input]); + return ( <> @@ -278,28 +352,47 @@ const GcpAccountTypeSelect = ({ /> + {isGcpOrgDisabled && ( + <> + + + + + + )} { - updatePolicy( - getPosturePolicy(newPolicy, input.type, { - gcp_account_type: { - value: accountType, - type: 'text', - }, - }) - ); - }} + idSelected={getGcpAccountType(input) || ''} + options={gcpAccountTypeOptions} + onChange={(accountType) => + accountType !== getGcpAccountType(input) && onSetupFormatChange(accountType) + } size="m" /> - - - - + {getGcpAccountType(input) === GCP_ORGANIZATION_ACCOUNT && ( + <> + + + + + + )} + {getGcpAccountType(input) === GCP_SINGLE_ACCOUNT && ( + <> + + + + + + )} ); }; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_google_cloud_shell_modal.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_google_cloud_shell_modal.tsx index a9185d3efa743..ce43298d8a97d 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_google_cloud_shell_modal.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/components/post_install_google_cloud_shell_modal.tsx @@ -30,6 +30,7 @@ import { } from '../../../../../hooks'; import { GoogleCloudShellGuide } from '../../../../../components'; import { ManualInstructions } from '../../../../../../../components/enrollment_instructions'; +import { getGcpIntegrationDetailsFromPackagePolicy } from '../../../../../../../services'; export const PostInstallGoogleCloudShellModal: React.FunctionComponent<{ onConfirm: () => void; @@ -46,6 +47,8 @@ export const PostInstallGoogleCloudShellModal: React.FunctionComponent<{ ); const { fleetServerHosts, fleetProxy } = useFleetServerHostsForPolicy(agentPolicy); const agentVersion = useAgentVersion(); + const { gcpProjectId, gcpOrganizationId, gcpAccountType } = + getGcpIntegrationDetailsFromPackagePolicy(packagePolicy); const { cloudShellUrl, error, isError, isLoading } = useCreateCloudShellUrl({ enrollmentAPIKey: apyKeysData?.data?.items[0]?.api_key, @@ -61,6 +64,9 @@ export const PostInstallGoogleCloudShellModal: React.FunctionComponent<{ fleetServerHosts, fleetProxy, agentVersion, + gcpProjectId, + gcpOrganizationId, + gcpAccountType, }); return ( @@ -75,7 +81,10 @@ export const PostInstallGoogleCloudShellModal: React.FunctionComponent<{ - + {error && isError && ( <> diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/google_cloud_shell_instructions.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/google_cloud_shell_instructions.tsx index 89b6f67fe0dcf..a7090370680e4 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/google_cloud_shell_instructions.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/google_cloud_shell_instructions.tsx @@ -14,15 +14,17 @@ import { GoogleCloudShellGuide } from '../google_cloud_shell_guide'; interface Props { cloudShellUrl: string; cloudShellCommand: string; + projectId?: string; } export const GoogleCloudShellInstructions: React.FunctionComponent = ({ cloudShellUrl, cloudShellCommand, + projectId, }) => { return ( <> - + = ({ const agentVersion = useAgentVersion(); + const { gcpProjectId, gcpOrganizationId, gcpAccountType } = + getGcpIntegrationDetailsFromAgentPolicy(selectedPolicy); + const fleetServerHost = fleetServerHosts?.[0]; const installManagedCommands = ManualInstructions({ @@ -228,6 +235,9 @@ export const ManagedSteps: React.FunctionComponent = ({ fleetServerHosts, fleetProxy, agentVersion: agentVersion || '', + gcpProjectId, + gcpOrganizationId, + gcpAccountType, }); const instructionsSteps = useMemo(() => { @@ -273,6 +283,7 @@ export const ManagedSteps: React.FunctionComponent = ({ selectedApiKeyId, cloudShellUrl: cloudSecurityIntegration.cloudShellUrl, cloudShellCommand: installManagedCommands.googleCloudShell, + projectId: gcpProjectId, }) ); } else if (cloudSecurityIntegration?.isAzureArmTemplate) { @@ -343,6 +354,7 @@ export const ManagedSteps: React.FunctionComponent = ({ enrolledAgentIds, agentDataConfirmed, installedPackagePolicy, + gcpProjectId, ]); if (!agentVersion) { diff --git a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/install_google_cloud_shell_managed_agent_step.tsx b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/install_google_cloud_shell_managed_agent_step.tsx index ff367be9125fd..4e5b15c626735 100644 --- a/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/install_google_cloud_shell_managed_agent_step.tsx +++ b/x-pack/plugins/fleet/public/components/agent_enrollment_flyout/steps/install_google_cloud_shell_managed_agent_step.tsx @@ -21,12 +21,14 @@ export const InstallGoogleCloudShellManagedAgentStep = ({ isComplete, cloudShellUrl, cloudShellCommand, + projectId, }: { selectedApiKeyId?: string; apiKeyData?: GetOneEnrollmentAPIKeyResponse | null; isComplete?: boolean; cloudShellUrl?: string | undefined; cloudShellCommand?: string; + projectId?: string; }): EuiContainedStepProps => { const nonCompleteStatus = selectedApiKeyId ? undefined : 'disabled'; const status = isComplete ? 'complete' : nonCompleteStatus; @@ -41,6 +43,7 @@ export const InstallGoogleCloudShellManagedAgentStep = ({ ) : ( diff --git a/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx b/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx index b7a4fed713cad..21c8ec6172cc7 100644 --- a/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx +++ b/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx @@ -28,11 +28,17 @@ export const ManualInstructions = ({ fleetServerHosts, fleetProxy, agentVersion: agentVersion, + gcpProjectId = '', + gcpOrganizationId = '', + gcpAccountType, }: { apiKey: string; fleetServerHosts: string[]; fleetProxy?: FleetProxy; agentVersion: string; + gcpProjectId?: string; + gcpOrganizationId?: string; + gcpAccountType?: string; }) => { const enrollArgs = getfleetServerHostsEnrollArgs(apiKey, fleetServerHosts, fleetProxy); const fleetServerUrl = enrollArgs?.split('--url=')?.pop()?.split('--enrollment')[0]; @@ -64,7 +70,9 @@ sudo elastic-agent enroll ${enrollArgs} \nsudo systemctl enable elastic-agent \n sudo rpm -vi elastic-agent-${agentVersion}-x86_64.rpm sudo elastic-agent enroll ${enrollArgs} \nsudo systemctl enable elastic-agent \nsudo systemctl start elastic-agent`; - const googleCloudShellCommand = `gcloud config set project && \nFLEET_URL=${fleetServerUrl} ENROLLMENT_TOKEN=${enrollmentToken} STACK_VERSION=${agentVersion} ./deploy.sh`; + const googleCloudShellCommand = `gcloud config set project ${gcpProjectId} && ${ + gcpAccountType === 'organization-account' ? `\nORG_ID=${gcpOrganizationId}` : `` + } \nFLEET_URL=${fleetServerUrl} ENROLLMENT_TOKEN=${enrollmentToken} \nSTACK_VERSION=${agentVersion} ./deploy.sh`; return { linux: linuxCommand, diff --git a/x-pack/plugins/fleet/public/components/google_cloud_shell_guide.tsx b/x-pack/plugins/fleet/public/components/google_cloud_shell_guide.tsx index d494fc1075f41..1d5c804ef157a 100644 --- a/x-pack/plugins/fleet/public/components/google_cloud_shell_guide.tsx +++ b/x-pack/plugins/fleet/public/components/google_cloud_shell_guide.tsx @@ -23,7 +23,7 @@ const Link = ({ children, url }: { children: React.ReactNode; url: string }) => ); -export const GoogleCloudShellGuide = (props: { commandText: string }) => { +export const GoogleCloudShellGuide = (props: { commandText: string; hasProjectId?: boolean }) => { return ( <> @@ -48,10 +48,17 @@ export const GoogleCloudShellGuide = (props: { commandText: string }) => {
    1. <> - + {props?.hasProjectId ? ( + + ) : ( + + )} {props.commandText} diff --git a/x-pack/plugins/fleet/public/services/get_gcp_integration_details_from_agent_policy.test.ts b/x-pack/plugins/fleet/public/services/get_gcp_integration_details_from_agent_policy.test.ts new file mode 100644 index 0000000000000..e53e2dc36df07 --- /dev/null +++ b/x-pack/plugins/fleet/public/services/get_gcp_integration_details_from_agent_policy.test.ts @@ -0,0 +1,101 @@ +/* + * 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 { getGcpIntegrationDetailsFromAgentPolicy } from './get_gcp_integration_details_from_agent_policy'; + +const undefinedAllValue = { + gcpAccountType: undefined, + gcpOrganizationId: undefined, + gcpProjectId: undefined, +}; + +describe('getGcpIntegrationDetailsFromAgentPolicy', () => { + test('returns undefined when agentPolicy is undefined', () => { + const result = getGcpIntegrationDetailsFromAgentPolicy(undefined); + expect(result).toEqual(undefinedAllValue); + }); + + test('returns undefined when agentPolicy is defined but inputs are empty', () => { + const selectedPolicy = { inputs: [] }; + // @ts-expect-error + const result = getGcpIntegrationDetailsFromAgentPolicy(selectedPolicy); + expect(result).toEqual(undefinedAllValue); + }); + + it('should return undefined when no input has enabled and gcp integration details', () => { + const selectedPolicy = { + package_policies: [ + { + inputs: [ + { enabled: false, streams: [{}] }, + { enabled: true, streams: [{ vars: { other_property: 'false' } }] }, + { enabled: true, streams: [{ other_property: 'False' }] }, + ], + }, + { + inputs: [ + { enabled: false, streams: [{}] }, + { enabled: false, streams: [{}] }, + ], + }, + ], + }; + // @ts-expect-error + const result = getGcpIntegrationDetailsFromAgentPolicy(selectedPolicy); + expect(result).toEqual(undefinedAllValue); + }); + + it('should return the first gcp integration details when available', () => { + const selectedPolicy = { + package_policies: [ + { + inputs: [ + { enabled: false, streams: [{}] }, + { enabled: true, streams: [{ vars: { other_property: 'false' } }] }, + { enabled: true, streams: [{ other_property: 'False' }] }, + ], + }, + { + inputs: [ + { enabled: false, streams: [{}] }, + { + enabled: true, + streams: [ + { + vars: { + 'gcp.account_type': { value: 'account_type_test_1' }, + 'gcp.project_id': { value: 'project_id_1' }, + 'gcp.organization_id': { value: 'organization_id_1' }, + }, + }, + ], + }, + { + enabled: true, + streams: [ + { + vars: { + 'gcp.account_type': { value: 'account_type_test_2' }, + 'gcp.project_id': { value: 'project_id_2' }, + 'gcp.organization_id': { value: 'organization_id_2' }, + }, + }, + ], + }, + ], + }, + ], + }; + // @ts-expect-error + const result = getGcpIntegrationDetailsFromAgentPolicy(selectedPolicy); + expect(result).toEqual({ + gcpAccountType: 'account_type_test_1', + gcpOrganizationId: 'organization_id_1', + gcpProjectId: 'project_id_1', + }); + }); + // Add more test cases as needed +}); diff --git a/x-pack/plugins/fleet/public/services/get_gcp_integration_details_from_agent_policy.tsx b/x-pack/plugins/fleet/public/services/get_gcp_integration_details_from_agent_policy.tsx new file mode 100644 index 0000000000000..a1112683b4f1a --- /dev/null +++ b/x-pack/plugins/fleet/public/services/get_gcp_integration_details_from_agent_policy.tsx @@ -0,0 +1,71 @@ +/* + * 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 { AgentPolicy } from '../types'; + +/** + * Get the project id, organization id and account type of gcp integration from an agent policy + */ +export const getGcpIntegrationDetailsFromAgentPolicy = (selectedPolicy?: AgentPolicy) => { + let gcpProjectId = selectedPolicy?.package_policies?.reduce((acc, packagePolicy) => { + const findGcpProjectId = packagePolicy.inputs?.reduce((accInput, input) => { + if (accInput !== '') { + return accInput; + } + if (input?.enabled && input?.streams[0]?.vars?.['gcp.project_id']?.value) { + return input?.streams[0]?.vars?.['gcp.project_id']?.value; + } + return accInput; + }, ''); + if (findGcpProjectId) { + return findGcpProjectId; + } + return acc; + }, ''); + + let gcpOrganizationId = selectedPolicy?.package_policies?.reduce((acc, packagePolicy) => { + const findGcpProjectId = packagePolicy.inputs?.reduce((accInput, input) => { + if (accInput !== '') { + return accInput; + } + if (input?.enabled && input?.streams[0]?.vars?.['gcp.organization_id']?.value) { + return input?.streams[0]?.vars?.['gcp.organization_id']?.value; + } + return accInput; + }, ''); + if (findGcpProjectId) { + return findGcpProjectId; + } + return acc; + }, ''); + + let gcpAccountType = selectedPolicy?.package_policies?.reduce((acc, packagePolicy) => { + const findGcpProjectId = packagePolicy.inputs?.reduce((accInput, input) => { + if (accInput !== '') { + return accInput; + } + if (input?.enabled && input?.streams[0]?.vars?.['gcp.account_type']?.value) { + return input?.streams[0]?.vars?.['gcp.account_type']?.value; + } + return accInput; + }, ''); + if (findGcpProjectId) { + return findGcpProjectId; + } + return acc; + }, ''); + + gcpProjectId = gcpProjectId !== '' ? gcpProjectId : undefined; + gcpOrganizationId = gcpOrganizationId !== '' ? gcpOrganizationId : undefined; + gcpAccountType = gcpAccountType !== '' ? gcpAccountType : undefined; + + return { + gcpProjectId, + gcpOrganizationId, + gcpAccountType, + }; +}; diff --git a/x-pack/plugins/fleet/public/services/get_gcp_integration_details_from_package_policy.test.ts b/x-pack/plugins/fleet/public/services/get_gcp_integration_details_from_package_policy.test.ts new file mode 100644 index 0000000000000..44da2fb65383a --- /dev/null +++ b/x-pack/plugins/fleet/public/services/get_gcp_integration_details_from_package_policy.test.ts @@ -0,0 +1,80 @@ +/* + * 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 { getGcpIntegrationDetailsFromPackagePolicy } from './get_gcp_integration_details_from_package_policy'; + +const undefinedAllValue = { + gcpAccountType: undefined, + gcpOrganizationId: undefined, + gcpProjectId: undefined, +}; + +describe('getGcpIntegrationDetailsFromPackagePolicy', () => { + test('returns undefined when packagePolicy is undefined', () => { + const result = getGcpIntegrationDetailsFromPackagePolicy(undefined); + expect(result).toEqual(undefinedAllValue); + }); + + test('returns undefined when packagePolicy is defined but inputs are empty', () => { + const packagePolicy = { inputs: [] }; + // @ts-expect-error + const result = getGcpIntegrationDetailsFromPackagePolicy(packagePolicy); + expect(result).toEqual(undefinedAllValue); + }); + + it('should return undefined when no input has enabled and gcp integration details', () => { + const packagePolicy = { + inputs: [ + { enabled: false, streams: [{}] }, + { enabled: true, streams: [{ vars: { other_property: 'false' } }] }, + { enabled: true, streams: [{ other_property: 'False' }] }, + ], + }; + // @ts-expect-error + const result = getGcpIntegrationDetailsFromPackagePolicy(packagePolicy); + expect(result).toEqual(undefinedAllValue); + }); + + it('should return the first gcp integration details when available', () => { + const packagePolicy = { + inputs: [ + { enabled: false, streams: [{}] }, + { + enabled: true, + streams: [ + { + vars: { + 'gcp.account_type': { value: 'account_type_test_1' }, + 'gcp.project_id': { value: 'project_id_1' }, + 'gcp.organization_id': { value: 'organization_id_1' }, + }, + }, + ], + }, + { + enabled: true, + streams: [ + { + vars: { + 'gcp.account_type': { value: 'account_type_test_2' }, + 'gcp.project_id': { value: 'project_id_2' }, + 'gcp.organization_id': { value: 'organization_id_2' }, + }, + }, + ], + }, + ], + }; + // @ts-expect-error + const result = getGcpIntegrationDetailsFromPackagePolicy(packagePolicy); + expect(result).toEqual({ + gcpAccountType: 'account_type_test_1', + gcpOrganizationId: 'organization_id_1', + gcpProjectId: 'project_id_1', + }); + }); + // Add more test cases as needed +}); diff --git a/x-pack/plugins/fleet/public/services/get_gcp_integration_details_from_package_policy.tsx b/x-pack/plugins/fleet/public/services/get_gcp_integration_details_from_package_policy.tsx new file mode 100644 index 0000000000000..ae82352d51e0a --- /dev/null +++ b/x-pack/plugins/fleet/public/services/get_gcp_integration_details_from_package_policy.tsx @@ -0,0 +1,53 @@ +/* + * 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 { PackagePolicy } from '../types'; + +/** + * Get the project id, organization id and account type of gcp integration from a package policy + */ +export const getGcpIntegrationDetailsFromPackagePolicy = (packagePolicy?: PackagePolicy) => { + let gcpProjectId = packagePolicy?.inputs?.reduce((accInput, input) => { + if (accInput !== '') { + return accInput; + } + if (input?.enabled && input?.streams[0]?.vars?.['gcp.project_id']?.value) { + return input?.streams[0]?.vars?.['gcp.project_id']?.value; + } + return accInput; + }, ''); + + let gcpOrganizationId = packagePolicy?.inputs?.reduce((accInput, input) => { + if (accInput !== '') { + return accInput; + } + if (input?.enabled && input?.streams[0]?.vars?.['gcp.organization_id']?.value) { + return input?.streams[0]?.vars?.['gcp.organization_id']?.value; + } + return accInput; + }, ''); + + let gcpAccountType = packagePolicy?.inputs?.reduce((accInput, input) => { + if (accInput !== '') { + return accInput; + } + if (input?.enabled && input?.streams[0]?.vars?.['gcp.account_type']?.value) { + return input?.streams[0]?.vars?.['gcp.account_type']?.value; + } + return accInput; + }, ''); + + gcpProjectId = gcpProjectId !== '' ? gcpProjectId : undefined; + gcpOrganizationId = gcpOrganizationId !== '' ? gcpOrganizationId : undefined; + gcpAccountType = gcpAccountType !== '' ? gcpAccountType : undefined; + + return { + gcpProjectId, + gcpOrganizationId, + gcpAccountType, + }; +}; diff --git a/x-pack/plugins/fleet/public/services/index.ts b/x-pack/plugins/fleet/public/services/index.ts index 64009e4a11061..71f5fde90d93a 100644 --- a/x-pack/plugins/fleet/public/services/index.ts +++ b/x-pack/plugins/fleet/public/services/index.ts @@ -53,3 +53,5 @@ export { getTemplateUrlFromAgentPolicy } from './get_template_url_from_agent_pol export { getTemplateUrlFromPackageInfo } from './get_template_url_from_package_info'; export { getCloudShellUrlFromPackagePolicy } from './get_cloud_shell_url_from_package_policy'; export { getCloudShellUrlFromAgentPolicy } from './get_cloud_shell_url_from_agent_policy'; +export { getGcpIntegrationDetailsFromPackagePolicy } from './get_gcp_integration_details_from_package_policy'; +export { getGcpIntegrationDetailsFromAgentPolicy } from './get_gcp_integration_details_from_agent_policy';