Skip to content

Commit

Permalink
[Fleet] Improve UX for policy secrets (elastic#171405)
Browse files Browse the repository at this point in the history
## Summary

Closes elastic#171225

- Highlights secrets during package policy creation with a distinct
background and icon
- Add tooltip + docs link for secrets where appropriate
- Detect "new secrets" during policy upgrade and alert the user in a
separate callout

## To do
- [x] Fix any failing tests
- [x] Add tests for "new secrets" detection logic

## Screenshots


![image](https://github.com/elastic/kibana/assets/6766512/e943a3e8-68db-40eb-a5c3-b108e7d299ff)


![image](https://github.com/elastic/kibana/assets/6766512/751bbe50-7553-4dcc-a8dc-b9802f331013)


![image](https://github.com/elastic/kibana/assets/6766512/6cceb4cd-0b8e-42cd-aafb-d2e3ddcd23a8)

## How to test

There's probably an easier way to do this, but this is what I did

1. Clone https://github.com/elastic/package-registry and
https://github.com/elastic/integrations
2. Add the following to `config.yml` in your package-registry repo

```yml
package_paths:
  - path/to/your/integrations/build/packages
```

3. Build a version of an integration with some `secrets: true` for
various variables. I used `1password`

```shell
cd integrations/packages/1password
# Edit `manifest.yml` or a given `data_stream/*/manifest.yml` file to change some variables to `secret: true`. Also bump the version and update `changelog.yml`
elastic-package build
```

4. Run the local package registry e.g. 

```shell
cd package-registry
go run . --feature-proxy-mode=true -proxy-to=https://epr.elastic.co # makes it so you can still see EPR packages in Kibana
```

5. Update your `kibana.dev.yml` to point at your local package registry

```yml
xpack.fleet.registryUrl: http://localhost:8080
```

6. Start Kibana and Elasticsearch and install, upgrade, etc your package
in question to verify the changes

---------

Co-authored-by: David Kilfoyle <41695641+kilfoyle@users.noreply.github.com>
  • Loading branch information
kpollich and kilfoyle committed Nov 16, 2023
1 parent 2263213 commit 9396ef3
Show file tree
Hide file tree
Showing 12 changed files with 423 additions and 57 deletions.
1 change: 1 addition & 0 deletions packages/kbn-doc-links/src/get_doc_links.ts
Expand Up @@ -769,6 +769,7 @@ export const getDocLinks = ({ kibanaBranch }: GetDocLinkOptions): DocLinks => {
uninstallAgent: `${SECURITY_SOLUTION_DOCS}uninstall-agent.html`,
installAndUninstallIntegrationAssets: `${FLEET_DOCS}install-uninstall-integration-assets.html`,
elasticAgentInputConfiguration: `${FLEET_DOCS}elastic-agent-input-configuration.html`,
policySecrets: `${FLEET_DOCS}agent-policy.html#agent-policy-secret-values`,
},
ecs: {
guide: `${ELASTIC_WEBSITE_URL}guide/en/ecs/current/index.html`,
Expand Down
1 change: 1 addition & 0 deletions packages/kbn-doc-links/src/types.ts
Expand Up @@ -527,6 +527,7 @@ export interface DocLinks {
uninstallAgent: string;
installAndUninstallIntegrationAssets: string;
elasticAgentInputConfiguration: string;
policySecrets: string;
}>;
readonly ecs: {
readonly guide: string;
Expand Down
Expand Up @@ -23,11 +23,16 @@ import {
EuiFlexGroup,
EuiFlexItem,
EuiButtonEmpty,
EuiLink,
EuiToolTip,
EuiIcon,
} from '@elastic/eui';
import styled from 'styled-components';

import { CodeEditor } from '@kbn/kibana-react-plugin/public';

import { useStartServices } from '../../../../../../../../hooks';

import { ExperimentalFeaturesService } from '../../../../../../services';

import { DATASET_VAR_NAME } from '../../../../../../../../../common/constants';
Expand All @@ -41,6 +46,16 @@ const FixedHeightDiv = styled.div`
height: 300px;
`;

const FormRow = styled(EuiFormRow)`
.euiFormRow__label {
flex: 1;
}
.euiFormRow__fieldWrapper > .euiPanel {
padding: ${(props) => props.theme.eui.euiSizeXS};
}
`;

interface InputFieldProps {
varDef: RegistryVarsEntry;
value: any;
Expand Down Expand Up @@ -125,11 +140,11 @@ export const PackagePolicyInputVarField: React.FunctionComponent<InputFieldProps
});
}

return (
<EuiFormRow
const formRow = (
<FormRow
isInvalid={isInvalid}
error={errors}
label={fieldLabel}
label={varDef.secret ? <SecretFieldLabel fieldLabel={fieldLabel} /> : fieldLabel}
labelAppend={
isOptional ? (
<EuiText size="xs" color="subdued">
Expand All @@ -138,13 +153,16 @@ export const PackagePolicyInputVarField: React.FunctionComponent<InputFieldProps
defaultMessage="Optional"
/>
</EuiText>
) : null
) : undefined
}
helpText={description && <ReactMarkdown children={description} />}
fullWidth
>
{field}
</EuiFormRow>
</FormRow>
);

return varDef.secret ? <SecretFieldWrapper>{formRow}</SecretFieldWrapper> : formRow;
}
);

Expand Down Expand Up @@ -296,6 +314,53 @@ function getInputComponent({
}
}

const SecretFieldWrapper = ({ children }: { children: React.ReactNode }) => {
const { docLinks } = useStartServices();

return (
<EuiPanel hasShadow={false} color="subdued" paddingSize="m">
{children}

<EuiSpacer size="l" />

<EuiText size="xs">
<EuiLink href={docLinks.links.fleet.policySecrets} target="_blank">
<FormattedMessage
id="xpack.fleet.createPackagePolicy.stepConfigure.secretLearnMoreText"
defaultMessage="Learn more about policy secrets."
/>
</EuiLink>
</EuiText>
</EuiPanel>
);
};

const SecretFieldLabel = ({ fieldLabel }: { fieldLabel: string }) => {
return (
<>
<EuiFlexGroup alignItems="center" gutterSize="xs">
<EuiFlexItem grow={true} aria-label={fieldLabel}>
{fieldLabel}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiToolTip
content={
<FormattedMessage
id="xpack.fleet.createPackagePolicy.stepConfigure.secretLearnMorePopoverContent"
defaultMessage="This value is a secret. After you save this integration policy, you won't be able to view the value again."
/>
}
>
<EuiIcon aria-label="Secret value" type="questionInCircle" color="subdued" />
</EuiToolTip>
</EuiFlexItem>
</EuiFlexGroup>

<EuiSpacer size="s" />
</>
);
};

function SecretInputField({
varDef,
value,
Expand All @@ -313,10 +378,12 @@ function SecretInputField({
}: InputComponentProps) {
const [editMode, setEditMode] = useState(isEditPage && !value);
const valueOnFirstRender = useRef(value);

const lowercaseTitle = varDef.title?.toLowerCase();

if (isEditPage && !editMode) {
return (
<EuiPanel color="subdued" borderRadius="none" hasShadow={false}>
<>
<EuiText size="s" color="subdued">
<FormattedMessage
id="xpack.fleet.editPackagePolicy.stepConfigure.fieldSecretValueSet"
Expand All @@ -342,7 +409,7 @@ function SecretInputField({
}}
/>
</EuiButtonEmpty>
</EuiPanel>
</>
);
}

Expand Down
Expand Up @@ -24,29 +24,144 @@ import {
EuiFlyoutBody,
EuiFlyoutHeader,
EuiTitle,
EuiSpacer,
} from '@elastic/eui';
import styled from 'styled-components';

import type {
DryRunPackagePolicy,
PackagePolicy,
RegistryVarsEntry,
} from '../../../../../../../common';

import type { UpgradePackagePolicyDryRunResponse } from '../../../../../../../common/types/rest_spec';
import { useStartServices } from '../../../../hooks';

const FlyoutBody = styled(EuiFlyoutBody)`
.euiFlyoutBody__overflowContent {
padding: 0;
}
`;

const HasNewSecretsCallOut = ({ newSecrets }: { newSecrets: RegistryVarsEntry[] }) => {
const { docLinks } = useStartServices();

return (
<EuiCallOut
title={i18n.translate('xpack.fleet.upgradePackagePolicy.statusCallOut.hasNewSecretsTitle', {
defaultMessage: 'New secrets added',
})}
color="primary"
iconType="iInCircle"
>
<FormattedMessage
id="xpack.fleet.upgradePackagePolicy.statusCallout.hasNewSecrets"
defaultMessage="Some of this integration's form fields have been converted to secrets in this version. Your existing values are autofilled in each secret input during this upgrade, but you won't be able to view them again after saving. {learnMoreLink}"
values={{
learnMoreLink: (
<EuiLink href={docLinks.links.fleet.policySecrets} target="_blank">
Learn more.
</EuiLink>
),
}}
/>

<EuiSpacer size="s" />

<FormattedMessage
id="xpack.fleet.upgradePackagePolicy.statusCallout.hasNewSecretsList"
defaultMessage="New secrets: {secrets}"
values={{
secrets: (
<ul>
{newSecrets.map((secret) => (
<li key={secret.title}>{secret.title}</li>
))}
</ul>
),
}}
/>
</EuiCallOut>
);
};

const HasConflictsCallout = ({
currentPackagePolicy,
proposedUpgradePackagePolicy,
onPreviousConfigurationClick,
}: {
currentPackagePolicy?: PackagePolicy;
proposedUpgradePackagePolicy?: DryRunPackagePolicy;
onPreviousConfigurationClick?: () => void;
}) => {
return (
<EuiCallOut
title={i18n.translate('xpack.fleet.upgradePackagePolicy.statusCallOut.errorTitle', {
defaultMessage: 'Review field conflicts',
})}
color="warning"
iconType="warning"
>
<FormattedMessage
id="xpack.fleet.upgradePackagePolicy.statusCallout.errorContent"
defaultMessage="This integration has conflicting fields from version {currentVersion} to {upgradeVersion} Review the configuration and save to perform the upgrade. You may reference your {previousConfigurationLink} for comparison."
values={{
currentVersion: currentPackagePolicy?.package?.version,
upgradeVersion: proposedUpgradePackagePolicy?.package?.version,
previousConfigurationLink: (
<EuiLink onClick={onPreviousConfigurationClick}>
<FormattedMessage
id="xpack.fleet.upgradePackagePolicy.statusCallout.previousConfigurationLink"
defaultMessage="previous configuration"
/>
</EuiLink>
),
}}
/>
</EuiCallOut>
);
};

const ReadyToUpgradeCallOut = ({
currentPackagePolicy,
proposedUpgradePackagePolicy,
}: {
currentPackagePolicy?: PackagePolicy;
proposedUpgradePackagePolicy?: DryRunPackagePolicy;
}) => {
return (
<EuiCallOut
title={i18n.translate('xpack.fleet.upgradePackagePolicy.statusCallOut.successTitle', {
defaultMessage: 'Ready to upgrade',
})}
color="success"
iconType="checkInCircleFilled"
>
<FormattedMessage
id="xpack.fleet.upgradePackagePolicy.statusCallout.successContent"
defaultMessage="This integration is ready to be upgraded from version {currentVersion} to {upgradeVersion}. Review the changes below and save to upgrade."
values={{
currentVersion: currentPackagePolicy?.package?.version,
upgradeVersion: proposedUpgradePackagePolicy?.package?.version,
}}
/>
</EuiCallOut>
);
};

export const UpgradeStatusCallout: React.FunctionComponent<{
dryRunData: UpgradePackagePolicyDryRunResponse;
}> = ({ dryRunData }) => {
newSecrets: RegistryVarsEntry[];
}> = ({ dryRunData, newSecrets }) => {
const [isPreviousVersionFlyoutOpen, setIsPreviousVersionFlyoutOpen] = useState<boolean>(false);

if (!dryRunData) {
return null;
}

const isReadyForUpgrade = !dryRunData[0].hasErrors;

const hasNewSecrets = newSecrets.length > 0;
const [currentPackagePolicy, proposedUpgradePackagePolicy] = dryRunData[0].diff || [];
const isReadyForUpgrade = currentPackagePolicy && !dryRunData[0].hasErrors;

return (
<>
Expand All @@ -73,48 +188,23 @@ export const UpgradeStatusCallout: React.FunctionComponent<{
</EuiPortal>
)}

{isReadyForUpgrade && currentPackagePolicy ? (
<EuiCallOut
title={i18n.translate('xpack.fleet.upgradePackagePolicy.statusCallOut.successTitle', {
defaultMessage: 'Ready to upgrade',
})}
color="success"
iconType="checkInCircleFilled"
>
<FormattedMessage
id="xpack.fleet.upgradePackagePolicy.statusCallout.successContent"
defaultMessage="This integration is ready to be upgraded from version {currentVersion} to {upgradeVersion}. Review the changes below and save to upgrade."
values={{
currentVersion: currentPackagePolicy?.package?.version,
upgradeVersion: proposedUpgradePackagePolicy?.package?.version,
}}
/>
</EuiCallOut>
{isReadyForUpgrade ? (
<ReadyToUpgradeCallOut
currentPackagePolicy={currentPackagePolicy}
proposedUpgradePackagePolicy={proposedUpgradePackagePolicy}
/>
) : (
<EuiCallOut
title={i18n.translate('xpack.fleet.upgradePackagePolicy.statusCallOut.errorTitle', {
defaultMessage: 'Review field conflicts',
})}
color="warning"
iconType="warning"
>
<FormattedMessage
id="xpack.fleet.upgradePackagePolicy.statusCallout.errorContent"
defaultMessage="This integration has conflicting fields from version {currentVersion} to {upgradeVersion} Review the configuration and save to perform the upgrade. You may reference your {previousConfigurationLink} for comparison."
values={{
currentVersion: currentPackagePolicy?.package?.version,
upgradeVersion: proposedUpgradePackagePolicy?.package?.version,
previousConfigurationLink: (
<EuiLink onClick={() => setIsPreviousVersionFlyoutOpen(true)}>
<FormattedMessage
id="xpack.fleet.upgradePackagePolicy.statusCallout.previousConfigurationLink"
defaultMessage="previous configuration"
/>
</EuiLink>
),
}}
/>
</EuiCallOut>
<HasConflictsCallout
currentPackagePolicy={currentPackagePolicy}
proposedUpgradePackagePolicy={proposedUpgradePackagePolicy}
onPreviousConfigurationClick={() => setIsPreviousVersionFlyoutOpen(true)}
/>
)}
{hasNewSecrets && (
<>
<EuiSpacer size="m" />
<HasNewSecretsCallOut newSecrets={newSecrets} />
</>
)}
</>
);
Expand Down
Expand Up @@ -99,7 +99,9 @@ export function usePackagePolicyWithRelatedData(
policy: { elasticsearch, ...restPackagePolicy },
} = await prepareInputPackagePolicyDataset(packagePolicy);
const result = await sendUpdatePackagePolicy(packagePolicyId, restPackagePolicy);

setFormState('SUBMITTED');

return result;
};
// Update package policy validation
Expand Down

0 comments on commit 9396ef3

Please sign in to comment.