Merge pull request #38 from Kingsrook/feature/CE-567-script-writer-dev-setup-sdlc-ci-cd-setup

CE-567 Add concept of security lock Scope - e.g., READ-WRITE (blockin…
This commit is contained in:
2023-08-15 19:41:31 -05:00
committed by GitHub
16 changed files with 420 additions and 24 deletions

View File

@ -44,6 +44,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QUser;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -166,7 +167,7 @@ public class AuditAction extends AbstractQActionFunction<AuditInput, AuditOutput
///////////////////////////////////////////////////
// validate security keys on the table are given //
///////////////////////////////////////////////////
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks())))
{
if(auditSingleInput.getSecurityKeyValues() == null || !auditSingleInput.getSecurityKeyValues().containsKey(recordSecurityLock.getSecurityKeyType()))
{

View File

@ -53,6 +53,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.processes.QProcessMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.session.QSession;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -378,7 +379,7 @@ public class DMLAuditAction extends AbstractQActionFunction<DMLAuditInput, DMLAu
private static Map<String, Serializable> getRecordSecurityKeyValues(QTableMetaData table, QRecord record)
{
Map<String, Serializable> securityKeyValues = new HashMap<>();
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks())))
{
securityKeyValues.put(recordSecurityLock.getSecurityKeyType(), record == null ? null : record.getValue(recordSecurityLock.getFieldName()));
}

View File

@ -40,6 +40,7 @@ import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreDeleteCusto
import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader;
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.ValidateRecordSecurityLockHelper;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.logging.LogPair;
@ -321,6 +322,8 @@ public class DeleteAction
QTableMetaData table = deleteInput.getTable();
List<QRecord> primaryKeysNotFound = validateRecordsExistAndCanBeAccessed(deleteInput, oldRecordList.get());
ValidateRecordSecurityLockHelper.validateSecurityFields(table, oldRecordList.get(), ValidateRecordSecurityLockHelper.Action.DELETE);
///////////////////////////////////////////////////////////////////////////
// after all validations, run the pre-delete customizer, if there is one //
///////////////////////////////////////////////////////////////////////////

View File

@ -61,6 +61,8 @@ 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.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
@ -209,14 +211,16 @@ public class UpdateAction
{
validateRecordsExistAndCanBeAccessed(updateInput, oldRecordList.get());
}
else
{
ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE);
}
if(updateInput.getInputSource().shouldValidateRequiredFields())
{
validateRequiredFields(updateInput);
}
ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE);
///////////////////////////////////////////////////////////////////////////
// after all validations, run the pre-update customizer, if there is one //
///////////////////////////////////////////////////////////////////////////
@ -287,6 +291,8 @@ public class UpdateAction
QTableMetaData table = updateInput.getTable();
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
List<RecordSecurityLock> onlyWriteLocks = RecordSecurityLockFilters.filterForOnlyWriteLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks()));
for(List<QRecord> page : CollectionUtils.getPages(updateInput.getRecords(), 1000))
{
List<Serializable> primaryKeysToLookup = new ArrayList<>();
@ -320,6 +326,8 @@ public class UpdateAction
}
}
ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE);
for(QRecord record : page)
{
Serializable value = ValueUtils.getValueAsFieldType(primaryKeyField.getType(), record.getValue(table.getPrimaryKeyField()));
@ -332,6 +340,19 @@ public class UpdateAction
{
record.addError(new NotFoundStatusMessage("No record was found to update for " + primaryKeyField.getLabel() + " = " + value));
}
else
{
///////////////////////////////////////////////////////////////////////////////////////////
// if the table has any write-only locks, validate their values here, on the old-records //
///////////////////////////////////////////////////////////////////////////////////////////
for(RecordSecurityLock lock : onlyWriteLocks)
{
QRecord oldRecord = lookedUpRecords.get(value);
QFieldType fieldType = table.getField(lock.getFieldName()).getType();
Serializable lockValue = ValueUtils.getValueAsFieldType(fieldType, oldRecord.getValue(lock.getFieldName()));
ValidateRecordSecurityLockHelper.validateRecordSecurityValue(table, record, lock, lockValue, fieldType, ValidateRecordSecurityLockHelper.Action.UPDATE);
}
}
}
}
}

View File

