Resend android cert to host - frontend, update profile API (#42297)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #41541, #42293 - [x] Changes file added for user-visible changes in `changes/` - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually
This commit is contained in:
@@ -0,0 +1 @@
|
||||
- Added ability to re-send Android certificates to a specific host
|
||||
@@ -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<ActivityType, string> = {
|
||||
"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",
|
||||
|
||||
@@ -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
|
||||
|
||||
+12
@@ -646,6 +646,15 @@ const TAGGED_TEMPLATES = {
|
||||
</>
|
||||
);
|
||||
},
|
||||
resentCertificate: (activity: IActivity) => {
|
||||
return (
|
||||
<>
|
||||
{" "}
|
||||
resent {activity.details?.certificate_name} certificate for host{" "}
|
||||
<b>{activity.details?.host_display_name}</b>.
|
||||
</>
|
||||
);
|
||||
},
|
||||
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");
|
||||
}
|
||||
|
||||
@@ -740,13 +740,23 @@ const HostDetailsPage = ({
|
||||
const resendProfile = useCallback(
|
||||
(profileUUID: string): Promise<void> => {
|
||||
if (!host?.id) {
|
||||
return new Promise(() => undefined);
|
||||
return Promise.resolve();
|
||||
}
|
||||
return hostAPI.resendProfile(host.id, profileUUID);
|
||||
},
|
||||
[host?.id]
|
||||
);
|
||||
|
||||
const resendCertificate = useCallback(
|
||||
(certificateTemplateId: number): Promise<void> => {
|
||||
if (!host?.id) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return hostAPI.resendCertificate(host.id, certificateTemplateId);
|
||||
},
|
||||
[host?.id]
|
||||
);
|
||||
|
||||
const rotateRecoveryLockPassword = useCallback((): Promise<void> => {
|
||||
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}
|
||||
/>
|
||||
|
||||
@@ -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<void>;
|
||||
resendCertificateRequest?: (certificateTemplateId: number) => Promise<void>;
|
||||
rotateRecoveryLockPassword?: () => Promise<void>;
|
||||
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}
|
||||
/>
|
||||
|
||||
+33
-16
@@ -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<HTMLButtonElement, 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 (
|
||||
<Button
|
||||
disabled={isFetching}
|
||||
disabled={isResending}
|
||||
onClick={onClick}
|
||||
variant="inverse"
|
||||
className={classNames}
|
||||
@@ -266,6 +269,7 @@ interface IOSSettingsErrorCellProps {
|
||||
canRotateRecoveryLockPassword?: boolean;
|
||||
profile: IHostMdmProfileWithAddedStatus;
|
||||
resendRequest: (profileUUID: string) => Promise<void>;
|
||||
resendCertificateRequest?: (certificateTemplateId: number) => Promise<void>;
|
||||
rotateRecoveryLockPassword?: () => Promise<void>;
|
||||
onProfileResent?: () => void;
|
||||
}
|
||||
@@ -275,22 +279,35 @@ const OSSettingsErrorCell = ({
|
||||
canRotateRecoveryLockPassword = false,
|
||||
profile,
|
||||
resendRequest,
|
||||
resendCertificateRequest,
|
||||
rotateRecoveryLockPassword,
|
||||
onProfileResent = noop,
|
||||
}: IOSSettingsErrorCellProps) => {
|
||||
const { renderFlash } = useContext(NotificationContext);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isResending, setIsResending] = useState(false);
|
||||
const [isRotating, setIsRotating] = useState(false);
|
||||
|
||||
const isAndroidCertificate =
|
||||
profile.profile_uuid === FLEET_ANDROID_CERTIFICATE_TEMPLATE_PROFILE_ID;
|
||||
|
||||
const onResendProfile = async () => {
|
||||
setIsLoading(true);
|
||||
setIsResending(true);
|
||||
try {
|
||||
await resendRequest(profile.profile_uuid);
|
||||
onProfileResent();
|
||||
if (
|
||||
isAndroidCertificate &&
|
||||
resendCertificateRequest &&
|
||||
profile.certificate_template_id !== undefined
|
||||
) {
|
||||
await resendCertificateRequest(profile.certificate_template_id);
|
||||
onProfileResent();
|
||||
} else if (!isAndroidCertificate) {
|
||||
await resendRequest(profile.profile_uuid);
|
||||
onProfileResent();
|
||||
}
|
||||
} catch (e) {
|
||||
renderFlash("error", "Couldn't resend. Please try again.");
|
||||
}
|
||||
setIsLoading(false);
|
||||
setIsResending(false);
|
||||
};
|
||||
|
||||
const onRotatePassword = async () => {
|
||||
@@ -314,7 +331,7 @@ const OSSettingsErrorCell = ({
|
||||
|
||||
const isFailed = profile.status === "failed";
|
||||
const isVerified = profile.status === "verified";
|
||||
const showRefetchButton =
|
||||
const showResendButton =
|
||||
canResendProfiles &&
|
||||
(isFailed || isVerified) &&
|
||||
profile.profile_uuid !== REC_LOCK_SYNTHETIC_PROFILE_UUID;
|
||||
@@ -331,13 +348,13 @@ const OSSettingsErrorCell = ({
|
||||
tooltip={tooltip}
|
||||
value={value}
|
||||
className={
|
||||
isFailed || showRefetchButton || showRotateButton
|
||||
isFailed || showResendButton || showRotateButton
|
||||
? `${baseClass}__failed-message`
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
{showRefetchButton && (
|
||||
<RefetchButton isFetching={isLoading} onClick={onResendProfile} />
|
||||
{showResendButton && (
|
||||
<ResendButton isResending={isResending} onClick={onResendProfile} />
|
||||
)}
|
||||
{showRotateButton && (
|
||||
<RotateButton isRotating={isRotating} onClick={onRotatePassword} />
|
||||
|
||||
@@ -12,6 +12,7 @@ interface IOSSettingsTableProps {
|
||||
canRotateRecoveryLockPassword?: boolean;
|
||||
tableData: IHostMdmProfileWithAddedStatus[];
|
||||
resendRequest: (profileUUID: string) => Promise<void>;
|
||||
resendCertificateRequest?: (certificateTemplateId: number) => Promise<void>;
|
||||
rotateRecoveryLockPassword?: () => Promise<void>;
|
||||
onProfileResent: () => void;
|
||||
}
|
||||
@@ -21,6 +22,7 @@ const OSSettingsTable = ({
|
||||
canRotateRecoveryLockPassword = false,
|
||||
tableData,
|
||||
resendRequest,
|
||||
resendCertificateRequest,
|
||||
rotateRecoveryLockPassword,
|
||||
onProfileResent,
|
||||
}: IOSSettingsTableProps) => {
|
||||
@@ -31,6 +33,7 @@ const OSSettingsTable = ({
|
||||
canResendProfiles,
|
||||
resendRequest,
|
||||
onProfileResent,
|
||||
resendCertificateRequest,
|
||||
canRotateRecoveryLockPassword,
|
||||
rotateRecoveryLockPassword
|
||||
),
|
||||
@@ -40,6 +43,7 @@ const OSSettingsTable = ({
|
||||
onProfileResent,
|
||||
canRotateRecoveryLockPassword,
|
||||
rotateRecoveryLockPassword,
|
||||
resendCertificateRequest,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
+10
-1
@@ -4,6 +4,7 @@ import { Column } from "react-table";
|
||||
import { IStringCellProps } from "interfaces/datatable_config";
|
||||
import { HostAndroidCertStatus, IHostMdmData } from "interfaces/host";
|
||||
import {
|
||||
FLEET_ANDROID_CERTIFICATE_TEMPLATE_PROFILE_ID,
|
||||
FLEET_FILEVAULT_PROFILE_DISPLAY_NAME,
|
||||
IHostMdmProfile,
|
||||
isLinuxDiskEncryptionStatus,
|
||||
@@ -47,6 +48,7 @@ const generateTableConfig = (
|
||||
canResendProfiles: boolean,
|
||||
resendRequest: (profileUUID: string) => Promise<void>,
|
||||
onProfileResent: () => void,
|
||||
resendCertificateRequest?: (certificateTemplateId: number) => Promise<void>,
|
||||
canRotateRecoveryLockPassword?: boolean,
|
||||
rotateRecoveryLockPassword?: () => Promise<void>
|
||||
): ITableColumnConfig[] => {
|
||||
@@ -97,6 +99,10 @@ const generateTableConfig = (
|
||||
const isAppleMobileConfigProfile =
|
||||
isAppleDevice(platform) && !isDDMProfile(cellProps.row.original);
|
||||
const isWindowsProfile = platform === "windows";
|
||||
const isAndroidCertificate =
|
||||
platform === "android" &&
|
||||
cellProps.row.original.profile_uuid ===
|
||||
FLEET_ANDROID_CERTIFICATE_TEMPLATE_PROFILE_ID;
|
||||
|
||||
const isRecoveryLockRow =
|
||||
cellProps.row.original.profile_uuid ===
|
||||
@@ -106,13 +112,16 @@ const generateTableConfig = (
|
||||
<OSSettingsErrorCell
|
||||
canResendProfiles={
|
||||
canResendProfiles &&
|
||||
(isWindowsProfile || isAppleMobileConfigProfile)
|
||||
(isWindowsProfile ||
|
||||
isAppleMobileConfigProfile ||
|
||||
isAndroidCertificate)
|
||||
}
|
||||
canRotateRecoveryLockPassword={
|
||||
isRecoveryLockRow && canRotateRecoveryLockPassword
|
||||
}
|
||||
profile={cellProps.row.original}
|
||||
resendRequest={resendRequest}
|
||||
resendCertificateRequest={resendCertificateRequest}
|
||||
rotateRecoveryLockPassword={rotateRecoveryLockPassword}
|
||||
onProfileResent={onProfileResent}
|
||||
/>
|
||||
|
||||
@@ -648,6 +648,18 @@ export default {
|
||||
return sendRequest("POST", HOST_RESEND_PROFILE(hostId, profileUUID));
|
||||
},
|
||||
|
||||
resendCertificate: (
|
||||
hostId: number,
|
||||
certificateTemplateId: number
|
||||
): Promise<void> => {
|
||||
const { HOST_RESEND_CERTIFICATE } = endpoints;
|
||||
|
||||
return sendRequest(
|
||||
"POST",
|
||||
HOST_RESEND_CERTIFICATE(hostId, certificateTemplateId)
|
||||
);
|
||||
},
|
||||
|
||||
getHostSoftware: (
|
||||
params: IHostSoftwareQueryKey
|
||||
): Promise<IGetHostSoftwareResponse> => {
|
||||
|
||||
@@ -89,6 +89,8 @@ export default {
|
||||
HOST_WIPE: (id: number) => `/${API_VERSION}/fleet/hosts/${id}/wipe`,
|
||||
HOST_RESEND_PROFILE: (hostId: number, profileUUID: string) =>
|
||||
`/${API_VERSION}/fleet/hosts/${hostId}/configuration_profiles/${profileUUID}/resend`,
|
||||
HOST_RESEND_CERTIFICATE: (hostId: number, certificateTemplateId: number) =>
|
||||
`/${API_VERSION}/fleet/hosts/${hostId}/certificates/${certificateTemplateId}/resend`,
|
||||
HOST_SOFTWARE: (id: number) => `/${API_VERSION}/fleet/hosts/${id}/software`,
|
||||
HOST_SOFTWARE_PACKAGE_INSTALL: (hostId: number, softwareId: number) =>
|
||||
`/${API_VERSION}/fleet/hosts/${hostId}/software/${softwareId}/install`,
|
||||
|
||||
@@ -22,6 +22,7 @@ func TestHostCertificateTemplate(t *testing.T) {
|
||||
require.Equal(t, "", profile.Name)
|
||||
require.Equal(t, "", profile.Platform)
|
||||
require.Nil(t, profile.Status)
|
||||
require.Nil(t, profile.CertificateTemplateID)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -42,6 +43,16 @@ func TestHostCertificateTemplate(t *testing.T) {
|
||||
require.Empty(t, profile.Detail)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "maps certificate_template_id correctly",
|
||||
template: &HostCertificateTemplate{
|
||||
CertificateTemplateID: 42,
|
||||
},
|
||||
expectation: func(t *testing.T, profile HostMDMProfile) {
|
||||
require.NotNil(t, profile.CertificateTemplateID)
|
||||
require.EqualValues(t, 42, *profile.CertificateTemplateID)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "maps detail correctly",
|
||||
template: &HostCertificateTemplate{
|
||||
|
||||
@@ -29,13 +29,15 @@ func (p *HostCertificateTemplate) ToHostMDMProfile() HostMDMProfile {
|
||||
}
|
||||
|
||||
status := string(p.Status)
|
||||
certTemplateID := p.CertificateTemplateID
|
||||
profile := HostMDMProfile{
|
||||
HostUUID: p.HostUUID,
|
||||
Name: p.Name,
|
||||
Platform: "android",
|
||||
Status: &status,
|
||||
OperationType: p.OperationType,
|
||||
ProfileUUID: AndroidCertificateTemplateProfileID,
|
||||
HostUUID: p.HostUUID,
|
||||
Name: p.Name,
|
||||
Platform: "android",
|
||||
Status: &status,
|
||||
OperationType: p.OperationType,
|
||||
ProfileUUID: AndroidCertificateTemplateProfileID,
|
||||
CertificateTemplateID: &certTemplateID,
|
||||
}
|
||||
if p.Detail != nil {
|
||||
profile.Detail = *p.Detail
|
||||
|
||||
+12
-11
@@ -466,17 +466,18 @@ type MDMProfilesSummary struct {
|
||||
// HostMDMProfile is the status of an MDM profile on a host. It can be used to represent either
|
||||
// a Windows or macOS profile.
|
||||
type HostMDMProfile struct {
|
||||
HostUUID string `db:"-" json:"-"`
|
||||
CommandUUID string `db:"-" json:"-"`
|
||||
ProfileUUID string `db:"-" json:"profile_uuid"`
|
||||
Name string `db:"-" json:"name"`
|
||||
Identifier string `db:"-" json:"-"`
|
||||
Status *string `db:"-" json:"status"` // MDMDeliveryStatus or CertificateTemplateStatus
|
||||
OperationType MDMOperationType `db:"-" json:"operation_type"`
|
||||
Detail string `db:"-" json:"detail"`
|
||||
Platform string `db:"-" json:"platform"`
|
||||
Scope *string `db:"-" json:"scope"` // Scope and ManagedLocalAccount will be null on unsupported platforms
|
||||
ManagedLocalAccount *string `db:"-" json:"managed_local_account"`
|
||||
HostUUID string `db:"-" json:"-"`
|
||||
CommandUUID string `db:"-" json:"-"`
|
||||
ProfileUUID string `db:"-" json:"profile_uuid"`
|
||||
Name string `db:"-" json:"name"`
|
||||
Identifier string `db:"-" json:"-"`
|
||||
Status *string `db:"-" json:"status"` // MDMDeliveryStatus or CertificateTemplateStatus
|
||||
OperationType MDMOperationType `db:"-" json:"operation_type"`
|
||||
Detail string `db:"-" json:"detail"`
|
||||
Platform string `db:"-" json:"platform"`
|
||||
Scope *string `db:"-" json:"scope"` // Scope and ManagedLocalAccount will be null on unsupported platforms
|
||||
ManagedLocalAccount *string `db:"-" json:"managed_local_account"`
|
||||
CertificateTemplateID *uint `db:"-" json:"certificate_template_id,omitempty"`
|
||||
}
|
||||
|
||||
// MDMDeliveryStatus is the status of an MDM command to apply a profile
|
||||
|
||||
Reference in New Issue
Block a user