From dfb0f3ddf18950855b8cca855b52624b9c5659d0 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Tue, 14 May 2024 19:53:36 -0500 Subject: [PATCH] CE-1172 - finish a somewhat usable version of field-level locks - but note - only removing the field from the meta-data that goes to the frontend -- queries, for example, will still fetch the field and send it through. In touching some code here, remove some un-used old ActionInput parameters. --- .../core/actions/metadata/MetaDataAction.java | 2 +- .../actions/metadata/TableMetaDataAction.java | 2 +- .../permissions/PermissionsHelper.java | 8 +- .../core/instances/QInstanceValidator.java | 2 +- .../frontend/QFrontendTableMetaData.java | 37 +++++-- .../metadata/security/FieldSecurityLock.java | 97 ++++++++++++++++--- .../metadata/security/QSecurityKeyType.java | 34 +++++++ .../permissions/PermissionsHelperTest.java | 36 +++---- .../instances/QInstanceValidatorTest.java | 4 +- .../frontend/QFrontendTableMetaDataTest.java | 89 +++++++++++++++++ .../security/FieldSecurityLockTest.java | 85 ++++++++++++++++ .../qqq/backend/core/utils/TestUtils.java | 2 +- 12 files changed, 346 insertions(+), 52 deletions(-) create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaDataTest.java create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/security/FieldSecurityLockTest.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java index 403d94e3..7ec33e50 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataAction.java @@ -84,7 +84,7 @@ public class MetaDataAction } QBackendMetaData backendForTable = metaDataInput.getInstance().getBackendForTable(tableName); - tables.put(tableName, new QFrontendTableMetaData(metaDataInput, backendForTable, table, false, false)); + tables.put(tableName, new QFrontendTableMetaData(backendForTable, table, false, false)); treeNodes.put(tableName, new AppTreeNode(table)); } metaDataOutput.setTables(tables); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/TableMetaDataAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/TableMetaDataAction.java index 46c4f243..824e926b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/TableMetaDataAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/TableMetaDataAction.java @@ -54,7 +54,7 @@ public class TableMetaDataAction throw (new QNotFoundException("Table [" + tableMetaDataInput.getTableName() + "] was not found.")); } QBackendMetaData backendForTable = tableMetaDataInput.getInstance().getBackendForTable(table.getName()); - tableMetaDataOutput.setTable(new QFrontendTableMetaData(tableMetaDataInput, backendForTable, table, true, true)); + tableMetaDataOutput.setTable(new QFrontendTableMetaData(backendForTable, table, true, true)); // todo post-customization - can do whatever w/ the result if you want diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/permissions/PermissionsHelper.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/permissions/PermissionsHelper.java index 57a368f7..1f70b7b2 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/permissions/PermissionsHelper.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/permissions/PermissionsHelper.java @@ -65,7 +65,7 @@ public class PermissionsHelper *******************************************************************************/ public static void checkTablePermissionThrowing(AbstractTableActionInput tableActionInput, TablePermissionSubType permissionSubType) throws QPermissionDeniedException { - checkTablePermissionThrowing(tableActionInput, tableActionInput.getTableName(), permissionSubType); + checkTablePermissionThrowing(tableActionInput.getTableName(), permissionSubType); } @@ -73,7 +73,7 @@ public class PermissionsHelper /******************************************************************************* ** *******************************************************************************/ - private static void checkTablePermissionThrowing(AbstractActionInput actionInput, String tableName, TablePermissionSubType permissionSubType) throws QPermissionDeniedException + private static void checkTablePermissionThrowing(String tableName, TablePermissionSubType permissionSubType) throws QPermissionDeniedException { warnAboutPermissionSubTypeForTables(permissionSubType); QTableMetaData table = QContext.getQInstance().getTable(tableName); @@ -99,11 +99,11 @@ public class PermissionsHelper /******************************************************************************* ** *******************************************************************************/ - public static boolean hasTablePermission(AbstractActionInput actionInput, String tableName, TablePermissionSubType permissionSubType) + public static boolean hasTablePermission(String tableName, TablePermissionSubType permissionSubType) { try { - checkTablePermissionThrowing(actionInput, tableName, permissionSubType); + checkTablePermissionThrowing(tableName, permissionSubType); return (true); } catch(QPermissionDeniedException e) 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 f371e86e..fc094711 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 @@ -914,7 +914,7 @@ public class QInstanceValidator } assertCondition(fieldSecurityLock.getDefaultBehavior() != null, prefix + "has a fieldSecurityLock that is missing a defaultBehavior"); - assertCondition(CollectionUtils.nullSafeHasContents(fieldSecurityLock.getOverrideValues()), prefix + "has a fieldSecurityLock that is missing overrideValues"); + assertCondition(CollectionUtils.nullSafeHasContents(fieldSecurityLock.getKeyValueBehaviors()), prefix + "has a fieldSecurityLock that is missing keyValueBehaviors"); } for(FieldAdornment adornment : CollectionUtils.nonNullList(field.getAdornments())) diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaData.java index ef93e024..322ea4c5 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaData.java @@ -35,16 +35,17 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper; import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType; import com.kingsrook.qqq.backend.core.context.QContext; -import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; 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.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock; import com.kingsrook.qqq.backend.core.model.metadata.sharing.ShareableTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability; import com.kingsrook.qqq.backend.core.model.metadata.tables.ExposedJoin; import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; import com.kingsrook.qqq.backend.core.model.metadata.tables.QSupplementalTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; @@ -87,12 +88,14 @@ public class QFrontendTableMetaData /******************************************************************************* ** *******************************************************************************/ - public QFrontendTableMetaData(AbstractActionInput actionInput, QBackendMetaData backendForTable, QTableMetaData tableMetaData, boolean includeFullMetaData, boolean includeJoins) + public QFrontendTableMetaData(QBackendMetaData backendForTable, QTableMetaData tableMetaData, boolean includeFullMetaData, boolean includeJoins) { this.name = tableMetaData.getName(); this.label = tableMetaData.getLabel(); this.isHidden = tableMetaData.getIsHidden(); + QSession qSession = QContext.getQSession(); + if(includeFullMetaData) { this.primaryKeyField = tableMetaData.getPrimaryKeyField(); @@ -100,7 +103,21 @@ public class QFrontendTableMetaData for(String fieldName : tableMetaData.getFields().keySet()) { QFieldMetaData field = tableMetaData.getField(fieldName); - if(!field.getIsHidden()) + + //////////////////////////////////////////////////////// + // apply field security lock behaviors, if applicable // + //////////////////////////////////////////////////////// + boolean isDenied = false; + if(field.getFieldSecurityLock() != null) + { + FieldSecurityLock.Behavior behavior = field.getFieldSecurityLock().getBehaviorForSession(qSession); + if(FieldSecurityLock.Behavior.DENY.equals(behavior)) + { + isDenied = true; + } + } + + if(!field.getIsHidden() && !isDenied) { this.fields.put(fieldName, new QFrontendFieldMetaData(field)); } @@ -124,7 +141,7 @@ public class QFrontendTableMetaData QTableMetaData joinTable = qInstance.getTable(exposedJoin.getJoinTable()); frontendExposedJoin.setLabel(exposedJoin.getLabel()); frontendExposedJoin.setIsMany(exposedJoin.getIsMany()); - frontendExposedJoin.setJoinTable(new QFrontendTableMetaData(actionInput, backendForTable, joinTable, includeFullMetaData, false)); + frontendExposedJoin.setJoinTable(new QFrontendTableMetaData(backendForTable, joinTable, includeFullMetaData, false)); for(String joinName : exposedJoin.getJoinPath()) { frontendExposedJoin.addJoin(qInstance.getJoin(joinName)); @@ -161,16 +178,16 @@ public class QFrontendTableMetaData setCapabilities(backendForTable, tableMetaData); - readPermission = PermissionsHelper.hasTablePermission(actionInput, tableMetaData.getName(), TablePermissionSubType.READ); - insertPermission = PermissionsHelper.hasTablePermission(actionInput, tableMetaData.getName(), TablePermissionSubType.INSERT); - editPermission = PermissionsHelper.hasTablePermission(actionInput, tableMetaData.getName(), TablePermissionSubType.EDIT); - deletePermission = PermissionsHelper.hasTablePermission(actionInput, tableMetaData.getName(), TablePermissionSubType.DELETE); + readPermission = PermissionsHelper.hasTablePermission(tableMetaData.getName(), TablePermissionSubType.READ); + insertPermission = PermissionsHelper.hasTablePermission(tableMetaData.getName(), TablePermissionSubType.INSERT); + editPermission = PermissionsHelper.hasTablePermission(tableMetaData.getName(), TablePermissionSubType.EDIT); + deletePermission = PermissionsHelper.hasTablePermission(tableMetaData.getName(), TablePermissionSubType.DELETE); - QBackendMetaData backend = actionInput.getInstance().getBackend(tableMetaData.getBackendName()); + QBackendMetaData backend = QContext.getQInstance().getBackend(tableMetaData.getBackendName()); if(backend != null && backend.getUsesVariants()) { usesVariants = true; - variantTableLabel = actionInput.getInstance().getTable(backend.getVariantOptionsTableName()).getLabel(); + variantTableLabel = QContext.getQInstance().getTable(backend.getVariantOptionsTableName()).getLabel(); } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/FieldSecurityLock.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/FieldSecurityLock.java index 298f6ad7..e9160301 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/FieldSecurityLock.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/FieldSecurityLock.java @@ -23,17 +23,38 @@ package com.kingsrook.qqq.backend.core.model.metadata.security; import java.io.Serializable; -import java.util.List; +import java.util.HashMap; +import java.util.Map; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; /******************************************************************************* ** Define, for a field, a lock that controls if users can or cannot see the field. + ** + ** The lock has a defaultBehavior, which is how the field should be treated, well, + ** by default. + ** The lock also references a securityKeyType; whose values, when looked up in + ** the lock's keyValueBehaviors map, change the default behavior. + ** + ** For example, consider a lock with a keyType of 'internalOrExternalUser' (with + ** possible values of 'internal' and 'external'), a defaultBehavior of DENY, + ** and a keyValueBehaviors map containing internal => ALLOW. If a session has + ** no security key of the internalOrExternalUser type, or a key with the value of + ** 'external', then the lock's behavior will be the default (DENY). However, + ** a key value of 'internal' would trigger the behavior specified for that key + ** (ALLOW). *******************************************************************************/ public class FieldSecurityLock { - private String securityKeyType; - private Behavior defaultBehavior = Behavior.DENY; - private List overrideValues; + private static final QLogger LOG = QLogger.getLogger(FieldSecurityLock.class); + + private String securityKeyType; + private Behavior defaultBehavior = Behavior.DENY; + + private Map keyValueBehaviors; @@ -89,7 +110,6 @@ public class FieldSecurityLock - /******************************************************************************* ** Getter for defaultBehavior *******************************************************************************/ @@ -122,33 +142,82 @@ public class FieldSecurityLock /******************************************************************************* - ** Getter for overrideValues + ** Getter for keyValueBehaviors *******************************************************************************/ - public List getOverrideValues() + public Map getKeyValueBehaviors() { - return (this.overrideValues); + return (this.keyValueBehaviors); } /******************************************************************************* - ** Setter for overrideValues + ** Setter for keyValueBehaviors *******************************************************************************/ - public void setOverrideValues(List overrideValues) + public void setKeyValueBehaviors(Map keyValueBehaviors) { - this.overrideValues = overrideValues; + this.keyValueBehaviors = keyValueBehaviors; } /******************************************************************************* - ** Fluent setter for overrideValues + ** Fluent setter for keyValueBehaviors *******************************************************************************/ - public FieldSecurityLock withOverrideValues(List overrideValues) + public FieldSecurityLock withKeyValueBehaviors(Map keyValueBehaviors) { - this.overrideValues = overrideValues; + this.keyValueBehaviors = keyValueBehaviors; return (this); } + + /******************************************************************************* + ** Fluent setter for a single keyValueBehavior + *******************************************************************************/ + public FieldSecurityLock withKeyValueBehavior(Serializable keyValue, Behavior behavior) + { + if(this.keyValueBehaviors == null) + { + this.keyValueBehaviors = new HashMap<>(); + } + this.keyValueBehaviors.put(keyValue, behavior); + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Behavior getBehaviorForSession(QSession session) + { + if(session != null && session.getSecurityKeyValues(this.securityKeyType) != null) + { + QSecurityKeyType securityKeyType = QContext.getQInstance().getSecurityKeyType(this.securityKeyType); + + for(Serializable securityKeyValue : session.getSecurityKeyValues(this.securityKeyType)) + { + try + { + if(securityKeyType.getValueType() != null) + { + securityKeyValue = ValueUtils.getValueAsFieldType(securityKeyType.getValueType(), securityKeyValue); + } + + if(keyValueBehaviors.containsKey(securityKeyValue)) + { + return keyValueBehaviors.get(securityKeyValue); + } + } + catch(Exception e) + { + LOG.warn("Error getting field behavior", e); + } + } + } + + return getDefaultBehavior(); + } + } 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 9e10ee4d..b814b6a1 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 @@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.model.metadata.security; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.TopLevelMetaDataInterface; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; /******************************************************************************* @@ -37,6 +38,8 @@ public class QSecurityKeyType implements TopLevelMetaDataInterface private String nullValueBehaviorKeyName; private String possibleValueSourceName; + private QFieldType valueType; + /******************************************************************************* @@ -151,6 +154,7 @@ public class QSecurityKeyType implements TopLevelMetaDataInterface } + /******************************************************************************* ** Getter for nullValueBehaviorKeyName *******************************************************************************/ @@ -181,4 +185,34 @@ public class QSecurityKeyType implements TopLevelMetaDataInterface } + + /******************************************************************************* + ** Getter for valueType + *******************************************************************************/ + public QFieldType getValueType() + { + return (this.valueType); + } + + + + /******************************************************************************* + ** Setter for valueType + *******************************************************************************/ + public void setValueType(QFieldType valueType) + { + this.valueType = valueType; + } + + + + /******************************************************************************* + ** Fluent setter for valueType + *******************************************************************************/ + public QSecurityKeyType withValueType(QFieldType valueType) + { + this.valueType = valueType; + return (this); + } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/permissions/PermissionsHelperTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/permissions/PermissionsHelperTest.java index cb61d4f7..3ab9d79d 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/permissions/PermissionsHelperTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/permissions/PermissionsHelperTest.java @@ -152,10 +152,10 @@ class PermissionsHelperTest extends BaseTest AbstractTableActionInput actionInput = new InsertInput().withTableName(TABLE_NAME); - assertTrue(PermissionsHelper.hasTablePermission(actionInput, TABLE_NAME, TablePermissionSubType.READ)); - assertFalse(PermissionsHelper.hasTablePermission(actionInput, TABLE_NAME, TablePermissionSubType.INSERT)); - assertFalse(PermissionsHelper.hasTablePermission(actionInput, TABLE_NAME, TablePermissionSubType.EDIT)); - assertFalse(PermissionsHelper.hasTablePermission(actionInput, TABLE_NAME, TablePermissionSubType.DELETE)); + assertTrue(PermissionsHelper.hasTablePermission(TABLE_NAME, TablePermissionSubType.READ)); + assertFalse(PermissionsHelper.hasTablePermission(TABLE_NAME, TablePermissionSubType.INSERT)); + assertFalse(PermissionsHelper.hasTablePermission(TABLE_NAME, TablePermissionSubType.EDIT)); + assertFalse(PermissionsHelper.hasTablePermission(TABLE_NAME, TablePermissionSubType.DELETE)); PermissionsHelper.checkTablePermissionThrowing(actionInput, TablePermissionSubType.READ); assertThatThrownBy(() -> PermissionsHelper.checkTablePermissionThrowing(actionInput, TablePermissionSubType.INSERT)).isInstanceOf(QPermissionDeniedException.class); assertThatThrownBy(() -> PermissionsHelper.checkTablePermissionThrowing(actionInput, TablePermissionSubType.EDIT)).isInstanceOf(QPermissionDeniedException.class); @@ -169,10 +169,10 @@ class PermissionsHelperTest extends BaseTest AbstractTableActionInput actionInput = new InsertInput().withTableName(TABLE_NAME); - assertFalse(PermissionsHelper.hasTablePermission(actionInput, TABLE_NAME, TablePermissionSubType.READ)); - assertTrue(PermissionsHelper.hasTablePermission(actionInput, TABLE_NAME, TablePermissionSubType.INSERT)); - assertTrue(PermissionsHelper.hasTablePermission(actionInput, TABLE_NAME, TablePermissionSubType.EDIT)); - assertTrue(PermissionsHelper.hasTablePermission(actionInput, TABLE_NAME, TablePermissionSubType.DELETE)); + assertFalse(PermissionsHelper.hasTablePermission(TABLE_NAME, TablePermissionSubType.READ)); + assertTrue(PermissionsHelper.hasTablePermission(TABLE_NAME, TablePermissionSubType.INSERT)); + assertTrue(PermissionsHelper.hasTablePermission(TABLE_NAME, TablePermissionSubType.EDIT)); + assertTrue(PermissionsHelper.hasTablePermission(TABLE_NAME, TablePermissionSubType.DELETE)); assertThatThrownBy(() -> PermissionsHelper.checkTablePermissionThrowing(actionInput, TablePermissionSubType.READ)).isInstanceOf(QPermissionDeniedException.class); PermissionsHelper.checkTablePermissionThrowing(actionInput, TablePermissionSubType.INSERT); PermissionsHelper.checkTablePermissionThrowing(actionInput, TablePermissionSubType.EDIT); @@ -244,10 +244,10 @@ class PermissionsHelperTest extends BaseTest AbstractTableActionInput actionInput = new InsertInput().withTableName(TABLE_NAME); - assertTrue(PermissionsHelper.hasTablePermission(actionInput, TABLE_NAME, TablePermissionSubType.READ)); - assertFalse(PermissionsHelper.hasTablePermission(actionInput, TABLE_NAME, TablePermissionSubType.INSERT)); - assertFalse(PermissionsHelper.hasTablePermission(actionInput, TABLE_NAME, TablePermissionSubType.EDIT)); - assertFalse(PermissionsHelper.hasTablePermission(actionInput, TABLE_NAME, TablePermissionSubType.DELETE)); + assertTrue(PermissionsHelper.hasTablePermission(TABLE_NAME, TablePermissionSubType.READ)); + assertFalse(PermissionsHelper.hasTablePermission(TABLE_NAME, TablePermissionSubType.INSERT)); + assertFalse(PermissionsHelper.hasTablePermission(TABLE_NAME, TablePermissionSubType.EDIT)); + assertFalse(PermissionsHelper.hasTablePermission(TABLE_NAME, TablePermissionSubType.DELETE)); PermissionsHelper.checkTablePermissionThrowing(actionInput, TablePermissionSubType.READ); assertThatThrownBy(() -> PermissionsHelper.checkTablePermissionThrowing(actionInput, TablePermissionSubType.INSERT)).isInstanceOf(QPermissionDeniedException.class); assertThatThrownBy(() -> PermissionsHelper.checkTablePermissionThrowing(actionInput, TablePermissionSubType.EDIT)).isInstanceOf(QPermissionDeniedException.class); @@ -261,10 +261,10 @@ class PermissionsHelperTest extends BaseTest AbstractTableActionInput actionInput = new InsertInput().withTableName(TABLE_NAME); - assertFalse(PermissionsHelper.hasTablePermission(actionInput, TABLE_NAME, TablePermissionSubType.READ)); - assertTrue(PermissionsHelper.hasTablePermission(actionInput, TABLE_NAME, TablePermissionSubType.INSERT)); - assertFalse(PermissionsHelper.hasTablePermission(actionInput, TABLE_NAME, TablePermissionSubType.EDIT)); - assertFalse(PermissionsHelper.hasTablePermission(actionInput, TABLE_NAME, TablePermissionSubType.DELETE)); + assertFalse(PermissionsHelper.hasTablePermission(TABLE_NAME, TablePermissionSubType.READ)); + assertTrue(PermissionsHelper.hasTablePermission(TABLE_NAME, TablePermissionSubType.INSERT)); + assertFalse(PermissionsHelper.hasTablePermission(TABLE_NAME, TablePermissionSubType.EDIT)); + assertFalse(PermissionsHelper.hasTablePermission(TABLE_NAME, TablePermissionSubType.DELETE)); assertThatThrownBy(() -> PermissionsHelper.checkTablePermissionThrowing(actionInput, TablePermissionSubType.READ)).isInstanceOf(QPermissionDeniedException.class); PermissionsHelper.checkTablePermissionThrowing(actionInput, TablePermissionSubType.INSERT); assertThatThrownBy(() -> PermissionsHelper.checkTablePermissionThrowing(actionInput, TablePermissionSubType.EDIT)).isInstanceOf(QPermissionDeniedException.class); @@ -581,7 +581,7 @@ class PermissionsHelperTest extends BaseTest for(TablePermissionSubType permissionSubType : TablePermissionSubType.values()) { - assertTrue(PermissionsHelper.hasTablePermission(actionInput, TABLE_NAME, permissionSubType), "Expected to have permission " + TABLE_NAME + ":" + permissionSubType); + assertTrue(PermissionsHelper.hasTablePermission(TABLE_NAME, permissionSubType), "Expected to have permission " + TABLE_NAME + ":" + permissionSubType); PermissionsHelper.checkTablePermissionThrowing(actionInput, permissionSubType); } @@ -600,7 +600,7 @@ class PermissionsHelperTest extends BaseTest for(TablePermissionSubType permissionSubType : TablePermissionSubType.values()) { - assertFalse(PermissionsHelper.hasTablePermission(actionInput, TABLE_NAME, permissionSubType)); + assertFalse(PermissionsHelper.hasTablePermission(TABLE_NAME, permissionSubType)); assertThatThrownBy(() -> PermissionsHelper.checkTablePermissionThrowing(actionInput, permissionSubType)) .isExactlyInstanceOf(QPermissionDeniedException.class); } 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 1c6947a3..c4506860 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 @@ -1922,8 +1922,8 @@ public class QInstanceValidatorTest extends BaseTest assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setSecurityKeyType(" ")), "missing a securityKeyType"); assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setSecurityKeyType("notAKeyType")), "unrecognized securityKeyType"); assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setDefaultBehavior(null)), "missing a defaultBehavior"); - assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setOverrideValues(null)), "missing overrideValues"); - assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setOverrideValues(Collections.emptyList())), "missing overrideValues"); + assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setKeyValueBehaviors(null)), "missing keyValueBehaviors"); + assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setKeyValueBehaviors(Collections.emptyMap())), "missing keyValueBehaviors"); } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaDataTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaDataTest.java new file mode 100644 index 00000000..96552c48 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendTableMetaDataTest.java @@ -0,0 +1,89 @@ +/* + * 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.frontend; + + +import java.util.function.Supplier; +import com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock; +import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/******************************************************************************* + ** Unit test for QFrontendTableMetaData + *******************************************************************************/ +class QFrontendTableMetaDataTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFieldLocks() + { + QContext.getQInstance().addSecurityKeyType(new QSecurityKeyType() + .withName("allowedToSeeFirstName") + .withValueType(QFieldType.BOOLEAN)); + + FieldSecurityLock fieldSecurityLock = new FieldSecurityLock() + .withSecurityKeyType("allowedToSeeFirstName") + .withDefaultBehavior(FieldSecurityLock.Behavior.DENY) + .withKeyValueBehavior(true, FieldSecurityLock.Behavior.ALLOW); + + QTableMetaData table = QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON_MEMORY); + table.getField("firstName").withFieldSecurityLock(fieldSecurityLock); + + Supplier run = () -> new QFrontendTableMetaData(QContext.getQInstance().getBackendForTable(TestUtils.TABLE_NAME_PERSON_MEMORY), table, true, false); + + ////////////////////////////////////////////////////////////// + // default session (no key) should NOT get to see firstName // + ////////////////////////////////////////////////////////////// + assertFalse(run.get().getFields().containsKey("firstName")); + + ///////////////////////////////////// + // with the key=true, then allowed // + ///////////////////////////////////// + QContext.setQSession(new QSession().withSecurityKeyValue("allowedToSeeFirstName", true)); + assertTrue(run.get().getFields().containsKey("firstName")); + + //////////////////////////////////////// + // try a string version of the key... // + //////////////////////////////////////// + QContext.setQSession(new QSession().withSecurityKeyValue("allowedToSeeFirstName", "true")); + assertTrue(run.get().getFields().containsKey("firstName")); + + //////////////////////////// + // try unrecognized value // + //////////////////////////// + QContext.setQSession(new QSession().withSecurityKeyValue("allowedToSeeFirstName", "nope")); + assertFalse(run.get().getFields().containsKey("firstName")); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/security/FieldSecurityLockTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/security/FieldSecurityLockTest.java new file mode 100644 index 00000000..21556866 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/security/FieldSecurityLockTest.java @@ -0,0 +1,85 @@ +/* + * 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 com.kingsrook.qqq.backend.core.BaseTest; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.core.model.session.QSession; +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 FieldSecurityLock + *******************************************************************************/ +class FieldSecurityLockTest extends BaseTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testGetBehaviorForSession() + { + QContext.getQInstance().addSecurityKeyType(new QSecurityKeyType() + .withName("foo") + .withValueType(QFieldType.STRING)); + + FieldSecurityLock fieldSecurityLock = new FieldSecurityLock() + .withSecurityKeyType("foo") + .withDefaultBehavior(FieldSecurityLock.Behavior.DENY) + .withKeyValueBehavior("bar", FieldSecurityLock.Behavior.ALLOW) + .withKeyValueBehavior("baz", FieldSecurityLock.Behavior.ALLOW) + .withKeyValueBehavior("boo", FieldSecurityLock.Behavior.DENY); + + QContext.getQInstance().getTable(TestUtils.TABLE_NAME_PERSON_MEMORY) + .getField("firstName").withFieldSecurityLock(fieldSecurityLock); + + //////////////////////////// + // no key value = default // + //////////////////////////// + assertEquals(FieldSecurityLock.Behavior.DENY, fieldSecurityLock.getBehaviorForSession(new QSession())); + + ///////////////////////////////////////////////// + // values specified get the behavior specified // + ///////////////////////////////////////////////// + assertEquals(FieldSecurityLock.Behavior.ALLOW, fieldSecurityLock.getBehaviorForSession(new QSession().withSecurityKeyValue("foo", "bar"))); + assertEquals(FieldSecurityLock.Behavior.ALLOW, fieldSecurityLock.getBehaviorForSession(new QSession().withSecurityKeyValue("foo", "baz"))); + assertEquals(FieldSecurityLock.Behavior.DENY, fieldSecurityLock.getBehaviorForSession(new QSession().withSecurityKeyValue("foo", "boo"))); + + ////////////////////////////////////////////// + // unrecognized values get default behavior // + ////////////////////////////////////////////// + assertEquals(FieldSecurityLock.Behavior.DENY, fieldSecurityLock.getBehaviorForSession(new QSession().withSecurityKeyValue("foo", "huh"))); + + ///////////////////////////////////////////////// + // if multiple key values, the first one wins. // + ///////////////////////////////////////////////// + assertEquals(FieldSecurityLock.Behavior.ALLOW, fieldSecurityLock.getBehaviorForSession(new QSession().withSecurityKeyValue("foo", "bar").withSecurityKeyValue("foo", "boo"))); + assertEquals(FieldSecurityLock.Behavior.DENY, fieldSecurityLock.getBehaviorForSession(new QSession().withSecurityKeyValue("foo", "boo").withSecurityKeyValue("foo", "foo"))); + + } + +} \ 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 fdd0f5ab..51c8adb6 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 @@ -660,7 +660,7 @@ public class TestUtils .withField(new QFieldMetaData("total", QFieldType.DECIMAL).withDisplayFormat(DisplayFormat.CURRENCY).withFieldSecurityLock(new FieldSecurityLock() .withSecurityKeyType(SECURITY_KEY_TYPE_INTERNAL_OR_EXTERNAL) .withDefaultBehavior(FieldSecurityLock.Behavior.DENY) - .withOverrideValues(List.of("internal")) + .withKeyValueBehavior("internal", FieldSecurityLock.Behavior.ALLOW) )); }