@ -44,6 +44,7 @@ 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.QSecurityKeyType;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.model.statusmessages.PermissionDeniedMessage;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -68,6 +69,7 @@ public class ValidateRecordSecurityLockHelper
{
INSERT,
UPDATE,
DELETE,
SELECT
}
@ -78,7 +80,7 @@ public class ValidateRecordSecurityLockHelper
*******************************************************************************/
public static void validateSecurityFields(QTableMetaData table, List<QRecord> records, Action action) throws QException
{
List<RecordSecurityLock> locksToCheck = getRecordSecurityLocks(table);
List<RecordSecurityLock> locksToCheck = getRecordSecurityLocks(table, action);
if(CollectionUtils.nullSafeIsEmpty(locksToCheck))
{
return;
@ -98,11 +100,12 @@ public class ValidateRecordSecurityLockHelper
for(QRecord record : records)
{
if(action.equals(Action.UPDATE) && !record.getValues().containsKey(field.getName()))
if(action.equals(Action.UPDATE) && !record.getValues().containsKey(field.getName()) && RecordSecurityLock.LockScope.READ_AND_WRITE.equals(recordSecurityLock.getLockScope()))
{
/////////////////////////////////////////////////////////////////////////
// if not updating the security field, then no error can come from it! //
/////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////////////////////////////
// if this is a read-write lock, then if we have the record, it means we were able to read the record. //
// So if we're not updating the security field, then no error can come from it! //
/////////////////////////////////////////////////////////////////////////////////////////////////////////
continue;
}
@ -244,11 +247,18 @@ public class ValidateRecordSecurityLockHelper
/*******************************************************************************
**
*******************************************************************************/
private static List<RecordSecurityLock> getRecordSecurityLocks(QTableMetaData table)
private static List<RecordSecurityLock> getRecordSecurityLocks(QTableMetaData table, Action action)
{
List<RecordSecurityLock> recordSecurityLocks = table.getRecordSecurityLocks();
List<RecordSecurityLock> recordSecurityLocks = CollectionUtils.nonNullList(table.getRecordSecurityLocks());
List<RecordSecurityLock> locksToCheck = new ArrayList<>();
recordSecurityLocks = switch(action)
{
case INSERT, UPDATE, DELETE -> RecordSecurityLockFilters.filterForWriteLocks(recordSecurityLocks);
case SELECT -> RecordSecurityLockFilters.filterForReadLocks(recordSecurityLocks);
default -> throw (new IllegalArgumentException("Unsupported action: " + action));
};
////////////////////////////////////////
// if there are no locks, just return //
////////////////////////////////////////
@ -281,7 +291,7 @@ public class ValidateRecordSecurityLockHelper
/*******************************************************************************
**
*******************************************************************************/
static void validateRecordSecurityValue(QTableMetaData table, QRecord record, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action)
public static void validateRecordSecurityValue(QTableMetaData table, QRecord record, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action)
{
if(recordSecurityValue == null)
{

View File

@ -589,6 +589,8 @@ public class QInstanceValidator
prefix = "Table " + table.getName() + " recordSecurityLock (of key type " + securityKeyTypeName + ") ";
assertCondition(recordSecurityLock.getLockScope() != null, prefix + " is missing its lockScope");
boolean hasAnyBadJoins = false;
for(String joinName : CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()))
{

View File

@ -30,6 +30,7 @@ import java.util.List;
import java.util.Map;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -246,7 +247,7 @@ public class AuditSingleInput
setAuditTableName(table.getName());
this.securityKeyValues = new HashMap<>();
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks())))
{
this.securityKeyValues.put(recordSecurityLock.getFieldName(), record.getValueInteger(recordSecurityLock.getFieldName()));
}

View File

@ -39,6 +39,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters;
import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
@ -87,7 +88,7 @@ public class JoinsContext
///////////////////////////////////////////////////////////////
// ensure any joins that contribute a recordLock are present //
///////////////////////////////////////////////////////////////
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(instance.getTable(tableName).getRecordSecurityLocks()))
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(instance.getTable(tableName).getRecordSecurityLocks())))
{
ensureRecordSecurityLockIsRepresented(instance, tableName, recordSecurityLock);
}

View File

@ -34,6 +34,10 @@ import java.util.List;
** - recordSecurityLock.fieldName = order.clientId
** - recordSecurityLock.joinNameChain = [orderJoinOrderLineItem, orderLineItemJoinOrderLineItemExtrinsic]
** that is - what's the chain that takes us FROM the security fieldName TO the table with the lock.
**
** LockScope controls what the lock prevents users from doing without a valid key.
** - READ_AND_WRITE means that users cannot read or write records without a valid key.
** - WRITE means that users cannot write records without a valid key (but they can read them).
*******************************************************************************/
public class RecordSecurityLock
{
@ -42,6 +46,8 @@ public class RecordSecurityLock
private List<String> joinNameChain;
private NullValueBehavior nullValueBehavior = NullValueBehavior.DENY;
private LockScope lockScope = LockScope.READ_AND_WRITE;
/*******************************************************************************
@ -66,6 +72,17 @@ public class RecordSecurityLock
/*******************************************************************************
**
*******************************************************************************/
public enum LockScope
{
READ_AND_WRITE,
WRITE
}
/*******************************************************************************
** Getter for securityKeyType
*******************************************************************************/
@ -188,4 +205,35 @@ public class RecordSecurityLock
return (this);
}
/*******************************************************************************
** Getter for lockScope
*******************************************************************************/
public LockScope getLockScope()
{
return (this.lockScope);
}
/*******************************************************************************
** Setter for lockScope
*******************************************************************************/
public void setLockScope(LockScope lockScope)
{
this.lockScope = lockScope;
}
/*******************************************************************************
** Fluent setter for lockScope
*******************************************************************************/
public RecordSecurityLock withLockScope(LockScope lockScope)
{
this.lockScope = lockScope;
return (this);
}
}

