- Labels
+ Labels
{labels.length === 0 ? (
No labels are associated with this host.
diff --git a/frontend/pages/hosts/details/cards/Labels/__styles.scss b/frontend/pages/hosts/details/cards/Labels/__styles.scss
new file mode 100644
index 0000000000..f70ba0f9a9
--- /dev/null
+++ b/frontend/pages/hosts/details/cards/Labels/__styles.scss
@@ -0,0 +1,7 @@
+.labels-card {
+
+ h2 {
+ font-size: $medium;
+ margin: 0 0 $pad-large;
+ }
+}
diff --git a/frontend/pages/hosts/details/cards/Users/Users.tsx b/frontend/pages/hosts/details/cards/Users/Users.tsx
index 3879e940ee..6fe749eef7 100644
--- a/frontend/pages/hosts/details/cards/Users/Users.tsx
+++ b/frontend/pages/hosts/details/cards/Users/Users.tsx
@@ -63,11 +63,11 @@ const Users = ({
<>
- Users
+ Users
{users?.length ? (
void;
+}
+
+const CertificateDetailsModal = ({
+ certificate,
+ onExit,
+}: ICertificateDetailsModalProps) => {
+ return (
+
+ <>
+
+
+
Subject Name
+
+
+
+
+
+
+
+
+
Issuer name
+
+
+
+
+
+
+
+
+
Validity period
+
+
+
+
+
+
+
+
+
Basic constraints
+
+
+
+
+
+
+
+
+
+ >
+
+ );
+};
+
+export default CertificateDetailsModal;
diff --git a/frontend/pages/hosts/details/modals/CertificateDetailsModal/_styles.scss b/frontend/pages/hosts/details/modals/CertificateDetailsModal/_styles.scss
new file mode 100644
index 0000000000..a522b0d39f
--- /dev/null
+++ b/frontend/pages/hosts/details/modals/CertificateDetailsModal/_styles.scss
@@ -0,0 +1,32 @@
+.certificate-details-modal {
+
+ &__content {
+ display: flex;
+ flex-direction: column;
+ gap: $pad-xlarge;
+ }
+
+ h3 {
+ margin: 0;
+ font-size: $small;
+ }
+
+ &__section {
+ display: flex;
+ gap: $pad-small;
+ flex-direction: column;
+ padding-top: $pad-xlarge;
+ border-top: 1px solid $ui-fleet-black-10;
+
+ &:first-child {
+ padding-top: 0;
+ border-top: none;
+ }
+
+ dl {
+ display: flex;
+ flex-direction: column;
+ gap: $pad-xsmall;
+ }
+ }
+}
diff --git a/frontend/pages/hosts/details/modals/CertificateDetailsModal/index.ts b/frontend/pages/hosts/details/modals/CertificateDetailsModal/index.ts
new file mode 100644
index 0000000000..7323ec4a28
--- /dev/null
+++ b/frontend/pages/hosts/details/modals/CertificateDetailsModal/index.ts
@@ -0,0 +1 @@
+export { default } from "./CertificateDetailsModal";
diff --git a/frontend/services/entities/device_user.ts b/frontend/services/entities/device_user.ts
index 0ec397fd8a..57fcb66306 100644
--- a/frontend/services/entities/device_user.ts
+++ b/frontend/services/entities/device_user.ts
@@ -1,8 +1,11 @@
import { IDeviceUserResponse } from "interfaces/host";
import { IDeviceSoftware } from "interfaces/software";
+import { IHostCertificate } from "interfaces/certificates";
import sendRequest from "services";
import endpoints from "utilities/endpoints";
import { buildQueryStringFromParams } from "utilities/url";
+import { createMockGetHostCertificatesResponse } from "__mocks__/certificatesMock";
+
import { IHostSoftwareQueryParams } from "./hosts";
export type ILoadHostDetailsExtension = "device_mapping" | "macadmins";
@@ -27,6 +30,14 @@ interface IGetDeviceDetailsRequest {
exclude_software?: boolean;
}
+export interface IGetDeviceCertificatesResponse {
+ certificates: IHostCertificate[];
+ meta: {
+ has_next_results: boolean;
+ has_previous_results: boolean;
+ };
+}
+
export default {
loadHostDetails: ({
token,
@@ -74,4 +85,16 @@ export default {
return sendRequest("POST", path);
},
+
+ getDeviceCertificates: (
+ deviceToken: string
+ ): Promise => {
+ const { DEVICE_CERTIFICATES } = endpoints;
+ const path = DEVICE_CERTIFICATES(deviceToken);
+
+ // return sendRequest("GET", path);
+ return new Promise((resolve) => {
+ resolve(createMockGetHostCertificatesResponse());
+ });
+ },
};
diff --git a/frontend/services/entities/hosts.ts b/frontend/services/entities/hosts.ts
index bf8aff8720..56b484ee8f 100644
--- a/frontend/services/entities/hosts.ts
+++ b/frontend/services/entities/hosts.ts
@@ -23,6 +23,11 @@ import {
} from "interfaces/mdm";
import { IMunkiIssuesAggregate } from "interfaces/macadmins";
import { PlatformValueOptions, PolicyResponse } from "utilities/constants";
+import { IHostCertificate } from "interfaces/certificates";
+import {
+ createMockGetHostCertificatesResponse,
+ createMockHostCertificate,
+} from "__mocks__/certificatesMock";
export interface ISortOption {
key: string;
@@ -171,6 +176,14 @@ export interface IHostSoftwareQueryKey extends IHostSoftwareQueryParams {
softwareUpdatedAt?: string;
}
+export interface IGetHostCertificatesResponse {
+ certificates: IHostCertificate[];
+ meta: {
+ has_next_results: boolean;
+ has_previous_results: boolean;
+ };
+}
+
export type ILoadHostDetailsExtension = "device_mapping" | "macadmins";
const LABEL_PREFIX = "labels/";
@@ -579,6 +592,7 @@ export default {
HOST_SOFTWARE_PACKAGE_INSTALL(hostId, softwareId)
);
},
+
uninstallHostSoftwarePackage: (hostId: number, softwareId: number) => {
const { HOST_SOFTWARE_PACKAGE_UNINSTALL } = endpoints;
return sendRequest(
@@ -586,4 +600,24 @@ export default {
HOST_SOFTWARE_PACKAGE_UNINSTALL(hostId, softwareId)
);
},
+
+ getHostCertificates: (
+ hostId: number
+ ): Promise => {
+ const { HOST_CERTIFICATES } = endpoints;
+
+ // 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",
+ }),
+ ],
+ })
+ );
+ });
+ },
};
diff --git a/frontend/utilities/endpoints.ts b/frontend/utilities/endpoints.ts
index ea08ed7a57..8452e85498 100644
--- a/frontend/utilities/endpoints.ts
+++ b/frontend/utilities/endpoints.ts
@@ -40,6 +40,9 @@ export default {
DEVICE_TRIGGER_LINUX_DISK_ENCRYPTION_KEY_ESCROW: (token: string): string => {
return `/${API_VERSION}/fleet/device/${token}/mdm/linux/trigger_escrow`;
},
+ DEVICE_CERTIFICATES: (token: string): string => {
+ return `/${API_VERSION}/fleet/device/${token}/certificates`;
+ },
// Host endpoints
HOST_SUMMARY: `/${API_VERSION}/fleet/host_summary`,
@@ -61,6 +64,8 @@ export default {
`/${API_VERSION}/fleet/hosts/${hostId}/software/${softwareId}/install`,
HOST_SOFTWARE_PACKAGE_UNINSTALL: (hostId: number, softwareId: number) =>
`/${API_VERSION}/fleet/hosts/${hostId}/software/${softwareId}/uninstall`,
+ HOST_CERTIFICATES: (id: number) =>
+ `/${API_VERSION}/fleet/hosts/${id}/certificates`,
INVITES: `/${API_VERSION}/fleet/invites`,
INVITE_VERIFY: (token: string) => `/${API_VERSION}/fleet/invites/${token}`,
diff --git a/frontend/utilities/helpers.tsx b/frontend/utilities/helpers.tsx
index 1e68d7322a..dc85d38b47 100644
--- a/frontend/utilities/helpers.tsx
+++ b/frontend/utilities/helpers.tsx
@@ -639,6 +639,13 @@ export const hasLicenseExpired = (expiration: string): boolean => {
return isAfter(new Date(), new Date(expiration));
};
+// just a rename of hasLicenseExpired so that it can be used in other contexts.
+// TODO: change hasLicenseExpired instances to hasExpired
+/**
+ * determines if a date has expired. This will check against the current date and time.
+ */
+export const hasExpired = hasLicenseExpired;
+
/**
* determines if a date will expire within "x" number of days. If the date has
* has already expired, this function will return false.
From ea35a2d77d9bf6177bc17dfece1c9731e41a93e9 Mon Sep 17 00:00:00 2001
From: Sarah Gillespie <73313222+gillespi314@users.noreply.github.com>
Date: Fri, 21 Feb 2025 15:57:09 -0600
Subject: [PATCH 06/14] Cleanup `host_certificates` table on host deletion
(#26524)
---
changes/23235-host-certificates | 1 +
server/datastore/mysql/host_certificates.go | 9 ++++++---
server/datastore/mysql/hosts.go | 1 +
server/datastore/mysql/hosts_test.go | 8 ++++++++
4 files changed, 16 insertions(+), 3 deletions(-)
create mode 100644 changes/23235-host-certificates
diff --git a/changes/23235-host-certificates b/changes/23235-host-certificates
new file mode 100644
index 0000000000..e63315609f
--- /dev/null
+++ b/changes/23235-host-certificates
@@ -0,0 +1 @@
+- Added new features to include certificates in host vitals for macOS, iOS, and iPadOS.
diff --git a/server/datastore/mysql/host_certificates.go b/server/datastore/mysql/host_certificates.go
index c4ab2d41b6..6bd7fe91bb 100644
--- a/server/datastore/mysql/host_certificates.go
+++ b/server/datastore/mysql/host_certificates.go
@@ -21,11 +21,11 @@ func (ds *Datastore) UpdateHostCertificates(ctx context.Context, hostID uint, ce
for _, cert := range certs {
if cert.HostID != hostID {
// caller should ensure this does not happen
- level.Debug(ds.logger).Log("msg", fmt.Sprintf("host ID does not match provided certificate: %d %d", hostID, cert.HostID))
+ level.Debug(ds.logger).Log("msg", fmt.Sprintf("host certificates: host ID does not match provided certificate: %d %d", hostID, cert.HostID))
}
if _, ok := incomingBySHA1[strings.ToUpper(hex.EncodeToString(cert.SHA1Sum))]; ok {
// TODO: sha1 is broken so this could be a sign of a problem, how should we handle?
- level.Info(ds.logger).Log("msg", "host has multiple certificates with the same SHA1, only the first will be recorded", "host_id", hostID, "sha1", string(cert.SHA1Sum))
+ level.Info(ds.logger).Log("msg", "host certificates: host has multiple certificates with the same SHA1, only the first will be recorded", "host_id", hostID, "sha1", string(cert.SHA1Sum))
continue
}
incomingBySHA1[strings.ToUpper(hex.EncodeToString(cert.SHA1Sum))] = cert
@@ -48,7 +48,7 @@ func (ds *Datastore) UpdateHostCertificates(ctx context.Context, hostID uint, ce
if _, ok := existingBySHA1[sha1]; ok {
// TODO: should we always update existing records? skipping updates reduces db load but
// osquery is using sha1 so we consider subtleties
- level.Debug(ds.logger).Log("msg", fmt.Sprintf("existing certificate: %s", sha1), "host_id", hostID)
+ level.Debug(ds.logger).Log("msg", fmt.Sprintf("host certificates: already exists: %s", sha1), "host_id", hostID) // TODO: silence this log after initial rollout period
} else {
toInsert = append(toInsert, incoming)
}
@@ -179,6 +179,9 @@ INSERT INTO host_certificates (
}
func softDeleteHostCertsDB(ctx context.Context, tx sqlx.ExtContext, hostID uint, toDelete []uint) error {
+ // TODO: consider whether we should hard delete certs after a certain period of time if we are seeing
+ // the table grow too large with soft deleted records
+
if len(toDelete) == 0 {
return nil
}
diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go
index 1dbd9d622a..c0051f0c0d 100644
--- a/server/datastore/mysql/hosts.go
+++ b/server/datastore/mysql/hosts.go
@@ -549,6 +549,7 @@ var hostRefs = []string{
"host_mdm_actions",
"host_calendar_events",
"upcoming_activities",
+ "host_certificates",
}
// NOTE: The following tables are explicity excluded from hostRefs list and accordingly are not
diff --git a/server/datastore/mysql/hosts_test.go b/server/datastore/mysql/hosts_test.go
index edb0fa529b..af10e4af83 100644
--- a/server/datastore/mysql/hosts_test.go
+++ b/server/datastore/mysql/hosts_test.go
@@ -2,6 +2,7 @@ package mysql
import (
"context"
+ "crypto/sha1"
"crypto/sha256"
"database/sql"
"encoding/json"
@@ -7057,6 +7058,13 @@ func testHostsDeleteHosts(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.True(t, added)
+ // Add a host certificate
+ require.NoError(t, ds.UpdateHostCertificates(ctx, host.ID, []*fleet.HostCertificateRecord{{
+ HostID: host.ID,
+ CommonName: "foo",
+ SHA1Sum: sha1.New().Sum([]byte("foo")),
+ }}))
+
// Check there's an entry for the host in all the associated tables.
for _, hostRef := range hostRefs {
var ok bool
From a141830f76484ecceda121f2eb10e620fbfe1b8e Mon Sep 17 00:00:00 2001
From: Martin Angers
Date: Mon, 24 Feb 2025 12:52:39 -0500
Subject: [PATCH 07/14] CHV: implement paginated list certificates endpoints
(#26554)
---
.../25460-add-list-host-certificates-endpoint | 1 +
server/datastore/mysql/host_certificates.go | 14 +-
server/fleet/host_certificates.go | 82 ++++++++---
server/fleet/service.go | 3 +
server/service/devices.go | 39 ++++++
server/service/handler.go | 4 +
server/service/hosts.go | 67 +++++++++
server/service/hosts_test.go | 8 ++
server/service/integration_core_test.go | 131 ++++++++++++++++++
server/service/testing_client.go | 4 +-
10 files changed, 323 insertions(+), 30 deletions(-)
create mode 100644 changes/25460-add-list-host-certificates-endpoint
diff --git a/changes/25460-add-list-host-certificates-endpoint b/changes/25460-add-list-host-certificates-endpoint
new file mode 100644
index 0000000000..7663b3f98e
--- /dev/null
+++ b/changes/25460-add-list-host-certificates-endpoint
@@ -0,0 +1 @@
+* Added the list host certificates (and list device's certificates) endpoints.
diff --git a/server/datastore/mysql/host_certificates.go b/server/datastore/mysql/host_certificates.go
index 6bd7fe91bb..7858305314 100644
--- a/server/datastore/mysql/host_certificates.go
+++ b/server/datastore/mysql/host_certificates.go
@@ -73,15 +73,8 @@ func (ds *Datastore) UpdateHostCertificates(ctx context.Context, hostID uint, ce
}
func listHostCertsDB(ctx context.Context, tx sqlx.QueryerContext, hostID uint, opts fleet.ListOptions) ([]*fleet.HostCertificateRecord, *fleet.PaginationMetadata, error) {
- // TODO: move this to the service layer and do validation of the order key?
- if opts.OrderKey == "" {
- // default sort by common name ascending
- opts.OrderKey = "common_name"
- opts.OrderDirection = fleet.OrderAscending
- }
-
stmt := `
-SELECT
+SELECT
id,
sha1_sum,
host_id,
@@ -105,8 +98,8 @@ SELECT
issuer_org_unit,
issuer_common_name
FROM
- host_certificates
-WHERE
+ host_certificates
+WHERE
host_id = ?
AND deleted_at IS NULL`
@@ -126,7 +119,6 @@ WHERE
certs = certs[:len(certs)-1]
}
}
-
return certs, metaData, nil
}
diff --git a/server/fleet/host_certificates.go b/server/fleet/host_certificates.go
index f7d8ceac3a..0d7a27975d 100644
--- a/server/fleet/host_certificates.go
+++ b/server/fleet/host_certificates.go
@@ -24,24 +24,24 @@ type HostCertificateRecord struct {
DeletedAt *time.Time `json:"-" db:"deleted_at"`
// The following fields are extracted from the certificate.
+ NotValidAfter time.Time `json:"-" db:"not_valid_after"`
+ NotValidBefore time.Time `json:"-" db:"not_valid_before"`
+ CertificateAuthority bool `json:"-" db:"certificate_authority"`
+ CommonName string `json:"-" db:"common_name"`
+ KeyAlgorithm string `json:"-" db:"key_algorithm"`
+ KeyStrength int `json:"-" db:"key_strength"`
+ KeyUsage string `json:"-" db:"key_usage"`
+ Serial string `json:"-" db:"serial"`
+ SigningAlgorithm string `json:"-" db:"signing_algorithm"`
- NotValidAfter time.Time `json:"-" db:"not_valid_after"`
- NotValidBefore time.Time `json:"-" db:"not_valid_before"`
- CertificateAuthority bool `json:"-" db:"certificate_authority"`
- CommonName string `json:"-" db:"common_name"`
- KeyAlgorithm string `json:"-" db:"key_algorithm"`
- KeyStrength int `json:"-" db:"key_strength"`
- KeyUsage string `json:"-" db:"key_usage"`
- Serial string `json:"-" db:"serial"`
- SigningAlgorithm string `json:"-" db:"signing_algorithm"`
- SubjectCountry string `json:"-" db:"subject_country"`
- SubjectOrganization string `json:"-" db:"subject_org"`
- SubjectOrganizationalUnit string `json:"-" db:"subject_org_unit"`
- SubjectCommonName string `json:"-" db:"subject_common_name"`
- IssuerCountry string `json:"-" db:"issuer_country"`
- IssuerOrganization string `json:"-" db:"issuer_org"`
- IssuerOrganizationalUnit string `json:"-" db:"issuer_org_unit"`
- IssuerCommonName string `json:"-" db:"issuer_common_name"`
+ SubjectCountry string `json:"-" db:"subject_country"`
+ SubjectOrganization string `json:"-" db:"subject_org"`
+ SubjectOrganizationalUnit string `json:"-" db:"subject_org_unit"`
+ SubjectCommonName string `json:"-" db:"subject_common_name"`
+ IssuerCountry string `json:"-" db:"issuer_country"`
+ IssuerOrganization string `json:"-" db:"issuer_org"`
+ IssuerOrganizationalUnit string `json:"-" db:"issuer_org_unit"`
+ IssuerCommonName string `json:"-" db:"issuer_common_name"`
}
func NewHostCertificateRecord(
@@ -82,6 +82,54 @@ func NewHostCertificateRecord(
}
}
+// ToPayload fills a HostCertificatePayload with the fields of a
+// HostCertificateRecord. The HostCertificatePayload is used in API responses.
+func (r *HostCertificateRecord) ToPayload() *HostCertificatePayload {
+ subject := &HostCertificateNameDetails{
+ CommonName: r.SubjectCommonName,
+ Country: r.SubjectCountry,
+ Organization: r.SubjectOrganization,
+ OrganizationalUnit: r.SubjectOrganizationalUnit,
+ }
+ issuer := &HostCertificateNameDetails{
+ CommonName: r.IssuerCommonName,
+ Country: r.IssuerCountry,
+ Organization: r.IssuerOrganization,
+ OrganizationalUnit: r.IssuerOrganizationalUnit,
+ }
+ return &HostCertificatePayload{
+ ID: r.ID,
+ NotValidAfter: r.NotValidAfter,
+ NotValidBefore: r.NotValidBefore,
+ CertificateAuthority: r.CertificateAuthority,
+ CommonName: r.CommonName,
+ KeyAlgorithm: r.KeyAlgorithm,
+ KeyStrength: r.KeyStrength,
+ KeyUsage: r.KeyUsage,
+ Serial: r.Serial,
+ SigningAlgorithm: r.SigningAlgorithm,
+ Subject: subject,
+ Issuer: issuer,
+ }
+}
+
+// HostCertificatePayload is the JSON model for API endpoints that return host certificates.
+type HostCertificatePayload struct {
+ ID uint `json:"id"`
+ NotValidAfter time.Time `json:"not_valid_after"`
+ NotValidBefore time.Time `json:"not_valid_before"`
+ CertificateAuthority bool `json:"certificate_authority"`
+ CommonName string `json:"common_name"`
+ KeyAlgorithm string `json:"key_algorithm"`
+ KeyStrength int `json:"key_strength"`
+ KeyUsage string `json:"key_usage"`
+ Serial string `json:"serial"`
+ SigningAlgorithm string `json:"signing_algorithm"`
+
+ Subject *HostCertificateNameDetails `json:"subject,omitempty"`
+ Issuer *HostCertificateNameDetails `json:"issuer,omitempty"`
+}
+
type HostCertificateNameDetails struct {
CommonName string `json:"common_name"`
Country string `json:"country"`
diff --git a/server/fleet/service.go b/server/fleet/service.go
index 4f524d2a69..1e753ad232 100644
--- a/server/fleet/service.go
+++ b/server/fleet/service.go
@@ -435,6 +435,9 @@ type Service interface {
// the specified host.
ListHostSoftware(ctx context.Context, hostID uint, opts HostSoftwareTitleListOptions) ([]*HostSoftwareWithInstaller, *PaginationMetadata, error)
+ // ListHostCertificates lists the certificates installed on the specified host.
+ ListHostCertificates(ctx context.Context, hostID uint, opts ListOptions) ([]*HostCertificatePayload, *PaginationMetadata, error)
+
// /////////////////////////////////////////////////////////////////////////////
// AppConfigService provides methods for configuring the Fleet application
diff --git a/server/service/devices.go b/server/service/devices.go
index 990dc6718f..f8b927ea2a 100644
--- a/server/service/devices.go
+++ b/server/service/devices.go
@@ -685,3 +685,42 @@ func getDeviceSoftwareEndpoint(ctx context.Context, request interface{}, svc fle
}
return getDeviceSoftwareResponse{Software: res, Meta: meta, Count: int(meta.TotalResults)}, nil //nolint:gosec // dismiss G115
}
+
+////////////////////////////////////////////////////////////////////////////////
+// List Current Device's Certificates
+////////////////////////////////////////////////////////////////////////////////
+
+type listDeviceCertificatesRequest struct {
+ Token string `url:"token"`
+ fleet.ListOptions
+}
+
+func (r *listDeviceCertificatesRequest) deviceAuthToken() string {
+ return r.Token
+}
+
+type listDeviceCertificatesResponse struct {
+ Certificates []*fleet.HostCertificatePayload `json:"certificates"`
+ Meta *fleet.PaginationMetadata `json:"meta,omitempty"`
+ Err error `json:"error,omitempty"`
+}
+
+func (r listDeviceCertificatesResponse) Error() error { return r.Err }
+
+func listDeviceCertificatesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
+ host, ok := hostctx.FromContext(ctx)
+ if !ok {
+ err := ctxerr.Wrap(ctx, fleet.NewAuthRequiredError("internal error: missing host from request context"))
+ return listDevicePoliciesResponse{Err: err}, nil
+ }
+
+ req := request.(*listDeviceCertificatesRequest)
+ res, meta, err := svc.ListHostCertificates(ctx, host.ID, req.ListOptions)
+ if err != nil {
+ return listDeviceCertificatesResponse{Err: err}, nil
+ }
+ if res == nil {
+ res = []*fleet.HostCertificatePayload{}
+ }
+ return listDeviceCertificatesResponse{Certificates: res, Meta: meta}, nil
+}
diff --git a/server/service/handler.go b/server/service/handler.go
index 3b5970bb3b..7b1f0d7669 100644
--- a/server/service/handler.go
+++ b/server/service/handler.go
@@ -409,6 +409,7 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
ue.POST("/api/_version_/fleet/hosts/{id:[0-9]+}/labels", addLabelsToHostEndpoint, addLabelsToHostRequest{})
ue.DELETE("/api/_version_/fleet/hosts/{id:[0-9]+}/labels", removeLabelsFromHostEndpoint, removeLabelsFromHostRequest{})
ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/software", getHostSoftwareEndpoint, getHostSoftwareRequest{})
+ ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/certificates", listHostCertificatesEndpoint, listHostCertificatesRequest{})
ue.GET("/api/_version_/fleet/hosts/summary/mdm", getHostMDMSummary, getHostMDMSummaryRequest{})
ue.GET("/api/_version_/fleet/hosts/{id:[0-9]+}/mdm", getHostMDM, getHostMDMRequest{})
@@ -810,6 +811,9 @@ func attachFleetAPIRoutes(r *mux.Router, svc fleet.Service, config config.FleetC
de.WithCustomMiddleware(
errorLimiter.Limit("install_self_service", desktopQuota),
).POST("/api/_version_/fleet/device/{token}/software/install/{software_title_id}", submitSelfServiceSoftwareInstall, fleetSelfServiceSoftwareInstallRequest{})
+ de.WithCustomMiddleware(
+ errorLimiter.Limit("get_device_certificates", desktopQuota),
+ ).GET("/api/_version_/fleet/device/{token}/certificates", listDeviceCertificatesEndpoint, listDeviceCertificatesRequest{})
// mdm-related endpoints available via device authentication
demdm := de.WithCustomMiddleware(mdmConfiguredMiddleware.VerifyAppleMDM())
diff --git a/server/service/hosts.go b/server/service/hosts.go
index 8c726d5354..08d522e2ad 100644
--- a/server/service/hosts.go
+++ b/server/service/hosts.go
@@ -2715,3 +2715,70 @@ func (svc *Service) ListHostSoftware(ctx context.Context, hostID uint, opts flee
software, meta, err := svc.ds.ListHostSoftware(ctx, host, opts)
return software, meta, ctxerr.Wrap(ctx, err, "list host software")
}
+
+////////////////////////////////////////////////////////////////////////////////
+// Host Certificates
+////////////////////////////////////////////////////////////////////////////////
+
+type listHostCertificatesRequest struct {
+ ID uint `url:"id"`
+ fleet.ListOptions
+}
+
+type listHostCertificatesResponse struct {
+ Certificates []*fleet.HostCertificatePayload `json:"certificates"`
+ Meta *fleet.PaginationMetadata `json:"meta,omitempty"`
+ Err error `json:"error,omitempty"`
+}
+
+func (r listHostCertificatesResponse) Error() error { return r.Err }
+
+func listHostCertificatesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
+ req := request.(*listHostCertificatesRequest)
+ res, meta, err := svc.ListHostCertificates(ctx, req.ID, req.ListOptions)
+ if err != nil {
+ return listHostCertificatesResponse{Err: err}, nil
+ }
+ if res == nil {
+ res = []*fleet.HostCertificatePayload{}
+ }
+ return listHostCertificatesResponse{Certificates: res, Meta: meta}, nil
+}
+
+var listHostCertificatesSortCols = map[string]bool{
+ "common_name": true,
+ "not_valid_after": true,
+}
+
+func (svc *Service) ListHostCertificates(ctx context.Context, hostID uint, opts fleet.ListOptions) ([]*fleet.HostCertificatePayload, *fleet.PaginationMetadata, error) {
+ if !svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken) {
+ host, err := svc.ds.HostLite(ctx, hostID)
+ if err != nil {
+ svc.authz.SkipAuthorization(ctx)
+ return nil, nil, ctxerr.Wrap(ctx, err, "failed to load host")
+ }
+ if err := svc.authz.Authorize(ctx, host, fleet.ActionRead); err != nil {
+ return nil, nil, err
+ }
+ }
+
+ // query/after not supported, always include pagination info
+ opts.MatchQuery = ""
+ opts.After = ""
+ opts.IncludeMetadata = true
+ // default sort order is common name ascending
+ if opts.OrderKey == "" || !listHostCertificatesSortCols[opts.OrderKey] {
+ opts.OrderKey = "common_name"
+ }
+
+ certs, meta, err := svc.ds.ListHostCertificates(ctx, hostID, opts)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ payload := make([]*fleet.HostCertificatePayload, 0, len(certs))
+ for _, cert := range certs {
+ payload = append(payload, cert.ToPayload())
+ }
+ return payload, meta, nil
+}
diff --git a/server/service/hosts_test.go b/server/service/hosts_test.go
index d115b4af21..377f624aa7 100644
--- a/server/service/hosts_test.go
+++ b/server/service/hosts_test.go
@@ -665,6 +665,9 @@ func TestHostAuth(t *testing.T) {
ds.IsHostConnectedToFleetMDMFunc = func(ctx context.Context, host *fleet.Host) (bool, error) {
return true, nil
}
+ ds.ListHostCertificatesFunc = func(ctx context.Context, hostID uint, opts fleet.ListOptions) ([]*fleet.HostCertificateRecord, *fleet.PaginationMetadata, error) {
+ return nil, nil, nil
+ }
testCases := []struct {
name string
@@ -812,6 +815,11 @@ func TestHostAuth(t *testing.T) {
_, _, err = svc.ListHostSoftware(ctx, 2, fleet.HostSoftwareTitleListOptions{})
checkAuthErr(t, tt.shouldFailGlobalRead, err)
+
+ _, _, err = svc.ListHostCertificates(ctx, 1, fleet.ListOptions{})
+ checkAuthErr(t, tt.shouldFailTeamRead, err)
+ _, _, err = svc.ListHostCertificates(ctx, 2, fleet.ListOptions{})
+ checkAuthErr(t, tt.shouldFailGlobalRead, err)
})
}
diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go
index ff3b4c4bbf..ab258cb181 100644
--- a/server/service/integration_core_test.go
+++ b/server/service/integration_core_test.go
@@ -3,6 +3,7 @@ package service
import (
"bytes"
"context"
+ "crypto/sha1" // nolint: gosec
"database/sql"
"encoding/csv"
"encoding/json"
@@ -12974,3 +12975,133 @@ func (s *integrationTestSuite) TestSecretVariables() {
require.Len(t, secrets, 1)
assert.Equal(t, "value", secrets[0].Value)
}
+
+func (s *integrationTestSuite) TestHostCertificates() {
+ t := s.T()
+ ctx := context.Background()
+
+ token := "good_token"
+ host := createOrbitEnrolledHost(t, "linux", "host1", s.ds)
+ createDeviceTokenForHost(t, s.ds, host.ID, token)
+
+ // no certificate at the moment
+ var certResp listHostCertificatesResponse
+ s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/certificates", host.ID), nil, http.StatusOK, &certResp)
+ require.Empty(t, certResp.Certificates)
+
+ certResp = listHostCertificatesResponse{}
+ res := s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/certificates", nil, http.StatusOK)
+ err := json.NewDecoder(res.Body).Decode(&certResp)
+ require.NoError(t, err)
+ require.Empty(t, certResp.Certificates)
+
+ // create some certs for that host
+ certNames := []string{"a", "b", "c", "d", "e"}
+ now := time.Now()
+ // sorting by not_valid_after should get us "d", "c", "e", "a", "b"
+ notValidAfterTimes := []time.Time{
+ now.Add(time.Minute), now.Add(time.Hour),
+ now.Add(time.Second), now.Add(time.Millisecond),
+ now.Add(2 * time.Second)}
+ certs := make([]*fleet.HostCertificateRecord, 0, len(certNames))
+ for i, name := range certNames {
+ certs = append(certs, &fleet.HostCertificateRecord{
+ HostID: host.ID,
+ CommonName: name,
+ SHA1Sum: sha1.New().Sum([]byte(name)), // nolint: gosec
+ SubjectCountry: "s" + name,
+ IssuerCountry: "i" + name,
+ NotValidAfter: notValidAfterTimes[i],
+ })
+ }
+ require.NoError(t, s.ds.UpdateHostCertificates(ctx, host.ID, certs))
+
+ // list all certs
+ certResp = listHostCertificatesResponse{}
+ s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/certificates", host.ID), nil, http.StatusOK, &certResp)
+ require.Len(t, certResp.Certificates, len(certNames))
+ for i, cert := range certResp.Certificates {
+ want := certNames[i]
+ require.Equal(t, want, cert.CommonName)
+ require.NotNil(t, cert.Subject)
+ require.Equal(t, "s"+want, cert.Subject.Country)
+ require.NotNil(t, cert.Issuer)
+ require.Equal(t, "i"+want, cert.Issuer.Country)
+ }
+
+ certResp = listHostCertificatesResponse{}
+ res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/certificates", nil, http.StatusOK)
+ err = json.NewDecoder(res.Body).Decode(&certResp)
+ require.NoError(t, err)
+ require.Len(t, certResp.Certificates, len(certNames))
+ for i, cert := range certResp.Certificates {
+ want := certNames[i]
+ require.Equal(t, want, cert.CommonName)
+ require.NotNil(t, cert.Subject)
+ require.Equal(t, "s"+want, cert.Subject.Country)
+ require.NotNil(t, cert.Issuer)
+ require.Equal(t, "i"+want, cert.Issuer.Country)
+ }
+
+ // non-existing host
+ certResp = listHostCertificatesResponse{}
+ s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/certificates", host.ID+1000), nil, http.StatusNotFound, &certResp)
+ // for the device endpoint, the token is the authentication so if it doesn't
+ // exist, the endpoint is unauthorized.
+ certResp = listHostCertificatesResponse{}
+ s.DoRawNoAuth("GET", "/api/latest/fleet/device/NO-SUCH-TOKEN/certificates", nil, http.StatusUnauthorized)
+
+ pluckCertNames := func(certs []*fleet.HostCertificatePayload) []string {
+ names := make([]string, 0, len(certs))
+ for _, cert := range certs {
+ names = append(names, cert.CommonName)
+ }
+ return names
+ }
+
+ // invalid sort column silently defaults to "common_name"
+ certResp = listHostCertificatesResponse{}
+ s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/certificates", host.ID), nil, http.StatusOK, &certResp, "order_key", "no-such-column")
+ require.Len(t, certResp.Certificates, len(certNames))
+ require.Equal(t, certNames, pluckCertNames(certResp.Certificates))
+
+ certResp = listHostCertificatesResponse{}
+ res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/certificates", nil, http.StatusOK, "order_key", "no-such-column")
+ err = json.NewDecoder(res.Body).Decode(&certResp)
+ require.NoError(t, err)
+ require.Len(t, certResp.Certificates, len(certNames))
+ require.Equal(t, certNames, pluckCertNames(certResp.Certificates))
+
+ // test the pagination options
+ cases := []struct {
+ queryParams []string
+ wantNames []string
+ wantMeta fleet.PaginationMetadata
+ }{
+ {queryParams: []string{"page", "0", "per_page", "2"}, wantNames: []string{"a", "b"}, wantMeta: fleet.PaginationMetadata{HasNextResults: true}},
+ {queryParams: []string{"page", "1", "per_page", "2"}, wantNames: []string{"c", "d"}, wantMeta: fleet.PaginationMetadata{HasNextResults: true, HasPreviousResults: true}},
+ {queryParams: []string{"page", "2", "per_page", "2"}, wantNames: []string{"e"}, wantMeta: fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true}},
+ {queryParams: []string{"page", "3", "per_page", "2"}, wantNames: []string{}, wantMeta: fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true}},
+ {queryParams: []string{"page", "0", "per_page", "4", "order_direction", "desc"}, wantNames: []string{"e", "d", "c", "b"}, wantMeta: fleet.PaginationMetadata{HasNextResults: true}},
+ {queryParams: []string{"page", "1", "per_page", "4", "order_direction", "desc"}, wantNames: []string{"a"}, wantMeta: fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true}},
+ {queryParams: []string{"page", "0", "per_page", "3", "order_key", "not_valid_after"}, wantNames: []string{"d", "c", "e"}, wantMeta: fleet.PaginationMetadata{HasNextResults: true}},
+ {queryParams: []string{"page", "1", "per_page", "3", "order_key", "not_valid_after"}, wantNames: []string{"a", "b"}, wantMeta: fleet.PaginationMetadata{HasNextResults: false, HasPreviousResults: true}},
+ }
+ for _, c := range cases {
+ t.Run(strings.Join(c.queryParams, "_"), func(t *testing.T) {
+ certResp = listHostCertificatesResponse{}
+ s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/certificates", host.ID), nil, http.StatusOK, &certResp, c.queryParams...)
+ require.Len(t, certResp.Certificates, len(c.wantNames))
+ require.Equal(t, c.wantNames, pluckCertNames(certResp.Certificates))
+ require.Equal(t, c.wantMeta, *certResp.Meta)
+
+ certResp = listHostCertificatesResponse{}
+ res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/certificates", nil, http.StatusOK, c.queryParams...)
+ err = json.NewDecoder(res.Body).Decode(&certResp)
+ require.NoError(t, err)
+ require.Len(t, certResp.Certificates, len(c.wantNames))
+ require.Equal(t, c.wantNames, pluckCertNames(certResp.Certificates))
+ require.Equal(t, c.wantMeta, *certResp.Meta)
+ })
+ }
+}
diff --git a/server/service/testing_client.go b/server/service/testing_client.go
index 4d5d01dcaa..378fa8e46a 100644
--- a/server/service/testing_client.go
+++ b/server/service/testing_client.go
@@ -293,8 +293,8 @@ func (ts *withServer) DoRaw(verb string, path string, rawBytes []byte, expectedS
}, queryParams...)
}
-func (ts *withServer) DoRawNoAuth(verb string, path string, rawBytes []byte, expectedStatusCode int) *http.Response {
- return ts.DoRawWithHeaders(verb, path, rawBytes, expectedStatusCode, nil)
+func (ts *withServer) DoRawNoAuth(verb string, path string, rawBytes []byte, expectedStatusCode int, queryParams ...string) *http.Response {
+ return ts.DoRawWithHeaders(verb, path, rawBytes, expectedStatusCode, nil, queryParams...)
}
func (ts *withServer) DoJSON(verb, path string, params interface{}, expectedStatusCode int, v interface{}, queryParams ...string) {
From 38f51ebc4e19d6f22fcb70aef6bcc704f2878225 Mon Sep 17 00:00:00 2001
From: Martin Angers
Date: Mon, 24 Feb 2025 14:30:22 -0500
Subject: [PATCH 08/14] Fix indent
---
frontend/pages/hosts/details/cards/About/_styles.scss | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/pages/hosts/details/cards/About/_styles.scss b/frontend/pages/hosts/details/cards/About/_styles.scss
index 10f08c6e3d..88d5f7b0e9 100644
--- a/frontend/pages/hosts/details/cards/About/_styles.scss
+++ b/frontend/pages/hosts/details/cards/About/_styles.scss
@@ -2,7 +2,7 @@
h2 {
font-size: $medium;
margin: 0 0 $pad-large;
- }
+ }
.truncated-tooltip {
.about-card__device-mapping__source {
From f5cf6a3c6f849f225faeded81055c8b704234644 Mon Sep 17 00:00:00 2001
From: Martin Angers
Date: Tue, 25 Feb 2025 10:42:17 -0500
Subject: [PATCH 09/14] Add validation extension to the make decoder flow,
validate order_key (#26567)
---
server/service/devices.go | 7 +++++++
server/service/hosts.go | 19 +++++++++++++------
server/service/integration_core_test.go | 13 ++++---------
.../endpoint_utils/endpoint_utils.go | 11 +++++++++++
4 files changed, 35 insertions(+), 15 deletions(-)
diff --git a/server/service/devices.go b/server/service/devices.go
index 258d04320e..2c084b8e34 100644
--- a/server/service/devices.go
+++ b/server/service/devices.go
@@ -695,6 +695,13 @@ type listDeviceCertificatesRequest struct {
fleet.ListOptions
}
+func (r *listDeviceCertificatesRequest) ValidateRequest() error {
+ if r.ListOptions.OrderKey != "" && !listHostCertificatesSortCols[r.ListOptions.OrderKey] {
+ return badRequest("invalid order key")
+ }
+ return nil
+}
+
func (r *listDeviceCertificatesRequest) deviceAuthToken() string {
return r.Token
}
diff --git a/server/service/hosts.go b/server/service/hosts.go
index 53384b834e..760b31ce9a 100644
--- a/server/service/hosts.go
+++ b/server/service/hosts.go
@@ -2725,6 +2725,18 @@ type listHostCertificatesRequest struct {
fleet.ListOptions
}
+var listHostCertificatesSortCols = map[string]bool{
+ "common_name": true,
+ "not_valid_after": true,
+}
+
+func (r *listHostCertificatesRequest) ValidateRequest() error {
+ if r.ListOptions.OrderKey != "" && !listHostCertificatesSortCols[r.ListOptions.OrderKey] {
+ return badRequest("invalid order key")
+ }
+ return nil
+}
+
type listHostCertificatesResponse struct {
Certificates []*fleet.HostCertificatePayload `json:"certificates"`
Meta *fleet.PaginationMetadata `json:"meta,omitempty"`
@@ -2745,11 +2757,6 @@ func listHostCertificatesEndpoint(ctx context.Context, request interface{}, svc
return listHostCertificatesResponse{Certificates: res, Meta: meta}, nil
}
-var listHostCertificatesSortCols = map[string]bool{
- "common_name": true,
- "not_valid_after": true,
-}
-
func (svc *Service) ListHostCertificates(ctx context.Context, hostID uint, opts fleet.ListOptions) ([]*fleet.HostCertificatePayload, *fleet.PaginationMetadata, error) {
if !svc.authz.IsAuthenticatedWith(ctx, authzctx.AuthnDeviceToken) {
host, err := svc.ds.HostLite(ctx, hostID)
@@ -2767,7 +2774,7 @@ func (svc *Service) ListHostCertificates(ctx context.Context, hostID uint, opts
opts.After = ""
opts.IncludeMetadata = true
// default sort order is common name ascending
- if opts.OrderKey == "" || !listHostCertificatesSortCols[opts.OrderKey] {
+ if opts.OrderKey == "" {
opts.OrderKey = "common_name"
}
diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go
index dcc2610ab0..9d6476ea7e 100644
--- a/server/service/integration_core_test.go
+++ b/server/service/integration_core_test.go
@@ -13068,18 +13068,13 @@ func (s *integrationTestSuite) TestHostCertificates() {
return names
}
- // invalid sort column silently defaults to "common_name"
+ // fails if order_key is invalid
certResp = listHostCertificatesResponse{}
- s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/certificates", host.ID), nil, http.StatusOK, &certResp, "order_key", "no-such-column")
- require.Len(t, certResp.Certificates, len(certNames))
- require.Equal(t, certNames, pluckCertNames(certResp.Certificates))
+ s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/certificates", host.ID), nil, http.StatusBadRequest, &certResp, "order_key", "no-such-column")
certResp = listHostCertificatesResponse{}
- res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/certificates", nil, http.StatusOK, "order_key", "no-such-column")
- err = json.NewDecoder(res.Body).Decode(&certResp)
- require.NoError(t, err)
- require.Len(t, certResp.Certificates, len(certNames))
- require.Equal(t, certNames, pluckCertNames(certResp.Certificates))
+ res = s.DoRawNoAuth("GET", "/api/latest/fleet/device/"+token+"/certificates", nil, http.StatusBadRequest, "order_key", "no-such-column")
+ require.Contains(t, extractServerErrorText(res.Body), "invalid order key")
// test the pagination options
cases := []struct {
diff --git a/server/service/middleware/endpoint_utils/endpoint_utils.go b/server/service/middleware/endpoint_utils/endpoint_utils.go
index 9c85aa175d..82bcf98d89 100644
--- a/server/service/middleware/endpoint_utils/endpoint_utils.go
+++ b/server/service/middleware/endpoint_utils/endpoint_utils.go
@@ -301,6 +301,12 @@ type requestDecoder interface {
DecodeRequest(ctx context.Context, r *http.Request) (interface{}, error)
}
+// A value that implements requestValidator is called after having the values
+// decoded into it to apply further validations.
+type requestValidator interface {
+ ValidateRequest() error
+}
+
// MakeDecoder creates a decoder for the type for the struct passed on. If the
// struct has at least 1 json tag it'll unmarshall the body. If the struct has
// a `url` tag with value list_options it'll gather fleet.ListOptions from the
@@ -439,6 +445,11 @@ func MakeDecoder(
}
}
+ if rv, ok := v.Interface().(requestValidator); ok {
+ if err := rv.ValidateRequest(); err != nil {
+ return nil, err
+ }
+ }
return v.Interface(), nil
}
}
From f49a45f26d96e50d472726d1ad5a4599c7220761 Mon Sep 17 00:00:00 2001
From: Gabriel Hernandez
Date: Wed, 26 Feb 2025 15:32:53 +0000
Subject: [PATCH 10/14] 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.
---
frontend/__mocks__/certificatesMock.ts | 2 +-
frontend/interfaces/certificates.ts | 2 +-
.../DeviceUserPage/DeviceUserPage.tests.tsx | 63 +++-
.../details/DeviceUserPage/DeviceUserPage.tsx | 59 ++--
.../HostDetailsPage/HostDetailsPage.tsx | 36 ++-
.../CertificatesTable/CertificatesTable.tsx | 3 +
.../CertificateDetailsModal.tsx | 286 +++++++++++-------
frontend/services/entities/device_user.ts | 16 +-
frontend/services/entities/hosts.ts | 22 +-
frontend/test/handlers/device-handler.ts | 15 +
10 files changed, 340 insertions(+), 164 deletions(-)
diff --git a/frontend/__mocks__/certificatesMock.ts b/frontend/__mocks__/certificatesMock.ts
index 49e0678030..9e6fa0f722 100644
--- a/frontend/__mocks__/certificatesMock.ts
+++ b/frontend/__mocks__/certificatesMock.ts
@@ -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",
diff --git a/frontend/interfaces/certificates.ts b/frontend/interfaces/certificates.ts
index c2ec9e7b55..55e76fbe21 100644
--- a/frontend/interfaces/certificates.ts
+++ b/frontend/interfaces/certificates.ts
@@ -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;
diff --git a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tests.tsx b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tests.tsx
index 29d42e4c15..fd8be10b38 100644
--- a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tests.tsx
+++ b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tests.tsx
@@ -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(
{
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(
+
+ );
+
+ // 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(
+
+ );
+
+ // 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) => {
mockServer.use(customDeviceHandler(overrides));
+ mockServer.use(defaultDeviceCertificatesHandler);
const render = createCustomRenderer({
withBackendMock: true,
diff --git a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx
index eeb74c0728..d90ceebd29 100644
--- a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx
+++ b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx
@@ -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 (
- {!host || isLoadingHost ? (
+ {!host || isLoadingHost || isLoadingDeviceCertificates ? (
) : (
@@ -447,15 +453,14 @@ const DeviceUserPage = ({
deviceMapping={deviceMapping}
munki={deviceMacAdminsData?.munki}
/>
- {(isIosOrIpadosHost || isDarwinHost) &&
- deviceCertificates?.certificates.length && (
-
- )}
+ {isAppleHost && deviceCertificates?.certificates.length && (
+
+ )}
{isPremiumTier && isSoftwareEnabled && hasSelfService && (
diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx
index 7afb70d77c..2580cb0cd9 100644
--- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx
+++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx
@@ -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 ;
}
@@ -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,
diff --git a/frontend/pages/hosts/details/cards/Certificates/CertificatesTable/CertificatesTable.tsx b/frontend/pages/hosts/details/cards/Certificates/CertificatesTable/CertificatesTable.tsx
index e6bb565b2a..6733521067 100644
--- a/frontend/pages/hosts/details/cards/Certificates/CertificatesTable/CertificatesTable.tsx
+++ b/frontend/pages/hosts/details/cards/Certificates/CertificatesTable/CertificatesTable.tsx
@@ -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={() => }
+ disablePagination
/>
);
};
diff --git a/frontend/pages/hosts/details/modals/CertificateDetailsModal/CertificateDetailsModal.tsx b/frontend/pages/hosts/details/modals/CertificateDetailsModal/CertificateDetailsModal.tsx
index d976ee3f73..1bcdc3968f 100644
--- a/frontend/pages/hosts/details/modals/CertificateDetailsModal/CertificateDetailsModal.tsx
+++ b/frontend/pages/hosts/details/modals/CertificateDetailsModal/CertificateDetailsModal.tsx
@@ -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 (
<>
-
-
Subject Name
-
-
-
-
-
-
-
-
-
Issuer name
-
-
-
-
-
-
-
-
-
Validity period
-
-
-
-
-
-
-
+ {showSubjectSection && (
+
+
Subject Name
+
+ {subjectCountry && (
+
+ )}
+ {subjectOrganization && (
+
+ )}
+ {subjectOrganizationalUnit && (
+
+ )}
+ {subjectCommonName && (
+
+ )}
+
+
+ )}
+ {showIssuerNameSection && (
+
+
Issuer name
+
+ {issuerCountry && (
+
+ )}
+ {issuerOrganization && (
+
+ )}
+ {issuerOrganizationalUnit && (
+
+ )}
+ {issuerCommonName && (
+
+ )}
+
+
+ )}
+ {showValidityPeriodSection && (
+
+
Validity period
+
+ {not_valid_before && (
+
+ )}
+ {not_valid_after && (
+
+ )}
+
+
+ )}
+ {showKeyInfoSection && (
+
+
Key info
+
+ {key_algorithm && (
+
+ )}
+ {key_strength && (
+
+ )}
+ {key_usage && (
+
+ )}
+ {serial && (
+
+ )}
+
+
+ )}
+ {/* will always show this section */}
-
+ {showSignatureSection && (
+
+ )}
diff --git a/frontend/services/entities/device_user.ts b/frontend/services/entities/device_user.ts
index 57fcb66306..72b7a93ac0 100644
--- a/frontend/services/entities/device_user.ts
+++ b/frontend/services/entities/device_user.ts
@@ -87,14 +87,18 @@ export default {
},
getDeviceCertificates: (
- deviceToken: string
+ deviceToken: string,
+ page = 0,
+ perPage = 10
): Promise
=> {
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);
},
};
diff --git a/frontend/services/entities/hosts.ts b/frontend/services/entities/hosts.ts
index 56b484ee8f..6d21f7500e 100644
--- a/frontend/services/entities/hosts.ts
+++ b/frontend/services/entities/hosts.ts
@@ -602,22 +602,16 @@ export default {
},
getHostCertificates: (
- hostId: number
+ hostId: number,
+ page = 0,
+ perPage = 10
): Promise => {
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);
},
};
diff --git a/frontend/test/handlers/device-handler.ts b/frontend/test/handlers/device-handler.ts
index ea8fb0532c..d089a4a50b 100644
--- a/frontend/test/handlers/device-handler.ts
+++ b/frontend/test/handlers/device-handler.ts
@@ -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({
+ certificates: [createMockHostCertificate()],
+ meta: {
+ has_next_results: false,
+ has_previous_results: false,
+ },
+ });
+ }
+);
From b2bf148b653a2e3738854c853d73675db784579e Mon Sep 17 00:00:00 2001
From: gillespi314 <73313222+gillespi314@users.noreply.github.com>
Date: Wed, 26 Feb 2025 12:49:43 -0600
Subject: [PATCH 11/14] Update migration timestamp
---
...sTable.go => 20250226000000_AddHostCertificatesTable.go} | 6 +++---
server/datastore/mysql/schema.sql | 4 ++--
2 files changed, 5 insertions(+), 5 deletions(-)
rename server/datastore/mysql/migrations/tables/{20250211141712_AddHostCertificatesTable.go => 20250226000000_AddHostCertificatesTable.go} (90%)
diff --git a/server/datastore/mysql/migrations/tables/20250211141712_AddHostCertificatesTable.go b/server/datastore/mysql/migrations/tables/20250226000000_AddHostCertificatesTable.go
similarity index 90%
rename from server/datastore/mysql/migrations/tables/20250211141712_AddHostCertificatesTable.go
rename to server/datastore/mysql/migrations/tables/20250226000000_AddHostCertificatesTable.go
index a374ebdb6c..c8c151b472 100644
--- a/server/datastore/mysql/migrations/tables/20250211141712_AddHostCertificatesTable.go
+++ b/server/datastore/mysql/migrations/tables/20250226000000_AddHostCertificatesTable.go
@@ -5,10 +5,10 @@ import (
)
func init() {
- MigrationClient.AddMigration(Up_20250211141712, Down_20250211141712)
+ MigrationClient.AddMigration(Up_20250226000000, Down_20250226000000)
}
-func Up_20250211141712(tx *sql.Tx) error {
+func Up_20250226000000(tx *sql.Tx) error {
_, err := tx.Exec(`
CREATE TABLE host_certificates (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
@@ -42,6 +42,6 @@ CREATE TABLE host_certificates (
return err
}
-func Down_20250211141712(tx *sql.Tx) error {
+func Down_20250226000000(tx *sql.Tx) error {
return nil
}
diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql
index 6190e3ac1a..8c4eb4629a 100644
--- a/server/datastore/mysql/schema.sql
+++ b/server/datastore/mysql/schema.sql
@@ -1189,9 +1189,9 @@ CREATE TABLE `migration_status_tables` (
`is_applied` tinyint(1) NOT NULL,
`tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
-) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=360 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=361 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
-INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241110152839,1,'2020-01-01 01:01:01'),(328,20241110152840,1,'2020-01-01 01:01:01'),(329,20241110152841,1,'2020-01-01 01:01:01'),(330,20241116233322,1,'2020-01-01 01:01:01'),(331,20241122171434,1,'2020-01-01 01:01:01'),(332,20241125150614,1,'2020-01-01 01:01:01'),(333,20241203125346,1,'2020-01-01 01:01:01'),(334,20241203130032,1,'2020-01-01 01:01:01'),(335,20241205122800,1,'2020-01-01 01:01:01'),(336,20241209164540,1,'2020-01-01 01:01:01'),(337,20241210140021,1,'2020-01-01 01:01:01'),(338,20241219180042,1,'2020-01-01 01:01:01'),(339,20241220100000,1,'2020-01-01 01:01:01'),(340,20241220114903,1,'2020-01-01 01:01:01'),(341,20241220114904,1,'2020-01-01 01:01:01'),(342,20241224000000,1,'2020-01-01 01:01:01'),(343,20241230000000,1,'2020-01-01 01:01:01'),(344,20241231112624,1,'2020-01-01 01:01:01'),(345,20250102121439,1,'2020-01-01 01:01:01'),(346,20250121094045,1,'2020-01-01 01:01:01'),(347,20250121094500,1,'2020-01-01 01:01:01'),(348,20250121094600,1,'2020-01-01 01:01:01'),(349,20250121094700,1,'2020-01-01 01:01:01'),(350,20250124194347,1,'2020-01-01 01:01:01'),(351,20250127162751,1,'2020-01-01 01:01:01'),(352,20250213104005,1,'2020-01-01 01:01:01'),(353,20250214205657,1,'2020-01-01 01:01:01'),(354,20250217093329,1,'2020-01-01 01:01:01'),(355,20250219090511,1,'2020-01-01 01:01:01'),(356,20250219100000,1,'2020-01-01 01:01:01'),(357,20250219142401,1,'2020-01-01 01:01:01'),(358,20250224184002,1,'2020-01-01 01:01:01'),(359,20250225085436,1,'2020-01-01 01:01:01');
+INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241110152839,1,'2020-01-01 01:01:01'),(328,20241110152840,1,'2020-01-01 01:01:01'),(329,20241110152841,1,'2020-01-01 01:01:01'),(330,20241116233322,1,'2020-01-01 01:01:01'),(331,20241122171434,1,'2020-01-01 01:01:01'),(332,20241125150614,1,'2020-01-01 01:01:01'),(333,20241203125346,1,'2020-01-01 01:01:01'),(334,20241203130032,1,'2020-01-01 01:01:01'),(335,20241205122800,1,'2020-01-01 01:01:01'),(336,20241209164540,1,'2020-01-01 01:01:01'),(337,20241210140021,1,'2020-01-01 01:01:01'),(338,20241219180042,1,'2020-01-01 01:01:01'),(339,20241220100000,1,'2020-01-01 01:01:01'),(340,20241220114903,1,'2020-01-01 01:01:01'),(341,20241220114904,1,'2020-01-01 01:01:01'),(342,20241224000000,1,'2020-01-01 01:01:01'),(343,20241230000000,1,'2020-01-01 01:01:01'),(344,20241231112624,1,'2020-01-01 01:01:01'),(345,20250102121439,1,'2020-01-01 01:01:01'),(346,20250121094045,1,'2020-01-01 01:01:01'),(347,20250121094500,1,'2020-01-01 01:01:01'),(348,20250121094600,1,'2020-01-01 01:01:01'),(349,20250121094700,1,'2020-01-01 01:01:01'),(350,20250124194347,1,'2020-01-01 01:01:01'),(351,20250127162751,1,'2020-01-01 01:01:01'),(352,20250213104005,1,'2020-01-01 01:01:01'),(353,20250214205657,1,'2020-01-01 01:01:01'),(354,20250217093329,1,'2020-01-01 01:01:01'),(355,20250219090511,1,'2020-01-01 01:01:01'),(356,20250219100000,1,'2020-01-01 01:01:01'),(357,20250219142401,1,'2020-01-01 01:01:01'),(358,20250224184002,1,'2020-01-01 01:01:01'),(359,20250225085436,1,'2020-01-01 01:01:01'),(360,20250226000000,1,'2020-01-01 01:01:01');
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `mobile_device_management_solutions` (
From e02ad241ea9802275d5d12feeb5540300247c220 Mon Sep 17 00:00:00 2001
From: Sarah Gillespie <73313222+gillespi314@users.noreply.github.com>
Date: Wed, 26 Feb 2025 15:59:20 -0600
Subject: [PATCH 12/14] Allow empty values when parsing distinguished name
(#26627)
---
server/fleet/host_certificates.go | 4 ----
server/fleet/host_certificates_test.go | 7 ++++++-
2 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/server/fleet/host_certificates.go b/server/fleet/host_certificates.go
index 0d7a27975d..cfb5448ae8 100644
--- a/server/fleet/host_certificates.go
+++ b/server/fleet/host_certificates.go
@@ -203,10 +203,6 @@ func ExtractDetailsFromOsqueryDistinguishedName(str string) (*HostCertificateNam
return nil, errors.New("invalid distinguished name, wrong key value pair format")
}
- if len(kv[1]) == 0 {
- return nil, errors.New("invalid distinguished name, missing value")
- }
-
switch strings.ToUpper(kv[0]) {
case "C":
details.Country = strings.Trim(kv[1], " ")
diff --git a/server/fleet/host_certificates_test.go b/server/fleet/host_certificates_test.go
index e6c6219e14..e57bdfe4d8 100644
--- a/server/fleet/host_certificates_test.go
+++ b/server/fleet/host_certificates_test.go
@@ -73,7 +73,12 @@ func TestExtractHostCertificateNameDetails(t *testing.T) {
{
name: "missing value",
input: "/C=US/O=Fleet Device Management Inc./OU=Fleet Device Management Inc./CN=",
- err: true,
+ expected: &HostCertificateNameDetails{
+ Country: "US",
+ Organization: "Fleet Device Management Inc.",
+ OrganizationalUnit: "Fleet Device Management Inc.",
+ CommonName: "",
+ },
},
{
name: "missing first slash",
From ecaea6104de643c8e84f3d93c72c8271eb67e68f Mon Sep 17 00:00:00 2001
From: Sarah Gillespie <73313222+gillespi314@users.noreply.github.com>
Date: Thu, 27 Feb 2025 05:33:49 -0600
Subject: [PATCH 13/14] Add host certificates refetch to UI (#26630)
Fix unreleased UI bugs
---
frontend/interfaces/platform.ts | 2 +-
.../details/DeviceUserPage/DeviceUserPage.tsx | 45 +++++++------
.../HostDetailsPage/HostDetailsPage.tsx | 67 ++++++++++---------
3 files changed, 60 insertions(+), 54 deletions(-)
diff --git a/frontend/interfaces/platform.ts b/frontend/interfaces/platform.ts
index 753cfc41b8..511b8b4f47 100644
--- a/frontend/interfaces/platform.ts
+++ b/frontend/interfaces/platform.ts
@@ -128,7 +128,7 @@ export const isLinuxLike = (platform: string) => {
);
};
-export const isAppleDevice = (platform: string) => {
+export const isAppleDevice = (platform = "") => {
return HOST_APPLE_PLATFORMS.includes(
platform as typeof HOST_APPLE_PLATFORMS[number]
);
diff --git a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx
index d90ceebd29..ac66fd66e9 100644
--- a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx
+++ b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx
@@ -157,8 +157,31 @@ const DeviceUserPage = ({
}
);
+ const {
+ data: deviceCertificates,
+ isLoading: isLoadingDeviceCertificates,
+ isError: isErrorDeviceCertificates,
+ refetch: refetchDeviceCertificates,
+ } = useQuery(
+ ["hostCertificates", deviceAuthToken],
+ () =>
+ deviceUserAPI.getDeviceCertificates(
+ deviceAuthToken,
+ DEFAULT_CERTIFICATES_PAGE,
+ DEFAULT_CERTIFICATES_PAGE_SIZE
+ ),
+ {
+ ...DEFAULT_USE_QUERY_OPTIONS,
+ // FIXME: is it worth disabling for unsupported platforms? we'd have to workaround the a
+ // catch-22 where we need to know the platform to know if it's supported but we also need to
+ // be able to include the cert refetch in the hosts query hook.
+ enabled: !!deviceUserAPI,
+ }
+ );
+
const refetchExtensions = () => {
deviceMapping !== null && refetchDeviceMapping();
+ deviceCertificates && refetchDeviceCertificates();
};
const isRefetching = ({
@@ -253,25 +276,7 @@ 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 isAppleHost = isAppleDevice(host?.platform);
const summaryData = normalizeEmptyValues(pick(host, HOST_SUMMARY_DATA));
@@ -453,7 +458,7 @@ const DeviceUserPage = ({
deviceMapping={deviceMapping}
munki={deviceMacAdminsData?.munki}
/>
- {isAppleHost && deviceCertificates?.certificates.length && (
+ {isAppleHost && !!deviceCertificates?.certificates.length && (
(
+ [
+ "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,
+ // FIXME: is it worth disabling for unsupported platforms? we'd have to workaround the a
+ // catch-22 where we need to know the platform to know if it's supported but we also need to
+ // be able to include the cert refetch in the hosts query hook.
+ enabled: !!hostIdFromURL,
+ }
+ );
+
const refetchExtensions = () => {
deviceMapping !== null && refetchDeviceMapping();
macadmins !== null && refetchMacadmins();
mdm?.enrollment_status !== null && refetchMdm();
+ hostCertificates && refetchHostCertificates();
};
const {
@@ -462,37 +494,6 @@ const HostDetailsPage = ({
}
);
- const {
- data: hostCertificates,
- isLoading: isLoadingHostCertificates,
- isError: isErrorHostCertificates,
- } = 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 &&
- (host?.platform === "darwin" ||
- host?.platform === "ios" ||
- host?.platform === "ipados"),
- }
- );
-
const featuresConfig = host?.team_id
? teams?.find((t) => t.id === host.team_id)?.features
: config?.features;
@@ -957,7 +958,7 @@ const HostDetailsPage = ({
/>
)}
{(isIosOrIpadosHost || isDarwinHost) &&
- hostCertificates?.certificates.length && (
+ !!hostCertificates?.certificates.length && (
- {host?.platform === "darwin" && macadmins?.munki?.version && (
+ {isDarwinHost && macadmins?.munki?.version && (
Date: Thu, 27 Feb 2025 16:57:41 +0000
Subject: [PATCH 14/14] fix single row click issue and pagination (#26644)
For #25464
quick fix for clicking rows and adding errors and pagination to UI
---
.../details/DeviceUserPage/DeviceUserPage.tsx | 42 ++++++++++++----
.../HostDetailsPage/HostDetailsPage.tsx | 43 ++++++++++-------
.../cards/Certificates/Certificates.tsx | 33 +++++++++++--
.../CertificatesTable/CertificatesTable.tsx | 48 +++++++++++++++----
4 files changed, 126 insertions(+), 40 deletions(-)
diff --git a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx
index ac66fd66e9..0730513d33 100644
--- a/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx
+++ b/frontend/pages/hosts/details/DeviceUserPage/DeviceUserPage.tsx
@@ -6,7 +6,9 @@ import { Tab, Tabs, TabList, TabPanel } from "react-tabs";
import { pick, findIndex } from "lodash";
import { NotificationContext } from "context/notification";
-import deviceUserAPI from "services/entities/device_user";
+import deviceUserAPI, {
+ IGetDeviceCertificatesResponse,
+} from "services/entities/device_user";
import diskEncryptionAPI from "services/entities/disk_encryption";
import {
IDeviceMappingResponse,
@@ -75,7 +77,7 @@ const FREE_TAB_PATHS = [
PATHS.DEVICE_USER_DETAILS_SOFTWARE,
] as const;
-const DEFAULT_CERTIFICATES_PAGE_SIZE = 500;
+const DEFAULT_CERTIFICATES_PAGE_SIZE = 10;
const DEFAULT_CERTIFICATES_PAGE = 0;
interface IDeviceUserPageProps {
@@ -125,10 +127,15 @@ const DeviceUserPage = ({
selectedSoftwareDetails,
setSelectedSoftwareDetails,
] = useState(null);
+
+ // certificates states
const [
selectedCertificate,
setSelectedCertificate,
] = useState(null);
+ const [certificatePage, setCertificatePage] = useState(
+ DEFAULT_CERTIFICATES_PAGE
+ );
const { data: deviceMapping, refetch: refetchDeviceMapping } = useQuery(
["deviceMapping", deviceAuthToken],
@@ -162,14 +169,22 @@ const DeviceUserPage = ({
isLoading: isLoadingDeviceCertificates,
isError: isErrorDeviceCertificates,
refetch: refetchDeviceCertificates,
- } = useQuery(
- ["hostCertificates", deviceAuthToken],
- () =>
- deviceUserAPI.getDeviceCertificates(
- deviceAuthToken,
- DEFAULT_CERTIFICATES_PAGE,
- DEFAULT_CERTIFICATES_PAGE_SIZE
- ),
+ } = useQuery<
+ IGetDeviceCertificatesResponse,
+ Error,
+ IGetDeviceCertificatesResponse,
+ Array<{ scope: string; token: string; page: number; perPage: number }>
+ >(
+ [
+ {
+ scope: "device-certificates",
+ token: deviceAuthToken,
+ page: certificatePage,
+ perPage: DEFAULT_CERTIFICATES_PAGE_SIZE,
+ },
+ ],
+ ({ queryKey: [{ token, page, perPage }] }) =>
+ deviceUserAPI.getDeviceCertificates(token, page, perPage),
{
...DEFAULT_USE_QUERY_OPTIONS,
// FIXME: is it worth disabling for unsupported platforms? we'd have to workaround the a
@@ -462,8 +477,15 @@ const DeviceUserPage = ({
setCertificatePage(certificatePage + 1)}
+ onPreviousPage={() =>
+ setCertificatePage(certificatePage - 1)
+ }
/>
)}
diff --git a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx
index cbed2f3ebd..92fc9f327b 100644
--- a/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx
+++ b/frontend/pages/hosts/details/HostDetailsPage/HostDetailsPage.tsx
@@ -132,7 +132,7 @@ interface IHostDetailsSubNavItem {
}
const DEFAULT_ACTIVITY_PAGE_SIZE = 8;
-const DEFAULT_CERTIFICATES_PAGE_SIZE = 500;
+const DEFAULT_CERTIFICATES_PAGE_SIZE = 10;
const DEFAULT_CERTIFICATES_PAGE = 0;
const HostDetailsPage = ({
@@ -208,10 +208,6 @@ const HostDetailsPage = ({
selectedCancelActivity,
setSelectedCancelActivity,
] = useState(null);
- const [
- selectedCertificate,
- setSelectedCertificate,
- ] = useState(null);
// activity states
const [activeActivityTab, setActiveActivityTab] = useState<
@@ -219,6 +215,15 @@ const HostDetailsPage = ({
>("past");
const [activityPage, setActivityPage] = useState(0);
+ // certificates states
+ const [
+ selectedCertificate,
+ setSelectedCertificate,
+ ] = useState(null);
+ const [certificatePage, setCertificatePage] = useState(
+ DEFAULT_CERTIFICATES_PAGE
+ );
+
const { data: teams } = useQuery(
"teams",
() => teamAPI.loadAll(),
@@ -282,20 +287,19 @@ const HostDetailsPage = ({
} = useQuery<
IGetHostCertificatesResponse,
Error,
- IGetHostCertificatesResponse
+ IGetHostCertificatesResponse,
+ Array<{ scope: string; hostId: number; page: number; perPage: number }>
>(
[
- "host-certificates",
- host_id,
- DEFAULT_CERTIFICATES_PAGE,
- DEFAULT_CERTIFICATES_PAGE_SIZE,
+ {
+ scope: "host-certificates",
+ hostId: hostIdFromURL,
+ page: certificatePage,
+ perPage: DEFAULT_CERTIFICATES_PAGE_SIZE,
+ },
],
- () =>
- hostAPI.getHostCertificates(
- hostIdFromURL,
- DEFAULT_CERTIFICATES_PAGE,
- DEFAULT_CERTIFICATES_PAGE_SIZE
- ),
+ ({ queryKey: [{ hostId, page, perPage }] }) =>
+ hostAPI.getHostCertificates(hostId, page, perPage),
{
...DEFAULT_USE_QUERY_OPTIONS,
// FIXME: is it worth disabling for unsupported platforms? we'd have to workaround the a
@@ -963,6 +967,13 @@ const HostDetailsPage = ({
data={hostCertificates}
hostPlatform={host.platform}
onSelectCertificate={onSelectCertificate}
+ isError={isErrorHostCertificates}
+ page={certificatePage}
+ pageSize={DEFAULT_CERTIFICATES_PAGE_SIZE}
+ onNextPage={() => setCertificatePage(certificatePage + 1)}
+ onPreviousPage={() =>
+ setCertificatePage(certificatePage - 1)
+ }
/>
)}
diff --git a/frontend/pages/hosts/details/cards/Certificates/Certificates.tsx b/frontend/pages/hosts/details/cards/Certificates/Certificates.tsx
index d003dfaea1..56433cc922 100644
--- a/frontend/pages/hosts/details/cards/Certificates/Certificates.tsx
+++ b/frontend/pages/hosts/details/cards/Certificates/Certificates.tsx
@@ -5,6 +5,7 @@ import { HostPlatform } from "interfaces/platform";
import { IGetHostCertificatesResponse } from "services/entities/hosts";
import Card from "components/Card";
+import DataError from "components/DataError";
import CertificatesTable from "./CertificatesTable";
@@ -13,16 +14,42 @@ const baseClass = "certificates-card";
interface ICertificatesProps {
data: IGetHostCertificatesResponse;
hostPlatform: HostPlatform;
+ page: number;
+ pageSize: number;
+ isError: boolean;
isMyDevicePage?: boolean;
onSelectCertificate: (certificate: IHostCertificate) => void;
+ onNextPage: () => void;
+ onPreviousPage: () => void;
}
const CertificatesCard = ({
data,
hostPlatform,
+ isError,
+ page,
+ pageSize,
isMyDevicePage = false,
onSelectCertificate,
+ onNextPage,
+ onPreviousPage,
}: ICertificatesProps) => {
+ const renderContent = () => {
+ if (isError) return ;
+
+ return (
+
+ );
+ };
+
return (
Certificates
-
+ {renderContent()}
);
};
diff --git a/frontend/pages/hosts/details/cards/Certificates/CertificatesTable/CertificatesTable.tsx b/frontend/pages/hosts/details/cards/Certificates/CertificatesTable/CertificatesTable.tsx
index 6733521067..9ae3fc00fa 100644
--- a/frontend/pages/hosts/details/cards/Certificates/CertificatesTable/CertificatesTable.tsx
+++ b/frontend/pages/hosts/details/cards/Certificates/CertificatesTable/CertificatesTable.tsx
@@ -1,33 +1,57 @@
-import React from "react";
-import { Row } from "react-table";
+import React, { useCallback } from "react";
import { IHostCertificate } from "interfaces/certificates";
+import { IGetHostCertificatesResponse } from "services/entities/hosts";
import TableContainer from "components/TableContainer";
import CustomLink from "components/CustomLink";
import TableCount from "components/TableContainer/TableCount";
+import { ITableQueryData } from "components/TableContainer/TableContainer";
import generateTableConfig from "./CertificatesTableConfig";
const baseClass = "certificates-table";
interface ICertificatesTableProps {
- data: IHostCertificate[];
+ data: IGetHostCertificatesResponse;
showHelpText: boolean;
+ page: number;
+ pageSize: number;
onSelectCertificate: (certificate: IHostCertificate) => void;
+ onNextPage: () => void;
+ onPreviousPage: () => void;
}
const CertificatesTable = ({
data,
showHelpText,
+ page,
+ pageSize,
onSelectCertificate,
+ onNextPage,
+ onPreviousPage,
}: ICertificatesTableProps) => {
const tableConfig = generateTableConfig();
- const onClickTableRow = (row: Row) => {
+ const onClickTableRow = (row: any) => {
onSelectCertificate(row.original);
};
+ const onQueryChange = useCallback(
+ async (newTableQuery: ITableQueryData) => {
+ console.log(newTableQuery);
+
+ if (page === newTableQuery.pageIndex) return;
+
+ if (newTableQuery.pageIndex > page) {
+ onNextPage();
+ } else {
+ onPreviousPage();
+ }
+ },
+ [onNextPage, onPreviousPage, page]
+ );
+
const helpText = showHelpText ? (
Showing certificates in the system keychain. To get all certificates, you
@@ -41,18 +65,24 @@ const CertificatesTable = ({
) : null;
return (
- >
+ null}
isAllPagesSelected={false}
showMarkAllPages={false}
isLoading={false}
- onClickRow={onClickTableRow}
+ disableMultiRowSelect
+ onSelectSingleRow={onClickTableRow}
renderTableHelpText={() => helpText}
- renderCount={() => }
- disablePagination
+ renderCount={() => (
+
+ )}
+ pageSize={pageSize}
+ defaultPageIndex={page}
+ onQueryChange={onQueryChange}
+ disableNextPage={data?.meta.has_next_results === false}
/>
);
};