Feat UI finish cert api integration (#26598)

For #25464

UI work to integrate the host certificates UI with the finished API
endpoints


- [x] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.
This commit is contained in:
Gabriel Hernandez 2025-02-26 15:32:53 +00:00 committed by GitHub
parent 0adf67e538
commit f49a45f26d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 340 additions and 164 deletions

View file

@ -10,7 +10,7 @@ const DEFAULT_HOST_CERTIFICATE_MOCK: IHostCertificate = {
key_algorithm: "rsaEncryption",
key_strength: 2048,
key_usage: "CRL Sign, Key Cert Sign",
serial: 1,
serial: "123",
signing_algorithm: "sha256WithRSAEncryption",
subject: {
country: "US",

View file

@ -7,7 +7,7 @@ export interface IHostCertificate {
key_algorithm: string;
key_strength: number;
key_usage: string;
serial: number;
serial: string;
signing_algorithm: string;
subject: {
country: string;

View file

@ -5,7 +5,11 @@ import { IDeviceUserResponse, IHostDevice } from "interfaces/host";
import createMockHost from "__mocks__/hostMock";
import mockServer from "test/mock-server";
import { createCustomRenderer } from "test/test-utils";
import { customDeviceHandler } from "test/handlers/device-handler";
import {
customDeviceHandler,
defaultDeviceCertificatesHandler,
defaultDeviceHandler,
} from "test/handlers/device-handler";
import DeviceUserPage from "./DeviceUserPage";
const mockRouter = {
@ -34,12 +38,14 @@ const mockLocation = {
describe("Device User Page", () => {
it("hides the software tab if the device has no software", async () => {
mockServer.use(defaultDeviceHandler);
mockServer.use(defaultDeviceCertificatesHandler);
const render = createCustomRenderer({
withBackendMock: true,
});
// TODO: fix return type from render
const { user } = render(
render(
<DeviceUserPage
router={mockRouter}
params={{ device_auth_token: "testToken" }}
@ -51,14 +57,61 @@ describe("Device User Page", () => {
await screen.findByText("About");
expect(screen.queryByText(/Software/)).not.toBeInTheDocument();
});
// TODO: Fix this to the new copy
// expect(screen.getByText("No software detected")).toBeInTheDocument();
it("hides the certificates card if the device has no certificates", async () => {
mockServer.use(defaultDeviceHandler);
mockServer.use(defaultDeviceCertificatesHandler);
const render = createCustomRenderer({
withBackendMock: true,
});
render(
<DeviceUserPage
router={mockRouter}
params={{ device_auth_token: "testToken" }}
location={mockLocation}
/>
);
// waiting for the device data to render
await screen.findByText("About");
expect(screen.queryByText(/Certificates/)).not.toBeInTheDocument();
});
it("hides the certificates card if the device is not an apple device (mac, iphone, ipad)", async () => {
const host = createMockHost() as IHostDevice;
host.mdm.enrollment_status = "On (manual)";
host.platform = "windows";
host.dep_assigned_to_fleet = false;
mockServer.use(customDeviceHandler({ host }));
mockServer.use(defaultDeviceCertificatesHandler);
const render = createCustomRenderer({
withBackendMock: true,
});
render(
<DeviceUserPage
router={mockRouter}
params={{ device_auth_token: "testToken" }}
location={mockLocation}
/>
);
// waiting for the device data to render
await screen.findByText("About");
expect(screen.queryByText(/Certificates/)).not.toBeInTheDocument();
});
describe("MDM enrollment", () => {
const setupTest = async (overrides: Partial<IDeviceUserResponse>) => {
mockServer.use(customDeviceHandler(overrides));
mockServer.use(defaultDeviceCertificatesHandler);
const render = createCustomRenderer({
withBackendMock: true,

View file

@ -18,6 +18,7 @@ import { IHostPolicy } from "interfaces/policy";
import { IDeviceGlobalConfig } from "interfaces/config";
import { IHostSoftware } from "interfaces/software";
import { IHostCertificate } from "interfaces/certificates";
import { isAppleDevice } from "interfaces/platform";
import DeviceUserError from "components/DeviceUserError";
// @ts-ignore
@ -74,6 +75,9 @@ const FREE_TAB_PATHS = [
PATHS.DEVICE_USER_DETAILS_SOFTWARE,
] as const;
const DEFAULT_CERTIFICATES_PAGE_SIZE = 500;
const DEFAULT_CERTIFICATES_PAGE = 0;
interface IDeviceUserPageProps {
location: {
pathname: string;
@ -153,19 +157,6 @@ const DeviceUserPage = ({
}
);
const {
data: deviceCertificates,
isLoading: isLoadingDeviceCertificates,
isError: isErrorDeviceCertificates,
} = useQuery(
["hostCertificates", deviceAuthToken],
() => deviceUserAPI.getDeviceCertificates(deviceAuthToken),
{
...DEFAULT_USE_QUERY_OPTIONS,
enabled: !!deviceUserAPI,
}
);
const refetchExtensions = () => {
deviceMapping !== null && refetchDeviceMapping();
};
@ -262,6 +253,25 @@ const DeviceUserPage = ({
self_service: hasSelfService = false,
} = dupResponse || {};
const isPremiumTier = license?.tier === "premium";
const isAppleHost = host && isAppleDevice(host.platform);
const {
data: deviceCertificates,
isLoading: isLoadingDeviceCertificates,
isError: isErrorDeviceCertificates,
} = useQuery(
["hostCertificates", deviceAuthToken],
() =>
deviceUserAPI.getDeviceCertificates(
deviceAuthToken,
DEFAULT_CERTIFICATES_PAGE,
DEFAULT_CERTIFICATES_PAGE_SIZE
),
{
...DEFAULT_USE_QUERY_OPTIONS,
enabled: !!deviceUserAPI && isAppleHost,
}
);
const summaryData = normalizeEmptyValues(pick(host, HOST_SUMMARY_DATA));
@ -377,13 +387,9 @@ const DeviceUserPage = ({
const isSoftwareEnabled = !!globalConfig?.features
?.enable_software_inventory;
const isDarwinHost = host?.platform === "darwin";
const isIosOrIpadosHost =
host?.platform === "ios" || host?.platform === "ipados";
return (
<div className="core-wrapper">
{!host || isLoadingHost ? (
{!host || isLoadingHost || isLoadingDeviceCertificates ? (
<Spinner />
) : (
<div className={`${baseClass} main-content`}>
@ -447,15 +453,14 @@ const DeviceUserPage = ({
deviceMapping={deviceMapping}
munki={deviceMacAdminsData?.munki}
/>
{(isIosOrIpadosHost || isDarwinHost) &&
deviceCertificates?.certificates.length && (
<CertificatesCard
isMyDevicePage
data={deviceCertificates}
hostPlatform={host.platform}
onSelectCertificate={onSelectCertificate}
/>
)}
{isAppleHost && deviceCertificates?.certificates.length && (
<CertificatesCard
isMyDevicePage
data={deviceCertificates}
hostPlatform={host.platform}
onSelectCertificate={onSelectCertificate}
/>
)}
</TabPanel>
{isPremiumTier && isSoftwareEnabled && hasSelfService && (
<TabPanel>

View file

@ -15,7 +15,7 @@ import activitiesAPI, {
IHostPastActivitiesResponse,
IHostUpcomingActivitiesResponse,
} from "services/entities/activities";
import hostAPI from "services/entities/hosts";
import hostAPI, { IGetHostCertificatesResponse } from "services/entities/hosts";
import teamAPI, { ILoadTeamsResponse } from "services/entities/teams";
import {
@ -132,6 +132,8 @@ interface IHostDetailsSubNavItem {
}
const DEFAULT_ACTIVITY_PAGE_SIZE = 8;
const DEFAULT_CERTIFICATES_PAGE_SIZE = 500;
const DEFAULT_CERTIFICATES_PAGE = 0;
const HostDetailsPage = ({
router,
@ -461,12 +463,30 @@ const HostDetailsPage = ({
data: hostCertificates,
isLoading: isLoadingHostCertificates,
isError: isErrorHostCertificates,
} = useQuery(
["hostCertificates", host_id],
() => hostAPI.getHostCertificates(hostIdFromURL),
} = useQuery<
IGetHostCertificatesResponse,
Error,
IGetHostCertificatesResponse
>(
[
"host-certificates",
host_id,
DEFAULT_CERTIFICATES_PAGE,
DEFAULT_CERTIFICATES_PAGE_SIZE,
],
() =>
hostAPI.getHostCertificates(
hostIdFromURL,
DEFAULT_CERTIFICATES_PAGE,
DEFAULT_CERTIFICATES_PAGE_SIZE
),
{
...DEFAULT_USE_QUERY_OPTIONS,
enabled: !!hostIdFromURL,
enabled:
!!hostIdFromURL &&
(host?.platform === "darwin" ||
host?.platform === "ios" ||
host?.platform === "ipados"),
}
);
@ -756,7 +776,8 @@ const HostDetailsPage = ({
!host ||
isLoadingHost ||
pastActivitiesIsLoading ||
upcomingActivitiesIsLoading
upcomingActivitiesIsLoading ||
isLoadingHostCertificates
) {
return <Spinner />;
}
@ -828,8 +849,7 @@ const HostDetailsPage = ({
};
const isDarwinHost = host.platform === "darwin";
const isIosOrIpadosHost =
host.platform === "ios" || host.platform === "ipados";
const isIosOrIpadosHost = isIPadOrIPhone(host.platform);
const detailsPanelClass = classNames(`${baseClass}__details-panel`, {
[`${baseClass}__details-panel--ios-grid`]: isIosOrIpadosHost,

View file

@ -5,6 +5,7 @@ import { IHostCertificate } from "interfaces/certificates";
import TableContainer from "components/TableContainer";
import CustomLink from "components/CustomLink";
import TableCount from "components/TableContainer/TableCount";
import generateTableConfig from "./CertificatesTableConfig";
@ -50,6 +51,8 @@ const CertificatesTable = ({
isLoading={false}
onClickRow={onClickTableRow}
renderTableHelpText={() => helpText}
renderCount={() => <TableCount name="certificates" count={data.length} />}
disablePagination
/>
);
};

View file

@ -18,121 +18,203 @@ const CertificateDetailsModal = ({
certificate,
onExit,
}: ICertificateDetailsModalProps) => {
// Destructure the certificate object so we can check for presence of values
const {
subject: {
country: subjectCountry,
organization: subjectOrganization,
organizational_unit: subjectOrganizationalUnit,
common_name: subjectCommonName,
},
issuer: {
country: issuerCountry,
organization: issuerOrganization,
organizational_unit: issuerOrganizationalUnit,
common_name: issuerCommonName,
},
not_valid_before,
not_valid_after,
key_algorithm,
key_strength,
key_usage,
serial,
certificate_authority,
signing_algorithm,
} = certificate;
const showSubjectSection = Boolean(
subjectCountry ||
subjectOrganization ||
subjectOrganizationalUnit ||
subjectCommonName
);
const showIssuerNameSection = Boolean(
issuerCommonName ||
issuerCountry ||
issuerOrganization ||
issuerOrganizationalUnit
);
const showValidityPeriodSection = Boolean(
not_valid_before || not_valid_after
);
const showKeyInfoSection = Boolean(
key_algorithm || key_strength || key_usage || serial
);
const showSignatureSection = Boolean(signing_algorithm);
return (
<Modal className={baseClass} title="Certificate details" onExit={onExit}>
<>
<div className={`${baseClass}__content`}>
<div className={`${baseClass}__section`}>
<h3>Subject Name</h3>
<dl>
<DataSet
title="Country or region"
value={certificate.subject.country}
orientation="horizontal"
/>
<DataSet
title="Organization"
value={certificate.subject.organization}
orientation="horizontal"
/>
<DataSet
title="Organizational unit"
value={certificate.subject.organizational_unit}
orientation="horizontal"
/>
<DataSet
title="Common name"
value={certificate.subject.common_name}
orientation="horizontal"
/>
</dl>
</div>
<div className={`${baseClass}__section`}>
<h3>Issuer name</h3>
<dl>
<DataSet
title="Country or region"
value={certificate.issuer.country}
orientation="horizontal"
/>
<DataSet
title="Organization"
value={certificate.issuer.organization}
orientation="horizontal"
/>
<DataSet
title="Organizational unit"
value={certificate.issuer.organizational_unit}
orientation="horizontal"
/>
<DataSet
title="Common name"
value={certificate.issuer.common_name}
orientation="horizontal"
/>
</dl>
</div>
<div className={`${baseClass}__section`}>
<h3>Validity period</h3>
<dl>
<DataSet
title="Not valid before"
value={monthDayYearFormat(certificate.not_valid_before)}
orientation="horizontal"
/>
<DataSet
title="Not valid after"
value={monthDayYearFormat(certificate.not_valid_after)}
orientation="horizontal"
/>
</dl>
</div>
<div className={`${baseClass}__section`}>
<h3>Key info</h3>
<dl>
<DataSet
title="Algorithm"
value={certificate.key_algorithm}
orientation="horizontal"
/>
<DataSet
title="Key size"
value={certificate.key_strength}
orientation="horizontal"
/>
<DataSet
title="Key usage"
value={certificate.key_usage}
orientation="horizontal"
/>
<DataSet
title="Serial number"
value={certificate.serial}
orientation="horizontal"
/>
</dl>
</div>
{showSubjectSection && (
<div className={`${baseClass}__section`}>
<h3>Subject Name</h3>
<dl>
{subjectCountry && (
<DataSet
title="Country or region"
value={subjectCountry}
orientation="horizontal"
/>
)}
{subjectOrganization && (
<DataSet
title="Organization"
value={subjectOrganization}
orientation="horizontal"
/>
)}
{subjectOrganizationalUnit && (
<DataSet
title="Organizational unit"
value={subjectOrganizationalUnit}
orientation="horizontal"
/>
)}
{subjectCommonName && (
<DataSet
title="Common name"
value={subjectCommonName}
orientation="horizontal"
/>
)}
</dl>
</div>
)}
{showIssuerNameSection && (
<div className={`${baseClass}__section`}>
<h3>Issuer name</h3>
<dl>
{issuerCountry && (
<DataSet
title="Country or region"
value={issuerCountry}
orientation="horizontal"
/>
)}
{issuerOrganization && (
<DataSet
title="Organization"
value={issuerOrganization}
orientation="horizontal"
/>
)}
{issuerOrganizationalUnit && (
<DataSet
title="Organizational unit"
value={issuerOrganizationalUnit}
orientation="horizontal"
/>
)}
{issuerCommonName && (
<DataSet
title="Common name"
value={issuerCommonName}
orientation="horizontal"
/>
)}
</dl>
</div>
)}
{showValidityPeriodSection && (
<div className={`${baseClass}__section`}>
<h3>Validity period</h3>
<dl>
{not_valid_before && (
<DataSet
title="Not valid before"
value={monthDayYearFormat(not_valid_before)}
orientation="horizontal"
/>
)}
{not_valid_after && (
<DataSet
title="Not valid after"
value={monthDayYearFormat(not_valid_after)}
orientation="horizontal"
/>
)}
</dl>
</div>
)}
{showKeyInfoSection && (
<div className={`${baseClass}__section`}>
<h3>Key info</h3>
<dl>
{key_algorithm && (
<DataSet
title="Algorithm"
value={key_algorithm}
orientation="horizontal"
/>
)}
{key_strength && (
<DataSet
title="Key size"
value={key_strength}
orientation="horizontal"
/>
)}
{key_usage && (
<DataSet
title="Key usage"
value={key_usage}
orientation="horizontal"
/>
)}
{serial && (
<DataSet
title="Serial number"
value={serial}
orientation="horizontal"
/>
)}
</dl>
</div>
)}
{/* will always show this section */}
<div className={`${baseClass}__section`}>
<h3>Basic constraints</h3>
<dl>
<DataSet
title="Certificate authority"
value={certificate.certificate_authority ? "Yes" : "No"}
orientation="horizontal"
/>
</dl>
</div>
<div className={`${baseClass}__section`}>
<h3>Signature</h3>
<dl>
<DataSet
title="Algorithm"
value={certificate.signing_algorithm}
value={certificate_authority ? "Yes" : "No"}
orientation="horizontal"
/>
</dl>
</div>
{showSignatureSection && (
<div className={`${baseClass}__section`}>
<h3>Signature</h3>
<dl>
<DataSet
title="Algorithm"
value={signing_algorithm}
orientation="horizontal"
/>
</dl>
</div>
)}
</div>
<div className="modal-cta-wrap">
<Button onClick={onExit}>Done</Button>

View file

@ -87,14 +87,18 @@ export default {
},
getDeviceCertificates: (
deviceToken: string
deviceToken: string,
page = 0,
perPage = 10
): Promise<IGetDeviceCertificatesResponse> => {
const { DEVICE_CERTIFICATES } = endpoints;
const path = DEVICE_CERTIFICATES(deviceToken);
const path = `${DEVICE_CERTIFICATES(
deviceToken
)}?${buildQueryStringFromParams({
page,
per_page: perPage,
})}`;
// return sendRequest("GET", path);
return new Promise((resolve) => {
resolve(createMockGetHostCertificatesResponse());
});
return sendRequest("GET", path);
},
};

View file

@ -602,22 +602,16 @@ export default {
},
getHostCertificates: (
hostId: number
hostId: number,
page = 0,
perPage = 10
): Promise<IGetHostCertificatesResponse> => {
const { HOST_CERTIFICATES } = endpoints;
const path = `${HOST_CERTIFICATES(hostId)}?${buildQueryStringFromParams({
page,
per_page: perPage,
})}`;
// return sendRequest("GET", HOST_CERTIFICATES(hostId));
return new Promise((resolve) => {
resolve(
createMockGetHostCertificatesResponse({
certificates: [
createMockHostCertificate({
common_name: "Test 2",
not_valid_after: "2025-05-01T00:00:00.000Z",
}),
],
})
);
});
return sendRequest("GET", path);
},
};

View file

@ -6,9 +6,11 @@ import createMockDeviceUser, {
import createMockHost from "__mocks__/hostMock";
import createMockLicense from "__mocks__/licenseMock";
import createMockMacAdmins from "__mocks__/macAdminsMock";
import { createMockHostCertificate } from "__mocks__/certificatesMock";
import { baseUrl } from "test/test-utils";
import { IDeviceUserResponse } from "interfaces/host";
import { IGetDeviceSoftwareResponse } from "services/entities/device_user";
import { IGetHostCertificatesResponse } from "services/entities/hosts";
export const defaultDeviceHandler = http.get(baseUrl("/device/:token"), () => {
return HttpResponse.json({
@ -63,3 +65,16 @@ export const customDeviceSoftwareHandler = (
http.get(baseUrl("/device/:token/software"), () => {
return HttpResponse.json(createMockDeviceSoftwareResponse(overrides));
});
export const defaultDeviceCertificatesHandler = http.get(
baseUrl("/device/:token/certificates"),
() => {
return HttpResponse.json<IGetHostCertificatesResponse>({
certificates: [createMockHostCertificate()],
meta: {
has_next_results: false,
has_previous_results: false,
},
});
}
);