View File

@ -0,0 +1,80 @@
/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2023. 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;
/*******************************************************************************
** standard filtering operations for lists of record security locks.
*******************************************************************************/
public class RecordSecurityLockFilters
{
/*******************************************************************************
** filter a list of locks so that we only see the ones that apply to reads.
*******************************************************************************/
public static List<RecordSecurityLock> filterForReadLocks(List<RecordSecurityLock> recordSecurityLocks)
{
if(recordSecurityLocks == null)
{
return (null);
}
return (recordSecurityLocks.stream().filter(rsl -> RecordSecurityLock.LockScope.READ_AND_WRITE.equals(rsl.getLockScope())).toList());
}
/*******************************************************************************
** filter a list of locks so that we only see the ones that apply to writes.
*******************************************************************************/
public static List<RecordSecurityLock> filterForWriteLocks(List<RecordSecurityLock> recordSecurityLocks)
{
if(recordSecurityLocks == null)
{
return (null);
}
return (recordSecurityLocks.stream().filter(rsl ->
RecordSecurityLock.LockScope.READ_AND_WRITE.equals(rsl.getLockScope())
|| RecordSecurityLock.LockScope.WRITE.equals(rsl.getLockScope()
)).toList());
}
/*******************************************************************************
** filter a list of locks so that we only see the ones that are WRITE type only.
*******************************************************************************/
public static List<RecordSecurityLock> filterForOnlyWriteLocks(List<RecordSecurityLock> recordSecurityLocks)
{
if(recordSecurityLocks == null)
{
return (null);
}
return (recordSecurityLocks.stream().filter(rsl -> RecordSecurityLock.LockScope.WRITE.equals(rsl.getLockScope())).toList());
}
}

View File

@ -31,7 +31,10 @@ import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
import com.kingsrook.qqq.backend.core.actions.tables.helpers.ValidateRecordSecurityLockHelper;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
@ -46,6 +49,8 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.scripts.Script;
import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision;
import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevisionFile;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.StringUtils;
@ -69,7 +74,7 @@ public class StoreScriptRevisionProcessStep implements BackendStep
{
InsertAction insertAction = new InsertAction();
InsertInput insertInput = new InsertInput();
insertInput.setTableName("scriptRevision");
insertInput.setTableName(ScriptRevision.TABLE_NAME);
QBackendTransaction transaction = insertAction.openTransaction(insertInput);
insertInput.setTransaction(transaction);
@ -87,14 +92,23 @@ public class StoreScriptRevisionProcessStep implements BackendStep
// get the existing script, to update //
////////////////////////////////////////
GetInput getInput = new GetInput();
getInput.setTableName("script");
getInput.setTableName(Script.TABLE_NAME);
getInput.setPrimaryKey(scriptId);
getInput.setTransaction(transaction);
GetOutput getOutput = new GetAction().execute(getInput);
QRecord script = getOutput.getRecord();
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// in case the app added a security field to the scripts table, make sure the user is allowed to edit the script //
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
ValidateRecordSecurityLockHelper.validateSecurityFields(QContext.getQInstance().getTable(Script.TABLE_NAME), List.of(script), ValidateRecordSecurityLockHelper.Action.UPDATE);
if(CollectionUtils.nullSafeHasContents(script.getErrors()))
{
throw (new QPermissionDeniedException(script.getErrors().get(0).getMessage()));
}
QueryInput queryInput = new QueryInput();
queryInput.setTableName("scriptRevision");
queryInput.setTableName(ScriptRevision.TABLE_NAME);
queryInput.setFilter(new QQueryFilter()
.withCriteria(new QFilterCriteria("scriptId", QCriteriaOperator.EQUALS, List.of(script.getValue("id"))))
.withOrderBy(new QFilterOrderBy("sequenceNo", false))
@ -183,7 +197,7 @@ public class StoreScriptRevisionProcessStep implements BackendStep
////////////////////////////////////////////////////
script.setValue("currentScriptRevisionId", scriptRevision.getValue("id"));
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName("script");
updateInput.setTableName(Script.TABLE_NAME);
updateInput.setRecords(List.of(script));
updateInput.setTransaction(transaction);
new UpdateAction().execute(updateInput);
@ -198,6 +212,7 @@ public class StoreScriptRevisionProcessStep implements BackendStep
catch(Exception e)
{
transaction.rollback();
throw (e);
}
finally
{