Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions changes/37546-android-certificate-install-activity
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
* Added activity logging when a certificate is installed or fails to install on an Android host.
* Enabled the host activity card on the Android host details page.
6 changes: 5 additions & 1 deletion frontend/interfaces/activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ export enum ActivityType {
EditedHostIdpData = "edited_host_idp_data",
AddedCertificate = "added_certificate",
DeletedCertificate = "deleted_certificate",
InstalledCertificate = "installed_certificate",
EditedEnrollSecrets = "edited_enroll_secrets",
AddedMicrosoftEntraTenant = "added_microsoft_entra_tenant",
DeletedMicrosoftEntraTenant = "deleted_microsoft_entra_tenant",
Expand All @@ -181,7 +182,8 @@ export type IHostPastActivityType =
| ActivityType.CanceledRunScript
| ActivityType.CanceledInstallAppStoreApp
| ActivityType.CanceledInstallSoftware
| ActivityType.CanceledUninstallSoftware;
| ActivityType.CanceledUninstallSoftware
| ActivityType.InstalledCertificate;

/** This is a subset of ActivityType that are shown only for the host upcoming activities */
export type IHostUpcomingActivityType =
Expand Down Expand Up @@ -289,6 +291,7 @@ export interface IActivityDetails {
tenant_id?: string;
certificate_name?: string;
certificate_template_id?: number;
detail?: string;
}

// maps activity types to their corresponding label to use when filtering activites via the dropdown
Expand Down Expand Up @@ -459,5 +462,6 @@ export const ACTIVITY_TYPE_TO_FILTER_LABEL: Record<ActivityType, string> = {
[ActivityType.EditedHostIdpData]: "Edited host identity provider (IdP) data",
[ActivityType.AddedCertificate]: "Added certificate",
[ActivityType.DeletedCertificate]: "Deleted certificate",
[ActivityType.InstalledCertificate]: "Installed certificate",
[ActivityType.EditedEnrollSecrets]: "Edited enroll secrets",
};
120 changes: 56 additions & 64 deletions frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1151,7 +1151,6 @@ const HostDetailsPage = ({

const showSoftwareLibraryTab = isPremiumTier;
const showReportsTab = mdm?.enrollment_status !== "Pending";
const showActivityCard = !isAndroidHost;
const showAgentOptionsCard = !isIosOrIpadosHost && !isAndroidHost;
const showLocalUserAccountsCard = !isIosOrIpadosHost && !isAndroidHost;
const showCertificatesCard =
Expand Down Expand Up @@ -1354,64 +1353,61 @@ const HostDetailsPage = ({
toggleLocationModal={toggleLocationModal}
toggleMDMStatusModal={toggleMDMStatusModal}
/>
{showActivityCard && (
<ActivityCard
className={
showAgentOptionsCard
? tripleHeightCardClass
: defaultCardClass
}
activeTab={activeActivityTab}
activities={
activeActivityTab === "past"
? pastActivities
: upcomingActivities
}
commands={
activeActivityTab === "past"
? pastMDMCommands
: upcomingMDMCommands
}
isLoading={
activeActivityTab === "past"
? pastActivitiesIsFetching || pastMDMCommandsIsFetching
: upcomingActivitiesIsFetching ||
upcomingMDMCommandsIsFetching
}
isError={
activeActivityTab === "past"
? pastActivitiesIsError || pastMDMCommandsIsError
: upcomingActivitiesIsError ||
upcomingMDMCommandsIsError
}
canCancelActivities={
isGlobalAdmin ||
isGlobalMaintainer ||
isHostTeamAdmin ||
isHostTeamMaintainer
}
showMDMCommandsToggle={canGetMDMCommands}
showMDMCommands={showMDMCommands}
onShowMDMCommands={() => {
setActivityPage(0);
setShowMDMCommands(true);
}}
onHideMDMCommands={() => {
setActivityPage(0);
setShowMDMCommands(false);
}}
upcomingCount={
(upcomingActivities?.count || 0) +
(upcomingMDMCommands?.count || 0)
}
onChangeTab={onChangeActivityTab}
onNextPage={() => setActivityPage(activityPage + 1)}
onPreviousPage={() => setActivityPage(activityPage - 1)}
onShowDetails={onShowActivityDetails}
onShowCommandDetails={setMdmCommandDetails}
onCancel={onCancelActivity}
/>
)}
<ActivityCard
className={
showAgentOptionsCard
? tripleHeightCardClass
: defaultCardClass
}
activeTab={activeActivityTab}
activities={
activeActivityTab === "past"
? pastActivities
: upcomingActivities
}
commands={
activeActivityTab === "past"
? pastMDMCommands
: upcomingMDMCommands
}
isLoading={
activeActivityTab === "past"
? pastActivitiesIsFetching || pastMDMCommandsIsFetching
: upcomingActivitiesIsError ||
upcomingMDMCommandsIsFetching
}
isError={
activeActivityTab === "past"
? pastActivitiesIsError || pastMDMCommandsIsError
: upcomingActivitiesIsError || upcomingMDMCommandsIsError
}
canCancelActivities={
isGlobalAdmin ||
isGlobalMaintainer ||
isHostTeamAdmin ||
isHostTeamMaintainer
}
showMDMCommandsToggle={canGetMDMCommands}
showMDMCommands={showMDMCommands}
onShowMDMCommands={() => {
setActivityPage(0);
setShowMDMCommands(true);
}}
onHideMDMCommands={() => {
setActivityPage(0);
setShowMDMCommands(false);
}}
upcomingCount={
(upcomingActivities?.count || 0) +
(upcomingMDMCommands?.count || 0)
}
onChangeTab={onChangeActivityTab}
onNextPage={() => setActivityPage(activityPage + 1)}
onPreviousPage={() => setActivityPage(activityPage - 1)}
onShowDetails={onShowActivityDetails}
onShowCommandDetails={setMdmCommandDetails}
onCancel={onCancelActivity}
/>
<UserCard
className={defaultCardClass}
endUsers={host.end_users ?? []}
Expand All @@ -1430,11 +1426,7 @@ const HostDetailsPage = ({
}}
/>
<LabelsCard
className={
!showActivityCard && !showAgentOptionsCard
? fullWidthCardClass
: defaultCardClass
}
className={defaultCardClass}
labels={host?.labels || []}
onLabelClick={onLabelClick}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import InstalledSoftwareActivityItem from "./ActivityItems/InstalledSoftwareActi
import CanceledRunScriptActivityItem from "./ActivityItems/CanceledRunScriptActivityItem";
import CanceledInstallSoftwareActivityItem from "./ActivityItems/CanceledInstallSoftwareActivityItem";
import CanceledUninstallSoftwareActivtyItem from "./ActivityItems/CanceledUninstallSoftwareActivtyItem";
import InstalledCertificateActivityItem from "./ActivityItems/InstalledCertificateActivityItem";

/** The component props that all host activity items must adhere to */
export interface IHostActivityItemComponentProps {
Expand Down Expand Up @@ -64,6 +65,7 @@ export const pastActivityComponentMap: Record<
[ActivityType.CanceledInstallSoftware]: CanceledInstallSoftwareActivityItem,
[ActivityType.CanceledInstallAppStoreApp]: CanceledInstallSoftwareActivityItem,
[ActivityType.CanceledUninstallSoftware]: CanceledUninstallSoftwareActivtyItem,
[ActivityType.InstalledCertificate]: InstalledCertificateActivityItem,
};

export const upcomingActivityComponentMap: Record<
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from "react";

import ActivityItem from "components/ActivityItem";

import { IHostActivityItemComponentProps } from "../../ActivityConfig";

const baseClass = "installed-certificate-activity-item";

const InstalledCertificateActivityItem = ({
activity,
}: IHostActivityItemComponentProps) => {
const isFailed = activity.details?.status === "failed_install";

return (
<ActivityItem
className={baseClass}
activity={activity}
hideCancel
hideShowDetails
>
{isFailed ? (
<>
<b>Fleet</b> failed to install certificate{" "}
<b>{activity.details?.certificate_name}</b> on this host.
{activity.details?.detail && <> Detail: {activity.details.detail}</>}
</>
) : (
<>
<b>Fleet</b> installed certificate{" "}
<b>{activity.details?.certificate_name}</b> on this host.
</>
)}
</ActivityItem>
);
};

export default InstalledCertificateActivityItem;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from "./InstalledCertificateActivityItem";
26 changes: 26 additions & 0 deletions server/fleet/activities.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ var ActivityDetailsList = []ActivityDetails{
ActivityTypeEditedAndroidProfile{},
ActivityTypeEditedAndroidCertificate{},
ActivityTypeResentCertificate{},
ActivityTypeInstalledCertificate{},

ActivityTypeResentConfigurationProfile{},
ActivityTypeResentConfigurationProfileBatch{},
Expand Down Expand Up @@ -1826,3 +1827,28 @@ type ActivityTypeEditedEnrollSecrets struct {
func (a ActivityTypeEditedEnrollSecrets) ActivityName() string {
return "edited_enroll_secrets"
}

type ActivityTypeInstalledCertificate struct {
HostID uint `json:"host_id"`
HostDisplayName string `json:"host_display_name"`
CertificateTemplateID uint `json:"certificate_template_id"`
CertificateName string `json:"certificate_name"`
Status string `json:"status"`
Detail string `json:"detail,omitempty"`
}

func (a ActivityTypeInstalledCertificate) ActivityName() string {
return "installed_certificate"
}

func (a ActivityTypeInstalledCertificate) HostIDs() []uint {
return []uint{a.HostID}
}

func (a ActivityTypeInstalledCertificate) WasFromAutomation() bool {
return true
}

func (a ActivityTypeInstalledCertificate) HostOnly() bool {
return true
}
8 changes: 8 additions & 0 deletions server/fleet/certificate_templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ var (
CertificateTemplateVerified CertificateTemplateStatus = "verified"
)

// CertificateActivityStatus represents the status of a certificate install activity.
type CertificateActivityStatus string

const (
CertificateActivityInstalled CertificateActivityStatus = "installed"
CertificateActivityFailedInstall CertificateActivityStatus = "failed_install"
)

// CertificateTemplateStatusToMDMDeliveryStatus converts a CertificateTemplateStatus to MDMDeliveryStatus.
// This is used when converting HostCertificateTemplate to HostMDMProfile for the GetHost endpoint.
func CertificateTemplateStatusToMDMDeliveryStatus(s CertificateTemplateStatus) MDMDeliveryStatus {
Expand Down
36 changes: 35 additions & 1 deletion server/service/certificates.go
Original file line number Diff line number Diff line change
Expand Up @@ -695,7 +695,41 @@ func (svc *Service) UpdateCertificateStatus(ctx context.Context, update *fleet.C

// Fill in HostUUID from context
update.HostUUID = host.UUID
return svc.ds.UpsertCertificateStatus(ctx, update)
if err := svc.ds.UpsertCertificateStatus(ctx, update); err != nil {
return err
}

// Log activity for terminal install statuses only (not removals).
if update.OperationType == fleet.MDMOperationTypeInstall {
var actStatus fleet.CertificateActivityStatus
switch update.Status {
case fleet.MDMDeliveryVerified:
actStatus = fleet.CertificateActivityInstalled
case fleet.MDMDeliveryFailed:
actStatus = fleet.CertificateActivityFailedInstall
}
if actStatus != "" {
detail := ""
if update.Detail != nil {
detail = *update.Detail
}
if err := svc.NewActivity(ctx, nil, fleet.ActivityTypeInstalledCertificate{
HostID: host.ID,
HostDisplayName: host.DisplayName(),
CertificateTemplateID: update.CertificateTemplateID,
CertificateName: record.Name,
Status: string(actStatus),
Detail: detail,
}); err != nil {
// Log and continue since we don't want the client to retry.
svc.logger.ErrorContext(ctx, "failed to create certificate install activity", "host.id", host.ID, "activity.status", actStatus,
"err", err)
ctxerr.Handle(ctx, err)
}
}
}

return nil
}

////////////////////////////////////////////////////////////////////////////////
Expand Down
Loading