Show certificates actual total count in table (#32972)
fixes: #32103 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. ## Testing - [x] Added/updated automated tests - [x] QA'd all new/changed functionality manually <img width="1682" height="688" alt="image" src="https://github.com/user-attachments/assets/d4f59612-782e-4747-9090-b2895edc76ba" />
This commit is contained in:
@@ -0,0 +1 @@
|
||||
* Return count in list host certificates API response, and use it in the certificate table
|
||||
@@ -43,6 +43,7 @@ const DEFAULT_HOST_CERTIFICATES_RESPONSE_MOCK: IGetHostCertificatesResponse = {
|
||||
has_next_results: false,
|
||||
has_previous_results: false,
|
||||
},
|
||||
count: 1,
|
||||
};
|
||||
|
||||
export const createMockGetHostCertificatesResponse = (
|
||||
|
||||
+1
-3
@@ -88,9 +88,7 @@ const CertificatesTable = ({
|
||||
disableMultiRowSelect
|
||||
onSelectSingleRow={onClickTableRow}
|
||||
renderTableHelpText={() => helpText}
|
||||
renderCount={() => (
|
||||
<TableCount name="certificates" count={data.certificates.length} />
|
||||
)}
|
||||
renderCount={() => <TableCount name="certificates" count={data.count} />}
|
||||
pageSize={pageSize}
|
||||
pageIndex={page}
|
||||
defaultSortHeader={sortHeader}
|
||||
|
||||
@@ -43,6 +43,7 @@ export interface IGetDeviceCertificatesResponse {
|
||||
has_next_results: boolean;
|
||||
has_previous_results: boolean;
|
||||
};
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface IGetDeviceCertsRequestParams extends IListOptions {
|
||||
|
||||
@@ -216,6 +216,7 @@ export interface IGetHostCertificatesResponse {
|
||||
has_next_results: boolean;
|
||||
has_previous_results: boolean;
|
||||
};
|
||||
count: number;
|
||||
}
|
||||
|
||||
export type ILoadHostDetailsExtension = "macadmins";
|
||||
|
||||
@@ -79,6 +79,7 @@ export const defaultDeviceCertificatesHandler = http.get(
|
||||
has_next_results: false,
|
||||
has_previous_results: false,
|
||||
},
|
||||
count: 1,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -254,7 +254,6 @@ func loadHostCertIDsForSHA1DB(ctx context.Context, tx sqlx.QueryerContext, hostI
|
||||
|
||||
var certs []*fleet.HostCertificateRecord
|
||||
stmt, args, err := sqlx.In(stmt, binarySHA1s, hostID)
|
||||
|
||||
if err != nil {
|
||||
return nil, ctxerr.Wrap(ctx, err, "building load host cert ids query")
|
||||
}
|
||||
@@ -271,7 +270,16 @@ func loadHostCertIDsForSHA1DB(ctx context.Context, tx sqlx.QueryerContext, hostI
|
||||
}
|
||||
|
||||
func listHostCertsDB(ctx context.Context, tx sqlx.QueryerContext, hostID uint, opts fleet.ListOptions) ([]*fleet.HostCertificateRecord, *fleet.PaginationMetadata, error) {
|
||||
stmt := `
|
||||
const fromWhereClause = `
|
||||
FROM
|
||||
host_certificates hc
|
||||
INNER JOIN host_certificate_sources hcs ON hc.id = hcs.host_certificate_id
|
||||
WHERE
|
||||
hc.host_id = ?
|
||||
AND hc.deleted_at IS NULL
|
||||
`
|
||||
|
||||
stmt := fmt.Sprintf(`
|
||||
SELECT
|
||||
hc.id,
|
||||
hc.sha1_sum,
|
||||
@@ -297,15 +305,14 @@ SELECT
|
||||
hc.issuer_common_name,
|
||||
hcs.source,
|
||||
hcs.username
|
||||
FROM
|
||||
host_certificates hc
|
||||
INNER JOIN host_certificate_sources hcs ON hc.id = hcs.host_certificate_id
|
||||
WHERE
|
||||
hc.host_id = ?
|
||||
AND hc.deleted_at IS NULL`
|
||||
%s`, fromWhereClause)
|
||||
|
||||
args := []interface{}{hostID}
|
||||
stmtPaged, args := appendListOptionsWithCursorToSQL(stmt, args, &opts)
|
||||
countStmt := fmt.Sprintf(`
|
||||
SELECT COUNT(*) %s
|
||||
`, fromWhereClause)
|
||||
|
||||
baseArgs := []interface{}{hostID}
|
||||
stmtPaged, args := appendListOptionsWithCursorToSQL(stmt, baseArgs, &opts)
|
||||
|
||||
var certs []*fleet.HostCertificateRecord
|
||||
if err := sqlx.SelectContext(ctx, tx, &certs, stmtPaged, args...); err != nil {
|
||||
@@ -314,7 +321,11 @@ WHERE
|
||||
|
||||
var metaData *fleet.PaginationMetadata
|
||||
if opts.IncludeMetadata {
|
||||
metaData = &fleet.PaginationMetadata{HasPreviousResults: opts.Page > 0}
|
||||
var count uint
|
||||
if err := sqlx.GetContext(ctx, tx, &count, countStmt, baseArgs...); err != nil {
|
||||
return nil, nil, ctxerr.Wrap(ctx, err, "counting host certificates")
|
||||
}
|
||||
metaData = &fleet.PaginationMetadata{HasPreviousResults: opts.Page > 0, TotalResults: count}
|
||||
if len(certs) > int(opts.PerPage) { //nolint:gosec // dismiss G115
|
||||
metaData.HasNextResults = true
|
||||
certs = certs[:len(certs)-1]
|
||||
|
||||
@@ -32,6 +32,7 @@ func TestHostCertificates(t *testing.T) {
|
||||
{"Update certificate sources isolation", testUpdateHostCertificatesSourcesIsolation},
|
||||
{"Create certificates with long country code", testHostCertificateWithInvalidCountryCode},
|
||||
{"Truncate long certificate fields", testTruncateLongCertificateFields},
|
||||
{"Count matches main query", testListHostCertificatesCountMatches},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
@@ -79,7 +80,7 @@ func testUpdateAndListHostCertificates(t *testing.T, ds *Datastore) {
|
||||
require.NoError(t, ds.UpdateHostCertificates(ctx, 1, "95816502-d8c0-462c-882f-39991cc89a0c", payload))
|
||||
|
||||
// verify that we saved the records correctly
|
||||
certs, _, err := ds.ListHostCertificates(ctx, 1, fleet.ListOptions{OrderKey: "common_name"})
|
||||
certs, meta, err := ds.ListHostCertificates(ctx, 1, fleet.ListOptions{OrderKey: "common_name", IncludeMetadata: true})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, certs, 2)
|
||||
require.Equal(t, expected2.Subject.CommonName, certs[0].CommonName)
|
||||
@@ -88,6 +89,7 @@ func testUpdateAndListHostCertificates(t *testing.T, ds *Datastore) {
|
||||
require.Equal(t, expected1.Subject.CommonName, certs[1].CommonName)
|
||||
require.Equal(t, expected1.Subject.CommonName, certs[1].SubjectCommonName)
|
||||
require.Equal(t, fleet.SystemHostCertificate, certs[1].Source)
|
||||
require.EqualValues(t, 2, meta.TotalResults)
|
||||
|
||||
// order by not_valid_after descending
|
||||
certs, _, err = ds.ListHostCertificates(ctx, 1, fleet.ListOptions{OrderKey: "not_valid_after", OrderDirection: fleet.OrderAscending})
|
||||
@@ -728,3 +730,67 @@ func testTruncateLongCertificateFields(t *testing.T, ds *Datastore) {
|
||||
assert.Equal(t, fleet.UserHostCertificate, savedCert.Source, "Source should not be changed")
|
||||
assert.Equal(t, host.ID, savedCert.HostID, "HostID should not be changed")
|
||||
}
|
||||
|
||||
func testListHostCertificatesCountMatches(t *testing.T, ds *Datastore) {
|
||||
ctx := context.Background()
|
||||
|
||||
// create host
|
||||
host, err := ds.NewHost(ctx, &fleet.Host{
|
||||
DetailUpdatedAt: time.Now(),
|
||||
LabelUpdatedAt: time.Now(),
|
||||
PolicyUpdatedAt: time.Now(),
|
||||
SeenTime: time.Now(),
|
||||
OsqueryHostID: ptr.String("count-mismatch-host-osquery-id"),
|
||||
NodeKey: ptr.String("count-mismatch-host-node-key"),
|
||||
UUID: "count-mismatch-host-uuid",
|
||||
Hostname: "count-mismatch-host",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// create a cert template and record
|
||||
certTemplate := x509.Certificate{
|
||||
Subject: pkix.Name{
|
||||
Country: []string{"US"},
|
||||
CommonName: "count.example.com",
|
||||
Organization: []string{"Org"},
|
||||
OrganizationalUnit: []string{"Eng"},
|
||||
},
|
||||
Issuer: pkix.Name{
|
||||
Country: []string{"US"},
|
||||
CommonName: "issuer.example.com",
|
||||
Organization: []string{"Issuer"},
|
||||
},
|
||||
SerialNumber: big.NewInt(424242),
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
|
||||
SignatureAlgorithm: x509.SHA256WithRSA,
|
||||
NotBefore: time.Now().Add(-time.Hour).Truncate(time.Second).UTC(),
|
||||
NotAfter: time.Now().Add(24 * time.Hour).Truncate(time.Second).UTC(),
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
|
||||
certRec := generateTestHostCertificateRecord(t, host.ID, &certTemplate)
|
||||
|
||||
// Update using ds.UpdateHostCertificates with two sources: system and user
|
||||
certSys := *certRec
|
||||
certSys.Source = fleet.SystemHostCertificate
|
||||
certSys.Username = ""
|
||||
|
||||
certUser := *certRec
|
||||
certUser.Source = fleet.UserHostCertificate
|
||||
certUser.Username = "alice"
|
||||
|
||||
require.NoError(t, ds.UpdateHostCertificates(ctx, host.ID, host.UUID, []*fleet.HostCertificateRecord{&certSys, &certUser}))
|
||||
|
||||
// Now list with metadata
|
||||
certs, meta, err := ds.ListHostCertificates(ctx, host.ID, fleet.ListOptions{IncludeMetadata: true})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NotNil(t, meta)
|
||||
|
||||
// We expect two returned rows (one per source)
|
||||
require.Len(t, certs, 2)
|
||||
|
||||
require.Equal(t, uint(len(certs)), meta.TotalResults, "expected total results to match returned rows")
|
||||
}
|
||||
|
||||
@@ -883,6 +883,7 @@ func (r *listDeviceCertificatesRequest) deviceAuthToken() string {
|
||||
type listDeviceCertificatesResponse struct {
|
||||
Certificates []*fleet.HostCertificatePayload `json:"certificates"`
|
||||
Meta *fleet.PaginationMetadata `json:"meta,omitempty"`
|
||||
Count uint `json:"count"`
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
@@ -903,7 +904,7 @@ func listDeviceCertificatesEndpoint(ctx context.Context, request interface{}, sv
|
||||
if res == nil {
|
||||
res = []*fleet.HostCertificatePayload{}
|
||||
}
|
||||
return listDeviceCertificatesResponse{Certificates: res, Meta: meta}, nil
|
||||
return listDeviceCertificatesResponse{Certificates: res, Meta: meta, Count: meta.TotalResults}, nil
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -2972,6 +2972,7 @@ func (r *listHostCertificatesRequest) ValidateRequest() error {
|
||||
type listHostCertificatesResponse struct {
|
||||
Certificates []*fleet.HostCertificatePayload `json:"certificates"`
|
||||
Meta *fleet.PaginationMetadata `json:"meta,omitempty"`
|
||||
Count uint `json:"count"`
|
||||
Err error `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
@@ -2986,7 +2987,7 @@ func listHostCertificatesEndpoint(ctx context.Context, request interface{}, svc
|
||||
if res == nil {
|
||||
res = []*fleet.HostCertificatePayload{}
|
||||
}
|
||||
return listHostCertificatesResponse{Certificates: res, Meta: meta}, nil
|
||||
return listHostCertificatesResponse{Certificates: res, Meta: meta, Count: meta.TotalResults}, nil
|
||||
}
|
||||
|
||||
func (svc *Service) ListHostCertificates(ctx context.Context, hostID uint, opts fleet.ListOptions) ([]*fleet.HostCertificatePayload, *fleet.PaginationMetadata, error) {
|
||||
|
||||
Reference in New Issue
Block a user