Compare commits

..
11 changed files with 9 additions and 114 deletions

No files matched your search

+2 -2
View File
@@ -1,12 +1,12 @@
version: 2.1
setup: true
orbs:
qqq-orb: kingsrook/qqq-orb@0.7.12
qqq-orb: kingsrook/qqq-orb@0.7.15
workflows:
setup:
jobs:
- qqq-orb/generate_pipeline:
orb_version: 0.7.12
orb_version: 0.7.15
filters:
branches:
ignore: automation/qbit-rc-pending
+1 -2
View File
@@ -1,5 +1,5 @@
profile: qbit
orb_version: 0.7.12
orb_version: 0.7.15
default_branch: develop
java_version: '21'
browsers: true
@@ -13,4 +13,3 @@ publish:
release_candidate: true
release: true
asciidoc: false
release_completion: true
-6
View File
@@ -137,12 +137,6 @@ 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.
+1 -1
View File
@@ -22,7 +22,7 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<properties>
<revision>0.1.0-SNAPSHOT</revision>
<revision>0.1.0-RC.6</revision>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<jacoco.version>0.8.15</jacoco.version>
@@ -48,7 +48,6 @@ 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;
@@ -132,26 +131,7 @@ public class WormAuditQBitConfig implements QBitConfig
/*******************************************************************************
** 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).
** Read the app name.
*******************************************************************************/
public String getAppName()
{
@@ -214,10 +214,7 @@ public class WormAuditQBitProducer implements QBitMetaDataProducer<WormAuditQBit
{
qInstance.addScheduler(new QuartzSchedulerMetaData().withName(qBitConfig.getSchedulerName()));
}
if(qBitConfig.getIncludeApp())
{
output.add(WormAuditApp.app(qBitConfig, producedTables));
}
output.add(WormAuditApp.app(qBitConfig, producedTables));
for(QWidgetMetaData widget : WormAuditApp.widgets())
{
output.add(widget);
@@ -115,7 +115,7 @@ public final class WormAuditHelp
table.withHelpContent(SLOT, content(entry.getValue()));
}
}
else if(config.getIncludeApp() && key.startsWith("app:"))
else if(key.startsWith("app:"))
{
QAppMetaData app = qInstance.getApp(config.getAppName());
if(app != null && (app.getHelpContent() == null || !app.getHelpContent().containsKey(SLOT)))
@@ -24,7 +24,6 @@ package com.kingsrook.qbits.wormaudit.processes;
import java.time.Instant;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import com.kingsrook.qbits.wormaudit.WormAuditQBitConfig;
import com.kingsrook.qbits.wormaudit.chain.AuditStore;
import com.kingsrook.qbits.wormaudit.model.WormAuditTables;
@@ -44,11 +43,6 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
*******************************************************************************/
public final class VerificationRuns
{
/** How many break reasons the alert summary spells out; the title carries the total. */
private static final int ALERT_SUMMARY_REASONS = 3;
/*******************************************************************************
** Utility class.
*******************************************************************************/
@@ -85,15 +79,8 @@ public final class VerificationRuns
}
else
{
long problems = result.getBreakCount();
String summary = result.getBreaks().stream().limit(ALERT_SUMMARY_REASONS).map(b -> 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("Audit chain verification found " + problems + (problems == 1 ? " problem" : " problems")).withSummary(summary)
.withRecordRef(new AlertRecordRef(WormAuditTables.VERIFICATION, row.getValue("id"))));
.withTitle(result.getBreaks().getFirst().reason()).withRecordRef(new AlertRecordRef(WormAuditTables.VERIFICATION, row.getValue("id"))));
}
}
return (result);
@@ -142,8 +142,7 @@ class ProcessesAndAlertsTest
assertThat(Sink.raised.getFirst().getDedupKey()).isEqualTo("chain");
assertThat(Sink.raised.getFirst().getRecordRefs().getFirst().getTableName())
.isEqualTo(WormAuditTables.VERIFICATION);
assertThat(Sink.raised.getFirst().getTitle()).isEqualTo("Audit chain verification found 1 problem");
assertThat(Sink.raised.getFirst().getSummary()).contains("Break at link 1");
assertThat(Sink.raised.getFirst().getTitle()).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);
@@ -278,29 +278,6 @@ 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.
*******************************************************************************/
@@ -21,15 +21,10 @@
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;
@@ -48,7 +43,6 @@ 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();
@@ -166,36 +160,4 @@ 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());
}
}