From 0eff8d7d035edc9535dd1ceef5097121f842acde Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 28 Mar 2023 10:23:53 -0500 Subject: [PATCH] Adding requiredField, security validation to insert action --- .../core/actions/tables/InsertAction.java | 242 ++++++++++++++++++ .../widgets/ChildRecordListRendererTest.java | 2 + .../widgets/ParentWidgetRendererTest.java | 4 + .../widgets/ProcessWidgetRendererTest.java | 2 + .../dashboard/widgets/USMapRendererTest.java | 2 + .../core/actions/tables/DeleteActionTest.java | 2 + .../core/actions/tables/InsertActionTest.java | 228 ++++++++++++++++- .../core/actions/tables/UpdateActionTest.java | 12 +- .../qqq/backend/core/utils/TestUtils.java | 4 + .../qqq/api/javalin/QJavalinApiHandler.java | 4 - 10 files changed, 491 insertions(+), 11 deletions(-) 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 7d3d2d64..fa026f92 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 @@ -30,6 +30,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import com.kingsrook.qqq.backend.core.actions.AbstractQActionFunction; import com.kingsrook.qqq.backend.core.actions.ActionHelper; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; @@ -47,15 +48,26 @@ 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.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; /******************************************************************************* @@ -85,6 +97,8 @@ public class InsertAction extends AbstractQActionFunction requiredFields = table.getFields().values().stream() + .filter(f -> f.getIsRequired()) + .collect(Collectors.toSet()); + + if(!requiredFields.isEmpty()) + { + for(QRecord record : insertInput.getRecords()) + { + for(QFieldMetaData requiredField : requiredFields) + { + if(record.getValue(requiredField.getName()) == null || (requiredField.getType().isStringLike() && record.getValueString(requiredField.getName()).trim().equals(""))) + { + record.addError("Missing value in required field: " + requiredField.getLabel()); + } + } + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void validateSecurityFields(InsertInput insertInput) throws QException + { + QTableMetaData table = insertInput.getTable(); + List 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 join = QContext.getQInstance().getJoin(recordSecurityLock.getJoinNameChain().get(0)); // todo - many joins... + QTableMetaData joinTable = QContext.getQInstance().getTable(join.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(join.getLeftTable()); + QQueryFilter filter = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR); + queryInput.setFilter(filter); + + /////////////////////////////////////////////////////////////////////////////////////////////////// + // 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 : join.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)); + } + + 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 : join.getJoinOns()) + { + Serializable inputRecordValue = joinRecord.getValue(joinOn.getLeftField()); + joinRecordValues.add(inputRecordValue); + } + 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 = joinTable.getField(fieldName); + Serializable recordSecurityValue = joinRecord.getValue(fieldName); + + for(QRecord inputRecord : inputRecords) + { + validateRecordSecurityValue(table, inputRecord, recordSecurityLock, recordSecurityValue, field.getType()); + } + } + else + { + for(QRecord inputRecord : inputRecords) + { + inputRecord.addError("You do not have permission to insert this record - the referenced " + joinTable.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/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ChildRecordListRendererTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ChildRecordListRendererTest.java index 8232cadb..c810c1b4 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ChildRecordListRendererTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ChildRecordListRendererTest.java @@ -93,6 +93,7 @@ class ChildRecordListRendererTest extends BaseTest void testNoChildRecordsFound() throws QException { QInstance qInstance = QContext.getQInstance(); + QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true); QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(qInstance.getJoin("orderLineItem")) .withLabel("Line Items") .getWidgetMetaData(); @@ -123,6 +124,7 @@ class ChildRecordListRendererTest extends BaseTest void testChildRecordsFound() throws QException { QInstance qInstance = QContext.getQInstance(); + QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true); QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(qInstance.getJoin("orderLineItem")) .withLabel("Line Items") .getWidgetMetaData(); diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ParentWidgetRendererTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ParentWidgetRendererTest.java index db3d858d..71ed3361 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ParentWidgetRendererTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ParentWidgetRendererTest.java @@ -126,6 +126,8 @@ class ParentWidgetRendererTest extends BaseTest qInstance.addWidget(widget); reInitInstanceInContext(qInstance); + QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true); + TestUtils.insertRecords(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_ORDER), List.of( new QRecord().withValue("id", 1) )); @@ -157,6 +159,8 @@ class ParentWidgetRendererTest extends BaseTest qInstance.addWidget(widget); reInitInstanceInContext(qInstance); + QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true); + TestUtils.insertRecords(qInstance, qInstance.getTable(TestUtils.TABLE_NAME_ORDER), List.of( new QRecord().withValue("id", 1), new QRecord().withValue("id", 2) diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ProcessWidgetRendererTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ProcessWidgetRendererTest.java index 34289f8c..e4d89039 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ProcessWidgetRendererTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/ProcessWidgetRendererTest.java @@ -103,6 +103,7 @@ class ProcessWidgetRendererTest extends BaseTest void testNoChildRecordsFound() throws QException { QInstance qInstance = QContext.getQInstance(); + QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true); QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(qInstance.getJoin("orderLineItem")) .withLabel("Line Items") .getWidgetMetaData(); @@ -133,6 +134,7 @@ class ProcessWidgetRendererTest extends BaseTest void testChildRecordsFound() throws QException { QInstance qInstance = QContext.getQInstance(); + QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true); QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(qInstance.getJoin("orderLineItem")) .withLabel("Line Items") .getWidgetMetaData(); diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/USMapRendererTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/USMapRendererTest.java index 009b1ae1..e4f2d25a 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/USMapRendererTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/dashboard/widgets/USMapRendererTest.java @@ -110,6 +110,7 @@ class USMapRendererTest extends BaseTest void testNoChildRecordsFound() throws QException { QInstance qInstance = QContext.getQInstance(); + QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true); QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(qInstance.getJoin("orderLineItem")) .withLabel("Line Items") .getWidgetMetaData(); @@ -140,6 +141,7 @@ class USMapRendererTest extends BaseTest void testChildRecordsFound() throws QException { QInstance qInstance = QContext.getQInstance(); + QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true); QWidgetMetaData widget = ChildRecordListRenderer.widgetMetaDataBuilder(qInstance.getJoin("orderLineItem")) .withLabel("Line Items") .getWidgetMetaData(); diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteActionTest.java index 4145d3bc..eccc6d5c 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteActionTest.java @@ -160,6 +160,8 @@ class DeleteActionTest extends BaseTest @Test void testAssociatedDeletes() throws QException { + QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true); + { InsertInput insertInput = new InsertInput(); insertInput.setTableName(TestUtils.TABLE_NAME_ORDER); 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 05127d15..1664ea60 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 @@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.actions.tables; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import com.kingsrook.qqq.backend.core.BaseTest; import com.kingsrook.qqq.backend.core.context.QContext; @@ -31,6 +32,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock; import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; @@ -218,7 +220,7 @@ class InsertActionTest extends BaseTest void testInsertAssociations() throws QException { QInstance qInstance = QContext.getQInstance(); - QContext.getQSession().withSecurityKeyValue("storeId", 1); + QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE, 1); InsertInput insertInput = new InsertInput(); insertInput.setTableName(TestUtils.TABLE_NAME_ORDER); @@ -283,4 +285,228 @@ class InsertActionTest extends BaseTest assertEquals("LINE-VAL-3", lineItemExtrinsics.get(2).getValueString("value")); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testInsertSecurityJoins() 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 testSecurityKeyValueDenied() 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().withValue("storeId", 2))); + InsertOutput insertOutput = new InsertAction().execute(insertInput); + assertEquals("You do not have permission to insert a record with a value of 2 in the field: Store Id", insertOutput.getRecords().get(0).getErrors().get(0)); + assertEquals(0, TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_ORDER).size()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @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(qInstance, 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(qInstance, 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(qInstance, TestUtils.TABLE_NAME_ORDER).size()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testRequiredFields() throws QException + { + QInstance qInstance = QContext.getQInstance(); + qInstance.getTable(TestUtils.TABLE_NAME_ORDER).getField("orderNo").setIsRequired(true); + 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", 999).withValue("orderNo", "ORD1"), + new QRecord().withValue("storeId", 999).withValue("orderNo", " ") + )); + InsertOutput insertOutput = new InsertAction().execute(insertInput); + + //////////////////////////////////////////////////////////// + // 1st record had no value in orderNo - assert it errored // + //////////////////////////////////////////////////////////// + assertEquals(1, insertOutput.getRecords().get(0).getErrors().size()); + assertEquals("Missing value in required field: Order No", insertOutput.getRecords().get(0).getErrors().get(0)); + + /////////////////////////////////////////////// + // 2nd record had a value - it should insert // + /////////////////////////////////////////////// + assertEquals(0, insertOutput.getRecords().get(1).getErrors().size()); + assertEquals(1, TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_ORDER).size()); + + ////////////////////////////////////////////////////////////////// + // 3rd record had spaces-only in orderNo - make sure that fails // + ////////////////////////////////////////////////////////////////// + assertEquals(1, insertOutput.getRecords().get(2).getErrors().size()); + assertEquals("Missing value in required field: Order No", insertOutput.getRecords().get(2).getErrors().get(0)); + } + } 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 dc2e0e4a..afb6ea5e 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 @@ -75,7 +75,7 @@ class UpdateActionTest extends BaseTest void testUpdateAssociationsUpdateOneChild() throws QException { QInstance qInstance = QContext.getQInstance(); - QContext.getQSession().withSecurityKeyValue("storeId", 1); + QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true); insert2OrdersWith3Lines3LineExtrinsicsAnd4OrderExtrinsicAssociations(); @@ -115,7 +115,7 @@ class UpdateActionTest extends BaseTest void testUpdateAssociationsUpdateOneGrandChild() throws QException { QInstance qInstance = QContext.getQInstance(); - QContext.getQSession().withSecurityKeyValue("storeId", 1); + QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true); insert2OrdersWith3Lines3LineExtrinsicsAnd4OrderExtrinsicAssociations(); @@ -156,7 +156,7 @@ class UpdateActionTest extends BaseTest void testUpdateAssociationsDeleteOneChild() throws QException { QInstance qInstance = QContext.getQInstance(); - QContext.getQSession().withSecurityKeyValue("storeId", 1); + QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true); insert2OrdersWith3Lines3LineExtrinsicsAnd4OrderExtrinsicAssociations(); @@ -195,7 +195,7 @@ class UpdateActionTest extends BaseTest void testUpdateAssociationsDeleteGrandchildren() throws QException { QInstance qInstance = QContext.getQInstance(); - QContext.getQSession().withSecurityKeyValue("storeId", 1); + QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true); insert2OrdersWith3Lines3LineExtrinsicsAnd4OrderExtrinsicAssociations(); @@ -234,7 +234,7 @@ class UpdateActionTest extends BaseTest void testUpdateAssociationsInsertOneChild() throws QException { QInstance qInstance = QContext.getQInstance(); - QContext.getQSession().withSecurityKeyValue("storeId", 1); + QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true); insert2OrdersWith3Lines3LineExtrinsicsAnd4OrderExtrinsicAssociations(); @@ -276,7 +276,7 @@ class UpdateActionTest extends BaseTest void testUpdateAssociationsDeleteAllChildren() throws QException { QInstance qInstance = QContext.getQInstance(); - QContext.getQSession().withSecurityKeyValue("storeId", 1); + QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true); insert2OrdersWith3Lines3LineExtrinsicsAnd4OrderExtrinsicAssociations(); diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java index cbeabf93..dcfb9388 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/TestUtils.java @@ -571,6 +571,10 @@ public class TestUtils .withName(TABLE_NAME_LINE_ITEM) .withBackendName(MEMORY_BACKEND_NAME) .withPrimaryKeyField("id") + .withRecordSecurityLock(new RecordSecurityLock() + .withSecurityKeyType(SECURITY_KEY_TYPE_STORE) + .withFieldName("order.storeId") + .withJoinNameChain(List.of("orderLineItem"))) .withAssociation(new Association().withName("extrinsics").withAssociatedTableName(TABLE_NAME_LINE_ITEM_EXTRINSIC).withJoinName("lineItemLineItemExtrinsic")) .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false)) diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java index f88349b5..a57548f8 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java @@ -383,8 +383,6 @@ public class QJavalinApiHandler QJavalinAccessLogger.logStart("apiGet", logPair("table", tableName), logPair("primaryKey", primaryKey)); getInput.setTableName(tableName); - // i think not for api... getInput.setShouldGenerateDisplayValues(true); - getInput.setShouldTranslatePossibleValues(true); PermissionsHelper.checkTablePermissionThrowing(getInput, TablePermissionSubType.READ); @@ -441,8 +439,6 @@ public class QJavalinApiHandler QJavalinAccessLogger.logStart("apiQuery", logPair("table", tableName)); queryInput.setTableName(tableName); - //? queryInput.setShouldGenerateDisplayValues(true); - //? queryInput.setShouldTranslatePossibleValues(true); PermissionsHelper.checkTablePermissionThrowing(queryInput, TablePermissionSubType.READ);