43890 MLAPR frontend (#44739)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #43890 

Frontend for macOS Local Admin Password Rotation

Changes file added during past work

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [x] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.
- [x] Timeouts are implemented and retries are limited to avoid infinite
loops
- [x] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes

## Testing

- [x] Added/updated automated tests
- [x] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)

- [x] QA'd all new/changed functionality manually


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **New Features**
  * Added ability to rotate managed local account passwords for hosts
* Added visibility for auto-rotation scheduling and pending rotation
status
* New activity feed entries for managed local account password rotation
events (successful and failed rotations)

* **Improvements**
* Enhanced host action menu to display managed account options when
password is available
* Added real-time status updates and notifications during password
rotation operations

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Jordan Montgomery
2026-05-07 07:03:35 -04:00
committed by GitHub
parent 4910c450a4
commit a9c66471c0
20 changed files with 652 additions and 3 deletions
+8
View File
@@ -170,6 +170,8 @@ export enum ActivityType {
DisabledManagedLocalAccount = "disabled_managed_local_account",
ViewedManagedLocalAccount = "read_managed_local_account",
CreatedManagedLocalAccount = "created_managed_local_account",
RotatedManagedLocalAccountPassword = "rotated_managed_local_account_password",
FailedToRotateManagedLocalAccountPassword = "failed_to_rotate_managed_local_account_password",
FailedEnrollmentProfileRenewal = "failed_enrollment_profile_renewal",
CreatedLabel = "created_label",
EditedLabel = "edited_label",
@@ -204,6 +206,8 @@ export type IHostPastActivityType =
| ActivityType.ClearedPasscode
| ActivityType.ViewedManagedLocalAccount
| ActivityType.CreatedManagedLocalAccount
| ActivityType.RotatedManagedLocalAccountPassword
| ActivityType.FailedToRotateManagedLocalAccountPassword
| ActivityType.FailedEnrollmentProfileRenewal;
/** This is a subset of ActivityType that are shown only for the host upcoming activities */
@@ -503,6 +507,10 @@ export const ACTIVITY_TYPE_TO_FILTER_LABEL: Record<ActivityType, string> = {
"Turned off managed local account",
[ActivityType.ViewedManagedLocalAccount]: "Viewed managed account",
[ActivityType.CreatedManagedLocalAccount]: "Created managed account",
[ActivityType.RotatedManagedLocalAccountPassword]:
"Triggered managed local account password rotation",
[ActivityType.FailedToRotateManagedLocalAccountPassword]:
"Failed to rotate managed local account password",
[ActivityType.FailedEnrollmentProfileRenewal]:
"Enrollment profile renewal failed",
[ActivityType.CreatedLabel]: "Created label",
+4
View File
@@ -132,6 +132,8 @@ export interface IOSSettings {
managed_local_account?: {
status: string | null;
password_available: boolean;
auto_rotate_at?: string;
pending_rotation?: boolean;
};
certificates: IHostAndroidCert[];
}
@@ -268,6 +270,8 @@ export interface IHostManagedAccountPasswordResponse {
username: string;
password: string;
updated_at: string;
auto_rotate_at?: string;
pending_rotation?: boolean;
};
}
@@ -684,6 +684,58 @@ describe("Activity Feed", () => {
expect(screen.getByText("Alex's Macbook Air")).toBeInTheDocument();
});
it("renders a 'rotated_managed_local_account_password' type activity", () => {
const activity = createMockActivity({
type: ActivityType.RotatedManagedLocalAccountPassword,
details: { host_display_name: "Marsh's Macbook Air" },
});
render(<GlobalActivityItem activity={activity} isPremiumTier />);
expect(
screen.getByText(
"triggered rotation of the managed local account password for",
{ exact: false }
)
).toBeInTheDocument();
expect(screen.getByText("Marsh's Macbook Air")).toBeInTheDocument();
});
it("renders a Fleet-initiated 'rotated_managed_local_account_password' type activity", () => {
const activity = createMockActivity({
type: ActivityType.RotatedManagedLocalAccountPassword,
fleet_initiated: true,
details: { host_display_name: "Marsh's Macbook Air" },
});
render(<GlobalActivityItem activity={activity} isPremiumTier />);
expect(screen.getByText("Fleet")).toBeInTheDocument();
expect(
screen.getByText(
"triggered rotation of the managed local account password for",
{ exact: false }
)
).toBeInTheDocument();
expect(screen.getByText("Marsh's Macbook Air")).toBeInTheDocument();
});
it("renders a 'failed_to_rotate_managed_local_account_password' type activity", () => {
const activity = createMockActivity({
type: ActivityType.FailedToRotateManagedLocalAccountPassword,
fleet_initiated: true,
details: { host_display_name: "Marsh's Macbook Air" },
});
render(<GlobalActivityItem activity={activity} isPremiumTier />);
expect(screen.getByText("Fleet")).toBeInTheDocument();
expect(
screen.getByText(
"failed to rotate the managed local account password for",
{ exact: false }
)
).toBeInTheDocument();
expect(screen.getByText("Marsh's Macbook Air")).toBeInTheDocument();
});
it("renders an 'enabled_recovery_lock_passwords' type activity for a team", () => {
const activity = createMockActivity({
type: ActivityType.EnabledRecoveryLockPasswords,
@@ -583,6 +583,24 @@ const TAGGED_TEMPLATES = {
</>
);
},
rotatedManagedLocalAccountPassword: (activity: IActivity) => {
return (
<>
{" "}
triggered rotation of the managed local account password for{" "}
<b>{activity.details?.host_display_name}</b>.
</>
);
},
failedToRotateManagedLocalAccountPassword: (activity: IActivity) => {
return (
<>
{" "}
failed to rotate the managed local account password for{" "}
<b>{activity.details?.host_display_name}</b>.
</>
);
},
createdAppleOSProfile: (activity: IActivity, isPremiumTier: boolean) => {
const profileName = activity.details?.profile_name;
return (
@@ -2119,6 +2137,14 @@ const getDetail = (activity: IActivity, isPremiumTier: boolean) => {
case ActivityType.CreatedManagedLocalAccount: {
return TAGGED_TEMPLATES.createdManagedLocalAccount(activity);
}
case ActivityType.RotatedManagedLocalAccountPassword: {
return TAGGED_TEMPLATES.rotatedManagedLocalAccountPassword(activity);
}
case ActivityType.FailedToRotateManagedLocalAccountPassword: {
return TAGGED_TEMPLATES.failedToRotateManagedLocalAccountPassword(
activity
);
}
case ActivityType.CreatedAppleOSProfile: {
return TAGGED_TEMPLATES.createdAppleOSProfile(activity, isPremiumTier);
}
@@ -2051,6 +2051,40 @@ describe("Host Actions Dropdown", () => {
});
});
it("enables the action when status is pending but password is available (e.g. viewed-and-waiting)", async () => {
const render = createCustomRenderer({
context: {
app: {
isGlobalAdmin: true,
isPremiumTier: true,
currentUser: createMockUser(),
},
},
});
const { user } = render(
<HostActionsDropdown
hostTeamId={null}
onSelect={noop}
hostStatus="online"
hostMdmEnrollmentStatus="On (automatic)"
hostMdmDeviceStatus="unlocked"
hostScriptsEnabled
isConnectedToFleetMdm
hostPlatform="darwin"
isManagedLocalAccountEnabled
managedAccountStatus="pending"
managedAccountPasswordAvailable
/>
);
await user.click(screen.getByText("Actions"));
const option = screen.getByText("Show managed account");
expect(option).toBeInTheDocument();
expect(option).not.toHaveAttribute("aria-disabled", "true");
});
it("renders the action for company-owned ADE enrollment status", async () => {
const render = createCustomRenderer({
context: {
@@ -28,6 +28,7 @@ interface IHostActionsDropdownProps {
recoveryLockPasswordAvailable?: boolean;
isManagedLocalAccountEnabled?: boolean;
managedAccountStatus?: string | null;
managedAccountPasswordAvailable?: boolean;
}
const HostActionsDropdown = ({
@@ -46,6 +47,7 @@ const HostActionsDropdown = ({
recoveryLockPasswordAvailable = false,
isManagedLocalAccountEnabled = false,
managedAccountStatus,
managedAccountPasswordAvailable = false,
}: IHostActionsDropdownProps) => {
const {
isPremiumTier = false,
@@ -102,6 +104,7 @@ const HostActionsDropdown = ({
recoveryLockPasswordAvailable,
isManagedLocalAccountEnabled,
managedAccountStatus,
managedAccountPasswordAvailable,
});
// No options to render. Exit early
@@ -115,6 +115,7 @@ interface IHostActionConfigOptions {
recoveryLockPasswordAvailable: boolean;
isManagedLocalAccountEnabled: boolean;
managedAccountStatus: string | null | undefined;
managedAccountPasswordAvailable: boolean;
}
const canTransferTeam = (config: IHostActionConfigOptions) => {
@@ -523,6 +524,7 @@ const modifyOptions = (
diskEncryptionProfileStatus,
recoveryLockPasswordAvailable,
managedAccountStatus,
managedAccountPasswordAvailable,
}: IHostActionConfigOptions
) => {
const disableOptions = (optionsToDisable: IDropdownOption[]) => {
@@ -632,13 +634,18 @@ const modifyOptions = (
}
}
if (managedAccountStatus !== "verified") {
// Gate on password_available rather than status === "verified" — a row whose
// status is "pending" because of a recent view (or a deferred rotation
// waiting on UUID capture) still has a viewable password. Mirrors the
// backend gate in GetHostManagedAccountPassword.
if (!managedAccountPasswordAvailable) {
const managedAccountOption = options.find(
(option) => option.value === "managedAccount"
);
if (managedAccountOption) {
managedAccountOption.disabled = true;
if (managedAccountStatus === "pending") {
// No password yet — the AccountConfiguration command hasn't been acked.
managedAccountOption.tooltipContent = (
<>
The managed account is still being
@@ -1053,6 +1053,10 @@ const HostDetailsPage = ({
managedAccountStatus={
host.mdm.os_settings?.managed_local_account?.status
}
managedAccountPasswordAvailable={
host.mdm.os_settings?.managed_local_account?.password_available ??
false
}
/>
);
};
@@ -1657,10 +1661,25 @@ const HostDetailsPage = ({
{showManagedAccountModal && host && (
<ManagedAccountModal
hostId={host.id}
canRotatePassword={
isGlobalAdmin ||
isGlobalMaintainer ||
isHostTeamAdmin ||
isHostTeamMaintainer
}
onCancel={() => {
setShowManagedAccountModal(false);
// Opening the modal triggers a "viewed managed account"
// activity server-side, so refetch to show it in the feed.
// activity server-side and may set auto_rotate_at; refetch
// host details + activities so they reflect the new state.
refetchHostDetails();
refetchPastActivities();
}}
onRotate={() => {
// The rotation activity and cleared auto_rotate_at land on
// host details / activities — refresh both so the banner and
// feed are in sync with the newly-rotated state.
refetchHostDetails();
refetchPastActivities();
}}
/>
@@ -0,0 +1,237 @@
import React from "react";
import { screen, waitFor } from "@testing-library/react";
import { createCustomRenderer } from "test/test-utils";
import hostAPI from "services/entities/hosts";
import ManagedAccountModal from "./ManagedAccountModal";
jest.mock("services/entities/hosts");
const mockPasswordResponse = {
host_id: 7,
managed_account_password: {
username: "_fleetadmin",
password: "supersecret",
updated_at: "2026-04-30T13:00:00Z",
},
};
describe("ManagedAccountModal", () => {
const render = createCustomRenderer({ withBackendMock: true });
beforeEach(() => {
jest.resetAllMocks();
(hostAPI.getManagedAccountPassword as jest.Mock).mockResolvedValue(
mockPasswordResponse
);
});
it("renders username and password masked input", async () => {
render(
<ManagedAccountModal
hostId={7}
canRotatePassword
onCancel={jest.fn()}
onRotate={jest.fn()}
/>
);
await waitFor(() => {
expect(screen.getByText("_fleetadmin")).toBeVisible();
});
expect(screen.getByText("Username")).toBeVisible();
});
it("shows the auto-rotate banner when auto_rotate_at is in the response", async () => {
(hostAPI.getManagedAccountPassword as jest.Mock).mockResolvedValue({
...mockPasswordResponse,
managed_account_password: {
...mockPasswordResponse.managed_account_password,
auto_rotate_at: "2026-04-30T14:35:00Z",
},
});
render(
<ManagedAccountModal
hostId={7}
canRotatePassword
onCancel={jest.fn()}
onRotate={jest.fn()}
/>
);
await waitFor(() => {
expect(
screen.getByText(/Password rotates automatically after/i)
).toBeVisible();
});
});
it("does not show the banner when auto_rotate_at is missing from the response", async () => {
render(
<ManagedAccountModal
hostId={7}
canRotatePassword
onCancel={jest.fn()}
onRotate={jest.fn()}
/>
);
await waitFor(() => {
expect(screen.getByText("_fleetadmin")).toBeVisible();
});
expect(
screen.queryByText(/Password rotates automatically after/i)
).not.toBeInTheDocument();
});
it("hides the rotate button when canRotatePassword is false", async () => {
render(
<ManagedAccountModal
hostId={7}
canRotatePassword={false}
onCancel={jest.fn()}
onRotate={jest.fn()}
/>
);
await waitFor(() => {
expect(screen.getByText("_fleetadmin")).toBeVisible();
});
expect(screen.queryByText("Rotate password")).not.toBeInTheDocument();
});
it("shows the rotate button when canRotatePassword is true", async () => {
render(
<ManagedAccountModal
hostId={7}
canRotatePassword
onCancel={jest.fn()}
onRotate={jest.fn()}
/>
);
await waitFor(() => {
expect(screen.getByText("Rotate password")).toBeVisible();
});
});
it("calls rotate API and onRotate on success", async () => {
(hostAPI.rotateManagedLocalAccountPassword as jest.Mock).mockResolvedValue(
undefined
);
const onRotate = jest.fn();
const { user } = render(
<ManagedAccountModal
hostId={7}
canRotatePassword
onCancel={jest.fn()}
onRotate={onRotate}
/>
);
const button = await screen.findByText("Rotate password");
await user.click(button);
await waitFor(() => {
expect(hostAPI.rotateManagedLocalAccountPassword).toHaveBeenCalledWith(7);
});
await waitFor(() => {
expect(onRotate).toHaveBeenCalled();
});
});
it("shows the pending-rotation banner when pending_rotation is in the response", async () => {
(hostAPI.getManagedAccountPassword as jest.Mock).mockResolvedValue({
...mockPasswordResponse,
managed_account_password: {
...mockPasswordResponse.managed_account_password,
pending_rotation: true,
},
});
render(
<ManagedAccountModal
hostId={7}
canRotatePassword
onCancel={jest.fn()}
onRotate={jest.fn()}
/>
);
await waitFor(() => {
expect(
screen.getByText(
"Password will rotate once the host acknowledges the request."
)
).toBeVisible();
});
expect(
screen.queryByText(/Password rotates automatically after/i)
).not.toBeInTheDocument();
});
it("shows the pending-rotation banner after a successful rotate", async () => {
(hostAPI.rotateManagedLocalAccountPassword as jest.Mock).mockResolvedValue(
undefined
);
(hostAPI.getManagedAccountPassword as jest.Mock).mockResolvedValue({
...mockPasswordResponse,
managed_account_password: {
...mockPasswordResponse.managed_account_password,
auto_rotate_at: "2026-04-30T14:35:00Z",
},
});
const { user } = render(
<ManagedAccountModal
hostId={7}
canRotatePassword
onCancel={jest.fn()}
onRotate={jest.fn()}
/>
);
const button = await screen.findByText("Rotate password");
await user.click(button);
await waitFor(() => {
expect(
screen.getByText(
"Password will rotate once the host acknowledges the request."
)
).toBeVisible();
});
// The auto-rotate banner is replaced by the pending-rotation banner.
expect(
screen.queryByText(/Password rotates automatically after/i)
).not.toBeInTheDocument();
});
it("does not call onRotate when rotate API errors", async () => {
(hostAPI.rotateManagedLocalAccountPassword as jest.Mock).mockRejectedValue(
new Error("boom")
);
const onRotate = jest.fn();
const { user } = render(
<ManagedAccountModal
hostId={7}
canRotatePassword
onCancel={jest.fn()}
onRotate={onRotate}
/>
);
const button = await screen.findByText("Rotate password");
await user.click(button);
await waitFor(() => {
expect(hostAPI.rotateManagedLocalAccountPassword).toHaveBeenCalledWith(7);
});
expect(onRotate).not.toHaveBeenCalled();
});
});
@@ -1,27 +1,45 @@
import React from "react";
import React, { useContext, useState } from "react";
import { useQuery } from "react-query";
import { IHostManagedAccountPasswordResponse } from "interfaces/host";
import hostAPI from "services/entities/hosts";
import { NotificationContext } from "context/notification";
import Modal from "components/Modal";
import Button from "components/buttons/Button";
import InputFieldHiddenContent from "components/forms/fields/InputFieldHiddenContent";
import DataError from "components/DataError";
import Spinner from "components/Spinner";
import Icon from "components/Icon";
import InfoBanner from "components/InfoBanner";
import { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants";
import { monthDayTimeFormat } from "utilities/date_format";
import { getErrorReason } from "interfaces/errors";
const baseClass = "managed-account-modal";
interface IManagedAccountModalProps {
hostId: number;
// TODO: For this modal the Figma dev note said "Hide option if not Admin or
// maintainer role." We're hiding here per the design, but the analogous
// RecoveryLockPasswordModal disables-with-tooltip in the same situation.
// We deferred this decision for now because this modal only displays for
// Admin or Maintainer roles
canRotatePassword: boolean;
onCancel: () => void;
onRotate: () => void;
}
const ManagedAccountModal = ({
hostId,
canRotatePassword,
onCancel,
onRotate,
}: IManagedAccountModalProps) => {
const { renderFlash } = useContext(NotificationContext);
const [isRotating, setIsRotating] = useState(false);
const [justRotated, setJustRotated] = useState(false);
const {
data: managedAccountData,
error: managedAccountError,
@@ -41,6 +59,32 @@ const ManagedAccountModal = ({
}
);
const onRotatePassword = async () => {
setIsRotating(true);
try {
await hostAPI.rotateManagedLocalAccountPassword(hostId);
setJustRotated(true);
renderFlash(
"success",
"Successfully sent request to rotate managed local account password."
);
// Notify parent so it can refetch host details + activities.
onRotate();
} catch (e) {
const msg = getErrorReason(e);
renderFlash(
"error",
msg ||
"Couldn't send request to rotate managed local account password. Please try again."
);
}
setIsRotating(false);
};
const showPendingRotationBanner =
justRotated || managedAccountData?.pending_rotation === true;
const autoRotateAt = managedAccountData?.auto_rotate_at;
return (
<Modal title="Managed account" onExit={onCancel} className={baseClass}>
{isLoading && <Spinner />}
@@ -57,8 +101,31 @@ const ManagedAccountModal = ({
value={managedAccountData?.password ?? ""}
name="Password"
/>
{showPendingRotationBanner ? (
<InfoBanner color="yellow">
Password will rotate once the host acknowledges the request.
</InfoBanner>
) : (
autoRotateAt && (
<InfoBanner color="yellow">
Password rotates automatically after{" "}
{monthDayTimeFormat(autoRotateAt)}.
</InfoBanner>
)
)}
<div className="modal-cta-wrap">
<Button onClick={onCancel}>Close</Button>
{canRotatePassword && (
<Button
variant="inverse"
onClick={onRotatePassword}
disabled={isRotating}
className={`${baseClass}__rotate-button`}
>
<Icon name="refresh" />
{isRotating ? "Rotating..." : "Rotate password"}
</Button>
)}
</div>
</>
)
@@ -16,4 +16,15 @@
font-size: $x-small;
color: $ui-fleet-black-75;
}
.info-banner {
margin-top: $pad-medium;
margin-bottom: $pad-small;
}
&__rotate-button .children-wrapper {
display: flex;
align-items: center;
gap: $pad-small;
}
}
@@ -29,6 +29,8 @@ import ClearedPasscodeActivityItem from "./ActivityItems/ClearedPasscodeActivity
import FailedWipeActivityItem from "./ActivityItems/FailedWipeActivityItem";
import ViewedManagedLocalAccountActivityItem from "./ActivityItems/ViewedManagedLocalAccountActivityItem/ViewedManagedLocalAccountActivityItem";
import CreatedManagedLocalAccountActivityItem from "./ActivityItems/CreatedManagedLocalAccountActivityItem/CreatedManagedLocalAccountActivityItem";
import RotatedManagedLocalAccountPasswordActivityItem from "./ActivityItems/RotatedManagedLocalAccountPassword";
import FailedToRotateManagedLocalAccountPasswordActivityItem from "./ActivityItems/FailedToRotateManagedLocalAccountPassword";
import FailedEnrollmentProfileRenewalActivityItem from "./ActivityItems/FailedEnrollmentProfileRenewalActivityItem";
/** The component props that all host activity items must adhere to */
@@ -79,6 +81,8 @@ export const pastActivityComponentMap: Record<
[ActivityType.ClearedPasscode]: ClearedPasscodeActivityItem,
[ActivityType.ViewedManagedLocalAccount]: ViewedManagedLocalAccountActivityItem,
[ActivityType.CreatedManagedLocalAccount]: CreatedManagedLocalAccountActivityItem,
[ActivityType.RotatedManagedLocalAccountPassword]: RotatedManagedLocalAccountPasswordActivityItem,
[ActivityType.FailedToRotateManagedLocalAccountPassword]: FailedToRotateManagedLocalAccountPasswordActivityItem,
[ActivityType.FailedEnrollmentProfileRenewal]: FailedEnrollmentProfileRenewalActivityItem,
};
@@ -0,0 +1,58 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { createMockHostPastActivity } from "__mocks__/activityMock";
import { ActivityType } from "interfaces/activity";
import FailedToRotateManagedLocalAccountPasswordActivityItem from "./FailedToRotateManagedLocalAccountPassword";
describe("FailedToRotateManagedLocalAccountPasswordActivityItem", () => {
it("renders Fleet-initiated failed rotation activity content", () => {
render(
<FailedToRotateManagedLocalAccountPasswordActivityItem
activity={createMockHostPastActivity({
actor_full_name: "Fleet",
fleet_initiated: true,
type: ActivityType.FailedToRotateManagedLocalAccountPassword,
})}
tab="past"
/>
);
expect(screen.getByText("Fleet")).toBeVisible();
expect(
screen.getByText(
/failed to rotate the managed local account password for this host/i
)
).toBeVisible();
});
it("does not render the cancel icon", () => {
render(
<FailedToRotateManagedLocalAccountPasswordActivityItem
activity={createMockHostPastActivity({
actor_full_name: "Fleet",
fleet_initiated: true,
type: ActivityType.FailedToRotateManagedLocalAccountPassword,
})}
tab="past"
/>
);
expect(screen.queryByTestId("close-icon")).not.toBeInTheDocument();
});
it("does not render the show details icon", () => {
render(
<FailedToRotateManagedLocalAccountPasswordActivityItem
activity={createMockHostPastActivity({
actor_full_name: "Fleet",
fleet_initiated: true,
type: ActivityType.FailedToRotateManagedLocalAccountPassword,
})}
tab="past"
/>
);
expect(screen.queryByTestId("info-outline-icon")).not.toBeInTheDocument();
});
});
@@ -0,0 +1,18 @@
import React from "react";
import ActivityItem from "components/ActivityItem";
import { IHostActivityItemComponentProps } from "../../ActivityConfig";
const FailedToRotateManagedLocalAccountPasswordActivityItem = ({
activity,
}: IHostActivityItemComponentProps) => {
return (
<ActivityItem activity={activity} hideCancel hideShowDetails>
<b>Fleet </b>
failed to rotate the managed local account password for this host.
</ActivityItem>
);
};
export default FailedToRotateManagedLocalAccountPasswordActivityItem;
@@ -0,0 +1 @@
export { default } from "./FailedToRotateManagedLocalAccountPassword";
@@ -0,0 +1,74 @@
import React from "react";
import { render, screen } from "@testing-library/react";
import { createMockHostPastActivity } from "__mocks__/activityMock";
import { ActivityType } from "interfaces/activity";
import RotatedManagedLocalAccountPasswordActivityItem from "./RotatedManagedLocalAccountPassword";
describe("RotatedManagedLocalAccountPasswordActivityItem", () => {
it("renders user-triggered rotation activity content", () => {
render(
<RotatedManagedLocalAccountPasswordActivityItem
activity={createMockHostPastActivity({
actor_full_name: "Test User",
type: ActivityType.RotatedManagedLocalAccountPassword,
})}
tab="past"
/>
);
expect(screen.getByText("Test User")).toBeVisible();
expect(
screen.getByText(
/triggered rotation of the managed local account password/i
)
).toBeVisible();
});
it("renders Fleet-initiated (auto) rotation activity content", () => {
render(
<RotatedManagedLocalAccountPasswordActivityItem
activity={createMockHostPastActivity({
actor_full_name: "Fleet",
type: ActivityType.RotatedManagedLocalAccountPassword,
})}
tab="past"
/>
);
expect(screen.getByText("Fleet")).toBeVisible();
expect(
screen.getByText(
/triggered rotation of the managed local account password/i
)
).toBeVisible();
});
it("does not render the cancel icon", () => {
render(
<RotatedManagedLocalAccountPasswordActivityItem
activity={createMockHostPastActivity({
actor_full_name: "Test User",
type: ActivityType.RotatedManagedLocalAccountPassword,
})}
tab="past"
/>
);
expect(screen.queryByTestId("close-icon")).not.toBeInTheDocument();
});
it("does not render the show details icon", () => {
render(
<RotatedManagedLocalAccountPasswordActivityItem
activity={createMockHostPastActivity({
actor_full_name: "Test User",
type: ActivityType.RotatedManagedLocalAccountPassword,
})}
tab="past"
/>
);
expect(screen.queryByTestId("info-outline-icon")).not.toBeInTheDocument();
});
});
@@ -0,0 +1,18 @@
import React from "react";
import ActivityItem from "components/ActivityItem";
import { IHostActivityItemComponentProps } from "../../ActivityConfig";
const RotatedManagedLocalAccountPasswordActivityItem = ({
activity,
}: IHostActivityItemComponentProps) => {
return (
<ActivityItem activity={activity} hideCancel hideShowDetails>
<b>{activity.actor_full_name} </b>
triggered rotation of the managed local account password for this host.
</ActivityItem>
);
};
export default RotatedManagedLocalAccountPasswordActivityItem;
@@ -0,0 +1 @@
export { default } from "./RotatedManagedLocalAccountPassword";
+5
View File
@@ -676,6 +676,11 @@ export default {
return sendRequest("GET", HOST_MANAGED_ACCOUNT_PASSWORD(id));
},
rotateManagedLocalAccountPassword: (id: number): Promise<void> => {
const { HOST_MANAGED_LOCAL_ACCOUNT_ROTATE } = endpoints;
return sendRequest("POST", HOST_MANAGED_LOCAL_ACCOUNT_ROTATE(id));
},
lockHost: (id: number) => {
const { HOST_LOCK } = endpoints;
return sendRequest("POST", HOST_LOCK(id));
+2
View File
@@ -204,6 +204,8 @@ export default {
`/${API_VERSION}/fleet/hosts/${id}/recovery_lock_password/rotate`,
HOST_MANAGED_ACCOUNT_PASSWORD: (id: number) =>
`/${API_VERSION}/fleet/hosts/${id}/managed_account_password`,
HOST_MANAGED_LOCAL_ACCOUNT_ROTATE: (id: number) =>
`/${API_VERSION}/fleet/hosts/${id}/managed_account_password/rotate`,
ME: `/${API_VERSION}/fleet/me`,