build: QQQ 4.1.0-RC.7; remove core stand-ins #15

Merged
KofTwentyTwo merged 4 commits from feature/remove-standins into develop 2026-10-14 21:33:34 +00:00
36 changed files with 81 additions and 1536 deletions

No files matched your search

+7 -1
View File
@@ -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.
- 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.
- Fold UI API code and tests into the main build and remove the profile and reflective registration.
@@ -15,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.
+7 -8
View File
@@ -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 **QQQ 4.1.0-RC.7** or later (it ships field access #1172 and audit value protection #1182).
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
@@ -16,7 +16,7 @@ Field access (#1172) and audit-value protection (#1182) are not in RC.6. Field-a
<dependency>
<groupId>com.kingsrook.qbits</groupId>
<artifactId>qbit-secured-data</artifactId>
<version>1.0.0-SNAPSHOT</version>
<version>0.1.0-RC.1</version>
</dependency>
```
@@ -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
+3 -3
View File
@@ -22,9 +22,9 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<properties>
<!-- Released APIs; fieldaccess and audit protection remain RC.7 stand-ins. -->
<qqq.snapshot.version>4.1.0-RC.6</qqq.snapshot.version>
<revision>1.0.0-SNAPSHOT</revision>
<!-- QQQ 4.1.0-RC.7 ships field access (#1172) and audit value protection (#1182) -->
<qqq.snapshot.version>4.1.0-RC.7</qqq.snapshot.version>
<revision>0.1.0-SNAPSHOT</revision>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<jacoco.version>0.8.15</jacoco.version>
@@ -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<ClassifiedValueKeySource.KeyAndId> 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);
}
}
@@ -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<String>) type.getMethod("getFieldNames").invoke(source))
.withPrimaryKeys((List<Serializable>) 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<String>) 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);
}
}
@@ -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.
*******************************************************************************/
@@ -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));
}
}
@@ -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
{
@@ -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.
*******************************************************************************/
@@ -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.
*******************************************************************************/
@@ -47,7 +49,7 @@ public class SecuredDataQBitProducer implements QBitMetaDataProducer<SecuredData
{
public static final String GROUP_ID = "com.kingsrook.qbits";
public static final String ARTIFACT_ID = "qbit-secured-data";
public static final String VERSION = "1.0.0";
public static final String VERSION = "0.1.0";
private SecuredDataQBitConfig qBitConfig;
private String namespace;
@@ -171,8 +173,9 @@ public class SecuredDataQBitProducer implements QBitMetaDataProducer<SecuredData
runtime.addSelfToInstance(qInstance);
try
{
CoreFieldAccessBridge.register(qInstance, runtime.resolver());
CoreAuditBridge.register(qInstance, runtime);
FieldAccessResolverMetaData.ofOrWithNew(qInstance).withResolverInstance(runtime.resolver());
ClassifiedValueKeySourceMetaData.ofOrWithNew(qInstance).withKeySourceInstance(runtime.keys());
AuditValueProtectionMetaData.ofOrWithNew(qInstance).withProtectorInstance(new SecuredDataAuditProtector());
}
catch(RuntimeException e)
{
@@ -36,14 +36,14 @@ import java.util.Objects;
import java.util.TreeSet;
import com.kingsrook.qbits.secureddata.metadata.SecuredDataTables;
import com.kingsrook.qbits.secureddata.permissions.SecuredDataPermissions;
import com.kingsrook.qbits.secureddata.standin.FieldAccessDecision;
import com.kingsrook.qbits.secureddata.standin.FieldAccessEvent;
import com.kingsrook.qbits.secureddata.standin.FieldAccessResolverInterface;
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.DataClassification;
import com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.FieldAccessDecision;
import com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.FieldAccessEvent;
import com.kingsrook.qqq.backend.core.actions.permissions.fieldaccess.FieldAccessResolverInterface;
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.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
@@ -57,7 +57,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;
/*******************************************************************************
** Seven-step policy resolution with instance-versioned, bounded per-user state.
*******************************************************************************/
@@ -23,15 +23,14 @@ package com.kingsrook.qbits.secureddata.metadata;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qbits.secureddata.standin.FieldAccessTier;
import com.kingsrook.qbits.secureddata.standin.FieldMaskStyle;
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.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
/*******************************************************************************
** String-valued possible-value sources shared by UI metadata and the DDL.
*******************************************************************************/
@@ -35,19 +35,20 @@ import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import com.kingsrook.qbits.secureddata.CoreAuditBridge;
import com.kingsrook.qbits.secureddata.SecuredDataKeySource;
import com.kingsrook.qbits.secureddata.SecuredDataRuntime;
import com.kingsrook.qbits.secureddata.metadata.SecuredDataTables;
import com.kingsrook.qbits.secureddata.support.ActivityLog;
import com.kingsrook.qbits.secureddata.support.SystemWrites;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.actions.audits.AuditShreddedEvent;
import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction;
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;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.model.actions.tables.WipeAuditFlag;
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
@@ -292,7 +293,7 @@ public final class WipeService
{
throw (new QException("Audit key deletion failed."));
}
CoreAuditBridge.detail(table, pk, field, rule.getValueString("id"), true, transaction);
AuditShreddedEvent.shreddedDetail(table, pk, table.getField(field), rule.getValueString("id"), transaction);
}
markers.add(new QRecord().withValue("table_name", table.getName()).withValue("record_id", String.valueOf(pk)).withValue("field_name", field)
.withValue("rule_id", rule.getValue("id")).withValue("action", action).withValue("wiped_at", started));
@@ -385,15 +386,9 @@ public final class WipeService
patch.setValue(field, value);
}
UpdateInput input = new UpdateInput(table.getName()).withRecord(patch).withTransaction(transaction);
CoreAuditBridge.flag(input, rule.getValueString("id"));
input.setFlags(Set.of(new WipeAuditFlag(rule.getValueString("id"))));
check(new UpdateAction().execute(input).getRecords());
if(!CoreAuditBridge.available())
{
for(String field : fields)
{
CoreAuditBridge.detail(table, pk, field, rule.getValueString("id"), false, transaction);
}
}
}
/*******************************************************************************
@@ -30,13 +30,13 @@ import java.util.Objects;
import com.kingsrook.qbits.secureddata.SecuredDataResolver;
import com.kingsrook.qbits.secureddata.SecuredDataRuntime;
import com.kingsrook.qbits.secureddata.metadata.SecuredDataTables;
import com.kingsrook.qbits.secureddata.standin.FieldAccessTier;
import com.kingsrook.qbits.secureddata.support.ActivityLog;
import com.kingsrook.qbits.secureddata.support.SecuredDataMetadata;
import com.kingsrook.qbits.secureddata.support.SystemWrites;
import com.kingsrook.qqq.backend.core.actions.permissions.CorePermissionEnforcer;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionEnforcement;
import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType;
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.AlertSignal;
import com.kingsrook.qqq.backend.core.alerts.QAlerts;
@@ -1,50 +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.qbits.secureddata.standin.DMLType;
import com.kingsrook.qqq.backend.core.actions.ai.AiDataClassifications;
import com.kingsrook.qqq.backend.core.actions.ai.DataClassification;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
/*******************************************************************************
** Application hook for audit tokens and effective classification. Never return plain values.
*******************************************************************************/
// STAND-IN(core=QRun-IO/qqq#1182, removeBy=RC.8)
public interface AuditValueProtectorInterface
{
/*******************************************************************************
** Protect the values in the current action's transaction before audit insertion.
*******************************************************************************/
ProtectedAuditValues protect(QTableMetaData table, QFieldMetaData field, Serializable primaryKey, Serializable oldValue, Serializable newValue, DMLType dmlType);
/*******************************************************************************
** Override to use an application-owned classification source.
*******************************************************************************/
default DataClassification classificationOf(QTableMetaData table, QFieldMetaData field)
{
return (AiDataClassifications.ofField(table, field.getName()));
}
}
@@ -1,70 +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.Optional;
/*******************************************************************************
** Supplies instance keys and optional per-record, per-field shreddable keys.
*******************************************************************************/
// STAND-IN(core=QRun-IO/qqq#1182, removeBy=RC.8)
public interface ClassifiedValueKeySource
{
/*******************************************************************************
** The active instance key, if configured.
*******************************************************************************/
Optional<KeyAndId> instanceKey();
/*******************************************************************************
** Verification uses create=false and must never recreate a destroyed key.
*******************************************************************************/
Optional<KeyAndId> 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());
}
}
}
@@ -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<KeyAndId> 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<String> 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<String> 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<String> 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<String> 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<String> 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<KeyAndId> key, String kind, List<String> 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<String> 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());
}
}
@@ -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
}
@@ -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);
}
}
@@ -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<String> fieldNames;
private List<Serializable> primaryKeys;
private Integer recordCount;
private String actionName;
private FieldAccessOutcome outcome;
private List<String> 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<String> getFieldNames()
{
return (this.fieldNames);
}
/*******************************************************************************
** Setter for fieldNames
*******************************************************************************/
public void setFieldNames(List<String> fieldNames)
{
this.fieldNames = fieldNames == null ? null : new ArrayList<>(fieldNames);
}
/*******************************************************************************
** Fluent setter for fieldNames
*******************************************************************************/
public FieldAccessEvent withFieldNames(List<String> fieldNames)
{
setFieldNames(fieldNames);
return (this);
}
/*******************************************************************************
** Getter for primaryKeys
*******************************************************************************/
public List<Serializable> getPrimaryKeys()
{
return (this.primaryKeys == null ? null : Collections.unmodifiableList(this.primaryKeys));
}
/*******************************************************************************
** Setter for primaryKeys
*******************************************************************************/
public void setPrimaryKeys(List<Serializable> primaryKeys)
{
this.primaryKeys = primaryKeys == null ? null : new ArrayList<>(primaryKeys.subList(0, Math.min(50, primaryKeys.size())));
}
/*******************************************************************************
** Fluent setter for primaryKeys
*******************************************************************************/
public FieldAccessEvent withPrimaryKeys(List<Serializable> 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<String> getSearchValueFingerprints()
{
return (searchValueFingerprints);
}
/*******************************************************************************
** Fluent setter for searchValueFingerprints
*******************************************************************************/
public FieldAccessEvent withSearchValueFingerprints(List<String> searchValueFingerprints)
{
this.searchValueFingerprints = searchValueFingerprints == null ? null : List.copyOf(searchValueFingerprints);
return (this);
}
}
@@ -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
}
@@ -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
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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
}
@@ -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)
{
}
@@ -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.
*******************************************************************************/
@@ -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.
*******************************************************************************/
@@ -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.*;
@@ -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;
@@ -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;
@@ -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.*;
@@ -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.
@@ -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)
@@ -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;
@@ -89,7 +88,7 @@ class SecuredDataQBitProducerTest
{
QInstance instance = setup();
assertThat(instance.getTables().keySet()).containsAll(SecuredDataTables.ALL);
assertThat(instance.getQBits().get("com.kingsrook.qbits:qbit-secured-data").getVersion()).isEqualTo("1.0.0");
assertThat(instance.getQBits().get("com.kingsrook.qbits:qbit-secured-data").getVersion()).isEqualTo("0.1.0");
for(String name : SecuredDataTables.ALL)
{
QTableMetaData table = instance.getTable(name);
@@ -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;