From 0dec9463a632faadcdc0a5d4728f57e9529e96ef Mon Sep 17 00:00:00 2001 From: James Maes Date: Wed, 14 Oct 2026 10:10:18 -0500 Subject: [PATCH 1/3] chore: use core field access and audit-token APIs directly; remove stand-ins QQQ #1172 and #1182 are released, so the standin package and the reflective core bridges go; the resolver, key source and audit protector register through core metadata, and the core-dependent tests run unconditionally. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 6 + README.md | 13 +- pom.xml | 4 +- .../qbits/secureddata/CoreAuditBridge.java | 220 -------------- .../secureddata/CoreFieldAccessBridge.java | 167 ----------- .../secureddata/SecuredDataAccessLogger.java | 7 +- .../SecuredDataAuditProtector.java | 11 +- .../secureddata/SecuredDataKeySource.java | 5 +- .../secureddata/SecuredDataQBitConfig.java | 3 +- .../secureddata/SecuredDataQBitProducer.java | 9 +- .../secureddata/SecuredDataResolver.java | 11 +- .../metadata/SecuredDataEnums.java | 5 +- .../secureddata/retention/WipeService.java | 15 +- .../secureddata/reveal/RevealService.java | 2 +- .../standin/AuditValueProtectorInterface.java | 50 ---- .../standin/ClassifiedValueKeySource.java | 70 ----- .../standin/ClassifiedValueTokens.java | 218 -------------- .../qbits/secureddata/standin/DMLType.java | 30 -- .../standin/FieldAccessDecision.java | 43 --- .../secureddata/standin/FieldAccessEvent.java | 272 ------------------ .../standin/FieldAccessEventType.java | 33 --- .../standin/FieldAccessOutcome.java | 33 --- .../standin/FieldAccessResolverInterface.java | 90 ------ .../secureddata/standin/FieldAccessTier.java | 126 -------- .../secureddata/standin/FieldMaskStyle.java | 33 --- .../standin/ProtectedAuditValues.java | 30 -- ...ssBridgeIT.java => CoreFieldAccessIT.java} | 27 +- .../qbits/secureddata/RetentionProofTest.java | 18 +- .../qbits/secureddata/RetentionWipeTest.java | 12 +- .../secureddata/RevealAndBreakGlassTest.java | 4 +- .../SecuredDataAccessLoggerTest.java | 7 +- .../secureddata/SecuredDataAnomaliesTest.java | 6 +- .../qbits/secureddata/SecuredDataDdlTest.java | 5 +- .../secureddata/SecuredDataHomeTest.java | 4 +- .../SecuredDataQBitProducerTest.java | 9 +- .../secureddata/SecuredDataResolverTest.java | 9 +- 36 files changed, 76 insertions(+), 1531 deletions(-) delete mode 100644 src/main/java/com/kingsrook/qbits/secureddata/CoreAuditBridge.java delete mode 100644 src/main/java/com/kingsrook/qbits/secureddata/CoreFieldAccessBridge.java delete mode 100644 src/main/java/com/kingsrook/qbits/secureddata/standin/AuditValueProtectorInterface.java delete mode 100644 src/main/java/com/kingsrook/qbits/secureddata/standin/ClassifiedValueKeySource.java delete mode 100644 src/main/java/com/kingsrook/qbits/secureddata/standin/ClassifiedValueTokens.java delete mode 100644 src/main/java/com/kingsrook/qbits/secureddata/standin/DMLType.java delete mode 100644 src/main/java/com/kingsrook/qbits/secureddata/standin/FieldAccessDecision.java delete mode 100644 src/main/java/com/kingsrook/qbits/secureddata/standin/FieldAccessEvent.java delete mode 100644 src/main/java/com/kingsrook/qbits/secureddata/standin/FieldAccessEventType.java delete mode 100644 src/main/java/com/kingsrook/qbits/secureddata/standin/FieldAccessOutcome.java delete mode 100644 src/main/java/com/kingsrook/qbits/secureddata/standin/FieldAccessResolverInterface.java delete mode 100644 src/main/java/com/kingsrook/qbits/secureddata/standin/FieldAccessTier.java delete mode 100644 src/main/java/com/kingsrook/qbits/secureddata/standin/FieldMaskStyle.java delete mode 100644 src/main/java/com/kingsrook/qbits/secureddata/standin/ProtectedAuditValues.java rename src/test/java/com/kingsrook/qbits/secureddata/{CoreFieldAccessBridgeIT.java => CoreFieldAccessIT.java} (93%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c85461..8d3f98d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes are documented here, following [Keep a Changelog](https://ke ## [Unreleased] +### Changed — direct core field access and audit protection +- Remove all #1172/#1182 stand-ins and reflective bridges; use core field-access, classified-value token, audit protection, wipe flag and shredded-event APIs directly. +- Register the resolver, key source and audit protector through core supplemental metadata. +- Run core field-access integration and retention proof tests unconditionally. +- Temporarily pin QQQ to `4.1.0-SECUREDATA-SNAPSHOT`, pending the first RC with #1172 and #1182 (RC.7 expected). + ### Changed — RC.6 migration - Pin QQQ to 4.1.0-RC.6; use released classification, client-channel and table-feature APIs directly. - Fold UI API code and tests into the main build and remove the profile and reflective registration. diff --git a/README.md b/README.md index 76f6226..1891a73 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,11 @@ Phases (b), (c), (e) and (f) of [SECURED-DATA](https://github.com/QRun-IO/qqq/bl ## Compatibility -Java 21, Maven wrapper 3.9.11, **QQQ 4.1.0-RC.6**. +Java 21, Maven wrapper 3.9.11, and the first **QQQ RC that includes #1172 and #1182 (RC.7 expected)**. Until that RC is available, the build uses the locally installed `4.1.0-SECUREDATA-SNAPSHOT`. -RC.6 supplies core classifications, AI metadata, client channels, table-feature permissions and the UI API extension points. These are used directly; the UI API and its contract test are part of the main build. The installed middleware jar provides `UiApiContract`, but no `UiApiContractAssertions` facade, so the contract test calls that released API directly. +Core classifications, AI metadata, client channels, table-feature permissions and UI API extension points are used directly. The UI API and its contract test are part of the main build; the contract test calls `UiApiContract` directly. -Field access (#1172) and audit-value protection (#1182) are not in RC.6. Field-access stand-ins and `CoreFieldAccessBridge` remain until the first QQQ RC that ships both #1172 and #1182 (expected RC.8). RC.6 **does not enforce field tiers**. Integration tests can use `4.1.0-SECUREDATA-SNAPSHOT`. +Field access (#1172), classified-value tokens and audit-value protection (#1182) use core types directly. The producer registers the resolver, key source and audit protector through core supplemental metadata; QQQ core enforces field tiers. ## Install and register @@ -159,10 +159,9 @@ Configure before producing the QBit; runtime infrastructure and HMAC keys are ca ```powershell ./mvnw.cmd -q -o clean verify -./mvnw.cmd -q -o clean verify -Dqqq.snapshot.version=4.1.0-SECUREDATA-SNAPSHOT ``` -The direct suite uses memory and H2. `CoreFieldAccessBridgeIT` is included by Surefire and uses JUnit assumptions to skip unless the actual core resolver metadata is present. With the installed core snapshot it exercises Query/Get, policy tiers, masking, exact matches and outcomes, sorting denial, rate/entropy refusal and the assistant wall. Checkstyle and JaCoCo remain 70% instructions / 90% classes; stand-ins are not excluded. +The suite uses memory and H2. `CoreFieldAccessIT` runs unconditionally through Surefire and exercises Query/Get, policy tiers, masking, exact matches and outcomes, sorting denial, rate/entropy refusal and the assistant wall. `RetentionProofTest` also runs unconditionally. Checkstyle and JaCoCo gates remain 70% instructions / 90% classes. ## Anomaly monitoring (phase e) @@ -243,11 +242,11 @@ Each is read from a JVM property or the environment and is never logged. Losing ### Core hooks -Audit-value protection (QRun-IO/qqq#1182) and the wipe audit flag need core classes that are not in RC.6. On RC.6 the qbit uses marked stand-ins (`removeBy=RC.8`), writes value-free wipe audit details itself, and core does not tokenise ordinary audits. Run the build with `-Dqqq.snapshot.version=4.1.0-SECUREDATA-SNAPSHOT` to exercise the real hooks, including the section 15 canary and wipe-proof tests (`RetentionProofTest`), which skip themselves when the hooks are absent. +Audit-value protection, wipe audit flags and shredded audit events use the core #1182 APIs directly. Core tokenises ordinary audits and writes value-free wipe details. The section 15 canary and wipe-proof tests (`RetentionProofTest`) are mandatory in the normal build. ## Remaining phases -RC.6 migration is complete; field-access and audit-token compatibility types remain until RC.8. +Field-access and audit-token stand-ins and reflective bridges have been removed. The coordinator will replace the temporary snapshot pin with the first QQQ RC containing #1172 and #1182. ## License diff --git a/pom.xml b/pom.xml index a5bba5d..3b2c6d1 100644 --- a/pom.xml +++ b/pom.xml @@ -22,8 +22,8 @@ - - 4.1.0-RC.6 + + 4.1.0-SECUREDATA-SNAPSHOT 1.0.0-SNAPSHOT 21 21 diff --git a/src/main/java/com/kingsrook/qbits/secureddata/CoreAuditBridge.java b/src/main/java/com/kingsrook/qbits/secureddata/CoreAuditBridge.java deleted file mode 100644 index bbb4a57..0000000 --- a/src/main/java/com/kingsrook/qbits/secureddata/CoreAuditBridge.java +++ /dev/null @@ -1,220 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * 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 - * - * https://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.kingsrook.qbits.secureddata; - -import java.io.Serializable; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Proxy; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import com.kingsrook.qbits.secureddata.standin.ClassifiedValueKeySource; -import com.kingsrook.qbits.secureddata.standin.ClassifiedValueTokens; -import com.kingsrook.qbits.secureddata.standin.DMLType; -import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; -import com.kingsrook.qqq.backend.core.actions.audits.AuditAction; -import com.kingsrook.qqq.backend.core.exceptions.QException; -import com.kingsrook.qqq.backend.core.model.actions.audits.AuditInput; -import com.kingsrook.qqq.backend.core.model.actions.tables.ActionFlag; -import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput; -import com.kingsrook.qqq.backend.core.model.data.QRecord; -import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; - -/******************************************************************************* - ** A single reflection boundary for #1182, failing closed for incompatible hooks. - *******************************************************************************/ -// STAND-IN(core=QRun-IO/qqq#1182, removeBy=RC.8) -public final class CoreAuditBridge -{ - private static final String TOKENS = "com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess."; - private static final String AUDITS = "com.kingsrook.qqq.backend.core.actions.audits."; - - /******************************************************************************* - ** Utility class. - *******************************************************************************/ - private CoreAuditBridge() - { - } - - /******************************************************************************* - ** Core hook availability (RC.6 has none). - *******************************************************************************/ - public static boolean available() - { - try - { - Class.forName(TOKENS + "ClassifiedValueTokens"); - return (true); - } - catch(ClassNotFoundException expected) - { - return (false); - } - } - - /******************************************************************************* - ** Register exact #1182 contracts when present; no silent partial registration. - *******************************************************************************/ - public static void register(QInstance instance, SecuredDataRuntime runtime) - { - if(!available()) - { - return; - } - try - { - Class contract = Class.forName(TOKENS + "ClassifiedValueKeySource"); - Object keyProxy = Proxy.newProxyInstance(contract.getClassLoader(), new Class[] {contract}, (self, method, args) -> - { - if(method.getDeclaringClass() == Object.class) - { - return (objectMethod(self, method.getName(), args)); - } - Optional key = switch(method.getName()) - { - case "instanceKey" -> runtime.keys().instanceKey(); - case "keyForRecordField" -> runtime.keys().keyForRecordField((String) args[0], (String) args[1], (Serializable) args[2], (Boolean) args[3]); - default -> throw new UnsupportedOperationException(method.getName()); - }; - return (key.isEmpty() ? Optional.empty() : Optional.of(Class.forName(TOKENS + "ClassifiedValueKeySource$KeyAndId") - .getConstructor(byte[].class, String.class).newInstance(key.get().key(), key.get().id()))); - }); - Class metadata = Class.forName(TOKENS + "ClassifiedValueKeySourceMetaData"); - Object registry = metadata.getMethod("ofOrWithNew", QInstance.class).invoke(null, instance); - metadata.getMethod("withKeySourceInstance", contract).invoke(registry, keyProxy); - SecuredDataAuditProtector protector = new SecuredDataAuditProtector(); - Class auditContract = Class.forName(AUDITS + "AuditValueProtectorInterface"); - Object auditProxy = Proxy.newProxyInstance(auditContract.getClassLoader(), new Class[] {auditContract}, (self, method, args) -> - { - if(method.getDeclaringClass() == Object.class) - { - return (objectMethod(self, method.getName(), args)); - } - if("classificationOf".equals(method.getName())) - { - return (protector.classificationOf((QTableMetaData) args[0], (QFieldMetaData) args[1])); - } - if(!"protect".equals(method.getName())) - { - throw (new UnsupportedOperationException(method.getName())); - } - var values = protector.protect((QTableMetaData) args[0], (QFieldMetaData) args[1], (Serializable) args[2], (Serializable) args[3], (Serializable) args[4], DMLType.valueOf(args[5].toString())); - return (Class.forName(AUDITS + "ProtectedAuditValues").getConstructor(String.class, String.class, String.class) - .newInstance(values.oldToken(), values.newToken(), values.message())); - }); - metadata = Class.forName(AUDITS + "AuditValueProtectionMetaData"); - registry = metadata.getMethod("ofOrWithNew", QInstance.class).invoke(null, instance); - metadata.getMethod("withProtectorInstance", auditContract).invoke(registry, auditProxy); - } - catch(ReflectiveOperationException e) - { - throw (new IllegalStateException("Unable to register core audit protection", e)); - } - } - - /******************************************************************************* - ** Stable object behavior for registered service proxies. - *******************************************************************************/ - private static Object objectMethod(Object self, String name, Object[] args) - { - return (switch(name) - { - case "toString" -> "Secured-data audit bridge"; - case "hashCode" -> System.identityHashCode(self); - case "equals" -> self == args[0]; - default -> throw new UnsupportedOperationException(name); - }); - } - - /******************************************************************************* - ** Delegate to the installed core helper, with contract-identical RC.6 fallback. - *******************************************************************************/ - public static String token(QTableMetaData table, QFieldMetaData field, Serializable pk, Serializable value) - { - if(!available()) - { - return (ClassifiedValueTokens.forRecordField(table, field, pk, value)); - } - try - { - return ((String) Class.forName(TOKENS + "ClassifiedValueTokens").getMethod("forRecordField", QTableMetaData.class, QFieldMetaData.class, Serializable.class, Serializable.class) - .invoke(null, table, field, pk, value)); - } - catch(InvocationTargetException e) - { - throw (new IllegalStateException("Classified token creation failed")); - } - catch(ReflectiveOperationException e) - { - throw (new IllegalStateException("Incompatible core token API", e)); - } - } - - /******************************************************************************* - ** Real WipeAuditFlag on snapshot; RC.6 uses a value-free explicit audit below. - *******************************************************************************/ - public static void flag(UpdateInput input, String ruleId) - { - if(!available()) - { - input.setOmitDmlAudit(true); - return; - } - try - { - ActionFlag flag = (ActionFlag) Class.forName("com.kingsrook.qqq.backend.core.model.actions.tables.WipeAuditFlag").getConstructor(String.class).newInstance(ruleId); - input.setFlags(Set.of(flag)); - } - catch(ReflectiveOperationException e) - { - throw (new IllegalStateException("Incompatible core wipe API", e)); - } - } - - /******************************************************************************* - ** Append immutable value-free audit evidence in the batch transaction. - *******************************************************************************/ - public static void detail(QTableMetaData table, Serializable pk, String field, String ruleId, boolean shredded, QBackendTransaction transaction) throws QException - { - if(shredded && available()) - { - try - { - Class.forName(AUDITS + "AuditShreddedEvent").getMethod("shreddedDetail", QTableMetaData.class, Serializable.class, QFieldMetaData.class, String.class, QBackendTransaction.class) - .invoke(null, table, pk, table.getField(field), ruleId, transaction); - return; - } - catch(ReflectiveOperationException e) - { - throw (new QException("Unable to append shredded audit detail")); - } - } - String message = (shredded ? "Audit values shredded" : "Wiped") + " per policy"; - QRecord detail = new QRecord().withValue("fieldName", field).withValue("newValue", (shredded ? "shredded:v1:" : "wiped:v1:") + ruleId).withValue("message", message); - AuditInput input = new AuditInput(); - input.setTransaction(transaction); - AuditAction.appendToInput(input, table.getName(), pk, AuditAction.getRecordSecurityKeyValues(table, - new QRecord().withValue(table.getPrimaryKeyField(), pk), Optional.empty()), message, List.of(detail)); - new AuditAction().execute(input); - } -} diff --git a/src/main/java/com/kingsrook/qbits/secureddata/CoreFieldAccessBridge.java b/src/main/java/com/kingsrook/qbits/secureddata/CoreFieldAccessBridge.java deleted file mode 100644 index dd2a424..0000000 --- a/src/main/java/com/kingsrook/qbits/secureddata/CoreFieldAccessBridge.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * 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 - * - * https://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.kingsrook.qbits.secureddata; - - -import java.io.Serializable; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Proxy; -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; -import com.kingsrook.qbits.secureddata.standin.FieldAccessDecision; -import com.kingsrook.qbits.secureddata.standin.FieldAccessEvent; -import com.kingsrook.qbits.secureddata.standin.FieldAccessEventType; -import com.kingsrook.qbits.secureddata.standin.FieldAccessOutcome; -import com.kingsrook.qbits.secureddata.standin.FieldAccessResolverInterface; -import com.kingsrook.qqq.backend.core.logging.QLogger; -import com.kingsrook.qqq.backend.core.model.metadata.QInstance; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; - - -/******************************************************************************* - ** The only boundary between the temporary field-access API and unreleased core. - *******************************************************************************/ -// STAND-IN(core=QRun-IO/qqq#1172, removeBy=RC.8) -public final class CoreFieldAccessBridge -{ - private static final String PACKAGE = "com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess."; - private static final QLogger LOG = QLogger.getLogger(CoreFieldAccessBridge.class); - private static final AtomicBoolean WARNED = new AtomicBoolean(); - - - - /******************************************************************************* - ** Utility class. - *******************************************************************************/ - private CoreFieldAccessBridge() - { - } - - - - /******************************************************************************* - ** Register only when core can actually enforce field access. - *******************************************************************************/ - public static boolean register(QInstance instance, FieldAccessResolverInterface resolver) - { - try - { - Class metadata = Class.forName(PACKAGE + "FieldAccessResolverMetaData"); - Class contract = Class.forName(PACKAGE + "FieldAccessResolverInterface"); - Object proxy = Proxy.newProxyInstance(contract.getClassLoader(), new Class[] {contract}, (self, method, args) -> - { - return (switch(method.getName()) - { - case "resolve" -> decision(resolver.resolve((QTableMetaData) args[0], (QFieldMetaData) args[1])); - case "getAccessFingerprint" -> resolver.getAccessFingerprint(); - case "isRevealActive" -> resolver.isRevealActive((String) args[0], (String) args[1], (Serializable) args[2]); - case "fingerprintSearchValue" -> resolver.fingerprintSearchValue((Serializable) args[0]); - case "checkExactMatch" -> - { - resolver.checkExactMatch(event(args[0])); - yield null; - } - case "onAccess" -> - { - resolver.onAccess(event(args[0])); - yield null; - } - case "toString" -> "SecuredData core bridge"; - case "hashCode" -> System.identityHashCode(self); - case "equals" -> self == args[0]; - default -> throw new UnsupportedOperationException(method.getName()); - }); - }); - Object registry = metadata.getMethod("ofOrWithNew", QInstance.class).invoke(null, instance); - metadata.getMethod("withResolverInstance", contract).invoke(registry, proxy); - return (true); - } - catch(ClassNotFoundException e) - { - if(WARNED.compareAndSet(false, true)) - { - LOG.info("Secured-data field enforcement needs QQQ RC.6+; policy and access-log services are available."); - } - return (false); - } - catch(ReflectiveOperationException e) - { - throw (new IllegalStateException("Core field-access API is incompatible", unwrap(e))); - } - } - - - - /******************************************************************************* - ** Construct the core record by its declared component types and enum names. - *******************************************************************************/ - private static Object decision(FieldAccessDecision decision) throws ReflectiveOperationException - { - Class type = Class.forName(PACKAGE + "FieldAccessDecision"); - Class tier = type.getRecordComponents()[0].getType(); - Class mask = type.getRecordComponents()[1].getType(); - return (type.getConstructor(tier, mask, String.class, boolean.class).newInstance( - enumValue(tier, decision.tier().name()), decision.maskStyle() == null ? null : enumValue(mask, decision.maskStyle().name()), - decision.reason(), decision.exactMatchAllowed())); - } - - - - /******************************************************************************* - ** Convert enum types without sharing class identity across the bridge. - *******************************************************************************/ - private static Object enumValue(Class type, String name) throws ReflectiveOperationException - { - return (type.getMethod("valueOf", String.class).invoke(null, name)); - } - - - - /******************************************************************************* - ** Events carry identifiers and keyed fingerprints, never search literals. - *******************************************************************************/ - @SuppressWarnings("unchecked") - private static FieldAccessEvent event(Object source) throws ReflectiveOperationException - { - Class type = source.getClass(); - Object outcome = type.getMethod("getOutcome").invoke(source); - return (new FieldAccessEvent() - .withType(FieldAccessEventType.valueOf(type.getMethod("getType").invoke(source).toString())) - .withTableName((String) type.getMethod("getTableName").invoke(source)) - .withFieldNames((List) type.getMethod("getFieldNames").invoke(source)) - .withPrimaryKeys((List) type.getMethod("getPrimaryKeys").invoke(source)) - .withRecordCount((Integer) type.getMethod("getRecordCount").invoke(source)) - .withActionName((String) type.getMethod("getActionName").invoke(source)) - .withOutcome(outcome == null ? null : FieldAccessOutcome.valueOf(outcome.toString())) - .withSearchValueFingerprints((List) type.getMethod("getSearchValueFingerprints").invoke(source))); - } - - - - /******************************************************************************* - ** Preserve the startup failure, including duplicate resolver registration. - *******************************************************************************/ - private static Throwable unwrap(ReflectiveOperationException exception) - { - return (exception instanceof InvocationTargetException invocation ? invocation.getCause() : exception); - } -} diff --git a/src/main/java/com/kingsrook/qbits/secureddata/SecuredDataAccessLogger.java b/src/main/java/com/kingsrook/qbits/secureddata/SecuredDataAccessLogger.java index 8cf0822..0e9c806 100644 --- a/src/main/java/com/kingsrook/qbits/secureddata/SecuredDataAccessLogger.java +++ b/src/main/java/com/kingsrook/qbits/secureddata/SecuredDataAccessLogger.java @@ -38,11 +38,11 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import com.kingsrook.qbits.secureddata.metadata.SecuredDataTables; -import com.kingsrook.qbits.secureddata.standin.FieldAccessEvent; -import com.kingsrook.qbits.secureddata.standin.FieldAccessEventType; -import com.kingsrook.qbits.secureddata.standin.FieldAccessOutcome; import com.kingsrook.qbits.secureddata.support.SecuredDataMetadata; import com.kingsrook.qbits.secureddata.support.SystemWrites; +import com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.FieldAccessEvent; +import com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.FieldAccessEventType; +import com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.FieldAccessOutcome; import com.kingsrook.qqq.backend.core.alerts.AlertSignal; import com.kingsrook.qqq.backend.core.alerts.QAlerts; import com.kingsrook.qqq.backend.core.context.CapturedContext; @@ -60,7 +60,6 @@ import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession; import com.kingsrook.qqq.backend.core.utils.JsonUtils; - /******************************************************************************* ** Bounded asynchronous value-free audit, with persisted and local enumeration accounting. *******************************************************************************/ diff --git a/src/main/java/com/kingsrook/qbits/secureddata/SecuredDataAuditProtector.java b/src/main/java/com/kingsrook/qbits/secureddata/SecuredDataAuditProtector.java index a3630e1..c290f43 100644 --- a/src/main/java/com/kingsrook/qbits/secureddata/SecuredDataAuditProtector.java +++ b/src/main/java/com/kingsrook/qbits/secureddata/SecuredDataAuditProtector.java @@ -22,10 +22,11 @@ package com.kingsrook.qbits.secureddata; import java.io.Serializable; import com.kingsrook.qbits.secureddata.retention.RetentionRules; -import com.kingsrook.qbits.secureddata.standin.AuditValueProtectorInterface; -import com.kingsrook.qbits.secureddata.standin.DMLType; -import com.kingsrook.qbits.secureddata.standin.ProtectedAuditValues; import com.kingsrook.qqq.backend.core.actions.ai.DataClassification; +import com.kingsrook.qqq.backend.core.actions.audits.AuditValueProtectorInterface; +import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction.DMLType; +import com.kingsrook.qqq.backend.core.actions.audits.ProtectedAuditValues; +import com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.ClassifiedValueTokens; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; @@ -56,7 +57,7 @@ public final class SecuredDataAuditProtector implements AuditValueProtectorInter { boolean changed = RetentionRules.changedOnly(table.getName(), field.getName()); String message = (oldValue == null ? "Set " : newValue == null ? "Removed " : "Changed ") + field.getLabel(); - return (new ProtectedAuditValues(oldValue == null ? null : changed ? "changed:v1" : CoreAuditBridge.token(table, field, pk, oldValue), - newValue == null ? null : changed ? "changed:v1" : CoreAuditBridge.token(table, field, pk, newValue), message)); + return (new ProtectedAuditValues(oldValue == null ? null : changed ? "changed:v1" : ClassifiedValueTokens.forRecordField(table, field, pk, oldValue), + newValue == null ? null : changed ? "changed:v1" : ClassifiedValueTokens.forRecordField(table, field, pk, newValue), message)); } } diff --git a/src/main/java/com/kingsrook/qbits/secureddata/SecuredDataKeySource.java b/src/main/java/com/kingsrook/qbits/secureddata/SecuredDataKeySource.java index cdf6c15..ac34362 100644 --- a/src/main/java/com/kingsrook/qbits/secureddata/SecuredDataKeySource.java +++ b/src/main/java/com/kingsrook/qbits/secureddata/SecuredDataKeySource.java @@ -37,9 +37,10 @@ import java.util.Map; import java.util.Optional; import com.kingsrook.qbits.secureddata.metadata.SecuredDataTables; import com.kingsrook.qbits.secureddata.retention.RetentionRules; -import com.kingsrook.qbits.secureddata.standin.ClassifiedValueKeySource; import com.kingsrook.qbits.secureddata.support.SystemWrites; import com.kingsrook.qqq.backend.core.actions.ai.DataClassification; +import com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.ClassifiedValueKeySource; +import com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.ClassifiedValueTokens; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; @@ -280,7 +281,7 @@ public final class SecuredDataKeySource implements ClassifiedValueKeySource, Aut instanceOnly.set(true); try { - return (CoreAuditBridge.token(table, field, pk, value)); + return (ClassifiedValueTokens.forRecordField(table, field, pk, value)); } finally { diff --git a/src/main/java/com/kingsrook/qbits/secureddata/SecuredDataQBitConfig.java b/src/main/java/com/kingsrook/qbits/secureddata/SecuredDataQBitConfig.java index 10e5f8a..edc18da 100644 --- a/src/main/java/com/kingsrook/qbits/secureddata/SecuredDataQBitConfig.java +++ b/src/main/java/com/kingsrook/qbits/secureddata/SecuredDataQBitConfig.java @@ -29,13 +29,12 @@ import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Supplier; -import com.kingsrook.qbits.secureddata.standin.FieldAccessTier; import com.kingsrook.qqq.backend.core.actions.ai.DataClassification; +import com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.FieldAccessTier; import com.kingsrook.qqq.backend.core.model.metadata.producers.MetaDataCustomizerInterface; import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitConfig; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; - /******************************************************************************* ** Configuration for policy resolution and bounded asynchronous access logging. *******************************************************************************/ diff --git a/src/main/java/com/kingsrook/qbits/secureddata/SecuredDataQBitProducer.java b/src/main/java/com/kingsrook/qbits/secureddata/SecuredDataQBitProducer.java index f9041f0..c16b4bb 100644 --- a/src/main/java/com/kingsrook/qbits/secureddata/SecuredDataQBitProducer.java +++ b/src/main/java/com/kingsrook/qbits/secureddata/SecuredDataQBitProducer.java @@ -29,7 +29,10 @@ import com.kingsrook.qbits.secureddata.metadata.SecuredDataTableCustomizers; import com.kingsrook.qbits.secureddata.metadata.SecuredDataTables; import com.kingsrook.qbits.secureddata.support.SecuredDataMetadata; import com.kingsrook.qqq.backend.core.actions.ai.DataClassification; +import com.kingsrook.qqq.backend.core.actions.audits.AuditValueProtectionMetaData; import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; +import com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.ClassifiedValueKeySourceMetaData; +import com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.FieldAccessResolverMetaData; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.metadata.MetaDataProducerMultiOutput; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; @@ -39,7 +42,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitMetaData; import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitMetaDataProducer; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; - /******************************************************************************* ** Entry point for secured-data policy, classification and access logging. *******************************************************************************/ @@ -171,8 +173,9 @@ public class SecuredDataQBitProducer implements QBitMetaDataProducer instanceKey(); - - /******************************************************************************* - ** Verification uses create=false and must never recreate a destroyed key. - *******************************************************************************/ - Optional keyForRecordField(String table, String field, Serializable pk, boolean create); - - /******************************************************************************* - ** Key bytes and the public identifier stored in a token. - *******************************************************************************/ - record KeyAndId(byte[] key, String id) - { - /******************************************************************************* - ** Own the key bytes; identifiers must keep tokens ASCII and under 200 characters. - *******************************************************************************/ - public KeyAndId - { - if(key == null || key.length == 0 || id == null || !id.matches("[A-Za-z0-9._-]{1,140}")) - { - throw new IllegalArgumentException("Invalid classified-value key or identifier"); - } - key = key.clone(); - } - - /******************************************************************************* - ** Do not expose mutable key material. - *******************************************************************************/ - @Override - public byte[] key() - { - return (key.clone()); - } - } -} diff --git a/src/main/java/com/kingsrook/qbits/secureddata/standin/ClassifiedValueTokens.java b/src/main/java/com/kingsrook/qbits/secureddata/standin/ClassifiedValueTokens.java deleted file mode 100644 index 41dd7ee..0000000 --- a/src/main/java/com/kingsrook/qbits/secureddata/standin/ClassifiedValueTokens.java +++ /dev/null @@ -1,218 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2026. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * 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 - * - * https://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.kingsrook.qbits.secureddata.standin; - - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; -import java.io.Serializable; -import java.math.BigDecimal; -import java.nio.charset.StandardCharsets; -import java.security.GeneralSecurityException; -import java.security.MessageDigest; -import java.util.Base64; -import java.util.List; -import java.util.Optional; -import com.kingsrook.qbits.secureddata.standin.ClassifiedValueKeySource.KeyAndId; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; -import com.kingsrook.qqq.backend.core.utils.ValueUtils; - - -/******************************************************************************* - ** Shared, versioned audit/email value tokens. The MAC input is UTF-8. - *******************************************************************************/ -// STAND-IN(core=QRun-IO/qqq#1182, removeBy=RC.8) -public final class ClassifiedValueTokens -{ - - /******************************************************************************* - ** Static utility. - *******************************************************************************/ - private ClassifiedValueTokens() - { - } - - - - /******************************************************************************* - ** Bind a value to the table, field and primary key, preferring a shreddable key. - *******************************************************************************/ - public static String forRecordField(QTableMetaData table, QFieldMetaData field, Serializable pk, Serializable value) - { - if(value == null) - { - return null; - } - ClassifiedValueKeySource source = source(); - Optional key = source.keyForRecordField(table.getName(), field.getName(), pk, true); - return key.isPresent() ? token("shred", key.get(), binding(table, field, pk), value) - : instanceToken(source, binding(table, field, pk), value); - } - - - - /******************************************************************************* - ** Non-record bindings must start with a nonempty domain. - *******************************************************************************/ - public static String forContext(List bindingParts, Serializable value) - { - validateDomain(bindingParts); - return value == null ? null : instanceToken(source(), bindingParts, value); - } - - - - /******************************************************************************* - ** No derivative of the original value. - *******************************************************************************/ - public static String changed() - { - return "changed:v1"; - } - - - - /******************************************************************************* - ** A key-destruction event, never a derivative of the value. - *******************************************************************************/ - public static String shreddedToken(String ruleId) - { - if(ruleId == null || !ruleId.matches("[A-Za-z0-9._-]{1,180}")) - { - throw new IllegalArgumentException("Invalid shred rule identifier"); - } - return "shredded:v1:" + ruleId; - } - - - - /******************************************************************************* - ** Verify only keyed tokens, using existing keys and constant-time MAC comparison. - *******************************************************************************/ - public static boolean verifyRecordField(QTableMetaData table, QFieldMetaData field, Serializable pk, Serializable candidate, String token) - { - if(candidate == null || token == null) - { - return false; - } - ClassifiedValueKeySource source = source(); - if(token.startsWith("shred:v1:")) - { - return verify(source.keyForRecordField(table.getName(), field.getName(), pk, false), "shred", binding(table, field, pk), candidate, token); - } - return verify(source.instanceKey(), "hmac", binding(table, field, pk), candidate, token); - } - - - - /******************************************************************************* - ** Verify a non-record token without creating a key. - *******************************************************************************/ - public static boolean verifyContext(List bindingParts, Serializable candidate, String token) - { - validateDomain(bindingParts); - return candidate != null && token != null && verify(source().instanceKey(), "hmac", bindingParts, candidate, token); - } - - - - /******************************************************************************* - ** Canonical ValueUtils strings, with scale-independent plain decimal notation. - *******************************************************************************/ - public static String canonical(Serializable value) - { - return value instanceof BigDecimal decimal ? decimal.stripTrailingZeros().toPlainString() : ValueUtils.getValueAsString(value); - } - - - - /******************************************************************************* - ** Validate the context even for absent values. - *******************************************************************************/ - private static void validateDomain(List parts) - { - if(parts == null || parts.isEmpty() || parts.get(0) == null || parts.get(0).isBlank() || parts.stream().anyMatch(p -> p == null || p.indexOf('\u001F') >= 0)) - { - throw new IllegalArgumentException("A nonempty domain and unambiguous binding parts are required"); - } - } - - - - /******************************************************************************* - ** The agreed record binding does not prepend an additional domain. - *******************************************************************************/ - private static List binding(QTableMetaData table, QFieldMetaData field, Serializable pk) - { - return List.of(table.getName(), field.getName(), String.valueOf(pk)); - } - - - - /******************************************************************************* - ** Emit a keyed token or a value-free fallback. - *******************************************************************************/ - private static String instanceToken(ClassifiedValueKeySource source, List parts, Serializable value) - { - return source.instanceKey().map(key -> token("hmac", key, parts, value)).orElseGet(ClassifiedValueTokens::changed); - } - - - - /******************************************************************************* - ** Compare full tokens, including the key identifier and version. - *******************************************************************************/ - private static boolean verify(Optional key, String kind, List parts, Serializable value, String supplied) - { - return key.map(k -> MessageDigest.isEqual(token(kind, k, parts, value).getBytes(StandardCharsets.UTF_8), supplied.getBytes(StandardCharsets.UTF_8))).orElse(false); - } - - - - /******************************************************************************* - ** Encode the HMAC with URL-safe base64 and no padding. - *******************************************************************************/ - private static String token(String kind, KeyAndId key, List parts, Serializable value) - { - try - { - Mac mac = Mac.getInstance("HmacSHA256"); - mac.init(new SecretKeySpec(key.key(), "HmacSHA256")); - byte[] digest = mac.doFinal((String.join("\u001F", parts) + "\u001F" + canonical(value)).getBytes(StandardCharsets.UTF_8)); - return kind + ":v1:" + key.id() + ":" + Base64.getUrlEncoder().withoutPadding().encodeToString(digest); - } - catch(GeneralSecurityException e) - { - throw new IllegalStateException("Unable to compute classified-value token", e); - } - } - - - - /******************************************************************************* - ** Use instance metadata, falling back to environment/property configuration. - *******************************************************************************/ - private static ClassifiedValueKeySource source() - { - return (com.kingsrook.qbits.secureddata.SecuredDataRuntime.current().keys()); - } -} diff --git a/src/main/java/com/kingsrook/qbits/secureddata/standin/DMLType.java b/src/main/java/com/kingsrook/qbits/secureddata/standin/DMLType.java deleted file mode 100644 index e6d1d68..0000000 --- a/src/main/java/com/kingsrook/qbits/secureddata/standin/DMLType.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2025. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * 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 - * - * https://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.kingsrook.qbits.secureddata.standin; - -/******************************************************************************* - ** RC.6's nested DMLType is package-private; #1182 makes it public. - *******************************************************************************/ -// STAND-IN(core=QRun-IO/qqq#1182, removeBy=RC.8) -public enum DMLType -{ - INSERT, UPDATE, DELETE, OTHER -} diff --git a/src/main/java/com/kingsrook/qbits/secureddata/standin/FieldAccessDecision.java b/src/main/java/com/kingsrook/qbits/secureddata/standin/FieldAccessDecision.java deleted file mode 100644 index a071d3a..0000000 --- a/src/main/java/com/kingsrook/qbits/secureddata/standin/FieldAccessDecision.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2026. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * 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 - * - * https://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.kingsrook.qbits.secureddata.standin; - - - - -/******************************************************************************* - ** A resolver decision; the reason must never contain a field value. - *******************************************************************************/ -// STAND-IN(core=QRun-IO/qqq#1172, removeBy=RC.8): copy of com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.FieldAccessDecision; delete when the pin moves to the first QQQ RC with #1172 and #1182 (RC.8). -public record FieldAccessDecision(FieldAccessTier tier, FieldMaskStyle maskStyle, String reason, boolean exactMatchAllowed) -{ - public static final FieldAccessDecision FULL = new FieldAccessDecision(FieldAccessTier.FULL, null, null); - - - - /******************************************************************************* - ** Compatibility constructor: exact-match searches are allowed by default. - *******************************************************************************/ - public FieldAccessDecision(FieldAccessTier tier, FieldMaskStyle maskStyle, String reason) - { - this(tier, maskStyle, reason, true); - } -} diff --git a/src/main/java/com/kingsrook/qbits/secureddata/standin/FieldAccessEvent.java b/src/main/java/com/kingsrook/qbits/secureddata/standin/FieldAccessEvent.java deleted file mode 100644 index f731129..0000000 --- a/src/main/java/com/kingsrook/qbits/secureddata/standin/FieldAccessEvent.java +++ /dev/null @@ -1,272 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2026. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * 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 - * - * https://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.kingsrook.qbits.secureddata.standin; - - -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - - -/******************************************************************************* - ** Value-free audit notification. Primary key samples contain at most 50 keys. - *******************************************************************************/ -// STAND-IN(core=QRun-IO/qqq#1172, removeBy=RC.8): copy of com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.FieldAccessEvent; delete when the pin moves to the first QQQ RC with #1172 and #1182 (RC.8). -public class FieldAccessEvent -{ - private FieldAccessEventType type; - private String tableName; - private List fieldNames; - private List primaryKeys; - private Integer recordCount; - private String actionName; - private FieldAccessOutcome outcome; - private List searchValueFingerprints; - - - - /******************************************************************************* - ** Getter for type - *******************************************************************************/ - public FieldAccessEventType getType() - { - return (this.type); - } - - - - /******************************************************************************* - ** Setter for type - *******************************************************************************/ - public void setType(FieldAccessEventType type) - { - this.type = type; - } - - - - /******************************************************************************* - ** Fluent setter for type - *******************************************************************************/ - public FieldAccessEvent withType(FieldAccessEventType type) - { - setType(type); - return (this); - } - - - - /******************************************************************************* - ** Getter for tableName - *******************************************************************************/ - public String getTableName() - { - return (this.tableName); - } - - - - /******************************************************************************* - ** Setter for tableName - *******************************************************************************/ - public void setTableName(String tableName) - { - this.tableName = tableName; - } - - - - /******************************************************************************* - ** Fluent setter for tableName - *******************************************************************************/ - public FieldAccessEvent withTableName(String tableName) - { - setTableName(tableName); - return (this); - } - - - - /******************************************************************************* - ** Getter for fieldNames - *******************************************************************************/ - public List getFieldNames() - { - return (this.fieldNames); - } - - - - /******************************************************************************* - ** Setter for fieldNames - *******************************************************************************/ - public void setFieldNames(List fieldNames) - { - this.fieldNames = fieldNames == null ? null : new ArrayList<>(fieldNames); - } - - - - /******************************************************************************* - ** Fluent setter for fieldNames - *******************************************************************************/ - public FieldAccessEvent withFieldNames(List fieldNames) - { - setFieldNames(fieldNames); - return (this); - } - - - - /******************************************************************************* - ** Getter for primaryKeys - *******************************************************************************/ - public List getPrimaryKeys() - { - return (this.primaryKeys == null ? null : Collections.unmodifiableList(this.primaryKeys)); - } - - - - /******************************************************************************* - ** Setter for primaryKeys - *******************************************************************************/ - public void setPrimaryKeys(List primaryKeys) - { - this.primaryKeys = primaryKeys == null ? null : new ArrayList<>(primaryKeys.subList(0, Math.min(50, primaryKeys.size()))); - } - - - - /******************************************************************************* - ** Fluent setter for primaryKeys - *******************************************************************************/ - public FieldAccessEvent withPrimaryKeys(List primaryKeys) - { - setPrimaryKeys(primaryKeys); - return (this); - } - - - - /******************************************************************************* - ** Getter for recordCount - *******************************************************************************/ - public Integer getRecordCount() - { - return (this.recordCount); - } - - - - /******************************************************************************* - ** Setter for recordCount - *******************************************************************************/ - public void setRecordCount(Integer recordCount) - { - this.recordCount = recordCount; - } - - - - /******************************************************************************* - ** Fluent setter for recordCount - *******************************************************************************/ - public FieldAccessEvent withRecordCount(Integer recordCount) - { - setRecordCount(recordCount); - return (this); - } - - - - /******************************************************************************* - ** Getter for actionName - *******************************************************************************/ - public String getActionName() - { - return (this.actionName); - } - - - - /******************************************************************************* - ** Setter for actionName - *******************************************************************************/ - public void setActionName(String actionName) - { - this.actionName = actionName; - } - - - - /******************************************************************************* - ** Fluent setter for actionName - *******************************************************************************/ - public FieldAccessEvent withActionName(String actionName) - { - setActionName(actionName); - return (this); - } - - - - /******************************************************************************* - ** Getter for outcome - *******************************************************************************/ - public FieldAccessOutcome getOutcome() - { - return (outcome); - } - - - - /******************************************************************************* - ** Fluent setter for outcome - *******************************************************************************/ - public FieldAccessEvent withOutcome(FieldAccessOutcome outcome) - { - this.outcome = outcome; - return (this); - } - - - - /******************************************************************************* - ** Getter for searchValueFingerprints - *******************************************************************************/ - public List getSearchValueFingerprints() - { - return (searchValueFingerprints); - } - - - - /******************************************************************************* - ** Fluent setter for searchValueFingerprints - *******************************************************************************/ - public FieldAccessEvent withSearchValueFingerprints(List searchValueFingerprints) - { - this.searchValueFingerprints = searchValueFingerprints == null ? null : List.copyOf(searchValueFingerprints); - return (this); - } -} diff --git a/src/main/java/com/kingsrook/qbits/secureddata/standin/FieldAccessEventType.java b/src/main/java/com/kingsrook/qbits/secureddata/standin/FieldAccessEventType.java deleted file mode 100644 index 496ae57..0000000 --- a/src/main/java/com/kingsrook/qbits/secureddata/standin/FieldAccessEventType.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2026. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * 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 - * - * https://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.kingsrook.qbits.secureddata.standin; - - - - -/******************************************************************************* - ** Kinds of field access reported to the resolver. - *******************************************************************************/ -// STAND-IN(core=QRun-IO/qqq#1172, removeBy=RC.8): copy of com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.FieldAccessEventType; delete when the pin moves to the first QQQ RC with #1172 and #1182 (RC.8). -public enum FieldAccessEventType -{ - VIEW, EXACT_MATCH_FILTER, DENIED -} diff --git a/src/main/java/com/kingsrook/qbits/secureddata/standin/FieldAccessOutcome.java b/src/main/java/com/kingsrook/qbits/secureddata/standin/FieldAccessOutcome.java deleted file mode 100644 index 0297ab8..0000000 --- a/src/main/java/com/kingsrook/qbits/secureddata/standin/FieldAccessOutcome.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2026. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * 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 - * - * https://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.kingsrook.qbits.secureddata.standin; - - - - -/******************************************************************************* - ** Outcome of an exact-match search. - *******************************************************************************/ -// STAND-IN(core=QRun-IO/qqq#1172, removeBy=RC.8): copy of com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.FieldAccessOutcome; delete when the pin moves to the first QQQ RC with #1172 and #1182 (RC.8). -public enum FieldAccessOutcome -{ - MATCH, NO_MATCH, REFUSED -} diff --git a/src/main/java/com/kingsrook/qbits/secureddata/standin/FieldAccessResolverInterface.java b/src/main/java/com/kingsrook/qbits/secureddata/standin/FieldAccessResolverInterface.java deleted file mode 100644 index 93b2a01..0000000 --- a/src/main/java/com/kingsrook/qbits/secureddata/standin/FieldAccessResolverInterface.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2026. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * 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 - * - * https://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.kingsrook.qbits.secureddata.standin; - - -import java.io.Serializable; -import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; - - -/******************************************************************************* - ** Session-aware policy, reveal grants and value-free access notifications. - *******************************************************************************/ -// STAND-IN(core=QRun-IO/qqq#1172, removeBy=RC.8): copy of com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.FieldAccessResolverInterface; delete when the pin moves to the first QQQ RC with #1172 and #1182 (RC.8). -public interface FieldAccessResolverInterface -{ - /******************************************************************************* - ** Resolve using the current QContext session. - *******************************************************************************/ - FieldAccessDecision resolve(QTableMetaData table, QFieldMetaData field); - - - - /******************************************************************************* - ** Whether this record and field have an active reveal grant. - *******************************************************************************/ - default boolean isRevealActive(String tableName, String fieldName, Serializable primaryKey) - { - return (false); - } - - - - /******************************************************************************* - ** Stable identity of effective access, excluding reveal grants. - *******************************************************************************/ - default String getAccessFingerprint() - { - return (""); - } - - - - /******************************************************************************* - ** Receive an audit notification; failures never prevent enforcement. - *******************************************************************************/ - default void onAccess(FieldAccessEvent event) - { - - } - - - - /******************************************************************************* - ** Refuse an exact-match search before execution; the exception message is user-facing. - *******************************************************************************/ - default void checkExactMatch(FieldAccessEvent event) throws QPermissionDeniedException - { - - } - - - - /******************************************************************************* - ** One-way fingerprint of a search literal. Raw values never enter access events. - *******************************************************************************/ - default String fingerprintSearchValue(Serializable value) - { - return (null); - } -} diff --git a/src/main/java/com/kingsrook/qbits/secureddata/standin/FieldAccessTier.java b/src/main/java/com/kingsrook/qbits/secureddata/standin/FieldAccessTier.java deleted file mode 100644 index 6cd36d8..0000000 --- a/src/main/java/com/kingsrook/qbits/secureddata/standin/FieldAccessTier.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2026. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * 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 - * - * https://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.kingsrook.qbits.secureddata.standin; - - -import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; - - -/******************************************************************************* - ** Effective field access, ordered from least to most restrictive. - *******************************************************************************/ -// STAND-IN(core=QRun-IO/qqq#1172, removeBy=RC.8): copy of com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.FieldAccessTier; delete when the pin moves to the first QQQ RC with #1172 and #1182 (RC.8). -public enum FieldAccessTier -{ - FULL, REVEALABLE, MASKED, HIDDEN; - - - - /******************************************************************************* - ** canReadValue - *******************************************************************************/ - public boolean canReadValue() - { - return (this == FULL); - } - - - - /******************************************************************************* - ** canSort - *******************************************************************************/ - public boolean canSort() - { - return (this == FULL); - } - - - - /******************************************************************************* - ** canGroupOrAggregate - *******************************************************************************/ - public boolean canGroupOrAggregate() - { - return (this == FULL); - } - - - - /******************************************************************************* - ** canQuickSearch - *******************************************************************************/ - public boolean canQuickSearch() - { - return (this == FULL); - } - - - - /******************************************************************************* - ** canWrite - *******************************************************************************/ - public boolean canWrite() - { - return (this == FULL); - } - - - - /******************************************************************************* - ** Whether the operator is available for this tier. - *******************************************************************************/ - public boolean canFilter(QCriteriaOperator operator) - { - return (this == FULL || (this == REVEALABLE && (operator == QCriteriaOperator.EQUALS || operator == QCriteriaOperator.IN))); - } - - - - /******************************************************************************* - ** Whether the field may be described to the caller. - *******************************************************************************/ - public boolean isInMetaData() - { - return (this != HIDDEN); - } - - - - /******************************************************************************* - ** Combine tiers; null means FULL. - *******************************************************************************/ - public FieldAccessTier mostRestrictive(FieldAccessTier other) - { - FieldAccessTier candidate = other == null ? FULL : other; - return (ordinal() > candidate.ordinal() ? this : candidate); - } - - - - /******************************************************************************* - ** Combine tiers; null means FULL. - *******************************************************************************/ - public FieldAccessTier leastRestrictive(FieldAccessTier other) - { - FieldAccessTier candidate = other == null ? FULL : other; - return (ordinal() < candidate.ordinal() ? this : candidate); - } -} diff --git a/src/main/java/com/kingsrook/qbits/secureddata/standin/FieldMaskStyle.java b/src/main/java/com/kingsrook/qbits/secureddata/standin/FieldMaskStyle.java deleted file mode 100644 index f0f3862..0000000 --- a/src/main/java/com/kingsrook/qbits/secureddata/standin/FieldMaskStyle.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2026. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * 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 - * - * https://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.kingsrook.qbits.secureddata.standin; - - - - -/******************************************************************************* - ** Presentation rules for a restricted value. - *******************************************************************************/ -// STAND-IN(core=QRun-IO/qqq#1172, removeBy=RC.8): copy of com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.FieldMaskStyle; delete when the pin moves to the first QQQ RC with #1172 and #1182 (RC.8). -public enum FieldMaskStyle -{ - AUTO, EMAIL, LAST_4, FIRST_LAST, FULL -} diff --git a/src/main/java/com/kingsrook/qbits/secureddata/standin/ProtectedAuditValues.java b/src/main/java/com/kingsrook/qbits/secureddata/standin/ProtectedAuditValues.java deleted file mode 100644 index f795bfd..0000000 --- a/src/main/java/com/kingsrook/qbits/secureddata/standin/ProtectedAuditValues.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * QQQ - Low-code Application Framework for Engineers. - * Copyright (C) 2021-2026. Kingsrook, LLC - * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States - * contact@kingsrook.com - * https://github.com/Kingsrook/ - * - * 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 - * - * https://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.kingsrook.qbits.secureddata.standin; - - -/******************************************************************************* - ** Tokens and a value-free message supplied before an audit is inserted. - *******************************************************************************/ -// STAND-IN(core=QRun-IO/qqq#1182, removeBy=RC.8) -public record ProtectedAuditValues(String oldToken, String newToken, String message) -{ -} diff --git a/src/test/java/com/kingsrook/qbits/secureddata/CoreFieldAccessBridgeIT.java b/src/test/java/com/kingsrook/qbits/secureddata/CoreFieldAccessIT.java similarity index 93% rename from src/test/java/com/kingsrook/qbits/secureddata/CoreFieldAccessBridgeIT.java rename to src/test/java/com/kingsrook/qbits/secureddata/CoreFieldAccessIT.java index ab548fb..7297a1c 100644 --- a/src/test/java/com/kingsrook/qbits/secureddata/CoreFieldAccessBridgeIT.java +++ b/src/test/java/com/kingsrook/qbits/secureddata/CoreFieldAccessIT.java @@ -35,8 +35,6 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assumptions; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static com.kingsrook.qbits.secureddata.SecuredDataTestSupport.channelUser; @@ -55,34 +53,13 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; /******************************************************************************* - ** Core field access bridge it. + ** Core field access integration tests. *******************************************************************************/ -class CoreFieldAccessBridgeIT +class CoreFieldAccessIT { static final String EMAIL = "CANARY-EMAIL-918273@example.invalid"; static final String SSN = "CANARY-PHI-192837465"; - /******************************************************************************* - ** Require core. - *******************************************************************************/ - @BeforeEach - void requireCore() - { - boolean available; - try - { - Class.forName("com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.FieldAccessResolverMetaData"); - available = true; - } - catch(ClassNotFoundException expected) - { - available = false; - } - Assumptions.assumeTrue(available, "QQQ field-access core (#1172) is not on this classpath"); - } - - - /******************************************************************************* ** Cleanup. *******************************************************************************/ diff --git a/src/test/java/com/kingsrook/qbits/secureddata/RetentionProofTest.java b/src/test/java/com/kingsrook/qbits/secureddata/RetentionProofTest.java index 0e473b3..682a1d8 100644 --- a/src/test/java/com/kingsrook/qbits/secureddata/RetentionProofTest.java +++ b/src/test/java/com/kingsrook/qbits/secureddata/RetentionProofTest.java @@ -35,9 +35,9 @@ import java.util.Map; import java.util.Optional; import com.kingsrook.qbits.secureddata.metadata.SecuredDataTables; import com.kingsrook.qbits.secureddata.retention.WipeService; -import com.kingsrook.qbits.secureddata.standin.ClassifiedValueKeySource.KeyAndId; -import com.kingsrook.qbits.secureddata.standin.ClassifiedValueTokens; import com.kingsrook.qbits.secureddata.support.SystemWrites; +import com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.ClassifiedValueKeySource.KeyAndId; +import com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.ClassifiedValueTokens; import com.kingsrook.qqq.backend.core.actions.reporting.ExportAction; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; @@ -56,8 +56,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.JsonUtils; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assumptions; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static com.kingsrook.qbits.secureddata.RetentionTestSupport.rows; import static com.kingsrook.qbits.secureddata.RetentionTestSupport.rule; @@ -67,7 +65,6 @@ import static org.assertj.core.api.Assertions.assertThat; /******************************************************************************* ** SECURED-DATA section 15 proofs: a planted canary never leaves the host table, ** and shredded audit tokens cannot be brute-forced once the data key is gone. - ** Both need the core audit-protection hooks (#1182), so RC.6 skips them. *******************************************************************************/ class RetentionProofTest { @@ -76,17 +73,6 @@ class RetentionProofTest - /******************************************************************************* - ** Skip unless core supplies the audit hooks. - *******************************************************************************/ - @BeforeEach - void requireCoreHooks() - { - Assumptions.assumeTrue(CoreAuditBridge.available(), "QQQ core audit-protection hooks (#1182) are not on this classpath"); - } - - - /******************************************************************************* ** Cleanup. *******************************************************************************/ diff --git a/src/test/java/com/kingsrook/qbits/secureddata/RetentionWipeTest.java b/src/test/java/com/kingsrook/qbits/secureddata/RetentionWipeTest.java index fc5437b..dce0459 100644 --- a/src/test/java/com/kingsrook/qbits/secureddata/RetentionWipeTest.java +++ b/src/test/java/com/kingsrook/qbits/secureddata/RetentionWipeTest.java @@ -20,23 +20,23 @@ package com.kingsrook.qbits.secureddata; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Base64; import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.Base64; import java.util.Set; -import java.nio.charset.StandardCharsets; import com.kingsrook.qbits.secureddata.metadata.SecuredDataTables; import com.kingsrook.qbits.secureddata.retention.RetentionRules; import com.kingsrook.qbits.secureddata.retention.RetentionWidgets; -import com.kingsrook.qbits.secureddata.retention.WipeService; import com.kingsrook.qbits.secureddata.retention.WipeProcesses; -import com.kingsrook.qbits.secureddata.standin.DMLType; -import com.kingsrook.qbits.secureddata.standin.ClassifiedValueTokens; +import com.kingsrook.qbits.secureddata.retention.WipeService; import com.kingsrook.qbits.secureddata.support.SystemWrites; +import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction.DMLType; import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizerInterface; import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; +import com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.ClassifiedValueTokens; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; import com.kingsrook.qqq.backend.core.context.QContext; @@ -50,8 +50,8 @@ import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import static com.kingsrook.qbits.secureddata.RetentionTestSupport.*; diff --git a/src/test/java/com/kingsrook/qbits/secureddata/RevealAndBreakGlassTest.java b/src/test/java/com/kingsrook/qbits/secureddata/RevealAndBreakGlassTest.java index 2da5f8f..86ec3c2 100644 --- a/src/test/java/com/kingsrook/qbits/secureddata/RevealAndBreakGlassTest.java +++ b/src/test/java/com/kingsrook/qbits/secureddata/RevealAndBreakGlassTest.java @@ -31,9 +31,9 @@ import com.kingsrook.qbits.secureddata.metadata.SecuredDataTables; import com.kingsrook.qbits.secureddata.processes.BreakGlassProcesses; import com.kingsrook.qbits.secureddata.reveal.RevealCappedException; import com.kingsrook.qbits.secureddata.reveal.RevealService; -import com.kingsrook.qqq.backend.core.actions.ai.DataClassification; -import com.kingsrook.qbits.secureddata.standin.FieldAccessTier; import com.kingsrook.qbits.secureddata.support.SystemWrites; +import com.kingsrook.qqq.backend.core.actions.ai.DataClassification; +import com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.FieldAccessTier; import com.kingsrook.qqq.backend.core.actions.tables.GetAction; import com.kingsrook.qqq.backend.core.alerts.AlertResolution; import com.kingsrook.qqq.backend.core.alerts.AlertSeverity; diff --git a/src/test/java/com/kingsrook/qbits/secureddata/SecuredDataAccessLoggerTest.java b/src/test/java/com/kingsrook/qbits/secureddata/SecuredDataAccessLoggerTest.java index 96ac9a2..9e5d1a3 100644 --- a/src/test/java/com/kingsrook/qbits/secureddata/SecuredDataAccessLoggerTest.java +++ b/src/test/java/com/kingsrook/qbits/secureddata/SecuredDataAccessLoggerTest.java @@ -33,9 +33,9 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.IntStream; import com.kingsrook.qbits.secureddata.metadata.SecuredDataTables; import com.kingsrook.qqq.backend.core.actions.ai.DataClassification; -import com.kingsrook.qbits.secureddata.standin.FieldAccessEvent; -import com.kingsrook.qbits.secureddata.standin.FieldAccessEventType; -import com.kingsrook.qbits.secureddata.standin.FieldAccessOutcome; +import com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.FieldAccessEvent; +import com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.FieldAccessEventType; +import com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.FieldAccessOutcome; import com.kingsrook.qqq.backend.core.alerts.AlertResolution; import com.kingsrook.qqq.backend.core.alerts.AlertSignal; import com.kingsrook.qqq.backend.core.alerts.AlertSinkInterface; @@ -49,7 +49,6 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.utils.JsonUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; - import static com.kingsrook.qbits.secureddata.SecuredDataTestSupport.CLOCK; import static com.kingsrook.qbits.secureddata.SecuredDataTestSupport.NOW; import static com.kingsrook.qbits.secureddata.SecuredDataTestSupport.channelUser; diff --git a/src/test/java/com/kingsrook/qbits/secureddata/SecuredDataAnomaliesTest.java b/src/test/java/com/kingsrook/qbits/secureddata/SecuredDataAnomaliesTest.java index 7075d90..776ccda 100644 --- a/src/test/java/com/kingsrook/qbits/secureddata/SecuredDataAnomaliesTest.java +++ b/src/test/java/com/kingsrook/qbits/secureddata/SecuredDataAnomaliesTest.java @@ -25,16 +25,16 @@ import java.util.List; import com.kingsrook.qbits.secureddata.breakglass.BreakGlassService; import com.kingsrook.qbits.secureddata.metadata.SecuredDataTables; import com.kingsrook.qbits.secureddata.reveal.RevealService; -import com.kingsrook.qbits.secureddata.standin.FieldAccessEventType; -import com.kingsrook.qbits.secureddata.standin.FieldAccessOutcome; import com.kingsrook.qbits.secureddata.support.ActivityLog; +import com.kingsrook.qbits.secureddata.support.SystemWrites; +import com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.FieldAccessEventType; +import com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.FieldAccessOutcome; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.alerts.AlertsInstanceMetaData; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; -import com.kingsrook.qbits.secureddata.support.SystemWrites; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import static com.kingsrook.qbits.secureddata.SecuredDataTestSupport.*; diff --git a/src/test/java/com/kingsrook/qbits/secureddata/SecuredDataDdlTest.java b/src/test/java/com/kingsrook/qbits/secureddata/SecuredDataDdlTest.java index e2bd08a..1218a31 100644 --- a/src/test/java/com/kingsrook/qbits/secureddata/SecuredDataDdlTest.java +++ b/src/test/java/com/kingsrook/qbits/secureddata/SecuredDataDdlTest.java @@ -33,7 +33,6 @@ import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSBackendMetaDat import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSTableBackendDetails; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; - import static com.kingsrook.qbits.secureddata.SecuredDataTestSupport.CLOCK; import static com.kingsrook.qbits.secureddata.SecuredDataTestSupport.RUNTIMES; import static com.kingsrook.qbits.secureddata.SecuredDataTestSupport.base; @@ -97,8 +96,8 @@ class SecuredDataDdlTest classification(null, "PII"); policy("cs_admin", "PII", "REVEALABLE"); assertThat(SecuredDataRuntime.current().resolver().resolve(instance.getTable("person"), instance.getTable("person").getField("email")).tier().name()).isEqualTo("REVEALABLE"); - SecuredDataRuntime.current().logger().onAccess(SecuredDataAccessLoggerTest.event(com.kingsrook.qbits.secureddata.standin.FieldAccessEventType.EXACT_MATCH_FILTER, - com.kingsrook.qbits.secureddata.standin.FieldAccessOutcome.MATCH, "hmac")); + SecuredDataRuntime.current().logger().onAccess(SecuredDataAccessLoggerTest.event(com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.FieldAccessEventType.EXACT_MATCH_FILTER, + com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.FieldAccessOutcome.MATCH, "hmac")); SecuredDataRuntime.current().logger().flush(); assertThat(logs()).extracting(row -> row.getValueString("event_type")).containsExactly("CLASSIFICATION_CHANGE", "POLICY_CHANGE", "EXACT_MATCH_FILTER"); // Equal timestamps must be counted by SQL as well as the memory backend. diff --git a/src/test/java/com/kingsrook/qbits/secureddata/SecuredDataHomeTest.java b/src/test/java/com/kingsrook/qbits/secureddata/SecuredDataHomeTest.java index c43793d..acbec35 100644 --- a/src/test/java/com/kingsrook/qbits/secureddata/SecuredDataHomeTest.java +++ b/src/test/java/com/kingsrook/qbits/secureddata/SecuredDataHomeTest.java @@ -88,8 +88,8 @@ class SecuredDataHomeTest delete(SecuredDataTables.POLICY, policy); classification("email", "PII"); var logger = SecuredDataRuntime.current().logger(); - logger.onAccess(SecuredDataAccessLoggerTest.event(com.kingsrook.qbits.secureddata.standin.FieldAccessEventType.VIEW, null, null)); - logger.onAccess(SecuredDataAccessLoggerTest.event(com.kingsrook.qbits.secureddata.standin.FieldAccessEventType.VIEW, null, null)); + logger.onAccess(SecuredDataAccessLoggerTest.event(com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.FieldAccessEventType.VIEW, null, null)); + logger.onAccess(SecuredDataAccessLoggerTest.event(com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.FieldAccessEventType.VIEW, null, null)); var data = SecuredDataHomeData.load(); assertThat(data.getKpis()).containsEntry("Reveals today", 15L).containsEntry("Active reveal grants", 1L) .containsEntry("Active break-glass sessions", 1L).containsEntry("Anomaly flags (7 days)", 1L) diff --git a/src/test/java/com/kingsrook/qbits/secureddata/SecuredDataQBitProducerTest.java b/src/test/java/com/kingsrook/qbits/secureddata/SecuredDataQBitProducerTest.java index c3f3eac..72c9deb 100644 --- a/src/test/java/com/kingsrook/qbits/secureddata/SecuredDataQBitProducerTest.java +++ b/src/test/java/com/kingsrook/qbits/secureddata/SecuredDataQBitProducerTest.java @@ -26,11 +26,9 @@ import java.util.Map; import com.kingsrook.qbits.secureddata.metadata.FieldsOfTablePossibleValueSource; import com.kingsrook.qbits.secureddata.metadata.SecuredDataEnums; import com.kingsrook.qbits.secureddata.metadata.SecuredDataTables; -import com.kingsrook.qqq.backend.core.model.metadata.permissions.ClientChannelsMetaData; -import com.kingsrook.qqq.backend.core.actions.ai.DataClassification; -import com.kingsrook.qbits.secureddata.standin.FieldAccessTier; -import com.kingsrook.qqq.backend.core.model.metadata.permissions.TableFeaturePermissionsMetaData; import com.kingsrook.qbits.secureddata.support.SecuredDataMetadata; +import com.kingsrook.qqq.backend.core.actions.ai.DataClassification; +import com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.FieldAccessTier; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.context.QContext; @@ -40,11 +38,12 @@ import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSo import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.ClientChannelsMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.TableFeaturePermissionsMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; - import static com.kingsrook.qbits.secureddata.SecuredDataTestSupport.NOW; import static com.kingsrook.qbits.secureddata.SecuredDataTestSupport.RUNTIMES; import static com.kingsrook.qbits.secureddata.SecuredDataTestSupport.base; diff --git a/src/test/java/com/kingsrook/qbits/secureddata/SecuredDataResolverTest.java b/src/test/java/com/kingsrook/qbits/secureddata/SecuredDataResolverTest.java index c9910c5..be767dc 100644 --- a/src/test/java/com/kingsrook/qbits/secureddata/SecuredDataResolverTest.java +++ b/src/test/java/com/kingsrook/qbits/secureddata/SecuredDataResolverTest.java @@ -23,12 +23,12 @@ package com.kingsrook.qbits.secureddata; import java.util.List; import com.kingsrook.qbits.secureddata.metadata.SecuredDataTables; -import com.kingsrook.qqq.backend.core.actions.ai.AiTableMetaData; -import com.kingsrook.qqq.backend.core.actions.ai.DataClassification; -import com.kingsrook.qbits.secureddata.standin.FieldAccessTier; -import com.kingsrook.qbits.secureddata.standin.FieldMaskStyle; import com.kingsrook.qbits.secureddata.support.SecuredDataMetadata; import com.kingsrook.qbits.secureddata.support.SystemWrites; +import com.kingsrook.qqq.backend.core.actions.ai.AiTableMetaData; +import com.kingsrook.qqq.backend.core.actions.ai.DataClassification; +import com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.FieldAccessTier; +import com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.FieldMaskStyle; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; @@ -39,7 +39,6 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; - import static com.kingsrook.qbits.secureddata.SecuredDataTestSupport.CLOCK; import static com.kingsrook.qbits.secureddata.SecuredDataTestSupport.NOW; import static com.kingsrook.qbits.secureddata.SecuredDataTestSupport.channelUser; -- 2.54.0 From 527ee4fc96e0404ebafa6a8cec53405de7df9775 Mon Sep 17 00:00:00 2001 From: James Maes Date: Wed, 14 Oct 2026 16:25:17 -0500 Subject: [PATCH 2/3] build: pin QQQ 4.1.0-RC.7 RC.7 ships field access (#1172) and audit value protection (#1182), so the qbit builds on the released core with no stand-ins. Also repairs a non-UTF-8 character in the changelog. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 6 +++--- README.md | 2 +- pom.xml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d3f98d..19ded8e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,11 @@ All notable changes are documented here, following [Keep a Changelog](https://ke ## [Unreleased] -### Changed — direct core field access and audit protection +### Changed — direct core field access and audit protection - Remove all #1172/#1182 stand-ins and reflective bridges; use core field-access, classified-value token, audit protection, wipe flag and shredded-event APIs directly. - Register the resolver, key source and audit protector through core supplemental metadata. - Run core field-access integration and retention proof tests unconditionally. -- Temporarily pin QQQ to `4.1.0-SECUREDATA-SNAPSHOT`, pending the first RC with #1172 and #1182 (RC.7 expected). +- Pin QQQ to `4.1.0-RC.7`, which ships field access (#1172) and audit value protection (#1182); all core stand-ins and reflective bridges are removed. ### Changed — RC.6 migration - Pin QQQ to 4.1.0-RC.6; use released classification, client-channel and table-feature APIs directly. @@ -21,7 +21,7 @@ All notable changes are documented here, following [Keep a Changelog](https://ke - Atomic wipe batches (fields, markers, key deletions, audits, access log and run row in one rollback-capable transaction), per-record and per-table legal holds, a Review/Result "Run wipe now" process and a daily scheduled run. - Per-record-field AES-GCM wrapped DEKs (AAD-bound) for crypto-shredding of audit tokens, `CHANGED_ONLY` audit treatment, and the audit-value protector. - App-home warning when known backup retention exceeds a rule's `age_days`; `withBackupRetentionDays` and `withArchiveSink` options; README retention section and the backups residual risk. -- Tests: all actions, holds, rollback, Review/Result, schedule, permissions, wrap/unwrap, and (on the SECUREDATA snapshot) the section 15 canary and wipe-proof tests. +- Tests: all actions, holds, rollback, Review/Result, schedule, permissions, wrap/unwrap, and the section 15 canary and wipe-proof tests. ### Fixed - The key source returned a DEK array it then zeroed; it now returns copies, with a regression test. diff --git a/README.md b/README.md index 1891a73..b56f299 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Phases (b), (c), (e) and (f) of [SECURED-DATA](https://github.com/QRun-IO/qqq/bl ## Compatibility -Java 21, Maven wrapper 3.9.11, and the first **QQQ RC that includes #1172 and #1182 (RC.7 expected)**. Until that RC is available, the build uses the locally installed `4.1.0-SECUREDATA-SNAPSHOT`. +Java 21, Maven wrapper 3.9.11, and **QQQ 4.1.0-RC.7** or later (it ships field access #1172 and audit value protection #1182). Core classifications, AI metadata, client channels, table-feature permissions and UI API extension points are used directly. The UI API and its contract test are part of the main build; the contract test calls `UiApiContract` directly. diff --git a/pom.xml b/pom.xml index 3b2c6d1..5fe44d1 100644 --- a/pom.xml +++ b/pom.xml @@ -22,8 +22,8 @@ - - 4.1.0-SECUREDATA-SNAPSHOT + + 4.1.0-RC.7 1.0.0-SNAPSHOT 21 21 -- 2.54.0 From 677d611b15ac20f4bfdd942976fd2783c02d7644 Mon Sep 17 00:00:00 2001 From: James Maes Date: Wed, 14 Oct 2026 16:27:45 -0500 Subject: [PATCH 3/3] build: start on the 0.1 version line, like the other new qbits The UI API extension keeps its own semver (secured-data 1.0.0). Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- pom.xml | 2 +- .../kingsrook/qbits/secureddata/SecuredDataQBitProducer.java | 2 +- .../qbits/secureddata/SecuredDataQBitProducerTest.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b56f299..88550a5 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ Field access (#1172), classified-value tokens and audit-value protection (#1182) com.kingsrook.qbits qbit-secured-data - 1.0.0-SNAPSHOT + 0.1.0-RC.1 ``` diff --git a/pom.xml b/pom.xml index 5fe44d1..b6b548a 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ 4.1.0-RC.7 - 1.0.0-SNAPSHOT + 0.1.0-SNAPSHOT 21 21 0.8.15 diff --git a/src/main/java/com/kingsrook/qbits/secureddata/SecuredDataQBitProducer.java b/src/main/java/com/kingsrook/qbits/secureddata/SecuredDataQBitProducer.java index c16b4bb..4d228d0 100644 --- a/src/main/java/com/kingsrook/qbits/secureddata/SecuredDataQBitProducer.java +++ b/src/main/java/com/kingsrook/qbits/secureddata/SecuredDataQBitProducer.java @@ -49,7 +49,7 @@ public class SecuredDataQBitProducer implements QBitMetaDataProducer