CE-937 Add concept of nullValueBehaviorKeyName to QSecurityKeyType - so that a session can override the null-value-behavior for a table's lock (e.g., allow super-users access to null records, where normal users cannot).

This commit is contained in:
2024-02-26 15:14:55 -06:00
parent 7d25fc7390
commit 45899400ad
12 changed files with 334 additions and 18 deletions

View File

@ -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#

View File

@ -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));

View File

@ -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);

View File

@ -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);
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<Serializable> 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());
}
}

View File

@ -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);
}
}

View File

@ -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<String, NullValueBehavior> 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());
}
}

View File

@ -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");

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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)));
}
}
}

View File

@ -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
/*******************************************************************************
**
*******************************************************************************/

View File

@ -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));
}

View File

@ -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));
}