diff --git a/README.md b/README.md index 6240f01..863a3f6 100644 --- a/README.md +++ b/README.md @@ -298,7 +298,10 @@ Date: Fri, 25 Nov 2022 06:35:06 GMT The `audit-base` project provides auditing functionality for easier investigation of issues. Audit records are stored in a database and can be easily queried. The auditing library also handles removal of old audit records. -The audit library requires one database table `audit_log` and optionally the second table `audit_params` for logging detail parameters. The DDL is available for the following databases: +The audit library requires one database table `audit_log` and optionally the second table `audit_params` for logging detail parameters. +Also the `shedlock` table is required for locking scheduled tasks. + +The DDL is available for the following databases: - [DDL for MySQL](./docs/sql/mysql/create_schema.sql) - [DDL for Oracle](./docs/sql/oracle/create_schema.sql) - [DDL for PostgreSQL](./docs/sql/postgresql/create_schema.sql) @@ -306,7 +309,7 @@ The audit library requires one database table `audit_log` and optionally the sec ### Configuration The following configuration is required for integration of the auditing library: -- Enable scheduling on the application using `@EnableScheduling` annotation on class annotated with `@SpringBootApplication` so that the `flush` and `cleanup` functionality can be scheduled. +- Enable scheduling on the application using `@EnableScheduling` annotation on class annotated with `@SpringBootApplication` so that the `flush` and `cleanup` functionality can be scheduled. In order to enable schedule locking use `@EnableSchedulerLock` annotation and configure the `LockProvider` bean, see [ShedLock documentation](https://github.com/lukas-krecan/ShedLock) for details. - Add the `com.wultra.core.audit.base` package to the `@ComponentScan`, e.g. `@ComponentScan(basePackages = {"...", "com.wultra.core.audit.base"})`, so that the annotations used in auditing library can be discovered. - Configure the `spring.application.name` property to enable storing application name with audit records. @@ -318,7 +321,10 @@ The following properties can be configured in case the default configuration nee - `audit.db.table.log.name` - name of audit log database table (default: `audit_log`) - `audit.db.table.param.name` - name of audit parameters database table (default: `audit_param`) - `audit.db.table.param.enabled` - flag if logging params to parameters database is enabled (default: `false`) -- `audit.db.batch.size` - database batch size (default: `1000`) +- `audit.db.batch.size` - database batch size (default: `1000`) +- `audit.cleanup.cron` - A cron expression for the cleanup job. (default: `0 0 * * * *`, use `-` to turn it off completely) +- `audit.cleanup.lockAtLeastFor` - The lock will be held at least for given duration. (default: `5s`) +- `audit.cleanup.lockAtMostFor` - The lock will be held at most for given duration. (default: `30m`) You can configure database schema used by the auditing library using regular Spring JPA/Hibernate property in your application: - `spring.jpa.properties.hibernate.default_schema` - database database schema (default: none) diff --git a/annotations/pom.xml b/annotations/pom.xml index fb7fe9c..2edb9ec 100644 --- a/annotations/pom.xml +++ b/annotations/pom.xml @@ -7,7 +7,7 @@ io.getlime.core lime-java-core-parent - 1.11.0 + 1.12.0-SNAPSHOT annotations diff --git a/audit-base/pom.xml b/audit-base/pom.xml index 4cd56a8..bf885ba 100644 --- a/audit-base/pom.xml +++ b/audit-base/pom.xml @@ -6,7 +6,7 @@ io.getlime.core lime-java-core-parent - 1.11.0 + 1.12.0-SNAPSHOT audit-base @@ -31,6 +31,21 @@ jakarta.annotation jakarta.annotation-api + + + + net.javacrumbs.shedlock + shedlock-provider-jdbc-template + ${shedlock.version} + + + + net.javacrumbs.shedlock + shedlock-spring + ${shedlock.version} + + + org.springframework.boot spring-boot-starter-test diff --git a/audit-base/src/main/java/com/wultra/core/audit/base/database/DatabaseAuditWriter.java b/audit-base/src/main/java/com/wultra/core/audit/base/database/DatabaseAuditWriter.java index a4d75ea..e4f64c1 100644 --- a/audit-base/src/main/java/com/wultra/core/audit/base/database/DatabaseAuditWriter.java +++ b/audit-base/src/main/java/com/wultra/core/audit/base/database/DatabaseAuditWriter.java @@ -23,6 +23,8 @@ import com.wultra.core.audit.base.util.JsonUtil; import com.wultra.core.audit.base.util.StringUtil; import jakarta.annotation.PreDestroy; +import net.javacrumbs.shedlock.core.LockAssert; +import net.javacrumbs.shedlock.spring.annotation.SchedulerLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -60,6 +62,8 @@ public class DatabaseAuditWriter implements AuditWriter { private static final Logger logger = LoggerFactory.getLogger(DatabaseAuditWriter.class); private static final String SPRING_FRAMEWORK_PACKAGE_PREFIX = "org.springframework"; + private static final String JDK_INTERNAL_REFLECT_PACKAGE_PREFIX = "jdk.internal.reflect"; + private static final String JAVA_LANG_REFLECT_PACKAGE_PREFIX = "java.lang.reflect"; private final BlockingQueue queue; private final JdbcTemplate jdbcTemplate; @@ -133,9 +137,11 @@ private void prepareSqlInsertQueries() { @Override public void write(AuditRecord auditRecord) { - List packageFilter = new ArrayList<>(); - packageFilter.add(this.getClass().getPackage().getName()); - packageFilter.add(SPRING_FRAMEWORK_PACKAGE_PREFIX); + final List packageFilter = List.of( + this.getClass().getPackage().getName(), + SPRING_FRAMEWORK_PACKAGE_PREFIX, + JDK_INTERNAL_REFLECT_PACKAGE_PREFIX, + JAVA_LANG_REFLECT_PACKAGE_PREFIX); auditRecord.setCallingClass(ClassUtil.getCallingClass(packageFilter)); auditRecord.setThreadName(Thread.currentThread().getName()); try { @@ -283,12 +289,16 @@ public void scheduledFlush() { } /** - * Scheduled cleanup of audit data in database. + * Scheduled cleanup of audit data in the database. */ - @Scheduled(fixedDelayString = "${audit.cleanup.delay.fixed:3600000}", initialDelayString = "${audit.cleanup.delay.initial:1000}") + @Scheduled(cron = "${audit.cleanup.cron:0 0 * * * *}") + @SchedulerLock(name = "audit.cleanup", lockAtLeastFor = "${audit.cleanup.lockAtLeastFor:5s}", lockAtMostFor = "${audit.cleanup.lockAtMostFor:30m}") public void scheduledCleanup() { - logger.debug("Scheduled audit log cleanup called"); + logger.info("action: scheduledCleanup, state: initiated"); + LockAssert.assertLocked(); + logger.info("action: scheduledCleanup, state: lockAsserted"); cleanup(); + logger.info("action: scheduledCleanup, state: succeeded"); } /** diff --git a/audit-base/src/test/java/com/wultra/core/audit/base/AuditTest.java b/audit-base/src/test/java/com/wultra/core/audit/base/AuditTest.java index e568832..de881ab 100644 --- a/audit-base/src/test/java/com/wultra/core/audit/base/AuditTest.java +++ b/audit-base/src/test/java/com/wultra/core/audit/base/AuditTest.java @@ -32,7 +32,7 @@ import static org.junit.jupiter.api.Assertions.*; @SpringBootTest(classes = TestApplication.class, properties = {"audit.db.table.param.enabled=false"}) -@Sql(scripts = "/db_schema.sql") +@Sql("/db_schema.sql") class AuditTest { private final AuditFactory auditFactory; @@ -45,7 +45,7 @@ public AuditTest(AuditFactory auditFactory, JdbcTemplate jdbcTemplate) { } @BeforeEach - public void cleanTestDb() { + void cleanTestDb() { jdbcTemplate.execute("DELETE FROM audit_log"); jdbcTemplate.execute("DELETE FROM audit_param"); } diff --git a/audit-base/src/test/java/com/wultra/core/audit/base/TestApplication.java b/audit-base/src/test/java/com/wultra/core/audit/base/TestApplication.java index de9f0df..d97006f 100644 --- a/audit-base/src/test/java/com/wultra/core/audit/base/TestApplication.java +++ b/audit-base/src/test/java/com/wultra/core/audit/base/TestApplication.java @@ -15,12 +15,20 @@ */ package com.wultra.core.audit.base; +import net.javacrumbs.shedlock.core.LockProvider; +import net.javacrumbs.shedlock.provider.jdbctemplate.JdbcTemplateLockProvider; +import net.javacrumbs.shedlock.spring.annotation.EnableSchedulerLock; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.scheduling.annotation.EnableScheduling; +import javax.sql.DataSource; + @SpringBootApplication @EnableScheduling +@EnableSchedulerLock(defaultLockAtMostFor = "30m") public class TestApplication { /** @@ -32,4 +40,13 @@ public static void main(String[] args) { SpringApplication.run(TestApplication.class, args); } + @Bean + public LockProvider lockProvider(final DataSource dataSource) { + return new JdbcTemplateLockProvider( + JdbcTemplateLockProvider.Configuration.builder() + .withJdbcTemplate(new JdbcTemplate(dataSource)) + .usingDbTime() + .build() + ); + } } diff --git a/audit-base/src/test/java/com/wultra/core/audit/base/database/DatabaseAuditWriterTest.java b/audit-base/src/test/java/com/wultra/core/audit/base/database/DatabaseAuditWriterTest.java new file mode 100644 index 0000000..36002af --- /dev/null +++ b/audit-base/src/test/java/com/wultra/core/audit/base/database/DatabaseAuditWriterTest.java @@ -0,0 +1,111 @@ +/* + * Copyright 2025 Wultra s.r.o. + * + * 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. + */ +package com.wultra.core.audit.base.database; + +import com.wultra.core.audit.base.Audit; +import com.wultra.core.audit.base.AuditFactory; +import com.wultra.core.audit.base.TestApplication; +import com.wultra.core.audit.base.model.AuditDetail; +import org.awaitility.Awaitility; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.support.rowset.SqlRowSet; +import org.springframework.test.context.jdbc.Sql; + +import java.time.Duration; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test for {@link DatabaseAuditWriter}. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +class DatabaseAuditWriterTest { + + @SpringBootTest( + classes = TestApplication.class, + properties = { + "audit.cleanup.cron=0/3 * * * * *", + "audit.db.cleanup.days=-1" // time shift to the future to enable cleanup test + } + ) + @Sql("/db_schema.sql") + @Nested + class ScheduledCleanupOn { + + @Autowired + private AuditFactory auditFactory; + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Test + void testAuditScheduledCleanup() { + final Audit audit = auditFactory.getAudit(); + audit.info("test message", AuditDetail.builder().param("my_id", "test_id").build()); + audit.flush(); + + assertEquals(1, countAuditLogs(jdbcTemplate)); + + Awaitility.await() + .atMost(Duration.ofSeconds(5)) + .until(() -> countAuditLogs(jdbcTemplate) == 0); + } + } + + @SpringBootTest( + classes = TestApplication.class, + properties = { + "audit.cleanup.cron=-", + "audit.db.cleanup.days=-1" // time shift to the future to enable cleanup test + } + ) + @Sql("/db_schema.sql") + @Nested + class ScheduledCleanupOff { + + @Autowired + private AuditFactory auditFactory; + + @Autowired + private JdbcTemplate jdbcTemplate; + + @Test + void testAuditScheduledCleanup() { + final Audit audit = auditFactory.getAudit(); + audit.info("test message", AuditDetail.builder().param("my_id", "test_id").build()); + audit.flush(); + + assertEquals(1, countAuditLogs(jdbcTemplate)); + + Awaitility.await() + .atMost(Duration.ofSeconds(6)) + .pollInterval(Duration.ofSeconds(5)) + .until(() -> countAuditLogs(jdbcTemplate) == 1); + } + } + + private int countAuditLogs(final JdbcTemplate jdbcTemplate) { + final SqlRowSet rs = jdbcTemplate.queryForRowSet("SELECT COUNT(*) FROM audit_log"); + assertTrue(rs.next()); + return rs.getInt(1); + } +} diff --git a/audit-base/src/test/resources/db_schema.sql b/audit-base/src/test/resources/db_schema.sql index d8a2f26..1e1e058 100644 --- a/audit-base/src/test/resources/db_schema.sql +++ b/audit-base/src/test/resources/db_schema.sql @@ -44,3 +44,6 @@ CREATE INDEX audit_param_log ON audit_param (audit_log_id); CREATE INDEX audit_param_timestamp ON audit_param (timestamp_created); CREATE INDEX audit_param_key ON audit_param (param_key); CREATE INDEX audit_param_value ON audit_param (param_value); + +-- Shedlock +CREATE TABLE IF NOT EXISTS shedlock(name VARCHAR(64) NOT NULL, lock_until TIMESTAMP NOT NULL, locked_at TIMESTAMP NOT NULL, locked_by VARCHAR(255) NOT NULL, PRIMARY KEY (name)); \ No newline at end of file diff --git a/bom/pom.xml b/bom/pom.xml index 00301f5..c9cca6f 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -7,7 +7,7 @@ io.getlime.core lime-java-core-parent - 1.11.0 + 1.12.0-SNAPSHOT core-bom diff --git a/docs/sql/mysql/create_schema.sql b/docs/sql/mysql/create_schema.sql index 33851eb..c35dfdc 100644 --- a/docs/sql/mysql/create_schema.sql +++ b/docs/sql/mysql/create_schema.sql @@ -37,4 +37,7 @@ CREATE INDEX audit_log_type ON audit_log (audit_type); CREATE INDEX audit_param_log ON audit_param (audit_log_id); CREATE INDEX audit_param_timestamp ON audit_param (timestamp_created); CREATE INDEX audit_param_key ON audit_param (param_key); -CREATE FULLTEXT INDEX audit_param_value ON audit_param (param_value); \ No newline at end of file +CREATE FULLTEXT INDEX audit_param_value ON audit_param (param_value); + +-- Shedlock +CREATE TABLE shedlock(name VARCHAR(64) NOT NULL, lock_until TIMESTAMP(3) NOT NULL, locked_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), locked_by VARCHAR(255) NOT NULL, PRIMARY KEY (name)); \ No newline at end of file diff --git a/docs/sql/oracle/create_schema.sql b/docs/sql/oracle/create_schema.sql index a2a257b..d5df5be 100644 --- a/docs/sql/oracle/create_schema.sql +++ b/docs/sql/oracle/create_schema.sql @@ -37,4 +37,7 @@ CREATE INDEX audit_log_type ON audit_log (audit_type); CREATE INDEX audit_param_log ON audit_param (audit_log_id); CREATE INDEX audit_param_timestamp ON audit_param (timestamp_created); CREATE INDEX audit_param_key ON audit_param (param_key); -CREATE INDEX audit_param_value ON audit_param (param_value); \ No newline at end of file +CREATE INDEX audit_param_value ON audit_param (param_value); + +-- Shedlock +CREATE TABLE shedlock(name VARCHAR(64) NOT NULL, lock_until TIMESTAMP(3) NOT NULL, locked_at TIMESTAMP(3) NOT NULL, locked_by VARCHAR(255) NOT NULL, PRIMARY KEY (name)); \ No newline at end of file diff --git a/docs/sql/postgresql/create_schema.sql b/docs/sql/postgresql/create_schema.sql index d969b8e..974892b 100644 --- a/docs/sql/postgresql/create_schema.sql +++ b/docs/sql/postgresql/create_schema.sql @@ -37,4 +37,7 @@ CREATE INDEX audit_log_type ON audit_log (audit_type); CREATE INDEX audit_param_log ON audit_param (audit_log_id); CREATE INDEX audit_param_timestamp ON audit_param (timestamp_created); CREATE INDEX audit_param_key ON audit_param (param_key); -CREATE INDEX audit_param_value ON audit_param (param_value); \ No newline at end of file +CREATE INDEX audit_param_value ON audit_param (param_value); + +-- Shedlock +CREATE TABLE shedlock(name VARCHAR(64) NOT NULL, lock_until TIMESTAMP NOT NULL, locked_at TIMESTAMP NOT NULL, locked_by VARCHAR(255) NOT NULL, PRIMARY KEY (name)); \ No newline at end of file diff --git a/http-common/pom.xml b/http-common/pom.xml index e7a65ec..6827669 100644 --- a/http-common/pom.xml +++ b/http-common/pom.xml @@ -7,7 +7,7 @@ io.getlime.core lime-java-core-parent - 1.11.0 + 1.12.0-SNAPSHOT http-common diff --git a/pom.xml b/pom.xml index f652dd7..0955530 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ Wultra - Core Java Libraries io.getlime.core lime-java-core-parent - 1.11.0 + 1.12.0-SNAPSHOT pom 2017 @@ -56,12 +56,13 @@ 17 ${java.version} - 3.13.0 - 3.5.0 + 3.14.0 + 3.5.3 3.5.0 - 3.3.4 + 3.4.6 + 6.7.0 @@ -119,7 +120,7 @@ org.apache.maven.plugins maven-javadoc-plugin - 3.10.0 + 3.11.2 false @@ -135,7 +136,7 @@ org.apache.maven.plugins maven-deploy-plugin - 3.1.3 + 3.1.4 org.apache.maven.plugins diff --git a/rest-client-base/pom.xml b/rest-client-base/pom.xml index d148272..da1323b 100644 --- a/rest-client-base/pom.xml +++ b/rest-client-base/pom.xml @@ -6,7 +6,7 @@ io.getlime.core lime-java-core-parent - 1.11.0 + 1.12.0-SNAPSHOT rest-client-base diff --git a/rest-client-base/src/main/java/com/wultra/core/rest/client/base/DefaultRestClient.java b/rest-client-base/src/main/java/com/wultra/core/rest/client/base/DefaultRestClient.java index a64e19c..4e8f5a6 100644 --- a/rest-client-base/src/main/java/com/wultra/core/rest/client/base/DefaultRestClient.java +++ b/rest-client-base/src/main/java/com/wultra/core/rest/client/base/DefaultRestClient.java @@ -1296,6 +1296,16 @@ public CertificateAuthBuilder keyStoreLocation(String keyStoreLocation) { return this; } + /** + * Set keystore bytes. + * @param keyStoreBytes Keystore bytes. + * @return Builder. + */ + public CertificateAuthBuilder keyStoreBytes(byte[] keyStoreBytes) { + mainBuilder.config.setKeyStoreBytes(keyStoreBytes); + return this; + } + /** * Set keystore password. * @param keyStorePassword Keystore password. @@ -1345,6 +1355,16 @@ public CertificateAuthBuilder trustStoreLocation(String trustStoreLocation) { return this; } + /** + * Set truststore bytes. + * @param trustStoreBytes Truststore bytes. + * @return Builder. + */ + public CertificateAuthBuilder trustStoreBytes(byte[] trustStoreBytes) { + mainBuilder.config.setTrustStoreBytes(trustStoreBytes); + return this; + } + /** * Set truststore password. * @param trustStorePassword Truststore password. diff --git a/rest-client-base/src/main/java/com/wultra/core/rest/client/base/RestClientConfiguration.java b/rest-client-base/src/main/java/com/wultra/core/rest/client/base/RestClientConfiguration.java index f00ee1b..0150b0a 100644 --- a/rest-client-base/src/main/java/com/wultra/core/rest/client/base/RestClientConfiguration.java +++ b/rest-client-base/src/main/java/com/wultra/core/rest/client/base/RestClientConfiguration.java @@ -543,6 +543,10 @@ public byte[] getKeyStoreBytes() { * @param keyStoreBytes Byte data with the key store. */ public void setKeyStoreBytes(byte[] keyStoreBytes) { + if (keyStoreBytes == null) { + this.keyStoreBytes = null; + return; + } this.keyStoreBytes = Arrays.copyOf(keyStoreBytes, keyStoreBytes.length); } @@ -643,6 +647,10 @@ public byte[] getTrustStoreBytes() { * @param trustStoreBytes Byte data with the trust store. */ public void setTrustStoreBytes(byte[] trustStoreBytes) { + if (trustStoreBytes == null) { + this.trustStoreBytes = null; + return; + } this.trustStoreBytes = Arrays.copyOf(trustStoreBytes, trustStoreBytes.length); } diff --git a/rest-model-base/pom.xml b/rest-model-base/pom.xml index 5c28381..45bc586 100644 --- a/rest-model-base/pom.xml +++ b/rest-model-base/pom.xml @@ -6,7 +6,7 @@ io.getlime.core lime-java-core-parent - 1.11.0 + 1.12.0-SNAPSHOT rest-model-base