diff --git a/changes/39723-mac-recovery-lock-passwords b/changes/39723-mac-recovery-lock-passwords new file mode 100644 index 0000000000..b9de1ea837 --- /dev/null +++ b/changes/39723-mac-recovery-lock-passwords @@ -0,0 +1 @@ +- Added ability to set and manually rotate Mac recovery lock passwords. diff --git a/frontend/interfaces/activity.ts b/frontend/interfaces/activity.ts index 530dfba4ca..7938fe24a8 100644 --- a/frontend/interfaces/activity.ts +++ b/frontend/interfaces/activity.ts @@ -48,6 +48,7 @@ export enum ActivityType { ReadHostDiskEncryptionKey = "read_host_disk_encryption_key", ViewedHostRecoveryLockPassword = "viewed_host_recovery_lock_password", SetHostRecoveryLockPassword = "set_host_recovery_lock_password", + RotatedHostRecoveryLockPassword = "rotated_host_recovery_lock_password", EnabledRecoveryLockPasswords = "enabled_recovery_lock_passwords", DisabledRecoveryLockPasswords = "disabled_recovery_lock_passwords", /** Note: BE not renamed (yet) from macOS even though activity is also used for iOS and iPadOS */ @@ -171,6 +172,7 @@ export type IHostPastActivityType = | ActivityType.ReadHostDiskEncryptionKey | ActivityType.ViewedHostRecoveryLockPassword | ActivityType.SetHostRecoveryLockPassword + | ActivityType.RotatedHostRecoveryLockPassword | ActivityType.UnlockedHost | ActivityType.InstalledSoftware | ActivityType.UninstalledSoftware @@ -398,6 +400,7 @@ export const ACTIVITY_TYPE_TO_FILTER_LABEL: Record = { read_host_disk_encryption_key: "Viewed disk encryption key", viewed_host_recovery_lock_password: "Viewed Recovery Lock password", set_host_recovery_lock_password: "Set Recovery Lock password", + rotated_host_recovery_lock_password: "Rotated Recovery Lock password", enabled_recovery_lock_passwords: "Turned on Recovery Lock passwords", disabled_recovery_lock_passwords: "Turned off Recovery Lock passwords", resent_configuration_profile: "Resent configuration profile", diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tests.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tests.tsx index 7aac90e2b1..5e57ebdf55 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tests.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tests.tsx @@ -742,6 +742,21 @@ describe("Activity Feed", () => { expect(screen.getByText("Anna's MacBook Pro")).toBeInTheDocument(); }); + it("renders a 'rotated_host_recovery_lock_password' type activity", () => { + const activity = createMockActivity({ + type: ActivityType.RotatedHostRecoveryLockPassword, + details: { host_display_name: "Alex's Macbook Air" }, + }); + render(); + + expect( + screen.getByText("rotated the Recovery Lock password for", { + exact: false, + }) + ).toBeInTheDocument(); + expect(screen.getByText("Alex's Macbook Air")).toBeInTheDocument(); + }); + it("renders an 'enabled_recovery_lock_passwords' type activity for a team", () => { const activity = createMockActivity({ type: ActivityType.EnabledRecoveryLockPasswords, diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx index d501003cb4..ad943f8ec2 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx @@ -503,6 +503,15 @@ const TAGGED_TEMPLATES = { ); }, + rotatedHostRecoveryLockPassword: (activity: IActivity) => { + return ( + <> + {" "} + rotated the Recovery Lock password for{" "} + {activity.details?.host_display_name}. + + ); + }, createdAppleOSProfile: (activity: IActivity, isPremiumTier: boolean) => { const profileName = activity.details?.profile_name; return ( @@ -1858,6 +1867,9 @@ const getDetail = (activity: IActivity, isPremiumTier: boolean) => { case ActivityType.SetHostRecoveryLockPassword: { return TAGGED_TEMPLATES.setHostRecoveryLockPassword(activity); } + case ActivityType.RotatedHostRecoveryLockPassword: { + return TAGGED_TEMPLATES.rotatedHostRecoveryLockPassword(activity); + } case ActivityType.CreatedAppleOSProfile: { return TAGGED_TEMPLATES.createdAppleOSProfile(activity, isPremiumTier); } diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/Passwords/Passwords.tsx b/frontend/pages/ManageControlsPage/OSSettings/cards/Passwords/Passwords.tsx index 2f2f429be2..32365fcbd1 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/Passwords/Passwords.tsx +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/Passwords/Passwords.tsx @@ -54,6 +54,7 @@ const Passwords = ({ currentTeamId, onMutation }: IOSSettingsCommonProps) => { const [enableRecoveryLockPassword, setEnableRecoveryLockPassword] = useState< boolean | undefined >(undefined); + const [updating, setUpdating] = useState(false); const { isLoading: isLoadingTeam, @@ -93,6 +94,7 @@ const Passwords = ({ currentTeamId, onMutation }: IOSSettingsCommonProps) => { !isFormReady || isTeamError || enableRecoveryLockPassword === undefined; const onUpdateRecoveryLockPassword = async () => { + setUpdating(true); try { if (currentTeamId === API_NO_TEAM_ID) { await configAPI.update({ @@ -116,6 +118,8 @@ const Passwords = ({ currentTeamId, onMutation }: IOSSettingsCommonProps) => { getErrorReason(e) ?? "Couldn't update Recovery Lock password enforcement. Please try again."; renderFlash("error", errorMsg); + } finally { + setUpdating(false); } }; @@ -150,6 +154,7 @@ const Passwords = ({ currentTeamId, onMutation }: IOSSettingsCommonProps) => { renderChildren={(gitopsDisabled) => ( + ); + } + + return ( + + + + + Rotate password + + + + ); + }; + return (
+ {renderRotateButton()}
) diff --git a/frontend/pages/hosts/details/HostDetailsPage/modals/RecoveryLockPasswordModal/_styles.scss b/frontend/pages/hosts/details/HostDetailsPage/modals/RecoveryLockPasswordModal/_styles.scss index f8d65a18c1..41162222e3 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/modals/RecoveryLockPasswordModal/_styles.scss +++ b/frontend/pages/hosts/details/HostDetailsPage/modals/RecoveryLockPasswordModal/_styles.scss @@ -2,4 +2,34 @@ .input-field { font-family: SourceCodePro, $monospace; } + + .modal-cta-wrap { + display: flex; + justify-content: flex-end; + } + + &__rotate-button { + display: flex; + align-items: center; + gap: $pad-xsmall; + + .icon { + margin-right: 0; + } + } + + &__rotate-button--disabled { + color: $ui-fleet-black-50; + cursor: default; + + .icon { + color: $ui-fleet-black-50; + } + } + + &__rotate-button-content { + display: flex; + align-items: center; + gap: $pad-xsmall; + } } diff --git a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsModal.tsx b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsModal.tsx index 025bd2a341..df463eed2e 100644 --- a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsModal.tsx +++ b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsModal.tsx @@ -11,10 +11,13 @@ interface IOSSettingsModalProps { hostMDMData: IHostMdmData; /** controls showing the action for a user to resend a profile. Defaults to `false` */ canResendProfiles?: boolean; + /** controls showing the rotate action for the recovery lock password row. Defaults to `false` */ + canRotateRecoveryLockPassword?: boolean; /** This request method will be called when a user clicks on the resend button. * This behaviour is dynamic based on the page this modal is rendered on * so we allow the request function to be passed in */ resendRequest: (profileUUID: string) => Promise; + rotateRecoveryLockPassword?: () => Promise; onClose: () => void; /** handler that fires when a profile was reset. Requires `canResendProfiles` prop * to be `true`, otherwise has no effect. @@ -28,8 +31,10 @@ const OSSettingsModal = ({ platform, hostMDMData, canResendProfiles = false, + canRotateRecoveryLockPassword = false, onClose, resendRequest, + rotateRecoveryLockPassword, onProfileResent, }: IOSSettingsModalProps) => { // the caller should ensure that hostMDMData is not undefined and that platform is supported otherwise we will allow an empty modal will be rendered. @@ -49,8 +54,10 @@ const OSSettingsModal = ({ >
diff --git a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsErrorCell/OSSettingsErrorCell.tests.tsx b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsErrorCell/OSSettingsErrorCell.tests.tsx index e81589d2c7..13a32850b0 100644 --- a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsErrorCell/OSSettingsErrorCell.tests.tsx +++ b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsErrorCell/OSSettingsErrorCell.tests.tsx @@ -3,20 +3,26 @@ import { render, screen } from "@testing-library/react"; import { createMockHostMdmProfile } from "__mocks__/hostMock"; +import { REC_LOCK_SYNTHETIC_PROFILE_UUID } from "pages/hosts/details/helpers"; + import OSSettingsErrorCell from "./OSSettingsErrorCell"; +const noop = () => new Promise(() => undefined); + describe("OSSettingsErrorCell", () => { it("should render a formatted message for windows profiles", () => { render( new Promise(() => undefined)} + resendRequest={noop} + rotateRecoveryLockPassword={noop} /> ); @@ -51,8 +57,10 @@ describe("OSSettingsErrorCell", () => { render( new Promise(() => undefined)} + resendRequest={noop} + rotateRecoveryLockPassword={noop} /> ); @@ -63,8 +71,10 @@ describe("OSSettingsErrorCell", () => { render( new Promise(() => undefined)} + resendRequest={noop} + rotateRecoveryLockPassword={noop} /> ); @@ -75,8 +85,10 @@ describe("OSSettingsErrorCell", () => { render( new Promise(() => undefined)} + resendRequest={noop} + rotateRecoveryLockPassword={noop} /> ); @@ -87,11 +99,13 @@ describe("OSSettingsErrorCell", () => { render( new Promise(() => undefined)} + resendRequest={noop} + rotateRecoveryLockPassword={noop} /> ); @@ -105,11 +119,13 @@ describe("OSSettingsErrorCell", () => { render( new Promise(() => undefined)} + resendRequest={noop} + rotateRecoveryLockPassword={noop} /> ); @@ -125,11 +141,13 @@ describe("OSSettingsErrorCell", () => { render( new Promise(() => undefined)} + resendRequest={noop} + rotateRecoveryLockPassword={noop} /> ); @@ -145,12 +163,14 @@ describe("OSSettingsErrorCell", () => { render( new Promise(() => undefined)} + resendRequest={noop} + rotateRecoveryLockPassword={noop} /> ); @@ -162,15 +182,89 @@ describe("OSSettingsErrorCell", () => { expect(screen.getByText("Profile GUID")).toBeInTheDocument(); }); + it("renders a rotate button when canRotateRecoveryLockPassword is true and password status is verified", () => { + render( + + ); + + expect(screen.getByRole("button", { name: "Rotate" })).toBeInTheDocument(); + }); + + it("renders a rotate button when canRotateRecoveryLockPassword is true and password status is failed", () => { + render( + + ); + + expect(screen.getByRole("button", { name: "Rotate" })).toBeInTheDocument(); + }); + + it("does not render a rotate button when canRotateRecoveryLockPassword is false", () => { + render( + + ); + + expect( + screen.queryByRole("button", { name: "Rotate" }) + ).not.toBeInTheDocument(); + }); + + it("does not render a rotate button when password status is pending", () => { + render( + + ); + + expect( + screen.queryByRole("button", { name: "Rotate" }) + ).not.toBeInTheDocument(); + }); + it("renders a formatted tooltip when the error message matches digicert token patern", () => { render( new Promise(() => undefined)} + resendRequest={noop} + rotateRecoveryLockPassword={noop} /> ); diff --git a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsErrorCell/OSSettingsErrorCell.tsx b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsErrorCell/OSSettingsErrorCell.tsx index 53244508e6..512b99ca92 100644 --- a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsErrorCell/OSSettingsErrorCell.tsx +++ b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsErrorCell/OSSettingsErrorCell.tsx @@ -232,21 +232,52 @@ const generateErrorTooltip = ( return cellValue; }; +interface IRotateButtonProps { + isRotating: boolean; + onClick: () => void; +} + +const RotateButton = ({ isRotating, onClick }: IRotateButtonProps) => { + const classNames = classnames(`${baseClass}__rotate-button`, "rotate-link", { + [`${baseClass}__rotating`]: isRotating, + }); + + const buttonText = isRotating ? "Rotating..." : "Rotate"; + + return ( + + ); +}; + interface IOSSettingsErrorCellProps { canResendProfiles: boolean; + canRotateRecoveryLockPassword?: boolean; profile: IHostMdmProfileWithAddedStatus; resendRequest: (profileUUID: string) => Promise; + rotateRecoveryLockPassword?: () => Promise; onProfileResent?: () => void; } const OSSettingsErrorCell = ({ canResendProfiles, + canRotateRecoveryLockPassword = false, profile, resendRequest, + rotateRecoveryLockPassword, onProfileResent = noop, }: IOSSettingsErrorCellProps) => { const { renderFlash } = useContext(NotificationContext); const [isLoading, setIsLoading] = useState(false); + const [isRotating, setIsRotating] = useState(false); const onResendProfile = async () => { setIsLoading(true); @@ -259,9 +290,29 @@ const OSSettingsErrorCell = ({ setIsLoading(false); }; + const onRotatePassword = async () => { + if (!rotateRecoveryLockPassword) return; + setIsRotating(true); + try { + await rotateRecoveryLockPassword(); + renderFlash( + "success", + "Successfully sent request to rotate Recovery Lock password." + ); + } catch (e) { + renderFlash( + "error", + "Couldn't send request to rotate Recovery Lock password. Please try again." + ); + } + setIsRotating(false); + }; + const isFailed = profile.status === "failed"; const isVerified = profile.status === "verified"; const showRefetchButton = canResendProfiles && (isFailed || isVerified); + const showRotateButton = + canRotateRecoveryLockPassword && (isFailed || isVerified); const value = (isFailed && profile.detail) || DEFAULT_EMPTY_CELL_VALUE; const tooltip = generateErrorTooltip(value, profile); @@ -275,7 +326,7 @@ const OSSettingsErrorCell = ({ // we dont want the default "w250" class so we pass in empty string classes="" className={ - isFailed || showRefetchButton + isFailed || showRefetchButton || showRotateButton ? `${baseClass}__failed-message` : undefined } @@ -283,6 +334,9 @@ const OSSettingsErrorCell = ({ {showRefetchButton && ( )} + {showRotateButton && ( + + )}
); }; diff --git a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsErrorCell/_styles.scss b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsErrorCell/_styles.scss index 354baafdb6..fd94f47d51 100644 --- a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsErrorCell/_styles.scss +++ b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsErrorCell/_styles.scss @@ -55,4 +55,32 @@ } } } + + &__rotate-button { + width: 106px; + display: flex; + + .children-wrapper { + display: flex; + + .icon { + vertical-align: middle; + margin-right: 8px; + } + } + } + + &__rotating { + color: $core-vibrant-blue; + cursor: default; + font-size: $x-small; + height: 38px; + opacity: 50%; + filter: saturate(100%); + + .icon { + vertical-align: middle; + animation: spin 2s linear infinite; + } + } } diff --git a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsTable.tsx b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsTable.tsx index e9653c9e79..3dee045b81 100644 --- a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsTable.tsx +++ b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsTable.tsx @@ -9,22 +9,38 @@ const baseClass = "os-settings-table"; interface IOSSettingsTableProps { canResendProfiles: boolean; + canRotateRecoveryLockPassword?: boolean; tableData: IHostMdmProfileWithAddedStatus[]; resendRequest: (profileUUID: string) => Promise; + rotateRecoveryLockPassword?: () => Promise; onProfileResent: () => void; } const OSSettingsTable = ({ canResendProfiles, + canRotateRecoveryLockPassword = false, tableData, resendRequest, + rotateRecoveryLockPassword, onProfileResent, }: IOSSettingsTableProps) => { // useMemo prevents tooltip flashing during host data refetch const tableConfig = useMemo( () => - generateTableHeaders(canResendProfiles, resendRequest, onProfileResent), - [canResendProfiles, resendRequest, onProfileResent] + generateTableHeaders( + canResendProfiles, + resendRequest, + onProfileResent, + canRotateRecoveryLockPassword, + rotateRecoveryLockPassword + ), + [ + canResendProfiles, + resendRequest, + onProfileResent, + canRotateRecoveryLockPassword, + rotateRecoveryLockPassword, + ] ); return ( diff --git a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsTableConfig.tsx b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsTableConfig.tsx index 1a25ab1052..833fde73df 100644 --- a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsTableConfig.tsx +++ b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsTableConfig.tsx @@ -22,6 +22,7 @@ import { generateLinuxDiskEncryptionSetting, generateRecoveryLockPasswordSetting, generateWinDiskEncryptionSetting, + REC_LOCK_SYNTHETIC_PROFILE_UUID, } from "../../helpers"; export interface IHostMdmProfileWithAddedStatus @@ -45,7 +46,9 @@ export type OsSettingsTableStatusValue = const generateTableConfig = ( canResendProfiles: boolean, resendRequest: (profileUUID: string) => Promise, - onProfileResent: () => void + onProfileResent: () => void, + canRotateRecoveryLockPassword?: boolean, + rotateRecoveryLockPassword?: () => Promise ): ITableColumnConfig[] => { return [ { @@ -95,14 +98,22 @@ const generateTableConfig = ( isAppleDevice(platform) && !isDDMProfile(cellProps.row.original); const isWindowsProfile = platform === "windows"; + const isRecoveryLockRow = + cellProps.row.original.profile_uuid === + REC_LOCK_SYNTHETIC_PROFILE_UUID; + return ( ); diff --git a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/_styles.scss b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/_styles.scss index b80497df2d..8573059dd6 100644 --- a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/_styles.scss +++ b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/_styles.scss @@ -7,9 +7,9 @@ width: initial; } tbody td.detail__cell { - // these styles are a little trick that allows the cell to + // these styles are a little trick that allows the cell to // shrink while still follwing the auto width behavior of the table. - // This is needed to show the error text and button in the + // This is needed to show the error text and button in the // error cell correctly without overflowing outside of the table. max-width: 0; width: 100%; @@ -43,4 +43,16 @@ opacity: 1; } } + + // row hover effect for rotate button, matching resend pattern + .rotate-link:not(.os-settings-error-cell__rotating) { + opacity: 0; + transition: opacity 250ms; + } + + tr:hover { + .rotate-link:not(.os-settings-error-cell__rotating) { + opacity: 1; + } + } } diff --git a/frontend/pages/hosts/details/cards/Activity/ActivityConfig.tsx b/frontend/pages/hosts/details/cards/Activity/ActivityConfig.tsx index b4d3de5522..1c4517d54a 100644 --- a/frontend/pages/hosts/details/cards/Activity/ActivityConfig.tsx +++ b/frontend/pages/hosts/details/cards/Activity/ActivityConfig.tsx @@ -17,6 +17,7 @@ import UnlockedHostActivityItem from "./ActivityItems/UnlockedHostActivityItem"; import ReadHostDiskEncryptionKeyActivityItem from "./ActivityItems/ReadHostDiskEncryptionKey"; import ViewedHostRecoveryLockPasswordActivityItem from "./ActivityItems/ViewedHostRecoveryLockPassword"; import SetHostRecoveryLockPasswordActivityItem from "./ActivityItems/SetHostRecoveryLockPassword"; +import RotatedHostRecoveryLockPasswordActivityItem from "./ActivityItems/RotatedHostRecoveryLockPassword"; import InstalledSoftwareActivityItem from "./ActivityItems/InstalledSoftwareActivityItem"; import CanceledRunScriptActivityItem from "./ActivityItems/CanceledRunScriptActivityItem"; import CanceledInstallSoftwareActivityItem from "./ActivityItems/CanceledInstallSoftwareActivityItem"; @@ -54,6 +55,7 @@ export const pastActivityComponentMap: Record< [ActivityType.ReadHostDiskEncryptionKey]: ReadHostDiskEncryptionKeyActivityItem, [ActivityType.ViewedHostRecoveryLockPassword]: ViewedHostRecoveryLockPasswordActivityItem, [ActivityType.SetHostRecoveryLockPassword]: SetHostRecoveryLockPasswordActivityItem, + [ActivityType.RotatedHostRecoveryLockPassword]: RotatedHostRecoveryLockPasswordActivityItem, [ActivityType.UnlockedHost]: UnlockedHostActivityItem, [ActivityType.InstalledSoftware]: InstalledSoftwareActivityItem, [ActivityType.UninstalledSoftware]: InstalledSoftwareActivityItem, diff --git a/frontend/pages/hosts/details/cards/Activity/ActivityItems/RotatedHostRecoveryLockPassword/RotatedHostRecoveryLockPassword.tests.tsx b/frontend/pages/hosts/details/cards/Activity/ActivityItems/RotatedHostRecoveryLockPassword/RotatedHostRecoveryLockPassword.tests.tsx new file mode 100644 index 0000000000..834ca6c9fd --- /dev/null +++ b/frontend/pages/hosts/details/cards/Activity/ActivityItems/RotatedHostRecoveryLockPassword/RotatedHostRecoveryLockPassword.tests.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import { render, screen } from "@testing-library/react"; +import { createMockHostPastActivity } from "__mocks__/activityMock"; + +import RotatedHostRecoveryLockPasswordActivityItem from "./RotatedHostRecoveryLockPassword"; + +describe("RotatedHostRecoveryLockPasswordActivityItem", () => { + it("renders the activity content", () => { + render( + + ); + + expect(screen.getByText("Test User")).toBeVisible(); + expect( + screen.getByText(/rotated the Recovery Lock password/i) + ).toBeVisible(); + }); + + it("does not render the cancel icon", () => { + render( + + ); + + expect(screen.queryByTestId("close-icon")).not.toBeInTheDocument(); + }); + + it("does not render the show details icon", () => { + render( + + ); + + expect(screen.queryByTestId("info-outline-icon")).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/pages/hosts/details/cards/Activity/ActivityItems/RotatedHostRecoveryLockPassword/RotatedHostRecoveryLockPassword.tsx b/frontend/pages/hosts/details/cards/Activity/ActivityItems/RotatedHostRecoveryLockPassword/RotatedHostRecoveryLockPassword.tsx new file mode 100644 index 0000000000..70f866129e --- /dev/null +++ b/frontend/pages/hosts/details/cards/Activity/ActivityItems/RotatedHostRecoveryLockPassword/RotatedHostRecoveryLockPassword.tsx @@ -0,0 +1,18 @@ +import React from "react"; + +import ActivityItem from "components/ActivityItem"; + +import { IHostActivityItemComponentProps } from "../../ActivityConfig"; + +const RotatedHostRecoveryLockPasswordActivityItem = ({ + activity, +}: IHostActivityItemComponentProps) => { + return ( + + {activity.actor_full_name} + rotated the Recovery Lock password for this host. + + ); +}; + +export default RotatedHostRecoveryLockPasswordActivityItem; diff --git a/frontend/pages/hosts/details/cards/Activity/ActivityItems/RotatedHostRecoveryLockPassword/index.ts b/frontend/pages/hosts/details/cards/Activity/ActivityItems/RotatedHostRecoveryLockPassword/index.ts new file mode 100644 index 0000000000..9e28cc1971 --- /dev/null +++ b/frontend/pages/hosts/details/cards/Activity/ActivityItems/RotatedHostRecoveryLockPassword/index.ts @@ -0,0 +1 @@ +export { default } from "./RotatedHostRecoveryLockPassword"; diff --git a/frontend/pages/hosts/details/helpers.ts b/frontend/pages/hosts/details/helpers.ts index 040f2294ed..17ab925223 100644 --- a/frontend/pages/hosts/details/helpers.ts +++ b/frontend/pages/hosts/details/helpers.ts @@ -72,12 +72,14 @@ export const generateLinuxDiskEncryptionSetting = ( }; }; +export const REC_LOCK_SYNTHETIC_PROFILE_UUID = "rec_lock_dummy"; + export const generateRecoveryLockPasswordSetting = ( status: RecoveryLockPasswordStatus, detail: string ): IHostMdmProfile => { return { - profile_uuid: "rec_lock_dummy", + profile_uuid: REC_LOCK_SYNTHETIC_PROFILE_UUID, platform: "darwin", name: "Recovery Lock password", status, diff --git a/frontend/services/entities/hosts.ts b/frontend/services/entities/hosts.ts index 984c3f7253..ad5e63ccb3 100644 --- a/frontend/services/entities/hosts.ts +++ b/frontend/services/entities/hosts.ts @@ -614,6 +614,11 @@ export default { return sendRequest("GET", HOST_RECOVERY_LOCK_PASSWORD(id)); }, + rotateRecoveryLockPassword: (id: number): Promise => { + const { HOST_RECOVERY_LOCK_PASSWORD_ROTATE } = endpoints; + return sendRequest("POST", HOST_RECOVERY_LOCK_PASSWORD_ROTATE(id)); + }, + lockHost: (id: number) => { const { HOST_LOCK } = endpoints; return sendRequest("POST", HOST_LOCK(id)); diff --git a/frontend/utilities/endpoints.ts b/frontend/utilities/endpoints.ts index 95c81cabf2..9b0d4a9a18 100644 --- a/frontend/utilities/endpoints.ts +++ b/frontend/utilities/endpoints.ts @@ -187,6 +187,8 @@ export default { `/${API_VERSION}/fleet/hosts/${id}/encryption_key`, HOST_RECOVERY_LOCK_PASSWORD: (id: number) => `/${API_VERSION}/fleet/hosts/${id}/recovery_lock_password`, + HOST_RECOVERY_LOCK_PASSWORD_ROTATE: (id: number) => + `/${API_VERSION}/fleet/hosts/${id}/recovery_lock_password/rotate`, ME: `/${API_VERSION}/fleet/me`,