diff --git a/.circleci/config.yml b/.circleci/config.yml index 23c1a84..f890c28 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,12 +1,12 @@ version: 2.1 setup: true orbs: - qqq-orb: kingsrook/qqq-orb@0.7.15 + qqq-orb: kingsrook/qqq-orb@0.8 workflows: setup: jobs: - qqq-orb/generate_pipeline: - orb_version: 0.7.15 + orb_version: '0.8' filters: branches: ignore: automation/qbit-rc-pending diff --git a/.gitea/workflows/build.yaml b/.gitea/workflows/build.yaml new file mode 100644 index 0000000..ce00351 --- /dev/null +++ b/.gitea/workflows/build.yaml @@ -0,0 +1,7 @@ +name: build +on: + push: +jobs: + maven: + uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@v1 + secrets: inherit diff --git a/.qqq-ci.yml b/.qqq-ci.yml index e32feac..fa97e65 100644 --- a/.qqq-ci.yml +++ b/.qqq-ci.yml @@ -1,5 +1,5 @@ profile: qbit -orb_version: 0.7.15 +orb_version: '0.8' default_branch: develop java_version: '21' browsers: true @@ -13,3 +13,4 @@ publish: release_candidate: true release: true asciidoc: false +release_completion: true diff --git a/README.md b/README.md index 7db37ac..566395e 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,12 @@ Open `/app/worm_audit` (or `/app/admin/worm_audit` with `parentAppName="admin"`) The full-width `wormAuditHome` HTML widget uses the standard `/qqq/v1/widget` API. Hosts can add `wormAuditHealthCard` to their own dashboard. Both widgets and the app require read permission on at least one WORM table, using the host's table permission rules. +To embed WORM in an existing host app, set `withIncludeApp(false)` and configure +`appName`, `appLabel` and optional `parentAppName` for that host app. The host owns +its navigation, home widget placement and app help; the qbit does not create or replace +the app or fill its help. Both widgets and all table help, permissions, guards, +processes and schedules remain registered. The health card links to the configured +host app. Standalone app creation remains enabled by default. The aggregate home shows only identifiers, counts, times, shortened hashes and reasons; it never renders audit messages or field values. Record links retain the target table's permissions. diff --git a/pom.xml b/pom.xml index 68e3b52..f25baff 100644 --- a/pom.xml +++ b/pom.xml @@ -22,6 +22,7 @@ + 4.1.0-RC.26 0.1.0-RC.6 21 21 @@ -33,7 +34,7 @@ com.kingsrook qbit-build-parent - 2.0.0 + 2.2.1 com.kingsrook.qbits @@ -86,7 +87,7 @@ com.kingsrook.qbits qbit-object-storage - 0.1.0-RC.2 + 0.1.0-RC.10 com.kingsrook.qqq @@ -95,7 +96,7 @@ com.kingsrook.qqq qqq-frontend-next - 1.0.0-RC.14 + 1.0.0-RC.38 test @@ -231,7 +232,7 @@ - 4.1.0-RC.5 + 4.1.0-RC.26 diff --git a/src/main/java/com/kingsrook/qbits/wormaudit/WormAuditQBitConfig.java b/src/main/java/com/kingsrook/qbits/wormaudit/WormAuditQBitConfig.java index 2dc4230..15bd42f 100644 --- a/src/main/java/com/kingsrook/qbits/wormaudit/WormAuditQBitConfig.java +++ b/src/main/java/com/kingsrook/qbits/wormaudit/WormAuditQBitConfig.java @@ -48,6 +48,7 @@ public class WormAuditQBitConfig implements QBitConfig private String exportLockMode = "GOVERNANCE"; private int exportRetentionDays = 30; private final WormAuditSettings settings = new WormAuditSettings(this); + private boolean includeApp = true; private String appName = "worm_audit"; private String appLabel = "WORM Audit"; private String parentAppName; @@ -131,7 +132,26 @@ public class WormAuditQBitConfig implements QBitConfig /******************************************************************************* - ** Read the app name. + ** Whether to create the standalone app. Widgets remain available when false. + *******************************************************************************/ + public boolean getIncludeApp() + { + return (includeApp); + } + + + /******************************************************************************* + ** Disable standalone app creation when the host owns the Audit app and help. + *******************************************************************************/ + public WormAuditQBitConfig withIncludeApp(boolean includeApp) + { + this.includeApp = includeApp; + return (this); + } + + + /******************************************************************************* + ** Read the app name (the host-owned destination when includeApp is false). *******************************************************************************/ public String getAppName() { diff --git a/src/main/java/com/kingsrook/qbits/wormaudit/WormAuditQBitProducer.java b/src/main/java/com/kingsrook/qbits/wormaudit/WormAuditQBitProducer.java index 9415674..f8bf8f0 100644 --- a/src/main/java/com/kingsrook/qbits/wormaudit/WormAuditQBitProducer.java +++ b/src/main/java/com/kingsrook/qbits/wormaudit/WormAuditQBitProducer.java @@ -214,7 +214,10 @@ public class WormAuditQBitProducer implements QBitMetaDataProducer b.reason()).collect(Collectors.joining("; ")); + if(problems > ALERT_SUMMARY_REASONS) + { + summary += "; and " + (problems - ALERT_SUMMARY_REASONS) + " more"; + } QAlerts.raise(new AlertSignal("wormaudit.chain.break").withDedupKey("chain") - .withTitle(result.getBreaks().getFirst().reason()).withRecordRef(new AlertRecordRef(WormAuditTables.VERIFICATION, row.getValue("id")))); + .withTitle("Audit chain verification found " + problems + (problems == 1 ? " problem" : " problems")).withSummary(summary) + .withRecordRef(new AlertRecordRef(WormAuditTables.VERIFICATION, row.getValue("id")))); } } return (result); diff --git a/src/test/java/com/kingsrook/qbits/wormaudit/ProcessesAndAlertsTest.java b/src/test/java/com/kingsrook/qbits/wormaudit/ProcessesAndAlertsTest.java index 2c06ffd..bb1842c 100644 --- a/src/test/java/com/kingsrook/qbits/wormaudit/ProcessesAndAlertsTest.java +++ b/src/test/java/com/kingsrook/qbits/wormaudit/ProcessesAndAlertsTest.java @@ -142,7 +142,8 @@ class ProcessesAndAlertsTest assertThat(Sink.raised.getFirst().getDedupKey()).isEqualTo("chain"); assertThat(Sink.raised.getFirst().getRecordRefs().getFirst().getTableName()) .isEqualTo(WormAuditTables.VERIFICATION); - assertThat(Sink.raised.getFirst().getTitle()).contains("Break at link 1"); + assertThat(Sink.raised.getFirst().getTitle()).isEqualTo("Audit chain verification found 1 problem"); + assertThat(Sink.raised.getFirst().getSummary()).contains("Break at link 1"); var first = STORE.all(WormAuditTables.VERIFICATION).getFirst(); assertThat(first.getValueLong("first_break_link_id")).isEqualTo(1); assertThat(first.getValueLong("first_break_audit_id")).isEqualTo(1); diff --git a/src/test/java/com/kingsrook/qbits/wormaudit/WormAuditHomeTest.java b/src/test/java/com/kingsrook/qbits/wormaudit/WormAuditHomeTest.java index 1d32f24..f46f206 100644 --- a/src/test/java/com/kingsrook/qbits/wormaudit/WormAuditHomeTest.java +++ b/src/test/java/com/kingsrook/qbits/wormaudit/WormAuditHomeTest.java @@ -278,6 +278,29 @@ class WormAuditHomeTest } + /******************************************************************************* + ** Embedded widgets retain direct authorization without a standalone app. + *******************************************************************************/ + @Test + void embeddedWidgetsRequireReadPermission() throws Exception + { + var config = ChainVerificationTest.signed().withIncludeApp(false).withAppName("audit").withAppLabel("Audit"); + var instance = init(config); + assertThat(instance.getApp("worm_audit")).isNull(); + assertThat(instance.getApp("audit")).isNull(); + var input = new RenderWidgetInput().withWidgetMetaData(instance.getWidget(WormAuditApp.HEALTH_CARD_WIDGET)); + QContext.getQSession().withPermissions("admin.hasAccess"); + assertThatThrownBy(() -> new WormAuditApp.HealthCardRenderer().render(input)).isInstanceOf(QPermissionDeniedException.class); + assertThatThrownBy(() -> new WormAuditApp.HomeRenderer().render(input)).isInstanceOf(QPermissionDeniedException.class); + QContext.getQSession().withPermissions(WormAuditTables.CHAIN + ".read"); + String card = ((RawHTML) new WormAuditApp.HealthCardRenderer().render(input).getWidgetData()).getHtml(); + assertThat(card).contains("href=\"/app/audit\""); + assertThat(new WormAuditApp.HomeRenderer().render(input).getWidgetData()).isInstanceOf(RawHTML.class); + QContext.setQSession(new QSession()); + assertThatThrownBy(() -> new WormAuditApp.HealthCardRenderer().render(input)).isInstanceOf(QPermissionDeniedException.class); + } + + /******************************************************************************* ** Exercise the real SQL anti-join without Docker, including its exact boundary. *******************************************************************************/ diff --git a/src/test/java/com/kingsrook/qbits/wormaudit/WormAuditQBitProducerTest.java b/src/test/java/com/kingsrook/qbits/wormaudit/WormAuditQBitProducerTest.java index 575242f..c91296b 100644 --- a/src/test/java/com/kingsrook/qbits/wormaudit/WormAuditQBitProducerTest.java +++ b/src/test/java/com/kingsrook/qbits/wormaudit/WormAuditQBitProducerTest.java @@ -21,10 +21,15 @@ package com.kingsrook.qbits.wormaudit; import java.util.List; +import com.kingsrook.qbits.wormaudit.app.WormAuditApp; +import com.kingsrook.qbits.wormaudit.model.WormAuditHelp; import com.kingsrook.qbits.wormaudit.ddl.WormAuditDdl; import com.kingsrook.qbits.wormaudit.model.WormAuditTables; import com.kingsrook.qbits.wormaudit.processes.WormAuditProcesses; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; +import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule; import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -43,6 +48,7 @@ class WormAuditQBitProducerTest { var config = new WormAuditQBitConfig(); QInstance instance = WormAuditTestSupport.init(config); + assertThat(config.getIncludeApp()).isTrue(); assertThat(config.getRetentionYears()).isEqualTo(6); assertThat(config.getTableMetaDataCustomizer()).isNull(); assertThat(config.getSigningKey()).isNull(); @@ -160,4 +166,36 @@ class WormAuditQBitProducerTest () -> WormAuditDdl.resolveNames(new WormAuditQBitConfig(), new QInstance())) .isInstanceOf(IllegalArgumentException.class); } + + + /******************************************************************************* + ** Embedded production keeps the host app intact and registers all evidence metadata. + *******************************************************************************/ + @Test + void embeddedMetadataPreservesHostApp() throws Exception + { + var config = new WormAuditQBitConfig().withBackendName("memory").withIncludeApp(false).withAppName("audit"); + var instance = WormAuditTestSupport.core(new QBackendMetaData().withName("memory").withBackendType(MemoryBackendModule.class)); + var host = new QAppMetaData().withName("audit").withLabel("Combined Audit").withWidgets(List.of("hostSummary")); + instance.addApp(host); + new WormAuditQBitProducer().withQBitConfig(config).produce(instance); + assertThat(config.getIncludeApp()).isFalse(); + assertThat(instance.getApp("worm_audit")).isNull(); + assertThat(instance.getApp("audit")).isSameAs(host); + assertThat(host.getLabel()).isEqualTo("Combined Audit"); + assertThat(host.getWidgets()).containsExactly("hostSummary"); + assertThat(host.getHelpContent()).isNullOrEmpty(); + assertThat(instance.getWidget(WormAuditApp.HEALTH_CARD_WIDGET)).isNotNull(); + assertThat(instance.getWidget(WormAuditApp.HOME_WIDGET)).isNotNull(); + for(String name : WormAuditTables.ALL) + { + assertThat(instance.getTable(name).getHelpContent()).containsKey(WormAuditHelp.SLOT); + assertThat(instance.getTable(name).getPermissionRules().getPermissionBaseName()).isEqualTo(name); + } + assertThat(instance.getTable(WormAuditTables.CHAIN).getDisabledCapabilities()).contains(Capability.TABLE_UPDATE, Capability.TABLE_DELETE, Capability.TABLE_INSERT); + assertThat(instance.getTable("audit").getDisabledCapabilities()).contains(Capability.TABLE_UPDATE, Capability.TABLE_DELETE); + assertThat(instance.getProcess(WormAuditProcesses.VERIFY)).isNotNull(); + assertThat(instance.getProcess(WormAuditProcesses.CHAIN).getSchedule().getRepeatSeconds()).isEqualTo(config.getChainIntervalSeconds()); + assertThat(instance.getProcess(WormAuditProcesses.DAILY).getSchedule().getCronExpression()).isEqualTo(config.getVerifyCron()); + } }