From 0d0ab6c2e5bdab4a1c1e6514efe82de30e896a62 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 15 Aug 2023 16:55:36 -0500 Subject: [PATCH] CE-567 Add concept of security lock Scope - e.g., READ-WRITE (blocking all access to a record), or just WRITE - which means anyone can read, but you must have the key to WRITE. --- .../core/actions/audits/AuditAction.java | 3 +- .../core/actions/audits/DMLAuditAction.java | 3 +- .../core/actions/tables/DeleteAction.java | 3 + .../core/actions/tables/UpdateAction.java | 25 +++++- .../ValidateRecordSecurityLockHelper.java | 26 ++++-- .../core/instances/QInstanceValidator.java | 2 + .../actions/audits/AuditSingleInput.java | 3 +- .../actions/tables/query/JoinsContext.java | 3 +- .../metadata/security/RecordSecurityLock.java | 48 ++++++++++ .../security/RecordSecurityLockFilters.java | 80 +++++++++++++++++ .../StoreScriptRevisionProcessStep.java | 23 ++++- .../core/actions/tables/DeleteActionTest.java | 31 +++++++ .../core/actions/tables/InsertActionTest.java | 59 +++++++++++++ .../core/actions/tables/UpdateActionTest.java | 87 ++++++++++++++++++- .../qqq/backend/core/utils/TestUtils.java | 41 +++++++++ .../rdbms/actions/AbstractRDBMSAction.java | 7 +- 16 files changed, 420 insertions(+), 24 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/RecordSecurityLockFilters.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/AuditAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/AuditAction.java index 41015e57..b8b3d4e0 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/AuditAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/audits/AuditAction.java @@ -44,6 +44,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.security.RecordSecurityLock; +import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.session.QUser; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; @@ -166,7 +167,7 @@ public class AuditAction extends AbstractQActionFunction getRecordSecurityKeyValues(QTableMetaData table, QRecord record) { Map securityKeyValues = new HashMap<>(); - for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks())) + for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks()))) { securityKeyValues.put(recordSecurityLock.getSecurityKeyType(), record == null ? null : record.getValue(recordSecurityLock.getFieldName())); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteAction.java index ca9badc9..ee49499a 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/DeleteAction.java @@ -40,6 +40,7 @@ import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPreDeleteCusto 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.interfaces.DeleteInterface; +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.LogPair; @@ -321,6 +322,8 @@ public class DeleteAction QTableMetaData table = deleteInput.getTable(); List primaryKeysNotFound = validateRecordsExistAndCanBeAccessed(deleteInput, oldRecordList.get()); + ValidateRecordSecurityLockHelper.validateSecurityFields(table, oldRecordList.get(), ValidateRecordSecurityLockHelper.Action.DELETE); + /////////////////////////////////////////////////////////////////////////// // after all validations, run the pre-delete customizer, if there is one // /////////////////////////////////////////////////////////////////////////// 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 c45197f7..a36decd0 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 @@ -61,6 +61,8 @@ 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.RecordSecurityLock; +import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters; 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.statusmessages.BadInputStatusMessage; @@ -209,14 +211,16 @@ public class UpdateAction { validateRecordsExistAndCanBeAccessed(updateInput, oldRecordList.get()); } + else + { + ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE); + } if(updateInput.getInputSource().shouldValidateRequiredFields()) { validateRequiredFields(updateInput); } - ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE); - /////////////////////////////////////////////////////////////////////////// // after all validations, run the pre-update customizer, if there is one // /////////////////////////////////////////////////////////////////////////// @@ -287,6 +291,8 @@ public class UpdateAction QTableMetaData table = updateInput.getTable(); QFieldMetaData primaryKeyField = table.getField(table.getPrimaryKeyField()); + List onlyWriteLocks = RecordSecurityLockFilters.filterForOnlyWriteLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks())); + for(List page : CollectionUtils.getPages(updateInput.getRecords(), 1000)) { List primaryKeysToLookup = new ArrayList<>(); @@ -320,6 +326,8 @@ public class UpdateAction } } + ValidateRecordSecurityLockHelper.validateSecurityFields(table, updateInput.getRecords(), ValidateRecordSecurityLockHelper.Action.UPDATE); + for(QRecord record : page) { Serializable value = ValueUtils.getValueAsFieldType(primaryKeyField.getType(), record.getValue(table.getPrimaryKeyField())); @@ -332,6 +340,19 @@ public class UpdateAction { record.addError(new NotFoundStatusMessage("No record was found to update for " + primaryKeyField.getLabel() + " = " + value)); } + else + { + /////////////////////////////////////////////////////////////////////////////////////////// + // if the table has any write-only locks, validate their values here, on the old-records // + /////////////////////////////////////////////////////////////////////////////////////////// + for(RecordSecurityLock lock : onlyWriteLocks) + { + QRecord oldRecord = lookedUpRecords.get(value); + QFieldType fieldType = table.getField(lock.getFieldName()).getType(); + Serializable lockValue = ValueUtils.getValueAsFieldType(fieldType, oldRecord.getValue(lock.getFieldName())); + ValidateRecordSecurityLockHelper.validateRecordSecurityValue(table, record, lock, lockValue, fieldType, ValidateRecordSecurityLockHelper.Action.UPDATE); + } + } } } } 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 index 041ed54a..e94c9b5a 100644 --- 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 @@ -44,6 +44,7 @@ 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.security.RecordSecurityLockFilters; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.statusmessages.PermissionDeniedMessage; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; @@ -68,6 +69,7 @@ public class ValidateRecordSecurityLockHelper { INSERT, UPDATE, + DELETE, SELECT } @@ -78,7 +80,7 @@ public class ValidateRecordSecurityLockHelper *******************************************************************************/ public static void validateSecurityFields(QTableMetaData table, List records, Action action) throws QException { - List locksToCheck = getRecordSecurityLocks(table); + List locksToCheck = getRecordSecurityLocks(table, action); if(CollectionUtils.nullSafeIsEmpty(locksToCheck)) { return; @@ -98,11 +100,12 @@ public class ValidateRecordSecurityLockHelper for(QRecord record : records) { - if(action.equals(Action.UPDATE) && !record.getValues().containsKey(field.getName())) + if(action.equals(Action.UPDATE) && !record.getValues().containsKey(field.getName()) && RecordSecurityLock.LockScope.READ_AND_WRITE.equals(recordSecurityLock.getLockScope())) { - ///////////////////////////////////////////////////////////////////////// - // if not updating the security field, then no error can come from it! // - ///////////////////////////////////////////////////////////////////////// + ///////////////////////////////////////////////////////////////////////////////////////////////////////// + // if this is a read-write lock, then if we have the record, it means we were able to read the record. // + // So if we're not updating the security field, then no error can come from it! // + ///////////////////////////////////////////////////////////////////////////////////////////////////////// continue; } @@ -244,11 +247,18 @@ public class ValidateRecordSecurityLockHelper /******************************************************************************* ** *******************************************************************************/ - private static List getRecordSecurityLocks(QTableMetaData table) + private static List getRecordSecurityLocks(QTableMetaData table, Action action) { - List recordSecurityLocks = table.getRecordSecurityLocks(); + List recordSecurityLocks = CollectionUtils.nonNullList(table.getRecordSecurityLocks()); List locksToCheck = new ArrayList<>(); + recordSecurityLocks = switch(action) + { + case INSERT, UPDATE, DELETE -> RecordSecurityLockFilters.filterForWriteLocks(recordSecurityLocks); + case SELECT -> RecordSecurityLockFilters.filterForReadLocks(recordSecurityLocks); + default -> throw (new IllegalArgumentException("Unsupported action: " + action)); + }; + //////////////////////////////////////// // if there are no locks, just return // //////////////////////////////////////// @@ -281,7 +291,7 @@ public class ValidateRecordSecurityLockHelper /******************************************************************************* ** *******************************************************************************/ - static void validateRecordSecurityValue(QTableMetaData table, QRecord record, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action) + public static void validateRecordSecurityValue(QTableMetaData table, QRecord record, RecordSecurityLock recordSecurityLock, Serializable recordSecurityValue, QFieldType fieldType, Action action) { if(recordSecurityValue == null) { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java index e18cb182..baa5a67e 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidator.java @@ -586,6 +586,8 @@ public class QInstanceValidator prefix = "Table " + table.getName() + " recordSecurityLock (of key type " + securityKeyTypeName + ") "; + assertCondition(recordSecurityLock.getLockScope() != null, prefix + " is missing its lockScope"); + boolean hasAnyBadJoins = false; for(String joinName : CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain())) { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/audits/AuditSingleInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/audits/AuditSingleInput.java index 2711f3d4..655d31d0 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/audits/AuditSingleInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/audits/AuditSingleInput.java @@ -30,6 +30,7 @@ import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock; +import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; @@ -246,7 +247,7 @@ public class AuditSingleInput setAuditTableName(table.getName()); this.securityKeyValues = new HashMap<>(); - for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks())) + for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks()))) { this.securityKeyValues.put(recordSecurityLock.getFieldName(), record.getValueInteger(recordSecurityLock.getFieldName())); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/JoinsContext.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/JoinsContext.java index 3d39455a..b48547f0 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/JoinsContext.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/JoinsContext.java @@ -37,6 +37,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock; +import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLockFilters; import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; @@ -81,7 +82,7 @@ public class JoinsContext /////////////////////////////////////////////////////////////// // ensure any joins that contribute a recordLock are present // /////////////////////////////////////////////////////////////// - for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(instance.getTable(tableName).getRecordSecurityLocks())) + for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(instance.getTable(tableName).getRecordSecurityLocks()))) { /////////////////////////////////////////////////////////////////////////////////////////////////// // ok - so - the join name chain is going to be like this: // diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/RecordSecurityLock.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/RecordSecurityLock.java index beac4587..06633998 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/RecordSecurityLock.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/RecordSecurityLock.java @@ -34,6 +34,10 @@ import java.util.List; ** - recordSecurityLock.fieldName = order.clientId ** - recordSecurityLock.joinNameChain = [orderJoinOrderLineItem, orderLineItemJoinOrderLineItemExtrinsic] ** that is - what's the chain that takes us FROM the security fieldName TO the table with the lock. + ** + ** LockScope controls what the lock prevents users from doing without a valid key. + ** - READ_AND_WRITE means that users cannot read or write records without a valid key. + ** - WRITE means that users cannot write records without a valid key (but they can read them). *******************************************************************************/ public class RecordSecurityLock { @@ -42,6 +46,8 @@ public class RecordSecurityLock private List joinNameChain; private NullValueBehavior nullValueBehavior = NullValueBehavior.DENY; + private LockScope lockScope = LockScope.READ_AND_WRITE; + /******************************************************************************* @@ -66,6 +72,17 @@ public class RecordSecurityLock + /******************************************************************************* + ** + *******************************************************************************/ + public enum LockScope + { + READ_AND_WRITE, + WRITE + } + + + /******************************************************************************* ** Getter for securityKeyType *******************************************************************************/ @@ -188,4 +205,35 @@ public class RecordSecurityLock return (this); } + + + /******************************************************************************* + ** Getter for lockScope + *******************************************************************************/ + public LockScope getLockScope() + { + return (this.lockScope); + } + + + + /******************************************************************************* + ** Setter for lockScope + *******************************************************************************/ + public void setLockScope(LockScope lockScope) + { + this.lockScope = lockScope; + } + + + + /******************************************************************************* + ** Fluent setter for lockScope + *******************************************************************************/ + public RecordSecurityLock withLockScope(LockScope lockScope) + { + this.lockScope = lockScope; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/RecordSecurityLockFilters.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/RecordSecurityLockFilters.java new file mode 100644 index 00000000..c8c7e9dc --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/RecordSecurityLockFilters.java @@ -0,0 +1,80 @@ +/* + * 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.model.metadata.security; + + +import java.util.List; + + +/******************************************************************************* + ** standard filtering operations for lists of record security locks. + *******************************************************************************/ +public class RecordSecurityLockFilters +{ + + /******************************************************************************* + ** filter a list of locks so that we only see the ones that apply to reads. + *******************************************************************************/ + public static List filterForReadLocks(List recordSecurityLocks) + { + if(recordSecurityLocks == null) + { + return (null); + } + + return (recordSecurityLocks.stream().filter(rsl -> RecordSecurityLock.LockScope.READ_AND_WRITE.equals(rsl.getLockScope())).toList()); + } + + + + /******************************************************************************* + ** filter a list of locks so that we only see the ones that apply to writes. + *******************************************************************************/ + public static List filterForWriteLocks(List recordSecurityLocks) + { + if(recordSecurityLocks == null) + { + return (null); + } + + return (recordSecurityLocks.stream().filter(rsl -> + RecordSecurityLock.LockScope.READ_AND_WRITE.equals(rsl.getLockScope()) + || RecordSecurityLock.LockScope.WRITE.equals(rsl.getLockScope() + )).toList()); + } + + + + /******************************************************************************* + ** filter a list of locks so that we only see the ones that are WRITE type only. + *******************************************************************************/ + public static List filterForOnlyWriteLocks(List recordSecurityLocks) + { + if(recordSecurityLocks == null) + { + return (null); + } + + return (recordSecurityLocks.stream().filter(rsl -> RecordSecurityLock.LockScope.WRITE.equals(rsl.getLockScope())).toList()); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/scripts/StoreScriptRevisionProcessStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/scripts/StoreScriptRevisionProcessStep.java index f7b94ab9..4893920a 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/scripts/StoreScriptRevisionProcessStep.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/scripts/StoreScriptRevisionProcessStep.java @@ -31,7 +31,10 @@ import com.kingsrook.qqq.backend.core.actions.tables.GetAction; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; +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.exceptions.QPermissionDeniedException; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput; @@ -46,6 +49,8 @@ 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.actions.tables.update.UpdateInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.scripts.Script; +import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevision; import com.kingsrook.qqq.backend.core.model.scripts.ScriptRevisionFile; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; @@ -69,7 +74,7 @@ public class StoreScriptRevisionProcessStep implements BackendStep { InsertAction insertAction = new InsertAction(); InsertInput insertInput = new InsertInput(); - insertInput.setTableName("scriptRevision"); + insertInput.setTableName(ScriptRevision.TABLE_NAME); QBackendTransaction transaction = insertAction.openTransaction(insertInput); insertInput.setTransaction(transaction); @@ -87,14 +92,23 @@ public class StoreScriptRevisionProcessStep implements BackendStep // get the existing script, to update // //////////////////////////////////////// GetInput getInput = new GetInput(); - getInput.setTableName("script"); + getInput.setTableName(Script.TABLE_NAME); getInput.setPrimaryKey(scriptId); getInput.setTransaction(transaction); GetOutput getOutput = new GetAction().execute(getInput); QRecord script = getOutput.getRecord(); + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // in case the app added a security field to the scripts table, make sure the user is allowed to edit the script // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + ValidateRecordSecurityLockHelper.validateSecurityFields(QContext.getQInstance().getTable(Script.TABLE_NAME), List.of(script), ValidateRecordSecurityLockHelper.Action.UPDATE); + if(CollectionUtils.nullSafeHasContents(script.getErrors())) + { + throw (new QPermissionDeniedException(script.getErrors().get(0).getMessage())); + } + QueryInput queryInput = new QueryInput(); - queryInput.setTableName("scriptRevision"); + queryInput.setTableName(ScriptRevision.TABLE_NAME); queryInput.setFilter(new QQueryFilter() .withCriteria(new QFilterCriteria("scriptId", QCriteriaOperator.EQUALS, List.of(script.getValue("id")))) .withOrderBy(new QFilterOrderBy("sequenceNo", false)) @@ -183,7 +197,7 @@ public class StoreScriptRevisionProcessStep implements BackendStep //////////////////////////////////////////////////// script.setValue("currentScriptRevisionId", scriptRevision.getValue("id")); UpdateInput updateInput = new UpdateInput(); - updateInput.setTableName("script"); + updateInput.setTableName(Script.TABLE_NAME); updateInput.setRecords(List.of(script)); updateInput.setTransaction(transaction); new UpdateAction().execute(updateInput); @@ -198,6 +212,7 @@ public class StoreScriptRevisionProcessStep implements BackendStep catch(Exception e) { transaction.rollback(); + throw (e); } finally { 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 076a3f39..b2abd0a8 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 @@ -53,6 +53,7 @@ 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.assertThrows; @@ -365,6 +366,36 @@ class DeleteActionTest extends BaseTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSecurityLockWriteScope() throws QException + { + TestUtils.updatePersonMemoryTableInContextWithWritableByWriteLockAndInsert3TestRecords(); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////// + // try to delete 1, 2, and 3. 2 should be blocked, because it has a writable-By that isn't in our session // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////// + QContext.getQSession().setSecurityKeyValues(MapBuilder.of("writableBy", ListBuilder.of("jdoe"))); + DeleteInput deleteInput = new DeleteInput(); + deleteInput.setTableName(TestUtils.TABLE_NAME_PERSON_MEMORY); + deleteInput.setPrimaryKeys(List.of(1, 2, 3)); + DeleteOutput deleteOutput = new DeleteAction().execute(deleteInput); + + assertEquals(1, deleteOutput.getRecordsWithErrors().size()); + assertThat(deleteOutput.getRecordsWithErrors().get(0).getErrors().get(0).getMessage()) + .contains("You do not have permission") + .contains("kmarsh") + .contains("Only Writable By"); + + assertEquals(1, new CountAction().execute(new CountInput(TestUtils.TABLE_NAME_PERSON_MEMORY)).getCount()); + assertEquals(1, new CountAction().execute(new CountInput(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.EQUALS, 2)))).getCount()); + } + + + /******************************************************************************* ** *******************************************************************************/ 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 f83b10c0..2fa44957 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 @@ -35,9 +35,12 @@ 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.model.statusmessages.QErrorMessage; import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; 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.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -692,4 +695,60 @@ class InsertActionTest extends BaseTest assertEquals(3, TestUtils.queryTable(qInstance, TestUtils.TABLE_NAME_ORDER).size()); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSecurityLockWriteScope() throws QException + { + TestUtils.updatePersonMemoryTableInContextWithWritableByWriteLockAndInsert3TestRecords(); + + QContext.getQSession().setSecurityKeyValues(MapBuilder.of("writableBy", ListBuilder.of("hsimpson"))); + + ///////////////////////////////////////////////////////////////////////////////////////// + // with only hsimpson in our key, make sure we can't insert a row w/ a different value // + ///////////////////////////////////////////////////////////////////////////////////////// + { + InsertOutput insertOutput = new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withRecords(List.of( + new QRecord().withValue("id", 100).withValue("firstName", "Jean-Luc").withValue("onlyWritableBy", "jkirk") + ))); + List errors = insertOutput.getRecords().get(0).getErrors(); + assertEquals(1, errors.size()); + assertThat(errors.get(0).getMessage()) + .contains("You do not have permission") + .contains("jkirk") + .contains("Only Writable By"); + } + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // make sure we can insert w/ a null in onlyWritableBy (because key (from test utils) was set to allow null) // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////// + { + InsertOutput insertOutput = new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withRecords(List.of( + new QRecord().withValue("id", 101).withValue("firstName", "Benajamin").withValue("onlyWritableBy", null) + ))); + List errors = insertOutput.getRecords().get(0).getErrors(); + assertEquals(0, errors.size()); + } + + /////////////////////////////////////////////////////////////////////////////// + // change the null behavior to deny, and try above again, expecting an error // + /////////////////////////////////////////////////////////////////////////////// + QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).getRecordSecurityLocks().get(0).setNullValueBehavior(RecordSecurityLock.NullValueBehavior.DENY); + { + InsertOutput insertOutput = new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withRecords(List.of( + new QRecord().withValue("id", 102).withValue("firstName", "Katherine").withValue("onlyWritableBy", null) + ))); + List errors = insertOutput.getRecords().get(0).getErrors(); + assertEquals(1, errors.size()); + assertThat(errors.get(0).getMessage()) + .contains("You do not have permission") + .contains("without a value") + .contains("Only Writable By"); + } + + } + } 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 364f1415..27878e2f 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,12 +29,18 @@ import java.util.Objects; 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.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +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.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.model.metadata.security.RecordSecurityLock; +import com.kingsrook.qqq.backend.core.model.statusmessages.QErrorMessage; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; 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; @@ -492,7 +498,7 @@ class UpdateActionTest extends BaseTest updateInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM); updateInput.setRecords(List.of(new QRecord().withValue("id", 20).withValue("sku", "BASIC3"))); UpdateOutput updateOutput = new UpdateAction().execute(updateInput); - assertEquals("No record was found to update for Id = 20", updateOutput.getRecords().get(0).getErrors().get(0).getMessage()); + assertTrue(updateOutput.getRecords().get(0).getErrors().stream().anyMatch(em -> em.getMessage().equals("No record was found to update for Id = 20"))); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -504,7 +510,7 @@ class UpdateActionTest extends BaseTest updateInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM); updateInput.setRecords(List.of(new QRecord().withValue("id", 10).withValue("orderId", 2).withValue("sku", "BASIC3"))); UpdateOutput updateOutput = new UpdateAction().execute(updateInput); - assertEquals("You do not have permission to update this record - the referenced Order was not found.", updateOutput.getRecords().get(0).getErrors().get(0).getMessage()); + assertTrue(updateOutput.getRecords().get(0).getErrors().stream().anyMatch(em -> em.getMessage().equals("You do not have permission to update this record - the referenced Order was not found."))); } /////////////////////////////////////////////////////////// @@ -528,7 +534,7 @@ class UpdateActionTest extends BaseTest updateInput.setTableName(TestUtils.TABLE_NAME_LINE_ITEM_EXTRINSIC); updateInput.setRecords(List.of(new QRecord().withValue("id", 200).withValue("key", "updatedKey"))); UpdateOutput updateOutput = new UpdateAction().execute(updateInput); - assertEquals("No record was found to update for Id = 200", updateOutput.getRecords().get(0).getErrors().get(0).getMessage()); + assertTrue(updateOutput.getRecords().get(0).getErrors().stream().anyMatch(em -> em.getMessage().equals("No record was found to update for Id = 200"))); } ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -706,4 +712,79 @@ class UpdateActionTest extends BaseTest assertEquals(1, TestUtils.queryTable(TestUtils.TABLE_NAME_ORDER).stream().filter(r -> r.getValue("storeId") == null).count()); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSecurityLockWriteScope() throws QException + { + TestUtils.updatePersonMemoryTableInContextWithWritableByWriteLockAndInsert3TestRecords(); + + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // try to update them all 1, 2, and 3. 2 should be blocked, because it has a writable-By that isn't in our session // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + { + QContext.getQSession().setSecurityKeyValues(MapBuilder.of("writableBy", ListBuilder.of("jdoe"))); + UpdateOutput updateOutput = new UpdateAction().execute(new UpdateInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withRecords(List.of( + new QRecord().withValue("id", 1).withValue("lastName", "Kelkhoff"), + new QRecord().withValue("id", 2).withValue("lastName", "Chamberlain"), + new QRecord().withValue("id", 3).withValue("lastName", "Maes") + ))); + + List errorRecords = updateOutput.getRecords().stream().filter(r -> CollectionUtils.nullSafeHasContents(r.getErrors())).toList(); + assertEquals(1, errorRecords.size()); + assertEquals(2, errorRecords.get(0).getValueInteger("id")); + assertThat(errorRecords.get(0).getErrors().get(0).getMessage()) + .contains("You do not have permission") + .contains("kmarsh") + .contains("Only Writable By"); + + assertEquals(2, new CountAction().execute(new CountInput(TestUtils.TABLE_NAME_PERSON_MEMORY) + .withFilter(new QQueryFilter(new QFilterCriteria("lastName", QCriteriaOperator.IS_NOT_BLANK)))).getCount()); + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // now try to change one of the records to have a different value in the lock-field. Should fail (as it's not in our session) // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + { + UpdateOutput updateOutput = new UpdateAction().execute(new UpdateInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withRecords(List.of( + new QRecord().withValue("id", 1).withValue("onlyWritableBy", "ecartman")))); + + List errorRecords = updateOutput.getRecords().stream().filter(r -> CollectionUtils.nullSafeHasContents(r.getErrors())).toList(); + assertEquals(1, errorRecords.size()); + assertThat(errorRecords.get(0).getErrors().get(0).getMessage()) + .contains("You do not have permission") + .contains("ecartman") + .contains("Only Writable By"); + } + + /////////////////////////////////////////////////////////////// + // add that to our session and confirm we can do that update // + /////////////////////////////////////////////////////////////// + { + QContext.getQSession().setSecurityKeyValues(MapBuilder.of("writableBy", ListBuilder.of("jdoe", "ecartman"))); + UpdateOutput updateOutput = new UpdateAction().execute(new UpdateInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withRecords(List.of( + new QRecord().withValue("id", 1).withValue("onlyWritableBy", "ecartman")))); + List errorRecords = updateOutput.getRecords().stream().filter(r -> CollectionUtils.nullSafeHasContents(r.getErrors())).toList(); + assertEquals(0, errorRecords.size()); + } + + ///////////////////////////////////////////////////////////////////////////////////////////////// + // change the null behavior to deny, then try to udpate a record and remove its onlyWritableBy // + ///////////////////////////////////////////////////////////////////////////////////////////////// + QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON_MEMORY).getRecordSecurityLocks().get(0).setNullValueBehavior(RecordSecurityLock.NullValueBehavior.DENY); + { + UpdateOutput updateOutput = new UpdateAction().execute(new UpdateInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withRecords(List.of( + new QRecord().withValue("id", 1).withValue("onlyWritableBy", null)))); + List errors = updateOutput.getRecords().get(0).getErrors(); + assertEquals(1, errors.size()); + assertThat(errors.get(0).getMessage()) + .contains("You do not have permission") + .contains("without a value") + .contains("Only Writable By"); + } + } + } 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 a38be542..152c8d22 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 @@ -33,15 +33,18 @@ import com.kingsrook.qqq.backend.core.actions.dashboard.PersonsByCreateDateBarCh import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.AddAge; import com.kingsrook.qqq.backend.core.actions.processes.person.addtopeoplesage.GetAgeStatistics; +import com.kingsrook.qqq.backend.core.actions.tables.CountAction; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.actions.tables.UpdateAction; import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider; +import com.kingsrook.qqq.backend.core.context.QContext; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.instances.QMetaDataVariableInterpreter; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.count.CountInput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; @@ -110,6 +113,9 @@ import com.kingsrook.qqq.backend.core.processes.implementations.etl.basic.BasicE import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamed.StreamedETLProcess; import com.kingsrook.qqq.backend.core.processes.implementations.mock.MockBackendStep; import com.kingsrook.qqq.backend.core.processes.implementations.reports.RunReportForRecordProcess; +import com.kingsrook.qqq.backend.core.utils.collections.ListBuilder; +import com.kingsrook.qqq.backend.core.utils.collections.MapBuilder; +import static org.junit.jupiter.api.Assertions.assertEquals; /******************************************************************************* @@ -1393,4 +1399,39 @@ public class TestUtils )) ); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void updatePersonMemoryTableInContextWithWritableByWriteLockAndInsert3TestRecords() throws QException + { + QInstance qInstance = QContext.getQInstance(); + qInstance.addSecurityKeyType(new QSecurityKeyType() + .withName("writableBy")); + + QTableMetaData table = qInstance.getTable(TestUtils.TABLE_NAME_PERSON_MEMORY); + table.withField(new QFieldMetaData("onlyWritableBy", QFieldType.STRING).withLabel("Only Writable By")); + table.withRecordSecurityLock(new RecordSecurityLock() + .withSecurityKeyType("writableBy") + .withFieldName("onlyWritableBy") + .withNullValueBehavior(RecordSecurityLock.NullValueBehavior.ALLOW) + .withLockScope(RecordSecurityLock.LockScope.WRITE)); + + QContext.getQSession().setSecurityKeyValues(MapBuilder.of("writableBy", ListBuilder.of("jdoe", "kmarsh"))); + + new InsertAction().execute(new InsertInput(TestUtils.TABLE_NAME_PERSON_MEMORY).withRecords(List.of( + new QRecord().withValue("id", 1).withValue("firstName", "Darin"), + new QRecord().withValue("id", 2).withValue("firstName", "Tim").withValue("onlyWritableBy", "kmarsh"), + new QRecord().withValue("id", 3).withValue("firstName", "James").withValue("onlyWritableBy", "jdoe") + ))); + + ////////////////////////////////////////////// + // make sure we can query for all 3 records // + ////////////////////////////////////////////// + QContext.getQSession().setSecurityKeyValues(MapBuilder.of("writableBy", ListBuilder.of("jdoe"))); + assertEquals(3, new CountAction().execute(new CountInput(TestUtils.TABLE_NAME_PERSON_MEMORY)).getCount()); + } + } 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 1301f2d5..8d223435 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 @@ -70,6 +70,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinType; 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.security.RecordSecurityLockFilters; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.querystats.QueryStat; import com.kingsrook.qqq.backend.core.model.session.QSession; @@ -387,7 +388,7 @@ public abstract class AbstractRDBMSAction implements QActionInterface QQueryFilter securityFilter = new QQueryFilter(); securityFilter.setBooleanOperator(QQueryFilter.BooleanOperator.AND); - for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks())) + for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks()))) { // todo - uh, if it's a RIGHT (or FULL) join, then, this should be isOuter = true, right? boolean isOuter = false; @@ -407,7 +408,7 @@ public abstract class AbstractRDBMSAction implements QActionInterface } QTableMetaData joinTable = instance.getTable(queryJoin.getJoinTable()); - for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(joinTable.getRecordSecurityLocks())) + for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(joinTable.getRecordSecurityLocks()))) { boolean isOuter = queryJoin.getType().equals(QueryJoin.Type.LEFT); // todo full? addSubFilterForRecordSecurityLock(instance, session, joinTable, securityFilter, recordSecurityLock, joinsContext, queryJoin.getJoinTableOrItsAlias(), isOuter); @@ -1035,7 +1036,7 @@ public abstract class AbstractRDBMSAction implements QActionInterface { if(table != null) { - for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks())) + for(RecordSecurityLock recordSecurityLock : RecordSecurityLockFilters.filterForReadLocks(CollectionUtils.nonNullList(table.getRecordSecurityLocks()))) { for(String joinName : CollectionUtils.nonNullList(recordSecurityLock.getJoinNameChain())) {