dashboard: Add certificate widget that displays CAs and Certs sorted by expiration date (#8105)

* dashboard: Add certificate widget that displays CAs and Certs sorted by expiration date

* dashboard: Certificate widget, fix certificate hiding configuration, refresh immediately on config change, increase tick timeout

* dashboard: Certificate widget, different text for expired certificates

* dashboard: Certificate widget, create links that fill the search-field of the bootgrid to display the certificate directly

* dashboard: Certificate widget, search for uuid in bootgrid and call corresponding form
This commit is contained in:
Monviech 2024-12-05 11:31:39 +01:00 committed by GitHub
parent f4b9017cd9
commit 397a3dcdce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 264 additions and 2 deletions

1
plist
View File

@ -2070,6 +2070,7 @@
/usr/local/opnsense/www/js/widgets/BaseTableWidget.js
/usr/local/opnsense/www/js/widgets/BaseWidget.js
/usr/local/opnsense/www/js/widgets/Carp.js
/usr/local/opnsense/www/js/widgets/Certificates.js
/usr/local/opnsense/www/js/widgets/Cpu.js
/usr/local/opnsense/www/js/widgets/Disk.js
/usr/local/opnsense/www/js/widgets/Firewall.js

View File

@ -142,7 +142,7 @@ class CaController extends ApiMutableModelControllerBase
{
return $this->searchBase(
'ca',
['refid', 'descr', 'caref', 'name', 'refcount', 'valid_from', 'valid_to'],
['uuid', 'refid', 'descr', 'caref', 'name', 'refcount', 'valid_from', 'valid_to'],
);
}

View File

@ -186,7 +186,7 @@ class CertController extends ApiMutableModelControllerBase
return $this->searchBase(
'cert',
[
'refid', 'descr', 'caref', 'rfc3280_purpose', 'name',
'uuid', 'refid', 'descr', 'caref', 'rfc3280_purpose', 'name',
'valid_from', 'valid_to' , 'in_use', 'is_user', 'commonname'
],
null,

View File

@ -160,6 +160,34 @@
}
}
});
/* For certificate dashboard widget */
function handleSearchAndEdit() {
const hash = window.location.hash;
if (hash.includes('#SearchPhrase=')) {
const searchPhrase = decodeURIComponent(hash.split('=')[1]);
const searchField = $('.search-field');
if (searchField.val() !== searchPhrase) {
searchField.val(searchPhrase).trigger('keyup');
// Wait for grid to reload after search and simulate edit button click
$('#grid-cert').one("loaded.rs.jquery.bootgrid", function () {
const editButton = $(`#grid-cert .command-edit[data-row-id="${searchPhrase}"]`);
if (editButton.length) {
editButton.trigger('click');
}
});
history.replaceState(null, null, window.location.pathname + window.location.search);
}
}
}
$('#grid-cert').on("loaded.rs.jquery.bootgrid", handleSearchAndEdit);
$(window).on('hashchange', handleSearchAndEdit);
});
</script>

View File

@ -281,6 +281,33 @@
}
});
/* For certificate dashboard widget */
function handleSearchAndEdit() {
const hash = window.location.hash;
if (hash.includes('#SearchPhrase=')) {
const searchPhrase = decodeURIComponent(hash.split('=')[1]);
const searchField = $('.search-field');
if (searchField.val() !== searchPhrase) {
searchField.val(searchPhrase).trigger('keyup');
// Wait for grid to reload after search and simulate edit button click
$('#grid-cert').one("loaded.rs.jquery.bootgrid", function () {
const editButton = $(`#grid-cert .command-edit[data-row-id="${searchPhrase}"]`);
if (editButton.length) {
editButton.trigger('click');
}
});
history.replaceState(null, null, window.location.pathname + window.location.search);
}
}
}
$('#grid-cert').on("loaded.rs.jquery.bootgrid", handleSearchAndEdit);
$(window).on('hashchange', handleSearchAndEdit);
});
</script>

View File

