Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
b02a2d1
Improve filtering on commands endpoints
JordanMontgomery Apr 29, 2026
0cb9e1e
Initial revs for password rotation
JordanMontgomery Apr 30, 2026
59c8c4c
Merge branch 'main' into JM-43887
JordanMontgomery Apr 30, 2026
cdd353f
Add managed account rotation frontend changes
JordanMontgomery Apr 30, 2026
b1018d6
Show password while pending
JordanMontgomery May 1, 2026
5f2e183
Fix timestamps during rotation
JordanMontgomery May 4, 2026
9904043
Merge branch 'JM-43887' into JM-43890
JordanMontgomery May 4, 2026
6d8b3b1
Add failed activity
JordanMontgomery May 4, 2026
010414f
Add rotation in-flight markers to the main endpoint as well
JordanMontgomery May 4, 2026
4ba4431
Update frontend behaviors around pending passwords
JordanMontgomery May 5, 2026
1f3da0d
Merge branch 'main' into JM-43887
JordanMontgomery May 5, 2026
5fe76d5
Merge branch 'JM-43887' into JM-43890
JordanMontgomery May 5, 2026
4010fe2
Update errors
JordanMontgomery May 5, 2026
6e3c26d
Update another error
JordanMontgomery May 5, 2026
ebbab3a
Cleanup comments
JordanMontgomery May 5, 2026
66cf5a9
Fix CI
JordanMontgomery May 5, 2026
1295ddf
Merge branch 'JM-43887' into JM-43890
JordanMontgomery May 5, 2026
96f2de7
Fix build
JordanMontgomery May 5, 2026
99e558c
Fix modal error messages
JordanMontgomery May 5, 2026
20199f8
Fix duplicate activities
JordanMontgomery May 5, 2026
cb8c413
Update comments
JordanMontgomery May 5, 2026
6175f03
Merge branch 'JM-43887' into JM-43890
JordanMontgomery May 5, 2026
2e1fb17
Update a couple comments
JordanMontgomery May 5, 2026
69038b4
Merge branch 'JM-43887' into JM-43890
JordanMontgomery May 5, 2026
173e375
Fix AI review feedback
JordanMontgomery May 5, 2026
972acdd
enhance tests slightly
JordanMontgomery May 5, 2026
9999a95
Address review feedback
JordanMontgomery May 6, 2026
fd4616b
Use a single helper for enqueueing managed account password rotations
JordanMontgomery May 6, 2026
54910c3
Update per API doc changes
JordanMontgomery May 6, 2026
3a28e0a
Fix AI review feedback
JordanMontgomery May 5, 2026
25e8442
enhance tests slightly
JordanMontgomery May 5, 2026
e99d1e4
Address review feedback
JordanMontgomery May 6, 2026
fb46f6e
Use a single helper for enqueueing managed account password rotations
JordanMontgomery May 6, 2026
0e344bb
Merge branch 'JM-43887' into JM-43890
JordanMontgomery May 6, 2026
cc54946
Update frontend endpoint for rotation
JordanMontgomery May 6, 2026
7e34da4
Update TODO comments
JordanMontgomery May 6, 2026
5cb1acf
Merge branch 'main' into JM-43890
JordanMontgomery May 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions frontend/interfaces/activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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",
Expand Down
4 changes: 4 additions & 0 deletions frontend/interfaces/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
}
Expand Down Expand Up @@ -268,6 +270,8 @@ export interface IHostManagedAccountPasswordResponse {
username: string;
password: string;
updated_at: string;
auto_rotate_at?: string;
pending_rotation?: boolean;
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ interface IHostActionsDropdownProps {
recoveryLockPasswordAvailable?: boolean;
isManagedLocalAccountEnabled?: boolean;
managedAccountStatus?: string | null;
managedAccountPasswordAvailable?: boolean;
}

const HostActionsDropdown = ({
Expand All @@ -46,6 +47,7 @@ const HostActionsDropdown = ({
recoveryLockPasswordAvailable = false,
isManagedLocalAccountEnabled = false,
managedAccountStatus,
managedAccountPasswordAvailable = false,
}: IHostActionsDropdownProps) => {
const {
isPremiumTier = false,
Expand Down Expand Up @@ -102,6 +104,7 @@ const HostActionsDropdown = ({
recoveryLockPasswordAvailable,
isManagedLocalAccountEnabled,
managedAccountStatus,
managedAccountPasswordAvailable,
});

// No options to render. Exit early
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ interface IHostActionConfigOptions {
recoveryLockPasswordAvailable: boolean;
isManagedLocalAccountEnabled: boolean;
managedAccountStatus: string | null | undefined;
managedAccountPasswordAvailable: boolean;
}

const canTransferTeam = (config: IHostActionConfigOptions) => {
Expand Down Expand Up @@ -523,6 +524,7 @@ const modifyOptions = (
diskEncryptionProfileStatus,
recoveryLockPasswordAvailable,
managedAccountStatus,
managedAccountPasswordAvailable,
}: IHostActionConfigOptions
) => {
const disableOptions = (optionsToDisable: IDropdownOption[]) => {
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
/>
);
};
Expand Down Expand Up @@ -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();
}}
/>
Expand Down
Loading
Loading