diff --git a/changes/37556-resend-android-certs b/changes/37556-resend-android-certs new file mode 100644 index 0000000000..6da526f6af --- /dev/null +++ b/changes/37556-resend-android-certs @@ -0,0 +1 @@ +- Added ability to re-send Android certificates to a specific host diff --git a/frontend/interfaces/activity.ts b/frontend/interfaces/activity.ts index e278bfea0e..110432f58a 100644 --- a/frontend/interfaces/activity.ts +++ b/frontend/interfaces/activity.ts @@ -85,6 +85,7 @@ export enum ActivityType { DeletedAndroidProfile = "deleted_android_profile", EditedAndroidProfile = "edited_android_profile", EditedAndroidCertificate = "edited_android_certificate", + ResentCertificate = "resent_certificate", // Note: Both "enabled_disk_encryption" and "enabled_macos_disk_encryption" display the same // message. The latter is deprecated in the API but it is retained here for backwards compatibility. EnabledDiskEncryption = "enabled_disk_encryption", @@ -286,6 +287,8 @@ export interface IActivityDetails { host_idp_username?: string; idp_full_name?: string; tenant_id?: string; + certificate_name?: string; + certificate_template_id?: number; } // maps activity types to their corresponding label to use when filtering activites via the dropdown @@ -443,6 +446,7 @@ export const ACTIVITY_TYPE_TO_FILTER_LABEL: Record = { "GitOps: edited configuration profiles: Android", [ActivityType.EditedAndroidCertificate]: "GitOps: edited certificate templates: Android", + [ActivityType.ResentCertificate]: "Resent certificate", [ActivityType.AddedConditionalAccessOkta]: "Added conditional access: Okta", [ActivityType.HostBypassedConditionalAccess]: "Host bypassed conditional access", diff --git a/frontend/interfaces/mdm.ts b/frontend/interfaces/mdm.ts index c11ef71a9b..1c2b2b5f26 100644 --- a/frontend/interfaces/mdm.ts +++ b/frontend/interfaces/mdm.ts @@ -194,6 +194,8 @@ export interface IHostMdmProfile { detail: string; scope: ProfileScope | null; managed_local_account: string | null; + // identifier when this profile represents an Android certificate template + certificate_template_id?: number; } // TODO - move disk encryption related types to dedicated file diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx index 8bd94f8171..8258da1199 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/GlobalActivityItem/GlobalActivityItem.tsx @@ -646,6 +646,15 @@ const TAGGED_TEMPLATES = { ); }, + resentCertificate: (activity: IActivity) => { + return ( + <> + {" "} + resent {activity.details?.certificate_name} certificate for host{" "} + {activity.details?.host_display_name}. + + ); + }, addedCertificateAuthority: (name = "") => { return name ? ( <> @@ -1902,6 +1911,9 @@ const getDetail = (activity: IActivity, isPremiumTier: boolean) => { case ActivityType.EditedAndroidCertificate: { return TAGGED_TEMPLATES.editedAndroidCertificate(activity, isPremiumTier); } + case ActivityType.ResentCertificate: { + return TAGGED_TEMPLATES.resentCertificate(activity); + } case ActivityType.AddedNdesScepProxy: { return TAGGED_TEMPLATES.addedCertificateAuthority("NDES"); } diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx index ffd6cda804..9a04894a3f 100644 --- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx +++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx @@ -740,13 +740,23 @@ const HostDetailsPage = ({ const resendProfile = useCallback( (profileUUID: string): Promise => { if (!host?.id) { - return new Promise(() => undefined); + return Promise.resolve(); } return hostAPI.resendProfile(host.id, profileUUID); }, [host?.id] ); + const resendCertificate = useCallback( + (certificateTemplateId: number): Promise => { + if (!host?.id) { + return Promise.resolve(); + } + return hostAPI.resendCertificate(host.id, certificateTemplateId); + }, + [host?.id] + ); + const rotateRecoveryLockPassword = useCallback((): Promise => { if (!host?.id) { return new Promise(() => undefined); @@ -1125,7 +1135,7 @@ const HostDetailsPage = ({ const isAppleDeviceHost = isAppleDevice(host.platform); const canResendProfiles = - (isAppleDeviceHost || isWindowsHost) && + (isAppleDeviceHost || isWindowsHost || isAndroidHost) && (isGlobalAdmin || isGlobalMaintainer || isGlobalTechnician || @@ -1549,6 +1559,7 @@ const HostDetailsPage = ({ hostMDMData={host.mdm} onClose={toggleOSSettingsModal} resendRequest={resendProfile} + resendCertificateRequest={resendCertificate} rotateRecoveryLockPassword={rotateRecoveryLockPassword} onProfileResent={refetchHostDetails} /> diff --git a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsModal.tsx b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsModal.tsx index 254465b5cf..bdefa983e5 100644 --- a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsModal.tsx +++ b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsModal.tsx @@ -17,6 +17,7 @@ interface IOSSettingsModalProps { * 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; + resendCertificateRequest?: (certificateTemplateId: number) => Promise; rotateRecoveryLockPassword?: () => Promise; onClose: () => void; /** handler that fires when a profile was reset. Requires `canResendProfiles` prop @@ -34,6 +35,7 @@ const OSSettingsModal = ({ canRotateRecoveryLockPassword = false, onClose, resendRequest, + resendCertificateRequest, rotateRecoveryLockPassword, onProfileResent, }: IOSSettingsModalProps) => { @@ -57,6 +59,7 @@ const OSSettingsModal = ({ canRotateRecoveryLockPassword={canRotateRecoveryLockPassword} tableData={memoizedTableData ?? []} resendRequest={resendRequest} + resendCertificateRequest={resendCertificateRequest} rotateRecoveryLockPassword={rotateRecoveryLockPassword} onProfileResent={onProfileResent} /> diff --git a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsErrorCell/OSSettingsErrorCell.tsx b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsErrorCell/OSSettingsErrorCell.tsx index eea0592963..2096f7d726 100644 --- a/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsErrorCell/OSSettingsErrorCell.tsx +++ b/frontend/pages/hosts/details/OSSettingsModal/OSSettingsTable/OSSettingsErrorCell/OSSettingsErrorCell.tsx @@ -6,7 +6,10 @@ import { REC_LOCK_SYNTHETIC_PROFILE_UUID } from "pages/hosts/details/helpers"; import { DEFAULT_EMPTY_CELL_VALUE } from "utilities/constants"; import { NotificationContext } from "context/notification"; -import { IHostMdmProfile } from "interfaces/mdm"; +import { + FLEET_ANDROID_CERTIFICATE_TEMPLATE_PROFILE_ID, + IHostMdmProfile, +} from "interfaces/mdm"; import { getErrorReason } from "interfaces/errors"; import TooltipTruncatedTextCell from "components/TableContainer/DataTable/TooltipTruncatedTextCell"; @@ -18,23 +21,23 @@ import { IHostMdmProfileWithAddedStatus } from "../OSSettingsTableConfig"; const baseClass = "os-settings-error-cell"; -interface IRefetchButtonProps { - isFetching: boolean; +interface IResendButtonProps { + isResending: boolean; onClick: (evt: React.MouseEvent) => void; } -const RefetchButton = ({ isFetching, onClick }: IRefetchButtonProps) => { +const ResendButton = ({ isResending, onClick }: IResendButtonProps) => { const classNames = classnames(`${baseClass}__resend-button`, "resend-link", { - [`${baseClass}__resending`]: isFetching, + [`${baseClass}__resending`]: isResending, }); - const buttonText = isFetching ? "Resending..." : "Resend"; + const buttonText = isResending ? "Resending..." : "Resend"; // add additional props when we need to display a tooltip for the button return (