diff --git a/docs/metaData/SecurtiyKeyTypes.adoc b/docs/metaData/SecurtiyKeyTypes.adoc
index acd6a782..23c4d8f3 100644
--- a/docs/metaData/SecurtiyKeyTypes.adoc
+++ b/docs/metaData/SecurtiyKeyTypes.adoc
@@ -2,16 +2,79 @@
== Security Key Types
include::../variables.adoc[]
-#TODO#
+In QQQ, record-level security is provided by using a lock & key metaphor.
+
+The use-case being handled here is:
+
+* A user has full permission on a table (query, insert, update, and delete).
+* However, they should only be allowed to read a sub-set of the rows in the table.
+** e.g., maybe it's a multi-tenant system, or the table has user-specific records.
+
+The lock & key metaphor is realized by the user being associated with one or more "Keys"
+(as values in their session), and records in tables being associated with one or more "Locks"
+(as values in fields).
+A user is only allowed to access records where the user's key(s) match the record's security lock(s).
+
+For a practical example, picture a multi-tenant Order Management System,where all orders are assigned to a "client".
+Users (customers) should only be able to see orders associated with the client which that user works for.
+
+In this scenario, the `order` table would have a "lock" on its `clientId` field.
+Customer-users would have a `clientId` key in their session.
+When the QQQ backend did a search for records (e.g., an SQL query) it would implicitly
+(without any code being written by the application developer) filter the table to only
+allow the user to see records with their `clientId`.
+
+To implement this scenario, the application would define the following pieces of meta-data:
+
+* At the QQQ-Instance level, a `SecurityKeyType`,
+to define a domain of possible locks & keys within an application.
+** An application can define multiple Security Key Types.
+For example, maybe `clientId` and `userId` as key types.
+* At the per-table level, a `RecordSecurityLock`,
+which references a security key type, and how that key type should be applied to the table.
+** For example, what field stores the `clientId` value in the `order` table.
+* Finally, when a user's session is constructed via a QQQ Authentication provider,
+security key values are set, based on data from the authentication backend.
+
+=== Additional Scenarios
+
+==== All Access Key
+A "super-user" may be allowed to access all records in a table regardless of their record locks,
+if the Security Key Type specifies an `allAccessKeyName`,
+and if the user has a key in their session with that key name, and a value of `true`.
+Going back to the lock & key metaphor, this can be thought of as a "skeleton key",
+that can unlock any record lock (of the security key's type).
+
+==== Null Value Behaviors
+In a record security lock, different behaviors can be defined for handling rows with a null key value.
+
+For example:
+
+* Sometimes orders may be loaded into the OMS system described above, where the application doesn't yet know what client the order belongs to.
+In this case, the application may need to ensure that such records, with a `null` value in `clientId` are hidden from customer-users,
+to avoid potentially leaking a different client's data.
+** This can be accomplished with a record security lock on the `order` table, with a `nullValueBehavior` of `DENY`.
+** Furthermore, if internal/admin users _should_ be given access to such records, then the security key type can be
+configured with a `nullValueBehaviorKeyName` (e.g., `"clientIdNullValueBehavior"`), which can be set per-user to allow
+access to records, overriding the table lock's specified `nullValueBehavior`.
+*** This could also be done by giving internal/admin users an `allAccessKey`, but sometimes that is not what is required.
+
+* Maybe a warehouse locations table is assigned a `clientId` once inventory for a client is placed in the location,
+at which point in time, only the client's users should be allowed to see the record.
+But, if no client has been assigned to the location, and `clientId` is `null`,
+then you may want to allow any user to see such records.
+** This can be accomplished with a record security lock on the Warehouse Locations table, with a `nullValueBehavior` of `ALLOW`.
=== QSecurityKeyType
A Security Key Type is defined in a QQQ Instance in a `*QSecurityKeyType*` object.
-#TODO#
-
*QSecurityKeyType Properties:*
-* `name` - *String, Required* - Unique name for the security key type within the QQQ Instance.
+* `name` - *String, Required* - Unique name for this security key type within the QQQ Instance.
+* `allAccessKeyName` - *String* - Optional name of the all-access security key associated with this key type.
+* `nullValueBehaviorKeyName` - *String* - Optional name of the null-value-behavior overriding security key associated with this key type.
+** Note, `name`, `allAccessKeyName`, and `nullValueBehaviorKeyName` are all checked against each other for uniqueness.
+A `QInstanceValidationException` will be thrown if any name collisions occur.
+* `possibleValueSourceName` - *String* - Optional reference to a possible value source from which value for the key can come.
-#TODO#
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/ValidateRecordSecurityLockHelper.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/ValidateRecordSecurityLockHelper.java
index e94c9b5a..1f235999 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/ValidateRecordSecurityLockHelper.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/ValidateRecordSecurityLockHelper.java
@@ -42,6 +42,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.security.NullValueBehaviorUtil;
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
@@ -230,7 +231,7 @@ public class ValidateRecordSecurityLockHelper
{
for(QRecord inputRecord : inputRecords)
{
- if(RecordSecurityLock.NullValueBehavior.DENY.equals(recordSecurityLock.getNullValueBehavior()))
+ if(RecordSecurityLock.NullValueBehavior.DENY.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
{
inputRecord.addError(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " this record - the referenced " + leftMostJoinTable.getLabel() + " was not found."));
}
@@ -298,7 +299,7 @@ public class ValidateRecordSecurityLockHelper
/////////////////////////////////////////////////////////////////
// handle null values - error if the NullValueBehavior is DENY //
/////////////////////////////////////////////////////////////////
- if(RecordSecurityLock.NullValueBehavior.DENY.equals(recordSecurityLock.getNullValueBehavior()))
+ if(RecordSecurityLock.NullValueBehavior.DENY.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
{
String lockLabel = CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()) ? recordSecurityLock.getSecurityKeyType() : table.getField(recordSecurityLock.getFieldName()).getLabel();
record.addError(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " a record without a value in the field: " + lockLabel));
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java
index 2e7c5797..66529619 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java
@@ -50,6 +50,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType;
import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType;
@@ -85,6 +86,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.Automatio
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails;
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheOf;
import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheUseCase;
+import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleCustomizerInterface;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
@@ -208,14 +210,23 @@ public class QInstanceValidator
if(assertCondition(StringUtils.hasContent(securityKeyType.getName()), "Missing name for a securityKeyType"))
{
assertCondition(Objects.equals(name, securityKeyType.getName()), "Inconsistent naming for securityKeyType: " + name + "/" + securityKeyType.getName() + ".");
- assertCondition(!usedNames.contains(name), "More than one SecurityKeyType with name (or allAccessKeyName) of: " + name);
+
+ String duplicateNameMessagePrefix = "More than one SecurityKeyType with name (or allAccessKeyName or nullValueBehaviorKeyName) of: ";
+ assertCondition(!usedNames.contains(name), duplicateNameMessagePrefix + name);
usedNames.add(name);
+
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()))
{
- assertCondition(!usedNames.contains(securityKeyType.getAllAccessKeyName()), "More than one SecurityKeyType with name (or allAccessKeyName) of: " + securityKeyType.getAllAccessKeyName());
+ assertCondition(!usedNames.contains(securityKeyType.getAllAccessKeyName()), duplicateNameMessagePrefix + securityKeyType.getAllAccessKeyName());
usedNames.add(securityKeyType.getAllAccessKeyName());
}
+ if(StringUtils.hasContent(securityKeyType.getNullValueBehaviorKeyName()))
+ {
+ assertCondition(!usedNames.contains(securityKeyType.getNullValueBehaviorKeyName()), duplicateNameMessagePrefix + securityKeyType.getNullValueBehaviorKeyName());
+ usedNames.add(securityKeyType.getNullValueBehaviorKeyName());
+ }
+
if(StringUtils.hasContent(securityKeyType.getPossibleValueSourceName()))
{
assertCondition(qInstance.getPossibleValueSource(securityKeyType.getPossibleValueSourceName()) != null, "Unrecognized possibleValueSourceName in securityKeyType: " + name);
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java
index ad446912..115fcccc 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java
@@ -1052,10 +1052,16 @@ public class QInstance
for(QSecurityKeyType securityKeyType : CollectionUtils.nonNullMap(getSecurityKeyTypes()).values())
{
rs.add(securityKeyType.getName());
+
if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()))
{
rs.add(securityKeyType.getAllAccessKeyName());
}
+
+ if(StringUtils.hasContent(securityKeyType.getNullValueBehaviorKeyName()))
+ {
+ rs.add(securityKeyType.getNullValueBehaviorKeyName());
+ }
}
return (rs);
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/NullValueBehaviorUtil.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/NullValueBehaviorUtil.java
new file mode 100644
index 00000000..3e99299f
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/NullValueBehaviorUtil.java
@@ -0,0 +1,75 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.metadata.security;
+
+
+import java.io.Serializable;
+import java.util.List;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.logging.QLogger;
+import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock.NullValueBehavior;
+import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
+import com.kingsrook.qqq.backend.core.utils.StringUtils;
+import com.kingsrook.qqq.backend.core.utils.ValueUtils;
+import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
+
+
+/*******************************************************************************
+ ** Utility for working with security key, nullValueBehaviors.
+ *******************************************************************************/
+public class NullValueBehaviorUtil
+{
+ private static final QLogger LOG = QLogger.getLogger(NullValueBehaviorUtil.class);
+
+
+
+ /*******************************************************************************
+ ** Look at a RecordSecurityLock, but also the active session - and if the session
+ ** has a null-value-behavior key for the lock's key-type, then allow that behavior
+ ** to override the lock's default.
+ *******************************************************************************/
+ public static NullValueBehavior getEffectiveNullValueBehavior(RecordSecurityLock recordSecurityLock)
+ {
+ QSecurityKeyType securityKeyType = QContext.getQInstance().getSecurityKeyType(recordSecurityLock.getSecurityKeyType());
+ if(StringUtils.hasContent(securityKeyType.getNullValueBehaviorKeyName()))
+ {
+ List nullValueSessionValueList = QContext.getQSession().getSecurityKeyValues(securityKeyType.getNullValueBehaviorKeyName());
+ if(CollectionUtils.nullSafeHasContents(nullValueSessionValueList))
+ {
+ NullValueBehavior nullValueBehavior = NullValueBehavior.tryToGetFromString(ValueUtils.getValueAsString(nullValueSessionValueList.get(0)));
+ if(nullValueBehavior != null)
+ {
+ return nullValueBehavior;
+ }
+ else
+ {
+ LOG.info("Unexpected value in nullValueBehavior security key. Will use recordSecurityLock's nullValueBehavior",
+ logPair("nullValueBehaviorKeyName", securityKeyType.getNullValueBehaviorKeyName()),
+ logPair("value", nullValueSessionValueList.get(0)));
+ }
+ }
+ }
+
+ return (recordSecurityLock.getNullValueBehavior());
+ }
+
+}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/QSecurityKeyType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/QSecurityKeyType.java
index 74290b75..9e10ee4d 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/QSecurityKeyType.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/QSecurityKeyType.java
@@ -34,6 +34,7 @@ public class QSecurityKeyType implements TopLevelMetaDataInterface
{
private String name;
private String allAccessKeyName;
+ private String nullValueBehaviorKeyName;
private String possibleValueSourceName;
@@ -149,4 +150,35 @@ public class QSecurityKeyType implements TopLevelMetaDataInterface
qInstance.addSecurityKeyType(this);
}
+
+ /*******************************************************************************
+ ** Getter for nullValueBehaviorKeyName
+ *******************************************************************************/
+ public String getNullValueBehaviorKeyName()
+ {
+ return (this.nullValueBehaviorKeyName);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for nullValueBehaviorKeyName
+ *******************************************************************************/
+ public void setNullValueBehaviorKeyName(String nullValueBehaviorKeyName)
+ {
+ this.nullValueBehaviorKeyName = nullValueBehaviorKeyName;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for nullValueBehaviorKeyName
+ *******************************************************************************/
+ public QSecurityKeyType withNullValueBehaviorKeyName(String nullValueBehaviorKeyName)
+ {
+ this.nullValueBehaviorKeyName = nullValueBehaviorKeyName;
+ return (this);
+ }
+
+
}
diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/RecordSecurityLock.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/RecordSecurityLock.java
index 06633998..b16deac0 100644
--- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/RecordSecurityLock.java
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/RecordSecurityLock.java
@@ -22,7 +22,9 @@
package com.kingsrook.qqq.backend.core.model.metadata.security;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
/*******************************************************************************
@@ -67,7 +69,34 @@ public class RecordSecurityLock
{
ALLOW,
ALLOW_WRITE_ONLY, // not common - but see Audit, where you can do a thing that inserts them into a generic table, even though you can't later read them yourself...
- DENY
+ DENY;
+
+
+ ////////////////////////////////////////////////////////////////////
+ // for use in tryToGetFromString, where we'll lowercase the input //
+ ////////////////////////////////////////////////////////////////////
+ private static final Map stringMapping = new HashMap<>();
+
+ static
+ {
+ stringMapping.put("allow", ALLOW);
+ stringMapping.put("allow_write_only", ALLOW_WRITE_ONLY);
+ stringMapping.put("allowwriteonly", ALLOW_WRITE_ONLY);
+ stringMapping.put("deny", DENY);
+ }
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public static NullValueBehavior tryToGetFromString(String string)
+ {
+ if(string == null)
+ {
+ return (null);
+ }
+
+ return stringMapping.get(string.toLowerCase());
+ }
}
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java
index 55b94bb0..a258a177 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java
@@ -75,6 +75,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier;
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction;
+import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleCustomizerInterface;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractTransformStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep;
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaDeleteStep;
@@ -1622,19 +1623,30 @@ class QInstanceValidatorTest extends BaseTest
assertValidationFailureReasons((qInstance ->
{
qInstance.addSecurityKeyType(new QSecurityKeyType().withName("clientId").withAllAccessKeyName("clientId"));
- }), "More than one SecurityKeyType with name (or allAccessKeyName) of: clientId");
+ }), "More than one SecurityKeyType with name (or allAccessKeyName or nullValueBehaviorKeyName) of: clientId");
+
+ assertValidationFailureReasonsAllowingExtraReasons((qInstance ->
+ {
+ qInstance.addSecurityKeyType(new QSecurityKeyType().withName("clientId").withAllAccessKeyName("clientId").withNullValueBehaviorKeyName("clientId"));
+ }), "More than one SecurityKeyType with name (or allAccessKeyName or nullValueBehaviorKeyName) of: clientId");
assertValidationFailureReasons((qInstance ->
{
qInstance.addSecurityKeyType(new QSecurityKeyType().withName("clientId").withAllAccessKeyName("allAccess"));
qInstance.addSecurityKeyType(new QSecurityKeyType().withName("warehouseId").withAllAccessKeyName("allAccess"));
- }), "More than one SecurityKeyType with name (or allAccessKeyName) of: allAccess");
+ }), "More than one SecurityKeyType with name (or allAccessKeyName or nullValueBehaviorKeyName) of: allAccess");
+
+ assertValidationFailureReasons((qInstance ->
+ {
+ qInstance.addSecurityKeyType(new QSecurityKeyType().withName("clientId").withNullValueBehaviorKeyName("nullBehavior"));
+ qInstance.addSecurityKeyType(new QSecurityKeyType().withName("warehouseId").withNullValueBehaviorKeyName("nullBehavior"));
+ }), "More than one SecurityKeyType with name (or allAccessKeyName or nullValueBehaviorKeyName) of: nullBehavior");
assertValidationFailureReasons((qInstance ->
{
qInstance.addSecurityKeyType(new QSecurityKeyType().withName("clientId").withAllAccessKeyName("allAccess"));
qInstance.addSecurityKeyType(new QSecurityKeyType().withName("allAccess"));
- }), "More than one SecurityKeyType with name (or allAccessKeyName) of: allAccess");
+ }), "More than one SecurityKeyType with name (or allAccessKeyName or nullValueBehaviorKeyName) of: allAccess");
assertValidationFailureReasons((qInstance -> qInstance.addSecurityKeyType(new QSecurityKeyType().withName("clientId").withPossibleValueSourceName("nonPVS"))),
"Unrecognized possibleValueSourceName in securityKeyType");
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/security/NullValueBehaviorUtilTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/security/NullValueBehaviorUtilTest.java
new file mode 100644
index 00000000..ee33b03f
--- /dev/null
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/security/NullValueBehaviorUtilTest.java
@@ -0,0 +1,84 @@
+/*
+ * QQQ - Low-code Application Framework for Engineers.
+ * Copyright (C) 2021-2024. Kingsrook, LLC
+ * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
+ * contact@kingsrook.com
+ * https://github.com/Kingsrook/
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package com.kingsrook.qqq.backend.core.model.metadata.security;
+
+
+import java.util.List;
+import java.util.Map;
+import com.kingsrook.qqq.backend.core.BaseTest;
+import com.kingsrook.qqq.backend.core.context.QContext;
+import com.kingsrook.qqq.backend.core.utils.TestUtils;
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+
+/*******************************************************************************
+ ** Unit test for NullValueBehaviorUtil
+ *******************************************************************************/
+class NullValueBehaviorUtilTest extends BaseTest
+{
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Test
+ void test()
+ {
+ ////////////////////////////////////////////////////////////////////////////////////////
+ // if session doesn't have a null-value key, then always get back the lock's behavior //
+ ////////////////////////////////////////////////////////////////////////////////////////
+ for(RecordSecurityLock.NullValueBehavior lockNullValueBehavior : RecordSecurityLock.NullValueBehavior.values())
+ {
+ assertEquals(lockNullValueBehavior, NullValueBehaviorUtil.getEffectiveNullValueBehavior(new RecordSecurityLock()
+ .withSecurityKeyType(TestUtils.SECURITY_KEY_TYPE_STORE)
+ .withNullValueBehavior(lockNullValueBehavior)));
+ }
+
+ /////////////////////////////////////////////////////////////////////
+ // if session DOES have a null-value key, then always gete it back //
+ /////////////////////////////////////////////////////////////////////
+ for(RecordSecurityLock.NullValueBehavior sessionNullValueBehavior : RecordSecurityLock.NullValueBehavior.values())
+ {
+ QContext.getQSession().withSecurityKeyValues(Map.of(TestUtils.SECURITY_KEY_TYPE_STORE_NULL_BEHAVIOR, List.of(sessionNullValueBehavior.toString())));
+
+ for(RecordSecurityLock.NullValueBehavior lockNullValueBehavior : RecordSecurityLock.NullValueBehavior.values())
+ {
+ assertEquals(sessionNullValueBehavior, NullValueBehaviorUtil.getEffectiveNullValueBehavior(new RecordSecurityLock()
+ .withSecurityKeyType(TestUtils.SECURITY_KEY_TYPE_STORE)
+ .withNullValueBehavior(lockNullValueBehavior)));
+ }
+ }
+
+ ////////////////////////////////////////////////////////////////////
+ // if session has an invalid key, always get back lock's behavior //
+ ////////////////////////////////////////////////////////////////////
+ for(RecordSecurityLock.NullValueBehavior lockNullValueBehavior : RecordSecurityLock.NullValueBehavior.values())
+ {
+ QContext.getQSession().withSecurityKeyValues(Map.of(TestUtils.SECURITY_KEY_TYPE_STORE_NULL_BEHAVIOR, List.of("xyz")));
+
+ assertEquals(lockNullValueBehavior, NullValueBehaviorUtil.getEffectiveNullValueBehavior(new RecordSecurityLock()
+ .withSecurityKeyType(TestUtils.SECURITY_KEY_TYPE_STORE)
+ .withNullValueBehavior(lockNullValueBehavior)));
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java
index 9656672e..2e3a937e 100644
--- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java
+++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java
@@ -173,6 +173,7 @@ public class TestUtils
public static final String SECURITY_KEY_TYPE_STORE = "store";
public static final String SECURITY_KEY_TYPE_STORE_ALL_ACCESS = "storeAllAccess";
+ public static final String SECURITY_KEY_TYPE_STORE_NULL_BEHAVIOR = "storeNullBehavior";
public static final String SECURITY_KEY_TYPE_INTERNAL_OR_EXTERNAL = "internalOrExternal";
@@ -471,6 +472,7 @@ public class TestUtils
return new QSecurityKeyType()
.withName(SECURITY_KEY_TYPE_STORE)
.withAllAccessKeyName(SECURITY_KEY_TYPE_STORE_ALL_ACCESS)
+ .withNullValueBehaviorKeyName(SECURITY_KEY_TYPE_STORE_NULL_BEHAVIOR)
.withPossibleValueSourceName(POSSIBLE_VALUE_SOURCE_STORE);
}
@@ -1255,7 +1257,6 @@ public class TestUtils
-
/*******************************************************************************
**
*******************************************************************************/
diff --git a/qqq-backend-module-mongodb/src/main/java/com/kingsrook/qqq/backend/module/mongodb/actions/AbstractMongoDBAction.java b/qqq-backend-module-mongodb/src/main/java/com/kingsrook/qqq/backend/module/mongodb/actions/AbstractMongoDBAction.java
index 626a232c..fb7d2156 100644
--- a/qqq-backend-module-mongodb/src/main/java/com/kingsrook/qqq/backend/module/mongodb/actions/AbstractMongoDBAction.java
+++ b/qqq-backend-module-mongodb/src/main/java/com/kingsrook/qqq/backend/module/mongodb/actions/AbstractMongoDBAction.java
@@ -47,6 +47,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
+import com.kingsrook.qqq.backend.core.model.metadata.security.NullValueBehaviorUtil;
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
@@ -479,7 +480,7 @@ public class AbstractMongoDBAction
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// handle user with no values -- they can only see null values, and only iff the lock's null-value behavior is ALLOW //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- if(RecordSecurityLock.NullValueBehavior.ALLOW.equals(recordSecurityLock.getNullValueBehavior()))
+ if(RecordSecurityLock.NullValueBehavior.ALLOW.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
{
lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IS_BLANK));
}
@@ -498,7 +499,7 @@ public class AbstractMongoDBAction
// else, if user/session has some values, build an IN rule - //
// noting that if the lock's null-value behavior is ALLOW, then we actually want IS_NULL_OR_IN, not just IN //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
- if(RecordSecurityLock.NullValueBehavior.ALLOW.equals(recordSecurityLock.getNullValueBehavior()))
+ if(RecordSecurityLock.NullValueBehavior.ALLOW.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
{
lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IS_NULL_OR_IN, securityKeyValues));
}
diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java
index ea3bba57..5ba71e75 100644
--- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java
+++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java
@@ -66,6 +66,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
+import com.kingsrook.qqq.backend.core.model.metadata.security.NullValueBehaviorUtil;
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
@@ -467,7 +468,7 @@ public abstract class AbstractRDBMSAction
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// handle user with no values -- they can only see null values, and only iff the lock's null-value behavior is ALLOW //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
- if(RecordSecurityLock.NullValueBehavior.ALLOW.equals(recordSecurityLock.getNullValueBehavior()))
+ if(RecordSecurityLock.NullValueBehavior.ALLOW.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
{
lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IS_BLANK));
}
@@ -486,7 +487,7 @@ public abstract class AbstractRDBMSAction
// else, if user/session has some values, build an IN rule - //
// noting that if the lock's null-value behavior is ALLOW, then we actually want IS_NULL_OR_IN, not just IN //
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
- if(RecordSecurityLock.NullValueBehavior.ALLOW.equals(recordSecurityLock.getNullValueBehavior()))
+ if(RecordSecurityLock.NullValueBehavior.ALLOW.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock)))
{
lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IS_NULL_OR_IN, securityKeyValues));
}