diff --git a/docs/metaData/SecurtiyKeyTypes.adoc b/docs/metaData/SecurtiyKeyTypes.adoc index acd6a782..23c4d8f3 100644 --- a/docs/metaData/SecurtiyKeyTypes.adoc +++ b/docs/metaData/SecurtiyKeyTypes.adoc @@ -2,16 +2,79 @@ == Security Key Types include::../variables.adoc[] -#TODO# +In QQQ, record-level security is provided by using a lock & key metaphor. + +The use-case being handled here is: + +* A user has full permission on a table (query, insert, update, and delete). +* However, they should only be allowed to read a sub-set of the rows in the table. +** e.g., maybe it's a multi-tenant system, or the table has user-specific records. + +The lock & key metaphor is realized by the user being associated with one or more "Keys" +(as values in their session), and records in tables being associated with one or more "Locks" +(as values in fields). +A user is only allowed to access records where the user's key(s) match the record's security lock(s). + +For a practical example, picture a multi-tenant Order Management System,where all orders are assigned to a "client". +Users (customers) should only be able to see orders associated with the client which that user works for. + +In this scenario, the `order` table would have a "lock" on its `clientId` field. +Customer-users would have a `clientId` key in their session. +When the QQQ backend did a search for records (e.g., an SQL query) it would implicitly +(without any code being written by the application developer) filter the table to only +allow the user to see records with their `clientId`. + +To implement this scenario, the application would define the following pieces of meta-data: + +* At the QQQ-Instance level, a `SecurityKeyType`, +to define a domain of possible locks & keys within an application. +** An application can define multiple Security Key Types. +For example, maybe `clientId` and `userId` as key types. +* At the per-table level, a `RecordSecurityLock`, +which references a security key type, and how that key type should be applied to the table. +** For example, what field stores the `clientId` value in the `order` table. +* Finally, when a user's session is constructed via a QQQ Authentication provider, +security key values are set, based on data from the authentication backend. + +=== Additional Scenarios + +==== All Access Key +A "super-user" may be allowed to access all records in a table regardless of their record locks, +if the Security Key Type specifies an `allAccessKeyName`, +and if the user has a key in their session with that key name, and a value of `true`. +Going back to the lock & key metaphor, this can be thought of as a "skeleton key", +that can unlock any record lock (of the security key's type). + +==== Null Value Behaviors +In a record security lock, different behaviors can be defined for handling rows with a null key value. + +For example: + +* Sometimes orders may be loaded into the OMS system described above, where the application doesn't yet know what client the order belongs to. +In this case, the application may need to ensure that such records, with a `null` value in `clientId` are hidden from customer-users, +to avoid potentially leaking a different client's data. +** This can be accomplished with a record security lock on the `order` table, with a `nullValueBehavior` of `DENY`. +** Furthermore, if internal/admin users _should_ be given access to such records, then the security key type can be +configured with a `nullValueBehaviorKeyName` (e.g., `"clientIdNullValueBehavior"`), which can be set per-user to allow +access to records, overriding the table lock's specified `nullValueBehavior`. +*** This could also be done by giving internal/admin users an `allAccessKey`, but sometimes that is not what is required. + +* Maybe a warehouse locations table is assigned a `clientId` once inventory for a client is placed in the location, +at which point in time, only the client's users should be allowed to see the record. +But, if no client has been assigned to the location, and `clientId` is `null`, +then you may want to allow any user to see such records. +** This can be accomplished with a record security lock on the Warehouse Locations table, with a `nullValueBehavior` of `ALLOW`. === QSecurityKeyType A Security Key Type is defined in a QQQ Instance in a `*QSecurityKeyType*` object. -#TODO# - *QSecurityKeyType Properties:* -* `name` - *String, Required* - Unique name for the security key type within the QQQ Instance. +* `name` - *String, Required* - Unique name for this security key type within the QQQ Instance. +* `allAccessKeyName` - *String* - Optional name of the all-access security key associated with this key type. +* `nullValueBehaviorKeyName` - *String* - Optional name of the null-value-behavior overriding security key associated with this key type. +** Note, `name`, `allAccessKeyName`, and `nullValueBehaviorKeyName` are all checked against each other for uniqueness. +A `QInstanceValidationException` will be thrown if any name collisions occur. +* `possibleValueSourceName` - *String* - Optional reference to a possible value source from which value for the key can come. -#TODO# 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 e94c9b5a..1f235999 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 @@ -42,6 +42,7 @@ 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.NullValueBehaviorUtil; 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; @@ -230,7 +231,7 @@ public class ValidateRecordSecurityLockHelper { for(QRecord inputRecord : inputRecords) { - if(RecordSecurityLock.NullValueBehavior.DENY.equals(recordSecurityLock.getNullValueBehavior())) + if(RecordSecurityLock.NullValueBehavior.DENY.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock))) { inputRecord.addError(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " this record - the referenced " + leftMostJoinTable.getLabel() + " was not found.")); } @@ -298,7 +299,7 @@ public class ValidateRecordSecurityLockHelper ///////////////////////////////////////////////////////////////// // handle null values - error if the NullValueBehavior is DENY // ///////////////////////////////////////////////////////////////// - if(RecordSecurityLock.NullValueBehavior.DENY.equals(recordSecurityLock.getNullValueBehavior())) + if(RecordSecurityLock.NullValueBehavior.DENY.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock))) { String lockLabel = CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinNameChain()) ? recordSecurityLock.getSecurityKeyType() : table.getField(recordSecurityLock.getFieldName()).getLabel(); record.addError(new PermissionDeniedMessage("You do not have permission to " + action.name().toLowerCase() + " a record without a value in the field: " + lockLabel)); 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 2e7c5797..66529619 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 @@ -50,6 +50,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.QSupplementalInstanceMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType; @@ -85,6 +86,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.Automatio import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails; import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheOf; import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheUseCase; +import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleCustomizerInterface; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; @@ -208,14 +210,23 @@ public class QInstanceValidator if(assertCondition(StringUtils.hasContent(securityKeyType.getName()), "Missing name for a securityKeyType")) { assertCondition(Objects.equals(name, securityKeyType.getName()), "Inconsistent naming for securityKeyType: " + name + "/" + securityKeyType.getName() + "."); - assertCondition(!usedNames.contains(name), "More than one SecurityKeyType with name (or allAccessKeyName) of: " + name); + + String duplicateNameMessagePrefix = "More than one SecurityKeyType with name (or allAccessKeyName or nullValueBehaviorKeyName) of: "; + assertCondition(!usedNames.contains(name), duplicateNameMessagePrefix + name); usedNames.add(name); + if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName())) { - assertCondition(!usedNames.contains(securityKeyType.getAllAccessKeyName()), "More than one SecurityKeyType with name (or allAccessKeyName) of: " + securityKeyType.getAllAccessKeyName()); + assertCondition(!usedNames.contains(securityKeyType.getAllAccessKeyName()), duplicateNameMessagePrefix + securityKeyType.getAllAccessKeyName()); usedNames.add(securityKeyType.getAllAccessKeyName()); } + if(StringUtils.hasContent(securityKeyType.getNullValueBehaviorKeyName())) + { + assertCondition(!usedNames.contains(securityKeyType.getNullValueBehaviorKeyName()), duplicateNameMessagePrefix + securityKeyType.getNullValueBehaviorKeyName()); + usedNames.add(securityKeyType.getNullValueBehaviorKeyName()); + } + if(StringUtils.hasContent(securityKeyType.getPossibleValueSourceName())) { assertCondition(qInstance.getPossibleValueSource(securityKeyType.getPossibleValueSourceName()) != null, "Unrecognized possibleValueSourceName in securityKeyType: " + name); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java index ad446912..115fcccc 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/QInstance.java @@ -1052,10 +1052,16 @@ public class QInstance for(QSecurityKeyType securityKeyType : CollectionUtils.nonNullMap(getSecurityKeyTypes()).values()) { rs.add(securityKeyType.getName()); + if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName())) { rs.add(securityKeyType.getAllAccessKeyName()); } + + if(StringUtils.hasContent(securityKeyType.getNullValueBehaviorKeyName())) + { + rs.add(securityKeyType.getNullValueBehaviorKeyName()); + } } return (rs); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/NullValueBehaviorUtil.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/NullValueBehaviorUtil.java new file mode 100644 index 00000000..3e99299f --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/NullValueBehaviorUtil.java @@ -0,0 +1,75 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. 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.io.Serializable; +import java.util.List; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock.NullValueBehavior; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.StringUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; + + +/******************************************************************************* + ** Utility for working with security key, nullValueBehaviors. + *******************************************************************************/ +public class NullValueBehaviorUtil +{ + private static final QLogger LOG = QLogger.getLogger(NullValueBehaviorUtil.class); + + + + /******************************************************************************* + ** Look at a RecordSecurityLock, but also the active session - and if the session + ** has a null-value-behavior key for the lock's key-type, then allow that behavior + ** to override the lock's default. + *******************************************************************************/ + public static NullValueBehavior getEffectiveNullValueBehavior(RecordSecurityLock recordSecurityLock) + { + QSecurityKeyType securityKeyType = QContext.getQInstance().getSecurityKeyType(recordSecurityLock.getSecurityKeyType()); + if(StringUtils.hasContent(securityKeyType.getNullValueBehaviorKeyName())) + { + List nullValueSessionValueList = QContext.getQSession().getSecurityKeyValues(securityKeyType.getNullValueBehaviorKeyName()); + if(CollectionUtils.nullSafeHasContents(nullValueSessionValueList)) + { + NullValueBehavior nullValueBehavior = NullValueBehavior.tryToGetFromString(ValueUtils.getValueAsString(nullValueSessionValueList.get(0))); + if(nullValueBehavior != null) + { + return nullValueBehavior; + } + else + { + LOG.info("Unexpected value in nullValueBehavior security key. Will use recordSecurityLock's nullValueBehavior", + logPair("nullValueBehaviorKeyName", securityKeyType.getNullValueBehaviorKeyName()), + logPair("value", nullValueSessionValueList.get(0))); + } + } + } + + return (recordSecurityLock.getNullValueBehavior()); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/QSecurityKeyType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/QSecurityKeyType.java index 74290b75..9e10ee4d 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/QSecurityKeyType.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/QSecurityKeyType.java @@ -34,6 +34,7 @@ public class QSecurityKeyType implements TopLevelMetaDataInterface { private String name; private String allAccessKeyName; + private String nullValueBehaviorKeyName; private String possibleValueSourceName; @@ -149,4 +150,35 @@ public class QSecurityKeyType implements TopLevelMetaDataInterface qInstance.addSecurityKeyType(this); } + + /******************************************************************************* + ** Getter for nullValueBehaviorKeyName + *******************************************************************************/ + public String getNullValueBehaviorKeyName() + { + return (this.nullValueBehaviorKeyName); + } + + + + /******************************************************************************* + ** Setter for nullValueBehaviorKeyName + *******************************************************************************/ + public void setNullValueBehaviorKeyName(String nullValueBehaviorKeyName) + { + this.nullValueBehaviorKeyName = nullValueBehaviorKeyName; + } + + + + /******************************************************************************* + ** Fluent setter for nullValueBehaviorKeyName + *******************************************************************************/ + public QSecurityKeyType withNullValueBehaviorKeyName(String nullValueBehaviorKeyName) + { + this.nullValueBehaviorKeyName = nullValueBehaviorKeyName; + return (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 06633998..b16deac0 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 @@ -22,7 +22,9 @@ package com.kingsrook.qqq.backend.core.model.metadata.security; +import java.util.HashMap; import java.util.List; +import java.util.Map; /******************************************************************************* @@ -67,7 +69,34 @@ public class RecordSecurityLock { ALLOW, ALLOW_WRITE_ONLY, // not common - but see Audit, where you can do a thing that inserts them into a generic table, even though you can't later read them yourself... - DENY + DENY; + + + //////////////////////////////////////////////////////////////////// + // for use in tryToGetFromString, where we'll lowercase the input // + //////////////////////////////////////////////////////////////////// + private static final Map stringMapping = new HashMap<>(); + + static + { + stringMapping.put("allow", ALLOW); + stringMapping.put("allow_write_only", ALLOW_WRITE_ONLY); + stringMapping.put("allowwriteonly", ALLOW_WRITE_ONLY); + stringMapping.put("deny", DENY); + } + + /******************************************************************************* + ** + *******************************************************************************/ + public static NullValueBehavior tryToGetFromString(String string) + { + if(string == null) + { + return (null); + } + + return stringMapping.get(string.toLowerCase()); + } } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java index 55b94bb0..a258a177 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/instances/QInstanceValidatorTest.java @@ -75,6 +75,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.TableAutomationAction; +import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleCustomizerInterface; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.AbstractTransformStep; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.ExtractViaQueryStep; import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwithfrontend.LoadViaDeleteStep; @@ -1622,19 +1623,30 @@ class QInstanceValidatorTest extends BaseTest assertValidationFailureReasons((qInstance -> { qInstance.addSecurityKeyType(new QSecurityKeyType().withName("clientId").withAllAccessKeyName("clientId")); - }), "More than one SecurityKeyType with name (or allAccessKeyName) of: clientId"); + }), "More than one SecurityKeyType with name (or allAccessKeyName or nullValueBehaviorKeyName) of: clientId"); + + assertValidationFailureReasonsAllowingExtraReasons((qInstance -> + { + qInstance.addSecurityKeyType(new QSecurityKeyType().withName("clientId").withAllAccessKeyName("clientId").withNullValueBehaviorKeyName("clientId")); + }), "More than one SecurityKeyType with name (or allAccessKeyName or nullValueBehaviorKeyName) of: clientId"); assertValidationFailureReasons((qInstance -> { qInstance.addSecurityKeyType(new QSecurityKeyType().withName("clientId").withAllAccessKeyName("allAccess")); qInstance.addSecurityKeyType(new QSecurityKeyType().withName("warehouseId").withAllAccessKeyName("allAccess")); - }), "More than one SecurityKeyType with name (or allAccessKeyName) of: allAccess"); + }), "More than one SecurityKeyType with name (or allAccessKeyName or nullValueBehaviorKeyName) of: allAccess"); + + assertValidationFailureReasons((qInstance -> + { + qInstance.addSecurityKeyType(new QSecurityKeyType().withName("clientId").withNullValueBehaviorKeyName("nullBehavior")); + qInstance.addSecurityKeyType(new QSecurityKeyType().withName("warehouseId").withNullValueBehaviorKeyName("nullBehavior")); + }), "More than one SecurityKeyType with name (or allAccessKeyName or nullValueBehaviorKeyName) of: nullBehavior"); assertValidationFailureReasons((qInstance -> { qInstance.addSecurityKeyType(new QSecurityKeyType().withName("clientId").withAllAccessKeyName("allAccess")); qInstance.addSecurityKeyType(new QSecurityKeyType().withName("allAccess")); - }), "More than one SecurityKeyType with name (or allAccessKeyName) of: allAccess"); + }), "More than one SecurityKeyType with name (or allAccessKeyName or nullValueBehaviorKeyName) of: allAccess"); assertValidationFailureReasons((qInstance -> qInstance.addSecurityKeyType(new QSecurityKeyType().withName("clientId").withPossibleValueSourceName("nonPVS"))), "Unrecognized possibleValueSourceName in securityKeyType"); diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/security/NullValueBehaviorUtilTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/security/NullValueBehaviorUtilTest.java new file mode 100644 index 00000000..ee33b03f --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/security/NullValueBehaviorUtilTest.java @@ -0,0 +1,84 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2024. 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; +import java.util.Map; +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/******************************************************************************* + ** Unit test for NullValueBehaviorUtil + *******************************************************************************/ +class NullValueBehaviorUtilTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() + { + //////////////////////////////////////////////////////////////////////////////////////// + // if session doesn't have a null-value key, then always get back the lock's behavior // + //////////////////////////////////////////////////////////////////////////////////////// + for(RecordSecurityLock.NullValueBehavior lockNullValueBehavior : RecordSecurityLock.NullValueBehavior.values()) + { + assertEquals(lockNullValueBehavior, NullValueBehaviorUtil.getEffectiveNullValueBehavior(new RecordSecurityLock() + .withSecurityKeyType(TestUtils.SECURITY_KEY_TYPE_STORE) + .withNullValueBehavior(lockNullValueBehavior))); + } + + ///////////////////////////////////////////////////////////////////// + // if session DOES have a null-value key, then always gete it back // + ///////////////////////////////////////////////////////////////////// + for(RecordSecurityLock.NullValueBehavior sessionNullValueBehavior : RecordSecurityLock.NullValueBehavior.values()) + { + QContext.getQSession().withSecurityKeyValues(Map.of(TestUtils.SECURITY_KEY_TYPE_STORE_NULL_BEHAVIOR, List.of(sessionNullValueBehavior.toString()))); + + for(RecordSecurityLock.NullValueBehavior lockNullValueBehavior : RecordSecurityLock.NullValueBehavior.values()) + { + assertEquals(sessionNullValueBehavior, NullValueBehaviorUtil.getEffectiveNullValueBehavior(new RecordSecurityLock() + .withSecurityKeyType(TestUtils.SECURITY_KEY_TYPE_STORE) + .withNullValueBehavior(lockNullValueBehavior))); + } + } + + //////////////////////////////////////////////////////////////////// + // if session has an invalid key, always get back lock's behavior // + //////////////////////////////////////////////////////////////////// + for(RecordSecurityLock.NullValueBehavior lockNullValueBehavior : RecordSecurityLock.NullValueBehavior.values()) + { + QContext.getQSession().withSecurityKeyValues(Map.of(TestUtils.SECURITY_KEY_TYPE_STORE_NULL_BEHAVIOR, List.of("xyz"))); + + assertEquals(lockNullValueBehavior, NullValueBehaviorUtil.getEffectiveNullValueBehavior(new RecordSecurityLock() + .withSecurityKeyType(TestUtils.SECURITY_KEY_TYPE_STORE) + .withNullValueBehavior(lockNullValueBehavior))); + } + } + +} \ No newline at end of file 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 9656672e..2e3a937e 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 @@ -173,6 +173,7 @@ public class TestUtils public static final String SECURITY_KEY_TYPE_STORE = "store"; public static final String SECURITY_KEY_TYPE_STORE_ALL_ACCESS = "storeAllAccess"; + public static final String SECURITY_KEY_TYPE_STORE_NULL_BEHAVIOR = "storeNullBehavior"; public static final String SECURITY_KEY_TYPE_INTERNAL_OR_EXTERNAL = "internalOrExternal"; @@ -471,6 +472,7 @@ public class TestUtils return new QSecurityKeyType() .withName(SECURITY_KEY_TYPE_STORE) .withAllAccessKeyName(SECURITY_KEY_TYPE_STORE_ALL_ACCESS) + .withNullValueBehaviorKeyName(SECURITY_KEY_TYPE_STORE_NULL_BEHAVIOR) .withPossibleValueSourceName(POSSIBLE_VALUE_SOURCE_STORE); } @@ -1255,7 +1257,6 @@ public class TestUtils - /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-module-mongodb/src/main/java/com/kingsrook/qqq/backend/module/mongodb/actions/AbstractMongoDBAction.java b/qqq-backend-module-mongodb/src/main/java/com/kingsrook/qqq/backend/module/mongodb/actions/AbstractMongoDBAction.java index 626a232c..fb7d2156 100644 --- a/qqq-backend-module-mongodb/src/main/java/com/kingsrook/qqq/backend/module/mongodb/actions/AbstractMongoDBAction.java +++ b/qqq-backend-module-mongodb/src/main/java/com/kingsrook/qqq/backend/module/mongodb/actions/AbstractMongoDBAction.java @@ -47,6 +47,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat; 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.security.NullValueBehaviorUtil; 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; @@ -479,7 +480,7 @@ public class AbstractMongoDBAction /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // handle user with no values -- they can only see null values, and only iff the lock's null-value behavior is ALLOW // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - if(RecordSecurityLock.NullValueBehavior.ALLOW.equals(recordSecurityLock.getNullValueBehavior())) + if(RecordSecurityLock.NullValueBehavior.ALLOW.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock))) { lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IS_BLANK)); } @@ -498,7 +499,7 @@ public class AbstractMongoDBAction // else, if user/session has some values, build an IN rule - // // noting that if the lock's null-value behavior is ALLOW, then we actually want IS_NULL_OR_IN, not just IN // ////////////////////////////////////////////////////////////////////////////////////////////////////////////// - if(RecordSecurityLock.NullValueBehavior.ALLOW.equals(recordSecurityLock.getNullValueBehavior())) + if(RecordSecurityLock.NullValueBehavior.ALLOW.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock))) { lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IS_NULL_OR_IN, securityKeyValues)); } 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 ea3bba57..5ba71e75 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 @@ -66,6 +66,7 @@ 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.JoinType; import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.security.NullValueBehaviorUtil; 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; @@ -467,7 +468,7 @@ public abstract class AbstractRDBMSAction /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // handle user with no values -- they can only see null values, and only iff the lock's null-value behavior is ALLOW // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - if(RecordSecurityLock.NullValueBehavior.ALLOW.equals(recordSecurityLock.getNullValueBehavior())) + if(RecordSecurityLock.NullValueBehavior.ALLOW.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock))) { lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IS_BLANK)); } @@ -486,7 +487,7 @@ public abstract class AbstractRDBMSAction // else, if user/session has some values, build an IN rule - // // noting that if the lock's null-value behavior is ALLOW, then we actually want IS_NULL_OR_IN, not just IN // ////////////////////////////////////////////////////////////////////////////////////////////////////////////// - if(RecordSecurityLock.NullValueBehavior.ALLOW.equals(recordSecurityLock.getNullValueBehavior())) + if(RecordSecurityLock.NullValueBehavior.ALLOW.equals(NullValueBehaviorUtil.getEffectiveNullValueBehavior(recordSecurityLock))) { lockCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IS_NULL_OR_IN, securityKeyValues)); }