diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-14 13:31:28 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-14 13:31:28 +0000 |
commit | 067008c5f094ba9606daacbe540f6b929dc124ea (patch) | |
tree | 3092ce2cd8bf1ac6db6c97f4c98c7f71a51c6ac8 /library/X509/ProvidedHook | |
parent | Initial commit. (diff) | |
download | icingaweb2-module-x509-upstream.tar.xz icingaweb2-module-x509-upstream.zip |
Adding upstream version 1:1.3.2.upstream/1%1.3.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'library/X509/ProvidedHook')
-rw-r--r-- | library/X509/ProvidedHook/DbMigration.php | 95 | ||||
-rw-r--r-- | library/X509/ProvidedHook/HostsImportSource.php | 91 | ||||
-rw-r--r-- | library/X509/ProvidedHook/ServicesImportSource.php | 143 | ||||
-rw-r--r-- | library/X509/ProvidedHook/X509ImportSource.php | 11 |
4 files changed, 340 insertions, 0 deletions
diff --git a/library/X509/ProvidedHook/DbMigration.php b/library/X509/ProvidedHook/DbMigration.php new file mode 100644 index 0000000..8314e3c --- /dev/null +++ b/library/X509/ProvidedHook/DbMigration.php @@ -0,0 +1,95 @@ +<?php + +/* Icinga Web 2 X.509 Module | (c) 2023 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\X509\ProvidedHook; + +use Icinga\Application\Hook\DbMigrationHook; +use Icinga\Module\X509\Common\Database; +use Icinga\Module\X509\Model\Schema; +use ipl\Orm\Query; +use ipl\Sql; +use ipl\Sql\Adapter\Pgsql; + +class DbMigration extends DbMigrationHook +{ + public function getName(): string + { + return $this->translate('Icinga Certificate Monitoring'); + } + + public function providedDescriptions(): array + { + return [ + '1.0.0' => $this->translate( + 'Adjusts the database type of several columns and changes some composed primary keys.' + ), + '1.1.0' => $this->translate( + 'Changes the composed x509_target index and x509_certificate valid from/to types to bigint.' + ), + '1.2.0' => $this->translate( + 'Changes all timestamp columns to bigint and adjusts enum types of "yes/no" to "n/y".' + ), + '1.3.0' => $this->translate( + 'Introduces the required tables to store jobs and job schedules in the database.' + ) + ]; + } + + public function getVersion(): string + { + if ($this->version === null) { + $conn = $this->getDb(); + $schema = $this->getSchemaQuery() + ->columns(['version', 'success']) + ->orderBy('id', SORT_DESC) + ->limit(2); + + if (static::tableExists($conn, $schema->getModel()->getTableName())) { + /** @var Schema $version */ + foreach ($schema as $version) { + if ($version->success) { + $this->version = $version->version; + + break; + } + } + + if (! $this->version) { + // Schema version table exist, but the user has probably deleted the entry! + $this->version = '1.3.0'; + } + } elseif ( + $this->getDb()->getAdapter() instanceof Pgsql + || static::getColumnType($conn, 'x509_certificate', 'ctime') === 'bigint(20) unsigned' + ) { + // We modified a bunch of timestamp columns to bigint in x509 version 1.2.0. + // We have also added Postgres support with x509 version 1.2 and never had an upgrade scripts until now. + $this->version = '1.2.0'; + } elseif (static::getColumnType($conn, 'x509_certificate_subject_alt_name', 'hash') !== null) { + if (static::getColumnType($conn, 'x509_certificate', 'valid_from') === 'bigint(20) unsigned') { + $this->version = '1.0.0'; + } else { + $this->version = '1.1.0'; + } + } else { + // X509 version 1.0 was the first release of this module, but due to some reason it also contains + // an upgrade script and adds `hash` column. However, if this column doesn't exist yet, we need + // to use the lowest possible release value as the initial (last migrated) version. + $this->version = '0.0.0'; + } + } + + return $this->version; + } + + public function getDb(): Sql\Connection + { + return Database::get(); + } + + protected function getSchemaQuery(): Query + { + return Schema::on($this->getDb()); + } +} diff --git a/library/X509/ProvidedHook/HostsImportSource.php b/library/X509/ProvidedHook/HostsImportSource.php new file mode 100644 index 0000000..70d584c --- /dev/null +++ b/library/X509/ProvidedHook/HostsImportSource.php @@ -0,0 +1,91 @@ +<?php + +// Icinga Web 2 X.509 Module | (c) 2019 Icinga GmbH | GPLv2 + +namespace Icinga\Module\X509\ProvidedHook; + +use Icinga\Module\X509\Common\Database; +use Icinga\Module\X509\Job; +use Icinga\Module\X509\Model\X509Target; +use ipl\Sql; + +class HostsImportSource extends X509ImportSource +{ + public function fetchData() + { + $conn = Database::get(); + $targets = X509Target::on($conn) + ->utilize('chain') + ->utilize('chain.certificate') + ->columns([ + 'ip', + 'host_name' => 'hostname' + ]); + + $targets + ->getSelectBase() + ->where(new Sql\Expression('target_chain_link.order = 0')) + ->groupBy(['ip', 'hostname']); + + if ($conn->getAdapter() instanceof Sql\Adapter\Pgsql) { + $targets->withColumns([ + 'host_ports' => new Sql\Expression("ARRAY_TO_STRING(ARRAY_AGG(DISTINCT port), ',')") + ]); + } else { + $targets->withColumns([ + 'host_ports' => new Sql\Expression("GROUP_CONCAT(DISTINCT port SEPARATOR ',')") + ]); + } + + $results = []; + $foundDupes = []; + /** @var X509Target $target */ + foreach ($targets as $target) { + $isV6 = Job::isIPV6($target->ip); + $target->host_ip = $target->ip; + $target->host_address = $isV6 ? null : $target->ip; + $target->host_address6 = $isV6 ? $target->ip : null; + + if (isset($foundDupes[$target->host_name])) { + // For load balanced systems the IP address is the better choice + $target->host_name_or_ip = $target->host_ip; + } elseif (! isset($results[$target->host_name])) { + // Hostnames are usually preferred, especially in the case of SNI + $target->host_name_or_ip = $target->host_name; + } else { + $dupe = $results[$target->host_name]; + unset($results[$target->host_name]); + $foundDupes[$dupe->host_name] = true; + $dupe->host_name_or_ip = $dupe->host_ip; + $results[$dupe->host_name_or_ip] = $dupe; + $target->host_name_or_ip = $target->host_ip; + } + + // Target ip is now obsolete and must not be included in the results. + // The relation is only used to utilize the query and must not be in the result set as well. + unset($target->ip); + unset($target->chain); + + $results[$target->host_name_or_ip] = (object) iterator_to_array($target); + } + + return $results; + } + + public function listColumns() + { + return [ + 'host_name_or_ip', + 'host_ip', + 'host_name', + 'host_ports', + 'host_address', + 'host_address6' + ]; + } + + public static function getDefaultKeyColumnName() + { + return 'host_name_or_ip'; + } +} diff --git a/library/X509/ProvidedHook/ServicesImportSource.php b/library/X509/ProvidedHook/ServicesImportSource.php new file mode 100644 index 0000000..7b87cd8 --- /dev/null +++ b/library/X509/ProvidedHook/ServicesImportSource.php @@ -0,0 +1,143 @@ +<?php + +// Icinga Web 2 X.509 Module | (c) 2019 Icinga GmbH | GPLv2 + +namespace Icinga\Module\X509\ProvidedHook; + +use Icinga\Module\X509\Common\Database; +use Icinga\Module\X509\Job; +use Icinga\Module\X509\Model\X509CertificateSubjectAltName; +use Icinga\Module\X509\Model\X509Target; +use ipl\Sql; + +class ServicesImportSource extends X509ImportSource +{ + public function fetchData() + { + $conn = Database::get(); + $targets = X509Target::on($conn) + ->with([ + 'chain', + 'chain.certificate', + 'chain.certificate.dn', + 'chain.certificate.issuer_certificate' + ]) + ->columns([ + 'ip', + 'host_name' => 'hostname', + 'host_port' => 'port', + 'cert_subject' => 'chain.certificate.subject', + 'cert_issuer' => 'chain.certificate.issuer', + 'cert_trusted' => 'chain.certificate.trusted', + 'cert_valid_from' => 'chain.certificate.valid_from', + 'cert_valid_to' => 'chain.certificate.valid_to', + 'cert_self_signed' => new Sql\Expression('COALESCE(%s, %s)', [ + 'chain.certificate.issuer_certificate.self_signed', + 'chain.certificate.self_signed' + ]) + ]); + + $targets->getWith()['target.chain.certificate.issuer_certificate']->setJoinType('LEFT'); + $targets + ->getSelectBase() + ->where(new Sql\Expression('target_chain_link.order = 0')) + ->groupBy(['ip, hostname, port']); + + $certAltName = X509CertificateSubjectAltName::on($conn); + $certAltName + ->getSelectBase() + ->where(new Sql\Expression('certificate_id = target_chain_certificate.id')) + ->groupBy(['alt_name.certificate_id']); + + if ($conn->getAdapter() instanceof Sql\Adapter\Pgsql) { + $targets + ->withColumns([ + 'cert_fingerprint' => new Sql\Expression("ENCODE(%s, 'hex')", [ + 'chain.certificate.fingerprint' + ]), + 'cert_dn' => new Sql\Expression( + "ARRAY_TO_STRING(ARRAY_AGG(CONCAT(%s, '=', %s)), ',')", + [ + 'chain.certificate.dn.key', + 'chain.certificate.dn.value' + ] + ) + ]) + ->getSelectBase() + ->groupBy(['target_chain_certificate.id', 'target_chain_certificate_issuer_certificate.id']); + + $certAltName->columns([ + new Sql\Expression("ARRAY_TO_STRING(ARRAY_AGG(CONCAT(%s, ':', %s)), ',')", ['type', 'value']) + ]); + } else { + $targets->withColumns([ + 'cert_fingerprint' => new Sql\Expression('HEX(%s)', ['chain.certificate.fingerprint']), + 'cert_dn' => new Sql\Expression( + "GROUP_CONCAT(CONCAT(%s, '=', %s) SEPARATOR ',')", + [ + 'chain.certificate.dn.key', + 'chain.certificate.dn.value' + ] + ) + ]); + + $certAltName->columns([ + new Sql\Expression("GROUP_CONCAT(CONCAT(%s, ':', %s) SEPARATOR ',')", ['type', 'value']) + ]); + } + + list($select, $values) = $certAltName->dump(); + $targets->withColumns(['cert_subject_alt_name' => new Sql\Expression("$select", null, ...$values)]); + + $results = []; + /** @var X509Target $target */ + foreach ($targets as $target) { + $isV6 = Job::isIPV6($target->ip); + $target->host_ip = $target->ip; + $target->host_address = $isV6 ? null : $target->ip; + $target->host_address6 = $isV6 ? $target->ip : null; + + $target->host_name_ip_and_port = sprintf( + '%s/%s:%d', + $target->host_name, + $target->host_ip, + $target->host_port + ); + + // Target ip is now obsolete and must not be included in the results. + // The relation is only used to utilize the query and must not be in the result set as well. + unset($target->ip); + unset($target->chain); + + $results[$target->host_name_ip_and_port] = (object) iterator_to_array($target); + } + + return $results; + } + + public function listColumns() + { + return [ + 'host_name_ip_and_port', + 'host_ip', + 'host_name', + 'host_port', + 'host_address', + 'host_address6', + 'cert_subject', + 'cert_issuer', + 'cert_self_signed', + 'cert_trusted', + 'cert_valid_from', + 'cert_valid_to', + 'cert_fingerprint', + 'cert_dn', + 'cert_subject_alt_name' + ]; + } + + public static function getDefaultKeyColumnName() + { + return 'host_name_ip_and_port'; + } +} diff --git a/library/X509/ProvidedHook/X509ImportSource.php b/library/X509/ProvidedHook/X509ImportSource.php new file mode 100644 index 0000000..dc280c0 --- /dev/null +++ b/library/X509/ProvidedHook/X509ImportSource.php @@ -0,0 +1,11 @@ +<?php + +// Icinga Web 2 X.509 Module | (c) 2019 Icinga GmbH | GPLv2 + +namespace Icinga\Module\X509\ProvidedHook; + +use Icinga\Module\Director\Hook\ImportSourceHook; + +abstract class X509ImportSource extends ImportSourceHook +{ +} |