Merge "Correctly filter out user-edit refs"
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2069abc --- /dev/null +++ b/.gitignore
@@ -0,0 +1,5 @@ +.idea +*iml +/lib +/out +.DS_Store \ No newline at end of file
diff --git a/admin/certificates-validity-checker-1.0.groovy b/admin/certificates-validity-checker-1.0.groovy new file mode 100644 index 0000000..841efdf --- /dev/null +++ b/admin/certificates-validity-checker-1.0.groovy
@@ -0,0 +1,186 @@ +// Copyright (C) 2023 The Android Open Source Project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import com.google.common.base.Strings +import com.google.common.flogger.FluentLogger +import com.google.gerrit.extensions.annotations.Listen +import com.google.gerrit.extensions.annotations.PluginName +import com.google.gerrit.extensions.events.LifecycleListener +import com.google.gerrit.metrics.CallbackMetric1 +import com.google.gerrit.metrics.Description +import com.google.gerrit.metrics.Field +import com.google.gerrit.metrics.MetricMaker +import com.google.gerrit.server.config.ConfigUtil +import com.google.gerrit.server.config.PluginConfigFactory +import com.google.gerrit.server.git.WorkQueue +import com.google.gerrit.server.logging.Metadata +import com.google.inject.Inject +import com.google.inject.Singleton +import sun.security.x509.GeneralNameInterface + +import javax.net.ssl.SSLSocket +import javax.net.ssl.SSLSocketFactory +import java.security.cert.Certificate +import java.security.cert.X509Certificate +import java.time.Duration +import java.util.concurrent.ScheduledFuture + +import static java.util.concurrent.TimeUnit.HOURS +import static java.util.concurrent.TimeUnit.MILLISECONDS +import static java.util.concurrent.TimeUnit.SECONDS + +@Singleton +@Listen +class CertificatesValidityChecker implements LifecycleListener { + private static final int DEFAULT_CHECK_INTERVAL_HOURS = 24 + private final WorkQueue queue + private final PluginConfigFactory config + private final String pluginName + private final CertificatesCheckMetrics metrics + + private ScheduledFuture<?> certificatesValidityChecksTask + private List<String> endpoints + private Long checkIntervalInMillis + + @Inject + CertificatesValidityChecker(WorkQueue queue, PluginConfigFactory cfg, + CertificatesCheckMetrics metrics, + @PluginName String pluginName) { + this.metrics = metrics + this.queue = queue + this.config = cfg + this.pluginName = pluginName + } + + @Override + void start() { + endpoints = getEndpointsList(config, pluginName) + checkIntervalInMillis = getCheckIntervalMillis(config, pluginName) + certificatesValidityChecksTask = queue.getDefaultQueue() + .scheduleAtFixedRate( + new CheckCertificatesValidityTask(metrics, endpoints), + SECONDS.toMillis(1), + checkIntervalInMillis, + MILLISECONDS) + } + + @Override + void stop() { + if (certificatesValidityChecksTask != null) { + certificatesValidityChecksTask.cancel(true) + certificatesValidityChecksTask = null + } + } + + private Long getCheckIntervalMillis(PluginConfigFactory cfg, String pluginName) { + String fromConfig = + Strings.nullToEmpty(cfg.getGlobalPluginConfig(pluginName).getString("validation",null,"checkInterval")) + return HOURS.toMillis(ConfigUtil.getTimeUnit(fromConfig, DEFAULT_CHECK_INTERVAL_HOURS, HOURS)) + } + + private List<String> getEndpointsList(PluginConfigFactory cfg, String pluginName) { + return cfg.getGlobalPluginConfig(pluginName).getStringList("validation",null,"endpoint") + } + + private static class CertificatesCheckMetrics { + private static final Field<String> ENDPOINT_NAME = + Field.ofString("endpoint_name", Metadata.Builder.&cacheName).build() + private final CallbackMetric1<String, Integer> metrics + + @Inject + CertificatesCheckMetrics(MetricMaker metricMaker) { + this.metrics = + metricMaker.newCallbackMetric( + "certificates/number_of_day_to_expire/per_endpoint", + Integer.class, + new Description("Per-endpoint certificate expiration date") + .setGauge() + .setUnit("days"), + ENDPOINT_NAME) + } + + def setMetric(String endpoint, int numberOfDays) { + metrics.set(endpoint, numberOfDays) + } + } + + private static class CheckCertificatesValidityTask implements Runnable { + private static final FluentLogger logger = FluentLogger.forEnclosingClass() + private final CertificatesCheckMetrics metrics + private final List<String> endpoints + + CheckCertificatesValidityTask(CertificatesCheckMetrics metrics, List<String> endpoints) { + this.endpoints = endpoints + this.metrics = metrics + } + + @Override + void run() { + for (String endpoint : endpoints) { + logger.atInfo().log("Checking certificate expiry date for %s endpoint", endpoint) + SSLSocket conn + try { + def (hostname, port) = parseEndpoint(endpoint) + conn = openConnection(hostname as String, port as int) + conn.startHandshake(); + Certificate[] certs = conn.getSession().getPeerCertificates(); + for (Certificate cert : certs) { + if (cert instanceof X509Certificate && + cert.getSubjectAlternativeNames().findAll{it[0] == GeneralNameInterface.NAME_DNS} + .any {isHostnameMatching(hostname as String, it.get(1) as String) }) { + def numberOfDaysToExpire = Duration + .between(new Date().toInstant(), cert.notAfter.toInstant()).toDays() + metrics + .setMetric( + hostname as String, + numberOfDaysToExpire.intValue()) + } else { + logger.atFine().log("Certificate type %s is not a valid X.509 certificate for the specified endpoint: %s. Skipping!", cert.getType(), endpoint) + } + } + } catch(e) { + logger.atSevere() + .withCause(e) + .log("Cannot check certificates expiry date for %s endpoint", endpoint) + } finally { + conn?.close() + } + } + } + + def parseEndpoint(String endpoint) { + def hostAndPort = endpoint.split(':') + if (hostAndPort.size() != 2) { + throw new IllegalArgumentException("Wrong endpoint format, expected <host>:<port> but was ${endpoint}") + } + + hostAndPort + } + + private boolean isHostnameMatching(String hostname, String certName) { + // Replace the wildcard (*) with a regex wildcard (.*) + def certPattern = certName.replaceFirst("\\*", ".*") + return hostname.matches(certPattern) + } + + private SSLSocket openConnection(String hostname, int port) { + logger + .atInfo() + .log("Opening connection for %s endpoint successful", + hostname) + (SSLSocket) SSLSocketFactory.getDefault() + .createSocket(hostname, port); + } + } +}
diff --git a/admin/certificates-validity-checker.md b/admin/certificates-validity-checker.md new file mode 100644 index 0000000..1f0b374 --- /dev/null +++ b/admin/certificates-validity-checker.md
@@ -0,0 +1,46 @@ +Certificates Validity Check utility +============================== + +DESCRIPTION +----------- +Check SSL Certificates expiry date and expose them as a Gerrit metrics. + +Configuration +========================= + +The certificates-validity-checker plugin is configured in +$site_path/etc/@PLUGIN@.config` files, example: + +```text +[validation] + endpoint = hostname:443 + endpoint = mail.hostname.com:993 + checkInterval = 1 day +``` + +Configuration parameters +--------------------- + +```validation.checkInterval``` +: Frequency of the SSL certificates expiry date check operation + Value should use common time unit suffixes to express their setting: + * h, hr, hour, hours + * d, day, days + * w, week, weeks (`1 week` is treated as `7 days`) + * mon, month, months (`1 month` is treated as `30 days`) + * y, year, years (`1 year` is treated as `365 days`) + If a time unit suffix is not specified, `hours` is assumed. + Time intervals smaller than one hour are not supported. + Default: 24 hours + +```validation.endpoint``` +: Specify for which endpoint SSL certificates expiry date should be + checked and expose as a Gerrit metric. + Endpoint format is <host>:<port> + It can be provided more than once. + +Metrics +--------------------- +Currently, the metrics exposed are the following: + +```groovy_certificates_validity_checker_certificates_number_of_day_to_expire_per_endpoint_<hostname>```
diff --git a/admin/warm-cache-1.0.groovy b/admin/warm-cache-1.0.groovy index eb1df2c..02adccb 100644 --- a/admin/warm-cache-1.0.groovy +++ b/admin/warm-cache-1.0.groovy
@@ -13,15 +13,13 @@ // limitations under the License. import com.google.gerrit.common.data.GlobalCapability +import com.google.gerrit.entities.AccountGroup import com.google.gerrit.sshd.* import com.google.gerrit.extensions.annotations.* import com.google.gerrit.server.project.* import com.google.gerrit.server.account.* import com.google.gerrit.server.IdentifiedUser -import com.google.gerrit.reviewdb.client.AccountGroup -import com.google.gerrit.reviewdb.server.ReviewDb import com.google.inject.* -import org.kohsuke.args4j.* abstract class BaseSshCommand extends SshCommand { @@ -39,9 +37,6 @@ @Inject ProjectCache cache - @Inject - GroupCache groupCache - public void run() { println "Loading project list ..." def start = System.currentTimeMillis() @@ -75,8 +70,8 @@ private HashSet<AccountGroup.UUID> allGroupsUUIDs() { def allGroupsUuids = new HashSet<AccountGroup.UUID>() for (project in cache.all()) { - def groupUuids = cache.get(project)?.getConfig()?.getAllGroupUUIDs() - if (groupUuids != null) { allGroupsUuids.addAll(groupUuids) } + def groupUuids = cache.get(project).stream().flatMap { it.config.allGroupUUIDs.stream() } + allGroupsUuids.addAll(groupUuids.collect()) } allGroupsUuids.addAll(groupIncludeCache.allExternalMembers()) return allGroupsUuids; @@ -90,12 +85,11 @@ def groupsLoaded = 0 for (groupUuid in allGroupsUuids) { - groupIncludeCache.subgroupsOf(groupUuid) groupIncludeCache.parentGroupsOf(groupUuid) def group = groupCache.get(groupUuid) - if(group != null) { - groupCache.get(group.getNameKey()) - groupCache.get(group.getId()) + group.ifPresent { internalGroup -> + groupCache.get(internalGroup.getNameKey()) + groupCache.get(internalGroup.getId()) } groupsLoaded++ @@ -117,16 +111,16 @@ AccountCache cache @Inject - Provider<ReviewDb> db + Accounts accounts public void run() { println "Loading accounts ..." def start = System.currentTimeMillis() - def allAccounts = db.get().accounts().all() + def allAccounts = accounts.all() def loaded = 0 for (account in allAccounts) { - cache.get(account.accountId) + cache.get(account.account().id()) loaded++ if (loaded%1000==0) { println "$loaded accounts" @@ -148,13 +142,13 @@ public void run() { println "Loading groups ..." def start = System.currentTimeMillis() - def allAccounts = db.get().accounts().all() + def allAccounts = accounts.all() def loaded = 0 def allGroupsUUIDs = new HashSet<AccountGroup.UUID>() def lastDisplay = 0 for (account in allAccounts) { - def user = userFactory.create(account.accountId) + def user = userFactory.create(account.account().id()) def groupsUUIDs = user?.getEffectiveGroups()?.getKnownGroups() if (groupsUUIDs != null) { allGroupsUUIDs.addAll(groupsUUIDs) }