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

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