diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/InsertAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/InsertAction.java index 5b59a722..3f25bf9b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/InsertAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/InsertAction.java @@ -41,6 +41,7 @@ import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostInsertCust 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.tables.helpers.UniqueKeyHelper; +import com.kingsrook.qqq.backend.core.actions.tables.helpers.ValidateRecordSecurityLockHelper; import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; @@ -48,27 +49,16 @@ import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.audits.DMLAuditInput; 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.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.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; -import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn; import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType; -import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock; 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.UniqueKey; 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.ListingHash; -import com.kingsrook.qqq.backend.core.utils.StringUtils; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; @@ -100,7 +90,7 @@ public class InsertAction extends AbstractQActionFunction errors = insertOutput.getRecords().stream().flatMap(r -> r.getErrors().stream()).toList(); @@ -153,231 +143,6 @@ public class InsertAction extends AbstractQActionFunction recordSecurityLocks = table.getRecordSecurityLocks(); - List locksToCheck = new ArrayList<>(); - - //////////////////////////////////////// - // if there are no locks, just return // - //////////////////////////////////////// - if(CollectionUtils.nullSafeIsEmpty(recordSecurityLocks)) - { - return; - } - - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // decide if any locks need checked - where one may not need checked if it has an all-access key, and the user has all-access // - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - for(RecordSecurityLock recordSecurityLock : recordSecurityLocks) - { - QSecurityKeyType securityKeyType = QContext.getQInstance().getSecurityKeyType(recordSecurityLock.getSecurityKeyType()); - if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()) && QContext.getQSession().hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN)) - { - LOG.debug("Session has " + securityKeyType.getAllAccessKeyName() + " - not checking this lock."); - } - else - { - locksToCheck.add(recordSecurityLock); - } - } - - ///////////////////////////////////////////////// - // if there are no locks to check, just return // - ///////////////////////////////////////////////// - if(locksToCheck.isEmpty()) - { - return; - } - - //////////////////////////////// - // actually check lock values // - //////////////////////////////// - for(RecordSecurityLock recordSecurityLock : locksToCheck) - { - if(CollectionUtils.nullSafeIsEmpty(recordSecurityLock.getJoinNameChain())) - { - for(QRecord record : insertInput.getRecords()) - { - ///////////////////////////////////////////////////////////////////////// - // handle the value being in the table we're inserting (e.g., no join) // - ///////////////////////////////////////////////////////////////////////// - QFieldMetaData field = table.getField(recordSecurityLock.getFieldName()); - Serializable recordSecurityValue = record.getValue(recordSecurityLock.getFieldName()); - validateRecordSecurityValue(table, record, recordSecurityLock, recordSecurityValue, field.getType()); - } - } - else - { - //////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // else look for the joined record - if it isn't found, assume a fail - else validate security value if found // - //////////////////////////////////////////////////////////////////////////////////////////////////////////////// - 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 inputRecordPage : CollectionUtils.getPages(insertInput.getRecords(), 500)) - { - //////////////////////////////////////////////////////////////////////////////////////////////// - // set up a query for joined records // - // query will be like (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) // - //////////////////////////////////////////////////////////////////////////////////////////////// - QueryInput queryInput = new QueryInput(); - 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) // - // also build up the query's sub-filters here (only adding them if they're unique). // - // e.g., 2 order-lines referencing the same orderId don't need to be added to the query twice // - /////////////////////////////////////////////////////////////////////////////////////////////////// - ListingHash, QRecord> inputRecordMapByJoinFields = new ListingHash<>(); - for(QRecord inputRecord : inputRecordPage) - { - List inputRecordJoinValues = new ArrayList<>(); - QQueryFilter subFilter = new QQueryFilter(); - - for(JoinOn joinOn : rightMostJoin.getJoinOns()) - { - Serializable inputRecordValue = inputRecord.getValue(joinOn.getRightField()); - inputRecordJoinValues.add(inputRecordValue); - - subFilter.addCriteria(inputRecordValue == null - ? new QFilterCriteria(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField(), QCriteriaOperator.IS_BLANK) - : new QFilterCriteria(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField(), QCriteriaOperator.EQUALS, inputRecordValue)); - } - - if(!inputRecordMapByJoinFields.containsKey(inputRecordJoinValues)) - { - //////////////////////////////////////////////////////////////////////////////// - // only add this sub-filter if it's for a list of keys we haven't seen before // - //////////////////////////////////////////////////////////////////////////////// - filter.addSubFilter(subFilter); - } - - inputRecordMapByJoinFields.add(inputRecordJoinValues, inputRecord); - } - - ////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // execute the query for joined records - then put them in a map with keys corresponding to the join values // - // e.g., (17,47)=(JoinRecord), (18,48)=(JoinRecord) // - ////////////////////////////////////////////////////////////////////////////////////////////////////////////// - QueryOutput queryOutput = new QueryAction().execute(queryInput); - Map, QRecord> joinRecordMapByJoinFields = new HashMap<>(); - for(QRecord joinRecord : queryOutput.getRecords()) - { - List joinRecordValues = new ArrayList<>(); - for(JoinOn joinOn : rightMostJoin.getJoinOns()) - { - 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); - } - - ////////////////////////////////////////////////////////////////////////////////////////////////// - // now for each input record, look for its joinRecord - if it isn't found, then this insert // - // isn't allowed. if it is found, then validate its value matches this session's security keys // - ////////////////////////////////////////////////////////////////////////////////////////////////// - for(Map.Entry, List> entry : inputRecordMapByJoinFields.entrySet()) - { - List inputRecordJoinValues = entry.getKey(); - List inputRecords = entry.getValue(); - if(joinRecordMapByJoinFields.containsKey(inputRecordJoinValues)) - { - QRecord joinRecord = joinRecordMapByJoinFields.get(inputRecordJoinValues); - - String fieldName = recordSecurityLock.getFieldName().replaceFirst(".*\\.", ""); - 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) - { - validateRecordSecurityValue(table, inputRecord, recordSecurityLock, recordSecurityValue, field.getType()); - } - } - else - { - for(QRecord inputRecord : inputRecords) - { - if(RecordSecurityLock.NullValueBehavior.DENY.equals(recordSecurityLock.getNullValueBehavior())) - { - inputRecord.addError("You do not have permission to insert this record - the referenced " + leftMostJoinTable.getLabel() + " was not found."); - } - } - } - } - } - } - } - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private static void validateRecordSecurityValue(QTableMetaData table, QRecord record, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType) - { - if(recordSecurityValue == null) - { - ///////////////////////////////////////////////////////////////// - // handle null values - error if the NullValueBehavior is DENY // - ///////////////////////////////////////////////////////////////// - if(RecordSecurityLock.NullValueBehavior.DENY.equals(recordSecurityLock.getNullValueBehavior())) - { - String lockLabel = CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()) ? recordSecurityLock.getSecurityKeyType() : table.getField(recordSecurityLock.getFieldName()).getLabel(); - record.addError("You do not have permission to insert a record without a value in the field: " + lockLabel); - } - } - else - { - if(!QContext.getQSession().hasSecurityKeyValue(recordSecurityLock.getSecurityKeyType(), recordSecurityValue, fieldType)) - { - if(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain())) - { - /////////////////////////////////////////////////////////////////////////////////////////////// - // avoid telling the user a value from a foreign record that they didn't pass in themselves. // - /////////////////////////////////////////////////////////////////////////////////////////////// - record.addError("You do not have permission to insert this record."); - } - else - { - QFieldMetaData field = table.getField(recordSecurityLock.getFieldName()); - record.addError("You do not have permission to insert a record with a value of " + recordSecurityValue + " in the field: " + field.getLabel()); - } - } - } - } - - - /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/UpdateAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/UpdateAction.java index 1d1eb07d..29366b21 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/UpdateAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/UpdateAction.java @@ -24,13 +24,16 @@ package com.kingsrook.qqq.backend.core.actions.tables; import java.io.Serializable; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import com.kingsrook.qqq.backend.core.actions.ActionHelper; import com.kingsrook.qqq.backend.core.actions.audits.DMLAuditAction; import com.kingsrook.qqq.backend.core.actions.automation.AutomationStatus; import com.kingsrook.qqq.backend.core.actions.automation.RecordAutomationStatusUpdater; +import com.kingsrook.qqq.backend.core.actions.tables.helpers.ValidateRecordSecurityLockHelper; import com.kingsrook.qqq.backend.core.actions.values.ValueBehaviorApplier; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; @@ -57,6 +60,7 @@ 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; import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; @@ -86,7 +90,10 @@ public class UpdateAction QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(updateInput.getBackend()); + validatePrimaryKeysAreGiven(updateInput); + validateRecordsExistAndCanBeAccessed(updateInput, oldRecordList); validateRequiredFields(updateInput); + ValidateRecordSecurityLockHelper.validateSecurityFields(updateInput.getTable(), updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE); // todo pre-customization - just get to modify the request? UpdateOutput updateOutput = qModule.getUpdateInterface().execute(updateInput); @@ -114,6 +121,86 @@ public class UpdateAction + /******************************************************************************* + ** + *******************************************************************************/ + private void validatePrimaryKeysAreGiven(UpdateInput updateInput) + { + QTableMetaData table = updateInput.getTable(); + for(QRecord record : CollectionUtils.nonNullList(updateInput.getRecords())) + { + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // to update a record, we must have its primary key value - so - check - if it's missing, mark it as an error // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + if(record.getValue(table.getPrimaryKeyField()) == null) + { + record.addError("Missing value in primary key field"); + } + } + } + + + + /******************************************************************************* + ** 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! + *******************************************************************************/ + private void validateRecordsExistAndCanBeAccessed(UpdateInput updateInput, List oldRecordList) throws QException + { + QTableMetaData table = updateInput.getTable(); + QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField()); + + for(List page : CollectionUtils.getPages(updateInput.getRecords(), 1000)) + { + List primaryKeysToLookup = new ArrayList<>(); + for(QRecord record : page) + { + Serializable primaryKeyValue = record.getValue(table.getPrimaryKeyField()); + if(primaryKeyValue != null) + { + primaryKeysToLookup.add(primaryKeyValue); + } + } + + Map 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(QRecord record : page) + { + Serializable value = ValueUtils.getValueAsFieldType(primaryKeyField.getType(), record.getValue(table.getPrimaryKeyField())); + if(value == null) + { + continue; + } + + if(!lookedUpRecords.containsKey(value)) + { + record.addError("No record was found to update for " + primaryKeyField.getLabel() + " = " + value); + } + } + } + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -126,7 +213,7 @@ public class UpdateAction if(!requiredFields.isEmpty()) { - for(QRecord record : updateInput.getRecords()) + for(QRecord record : CollectionUtils.nonNullList(updateInput.getRecords())) { for(QFieldMetaData requiredField : requiredFields) { @@ -279,7 +366,7 @@ public class UpdateAction if(AuditLevel.FIELD.equals(auditLevel)) { String primaryKeyField = updateInput.getTable().getPrimaryKeyField(); - List pkeysBeingUpdated = updateInput.getRecords().stream().map(r -> r.getValue(primaryKeyField)).toList(); + List pkeysBeingUpdated = CollectionUtils.nonNullList(updateInput.getRecords()).stream().map(r -> r.getValue(primaryKeyField)).toList(); QueryInput queryInput = new QueryInput(); queryInput.setTableName(updateInput.getTableName()); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/ValidateRecordSecurityLockHelper.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/ValidateRecordSecurityLockHelper.java new file mode 100644 index 00000000..c69a2fc0 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/ValidateRecordSecurityLockHelper.java @@ -0,0 +1,313 @@ +/* + * 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 . + */ + +package com.kingsrook.qqq.backend.core.actions.tables.helpers; + + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; +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.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; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn; +import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType; +import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock; +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.ListingHash; +import com.kingsrook.qqq.backend.core.utils.StringUtils; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ValidateRecordSecurityLockHelper +{ + private static final QLogger LOG = QLogger.getLogger(ValidateRecordSecurityLockHelper.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public enum Action + { + INSERT, + UPDATE, + SELECT + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void validateSecurityFields(QTableMetaData table, List records, Action action) throws QException + { + List locksToCheck = getRecordSecurityLocks(table); + if(CollectionUtils.nullSafeIsEmpty(locksToCheck)) + { + return; + } + + //////////////////////////////// + // actually check lock values // + //////////////////////////////// + for(RecordSecurityLock recordSecurityLock : locksToCheck) + { + if(CollectionUtils.nullSafeIsEmpty(recordSecurityLock.getJoinNameChain())) + { + ////////////////////////////////////////////////////////////////////////////////// + // handle the value being in the table we're inserting/updating (e.g., no join) // + ////////////////////////////////////////////////////////////////////////////////// + QFieldMetaData field = table.getField(recordSecurityLock.getFieldName()); + + for(QRecord record : records) + { + if(action.equals(Action.UPDATE) && !record.getValues().containsKey(field.getName())) + { + ///////////////////////////////////////////////////////////////////////// + // if not updating the security field, then no error can come from it! // + ///////////////////////////////////////////////////////////////////////// + continue; + } + + Serializable recordSecurityValue = record.getValue(field.getName()); + validateRecordSecurityValue(table, record, recordSecurityLock, recordSecurityValue, field.getType(), action); + } + } + else + { + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // else look for the joined record - if it isn't found, assume a fail - else validate security value if found // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////// + 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 inputRecordPage : CollectionUtils.getPages(records, 500)) + { + //////////////////////////////////////////////////////////////////////////////////////////////// + // set up a query for joined records // + // query will be like (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) OR (fkey1=? and fkey2=?) // + //////////////////////////////////////////////////////////////////////////////////////////////// + QueryInput queryInput = new QueryInput(); + 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) // + // also build up the query's sub-filters here (only adding them if they're unique). // + // e.g., 2 order-lines referencing the same orderId don't need to be added to the query twice // + /////////////////////////////////////////////////////////////////////////////////////////////////// + ListingHash, QRecord> inputRecordMapByJoinFields = new ListingHash<>(); + for(QRecord inputRecord : inputRecordPage) + { + List inputRecordJoinValues = new ArrayList<>(); + QQueryFilter subFilter = new QQueryFilter(); + + for(JoinOn joinOn : rightMostJoin.getJoinOns()) + { + Serializable inputRecordValue = inputRecord.getValue(joinOn.getRightField()); + inputRecordJoinValues.add(inputRecordValue); + + subFilter.addCriteria(inputRecordValue == null + ? new QFilterCriteria(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField(), QCriteriaOperator.IS_BLANK) + : new QFilterCriteria(rightMostJoin.getLeftTable() + "." + joinOn.getLeftField(), QCriteriaOperator.EQUALS, inputRecordValue)); + } + + if(!inputRecordMapByJoinFields.containsKey(inputRecordJoinValues)) + { + //////////////////////////////////////////////////////////////////////////////// + // only add this sub-filter if it's for a list of keys we haven't seen before // + //////////////////////////////////////////////////////////////////////////////// + filter.addSubFilter(subFilter); + } + + inputRecordMapByJoinFields.add(inputRecordJoinValues, inputRecord); + } + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // execute the query for joined records - then put them in a map with keys corresponding to the join values // + // e.g., (17,47)=(JoinRecord), (18,48)=(JoinRecord) // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + QueryOutput queryOutput = new QueryAction().execute(queryInput); + Map, QRecord> joinRecordMapByJoinFields = new HashMap<>(); + for(QRecord joinRecord : queryOutput.getRecords()) + { + List joinRecordValues = new ArrayList<>(); + for(JoinOn joinOn : rightMostJoin.getJoinOns()) + { + 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); + } + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // now for each input record, look for its joinRecord - if it isn't found, then this insert // + // isn't allowed. if it is found, then validate its value matches this session's security keys // + ////////////////////////////////////////////////////////////////////////////////////////////////// + for(Map.Entry, List> entry : inputRecordMapByJoinFields.entrySet()) + { + List inputRecordJoinValues = entry.getKey(); + List inputRecords = entry.getValue(); + if(joinRecordMapByJoinFields.containsKey(inputRecordJoinValues)) + { + QRecord joinRecord = joinRecordMapByJoinFields.get(inputRecordJoinValues); + + String fieldName = recordSecurityLock.getFieldName().replaceFirst(".*\\.", ""); + 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) + { + validateRecordSecurityValue(table, inputRecord, recordSecurityLock, recordSecurityValue, field.getType(), action); + } + } + else + { + for(QRecord inputRecord : inputRecords) + { + if(RecordSecurityLock.NullValueBehavior.DENY.equals(recordSecurityLock.getNullValueBehavior())) + { + inputRecord.addError("You do not have permission to " + action.name().toLowerCase() + " this record - the referenced " + leftMostJoinTable.getLabel() + " was not found."); + } + } + } + } + } + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static List getRecordSecurityLocks(QTableMetaData table) + { + List recordSecurityLocks = table.getRecordSecurityLocks(); + List locksToCheck = new ArrayList<>(); + + //////////////////////////////////////// + // if there are no locks, just return // + //////////////////////////////////////// + if(CollectionUtils.nullSafeIsEmpty(recordSecurityLocks)) + { + return (null); + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // decide if any locks need checked - where one may not need checked if it has an all-access key, and the user has all-access // + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + for(RecordSecurityLock recordSecurityLock : recordSecurityLocks) + { + QSecurityKeyType securityKeyType = QContext.getQInstance().getSecurityKeyType(recordSecurityLock.getSecurityKeyType()); + if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName()) && QContext.getQSession().hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN)) + { + LOG.debug("Session has " + securityKeyType.getAllAccessKeyName() + " - not checking this lock."); + } + else + { + locksToCheck.add(recordSecurityLock); + } + } + + return (locksToCheck); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + static void validateRecordSecurityValue(QTableMetaData table, QRecord record, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action) + { + if(recordSecurityValue == null) + { + ///////////////////////////////////////////////////////////////// + // handle null values - error if the NullValueBehavior is DENY // + ///////////////////////////////////////////////////////////////// + if(RecordSecurityLock.NullValueBehavior.DENY.equals(recordSecurityLock.getNullValueBehavior())) + { + String lockLabel = CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()) ? recordSecurityLock.getSecurityKeyType() : table.getField(recordSecurityLock.getFieldName()).getLabel(); + record.addError("You do not have permission to " + action.name().toLowerCase() + " a record without a value in the field: " + lockLabel); + } + } + else + { + if(!QContext.getQSession().hasSecurityKeyValue(recordSecurityLock.getSecurityKeyType(), recordSecurityValue, fieldType)) + { + if(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain())) + { + /////////////////////////////////////////////////////////////////////////////////////////////// + // avoid telling the user a value from a foreign record that they didn't pass in themselves. // + /////////////////////////////////////////////////////////////////////////////////////////////// + record.addError("You do not have permission to " + action.name().toLowerCase() + " this record."); + } + else + { + QFieldMetaData field = table.getField(recordSecurityLock.getFieldName()); + record.addError("You do not have permission to " + action.name().toLowerCase() + " a record with a value of " + recordSecurityValue + " in the field: " + field.getLabel()); + } + } + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java index d08cb943..94cbcc59 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java @@ -30,7 +30,10 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import com.kingsrook.qqq.backend.core.actions.tables.helpers.ValidateRecordSecurityLockHelper; import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.logging.QLogger; 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; @@ -54,6 +57,8 @@ import com.kingsrook.qqq.backend.core.utils.ValueUtils; *******************************************************************************/ public class MemoryRecordStore { + private static final QLogger LOG = QLogger.getLogger(MemoryRecordStore.class); + private static MemoryRecordStore instance; private Map> data; @@ -135,7 +140,7 @@ public class MemoryRecordStore /******************************************************************************* ** *******************************************************************************/ - public List query(QueryInput input) + public List query(QueryInput input) throws QException { incrementStatistic(input); @@ -153,7 +158,20 @@ public class MemoryRecordStore if(recordMatches) { - records.add(qRecord); + qRecord.setErrors(new ArrayList<>()); + ValidateRecordSecurityLockHelper.validateSecurityFields(input.getTable(), List.of(qRecord), ValidateRecordSecurityLockHelper.Action.SELECT); + if(CollectionUtils.nullSafeHasContents(qRecord.getErrors())) + { + ////////////////////////////////////////////////////////////////////////////////////////////////////// + // security error! no record for you. but remove the error, so future generations won't see it... // + ////////////////////////////////////////////////////////////////////////////////////////////////////// + qRecord.setErrors(new ArrayList<>()); + LOG.trace("Error selecting record (presumably security?): " + qRecord.getErrors()); + } + else + { + records.add(qRecord); + } } } @@ -175,7 +193,7 @@ public class MemoryRecordStore for(QRecord record : getTableData(leftTable).values()) { QRecord productRecord = new QRecord(); - addRecordToProduct(productRecord, record, leftTable.getName()); + addRecordToProduct(productRecord, record, null); crossProduct.add(productRecord); } @@ -220,7 +238,9 @@ public class MemoryRecordStore { for(JoinOn joinOn : queryJoin.getJoinMetaData().getJoinOns()) { - Serializable leftValue = productRecord.getValue(queryJoin.getBaseTableOrAlias() + "." + joinOn.getLeftField()); + Serializable leftValue = productRecord.getValues().containsKey(queryJoin.getBaseTableOrAlias() + "." + joinOn.getLeftField()) + ? productRecord.getValue(queryJoin.getBaseTableOrAlias() + "." + joinOn.getLeftField()) + : productRecord.getValue(joinOn.getLeftField()); Serializable rightValue = nextTableRecord.getValue(joinOn.getRightField()); if(!Objects.equals(leftValue, rightValue)) { @@ -240,7 +260,7 @@ public class MemoryRecordStore { for(Map.Entry entry : record.getValues().entrySet()) { - productRecord.withValue(tableNameOrAlias + "." + entry.getKey(), entry.getValue()); + productRecord.withValue(tableNameOrAlias == null ? entry.getKey() : tableNameOrAlias + "." + entry.getKey(), entry.getValue()); } } @@ -249,7 +269,7 @@ public class MemoryRecordStore /******************************************************************************* ** *******************************************************************************/ - public Integer count(CountInput input) + public Integer count(CountInput input) throws QException { QueryInput queryInput = new QueryInput(); queryInput.setTableName(input.getTableName()); @@ -349,9 +369,8 @@ public class MemoryRecordStore { Serializable primaryKeyValue = ValueUtils.getValueAsFieldType(primaryKeyField.getType(), record.getValue(primaryKeyField.getName())); - if(primaryKeyValue == null) + if(CollectionUtils.nullSafeHasContents(record.getErrors())) { - record.addError("Missing value in primary key field"); outputRecords.add(record); continue; } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/InsertActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/InsertActionTest.java index 06238a8c..6d903b78 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/InsertActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/InsertActionTest.java @@ -341,7 +341,7 @@ class InsertActionTest extends BaseTest 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)); + assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertLineItemExtrinsicOutput.getRecords().get(0).getErrors().get(0)); } { @@ -373,7 +373,7 @@ class InsertActionTest extends BaseTest 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("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(2).getErrors().get(0)); assertEquals(0, insertOutput.getRecords().get(3).getErrors().size()); assertNotNull(insertOutput.getRecords().get(3).getValueInteger("id")); } @@ -397,12 +397,12 @@ class InsertActionTest extends BaseTest 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("You do not have permission to insert this record - the referenced Order was not found.", 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("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(6).getErrors().get(0)); assertEquals(0, insertOutput.getRecords().get(7).getErrors().size()); assertNotNull(insertOutput.getRecords().get(7).getValueInteger("id")); } @@ -457,7 +457,7 @@ class InsertActionTest extends BaseTest insertLineItemInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM); insertLineItemInput.setRecords(List.of(new QRecord().withValue("orderId", 42).withValue("sku", "BASIC1").withValue("quantity", 1))); InsertOutput insertLineItemOutput = new InsertAction().execute(insertLineItemInput); - assertEquals("You do not have permission to insert this record.", insertLineItemOutput.getRecords().get(0).getErrors().get(0)); + assertEquals("You do not have permission to insert this record - the referenced Order was not found.", insertLineItemOutput.getRecords().get(0).getErrors().get(0)); } { @@ -482,7 +482,7 @@ class InsertActionTest extends BaseTest 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("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(2).getErrors().get(0)); assertEquals(0, insertOutput.getRecords().get(3).getErrors().size()); assertNotNull(insertOutput.getRecords().get(3).getValueInteger("id")); } @@ -506,12 +506,12 @@ class InsertActionTest extends BaseTest 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("You do not have permission to insert this record - the referenced Order was not found.", 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("You do not have permission to insert this record - the referenced Order was not found.", insertOutput.getRecords().get(6).getErrors().get(0)); assertEquals(0, insertOutput.getRecords().get(7).getErrors().size()); assertNotNull(insertOutput.getRecords().get(7).getValueInteger("id")); } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/UpdateActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/UpdateActionTest.java index 5148cb2c..afea99c3 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/UpdateActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/UpdateActionTest.java @@ -29,13 +29,15 @@ import com.kingsrook.qqq.backend.core.BaseTest; import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; 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.update.UpdateInput; 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.metadata.QInstance; 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.Test; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -76,7 +78,6 @@ class UpdateActionTest extends BaseTest @Test void testUpdateAssociationsUpdateOneChild() throws QException { - QInstance qInstance = QContext.getQInstance(); QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true); insert2OrdersWith3Lines3LineExtrinsicsAnd4OrderExtrinsicAssociations(); @@ -93,18 +94,18 @@ class UpdateActionTest extends BaseTest )); new UpdateAction().execute(updateInput); - List orders = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_ORDER); + List orders = TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER); assertEquals(2, orders.size()); assertEquals("ORD123-b", orders.get(0).getValueString("orderNo")); - List orderLines = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_LINE_ITEM); + List orderLines = TestUtils.queryTable(TestUtils.TABLE_NAME_LINE_ITEM); assertEquals(3, orderLines.size()); assertEquals(17, orderLines.get(0).getValueInteger("quantity")); - List lineItemExtrinsics = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC); + List lineItemExtrinsics = TestUtils.queryTable(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC); assertEquals(3, lineItemExtrinsics.size()); - List orderExtrinsics = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_ORDER_EXTRINSIC); + List orderExtrinsics = TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER_EXTRINSIC); assertEquals(4, orderExtrinsics.size()); } @@ -116,7 +117,6 @@ class UpdateActionTest extends BaseTest @Test void testUpdateAssociationsUpdateOneGrandChild() throws QException { - QInstance qInstance = QContext.getQInstance(); QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true); insert2OrdersWith3Lines3LineExtrinsicsAnd4OrderExtrinsicAssociations(); @@ -134,18 +134,18 @@ class UpdateActionTest extends BaseTest )); new UpdateAction().execute(updateInput); - List orders = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_ORDER); + List orders = TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER); assertEquals(2, orders.size()); assertEquals("ORD123-b", orders.get(0).getValueString("orderNo")); - List orderLines = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_LINE_ITEM); + List orderLines = TestUtils.queryTable(TestUtils.TABLE_NAME_LINE_ITEM); assertEquals(3, orderLines.size()); - List lineItemExtrinsics = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC); + List lineItemExtrinsics = TestUtils.queryTable(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC); assertEquals(3, lineItemExtrinsics.size()); assertEquals("LINE-VAL-1-updated", lineItemExtrinsics.get(0).getValueString("value")); - List orderExtrinsics = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_ORDER_EXTRINSIC); + List orderExtrinsics = TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER_EXTRINSIC); assertEquals(4, orderExtrinsics.size()); } @@ -157,7 +157,6 @@ class UpdateActionTest extends BaseTest @Test void testUpdateAssociationsDeleteOneChild() throws QException { - QInstance qInstance = QContext.getQInstance(); QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true); insert2OrdersWith3Lines3LineExtrinsicsAnd4OrderExtrinsicAssociations(); @@ -173,18 +172,18 @@ class UpdateActionTest extends BaseTest )); new UpdateAction().execute(updateInput); - List orders = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_ORDER); + List orders = TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER); assertEquals(2, orders.size()); assertEquals("ORD123-b", orders.get(0).getValueString("orderNo")); - List orderLines = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_LINE_ITEM); + List orderLines = TestUtils.queryTable(TestUtils.TABLE_NAME_LINE_ITEM); assertEquals(2, orderLines.size()); assertTrue(orderLines.stream().noneMatch(r -> r.getValueInteger("id").equals(1))); // id=1 should be deleted - List lineItemExtrinsics = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC); + List lineItemExtrinsics = TestUtils.queryTable(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC); assertEquals(2, lineItemExtrinsics.size()); // one was deleted (when its parent was deleted) - List orderExtrinsics = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_ORDER_EXTRINSIC); + List orderExtrinsics = TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER_EXTRINSIC); assertEquals(4, orderExtrinsics.size()); } @@ -196,7 +195,6 @@ class UpdateActionTest extends BaseTest @Test void testUpdateAssociationsDeleteGrandchildren() throws QException { - QInstance qInstance = QContext.getQInstance(); QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true); insert2OrdersWith3Lines3LineExtrinsicsAnd4OrderExtrinsicAssociations(); @@ -213,17 +211,17 @@ class UpdateActionTest extends BaseTest )); new UpdateAction().execute(updateInput); - List orders = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_ORDER); + List orders = TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER); assertEquals(2, orders.size()); assertEquals("ORD123-b", orders.get(0).getValueString("orderNo")); - List orderLines = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_LINE_ITEM); + List orderLines = TestUtils.queryTable(TestUtils.TABLE_NAME_LINE_ITEM); assertEquals(3, orderLines.size()); - List lineItemExtrinsics = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC); + List lineItemExtrinsics = TestUtils.queryTable(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC); assertEquals(1, lineItemExtrinsics.size()); // deleted the two beneath line item id=2 - List orderExtrinsics = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_ORDER_EXTRINSIC); + List orderExtrinsics = TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER_EXTRINSIC); assertEquals(4, orderExtrinsics.size()); } @@ -235,7 +233,6 @@ class UpdateActionTest extends BaseTest @Test void testUpdateAssociationsInsertOneChild() throws QException { - QInstance qInstance = QContext.getQInstance(); QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true); insert2OrdersWith3Lines3LineExtrinsicsAnd4OrderExtrinsicAssociations(); @@ -253,19 +250,19 @@ class UpdateActionTest extends BaseTest )); new UpdateAction().execute(updateInput); - List orders = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_ORDER); + List orders = TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER); assertEquals(2, orders.size()); assertEquals("ORD123-b", orders.get(0).getValueString("orderNo")); - List orderLines = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_LINE_ITEM); + List orderLines = TestUtils.queryTable(TestUtils.TABLE_NAME_LINE_ITEM); assertEquals(4, orderLines.size()); assertEquals("BASIC4", orderLines.get(3).getValueString("sku")); assertEquals(47, orderLines.get(3).getValueInteger("quantity")); - List lineItemExtrinsics = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC); + List lineItemExtrinsics = TestUtils.queryTable(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC); assertEquals(3, lineItemExtrinsics.size()); - List orderExtrinsics = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_ORDER_EXTRINSIC); + List orderExtrinsics = TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER_EXTRINSIC); assertEquals(4, orderExtrinsics.size()); } @@ -277,7 +274,6 @@ class UpdateActionTest extends BaseTest @Test void testUpdateAssociationsDeleteAllChildren() throws QException { - QInstance qInstance = QContext.getQInstance(); QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true); insert2OrdersWith3Lines3LineExtrinsicsAnd4OrderExtrinsicAssociations(); @@ -293,16 +289,16 @@ class UpdateActionTest extends BaseTest )); new UpdateAction().execute(updateInput); - List orders = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_ORDER); + List orders = TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER); assertEquals(2, orders.size()); - List lineItemExtrinsics = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC); + List lineItemExtrinsics = TestUtils.queryTable(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC); assertEquals(0, lineItemExtrinsics.size()); - List orderLines = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_LINE_ITEM); + List orderLines = TestUtils.queryTable(TestUtils.TABLE_NAME_LINE_ITEM); assertEquals(0, orderLines.size()); // all of these got deleted too. - List orderExtrinsics = TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_ORDER_EXTRINSIC); + List orderExtrinsics = TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER_EXTRINSIC); assertEquals(4, orderExtrinsics.size()); } @@ -359,7 +355,7 @@ class UpdateActionTest extends BaseTest new QRecord().withValue("id", 3).withValue("storeId", 999).withValue("orderNo", "ORD3"), new QRecord().withValue("id", 4).withValue("storeId", 999).withValue("orderNo", "ORD4") )); - InsertOutput insertOutput = new InsertAction().execute(insertInput); + new InsertAction().execute(insertInput); ////////////////////////////////////////////////// // do our update that we'll test the results of // @@ -395,6 +391,361 @@ class UpdateActionTest extends BaseTest /////////////////////////////////////////////////////////////////////// assertEquals(1, updateOutput.getRecords().get(3).getErrors().size()); assertEquals("Missing value in required field: Order No", updateOutput.getRecords().get(3).getErrors().get(0)); - } + + /******************************************************************************* + ** + *******************************************************************************/ + /* + @Test + 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); + + ////////////////////////////////////////////////////////////////////////////////////// + // 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); + insertInput.setRecords(List.of(new QRecord().withValue("orderId", null).withValue("sku", "BASIC1").withValue("quantity", 1))); + 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); + insertInput.setRecords(List.of(new QRecord().withValue("orderId", 1701).withValue("sku", "BASIC1").withValue("quantity", 1))); + 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 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")); + + QContext.getQSession().withSecurityKeyValues(new HashMap<>()); + QContext.getQSession().withSecurityKeyValues(TestUtils.SECURITY_KEY_TYPE_STORE, List.of(1)); + InsertInput insertLineItemInput = new InsertInput(); + insertLineItemInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM); + insertLineItemInput.setRecords(List.of(new QRecord().withValue("orderId", 42).withValue("sku", "BASIC1").withValue("quantity", 1))); + InsertOutput insertLineItemOutput = new InsertAction().execute(insertLineItemInput); + assertEquals("You do not have permission to insert this record.", insertLineItemOutput.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))); + new InsertAction().execute(insertOrderInput); + + /////////////////////////////////////////////////////// + // combine all the above, plus one record that works // + /////////////////////////////////////////////////////// + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM); + insertInput.setRecords(List.of( + new QRecord().withValue("orderId", null).withValue("sku", "BASIC1").withValue("quantity", 1), + new QRecord().withValue("orderId", 1701).withValue("sku", "BASIC1").withValue("quantity", 1), + new QRecord().withValue("orderId", 42).withValue("sku", "BASIC1").withValue("quantity", 1), + new QRecord().withValue("orderId", 47).withValue("sku", "BASIC1").withValue("quantity", 1) + )); + 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); + insertInput.setRecords(List.of( + new QRecord().withValue("orderId", null).withValue("sku", "BASIC1").withValue("quantity", 1), + new QRecord().withValue("orderId", 1701).withValue("sku", "BASIC1").withValue("quantity", 1), + new QRecord().withValue("orderId", 42).withValue("sku", "BASIC1").withValue("quantity", 1), + new QRecord().withValue("orderId", 47).withValue("sku", "BASIC1").withValue("quantity", 1), + new QRecord().withValue("orderId", null).withValue("sku", "BASIC1").withValue("quantity", 1), + new QRecord().withValue("orderId", 1701).withValue("sku", "BASIC1").withValue("quantity", 1), + new QRecord().withValue("orderId", 42).withValue("sku", "BASIC1").withValue("quantity", 1), + new QRecord().withValue("orderId", 47).withValue("sku", "BASIC1").withValue("quantity", 1) + )); + 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 testUpdateNotFoundFails() throws QException + { + QContext.getQSession().withSecurityKeyValues(MapBuilder.of(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, ListBuilder.of(true))); + + UpdateInput updateInput = new UpdateInput(); + updateInput.setTableName(TestUtils.TABLE_NAME_ORDER); + updateInput.setRecords(List.of(new QRecord().withValue("id", 999).withValue("orderNo", "updated"))); + UpdateOutput updateOutput = new UpdateAction().execute(updateInput); + assertEquals("No record was found to update for Id = 999", updateOutput.getRecords().get(0).getErrors().get(0)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSecurityKeyValueDenied() throws QException + { + //////////////////////////////// + // insert an order in store 1 // + //////////////////////////////// + QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE, 1); + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(TestUtils.TABLE_NAME_ORDER); + insertInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("orderNo", "original").withValue("storeId", 1))); + new InsertAction().execute(insertInput); + + ////////////////////////////////////////////////////////////////////// + // now, as a session with store 2, try to update that store 1 order // + // it should error as "not found" // + ////////////////////////////////////////////////////////////////////// + QContext.getQSession().withSecurityKeyValues(MapBuilder.of(TestUtils.SECURITY_KEY_TYPE_STORE, ListBuilder.of(2))); + + UpdateInput updateInput = new UpdateInput(); + updateInput.setTableName(TestUtils.TABLE_NAME_ORDER); + updateInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("orderNo", "updated"))); + UpdateOutput updateOutput = new UpdateAction().execute(updateInput); + assertEquals("No record was found to update for Id = 1", updateOutput.getRecords().get(0).getErrors().get(0)); + + QContext.getQSession().withSecurityKeyValues(MapBuilder.of(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, ListBuilder.of(true))); + assertThat(TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER)).noneMatch(r -> r.getValueString("orderNo").equals("updated")); + assertThat(TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER)).anyMatch(r -> r.getValueString("orderNo").equals("original")); + + ///////////////////////////////////////////////////////////////////////////////// + // now, go back to store 1 in session, and try to change the order to store 2. // + // that should fail, as you don't have permission to write to store 2. // + ///////////////////////////////////////////////////////////////////////////////// + QContext.getQSession().withSecurityKeyValues(MapBuilder.of(TestUtils.SECURITY_KEY_TYPE_STORE, ListBuilder.of(1))); + + updateInput.setRecords(List.of(new QRecord().withValue("id", 1).withValue("orderNo", "updated").withValue("storeId", 2))); + updateOutput = new UpdateAction().execute(updateInput); + assertEquals("You do not have permission to update a record with a value of 2 in the field: Store Id", updateOutput.getRecords().get(0).getErrors().get(0)); + + QContext.getQSession().withSecurityKeyValues(MapBuilder.of(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, ListBuilder.of(true))); + assertThat(TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER)).noneMatch(r -> r.getValueString("orderNo").equals("updated")); + assertThat(TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER)).anyMatch(r -> r.getValueString("orderNo").equals("original")); + } + + /******************************************************************************* + ** + *******************************************************************************/ + /* + @Test + void testSecurityKeyNullDenied() throws QException + { + QInstance qInstance = QContext.getQInstance(); + QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE, 1); + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(TestUtils.TABLE_NAME_ORDER); + insertInput.setRecords(List.of(new QRecord())); + InsertOutput insertOutput = new InsertAction().execute(insertInput); + assertEquals("You do not have permission to insert a record without a value in the field: Store Id", insertOutput.getRecords().get(0).getErrors().get(0)); + assertEquals(0, TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER).size()); + } + */ + + /******************************************************************************* + ** + *******************************************************************************/ + /* + @Test + void testSecurityKeyNullAllowed() throws QException + { + QInstance qInstance = QContext.getQInstance(); + qInstance.getTable(TestUtils.TABLE_NAME_ORDER).getRecordSecurityLocks().get(0).setNullValueBehavior(RecordSecurityLock.NullValueBehavior.ALLOW); + QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE, 1); + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(TestUtils.TABLE_NAME_ORDER); + insertInput.setRecords(List.of(new QRecord())); + InsertOutput insertOutput = new InsertAction().execute(insertInput); + assertEquals(0, insertOutput.getRecords().get(0).getErrors().size()); + assertEquals(1, TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER).size()); + } + */ + + /******************************************************************************* + ** + *******************************************************************************/ + /* + @Test + void testSecurityKeyAllAccess() throws QException + { + QInstance qInstance = QContext.getQInstance(); + qInstance.getTable(TestUtils.TABLE_NAME_ORDER).getRecordSecurityLocks().get(0).setNullValueBehavior(RecordSecurityLock.NullValueBehavior.ALLOW); + QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true); + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(TestUtils.TABLE_NAME_ORDER); + insertInput.setRecords(List.of( + new QRecord().withValue("storeId", 999), + new QRecord().withValue("storeId", null) + )); + InsertOutput insertOutput = new InsertAction().execute(insertInput); + assertEquals(2, TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER).size()); + } + */ + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModuleTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModuleTest.java index f9678d02..032ca77a 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModuleTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModuleTest.java @@ -325,7 +325,7 @@ public class TableBasedAuthenticationModuleTest extends BaseTest MemoryRecordStore.setCollectStatistics(true); assertTrue(new TableBasedAuthenticationModule().isSessionValid(qInstance, session)); Map statistics = MemoryRecordStore.getStatistics(); - assertEquals(3, statistics.get(MemoryRecordStore.STAT_QUERIES_RAN)); + assertEquals(4, statistics.get(MemoryRecordStore.STAT_QUERIES_RAN)); } diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java index c82a048b..fc011387 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/AbstractRDBMSAction.java @@ -892,6 +892,16 @@ public abstract class AbstractRDBMSAction implements QActionInterface + /******************************************************************************* + ** Make it easy (e.g., for tests) to turn on logging of SQL + *******************************************************************************/ + public static void setLogSQL(boolean on) + { + System.setProperty("qqq.rdbms.logSQL", String.valueOf(on)); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSUpdateAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSUpdateAction.java index ee04e28f..26af479b 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSUpdateAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSUpdateAction.java @@ -104,14 +104,6 @@ public class RDBMSUpdateAction extends AbstractRDBMSAction implements UpdateInte .toList(); recordsByFieldBeingUpdated.add(updatableFields, record); - //////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // to update a record, we must have its primary key value - so - check - if it's missing, mark it as an error // - //////////////////////////////////////////////////////////////////////////////////////////////////////////////// - if(record.getValue(table.getPrimaryKeyField()) == null) - { - record.addError("Missing value in primary key field"); - } - if(CollectionUtils.nullSafeIsEmpty(record.getErrors())) { haveAnyWithoutErorrs = true; diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSUpdateActionTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSUpdateActionTest.java index b1405646..b75c2417 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSUpdateActionTest.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSUpdateActionTest.java @@ -27,6 +27,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.actions.tables.GetAction; +import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetOutput; @@ -74,7 +75,7 @@ public class RDBMSUpdateActionTest extends RDBMSActionTest { UpdateInput updateInput = initUpdateRequest(); updateInput.setRecords(null); - UpdateOutput updateResult = new RDBMSUpdateAction().execute(updateInput); + UpdateOutput updateResult = new UpdateAction().execute(updateInput); assertEquals(0, updateResult.getRecords().size()); } @@ -88,8 +89,8 @@ public class RDBMSUpdateActionTest extends RDBMSActionTest { UpdateInput updateInput = initUpdateRequest(); updateInput.setRecords(Collections.emptyList()); - new RDBMSUpdateAction().execute(updateInput); - UpdateOutput updateResult = new RDBMSUpdateAction().execute(updateInput); + new UpdateAction().execute(updateInput); + UpdateOutput updateResult = new UpdateAction().execute(updateInput); assertEquals(0, updateResult.getRecords().size()); } @@ -110,9 +111,9 @@ public class RDBMSUpdateActionTest extends RDBMSActionTest .withValue("birthDate", "2210-05-20"); updateInput.setRecords(List.of(record)); - UpdateOutput updateResult = new RDBMSUpdateAction().execute(updateInput); + UpdateOutput updateResult = new UpdateAction().execute(updateInput); Map statistics = QueryManager.getStatistics(); - assertEquals(1, statistics.get(QueryManager.STAT_QUERIES_RAN)); + assertEquals(2, statistics.get(QueryManager.STAT_QUERIES_RAN)); assertEquals(1, updateResult.getRecords().size(), "Should return 1 row"); assertEquals(2, updateResult.getRecords().get(0).getValue("id"), "Should have id=2 in the row"); @@ -163,12 +164,12 @@ public class RDBMSUpdateActionTest extends RDBMSActionTest updateInput.setRecords(List.of(record1, record2, record3)); - UpdateOutput updateResult = new RDBMSUpdateAction().execute(updateInput); + UpdateOutput updateResult = new UpdateAction().execute(updateInput); // this test runs one batch and one regular query Map statistics = QueryManager.getStatistics(); assertEquals(1, statistics.get(QueryManager.STAT_BATCHES_RAN)); - assertEquals(1, statistics.get(QueryManager.STAT_QUERIES_RAN)); + assertEquals(2, statistics.get(QueryManager.STAT_QUERIES_RAN)); assertEquals(3, updateResult.getRecords().size(), "Should return 3 rows"); assertEquals(1, updateResult.getRecords().get(0).getValue("id"), "Should have expected ids in the row"); @@ -234,7 +235,7 @@ public class RDBMSUpdateActionTest extends RDBMSActionTest updateInput.setRecords(List.of(record1, record2)); - UpdateOutput updateResult = new RDBMSUpdateAction().execute(updateInput); + UpdateOutput updateResult = new UpdateAction().execute(updateInput); Map statistics = QueryManager.getStatistics(); assertEquals(1, statistics.get(QueryManager.STAT_BATCHES_RAN)); @@ -287,9 +288,9 @@ public class RDBMSUpdateActionTest extends RDBMSActionTest updateInput.setRecords(records); - UpdateOutput updateResult = new RDBMSUpdateAction().execute(updateInput); + UpdateOutput updateResult = new UpdateAction().execute(updateInput); Map statistics = QueryManager.getStatistics(); - assertEquals(1, statistics.get(QueryManager.STAT_QUERIES_RAN)); + assertEquals(2, statistics.get(QueryManager.STAT_QUERIES_RAN)); assertEquals(5, updateResult.getRecords().size(), "Should return 5 rows"); // todo - add errors to QRecord? assertTrue(updateResult.getRecords().stream().noneMatch(qrs -> CollectionUtils.nullSafeHasContents(qrs.getErrors())), "There should be no errors"); @@ -320,7 +321,7 @@ public class RDBMSUpdateActionTest extends RDBMSActionTest .withValue("id", 1) .withValue("firstName", "Johnny Updated")); updateInput.setRecords(records); - new RDBMSUpdateAction().execute(updateInput); + new UpdateAction().execute(updateInput); String updatedModifyDate = selectModifyDate(1); @@ -345,7 +346,7 @@ public class RDBMSUpdateActionTest extends RDBMSActionTest .withValue("createDate", "2022-10-03T10:29:35Z") .withValue("firstName", "Johnny Updated")); updateInput.setRecords(records); - new RDBMSUpdateAction().execute(updateInput); + new UpdateAction().execute(updateInput); } @@ -362,7 +363,7 @@ public class RDBMSUpdateActionTest extends RDBMSActionTest records.add(new QRecord() .withValue("firstName", "Johnny Updated")); updateInput.setRecords(records); - UpdateOutput updateOutput = new RDBMSUpdateAction().execute(updateInput); + UpdateOutput updateOutput = new UpdateAction().execute(updateInput); assertFalse(updateOutput.getRecords().get(0).getErrors().isEmpty()); assertEquals("Missing value in primary key field", updateOutput.getRecords().get(0).getErrors().get(0)); } @@ -374,7 +375,7 @@ public class RDBMSUpdateActionTest extends RDBMSActionTest .withValue("id", null) .withValue("firstName", "Johnny Updated")); updateInput.setRecords(records); - UpdateOutput updateOutput = new RDBMSUpdateAction().execute(updateInput); + UpdateOutput updateOutput = new UpdateAction().execute(updateInput); assertFalse(updateOutput.getRecords().get(0).getErrors().isEmpty()); assertEquals("Missing value in primary key field", updateOutput.getRecords().get(0).getErrors().get(0)); } @@ -389,7 +390,7 @@ public class RDBMSUpdateActionTest extends RDBMSActionTest .withValue("id", 2) .withValue("firstName", "Johnny Updated")); updateInput.setRecords(records); - UpdateOutput updateOutput = new RDBMSUpdateAction().execute(updateInput); + UpdateOutput updateOutput = new UpdateAction().execute(updateInput); assertFalse(updateOutput.getRecords().get(0).getErrors().isEmpty()); assertEquals("Missing value in primary key field", updateOutput.getRecords().get(0).getErrors().get(0)); diff --git a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java index f43f8159..20528df4 100644 --- a/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java +++ b/qqq-middleware-api/src/test/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandlerTest.java @@ -76,9 +76,6 @@ class QJavalinApiHandlerTest extends BaseTest protected static QJavalinImplementation qJavalinImplementation; - private static final String OAUTH_CLIENT_ID = "test-oauth-client-id"; - private static final String OAUTH_CLIENT_SECRET = "test-oauth-client-secret"; - /*******************************************************************************