mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 05:01:07 +00:00
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:
@ -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#
|
||||
|
||||
|
@ -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));
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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");
|
||||
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
Reference in New Issue
Block a user