@ -0,0 +1,188 @@
/*
* Copyright (C) 2024 Cedrik Pischem
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
export default class Certificates extends BaseTableWidget {
constructor(config) {
super(config);
this.tickTimeout = 180;
this.configurable = true;
this.configChanged = false;
}
getGridOptions() {
return {
sizeToContent: 650
};
}
getMarkup() {
const $container = $('<div></div>');
const $certificateTable = this.createTable('certificateTable', {
headerPosition: 'none'
});
$container.append($certificateTable);
return $container;
}
async onWidgetTick() {
const cas = (await this.ajaxCall('/api/trust/ca/search')).rows || [];
const certs = (await this.ajaxCall('/api/trust/cert/search')).rows || [];
if (cas.length === 0 && certs.length === 0) {
this.displayError(`${this.translations.noitems}`);
return;
}
this.clearError();
await this.processCertificates(cas, certs);
}
displayError(message) {
const $error = $(`<div class="error-message">${message}</div>`);
$('#certificateTable').empty().append($error);
}
clearError() {
$('#certificateTable .error-message').remove();
}
processItems(items, type, hiddenItems, rows) {
items.forEach(item => {
if (!hiddenItems.includes(item.descr)) {
const validTo = new Date(parseInt(item.valid_to) * 1000);
const now = new Date();
const remainingDays = Math.max(0, Math.floor((validTo - now) / (1000 * 60 * 60 * 24)));
const colorClass = remainingDays === 0
? 'text-danger'
: remainingDays < 14
? 'text-warning'
: 'text-success';
const statusText = remainingDays === 0 ? this.translations.expired : this.translations.valid;
const iconClass = remainingDays === 0 ? 'fa fa-unlock' : 'fa fa-lock';
const expirationText = remainingDays === 0
? `${this.translations.expiredon} ${validTo.toLocaleString()}`
: `${this.translations.expiresin} ${remainingDays} ${this.translations.days}, ${validTo.toLocaleString()}`;
const descrContent = (type === 'cert' || type === 'ca')
? `<a href="/ui/trust/${type === 'cert' ? 'cert' : 'ca'}#SearchPhrase=${encodeURIComponent(item.uuid)}" class="${type}-link">${item.descr}</a>`
: `<b>${item.descr}</b>`;
const row = `
<div>
<i class="${iconClass} ${colorClass} certificate-tooltip" style="cursor: pointer;"
data-tooltip="${type}-${item.descr}" title="${statusText}">
</i>
&nbsp;
<span>${descrContent}</span>
<br/>
<div style="margin-top: 5px; margin-bottom: 5px;">
${expirationText}
</div>
</div>`;
rows.push({ html: row, expirationDate: validTo });
}
});
}
async processCertificates(cas, certs) {
const config = await this.getWidgetConfig() || {};
if (!this.dataChanged('certificates', { cas, certs }) && !this.configChanged) {
return;
}
if (this.configChanged) {
this.configChanged = false;
}
$('.certificate-tooltip').tooltip('hide');
const hiddenItems = config.hiddenItems || [];
const rows = [];
if (cas.length > 0) {
this.processItems(cas, 'ca', hiddenItems, rows);
}
if (certs.length > 0) {
this.processItems(certs, 'cert', hiddenItems, rows);
}
if (rows.length === 0) {
this.displayError(`${this.translations.noitems}`);
return;
}
// Sort rows by expiration date from earliest to latest
rows.sort((a, b) => a.expirationDate - b.expirationDate);
const sortedRows = rows.map(row => [row.html]);
super.updateTable('certificateTable', sortedRows);
$('.certificate-tooltip').tooltip({ container: 'body' });
}
async getWidgetOptions() {
const [caResponse, certResponse] = await Promise.all([
this.ajaxCall('/api/trust/ca/search'),
this.ajaxCall('/api/trust/cert/search')
]);
const hiddenItemOptions = [];
if (caResponse.rows) {
caResponse.rows.forEach(ca => {
hiddenItemOptions.push({ value: `${ca.descr}`, label: ca.descr });
});
}
if (certResponse.rows) {
certResponse.rows.forEach(cert => {
hiddenItemOptions.push({ value: `${cert.descr}`, label: cert.descr });
});
}
return {
hiddenItems: {
title: this.translations.hiddenitems,
type: 'select_multiple',
options: hiddenItemOptions,
default: [],
required: false
}
};
}
async onWidgetOptionsChanged() {
this.configChanged = true;
await this.onWidgetTick();
}
}

View File

@ -348,4 +348,22 @@
<nopicture>No picture was supplied for this system.</nopicture>
</translations>
</picture>
<certificates>
<filename>Certificates.js</filename>
<link>/ui/trust/cert</link>
<endpoints>
<endpoint>/api/trust/ca/search</endpoint>
<endpoint>/api/trust/cert/search</endpoint>
</endpoints>
<translations>
<title>Certificates</title>
<noitems>No certificates to display.</noitems>
<expired>Expired</expired>
<valid>Valid</valid>
<expiresin>Expires in</expiresin>
<expiredon>Expired on</expiredon>
<days>days</days>
<hiddenitems>Hidden Certificates</hiddenitems>
</translations>
</certificates>
</metadata>