Add check for records pre-delete action (for security and better errors); 404s and ids in 207s for bulk update & delete; ignore non-editable fields;

This commit is contained in:
2023-03-31 12:11:12 -05:00
parent 21e3cdd0a5
commit 084630918f
15 changed files with 690 additions and 388 deletions

View File

@ -24,7 +24,11 @@ package com.kingsrook.qqq.backend.core.actions.tables;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.ActionHelper;
import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction;
import com.kingsrook.qqq.backend.core.actions.interfaces.DeleteInterface;
@ -41,12 +45,14 @@ 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.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.audits.AuditLevel;
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.tables.Association;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
/*******************************************************************************
@ -57,6 +63,8 @@ public class DeleteAction
{
private static final QLogger LOG = QLogger.getLogger(DeleteAction.class);
public static final String NOT_FOUND_ERROR_PREFIX = "No record was found to delete";
/*******************************************************************************
@ -68,7 +76,6 @@ public class DeleteAction
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(deleteInput.getBackend());
// todo pre-customization - just get to modify the request?
if(CollectionUtils.nullSafeHasContents(deleteInput.getPrimaryKeys()) && deleteInput.getQueryFilter() != null)
{
@ -92,13 +99,24 @@ public class DeleteAction
}
}
List<QRecord> recordListForAudit = getRecordListForAuditIfNeeded(deleteInput);
List<QRecord> recordListForAudit = getRecordListForAuditIfNeeded(deleteInput);
List<QRecord> recordsWithValidationErrors = validateRecordsExistAndCanBeAccessed(deleteInput, recordListForAudit);
DeleteOutput deleteOutput = deleteInterface.execute(deleteInput);
manageAssociations(deleteInput);
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// merge the backend's output with any validation errors we found (whose ids wouldn't have gotten into the backend delete) //
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
List<QRecord> outputRecordsWithErrors = deleteOutput.getRecordsWithErrors();
if(outputRecordsWithErrors == null)
{
deleteOutput.setRecordsWithErrors(new ArrayList<>());
outputRecordsWithErrors = deleteOutput.getRecordsWithErrors();
}
// todo post-customization - can do whatever w/ the result if you want
outputRecordsWithErrors.addAll(recordsWithValidationErrors);
manageAssociations(deleteInput);
new DMLAuditAction().execute(new DMLAuditInput().withTableActionInput(deleteInput).withRecordList(recordListForAudit));
@ -188,6 +206,84 @@ public class DeleteAction
/*******************************************************************************
** Note - the "can be accessed" part of this method name - it implies that
** records that you can't see because of security - that they won't be found
** by the query here, so it's the same to you as if they don't exist at all!
**
** This method, if it finds any missing records, will:
** - remove those ids from the deleteInput
** - create a QRecord with that id and a not-found error message.
*******************************************************************************/
private List<QRecord> validateRecordsExistAndCanBeAccessed(DeleteInput deleteInput, List<QRecord> oldRecordList) throws QException
{
List<QRecord> recordsWithErrors = new ArrayList<>();
QTableMetaData table = deleteInput.getTable();
QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField());
Set<Serializable> primaryKeysToRemoveFromInput = new HashSet<>();
List<List<Serializable>> pages = CollectionUtils.getPages(deleteInput.getPrimaryKeys(), 1000);
for(List<Serializable> page : pages)
{
List<Serializable> primaryKeysToLookup = new ArrayList<>();
for(Serializable primaryKeyValue : page)
{
if(primaryKeyValue != null)
{
primaryKeysToLookup.add(primaryKeyValue);
}
}
Map<Serializable, QRecord> lookedUpRecords = new HashMap<>();
if(CollectionUtils.nullSafeHasContents(oldRecordList))
{
for(QRecord record : oldRecordList)
{
lookedUpRecords.put(record.getValue(table.getPrimaryKeyField()), record);
}
}
else if(!primaryKeysToLookup.isEmpty())
{
QueryInput queryInput = new QueryInput();
queryInput.setTableName(table.getName());
queryInput.setFilter(new QQueryFilter(new QFilterCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, primaryKeysToLookup)));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
for(QRecord record : queryOutput.getRecords())
{
lookedUpRecords.put(record.getValue(table.getPrimaryKeyField()), record);
}
}
for(Serializable primaryKeyValue : page)
{
primaryKeyValue = ValueUtils.getValueAsFieldType(primaryKeyField.getType(), primaryKeyValue);
if(!lookedUpRecords.containsKey(primaryKeyValue))
{
QRecord recordWithError = new QRecord();
recordsWithErrors.add(recordWithError);
recordWithError.setValue(primaryKeyField.getName(), primaryKeyValue);
recordWithError.addError(NOT_FOUND_ERROR_PREFIX + " for " + primaryKeyField.getLabel() + " = " + primaryKeyValue);
primaryKeysToRemoveFromInput.add(primaryKeyValue);
}
}
/////////////////////////////////////////////////////////////////
// do one mass removal of any bad keys from the input key list //
/////////////////////////////////////////////////////////////////
if(!primaryKeysToRemoveFromInput.isEmpty())
{
deleteInput.getPrimaryKeys().removeAll(primaryKeysToRemoveFromInput);
primaryKeysToRemoveFromInput.clear();
}
}
return (recordsWithErrors);
}
/*******************************************************************************
** For an implementation that doesn't support a queryFilter as its input,
** but a scenario where a query filter was passed in - run the query, to

View File

@ -72,6 +72,8 @@ public class UpdateAction
{
private static final QLogger LOG = QLogger.getLogger(UpdateAction.class);
public static final String NOT_FOUND_ERROR_PREFIX = "No record was found to update";
/*******************************************************************************
@ -193,7 +195,7 @@ public class UpdateAction
if(!lookedUpRecords.containsKey(value))
{
record.addError("No record was found to update for " + primaryKeyField.getLabel() + " = " + value);
record.addError(NOT_FOUND_ERROR_PREFIX + " for " + primaryKeyField.getLabel() + " = " + value);
}
}
}

View File

@ -27,6 +27,7 @@ import java.util.List;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.utils.collections.MutableList;
/*******************************************************************************
@ -101,7 +102,10 @@ public class DeleteInput extends AbstractTableActionInput
*******************************************************************************/
public void setPrimaryKeys(List<Serializable> primaryKeys)
{
this.primaryKeys = primaryKeys;
///////////////////////////////////////////////////////////////////////////////////////////////
// the action may edit this list (e.g., to remove keys w/ errors), so wrap it in MutableList //
///////////////////////////////////////////////////////////////////////////////////////////////
this.primaryKeys = new MutableList<>(primaryKeys);
}
@ -112,7 +116,7 @@ public class DeleteInput extends AbstractTableActionInput
*******************************************************************************/
public DeleteInput withPrimaryKeys(List<Serializable> primaryKeys)
{
this.primaryKeys = primaryKeys;
setPrimaryKeys(primaryKeys);
return (this);
}

View File

@ -348,7 +348,7 @@ public class BackendQueryFilterUtils
}
}
if(!criterion.getValues().contains(value))
if(value == null || !criterion.getValues().contains(value))
{
return (false);
}

View File

@ -27,10 +27,15 @@ import java.util.Map;
import java.util.function.Supplier;
@SuppressWarnings({ "checkstyle:javadoc", "DanglingJavadoc" })
/*******************************************************************************
** Map.of is "great", but annoying because it makes unmodifiable maps, and it
** NPE's on nulls... So, replace it with this, which returns HashMaps, which
** "don't suck"
** NPE's on nulls... So, replace it with this, which returns HashMaps (or maps
** of the type you choose).
**
** Can use it 2 ways:
** MapBuilder.of(key, value, key2, value2, ...) => Map (a HashMap)
** MapBuilder.<KeyType ValueType>of(SomeMap::new).with(key, value).with(key2, value2)...build() => SomeMap (the type you specify)
*******************************************************************************/
public class MapBuilder<K, V>
{