mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 13:10:44 +00:00
Merge pull request #37 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:
@ -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.actions.tables.insert.InsertOutput;
|
||||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
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.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.metadata.tables.QTableMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.session.QUser;
|
import com.kingsrook.qqq.backend.core.model.session.QUser;
|
||||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
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 //
|
// 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()))
|
if(auditSingleInput.getSecurityKeyValues() == null || !auditSingleInput.getSecurityKeyValues().containsKey(recordSecurityLock.getSecurityKeyType()))
|
||||||
{
|
{
|
||||||
|
@ -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.fields.QFieldType;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData;
|
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.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.metadata.tables.QTableMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
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)
|
private static Map<String, Serializable> getRecordSecurityKeyValues(QTableMetaData table, QRecord record)
|
||||||
{
|
{
|
||||||
Map<String, Serializable> securityKeyValues = new HashMap<>();
|
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()));
|
securityKeyValues.put(recordSecurityLock.getSecurityKeyType(), record == null ? null : record.getValue(recordSecurityLock.getFieldName()));
|
||||||
}
|
}
|
||||||
|
@ -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.QCodeLoader;
|
||||||
import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
|
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.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.context.QContext;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
import com.kingsrook.qqq.backend.core.logging.LogPair;
|
import com.kingsrook.qqq.backend.core.logging.LogPair;
|
||||||
@ -321,6 +322,8 @@ public class DeleteAction
|
|||||||
QTableMetaData table = deleteInput.getTable();
|
QTableMetaData table = deleteInput.getTable();
|
||||||
List<QRecord> primaryKeysNotFound = validateRecordsExistAndCanBeAccessed(deleteInput, oldRecordList.get());
|
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 //
|
// after all validations, run the pre-delete customizer, if there is one //
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -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.fields.QFieldType;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
|
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.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.Association;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
|
import com.kingsrook.qqq.backend.core.model.statusmessages.BadInputStatusMessage;
|
||||||
@ -209,14 +211,16 @@ public class UpdateAction
|
|||||||
{
|
{
|
||||||
validateRecordsExistAndCanBeAccessed(updateInput, oldRecordList.get());
|
validateRecordsExistAndCanBeAccessed(updateInput, oldRecordList.get());
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE);
|
||||||
|
}
|
||||||
|
|
||||||
if(updateInput.getInputSource().shouldValidateRequiredFields())
|
if(updateInput.getInputSource().shouldValidateRequiredFields())
|
||||||
{
|
{
|
||||||
validateRequiredFields(updateInput);
|
validateRequiredFields(updateInput);
|
||||||
}
|
}
|
||||||
|
|
||||||
ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE);
|
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
// after all validations, run the pre-update customizer, if there is one //
|
// after all validations, run the pre-update customizer, if there is one //
|
||||||
///////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////
|
||||||
@ -287,6 +291,8 @@ public class UpdateAction
|
|||||||
QTableMetaData table = updateInput.getTable();
|
QTableMetaData table = updateInput.getTable();
|
||||||
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
|
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
|
||||||
|
|
||||||
|
List<RecordSecurityLock> onlyWriteLocks = RecordSecurityLockFilters.filterForOnlyWriteLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks()));
|
||||||
|
|
||||||
for(List<QRecord> page : CollectionUtils.getPages(updateInput.getRecords(), 1000))
|
for(List<QRecord> page : CollectionUtils.getPages(updateInput.getRecords(), 1000))
|
||||||
{
|
{
|
||||||
List<Serializable> primaryKeysToLookup = new ArrayList<>();
|
List<Serializable> primaryKeysToLookup = new ArrayList<>();
|
||||||
@ -320,6 +326,8 @@ public class UpdateAction
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE);
|
||||||
|
|
||||||
for(QRecord record : page)
|
for(QRecord record : page)
|
||||||
{
|
{
|
||||||
Serializable value = ValueUtils.getValueAsFieldType(primaryKeyField.getType(), record.getValue(table.getPrimaryKeyField()));
|
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));
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.joins.QJoinMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
|
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.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.metadata.tables.QTableMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.statusmessages.PermissionDeniedMessage;
|
import com.kingsrook.qqq.backend.core.model.statusmessages.PermissionDeniedMessage;
|
||||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||||
@ -68,6 +69,7 @@ public class ValidateRecordSecurityLockHelper
|
|||||||
{
|
{
|
||||||
INSERT,
|
INSERT,
|
||||||
UPDATE,
|
UPDATE,
|
||||||
|
DELETE,
|
||||||
SELECT
|
SELECT
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,7 +80,7 @@ public class ValidateRecordSecurityLockHelper
|
|||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
public static void validateSecurityFields(QTableMetaData table, List<QRecord> records, Action action) throws QException
|
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))
|
if(CollectionUtils.nullSafeIsEmpty(locksToCheck))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@ -98,11 +100,12 @@ public class ValidateRecordSecurityLockHelper
|
|||||||
|
|
||||||
for(QRecord record : records)
|
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;
|
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<>();
|
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 //
|
// 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)
|
if(recordSecurityValue == null)
|
||||||
{
|
{
|
||||||
|
@ -586,6 +586,8 @@ public class QInstanceValidator
|
|||||||
|
|
||||||
prefix = "Table " + table.getName() + " recordSecurityLock (of key type " + securityKeyTypeName + ") ";
|
prefix = "Table " + table.getName() + " recordSecurityLock (of key type " + securityKeyTypeName + ") ";
|
||||||
|
|
||||||
|
assertCondition(recordSecurityLock.getLockScope() != null, prefix + " is missing its lockScope");
|
||||||
|
|
||||||
boolean hasAnyBadJoins = false;
|
boolean hasAnyBadJoins = false;
|
||||||
for(String joinName : CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()))
|
for(String joinName : CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()))
|
||||||
{
|
{
|
||||||
|
@ -30,6 +30,7 @@ import java.util.List;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
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.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.metadata.tables.QTableMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||||
|
|
||||||
@ -246,7 +247,7 @@ public class AuditSingleInput
|
|||||||
setAuditTableName(table.getName());
|
setAuditTableName(table.getName());
|
||||||
|
|
||||||
this.securityKeyValues = new HashMap<>();
|
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()));
|
this.securityKeyValues.put(recordSecurityLock.getFieldName(), record.getValueInteger(recordSecurityLock.getFieldName()));
|
||||||
}
|
}
|
||||||
|
@ -37,6 +37,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.fields.QFieldMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
|
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.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.ExposedJoin;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||||
@ -81,7 +82,7 @@ public class JoinsContext
|
|||||||
///////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////
|
||||||
// ensure any joins that contribute a recordLock are present //
|
// 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())))
|
||||||
{
|
{
|
||||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
// ok - so - the join name chain is going to be like this: //
|
// ok - so - the join name chain is going to be like this: //
|
||||||
|
@ -34,6 +34,10 @@ import java.util.List;
|
|||||||
** - recordSecurityLock.fieldName = order.clientId
|
** - recordSecurityLock.fieldName = order.clientId
|
||||||
** - recordSecurityLock.joinNameChain = [orderJoinOrderLineItem, orderLineItemJoinOrderLineItemExtrinsic]
|
** - recordSecurityLock.joinNameChain = [orderJoinOrderLineItem, orderLineItemJoinOrderLineItemExtrinsic]
|
||||||
** that is - what's the chain that takes us FROM the security fieldName TO the table with the lock.
|
** 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
|
public class RecordSecurityLock
|
||||||
{
|
{
|
||||||
@ -42,6 +46,8 @@ public class RecordSecurityLock
|
|||||||
private List<String> joinNameChain;
|
private List<String> joinNameChain;
|
||||||
private NullValueBehavior nullValueBehavior = NullValueBehavior.DENY;
|
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
|
** Getter for securityKeyType
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
@ -188,4 +205,35 @@ public class RecordSecurityLock
|
|||||||
return (this);
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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.InsertAction;
|
||||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
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.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.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.RunBackendStepInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
|
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.query.QueryOutput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
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.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.model.scripts.ScriptRevisionFile;
|
||||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||||
@ -69,7 +74,7 @@ public class StoreScriptRevisionProcessStep implements BackendStep
|
|||||||
{
|
{
|
||||||
InsertAction insertAction = new InsertAction();
|
InsertAction insertAction = new InsertAction();
|
||||||
InsertInput insertInput = new InsertInput();
|
InsertInput insertInput = new InsertInput();
|
||||||
insertInput.setTableName("scriptRevision");
|
insertInput.setTableName(ScriptRevision.TABLE_NAME);
|
||||||
QBackendTransaction transaction = insertAction.openTransaction(insertInput);
|
QBackendTransaction transaction = insertAction.openTransaction(insertInput);
|
||||||
insertInput.setTransaction(transaction);
|
insertInput.setTransaction(transaction);
|
||||||
|
|
||||||
@ -87,14 +92,23 @@ public class StoreScriptRevisionProcessStep implements BackendStep
|
|||||||
// get the existing script, to update //
|
// get the existing script, to update //
|
||||||
////////////////////////////////////////
|
////////////////////////////////////////
|
||||||
GetInput getInput = new GetInput();
|
GetInput getInput = new GetInput();
|
||||||
getInput.setTableName("script");
|
getInput.setTableName(Script.TABLE_NAME);
|
||||||
getInput.setPrimaryKey(scriptId);
|
getInput.setPrimaryKey(scriptId);
|
||||||
getInput.setTransaction(transaction);
|
getInput.setTransaction(transaction);
|
||||||
GetOutput getOutput = new GetAction().execute(getInput);
|
GetOutput getOutput = new GetAction().execute(getInput);
|
||||||
QRecord script = getOutput.getRecord();
|
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 queryInput = new QueryInput();
|
||||||
queryInput.setTableName("scriptRevision");
|
queryInput.setTableName(ScriptRevision.TABLE_NAME);
|
||||||
queryInput.setFilter(new QQueryFilter()
|
queryInput.setFilter(new QQueryFilter()
|
||||||
.withCriteria(new QFilterCriteria("scriptId", QCriteriaOperator.EQUALS, List.of(script.getValue("id"))))
|
.withCriteria(new QFilterCriteria("scriptId", QCriteriaOperator.EQUALS, List.of(script.getValue("id"))))
|
||||||
.withOrderBy(new QFilterOrderBy("sequenceNo", false))
|
.withOrderBy(new QFilterOrderBy("sequenceNo", false))
|
||||||
@ -183,7 +197,7 @@ public class StoreScriptRevisionProcessStep implements BackendStep
|
|||||||
////////////////////////////////////////////////////
|
////////////////////////////////////////////////////
|
||||||
script.setValue("currentScriptRevisionId", scriptRevision.getValue("id"));
|
script.setValue("currentScriptRevisionId", scriptRevision.getValue("id"));
|
||||||
UpdateInput updateInput = new UpdateInput();
|
UpdateInput updateInput = new UpdateInput();
|
||||||
updateInput.setTableName("script");
|
updateInput.setTableName(Script.TABLE_NAME);
|
||||||
updateInput.setRecords(List.of(script));
|
updateInput.setRecords(List.of(script));
|
||||||
updateInput.setTransaction(transaction);
|
updateInput.setTransaction(transaction);
|
||||||
new UpdateAction().execute(updateInput);
|
new UpdateAction().execute(updateInput);
|
||||||
@ -198,6 +212,7 @@ public class StoreScriptRevisionProcessStep implements BackendStep
|
|||||||
catch(Exception e)
|
catch(Exception e)
|
||||||
{
|
{
|
||||||
transaction.rollback();
|
transaction.rollback();
|
||||||
|
throw (e);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
@ -53,6 +53,7 @@ import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
|||||||
import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
|
import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
|
||||||
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
|
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
@ -365,6 +366,36 @@ class DeleteActionTest extends BaseTest
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testSecurityLockWriteScope() throws QException
|
||||||
|
{
|
||||||
|
TestUtils.updatePersonMemoryTableInContextWithWritableByWriteLockAndInsert3TestRecords();
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// try to delete 1, 2, and 3. 2 should be blocked, because it has a writable-By that isn't in our session //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
QContext.getQSession().setSecurityKeyValues(MapBuilder.of("writableBy", ListBuilder.of("jdoe")));
|
||||||
|
DeleteInput deleteInput = new DeleteInput();
|
||||||
|
deleteInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||||
|
deleteInput.setPrimaryKeys(List.of(1, 2, 3));
|
||||||
|
DeleteOutput deleteOutput = new DeleteAction().execute(deleteInput);
|
||||||
|
|
||||||
|
assertEquals(1, deleteOutput.getRecordsWithErrors().size());
|
||||||
|
assertThat(deleteOutput.getRecordsWithErrors().get(0).getErrors().get(0).getMessage())
|
||||||
|
.contains("You do not have permission")
|
||||||
|
.contains("kmarsh")
|
||||||
|
.contains("Only Writable By");
|
||||||
|
|
||||||
|
assertEquals(1, new CountAction().execute(new CountInput(TestUtils.TABLE_NAME_PERSON_MEMORY)).getCount());
|
||||||
|
assertEquals(1, new CountAction().execute(new CountInput(TestUtils.TABLE_NAME_PERSON_MEMORY)
|
||||||
|
.withFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.EQUALS, 2)))).getCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
**
|
**
|
||||||
*******************************************************************************/
|
*******************************************************************************/
|
||||||
|
@ -35,9 +35,12 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
|||||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.statusmessages.QErrorMessage;
|
||||||
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore;
|
import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore;
|
||||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||||
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@ -692,4 +695,60 @@ class InsertActionTest extends BaseTest
|
|||||||
assertEquals(3, TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_ORDER).size());
|
assertEquals(3, TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_ORDER).size());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testSecurityLockWriteScope() throws QException
|
||||||
|
{
|
||||||
|
TestUtils.updatePersonMemoryTableInContextWithWritableByWriteLockAndInsert3TestRecords();
|
||||||
|
|
||||||
|
QContext.getQSession().setSecurityKeyValues(MapBuilder.of("writableBy", ListBuilder.of("hsimpson")));
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// with only hsimpson in our key, make sure we can't insert a row w/ a different value //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
{
|
||||||
|
InsertOutput insertOutput = new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withRecords(List.of(
|
||||||
|
new QRecord().withValue("id", 100).withValue("firstName", "Jean-Luc").withValue("onlyWritableBy", "jkirk")
|
||||||
|
)));
|
||||||
|
List<QErrorMessage> errors = insertOutput.getRecords().get(0).getErrors();
|
||||||
|
assertEquals(1, errors.size());
|
||||||
|
assertThat(errors.get(0).getMessage())
|
||||||
|
.contains("You do not have permission")
|
||||||
|
.contains("jkirk")
|
||||||
|
.contains("Only Writable By");
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// make sure we can insert w/ a null in onlyWritableBy (because key (from test utils) was set to allow null) //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
{
|
||||||
|
InsertOutput insertOutput = new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withRecords(List.of(
|
||||||
|
new QRecord().withValue("id", 101).withValue("firstName", "Benajamin").withValue("onlyWritableBy", null)
|
||||||
|
)));
|
||||||
|
List<QErrorMessage> errors = insertOutput.getRecords().get(0).getErrors();
|
||||||
|
assertEquals(0, errors.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// change the null behavior to deny, and try above again, expecting an error //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).getRecordSecurityLocks().get(0).setNullValueBehavior(RecordSecurityLock.NullValueBehavior.DENY);
|
||||||
|
{
|
||||||
|
InsertOutput insertOutput = new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withRecords(List.of(
|
||||||
|
new QRecord().withValue("id", 102).withValue("firstName", "Katherine").withValue("onlyWritableBy", null)
|
||||||
|
)));
|
||||||
|
List<QErrorMessage> errors = insertOutput.getRecords().get(0).getErrors();
|
||||||
|
assertEquals(1, errors.size());
|
||||||
|
assertThat(errors.get(0).getMessage())
|
||||||
|
.contains("You do not have permission")
|
||||||
|
.contains("without a value")
|
||||||
|
.contains("Only Writable By");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -29,12 +29,18 @@ import java.util.Objects;
|
|||||||
import com.kingsrook.qqq.backend.core.BaseTest;
|
import com.kingsrook.qqq.backend.core.BaseTest;
|
||||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
|
import com.kingsrook.qqq.backend.core.model.actions.tables.update.UpdateOutput;
|
||||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.statusmessages.QErrorMessage;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||||
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
import com.kingsrook.qqq.backend.core.utils.TestUtils;
|
||||||
import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
|
import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
|
||||||
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
|
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
|
||||||
@ -492,7 +498,7 @@ class UpdateActionTest extends BaseTest
|
|||||||
updateInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
|
updateInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
|
||||||
updateInput.setRecords(List.of(new QRecord().withValue("id", 20).withValue("sku", "BASIC3")));
|
updateInput.setRecords(List.of(new QRecord().withValue("id", 20).withValue("sku", "BASIC3")));
|
||||||
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
|
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
|
||||||
assertEquals("No record was found to update for Id = 20", updateOutput.getRecords().get(0).getErrors().get(0).getMessage());
|
assertTrue(updateOutput.getRecords().get(0).getErrors().stream().anyMatch(em -> em.getMessage().equals("No record was found to update for Id = 20")));
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -504,7 +510,7 @@ class UpdateActionTest extends BaseTest
|
|||||||
updateInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
|
updateInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
|
||||||
updateInput.setRecords(List.of(new QRecord().withValue("id", 10).withValue("orderId", 2).withValue("sku", "BASIC3")));
|
updateInput.setRecords(List.of(new QRecord().withValue("id", 10).withValue("orderId", 2).withValue("sku", "BASIC3")));
|
||||||
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
|
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
|
||||||
assertEquals("You do not have permission to update this record - the referenced Order was not found.", updateOutput.getRecords().get(0).getErrors().get(0).getMessage());
|
assertTrue(updateOutput.getRecords().get(0).getErrors().stream().anyMatch(em -> em.getMessage().equals("You do not have permission to update this record - the referenced Order was not found.")));
|
||||||
}
|
}
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////
|
||||||
@ -528,7 +534,7 @@ class UpdateActionTest extends BaseTest
|
|||||||
updateInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
|
updateInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
|
||||||
updateInput.setRecords(List.of(new QRecord().withValue("id", 200).withValue("key", "updatedKey")));
|
updateInput.setRecords(List.of(new QRecord().withValue("id", 200).withValue("key", "updatedKey")));
|
||||||
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
|
UpdateOutput updateOutput = new UpdateAction().execute(updateInput);
|
||||||
assertEquals("No record was found to update for Id = 200", updateOutput.getRecords().get(0).getErrors().get(0).getMessage());
|
assertTrue(updateOutput.getRecords().get(0).getErrors().stream().anyMatch(em -> em.getMessage().equals("No record was found to update for Id = 200")));
|
||||||
}
|
}
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
@ -706,4 +712,79 @@ class UpdateActionTest extends BaseTest
|
|||||||
assertEquals(1, TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER).stream().filter(r -> r.getValue("storeId") == null).count());
|
assertEquals(1, TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER).stream().filter(r -> r.getValue("storeId") == null).count());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
@Test
|
||||||
|
void testSecurityLockWriteScope() throws QException
|
||||||
|
{
|
||||||
|
TestUtils.updatePersonMemoryTableInContextWithWritableByWriteLockAndInsert3TestRecords();
|
||||||
|
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// try to update them all 1, 2, and 3. 2 should be blocked, because it has a writable-By that isn't in our session //
|
||||||
|
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
{
|
||||||
|
QContext.getQSession().setSecurityKeyValues(MapBuilder.of("writableBy", ListBuilder.of("jdoe")));
|
||||||
|
UpdateOutput updateOutput = new UpdateAction().execute(new UpdateInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withRecords(List.of(
|
||||||
|
new QRecord().withValue("id", 1).withValue("lastName", "Kelkhoff"),
|
||||||
|
new QRecord().withValue("id", 2).withValue("lastName", "Chamberlain"),
|
||||||
|
new QRecord().withValue("id", 3).withValue("lastName", "Maes")
|
||||||
|
)));
|
||||||
|
|
||||||
|
List<QRecord> errorRecords = updateOutput.getRecords().stream().filter(r -> CollectionUtils.nullSafeHasContents(r.getErrors())).toList();
|
||||||
|
assertEquals(1, errorRecords.size());
|
||||||
|
assertEquals(2, errorRecords.get(0).getValueInteger("id"));
|
||||||
|
assertThat(errorRecords.get(0).getErrors().get(0).getMessage())
|
||||||
|
.contains("You do not have permission")
|
||||||
|
.contains("kmarsh")
|
||||||
|
.contains("Only Writable By");
|
||||||
|
|
||||||
|
assertEquals(2, new CountAction().execute(new CountInput(TestUtils.TABLE_NAME_PERSON_MEMORY)
|
||||||
|
.withFilter(new QQueryFilter(new QFilterCriteria("lastName", QCriteriaOperator.IS_NOT_BLANK)))).getCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// now try to change one of the records to have a different value in the lock-field. Should fail (as it's not in our session) //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
{
|
||||||
|
UpdateOutput updateOutput = new UpdateAction().execute(new UpdateInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withRecords(List.of(
|
||||||
|
new QRecord().withValue("id", 1).withValue("onlyWritableBy", "ecartman"))));
|
||||||
|
|
||||||
|
List<QRecord> errorRecords = updateOutput.getRecords().stream().filter(r -> CollectionUtils.nullSafeHasContents(r.getErrors())).toList();
|
||||||
|
assertEquals(1, errorRecords.size());
|
||||||
|
assertThat(errorRecords.get(0).getErrors().get(0).getMessage())
|
||||||
|
.contains("You do not have permission")
|
||||||
|
.contains("ecartman")
|
||||||
|
.contains("Only Writable By");
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////
|
||||||
|
// add that to our session and confirm we can do that update //
|
||||||
|
///////////////////////////////////////////////////////////////
|
||||||
|
{
|
||||||
|
QContext.getQSession().setSecurityKeyValues(MapBuilder.of("writableBy", ListBuilder.of("jdoe", "ecartman")));
|
||||||
|
UpdateOutput updateOutput = new UpdateAction().execute(new UpdateInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withRecords(List.of(
|
||||||
|
new QRecord().withValue("id", 1).withValue("onlyWritableBy", "ecartman"))));
|
||||||
|
List<QRecord> errorRecords = updateOutput.getRecords().stream().filter(r -> CollectionUtils.nullSafeHasContents(r.getErrors())).toList();
|
||||||
|
assertEquals(0, errorRecords.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// change the null behavior to deny, then try to udpate a record and remove its onlyWritableBy //
|
||||||
|
/////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).getRecordSecurityLocks().get(0).setNullValueBehavior(RecordSecurityLock.NullValueBehavior.DENY);
|
||||||
|
{
|
||||||
|
UpdateOutput updateOutput = new UpdateAction().execute(new UpdateInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withRecords(List.of(
|
||||||
|
new QRecord().withValue("id", 1).withValue("onlyWritableBy", null))));
|
||||||
|
List<QErrorMessage> errors = updateOutput.getRecords().get(0).getErrors();
|
||||||
|
assertEquals(1, errors.size());
|
||||||
|
assertThat(errors.get(0).getMessage())
|
||||||
|
.contains("You do not have permission")
|
||||||
|
.contains("without a value")
|
||||||
|
.contains("Only Writable By");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -33,15 +33,18 @@ import com.kingsrook.qqq.backend.core.actions.dashboard.PersonsByCreateDateBarCh
|
|||||||
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
|
import com.kingsrook.qqq.backend.core.actions.processes.BackendStep;
|
||||||
import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.AddAge;
|
import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.AddAge;
|
||||||
import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.GetAgeStatistics;
|
import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.GetAgeStatistics;
|
||||||
|
import com.kingsrook.qqq.backend.core.actions.tables.CountAction;
|
||||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
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.QueryAction;
|
||||||
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
|
import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction;
|
||||||
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
|
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
|
||||||
|
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||||
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
|
import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter;
|
||||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput;
|
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.processes.RunBackendStepOutput;
|
||||||
|
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
|
||||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||||
@ -110,6 +113,9 @@ import com.kingsrook.qqq.backend.core.processes.implementations.etl.basic.BasicE
|
|||||||
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamed.StreamedETLProcess;
|
import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamed.StreamedETLProcess;
|
||||||
import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep;
|
import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep;
|
||||||
import com.kingsrook.qqq.backend.core.processes.implementations.reports.RunReportForRecordProcess;
|
import com.kingsrook.qqq.backend.core.processes.implementations.reports.RunReportForRecordProcess;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder;
|
||||||
|
import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
|
||||||
/*******************************************************************************
|
/*******************************************************************************
|
||||||
@ -1393,4 +1399,39 @@ public class TestUtils
|
|||||||
))
|
))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*******************************************************************************
|
||||||
|
**
|
||||||
|
*******************************************************************************/
|
||||||
|
public static void updatePersonMemoryTableInContextWithWritableByWriteLockAndInsert3TestRecords() throws QException
|
||||||
|
{
|
||||||
|
QInstance qInstance = QContext.getQInstance();
|
||||||
|
qInstance.addSecurityKeyType(new QSecurityKeyType()
|
||||||
|
.withName("writableBy"));
|
||||||
|
|
||||||
|
QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY);
|
||||||
|
table.withField(new QFieldMetaData("onlyWritableBy", QFieldType.STRING).withLabel("Only Writable By"));
|
||||||
|
table.withRecordSecurityLock(new RecordSecurityLock()
|
||||||
|
.withSecurityKeyType("writableBy")
|
||||||
|
.withFieldName("onlyWritableBy")
|
||||||
|
.withNullValueBehavior(RecordSecurityLock.NullValueBehavior.ALLOW)
|
||||||
|
.withLockScope(RecordSecurityLock.LockScope.WRITE));
|
||||||
|
|
||||||
|
QContext.getQSession().setSecurityKeyValues(MapBuilder.of("writableBy", ListBuilder.of("jdoe", "kmarsh")));
|
||||||
|
|
||||||
|
new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withRecords(List.of(
|
||||||
|
new QRecord().withValue("id", 1).withValue("firstName", "Darin"),
|
||||||
|
new QRecord().withValue("id", 2).withValue("firstName", "Tim").withValue("onlyWritableBy", "kmarsh"),
|
||||||
|
new QRecord().withValue("id", 3).withValue("firstName", "James").withValue("onlyWritableBy", "jdoe")
|
||||||
|
)));
|
||||||
|
|
||||||
|
//////////////////////////////////////////////
|
||||||
|
// make sure we can query for all 3 records //
|
||||||
|
//////////////////////////////////////////////
|
||||||
|
QContext.getQSession().setSecurityKeyValues(MapBuilder.of("writableBy", ListBuilder.of("jdoe")));
|
||||||
|
assertEquals(3, new CountAction().execute(new CountInput(TestUtils.TABLE_NAME_PERSON_MEMORY)).getCount());
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -70,6 +70,7 @@ 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.joins.QJoinMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType;
|
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.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.metadata.tables.QTableMetaData;
|
||||||
import com.kingsrook.qqq.backend.core.model.querystats.QueryStat;
|
import com.kingsrook.qqq.backend.core.model.querystats.QueryStat;
|
||||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||||
@ -387,7 +388,7 @@ public abstract class AbstractRDBMSAction implements QActionInterface
|
|||||||
QQueryFilter securityFilter = new QQueryFilter();
|
QQueryFilter securityFilter = new QQueryFilter();
|
||||||
securityFilter.setBooleanOperator(QQueryFilter.BooleanOperator.AND);
|
securityFilter.setBooleanOperator(QQueryFilter.BooleanOperator.AND);
|
||||||
|
|
||||||
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
|
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks())))
|
||||||
{
|
{
|
||||||
// todo - uh, if it's a RIGHT (or FULL) join, then, this should be isOuter = true, right?
|
// todo - uh, if it's a RIGHT (or FULL) join, then, this should be isOuter = true, right?
|
||||||
boolean isOuter = false;
|
boolean isOuter = false;
|
||||||
@ -407,7 +408,7 @@ public abstract class AbstractRDBMSAction implements QActionInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
QTableMetaData joinTable = instance.getTable(queryJoin.getJoinTable());
|
QTableMetaData joinTable = instance.getTable(queryJoin.getJoinTable());
|
||||||
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(joinTable.getRecordSecurityLocks()))
|
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(joinTable.getRecordSecurityLocks())))
|
||||||
{
|
{
|
||||||
boolean isOuter = queryJoin.getType().equals(QueryJoin.Type.LEFT); // todo full?
|
boolean isOuter = queryJoin.getType().equals(QueryJoin.Type.LEFT); // todo full?
|
||||||
addSubFilterForRecordSecurityLock(instance, session, joinTable, securityFilter, recordSecurityLock, joinsContext, queryJoin.getJoinTableOrItsAlias(), isOuter);
|
addSubFilterForRecordSecurityLock(instance, session, joinTable, securityFilter, recordSecurityLock, joinsContext, queryJoin.getJoinTableOrItsAlias(), isOuter);
|
||||||
@ -1035,7 +1036,7 @@ public abstract class AbstractRDBMSAction implements QActionInterface
|
|||||||
{
|
{
|
||||||
if(table != null)
|
if(table != null)
|
||||||
{
|
{
|
||||||
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
|
for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks())))
|
||||||
{
|
{
|
||||||
for(String joinName : CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()))
|
for(String joinName : CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()))
|
||||||
{
|
{
|
||||||
|
Reference in New Issue
Block a user