diff --git a/frontend/interfaces/activity.ts b/frontend/interfaces/activity.ts index 7acc0cd687f..1dd1c6bd970 100644 --- a/frontend/interfaces/activity.ts +++ b/frontend/interfaces/activity.ts @@ -161,6 +161,11 @@ export enum ActivityType { AddedMicrosoftEntraTenant = "added_microsoft_entra_tenant", DeletedMicrosoftEntraTenant = "deleted_microsoft_entra_tenant", ClearedPasscode = "cleared_passcode", + EnabledManagedLocalAccount = "enabled_managed_local_account", + DisabledManagedLocalAccount = "disabled_managed_local_account", + ReadManagedLocalAccount = "read_managed_local_account", + CreatedManagedLocalAccount = "created_managed_local_account", + RotatedManagedLocalAccountPassword = "rotated_managed_local_account_password", } /** This is a subset of ActivityType that are shown only for the host past activities */ @@ -182,7 +187,10 @@ export type IHostPastActivityType = | ActivityType.CanceledUninstallSoftware | ActivityType.InstalledCertificate | ActivityType.ResentCertificate - | ActivityType.ClearedPasscode; + | ActivityType.ClearedPasscode + | ActivityType.ReadManagedLocalAccount + | ActivityType.CreatedManagedLocalAccount + | ActivityType.RotatedManagedLocalAccountPassword; /** This is a subset of ActivityType that are shown only for the host upcoming activities */ export type IHostUpcomingActivityType = @@ -462,4 +470,11 @@ export const ACTIVITY_TYPE_TO_FILTER_LABEL: Record = { [ActivityType.InstalledCertificate]: "Installed certificate", [ActivityType.EditedEnrollSecrets]: "Edited enroll secrets", [ActivityType.ClearedPasscode]: "Cleared passcode", + [ActivityType.EnabledManagedLocalAccount]: "Turned on managed local account", + [ActivityType.DisabledManagedLocalAccount]: + "Turned off managed local account", + [ActivityType.ReadManagedLocalAccount]: "Viewed managed account", + [ActivityType.CreatedManagedLocalAccount]: "Created managed account", + [ActivityType.RotatedManagedLocalAccountPassword]: + "Rotated managed account password", }; diff --git a/frontend/interfaces/config.ts b/frontend/interfaces/config.ts index bacbb297165..706b5fbd342 100644 --- a/frontend/interfaces/config.ts +++ b/frontend/interfaces/config.ts @@ -83,6 +83,7 @@ export interface IMdmConfig { macos_manual_agent_install: boolean | null; require_all_software_macos: boolean | null; lock_end_user_info: boolean | null; + enable_managed_local_account?: boolean; }; macos_migration: IMacOsMigrationSettings; windows_updates: { diff --git a/frontend/interfaces/host.ts b/frontend/interfaces/host.ts index 3d9bede6f02..984616b2f9e 100644 --- a/frontend/interfaces/host.ts +++ b/frontend/interfaces/host.ts @@ -258,6 +258,14 @@ export interface IHostRecoveryLockPasswordResponse { }; } +export interface IHostManagedAccountPasswordResponse { + host_id: number; + managed_account_password: { + password: string; + updated_at: string; + }; +} + export interface IHostIssues { total_issues_count: number; critical_vulnerabilities_count?: number; // Premium diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx index e325b9b91bd..54a067ade80 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx @@ -512,6 +512,63 @@ const TAGGED_TEMPLATES = { ); }, + enabledManagedLocalAccount: (activity: IActivity) => { + return ( + <> + {" "} + turned on managed local account for{" "} + {activity.details?.team_name ? ( + <> + {activity.details.team_name} fleet. + + ) : ( + "hosts with no fleet." + )} + + ); + }, + disabledManagedLocalAccount: (activity: IActivity) => { + return ( + <> + {" "} + turned off managed local account for{" "} + {activity.details?.team_name ? ( + <> + {activity.details.team_name} fleet. + + ) : ( + "hosts with no fleet." + )} + + ); + }, + readManagedLocalAccount: (activity: IActivity) => { + return ( + <> + {" "} + viewed the managed account for{" "} + {activity.details?.host_display_name}. + + ); + }, + createdManagedLocalAccount: (activity: IActivity) => { + return ( + <> + {" "} + created the managed account for{" "} + {activity.details?.host_display_name}. + + ); + }, + rotatedManagedLocalAccountPassword: (activity: IActivity) => { + return ( + <> + {" "} + rotated the managed account password for{" "} + {activity.details?.host_display_name}. + + ); + }, createdAppleOSProfile: (activity: IActivity, isPremiumTier: boolean) => { const profileName = activity.details?.profile_name; return ( @@ -1897,6 +1954,21 @@ const getDetail = (activity: IActivity, isPremiumTier: boolean) => { case ActivityType.RotatedHostRecoveryLockPassword: { return TAGGED_TEMPLATES.rotatedHostRecoveryLockPassword(activity); } + case ActivityType.EnabledManagedLocalAccount: { + return TAGGED_TEMPLATES.enabledManagedLocalAccount(activity); + } + case ActivityType.DisabledManagedLocalAccount: { + return TAGGED_TEMPLATES.disabledManagedLocalAccount(activity); + } + case ActivityType.ReadManagedLocalAccount: { + return TAGGED_TEMPLATES.readManagedLocalAccount(activity); + } + case ActivityType.CreatedManagedLocalAccount: { + return TAGGED_TEMPLATES.createdManagedLocalAccount(activity); + } + case ActivityType.RotatedManagedLocalAccountPassword: { + return TAGGED_TEMPLATES.rotatedManagedLocalAccountPassword(activity); + } case ActivityType.CreatedAppleOSProfile: { return TAGGED_TEMPLATES.createdAppleOSProfile(activity, isPremiumTier); } diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx index 74eefa80560..25aef8785f9 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/HostActionsDropdown.tsx @@ -26,6 +26,7 @@ interface IHostActionsDropdownProps { isRecoveryLockPasswordEnabled?: boolean; diskEncryptionProfileStatus?: string; recoveryLockPasswordAvailable?: boolean; + isManagedLocalAccountEnabled?: boolean; } const HostActionsDropdown = ({ @@ -42,6 +43,7 @@ const HostActionsDropdown = ({ isRecoveryLockPasswordEnabled = false, diskEncryptionProfileStatus, recoveryLockPasswordAvailable = false, + isManagedLocalAccountEnabled = false, }: IHostActionsDropdownProps) => { const { isPremiumTier = false, @@ -96,6 +98,7 @@ const HostActionsDropdown = ({ isRecoveryLockPasswordEnabled, diskEncryptionProfileStatus, recoveryLockPasswordAvailable, + isManagedLocalAccountEnabled, }); // No options to render. Exit early diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx index 21cc9c7d2a7..3d740d7207c 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostActionsDropdown/helpers.tsx @@ -49,6 +49,11 @@ const DEFAULT_OPTIONS = [ value: "recoveryLockPassword", disabled: false, }, + { + label: "Show managed account", + value: "managedAccount", + disabled: false, + }, { label: "Turn off MDM", value: "mdmOff", @@ -108,6 +113,7 @@ interface IHostActionConfigOptions { isRecoveryLockPasswordEnabled: boolean; diskEncryptionProfileStatus: string | undefined; recoveryLockPasswordAvailable: boolean; + isManagedLocalAccountEnabled: boolean; } const canTransferTeam = (config: IHostActionConfigOptions) => { @@ -317,6 +323,21 @@ const canShowRecoveryLockPassword = (config: IHostActionConfigOptions) => { return isRecoveryLockPasswordEnabled; }; +const canShowManagedAccount = (config: IHostActionConfigOptions) => { + const { + isPremiumTier, + isConnectedToFleetMdm, + isEnrolledInMdm, + hostPlatform, + isManagedLocalAccountEnabled, + } = config; + if (!isPremiumTier) return false; + if (hostPlatform !== "darwin") return false; + if (!isConnectedToFleetMdm) return false; + if (!isEnrolledInMdm) return false; + return isManagedLocalAccountEnabled; +}; + const canClearPasscode = (config: IHostActionConfigOptions) => { if (!config.isPremiumTier) { return false; @@ -398,6 +419,10 @@ const removeUnavailableOptions = ( ); } + if (!canShowManagedAccount(config)) { + options = options.filter((option) => option.value !== "managedAccount"); + } + if (!canClearPasscode(config)) { options = options.filter((option) => option.value !== "clearPasscode"); } diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index 70c2e987401..42b75abbd0e 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -117,6 +117,7 @@ import DeleteHostModal from "../../components/DeleteHostModal"; import UnenrollMdmModal from "./modals/UnenrollMdmModal"; import DiskEncryptionKeyModal from "./modals/DiskEncryptionKeyModal"; import RecoveryLockPasswordModal from "./modals/RecoveryLockPasswordModal"; +import ManagedAccountModal from "./modals/ManagedAccountModal"; import HostActionsDropdown from "./HostActionsDropdown/HostActionsDropdown"; import OSSettingsModal from "../OSSettingsModal"; import BootstrapPackageModal from "./modals/BootstrapPackageModal"; @@ -226,6 +227,7 @@ const HostDetailsPage = ({ showRecoveryLockPasswordModal, setShowRecoveryLockPasswordModal, ] = useState(false); + const [showManagedAccountModal, setShowManagedAccountModal] = useState(false); const [showBootstrapPackageModal, setShowBootstrapPackageModal] = useState( false ); @@ -948,6 +950,9 @@ const HostDetailsPage = ({ case "recoveryLockPassword": setShowRecoveryLockPasswordModal(true); break; + case "managedAccount": + setShowManagedAccountModal(true); + break; case "mdmOff": toggleUnenrollMdmModal(); break; @@ -1009,6 +1014,9 @@ const HostDetailsPage = ({ host.mdm.os_settings?.recovery_lock_password?.password_available ?? false } + isManagedLocalAccountEnabled={ + mdmConfig?.setup_experience?.enable_managed_local_account ?? false + } /> ); }; @@ -1611,6 +1619,12 @@ const HostDetailsPage = ({ onCancel={() => setShowRecoveryLockPasswordModal(false)} /> )} + {showManagedAccountModal && host && ( + setShowManagedAccountModal(false)} + /> + )} {showBootstrapPackageModal && bootstrapPackageData.details && bootstrapPackageData.name && ( diff --git a/frontend/pages/hosts/details/HostDetailsPage/modals/ManagedAccountModal/ManagedAccountModal.tsx b/frontend/pages/hosts/details/HostDetailsPage/modals/ManagedAccountModal/ManagedAccountModal.tsx new file mode 100644 index 00000000000..62cdfa7b9b0 --- /dev/null +++ b/frontend/pages/hosts/details/HostDetailsPage/modals/ManagedAccountModal/ManagedAccountModal.tsx @@ -0,0 +1,75 @@ +import React from "react"; +import { useQuery } from "react-query"; + +import { IHostManagedAccountPasswordResponse } from "interfaces/host"; +import hostAPI from "services/entities/hosts"; + +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 { DEFAULT_USE_QUERY_OPTIONS } from "utilities/constants"; + +const baseClass = "managed-account-modal"; + +interface IManagedAccountModalProps { + hostId: number; + onCancel: () => void; +} + +const ManagedAccountModal = ({ + hostId, + onCancel, +}: IManagedAccountModalProps) => { + const { + data: managedAccountData, + error: managedAccountError, + isLoading, + } = useQuery< + IHostManagedAccountPasswordResponse, + unknown, + IHostManagedAccountPasswordResponse["managed_account_password"] + >( + ["hostManagedAccountPassword", hostId], + () => hostAPI.getManagedAccountPassword(hostId), + { + ...DEFAULT_USE_QUERY_OPTIONS, + select: (data) => data.managed_account_password, + // prevent caching this sensitive string + cacheTime: 0, + } + ); + + return ( + + {isLoading && } + {managedAccountError ? ( + + ) : ( + !isLoading && ( + <> +
+ Username + _fleetadmin +
+ +
+ +
+ + ) + )} +
+ ); +}; + +export default ManagedAccountModal; diff --git a/frontend/pages/hosts/details/HostDetailsPage/modals/ManagedAccountModal/_styles.scss b/frontend/pages/hosts/details/HostDetailsPage/modals/ManagedAccountModal/_styles.scss new file mode 100644 index 00000000000..91bd92c32ed --- /dev/null +++ b/frontend/pages/hosts/details/HostDetailsPage/modals/ManagedAccountModal/_styles.scss @@ -0,0 +1,19 @@ +.managed-account-modal { + &__username { + display: flex; + flex-direction: column; + gap: 8px; + margin-bottom: 16px; + } + + &__label { + font-size: $x-small; + font-weight: $bold; + color: $core-fleet-black; + } + + &__value { + font-size: $x-small; + color: $ui-fleet-black-75; + } +} diff --git a/frontend/pages/hosts/details/HostDetailsPage/modals/ManagedAccountModal/index.ts b/frontend/pages/hosts/details/HostDetailsPage/modals/ManagedAccountModal/index.ts new file mode 100644 index 00000000000..e8715c81879 --- /dev/null +++ b/frontend/pages/hosts/details/HostDetailsPage/modals/ManagedAccountModal/index.ts @@ -0,0 +1 @@ +export { default } from "./ManagedAccountModal"; diff --git a/frontend/pages/hosts/details/cards/Activity/ActivityConfig.tsx b/frontend/pages/hosts/details/cards/Activity/ActivityConfig.tsx index ac3659f7734..fb80ac85709 100644 --- a/frontend/pages/hosts/details/cards/Activity/ActivityConfig.tsx +++ b/frontend/pages/hosts/details/cards/Activity/ActivityConfig.tsx @@ -25,6 +25,9 @@ import CanceledUninstallSoftwareActivtyItem from "./ActivityItems/CanceledUninst import InstalledCertificateActivityItem from "./ActivityItems/InstalledCertificateActivityItem"; import ResentCertificateActivityItem from "./ActivityItems/ResentCertificateActivityItem"; import ClearedPasscodeActivityItem from "./ActivityItems/ClearedPasscodeActivityItem"; +import ReadManagedLocalAccountActivityItem from "./ActivityItems/ReadManagedLocalAccountActivityItem/ReadManagedLocalAccountActivityItem"; +import CreatedManagedLocalAccountActivityItem from "./ActivityItems/CreatedManagedLocalAccountActivityItem/CreatedManagedLocalAccountActivityItem"; +import RotatedManagedLocalAccountPasswordActivityItem from "./ActivityItems/RotatedManagedLocalAccountPasswordActivityItem/RotatedManagedLocalAccountPasswordActivityItem"; /** The component props that all host activity items must adhere to */ export interface IHostActivityItemComponentProps { @@ -70,6 +73,9 @@ export const pastActivityComponentMap: Record< [ActivityType.InstalledCertificate]: InstalledCertificateActivityItem, [ActivityType.ResentCertificate]: ResentCertificateActivityItem, [ActivityType.ClearedPasscode]: ClearedPasscodeActivityItem, + [ActivityType.ReadManagedLocalAccount]: ReadManagedLocalAccountActivityItem, + [ActivityType.CreatedManagedLocalAccount]: CreatedManagedLocalAccountActivityItem, + [ActivityType.RotatedManagedLocalAccountPassword]: RotatedManagedLocalAccountPasswordActivityItem, }; export const upcomingActivityComponentMap: Record< diff --git a/frontend/pages/hosts/details/cards/Activity/ActivityItems/CreatedManagedLocalAccountActivityItem/CreatedManagedLocalAccountActivityItem.tsx b/frontend/pages/hosts/details/cards/Activity/ActivityItems/CreatedManagedLocalAccountActivityItem/CreatedManagedLocalAccountActivityItem.tsx new file mode 100644 index 00000000000..b6e3e4b0667 --- /dev/null +++ b/frontend/pages/hosts/details/cards/Activity/ActivityItems/CreatedManagedLocalAccountActivityItem/CreatedManagedLocalAccountActivityItem.tsx @@ -0,0 +1,18 @@ +import React from "react"; + +import ActivityItem from "components/ActivityItem"; + +import { IHostActivityItemComponentProps } from "../../ActivityConfig"; + +const CreatedManagedLocalAccountActivityItem = ({ + activity, +}: IHostActivityItemComponentProps) => { + return ( + + Fleet + created the managed account for this host. + + ); +}; + +export default CreatedManagedLocalAccountActivityItem; diff --git a/frontend/pages/hosts/details/cards/Activity/ActivityItems/ReadManagedLocalAccountActivityItem/ReadManagedLocalAccountActivityItem.tsx b/frontend/pages/hosts/details/cards/Activity/ActivityItems/ReadManagedLocalAccountActivityItem/ReadManagedLocalAccountActivityItem.tsx new file mode 100644 index 00000000000..be62e28380c --- /dev/null +++ b/frontend/pages/hosts/details/cards/Activity/ActivityItems/ReadManagedLocalAccountActivityItem/ReadManagedLocalAccountActivityItem.tsx @@ -0,0 +1,18 @@ +import React from "react"; + +import ActivityItem from "components/ActivityItem"; + +import { IHostActivityItemComponentProps } from "../../ActivityConfig"; + +const ReadManagedLocalAccountActivityItem = ({ + activity, +}: IHostActivityItemComponentProps) => { + return ( + + {activity.actor_full_name} + viewed the managed account for this host. + + ); +}; + +export default ReadManagedLocalAccountActivityItem; diff --git a/frontend/pages/hosts/details/cards/Activity/ActivityItems/RotatedManagedLocalAccountPasswordActivityItem/RotatedManagedLocalAccountPasswordActivityItem.tsx b/frontend/pages/hosts/details/cards/Activity/ActivityItems/RotatedManagedLocalAccountPasswordActivityItem/RotatedManagedLocalAccountPasswordActivityItem.tsx new file mode 100644 index 00000000000..d8a228f02e7 --- /dev/null +++ b/frontend/pages/hosts/details/cards/Activity/ActivityItems/RotatedManagedLocalAccountPasswordActivityItem/RotatedManagedLocalAccountPasswordActivityItem.tsx @@ -0,0 +1,18 @@ +import React from "react"; + +import ActivityItem from "components/ActivityItem"; + +import { IHostActivityItemComponentProps } from "../../ActivityConfig"; + +const RotatedManagedLocalAccountPasswordActivityItem = ({ + activity, +}: IHostActivityItemComponentProps) => { + return ( + + Fleet + rotated the managed account password for this host. + + ); +}; + +export default RotatedManagedLocalAccountPasswordActivityItem; diff --git a/frontend/services/entities/hosts.ts b/frontend/services/entities/hosts.ts index f09e4e6731b..8d760528382 100644 --- a/frontend/services/entities/hosts.ts +++ b/frontend/services/entities/hosts.ts @@ -671,6 +671,11 @@ export default { return sendRequest("POST", HOST_RECOVERY_LOCK_PASSWORD_ROTATE(id)); }, + getManagedAccountPassword: (id: number) => { + const { HOST_MANAGED_ACCOUNT_PASSWORD } = endpoints; + return sendRequest("GET", HOST_MANAGED_ACCOUNT_PASSWORD(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 bedb4a376c7..5719f72b489 100644 --- a/frontend/utilities/endpoints.ts +++ b/frontend/utilities/endpoints.ts @@ -197,6 +197,8 @@ export default { `/${API_VERSION}/fleet/hosts/${id}/recovery_lock_password`, HOST_RECOVERY_LOCK_PASSWORD_ROTATE: (id: number) => `/${API_VERSION}/fleet/hosts/${id}/recovery_lock_password/rotate`, + HOST_MANAGED_ACCOUNT_PASSWORD: (id: number) => + `/${API_VERSION}/fleet/hosts/${id}/managed_account_password`, ME: `/${API_VERSION}/fleet/me`,