diff --git a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-data/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/data/RevokedCertDataService.java b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-data/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/data/RevokedCertDataService.java new file mode 100644 index 00000000..64ab5eb4 --- /dev/null +++ b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-data/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/data/RevokedCertDataService.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2021 Ubique Innovation AG + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package ch.admin.bag.covidcertificate.backend.verifier.data; + +import ch.admin.bag.covidcertificate.backend.verifier.model.DbRevokedCert; +import ch.admin.bag.covidcertificate.backend.verifier.model.cert.db.RevokedCertsUpdateResponse; +import java.util.List; + +public interface RevokedCertDataService { + + /** upserts the given revoked uvcis into the db */ + public RevokedCertsUpdateResponse replaceRevokedCerts(List revokedUvcis); + + /** returns the next batch of revoked certs after `since` */ + public List findRevokedCerts(Long since); + + /** returns the highest revoked cert pk id */ + public long findMaxRevokedCertPkId(); +} diff --git a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-data/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/data/impl/JdbcRevokedCertDataServiceImpl.java b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-data/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/data/impl/JdbcRevokedCertDataServiceImpl.java new file mode 100644 index 00000000..53f245b9 --- /dev/null +++ b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-data/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/data/impl/JdbcRevokedCertDataServiceImpl.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2021 Ubique Innovation AG + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package ch.admin.bag.covidcertificate.backend.verifier.data.impl; + +import ch.admin.bag.covidcertificate.backend.verifier.data.RevokedCertDataService; +import ch.admin.bag.covidcertificate.backend.verifier.data.mapper.RevokedCertRowMapper; +import ch.admin.bag.covidcertificate.backend.verifier.model.DbRevokedCert; +import ch.admin.bag.covidcertificate.backend.verifier.model.cert.db.RevokedCertsUpdateResponse; +import java.util.Arrays; +import java.util.List; +import javax.sql.DataSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.transaction.annotation.Transactional; + +public class JdbcRevokedCertDataServiceImpl implements RevokedCertDataService { + + private static final Logger logger = + LoggerFactory.getLogger(JdbcRevokedCertDataServiceImpl.class); + + private static final int MAX_REVOKED_CERT_BATCH_COUNT = 1000; + private final NamedParameterJdbcTemplate jt; + + public JdbcRevokedCertDataServiceImpl(DataSource dataSource) { + this.jt = new NamedParameterJdbcTemplate(dataSource); + } + + @Transactional(readOnly = false) + @Override + public RevokedCertsUpdateResponse replaceRevokedCerts(List revokedUvcis) { + int insertCount = upsertRevokedCerts(revokedUvcis); + int removeCount = removeRevokedCertsNotIn(revokedUvcis); + return new RevokedCertsUpdateResponse(insertCount, removeCount); + } + + private int upsertRevokedCerts(List revokedUvcis) { + if (revokedUvcis != null && !revokedUvcis.isEmpty()) { + String sql = + "insert into t_revoked_cert" + + " (uvci)" + + " values (:uvci)" + + " on conflict (uvci)" + + " do nothing"; + int[] updateCounts = jt.batchUpdate(sql, createParams(revokedUvcis)); + return Arrays.stream(updateCounts).sum(); + } else { + return 0; + } + } + + private MapSqlParameterSource[] createParams(List revokedUvcis) { + if (revokedUvcis == null) { + return null; + } + + int size = revokedUvcis.size(); + MapSqlParameterSource[] params = new MapSqlParameterSource[size]; + for (int i = 0; i < size; i++) { + params[i] = new MapSqlParameterSource("uvci", revokedUvcis.get(i)); + } + return params; + } + + private int removeRevokedCertsNotIn(List revokedUvcis) { + String sql = "delete from t_revoked_cert"; + if (revokedUvcis != null && !revokedUvcis.isEmpty()) { + sql += " where uvci not in (:to_keep)"; + } + return jt.update(sql, new MapSqlParameterSource("to_keep", revokedUvcis)); + } + + @Transactional(readOnly = true) + @Override + public List findRevokedCerts(Long since) { + if (since == null) { + since = 0L; + } + String sql = + "select pk_revoked_cert_id, uvci from t_revoked_cert" + + " where pk_revoked_cert_id > :since" + + " order by pk_revoked_cert_id asc" + + " limit :max_batch_count"; + MapSqlParameterSource params = new MapSqlParameterSource(); + params.addValue("since", since); + params.addValue("max_batch_count", MAX_REVOKED_CERT_BATCH_COUNT); + return jt.query(sql, params, new RevokedCertRowMapper()); + } + + @Transactional(readOnly = true) + @Override + public long findMaxRevokedCertPkId() { + try { + String sql = + "select pk_revoked_cert_id from t_revoked_cert" + + " order by pk_revoked_cert_id desc" + + " limit 1"; + return jt.queryForObject(sql, new MapSqlParameterSource(), Long.class); + } catch (EmptyResultDataAccessException e) { + return 0L; + } + } +} diff --git a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-data/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/data/mapper/RevokedCertRowMapper.java b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-data/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/data/mapper/RevokedCertRowMapper.java new file mode 100644 index 00000000..ad6ba5c2 --- /dev/null +++ b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-data/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/data/mapper/RevokedCertRowMapper.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2021 Ubique Innovation AG + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package ch.admin.bag.covidcertificate.backend.verifier.data.mapper; + +import ch.admin.bag.covidcertificate.backend.verifier.model.DbRevokedCert; +import java.sql.ResultSet; +import java.sql.SQLException; +import org.springframework.jdbc.core.RowMapper; + +public class RevokedCertRowMapper implements RowMapper { + + @Override + public DbRevokedCert mapRow(ResultSet resultSet, int i) throws SQLException { + var revokedCert = new DbRevokedCert(); + revokedCert.setPkId(resultSet.getLong("pk_revoked_cert_id")); + revokedCert.setUvci(resultSet.getString("uvci")); + return revokedCert; + } +} diff --git a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-data/src/main/resources/db/migration/pgsql/V0_5__revoked_certs.sql b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-data/src/main/resources/db/migration/pgsql/V0_5__revoked_certs.sql new file mode 100644 index 00000000..65762304 --- /dev/null +++ b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-data/src/main/resources/db/migration/pgsql/V0_5__revoked_certs.sql @@ -0,0 +1,12 @@ +/* + * Created by Ubique Innovation AG + * https://www.ubique.ch + * Copyright (c) 2021. All rights reserved. + */ + +CREATE TABLE t_revoked_cert +( + pk_revoked_cert_id serial NOT NULL, + uvci character varying(50) UNIQUE NOT NULL, + CONSTRAINT pk_t_revoked_cert PRIMARY KEY (pk_revoked_cert_id) +); \ No newline at end of file diff --git a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-data/src/main/resources/db/migration/pgsql_cluster/V0_5__revoked_certs.sql b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-data/src/main/resources/db/migration/pgsql_cluster/V0_5__revoked_certs.sql new file mode 100644 index 00000000..65762304 --- /dev/null +++ b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-data/src/main/resources/db/migration/pgsql_cluster/V0_5__revoked_certs.sql @@ -0,0 +1,12 @@ +/* + * Created by Ubique Innovation AG + * https://www.ubique.ch + * Copyright (c) 2021. All rights reserved. + */ + +CREATE TABLE t_revoked_cert +( + pk_revoked_cert_id serial NOT NULL, + uvci character varying(50) UNIQUE NOT NULL, + CONSTRAINT pk_t_revoked_cert PRIMARY KEY (pk_revoked_cert_id) +); \ No newline at end of file diff --git a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-model/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/model/DbRevokedCert.java b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-model/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/model/DbRevokedCert.java new file mode 100644 index 00000000..49e3ba9e --- /dev/null +++ b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-model/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/model/DbRevokedCert.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2021 Ubique Innovation AG + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package ch.admin.bag.covidcertificate.backend.verifier.model; + +public class DbRevokedCert { + private Long pkId; + private String uvci; + + public Long getPkId() { + return pkId; + } + + public void setPkId(Long pkId) { + this.pkId = pkId; + } + + public String getUvci() { + return uvci; + } + + public void setUvci(String uvci) { + this.uvci = uvci; + } +} diff --git a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-model/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/model/RevocationResponse.java b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-model/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/model/RevocationResponse.java index 7b078066..e0d733d5 100644 --- a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-model/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/model/RevocationResponse.java +++ b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-model/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/model/RevocationResponse.java @@ -15,6 +15,12 @@ public class RevocationResponse { example = "172800000") private Duration validDuration = Duration.ofHours(48); + public RevocationResponse() {} + + public RevocationResponse(List revokedCerts) { + this.revokedCerts = revokedCerts; + } + public List getRevokedCerts() { return revokedCerts; } diff --git a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-model/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/model/cert/db/RevokedCertsUpdateResponse.java b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-model/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/model/cert/db/RevokedCertsUpdateResponse.java new file mode 100644 index 00000000..a15636d6 --- /dev/null +++ b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-model/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/model/cert/db/RevokedCertsUpdateResponse.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2021 Ubique Innovation AG + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package ch.admin.bag.covidcertificate.backend.verifier.model.cert.db; + +public class RevokedCertsUpdateResponse { + private final int insertCount; + private final int removeCount; + + public RevokedCertsUpdateResponse(int insertCount, int removeCount) { + this.insertCount = insertCount; + this.removeCount = removeCount; + } + + public int getInsertCount() { + return insertCount; + } + + public int getRemoveCount() { + return removeCount; + } +} diff --git a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/pom.xml b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/pom.xml index 88f4f37c..da39a077 100644 --- a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/pom.xml +++ b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/pom.xml @@ -237,7 +237,7 @@ ch.admin.bag.covidcertificate.backend.verifier.ws.controller.KeyControllerV2 - ch.admin.bag.covidcertificate.backend.verifier.ws.controller.RevocationListController + ch.admin.bag.covidcertificate.backend.verifier.ws.controller.RevocationListControllerV2 ch.admin.bag.covidcertificate.backend.verifier.ws.controller.VerificationRulesController diff --git a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/ws/client/RevocationListSyncer.java b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/ws/client/RevocationListSyncer.java new file mode 100644 index 00000000..f64dc653 --- /dev/null +++ b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/ws/client/RevocationListSyncer.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2021 Ubique Innovation AG + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package ch.admin.bag.covidcertificate.backend.verifier.ws.client; + +import ch.admin.bag.covidcertificate.backend.verifier.data.RevokedCertDataService; +import ch.admin.bag.covidcertificate.backend.verifier.model.cert.db.RevokedCertsUpdateResponse; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.RequestEntity; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +public class RevocationListSyncer { + + private static final Logger logger = LoggerFactory.getLogger(RevocationListSyncer.class); + + private final String baseurl; + private final String endpoint = "/v1/revocation-list"; + private final RevokedCertDataService revokedCertDataService; + @Autowired private RestTemplate rt; + + public RevocationListSyncer( + String revokedCertsBaseUrl, RevokedCertDataService revokedCertDataService) { + this.baseurl = revokedCertsBaseUrl; + this.revokedCertDataService = revokedCertDataService; + } + + public void updateRevokedCerts() { + logger.info("updating revoked certs"); + + try { + List revokedCerts = downloadRevokedCerts(); + logger.info("downloaded {} revoked certs", revokedCerts.size()); + + RevokedCertsUpdateResponse updateResponse = + revokedCertDataService.replaceRevokedCerts(revokedCerts); + + logger.info( + "finished updating revoked certs. inserted {}, removed {}", + updateResponse.getInsertCount(), + updateResponse.getRemoveCount()); + } catch (Exception e) { + logger.error("revoked certs update failed", e); + } + } + + private List downloadRevokedCerts() { + final var requestEndpoint = baseurl + endpoint; + final var uri = UriComponentsBuilder.fromHttpUrl(requestEndpoint).build().toUri(); + final RequestEntity requestEntity = + RequestEntity.get(uri).headers(createDownloadHeaders()).build(); + final var response = rt.exchange(requestEntity, String[].class).getBody(); + return new ArrayList<>(Arrays.asList(response)); + } + + private HttpHeaders createDownloadHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.add(HttpHeaders.ACCEPT, "application/json"); + return headers; + } +} diff --git a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/ws/config/SchedulingConfig.java b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/ws/config/SchedulingConfig.java new file mode 100644 index 00000000..007fcfd0 --- /dev/null +++ b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/ws/config/SchedulingConfig.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2021 Ubique Innovation AG + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package ch.admin.bag.covidcertificate.backend.verifier.ws.config; + +import ch.admin.bag.covidcertificate.backend.verifier.ws.client.RevocationListSyncer; +import net.javacrumbs.shedlock.core.LockAssert; +import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock; +import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; + +@Configuration +@EnableScheduling +@EnableSchedulerLock(defaultLockAtMostFor = "PT10M") +public class SchedulingConfig { + + private static final Logger logger = LoggerFactory.getLogger(SchedulingConfig.class); + + private final RevocationListSyncer revocationListSyncer; + + public SchedulingConfig(RevocationListSyncer revocationListSyncer) { + this.revocationListSyncer = revocationListSyncer; + } + + // Sync revocation list every full hour (default) + @Scheduled(cron = "${revocationList.sync.cron:0 0 * ? * *}") + @SchedulerLock(name = "revocation_list_sync", lockAtLeastFor = "PT15S") + public void syncRevocationList() { + LockAssert.assertLocked(); + revocationListSyncer.updateRevokedCerts(); + } +} diff --git a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/ws/config/WsBaseConfig.java b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/ws/config/WsBaseConfig.java index 269b657c..7dd7b177 100644 --- a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/ws/config/WsBaseConfig.java +++ b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/ws/config/WsBaseConfig.java @@ -11,12 +11,16 @@ package ch.admin.bag.covidcertificate.backend.verifier.ws.config; import ch.admin.bag.covidcertificate.backend.verifier.data.AppTokenDataService; +import ch.admin.bag.covidcertificate.backend.verifier.data.RevokedCertDataService; import ch.admin.bag.covidcertificate.backend.verifier.data.VerifierDataService; import ch.admin.bag.covidcertificate.backend.verifier.data.impl.JdbcAppTokenDataServiceImpl; +import ch.admin.bag.covidcertificate.backend.verifier.data.impl.JdbcRevokedCertDataServiceImpl; import ch.admin.bag.covidcertificate.backend.verifier.data.impl.JdbcVerifierDataServiceImpl; +import ch.admin.bag.covidcertificate.backend.verifier.ws.client.RevocationListSyncer; import ch.admin.bag.covidcertificate.backend.verifier.ws.controller.KeyController; import ch.admin.bag.covidcertificate.backend.verifier.ws.controller.KeyControllerV2; import ch.admin.bag.covidcertificate.backend.verifier.ws.controller.RevocationListController; +import ch.admin.bag.covidcertificate.backend.verifier.ws.controller.RevocationListControllerV2; import ch.admin.bag.covidcertificate.backend.verifier.ws.controller.ValueSetsController; import ch.admin.bag.covidcertificate.backend.verifier.ws.controller.VerificationRulesController; import ch.admin.bag.covidcertificate.backend.verifier.ws.interceptor.HeaderInjector; @@ -35,6 +39,8 @@ import java.util.List; import java.util.Map; import javax.sql.DataSource; +import net.javacrumbs.shedlock.core.LockProvider; +import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider; import org.flywaydb.core.Flyway; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,6 +48,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.web.client.RestTemplate; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -156,6 +163,23 @@ public RevocationListController revocationListController() { return new RevocationListController(revokedCertsBaseUrl); } + @Bean + public RevocationListControllerV2 revocationListControllerV2( + RevokedCertDataService revokedCertDataService) { + return new RevocationListControllerV2(revokedCertDataService); + } + + @Bean + public RevocationListSyncer revocationListSyncer( + RevokedCertDataService revokedCertDataService) { + return new RevocationListSyncer(revokedCertsBaseUrl, revokedCertDataService); + } + + @Bean + public RevokedCertDataService revokedCertDataService(DataSource dataSource) { + return new JdbcRevokedCertDataServiceImpl(dataSource); + } + @Bean public VerificationRulesController verificationRulesController() throws IOException, NoSuchAlgorithmException { @@ -171,4 +195,15 @@ public ValueSetsController valueSetsController() throws IOException, NoSuchAlgor public RestTemplate restTemplate() { return RestTemplateHelper.getRestTemplate(); } + + @Bean + public LockProvider lockProvider(DataSource dataSource) { + return new JdbcTemplateLockProvider( + JdbcTemplateLockProvider.Configuration.builder() + .withTableName("t_shedlock") + .withJdbcTemplate(new JdbcTemplate(dataSource)) + // Works on Postgres, MySQL, MariaDb, MS SQL, Oracle, DB2, HSQL and H2 + .usingDbTime() + .build()); + } } diff --git a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/ws/config/WsLocalConfig.java b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/ws/config/WsLocalConfig.java new file mode 100644 index 00000000..1e494633 --- /dev/null +++ b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/ws/config/WsLocalConfig.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2021 Ubique Innovation AG + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package ch.admin.bag.covidcertificate.backend.verifier.ws.config; + +import ch.admin.bag.covidcertificate.backend.verifier.ws.controller.dev.DevController; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import java.util.Properties; +import javax.sql.DataSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +@Configuration +@Profile("local") +public class WsLocalConfig { + + @Bean + public DevController devController() { + return new DevController(); + } +} diff --git a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/ws/controller/RevocationListControllerV2.java b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/ws/controller/RevocationListControllerV2.java new file mode 100644 index 00000000..8bb080d7 --- /dev/null +++ b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/ws/controller/RevocationListControllerV2.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2021 Ubique Innovation AG + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package ch.admin.bag.covidcertificate.backend.verifier.ws.controller; + +import ch.admin.bag.covidcertificate.backend.verifier.data.RevokedCertDataService; +import ch.admin.bag.covidcertificate.backend.verifier.model.DbRevokedCert; +import ch.admin.bag.covidcertificate.backend.verifier.model.RevocationResponse; +import ch.admin.bag.covidcertificate.backend.verifier.ws.utils.CacheUtil; +import ch.ubique.openapi.docannotations.Documentation; +import java.util.List; +import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.CacheControl; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.client.HttpStatusCodeException; + +@Controller +@RequestMapping("trust/v2") +@Documentation(description = "Endpoint to obtain the list of revoked certificates") +public class RevocationListControllerV2 { + + private static final Logger logger = LoggerFactory.getLogger(RevocationListControllerV2.class); + + private static final String NEXT_SINCE_HEADER = "X-Next-Since"; + private static final String UP_TO_DATE_HEADER = "up-to-date"; + + private final RevokedCertDataService revokedCertDataService; + + public RevocationListControllerV2(RevokedCertDataService revokedCertDataService) { + this.revokedCertDataService = revokedCertDataService; + } + + @Documentation( + description = "get list of revoked certificates", + responses = {"200 => next batch of revoked certificates"}, + responseHeaders = { + "X-Next-Since:`since` to set for next request:string", + "up-to-date:set to 'true' when no more certs to fetch:string" + }) + @CrossOrigin(origins = {"https://editor.swagger.io"}) + @GetMapping(value = "/revocationList") + public @ResponseBody ResponseEntity getRevokedCerts( + @RequestParam(required = false, defaultValue = "0") Long since) + throws HttpStatusCodeException { + List revokedCerts = revokedCertDataService.findRevokedCerts(since); + List revokedUvcis = + revokedCerts.stream().map(DbRevokedCert::getUvci).collect(Collectors.toList()); + return ResponseEntity.ok() + .headers(getRevokedCertsHeaders(revokedCerts)) + .cacheControl(CacheControl.maxAge(CacheUtil.REVOCATION_LIST_MAX_AGE)) + .body(new RevocationResponse(revokedUvcis)); + } + + private HttpHeaders getRevokedCertsHeaders(List revokedCerts) { + HttpHeaders headers = new HttpHeaders(); + long maxPkId = revokedCertDataService.findMaxRevokedCertPkId(); + Long nextSince = + revokedCerts.stream().mapToLong(DbRevokedCert::getPkId).max().orElse(maxPkId); + headers.add(NEXT_SINCE_HEADER, nextSince.toString()); + if (nextSince >= maxPkId) { + headers.add(UP_TO_DATE_HEADER, "true"); + } + return headers; + } +} diff --git a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/ws/controller/dev/DevController.java b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/ws/controller/dev/DevController.java new file mode 100644 index 00000000..f5437657 --- /dev/null +++ b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/main/java/ch/admin/bag/covidcertificate/backend/verifier/ws/controller/dev/DevController.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2021 Ubique Innovation AG + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * SPDX-License-Identifier: MPL-2.0 + */ + +package ch.admin.bag.covidcertificate.backend.verifier.ws.controller.dev; + +import ch.admin.bag.covidcertificate.backend.verifier.ws.utils.CacheUtil; +import ch.ubique.openapi.docannotations.Documentation; +import java.util.ArrayList; +import java.util.List; +import org.springframework.http.CacheControl; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; + +@Controller +@RequestMapping("") +@Documentation(description = "mocks endpoints that are not available during local development") +public class DevController { + + private static final String NEXT_SINCE_HEADER = "X-Next-Since"; + private static final String UP_TO_DATE_HEADER = "up-to-date"; + + @GetMapping(value = "/v1/revocation-list") + public @ResponseBody ResponseEntity> getMockRevokedCerts( + @RequestParam(required = false) String since) { + List response = new ArrayList<>(); + for (int i = 0; i < 10; i++) { + response.add("urn:uvci:01:CH:MOCK" + i); + } + return ResponseEntity.ok() + .header(NEXT_SINCE_HEADER, "1000") + .header(UP_TO_DATE_HEADER, "true") + .cacheControl(CacheControl.maxAge(CacheUtil.REVOCATION_LIST_MAX_AGE)) + .body(response); + } +} diff --git a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/main/resources/application-local.properties b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/main/resources/application-local.properties index 9b544c20..20479054 100644 --- a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/main/resources/application-local.properties +++ b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/main/resources/application-local.properties @@ -7,4 +7,7 @@ ws.authentication.apiKeys.local=4d1d5663-b4ef-46a5-85b6-3d1d376429da ws.monitor.prometheus.user=prometheus ws.monitor.prometheus.password=prometheus -server.port=8081 \ No newline at end of file +server.port=8081 + +revocationList.baseurl=http://localhost:8081 +revocationList.sync.cron=0 * * ? * * \ No newline at end of file diff --git a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/main/resources/application.properties b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/main/resources/application.properties index d2ed3d44..a9e765cd 100644 --- a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/main/resources/application.properties +++ b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/main/resources/application.properties @@ -25,5 +25,3 @@ datasource.maximumPoolSize=5 datasource.maxLifetime=1700000 datasource.idleTimeout=600000 datasource.connectionTimeout=30000 - -revocationList.baseurl= diff --git a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/test/resources/http/http-client.env.json b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/test/resources/http/http-client.env.json index 281472e1..b986c3a2 100644 --- a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/test/resources/http/http-client.env.json +++ b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/test/resources/http/http-client.env.json @@ -2,6 +2,7 @@ "local": { "baseUrl": "localhost:8081/trust", "since": 4, + "revokedSince": 4, "upTo": 4, "certFormat": "IOS" }, diff --git a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/test/resources/http/verifier-ws.http b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/test/resources/http/verifier-ws.http index 41445a77..45fdf6fc 100644 --- a/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/test/resources/http/verifier-ws.http +++ b/ch-covidcertificate-backend-verifier/ch-covidcertificate-backend-verifier-ws/src/test/resources/http/verifier-ws.http @@ -14,9 +14,8 @@ If-None-Match: "-720957702" Authorization: Bearer {{apiKey}} ### get revocation list -GET {{baseUrl}}/v1/revocationList +GET {{baseUrl}}/v2/revocationList?since={{revokedSince}} Accept: application/json -If-None-Match: "1089905096" Authorization: Bearer {{apiKey}} ### get verification rules