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/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 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()); + } }