mirror of
https://github.com/Kingsrook/qqq.git
synced 2025-07-18 05:01:07 +00:00
Checking record security locks that are more than 1 join away.
This commit is contained in:
@ -52,6 +52,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperat
|
||||
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.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
|
||||
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.fields.QFieldMetaData;
|
||||
@ -68,6 +69,7 @@ import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ListingHash;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@ -101,6 +103,11 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
validateSecurityFields(insertInput);
|
||||
|
||||
InsertOutput insertOutput = qModule.getInsertInterface().execute(insertInput);
|
||||
List<String> errors = insertOutput.getRecords().stream().flatMap(r -> r.getErrors().stream()).toList();
|
||||
if(CollectionUtils.nullSafeHasContents(errors))
|
||||
{
|
||||
LOG.warn("Errors in insertAction", logPair("tableName", table.getName()), logPair("errorCount", errors.size()), errors.size() < 10 ? logPair("errors", errors) : logPair("first10Errors", errors.subList(0, 10)));
|
||||
}
|
||||
|
||||
manageAssociations(table, insertOutput.getRecords());
|
||||
|
||||
@ -209,8 +216,9 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// else look for the joined record - if it isn't found, assume a fail - else validate security value if found //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QJoinMetaData join = QContext.getQInstance().getJoin(recordSecurityLock.getJoinNameChain().get(0)); // todo - many joins...
|
||||
QTableMetaData joinTable = QContext.getQInstance().getTable(join.getLeftTable());
|
||||
QJoinMetaData leftMostJoin = QContext.getQInstance().getJoin(recordSecurityLock.getJoinNameChain().get(0));
|
||||
QJoinMetaData rightMostJoin = QContext.getQInstance().getJoin(recordSecurityLock.getJoinNameChain().get(recordSecurityLock.getJoinNameChain().size() - 1));
|
||||
QTableMetaData leftMostJoinTable = QContext.getQInstance().getTable(leftMostJoin.getLeftTable());
|
||||
|
||||
for(List<QRecord> inputRecordPage : CollectionUtils.getPages(insertInput.getRecords(), 500))
|
||||
{
|
||||
@ -219,10 +227,21 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
// query will be like (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QueryInput queryInput = new QueryInput();
|
||||
queryInput.setTableName(join.getLeftTable());
|
||||
queryInput.setTableName(leftMostJoin.getLeftTable());
|
||||
QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR);
|
||||
queryInput.setFilter(filter);
|
||||
|
||||
for(String joinName : recordSecurityLock.getJoinNameChain())
|
||||
{
|
||||
///////////////////////////////////////
|
||||
// we don't need the right-most join //
|
||||
///////////////////////////////////////
|
||||
if(!joinName.equals(rightMostJoin.getName()))
|
||||
{
|
||||
queryInput.withQueryJoin(new QueryJoin().withJoinMetaData(QContext.getQInstance().getJoin(joinName)).withSelect(true));
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// foreach input record (in this page), put it in a listing hash, with key = list of join-values //
|
||||
// e.g., (17,47)=(QRecord1), (18,48)=(QRecord2,QRecord3) //
|
||||
@ -235,14 +254,14 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
List<Serializable> inputRecordJoinValues = new ArrayList<>();
|
||||
QQueryFilter subFilter = new QQueryFilter();
|
||||
|
||||
for(JoinOn joinOn : join.getJoinOns())
|
||||
for(JoinOn joinOn : rightMostJoin.getJoinOns())
|
||||
{
|
||||
Serializable inputRecordValue = inputRecord.getValue(joinOn.getRightField());
|
||||
inputRecordJoinValues.add(inputRecordValue);
|
||||
|
||||
subFilter.addCriteria(inputRecordValue == null
|
||||
? new QFilterCriteria(joinOn.getLeftField(), QCriteriaOperator.IS_BLANK)
|
||||
: new QFilterCriteria(joinOn.getLeftField(), QCriteriaOperator.EQUALS, inputRecordValue));
|
||||
? new QFilterCriteria(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField(), QCriteriaOperator.IS_BLANK)
|
||||
: new QFilterCriteria(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField(), QCriteriaOperator.EQUALS, inputRecordValue));
|
||||
}
|
||||
|
||||
if(!inputRecordMapByJoinFields.containsKey(inputRecordJoinValues))
|
||||
@ -265,11 +284,16 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
for(QRecord joinRecord : queryOutput.getRecords())
|
||||
{
|
||||
List<Serializable> joinRecordValues = new ArrayList<>();
|
||||
for(JoinOn joinOn : join.getJoinOns())
|
||||
for(JoinOn joinOn : rightMostJoin.getJoinOns())
|
||||
{
|
||||
Serializable inputRecordValue = joinRecord.getValue(joinOn.getLeftField());
|
||||
joinRecordValues.add(inputRecordValue);
|
||||
Serializable joinValue = joinRecord.getValue(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField());
|
||||
if(joinValue == null && joinRecord.getValues().keySet().stream().anyMatch(n -> !n.contains(".")))
|
||||
{
|
||||
joinValue = joinRecord.getValue(joinOn.getLeftField());
|
||||
}
|
||||
joinRecordValues.add(joinValue);
|
||||
}
|
||||
|
||||
joinRecordMapByJoinFields.put(joinRecordValues, joinRecord);
|
||||
}
|
||||
|
||||
@ -286,8 +310,12 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
QRecord joinRecord = joinRecordMapByJoinFields.get(inputRecordJoinValues);
|
||||
|
||||
String fieldName = recordSecurityLock.getFieldName().replaceFirst(".*\\.", "");
|
||||
QFieldMetaData field = joinTable.getField(fieldName);
|
||||
QFieldMetaData field = leftMostJoinTable.getField(fieldName);
|
||||
Serializable recordSecurityValue = joinRecord.getValue(fieldName);
|
||||
if(recordSecurityValue == null && joinRecord.getValues().keySet().stream().anyMatch(n -> n.contains(".")))
|
||||
{
|
||||
recordSecurityValue = joinRecord.getValue(recordSecurityLock.getFieldName());
|
||||
}
|
||||
|
||||
for(QRecord inputRecord : inputRecords)
|
||||
{
|
||||
@ -298,7 +326,7 @@ public class InsertAction extends AbstractQActionFunction<InsertInput, InsertOut
|
||||
{
|
||||
for(QRecord inputRecord : inputRecords)
|
||||
{
|
||||
inputRecord.addError("You do not have permission to insert this record - the referenced " + joinTable.getLabel() + " was not found.");
|
||||
inputRecord.addError("You do not have permission to insert this record - the referenced " + leftMostJoinTable.getLabel() + " was not found.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ package com.kingsrook.qqq.backend.core.instances;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -496,6 +497,7 @@ public class QInstanceValidator
|
||||
{
|
||||
String prefix = "Table " + table.getName() + " ";
|
||||
|
||||
RECORD_SECURITY_LOCKS_LOOP:
|
||||
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks()))
|
||||
{
|
||||
String securityKeyTypeName = recordSecurityLock.getSecurityKeyType();
|
||||
@ -522,21 +524,61 @@ public class QInstanceValidator
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
if(assertCondition(StringUtils.hasContent(fieldName), prefix + "is missing a fieldName") && !hasAnyBadJoins)
|
||||
{
|
||||
List<QueryJoin> joins = new ArrayList<>();
|
||||
for(String joinName : CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()))
|
||||
if(fieldName.contains("."))
|
||||
{
|
||||
QJoinMetaData join = qInstance.getJoin(joinName);
|
||||
if(join.getLeftTable().equals(table.getName()))
|
||||
if(assertCondition(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()), prefix + "field name " + fieldName + " looks like a join (has a dot), but no joinNameChain was given."))
|
||||
{
|
||||
joins.add(new QueryJoin(join));
|
||||
}
|
||||
else if(join.getRightTable().equals(table.getName()))
|
||||
{
|
||||
joins.add(new QueryJoin(join.flip()));
|
||||
List<QueryJoin> joins = new ArrayList<>();
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// ok - so - the join name chain is going to be like this: //
|
||||
// for a table: orderLineItemExtrinsic (that's 2 away from order, where the security field is): //
|
||||
// - securityFieldName = order.clientId //
|
||||
// - joinNameChain = orderJoinOrderLineItem, orderLineItemJoinOrderLineItemExtrinsic //
|
||||
// so - to navigate from the table to the security field, we need to reverse the joinNameChain, //
|
||||
// and step (via tmpTable variable) back to the securityField //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
ArrayList<String> joinNameChain = new ArrayList<>(CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()));
|
||||
Collections.reverse(joinNameChain);
|
||||
|
||||
QTableMetaData tmpTable = table;
|
||||
|
||||
for(String joinName : joinNameChain)
|
||||
{
|
||||
QJoinMetaData join = qInstance.getJoin(joinName);
|
||||
if(join == null)
|
||||
{
|
||||
errors.add(prefix + "joinNameChain contained an unrecognized join: " + joinName);
|
||||
continue RECORD_SECURITY_LOCKS_LOOP;
|
||||
}
|
||||
|
||||
if(join.getLeftTable().equals(tmpTable.getName()))
|
||||
{
|
||||
joins.add(new QueryJoin(join));
|
||||
tmpTable = qInstance.getTable(join.getRightTable());
|
||||
}
|
||||
else if(join.getRightTable().equals(tmpTable.getName()))
|
||||
{
|
||||
joins.add(new QueryJoin(join.flip()));
|
||||
tmpTable = qInstance.getTable(join.getLeftTable());
|
||||
}
|
||||
else
|
||||
{
|
||||
errors.add(prefix + "joinNameChain could not be followed through join: " + joinName);
|
||||
continue RECORD_SECURITY_LOCKS_LOOP;
|
||||
}
|
||||
}
|
||||
|
||||
assertCondition(findField(qInstance, table, joins, fieldName), prefix + "has an unrecognized fieldName: " + fieldName);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(assertCondition(CollectionUtils.nullSafeIsEmpty(recordSecurityLock.getJoinNameChain()), prefix + "field name " + fieldName + " does not look like a join (does not have a dot), but a joinNameChain was given."))
|
||||
{
|
||||
assertNoException(() -> table.getField(fieldName), prefix + "has an unrecognized fieldName: " + fieldName);
|
||||
}
|
||||
}
|
||||
|
||||
assertCondition(findField(qInstance, table, joins, fieldName), prefix + "has an unrecognized fieldName: " + fieldName);
|
||||
}
|
||||
|
||||
assertCondition(recordSecurityLock.getNullValueBehavior() != null, prefix + "is missing a nullValueBehavior");
|
||||
|
@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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.actions.tables.query;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Specialization of a QueryJoin, for when the join is added to the query,
|
||||
** not by the caller, but by the framework, because it is implicitly needed
|
||||
** to provide a security lock.
|
||||
*******************************************************************************/
|
||||
public class ImplicitQueryJoinForSecurityLock extends QueryJoin
|
||||
{
|
||||
|
||||
/*******************************************************************************
|
||||
** package-private constructor - to make so outside users should not create
|
||||
** instances.
|
||||
*******************************************************************************/
|
||||
ImplicitQueryJoinForSecurityLock()
|
||||
{
|
||||
}
|
||||
|
||||
}
|
@ -22,6 +22,8 @@
|
||||
package com.kingsrook.qqq.backend.core.model.actions.tables.query;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@ -75,25 +77,46 @@ public class JoinsContext
|
||||
///////////////////////////////////////////////////////////////
|
||||
for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(instance.getTable(tableName).getRecordSecurityLocks()))
|
||||
{
|
||||
for(String joinName : CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()))
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// ok - so - the join name chain is going to be like this: //
|
||||
// for a table: orderLineItemExtrinsic (that's 2 away from order, where the security field is): //
|
||||
// - securityFieldName = order.clientId //
|
||||
// - joinNameChain = orderJoinOrderLineItem, orderLineItemJoinOrderLineItemExtrinsic //
|
||||
// so - to navigate from the table to the security field, we need to reverse the joinNameChain, //
|
||||
// and step (via tmpTable variable) back to the securityField //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
ArrayList<String> joinNameChain = new ArrayList<>(CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain()));
|
||||
Collections.reverse(joinNameChain);
|
||||
|
||||
QTableMetaData tmpTable = instance.getTable(mainTableName);
|
||||
|
||||
for(String joinName : joinNameChain)
|
||||
{
|
||||
if(this.queryJoins.stream().anyMatch(qj -> qj.getJoinMetaData().getName().equals(joinName)))
|
||||
if(this.queryJoins.stream().anyMatch(queryJoin ->
|
||||
{
|
||||
///////////////////////////////////////////////////////
|
||||
// we're good - we're already joining on this table! //
|
||||
///////////////////////////////////////////////////////
|
||||
QJoinMetaData joinMetaData = Objects.requireNonNullElseGet(queryJoin.getJoinMetaData(), () -> findJoinMetaData(instance, tableName, queryJoin.getJoinTable()));
|
||||
return (joinMetaData != null && Objects.equals(joinMetaData.getName(), joinName));
|
||||
}))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
QJoinMetaData join = instance.getJoin(joinName);
|
||||
if(join.getLeftTable().equals(tmpTable.getName()))
|
||||
{
|
||||
QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock().withJoinMetaData(join).withType(QueryJoin.Type.INNER);
|
||||
this.queryJoins.add(queryJoin); // todo something else with aliases? probably.
|
||||
tmpTable = instance.getTable(join.getRightTable());
|
||||
}
|
||||
else if(join.getRightTable().equals(tmpTable.getName()))
|
||||
{
|
||||
QueryJoin queryJoin = new ImplicitQueryJoinForSecurityLock().withJoinMetaData(join.flip()).withType(QueryJoin.Type.INNER);
|
||||
this.queryJoins.add(queryJoin); // todo something else with aliases? probably.
|
||||
tmpTable = instance.getTable(join.getLeftTable());
|
||||
}
|
||||
else
|
||||
{
|
||||
QJoinMetaData join = instance.getJoin(joinName);
|
||||
if(tableName.equals(join.getRightTable()))
|
||||
{
|
||||
join = join.flip();
|
||||
}
|
||||
|
||||
QueryJoin queryJoin = new QueryJoin().withJoinMetaData(join).withType(QueryJoin.Type.INNER);
|
||||
this.queryJoins.add(queryJoin); // todo something else with aliases? probably.
|
||||
processQueryJoin(queryJoin);
|
||||
throw (new QException("Error adding security lock joins to query - table name [" + tmpTable.getName() + "] not found in join [" + joinName + "]"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -297,6 +320,69 @@ public class JoinsContext
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
public QJoinMetaData findJoinMetaData(QInstance instance, String baseTableName, String joinTableName)
|
||||
{
|
||||
List<QJoinMetaData> matches = new ArrayList<>();
|
||||
if(baseTableName != null)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
// if query specified a left-table, look for a join between left & right //
|
||||
///////////////////////////////////////////////////////////////////////////
|
||||
for(QJoinMetaData join : instance.getJoins().values())
|
||||
{
|
||||
if(join.getLeftTable().equals(baseTableName) && join.getRightTable().equals(joinTableName))
|
||||
{
|
||||
matches.add(join);
|
||||
}
|
||||
|
||||
//////////////////////////////
|
||||
// look in both directions! //
|
||||
//////////////////////////////
|
||||
if(join.getRightTable().equals(baseTableName) && join.getLeftTable().equals(joinTableName))
|
||||
{
|
||||
matches.add(join.flip());
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
// if query didn't specify a left-table, then look for any join to the right table //
|
||||
/////////////////////////////////////////////////////////////////////////////////////
|
||||
for(QJoinMetaData join : instance.getJoins().values())
|
||||
{
|
||||
if(join.getRightTable().equals(joinTableName) && this.hasTable(join.getLeftTable()))
|
||||
{
|
||||
matches.add(join);
|
||||
}
|
||||
|
||||
//////////////////////////////
|
||||
// look in both directions! //
|
||||
//////////////////////////////
|
||||
if(join.getLeftTable().equals(joinTableName) && this.hasTable(join.getRightTable()))
|
||||
{
|
||||
matches.add(join.flip());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(matches.size() == 1)
|
||||
{
|
||||
return (matches.get(0));
|
||||
}
|
||||
else if(matches.size() > 1)
|
||||
{
|
||||
throw (new RuntimeException("More than 1 join was found between [" + baseTableName + "] and [" + joinTableName + "]. Specify which one in your QueryJoin."));
|
||||
}
|
||||
|
||||
return (null);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -50,6 +50,7 @@ public class QJoinMetaData
|
||||
public QJoinMetaData flip()
|
||||
{
|
||||
return (new QJoinMetaData()
|
||||
.withName(name) // does this need to be different?, e.g., + "Flipped"?
|
||||
.withLeftTable(rightTable)
|
||||
.withRightTable(leftTable)
|
||||
.withType(type.flip())
|
||||
|
@ -28,13 +28,18 @@ import java.util.List;
|
||||
/*******************************************************************************
|
||||
** Define (for a table) a lock that applies to records in the table - e.g.,
|
||||
** a key type, and a field that has values for that key.
|
||||
*
|
||||
**
|
||||
** Here's an example of how the joinNameChain should be set up:
|
||||
** given a table: orderLineItemExtrinsic (that's 2 away from order, where the security field is):
|
||||
** - 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.
|
||||
*******************************************************************************/
|
||||
public class RecordSecurityLock
|
||||
{
|
||||
private String securityKeyType;
|
||||
private String fieldName;
|
||||
private List<String> joinNameChain; // todo - add validation in validator!!
|
||||
private List<String> joinNameChain;
|
||||
private NullValueBehavior nullValueBehavior = NullValueBehavior.DENY;
|
||||
|
||||
|
||||
|
@ -24,19 +24,24 @@ package com.kingsrook.qqq.backend.core.modules.backend.implementations.memory;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.delete.DeleteInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin;
|
||||
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.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.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.modules.backend.implementations.utils.BackendQueryFilterUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
|
||||
@ -134,10 +139,15 @@ public class MemoryRecordStore
|
||||
{
|
||||
incrementStatistic(input);
|
||||
|
||||
Map<Serializable, QRecord> tableData = getTableData(input.getTable());
|
||||
List<QRecord> records = new ArrayList<>();
|
||||
Collection<QRecord> tableData = getTableData(input.getTable()).values();
|
||||
List<QRecord> records = new ArrayList<>();
|
||||
|
||||
for(QRecord qRecord : tableData.values())
|
||||
if(CollectionUtils.nullSafeHasContents(input.getQueryJoins()))
|
||||
{
|
||||
tableData = buildJoinCrossProduct(input);
|
||||
}
|
||||
|
||||
for(QRecord qRecord : tableData)
|
||||
{
|
||||
boolean recordMatches = BackendQueryFilterUtils.doesRecordMatch(input.getFilter(), qRecord);
|
||||
|
||||
@ -155,6 +165,87 @@ public class MemoryRecordStore
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private Collection<QRecord> buildJoinCrossProduct(QueryInput input)
|
||||
{
|
||||
List<QRecord> crossProduct = new ArrayList<>();
|
||||
QTableMetaData leftTable = input.getTable();
|
||||
for(QRecord record : getTableData(leftTable).values())
|
||||
{
|
||||
QRecord productRecord = new QRecord();
|
||||
addRecordToProduct(productRecord, record, leftTable.getName());
|
||||
crossProduct.add(productRecord);
|
||||
}
|
||||
|
||||
for(QueryJoin queryJoin : input.getQueryJoins())
|
||||
{
|
||||
QTableMetaData nextTable = QContext.getQInstance().getTable(queryJoin.getJoinTable());
|
||||
Collection<QRecord> nextTableRecords = getTableData(nextTable).values();
|
||||
|
||||
List<QRecord> nextLevelProduct = new ArrayList<>();
|
||||
for(QRecord productRecord : crossProduct)
|
||||
{
|
||||
boolean matchFound = false;
|
||||
for(QRecord nextTableRecord : nextTableRecords)
|
||||
{
|
||||
if(joinMatches(productRecord, nextTableRecord, queryJoin))
|
||||
{
|
||||
QRecord joinRecord = new QRecord(productRecord);
|
||||
addRecordToProduct(joinRecord, nextTableRecord, queryJoin.getJoinTableOrItsAlias());
|
||||
nextLevelProduct.add(joinRecord);
|
||||
matchFound = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(!matchFound)
|
||||
{
|
||||
// todo - Left & Right joins
|
||||
}
|
||||
}
|
||||
|
||||
crossProduct = nextLevelProduct;
|
||||
}
|
||||
|
||||
return (crossProduct);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private boolean joinMatches(QRecord productRecord, QRecord nextTableRecord, QueryJoin queryJoin)
|
||||
{
|
||||
for(JoinOn joinOn : queryJoin.getJoinMetaData().getJoinOns())
|
||||
{
|
||||
Serializable leftValue = productRecord.getValue(queryJoin.getBaseTableOrAlias() + "." + joinOn.getLeftField());
|
||||
Serializable rightValue = nextTableRecord.getValue(joinOn.getRightField());
|
||||
if(!Objects.equals(leftValue, rightValue))
|
||||
{
|
||||
return (false);
|
||||
}
|
||||
}
|
||||
|
||||
return (true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
private void addRecordToProduct(QRecord productRecord, QRecord record, String tableNameOrAlias)
|
||||
{
|
||||
for(Map.Entry<String, Serializable> entry : record.getValues().entrySet())
|
||||
{
|
||||
productRecord.withValue(tableNameOrAlias + "." + entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
|
@ -27,6 +27,7 @@ import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria;
|
||||
@ -71,6 +72,21 @@ public class BackendQueryFilterUtils
|
||||
{
|
||||
String fieldName = criterion.getFieldName();
|
||||
Serializable value = qRecord.getValue(fieldName);
|
||||
if(value == null)
|
||||
{
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// if the value isn't in the record - check, if it looks like a table.fieldName, but none of the //
|
||||
// field names in the record are fully qualified, then just use the field-name portion... //
|
||||
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
if(fieldName.contains("."))
|
||||
{
|
||||
Map<String, Serializable> values = qRecord.getValues();
|
||||
if(values.keySet().stream().noneMatch(n -> n.contains(".")))
|
||||
{
|
||||
value = qRecord.getValue(fieldName.substring(fieldName.indexOf(".") + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean criterionMatches = doesCriteriaMatch(criterion, fieldName, value);
|
||||
|
||||
|
@ -291,7 +291,130 @@ class InsertActionTest extends BaseTest
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testInsertSecurityJoins() throws QException
|
||||
void testInsertMultiLevelSecurityJoins() throws QException
|
||||
{
|
||||
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE, 1);
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
// null value in the foreign key to the join-table that provides the security value //
|
||||
//////////////////////////////////////////////////////////////////////////////////////
|
||||
{
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
|
||||
insertInput.setRecords(List.of(new QRecord().withValue("lineItemId", null).withValue("key", "kidsCanCallYou").withValue("value", "HoJu")));
|
||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
||||
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(0).getErrors().get(0));
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// value in the foreign key to the join-table that provides the security value, but the referenced record isn't found //
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
{
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
|
||||
insertInput.setRecords(List.of(new QRecord().withValue("lineItemId", 1701).withValue("key", "kidsCanCallYou").withValue("value", "HoJu")));
|
||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
||||
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(0).getErrors().get(0));
|
||||
}
|
||||
|
||||
{
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// insert an order and lineItem with storeId=2 - then, reset our session to only have storeId=1 in it - and try to insert an order-line referencing that order. //
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
QContext.getQSession().withSecurityKeyValues(new HashMap<>());
|
||||
QContext.getQSession().withSecurityKeyValues(TestUtils.SECURITY_KEY_TYPE_STORE, List.of(2));
|
||||
InsertInput insertOrderInput = new InsertInput();
|
||||
insertOrderInput.setTableName(TestUtils.TABLE_NAME_ORDER);
|
||||
insertOrderInput.setRecords(List.of(new QRecord().withValue("id", 42).withValue("storeId", 2)));
|
||||
InsertOutput insertOrderOutput = new InsertAction().execute(insertOrderInput);
|
||||
assertEquals(42, insertOrderOutput.getRecords().get(0).getValueInteger("id"));
|
||||
|
||||
InsertInput insertLineItemInput = new InsertInput();
|
||||
insertLineItemInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
|
||||
insertLineItemInput.setRecords(List.of(new QRecord().withValue("id", 4200).withValue("orderId", 42).withValue("sku", "BASIC1").withValue("quantity", 24)));
|
||||
InsertOutput insertLineItemOutput = new InsertAction().execute(insertLineItemInput);
|
||||
assertEquals(4200, insertLineItemOutput.getRecords().get(0).getValueInteger("id"));
|
||||
|
||||
QContext.getQSession().withSecurityKeyValues(new HashMap<>());
|
||||
QContext.getQSession().withSecurityKeyValues(TestUtils.SECURITY_KEY_TYPE_STORE, List.of(1));
|
||||
InsertInput insertLineItemExtrinsicInput = new InsertInput();
|
||||
insertLineItemExtrinsicInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
|
||||
insertLineItemExtrinsicInput.setRecords(List.of(new QRecord().withValue("lineItemId", 4200).withValue("key", "kidsCanCallYou").withValue("value", "HoJu")));
|
||||
InsertOutput insertLineItemExtrinsicOutput = new InsertAction().execute(insertLineItemExtrinsicInput);
|
||||
assertEquals("You do not have permission to insert this record.", insertLineItemExtrinsicOutput.getRecords().get(0).getErrors().get(0));
|
||||
}
|
||||
|
||||
{
|
||||
QContext.getQSession().withSecurityKeyValues(new HashMap<>());
|
||||
QContext.getQSession().withSecurityKeyValues(TestUtils.SECURITY_KEY_TYPE_STORE, List.of(1));
|
||||
InsertInput insertOrderInput = new InsertInput();
|
||||
insertOrderInput.setTableName(TestUtils.TABLE_NAME_ORDER);
|
||||
insertOrderInput.setRecords(List.of(new QRecord().withValue("id", 47).withValue("storeId", 1)));
|
||||
InsertOutput insertOrderOutput = new InsertAction().execute(insertOrderInput);
|
||||
assertEquals(47, insertOrderOutput.getRecords().get(0).getValueInteger("id"));
|
||||
|
||||
InsertInput insertLineItemInput = new InsertInput();
|
||||
insertLineItemInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM);
|
||||
insertLineItemInput.setRecords(List.of(new QRecord().withValue("id", 4700).withValue("orderId", 47).withValue("sku", "BASIC1").withValue("quantity", 74)));
|
||||
InsertOutput insertLineItemOutput = new InsertAction().execute(insertLineItemInput);
|
||||
assertEquals(4700, insertLineItemOutput.getRecords().get(0).getValueInteger("id"));
|
||||
|
||||
///////////////////////////////////////////////////////
|
||||
// combine all the above, plus one record that works //
|
||||
///////////////////////////////////////////////////////
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
|
||||
insertInput.setRecords(List.of(
|
||||
new QRecord().withValue("lineItemId", null).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
|
||||
new QRecord().withValue("lineItemId", 1701).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
|
||||
new QRecord().withValue("lineItemId", 4200).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
|
||||
new QRecord().withValue("lineItemId", 4700).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu")
|
||||
));
|
||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
||||
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(0).getErrors().get(0));
|
||||
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(1).getErrors().get(0));
|
||||
assertEquals("You do not have permission to insert this record.", insertOutput.getRecords().get(2).getErrors().get(0));
|
||||
assertEquals(0, insertOutput.getRecords().get(3).getErrors().size());
|
||||
assertNotNull(insertOutput.getRecords().get(3).getValueInteger("id"));
|
||||
}
|
||||
|
||||
{
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
// one more time, but with multiple input records referencing each foreign key //
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
InsertInput insertInput = new InsertInput();
|
||||
insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC);
|
||||
insertInput.setRecords(List.of(
|
||||
new QRecord().withValue("lineItemId", null).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
|
||||
new QRecord().withValue("lineItemId", 1701).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
|
||||
new QRecord().withValue("lineItemId", 4200).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
|
||||
new QRecord().withValue("lineItemId", 4700).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
|
||||
new QRecord().withValue("lineItemId", null).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
|
||||
new QRecord().withValue("lineItemId", 1701).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
|
||||
new QRecord().withValue("lineItemId", 4200).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu"),
|
||||
new QRecord().withValue("lineItemId", 4700).withValue("key", "theKidsCanCallYou").withValue("value", "HoJu")
|
||||
));
|
||||
InsertOutput insertOutput = new InsertAction().execute(insertInput);
|
||||
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(0).getErrors().get(0));
|
||||
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(1).getErrors().get(0));
|
||||
assertEquals("You do not have permission to insert this record.", insertOutput.getRecords().get(2).getErrors().get(0));
|
||||
assertEquals(0, insertOutput.getRecords().get(3).getErrors().size());
|
||||
assertNotNull(insertOutput.getRecords().get(3).getValueInteger("id"));
|
||||
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(4).getErrors().get(0));
|
||||
assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(5).getErrors().get(0));
|
||||
assertEquals("You do not have permission to insert this record.", insertOutput.getRecords().get(6).getErrors().get(0));
|
||||
assertEquals(0, insertOutput.getRecords().get(7).getErrors().size());
|
||||
assertNotNull(insertOutput.getRecords().get(7).getValueInteger("id"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testInsertSingleLevelSecurityJoins() throws QException
|
||||
{
|
||||
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE, 1);
|
||||
|
||||
|
@ -1605,8 +1605,25 @@ class QInstanceValidatorTest extends BaseTest
|
||||
assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setFieldName("notAField")), "unrecognized field");
|
||||
assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setNullValueBehavior(null)), "missing a nullValueBehavior");
|
||||
|
||||
// todo - remove once implemented
|
||||
assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setFieldName("join.field")), "does not yet support finding a field that looks like a join field");
|
||||
assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setFieldName("join.field")), "Table order recordSecurityLock (of key type store) field name join.field looks like a join (has a dot), but no joinNameChain was given");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
**
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void testRecordSecurityLockJoinChains()
|
||||
{
|
||||
Function<QInstance, RecordSecurityLock> lockExtractor = qInstance -> qInstance.getTable(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC).getRecordSecurityLocks().get(0);
|
||||
|
||||
assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setJoinNameChain(null)), "looks like a join (has a dot), but no joinNameChain was given");
|
||||
assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setJoinNameChain(new ArrayList<>())), "looks like a join (has a dot), but no joinNameChain was given");
|
||||
assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setFieldName("storeId")), "does not look like a join (does not have a dot), but a joinNameChain was given");
|
||||
assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setFieldName("order.wrongId")), "unrecognized fieldName: order.wrongId");
|
||||
assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setJoinNameChain(List.of("notAJoin"))), "an unrecognized join");
|
||||
assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setJoinNameChain(List.of("orderLineItem"))), "joinNameChain could not be followed through join");
|
||||
}
|
||||
|
||||
|
||||
|
@ -596,6 +596,10 @@ public class TestUtils
|
||||
.withName(TABLE_NAME_LINE_ITEM_EXTRINSIC)
|
||||
.withBackendName(MEMORY_BACKEND_NAME)
|
||||
.withPrimaryKeyField("id")
|
||||
.withRecordSecurityLock(new RecordSecurityLock()
|
||||
.withSecurityKeyType(SECURITY_KEY_TYPE_STORE)
|
||||
.withFieldName("order.storeId")
|
||||
.withJoinNameChain(List.of("orderLineItem", "lineItemLineItemExtrinsic")))
|
||||
.withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false))
|
||||
.withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false))
|
||||
.withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false))
|
||||
|
Reference in New Issue
Block a user