Checking record security locks that are more than 1 join away.

This commit is contained in:
2023-03-29 09:55:05 -05:00
parent e62d2332ac
commit ef6ccc61c3
18 changed files with 634 additions and 151 deletions

View File

@ -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.");
}
}
}

View File

@ -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");

View File

@ -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()
{
}
}

View File

@ -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);
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -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())

View File

@ -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;

View File

@ -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());
}
}
/*******************************************************************************
**
*******************************************************************************/

View File

@ -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);

View File

@ -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);

View File

@ -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");
}

View File

@ -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))