diff --git a/checkstyle/config.xml b/checkstyle/config.xml index c6a1604e..501849be 100644 --- a/checkstyle/config.xml +++ b/checkstyle/config.xml @@ -262,6 +262,7 @@ + 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 944b8e6c..04dbc905 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 @@ -27,6 +27,8 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.actions.ActionHelper; +import com.kingsrook.qqq.backend.core.actions.permissions.PermissionCheckResult; +import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput; import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput; @@ -40,6 +42,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendTableMeta import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendWidgetMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; @@ -70,10 +73,18 @@ public class MetaDataAction Map tables = new LinkedHashMap<>(); for(Map.Entry entry : metaDataInput.getInstance().getTables().entrySet()) { - String tableName = entry.getKey(); + String tableName = entry.getKey(); + QTableMetaData table = entry.getValue(); + + PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, table); + if(permissionResult.equals(PermissionCheckResult.DENY_HIDE)) + { + continue; + } + QBackendMetaData backendForTable = metaDataInput.getInstance().getBackendForTable(tableName); - tables.put(tableName, new QFrontendTableMetaData(backendForTable, entry.getValue(), false)); - treeNodes.put(tableName, new AppTreeNode(entry.getValue())); + tables.put(tableName, new QFrontendTableMetaData(metaDataInput, backendForTable, table, false)); + treeNodes.put(tableName, new AppTreeNode(table)); } metaDataOutput.setTables(tables); @@ -83,8 +94,17 @@ public class MetaDataAction Map processes = new LinkedHashMap<>(); for(Map.Entry entry : metaDataInput.getInstance().getProcesses().entrySet()) { - processes.put(entry.getKey(), new QFrontendProcessMetaData(entry.getValue(), false)); - treeNodes.put(entry.getKey(), new AppTreeNode(entry.getValue())); + String processName = entry.getKey(); + QProcessMetaData process = entry.getValue(); + + PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, process); + if(permissionResult.equals(PermissionCheckResult.DENY_HIDE)) + { + continue; + } + + processes.put(processName, new QFrontendProcessMetaData(metaDataInput, process, false)); + treeNodes.put(processName, new AppTreeNode(process)); } metaDataOutput.setProcesses(processes); @@ -94,8 +114,17 @@ public class MetaDataAction Map reports = new LinkedHashMap<>(); for(Map.Entry entry : metaDataInput.getInstance().getReports().entrySet()) { - reports.put(entry.getKey(), new QFrontendReportMetaData(entry.getValue(), false)); - treeNodes.put(entry.getKey(), new AppTreeNode(entry.getValue())); + String reportName = entry.getKey(); + QReportMetaData report = entry.getValue(); + + PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, report); + if(permissionResult.equals(PermissionCheckResult.DENY_HIDE)) + { + continue; + } + + reports.put(reportName, new QFrontendReportMetaData(metaDataInput, report, false)); + treeNodes.put(reportName, new AppTreeNode(report)); } metaDataOutput.setReports(reports); @@ -105,7 +134,16 @@ public class MetaDataAction Map widgets = new LinkedHashMap<>(); for(Map.Entry entry : metaDataInput.getInstance().getWidgets().entrySet()) { - widgets.put(entry.getKey(), new QFrontendWidgetMetaData(entry.getValue())); + String widgetName = entry.getKey(); + QWidgetMetaDataInterface widget = entry.getValue(); + + PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, widget); + if(permissionResult.equals(PermissionCheckResult.DENY_HIDE)) + { + continue; + } + + widgets.put(widgetName, new QFrontendWidgetMetaData(metaDataInput, widget)); } metaDataOutput.setWidgets(widgets); @@ -115,14 +153,32 @@ public class MetaDataAction Map apps = new LinkedHashMap<>(); for(Map.Entry entry : metaDataInput.getInstance().getApps().entrySet()) { - apps.put(entry.getKey(), new QFrontendAppMetaData(entry.getValue())); - treeNodes.put(entry.getKey(), new AppTreeNode(entry.getValue())); + String appName = entry.getKey(); + QAppMetaData app = entry.getValue(); - if(CollectionUtils.nullSafeHasContents(entry.getValue().getChildren())) + PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, app); + if(permissionResult.equals(PermissionCheckResult.DENY_HIDE)) { - for(QAppChildMetaData child : entry.getValue().getChildren()) + continue; + } + + apps.put(appName, new QFrontendAppMetaData(app, metaDataOutput)); + treeNodes.put(appName, new AppTreeNode(app)); + + if(CollectionUtils.nullSafeHasContents(app.getChildren())) + { + for(QAppChildMetaData child : app.getChildren()) { - apps.get(entry.getKey()).addChild(new AppTreeNode(child)); + if(child instanceof MetaDataWithPermissionRules metaDataWithPermissionRules) + { + PermissionCheckResult childPermissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, metaDataWithPermissionRules); + if(childPermissionResult.equals(PermissionCheckResult.DENY_HIDE)) + { + continue; + } + } + + apps.get(appName).addChild(new AppTreeNode(child)); } } } @@ -136,7 +192,7 @@ public class MetaDataAction { if(appMetaData.getParentAppName() == null) { - buildAppTree(treeNodes, appTree, appMetaData); + buildAppTree(metaDataInput, treeNodes, appTree, appMetaData); } } metaDataOutput.setAppTree(appTree); @@ -161,7 +217,7 @@ public class MetaDataAction /******************************************************************************* ** *******************************************************************************/ - private void buildAppTree(Map treeNodes, List nodeList, QAppChildMetaData childMetaData) + private void buildAppTree(MetaDataInput metaDataInput, Map treeNodes, List nodeList, QAppChildMetaData childMetaData) { AppTreeNode treeNode = treeNodes.get(childMetaData.getName()); if(treeNode == null) @@ -176,7 +232,16 @@ public class MetaDataAction { for(QAppChildMetaData child : app.getChildren()) { - buildAppTree(treeNodes, treeNode.getChildren(), child); + if(child instanceof MetaDataWithPermissionRules metaDataWithPermissionRules) + { + PermissionCheckResult permissionResult = PermissionsHelper.getPermissionCheckResult(metaDataInput, metaDataWithPermissionRules); + if(permissionResult.equals(PermissionCheckResult.DENY_HIDE)) + { + continue; + } + } + + buildAppTree(metaDataInput, treeNodes, treeNode.getChildren(), child); } } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/ProcessMetaDataAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/ProcessMetaDataAction.java index eefcf4d6..a6419699 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/ProcessMetaDataAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/ProcessMetaDataAction.java @@ -52,7 +52,7 @@ public class ProcessMetaDataAction { throw (new QNotFoundException("Process [" + processMetaDataInput.getProcessName() + "] was not found.")); } - processMetaDataOutput.setProcess(new QFrontendProcessMetaData(process, true)); + processMetaDataOutput.setProcess(new QFrontendProcessMetaData(processMetaDataInput, process, 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/metadata/TableMetaDataAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/metadata/TableMetaDataAction.java index d67ec855..223eb475 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(backendForTable, table, true)); + tableMetaDataOutput.setTable(new QFrontendTableMetaData(tableMetaDataInput, backendForTable, table, 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/AvailablePermission.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/permissions/AvailablePermission.java new file mode 100644 index 00000000..9356c3f2 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/permissions/AvailablePermission.java @@ -0,0 +1,204 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.permissions; + + +import java.util.Objects; +import com.kingsrook.qqq.backend.core.model.data.QField; +import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class AvailablePermission extends QRecordEntity +{ + public static final String TABLE_NAME = "availablePermission"; + + @QField(label = "Permission Name") + private String name; + + @QField(label = "Object") + private String objectName; + + @QField() + private String objectType; + + @QField() + private String permissionType; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public boolean equals(Object o) + { + if(this == o) + { + return true; + } + if(o == null || getClass() != o.getClass()) + { + return false; + } + AvailablePermission that = (AvailablePermission) o; + return Objects.equals(name, that.name) && Objects.equals(objectName, that.objectName) && Objects.equals(objectType, that.objectType) && Objects.equals(permissionType, that.permissionType); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public int hashCode() + { + return Objects.hash(name, objectName, objectType, permissionType); + } + + + + /******************************************************************************* + ** Getter for name + *******************************************************************************/ + public String getName() + { + return (this.name); + } + + + + /******************************************************************************* + ** Setter for name + *******************************************************************************/ + public void setName(String name) + { + this.name = name; + } + + + + /******************************************************************************* + ** Fluent setter for name + *******************************************************************************/ + public AvailablePermission withName(String name) + { + this.name = name; + return (this); + } + + + + /******************************************************************************* + ** Getter for objectType + *******************************************************************************/ + public String getObjectType() + { + return (this.objectType); + } + + + + /******************************************************************************* + ** Setter for objectType + *******************************************************************************/ + public void setObjectType(String objectType) + { + this.objectType = objectType; + } + + + + /******************************************************************************* + ** Fluent setter for objectType + *******************************************************************************/ + public AvailablePermission withObjectType(String objectType) + { + this.objectType = objectType; + return (this); + } + + + + /******************************************************************************* + ** Getter for permissionType + *******************************************************************************/ + public String getPermissionType() + { + return (this.permissionType); + } + + + + /******************************************************************************* + ** Setter for permissionType + *******************************************************************************/ + public void setPermissionType(String permissionType) + { + this.permissionType = permissionType; + } + + + + /******************************************************************************* + ** Fluent setter for permissionType + *******************************************************************************/ + public AvailablePermission withPermissionType(String permissionType) + { + this.permissionType = permissionType; + return (this); + } + + + + /******************************************************************************* + ** Getter for objectName + *******************************************************************************/ + public String getObjectName() + { + return (this.objectName); + } + + + + /******************************************************************************* + ** Setter for objectName + *******************************************************************************/ + public void setObjectName(String objectName) + { + this.objectName = objectName; + } + + + + /******************************************************************************* + ** Fluent setter for objectName + *******************************************************************************/ + public AvailablePermission withObjectName(String objectName) + { + this.objectName = objectName; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/permissions/BulkTableActionProcessPermissionChecker.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/permissions/BulkTableActionProcessPermissionChecker.java new file mode 100644 index 00000000..752e0df1 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/permissions/BulkTableActionProcessPermissionChecker.java @@ -0,0 +1,69 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.permissions; + + +import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException; +import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; +import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class BulkTableActionProcessPermissionChecker implements CustomPermissionChecker +{ + private static final Logger LOG = LogManager.getLogger(BulkTableActionProcessPermissionChecker.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void checkPermissionsThrowing(AbstractActionInput actionInput, MetaDataWithPermissionRules metaDataWithPermissionRules) throws QPermissionDeniedException + { + String processName = metaDataWithPermissionRules.getName(); + if(processName != null && processName.indexOf('.') > -1) + { + String[] parts = processName.split("\\.", 2); + String tableName = parts[0]; + String bulkActionName = parts[1]; + + AbstractTableActionInput tableActionInput = new AbstractTableActionInput(actionInput.getInstance()); + tableActionInput.setSession(actionInput.getSession()); + tableActionInput.setTableName(tableName); + + switch(bulkActionName) + { + case "bulkInsert" -> PermissionsHelper.checkTablePermissionThrowing(tableActionInput, TablePermissionSubType.INSERT); + case "bulkEdit" -> PermissionsHelper.checkTablePermissionThrowing(tableActionInput, TablePermissionSubType.EDIT); + case "bulkDelete" -> PermissionsHelper.checkTablePermissionThrowing(tableActionInput, TablePermissionSubType.DELETE); + default -> LOG.warn("Unexpected bulk action name when checking permissions for process: " + processName); + } + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/permissions/CustomPermissionChecker.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/permissions/CustomPermissionChecker.java new file mode 100644 index 00000000..6da99d91 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/permissions/CustomPermissionChecker.java @@ -0,0 +1,41 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.permissions; + + +import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException; +import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules; + + +/******************************************************************************* + ** + *******************************************************************************/ +public interface CustomPermissionChecker +{ + + /******************************************************************************* + ** + *******************************************************************************/ + void checkPermissionsThrowing(AbstractActionInput actionInput, MetaDataWithPermissionRules metaDataWithPermissionRules) throws QPermissionDeniedException; + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/permissions/PermissionCheckResult.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/permissions/PermissionCheckResult.java new file mode 100644 index 00000000..f746494f --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/permissions/PermissionCheckResult.java @@ -0,0 +1,33 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.permissions; + + +/******************************************************************************* + ** + *******************************************************************************/ +public enum PermissionCheckResult +{ + ALLOW, + DENY_HIDE, + DENY_DISABLE; +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/permissions/PermissionSubType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/permissions/PermissionSubType.java new file mode 100644 index 00000000..10b28365 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/permissions/PermissionSubType.java @@ -0,0 +1,36 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.permissions; + + +/******************************************************************************* + ** + *******************************************************************************/ +sealed interface PermissionSubType permits PrivatePermissionSubType, TablePermissionSubType +{ + + /******************************************************************************* + ** + *******************************************************************************/ + String getPermissionSuffix(); + +} 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 new file mode 100644 index 00000000..f4c40993 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/permissions/PermissionsHelper.java @@ -0,0 +1,579 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.permissions; + + +import java.io.Serializable; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.stream.Collectors; +import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; +import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException; +import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; +import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.DenyBehavior; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithName; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; +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.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class PermissionsHelper +{ + private static final Logger LOG = LogManager.getLogger(PermissionsHelper.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void checkTablePermissionThrowing(AbstractTableActionInput tableActionInput, TablePermissionSubType permissionSubType) throws QPermissionDeniedException + { + checkTablePermissionThrowing(tableActionInput, tableActionInput.getTableName(), permissionSubType); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void checkTablePermissionThrowing(AbstractActionInput actionInput, String tableName, TablePermissionSubType permissionSubType) throws QPermissionDeniedException + { + warnAboutPermissionSubTypeForTables(permissionSubType); + QTableMetaData table = actionInput.getInstance().getTable(tableName); + + commonCheckPermissionThrowing(getEffectivePermissionRules(table, actionInput.getInstance()), permissionSubType, table.getName(), actionInput); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static boolean hasTablePermission(AbstractActionInput actionInput, String tableName, TablePermissionSubType permissionSubType) + { + try + { + checkTablePermissionThrowing(actionInput, tableName, permissionSubType); + return (true); + } + catch(QPermissionDeniedException e) + { + return (false); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static PermissionCheckResult getPermissionCheckResult(AbstractActionInput actionInput, MetaDataWithPermissionRules metaDataWithPermissionRules) + { + QPermissionRules rules = getEffectivePermissionRules(metaDataWithPermissionRules, actionInput.getInstance()); + String permissionBaseName = getEffectivePermissionBaseName(rules, metaDataWithPermissionRules.getName()); + + switch(rules.getLevel()) + { + case NOT_PROTECTED: + { + ///////////////////////////////////////////////// + // if the entity isn't protected, always ALLOW // + ///////////////////////////////////////////////// + return PermissionCheckResult.ALLOW; + } + case HAS_ACCESS_PERMISSION: + { + //////////////////////////////////////////////////////////////////////// + // if the entity just has a 'has access', then check for 'has access' // + //////////////////////////////////////////////////////////////////////// + return getPermissionCheckResult(actionInput, rules, permissionBaseName, PrivatePermissionSubType.HAS_ACCESS); + } + case READ_WRITE_PERMISSIONS: + { + //////////////////////////////////////////////////////////////// + // if the table is configured w/ read/write, check for either // + //////////////////////////////////////////////////////////////// + if(metaDataWithPermissionRules instanceof QTableMetaData) + { + return getPermissionCheckResult(actionInput, rules, permissionBaseName, PrivatePermissionSubType.READ, PrivatePermissionSubType.WRITE); + } + return getPermissionCheckResult(actionInput, rules, permissionBaseName, PrivatePermissionSubType.HAS_ACCESS); + } + case READ_INSERT_EDIT_DELETE_PERMISSIONS: + { + ////////////////////////////////////////////////////////////////////////// + // if the table is configured w/ read/insert/edit/delete, check for any // + ////////////////////////////////////////////////////////////////////////// + if(metaDataWithPermissionRules instanceof QTableMetaData) + { + return getPermissionCheckResult(actionInput, rules, permissionBaseName, TablePermissionSubType.READ, TablePermissionSubType.INSERT, TablePermissionSubType.EDIT, TablePermissionSubType.DELETE); + } + return getPermissionCheckResult(actionInput, rules, permissionBaseName, PrivatePermissionSubType.HAS_ACCESS); + } + default: + { + return getPermissionDeniedCheckResult(rules); + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void checkProcessPermissionThrowing(AbstractActionInput actionInput, String processName) throws QPermissionDeniedException + { + checkProcessPermissionThrowing(actionInput, processName, Collections.emptyMap()); + } + + + + static Map customPermissionCheckerMap = new HashMap<>(); + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void checkProcessPermissionThrowing(AbstractActionInput actionInput, String processName, Map processValues) throws QPermissionDeniedException + { + QProcessMetaData process = actionInput.getInstance().getProcess(processName); + QPermissionRules effectivePermissionRules = getEffectivePermissionRules(process, actionInput.getInstance()); + + if(effectivePermissionRules.getCustomPermissionChecker() != null) + { + ///////////////////////////////////// + // todo - avoid stack overflows... // + ///////////////////////////////////// + if(!customPermissionCheckerMap.containsKey(effectivePermissionRules.getCustomPermissionChecker().getName())) + { + CustomPermissionChecker customPermissionChecker = QCodeLoader.getAdHoc(CustomPermissionChecker.class, effectivePermissionRules.getCustomPermissionChecker()); + customPermissionCheckerMap.put(effectivePermissionRules.getCustomPermissionChecker().getName(), customPermissionChecker); + } + CustomPermissionChecker customPermissionChecker = customPermissionCheckerMap.get(effectivePermissionRules.getCustomPermissionChecker().getName()); + customPermissionChecker.checkPermissionsThrowing(actionInput, process); + return; + } + + commonCheckPermissionThrowing(effectivePermissionRules, PrivatePermissionSubType.HAS_ACCESS, process.getName(), actionInput); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static boolean hasProcessPermission(AbstractActionInput actionInput, String processName) + { + try + { + checkProcessPermissionThrowing(actionInput, processName); + return (true); + } + catch(QPermissionDeniedException e) + { + return (false); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void checkAppPermissionThrowing(AbstractActionInput actionInput, String appName) throws QPermissionDeniedException + { + QAppMetaData app = actionInput.getInstance().getApp(appName); + commonCheckPermissionThrowing(getEffectivePermissionRules(app, actionInput.getInstance()), PrivatePermissionSubType.HAS_ACCESS, app.getName(), actionInput); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static boolean hasAppPermission(AbstractActionInput actionInput, String appName) + { + try + { + checkAppPermissionThrowing(actionInput, appName); + return (true); + } + catch(QPermissionDeniedException e) + { + return (false); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void checkReportPermissionThrowing(AbstractActionInput actionInput, String reportName) throws QPermissionDeniedException + { + QReportMetaData report = actionInput.getInstance().getReport(reportName); + commonCheckPermissionThrowing(getEffectivePermissionRules(report, actionInput.getInstance()), PrivatePermissionSubType.HAS_ACCESS, report.getName(), actionInput); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static boolean hasReportPermission(AbstractActionInput actionInput, String reportName) + { + try + { + checkReportPermissionThrowing(actionInput, reportName); + return (true); + } + catch(QPermissionDeniedException e) + { + return (false); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static void checkWidgetPermissionThrowing(AbstractActionInput actionInput, String widgetName) throws QPermissionDeniedException + { + QWidgetMetaDataInterface widget = actionInput.getInstance().getWidget(widgetName); + commonCheckPermissionThrowing(getEffectivePermissionRules(widget, actionInput.getInstance()), PrivatePermissionSubType.HAS_ACCESS, widget.getName(), actionInput); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static boolean hasWidgetPermission(AbstractActionInput actionInput, String widgetName) + { + try + { + checkWidgetPermissionThrowing(actionInput, widgetName); + return (true); + } + catch(QPermissionDeniedException e) + { + return (false); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Collection getAllAvailablePermissionNames(QInstance instance) + { + return (getAllAvailablePermissions(instance).stream() + .map(AvailablePermission::getName) + .collect(Collectors.toCollection(LinkedHashSet::new))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Collection getAllAvailablePermissions(QInstance instance) + { + Collection rs = new LinkedHashSet<>(); + + for(QTableMetaData tableMetaData : instance.getTables().values()) + { + if(tableMetaData.getIsHidden()) + { + continue; + } + + QPermissionRules rules = getEffectivePermissionRules(tableMetaData, instance); + String baseName = getEffectivePermissionBaseName(rules, tableMetaData.getName()); + + for(TablePermissionSubType permissionSubType : TablePermissionSubType.values()) + { + addEffectiveAvailablePermission(rules, permissionSubType, rs, baseName, tableMetaData, "Table"); + } + } + + for(QProcessMetaData processMetaData : instance.getProcesses().values()) + { + if(processMetaData.getIsHidden()) + { + continue; + } + + QPermissionRules rules = getEffectivePermissionRules(processMetaData, instance); + String baseName = getEffectivePermissionBaseName(rules, processMetaData.getName()); + addEffectiveAvailablePermission(rules, PrivatePermissionSubType.HAS_ACCESS, rs, baseName, processMetaData, "Process"); + } + + for(QAppMetaData appMetaData : instance.getApps().values()) + { + QPermissionRules rules = getEffectivePermissionRules(appMetaData, instance); + String baseName = getEffectivePermissionBaseName(rules, appMetaData.getName()); + addEffectiveAvailablePermission(rules, PrivatePermissionSubType.HAS_ACCESS, rs, baseName, appMetaData, "App"); + } + + for(QReportMetaData reportMetaData : instance.getReports().values()) + { + QPermissionRules rules = getEffectivePermissionRules(reportMetaData, instance); + String baseName = getEffectivePermissionBaseName(rules, reportMetaData.getName()); + addEffectiveAvailablePermission(rules, PrivatePermissionSubType.HAS_ACCESS, rs, baseName, reportMetaData, "Report"); + } + + for(QWidgetMetaDataInterface widgetMetaData : instance.getWidgets().values()) + { + QPermissionRules rules = getEffectivePermissionRules(widgetMetaData, instance); + String baseName = getEffectivePermissionBaseName(rules, widgetMetaData.getName()); + addEffectiveAvailablePermission(rules, PrivatePermissionSubType.HAS_ACCESS, rs, baseName, widgetMetaData, "Widget"); + } + + return (rs); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void addEffectiveAvailablePermission(QPermissionRules rules, PermissionSubType permissionSubType, Collection rs, String baseName, MetaDataWithName metaDataWithName, String objectType) + { + PermissionSubType effectivePermissionSubType = getEffectivePermissionSubType(rules, permissionSubType); + if(effectivePermissionSubType != null) + { + rs.add(new AvailablePermission() + .withName(getPermissionName(baseName, effectivePermissionSubType)) + .withObjectName(metaDataWithName.getLabel()) + .withPermissionType(effectivePermissionSubType.toString()) + .withObjectType(objectType)); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QPermissionRules getEffectivePermissionRules(MetaDataWithPermissionRules metaDataWithPermissionRules, QInstance instance) + { + return (metaDataWithPermissionRules.getPermissionRules()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + static boolean hasPermission(QSession session, String permissionBaseName, PermissionSubType permissionSubType) + { + if(permissionSubType == null) + { + return (true); + } + + String permissionName = getPermissionName(permissionBaseName, permissionSubType); + return (session.hasPermission(permissionName)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + static PermissionCheckResult getPermissionCheckResult(AbstractActionInput actionInput, QPermissionRules rules, String permissionBaseName, PermissionSubType... permissionSubTypes) + { + for(PermissionSubType permissionSubType : permissionSubTypes) + { + PermissionSubType effectivePermissionSubType = getEffectivePermissionSubType(rules, permissionSubType); + if(hasPermission(actionInput.getSession(), permissionBaseName, effectivePermissionSubType)) + { + return (PermissionCheckResult.ALLOW); + } + } + + return (getPermissionDeniedCheckResult(rules)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + static String getEffectivePermissionBaseName(QPermissionRules rules, String standardName) + { + if(rules != null && StringUtils.hasContent(rules.getPermissionBaseName())) + { + return (rules.getPermissionBaseName()); + } + + return (standardName); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @SuppressWarnings("checkstyle:indentation") + static PermissionSubType getEffectivePermissionSubType(QPermissionRules rules, PermissionSubType originalPermissionSubType) + { + if(rules == null || rules.getLevel() == null) + { + return (originalPermissionSubType); + } + + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if the original permission sub-type is "hasAccess" - then this is a check for a process/report/widget. // + // in that case - never return the table-level read/write/insert/edit/delete options // + //////////////////////////////////////////////////////////////////////////////////////////////////////////// + if(PrivatePermissionSubType.HAS_ACCESS.equals(originalPermissionSubType)) + { + return switch(rules.getLevel()) + { + case NOT_PROTECTED -> null; + default -> PrivatePermissionSubType.HAS_ACCESS; + }; + } + else + { + //////////////////////////////////////////////////////////////////////////////////////////////////////// + // else, this is a table check - so - based on the rules being used for this table, map the requested // + // permission sub-type to what we expect to be set for the table // + //////////////////////////////////////////////////////////////////////////////////////////////////////// + return switch(rules.getLevel()) + { + case NOT_PROTECTED -> null; + case HAS_ACCESS_PERMISSION -> PrivatePermissionSubType.HAS_ACCESS; + case READ_WRITE_PERMISSIONS -> + { + if(PrivatePermissionSubType.READ.equals(originalPermissionSubType) || PrivatePermissionSubType.WRITE.equals(originalPermissionSubType)) + { + yield (originalPermissionSubType); + } + else if(TablePermissionSubType.INSERT.equals(originalPermissionSubType) || TablePermissionSubType.EDIT.equals(originalPermissionSubType) || TablePermissionSubType.DELETE.equals(originalPermissionSubType)) + { + yield (PrivatePermissionSubType.WRITE); + } + else if(TablePermissionSubType.READ.equals(originalPermissionSubType)) + { + yield (PrivatePermissionSubType.READ); + } + else + { + throw new IllegalStateException("Unexpected permissionSubType: " + originalPermissionSubType); + } + } + case READ_INSERT_EDIT_DELETE_PERMISSIONS -> originalPermissionSubType; + }; + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void commonCheckPermissionThrowing(QPermissionRules rules, PermissionSubType permissionSubType, String name, AbstractActionInput actionInput) throws QPermissionDeniedException + { + PermissionSubType effectivePermissionSubType = getEffectivePermissionSubType(rules, permissionSubType); + String permissionBaseName = getEffectivePermissionBaseName(rules, name); + + if(effectivePermissionSubType == null) + { + return; + } + + if(!hasPermission(actionInput.getSession(), permissionBaseName, effectivePermissionSubType)) + { + LOG.debug("Throwing permission denied for: " + getPermissionName(permissionBaseName, effectivePermissionSubType) + " for " + actionInput.getSession().getUser()); + throw (new QPermissionDeniedException("Permission denied.")); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static String getPermissionName(String permissionBaseName, PermissionSubType permissionSubType) + { + return permissionBaseName + "." + permissionSubType.getPermissionSuffix(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static PermissionCheckResult getPermissionDeniedCheckResult(QPermissionRules rules) + { + if(rules == null || rules.getDenyBehavior() == null || rules.getDenyBehavior().equals(DenyBehavior.HIDDEN)) + { + return (PermissionCheckResult.DENY_HIDE); + } + else + { + return (PermissionCheckResult.DENY_DISABLE); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void warnAboutPermissionSubTypeForTables(PermissionSubType permissionSubType) + { + if(permissionSubType == null) + { + return; + } + + if(permissionSubType == PrivatePermissionSubType.HAS_ACCESS) + { + LOG.warn("PermissionSubType.HAS_ACCESS should not be checked for a table"); + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/permissions/PrivatePermissionSubType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/permissions/PrivatePermissionSubType.java new file mode 100644 index 00000000..94f03f6d --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/permissions/PrivatePermissionSubType.java @@ -0,0 +1,56 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.permissions; + + +/******************************************************************************* + ** + *******************************************************************************/ +enum PrivatePermissionSubType implements PermissionSubType +{ + HAS_ACCESS("hasAccess"), // for processes, reports, etc - basically, not tables. + READ("read"), // for a table in read/write mode - or - for read (query, get, count) on a table in full-mode + WRITE("write"); + + private final String permissionSuffix; + + + + /******************************************************************************* + ** + *******************************************************************************/ + PrivatePermissionSubType(String permissionSuffix) + { + this.permissionSuffix = permissionSuffix; + } + + + + /******************************************************************************* + ** Getter for permissionSuffix + *******************************************************************************/ + public String getPermissionSuffix() + { + return (this.permissionSuffix); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/permissions/ReportProcessPermissionChecker.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/permissions/ReportProcessPermissionChecker.java new file mode 100644 index 00000000..f890dc90 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/permissions/ReportProcessPermissionChecker.java @@ -0,0 +1,53 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.permissions; + + +import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException; +import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class ReportProcessPermissionChecker implements CustomPermissionChecker +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void checkPermissionsThrowing(AbstractActionInput actionInput, MetaDataWithPermissionRules metaDataWithPermissionRules) throws QPermissionDeniedException + { + if(actionInput instanceof RunProcessInput runProcessInput) + { + String reportName = runProcessInput.getValueString("reportName"); + if(reportName != null) + { + PermissionsHelper.checkReportPermissionThrowing(actionInput, reportName); + } + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/permissions/TablePermissionSubType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/permissions/TablePermissionSubType.java new file mode 100644 index 00000000..2c910642 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/permissions/TablePermissionSubType.java @@ -0,0 +1,57 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.permissions; + + +/******************************************************************************* + ** + *******************************************************************************/ +public enum TablePermissionSubType implements PermissionSubType +{ + READ("read"), // for a table in read/write mode - or - for read (query, get, count) on a table in full-mode + INSERT("insert"), // for table-insert. + EDIT("edit"), // for table-edit. + DELETE("delete"); // for table-delete. + + private final String permissionSuffix; + + + + /******************************************************************************* + ** + *******************************************************************************/ + TablePermissionSubType(String permissionSuffix) + { + this.permissionSuffix = permissionSuffix; + } + + + + /******************************************************************************* + ** Getter for permissionSuffix + *******************************************************************************/ + public String getPermissionSuffix() + { + return (this.permissionSuffix); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QPermissionDeniedException.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QPermissionDeniedException.java new file mode 100644 index 00000000..76e617a4 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/exceptions/QPermissionDeniedException.java @@ -0,0 +1,51 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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.exceptions; + + +/******************************************************************************* + * Exception thrown if user doesn't have permission for an action + * + *******************************************************************************/ +public class QPermissionDeniedException extends QException +{ + + /******************************************************************************* + ** Constructor of message + ** + *******************************************************************************/ + public QPermissionDeniedException(String message) + { + super(message); + } + + + + /******************************************************************************* + ** Constructor of message & cause + ** + *******************************************************************************/ + public QPermissionDeniedException(String message, Throwable cause) + { + super(message, cause); + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java index 4f85eee3..54e193b8 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/instances/QInstanceEnricher.java @@ -32,8 +32,12 @@ import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import com.kingsrook.qqq.backend.core.actions.permissions.BulkTableActionProcessPermissionChecker; 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.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; +import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface; import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType; import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; @@ -42,6 +46,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection; import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType; import com.kingsrook.qqq.backend.core.model.metadata.processes.QComponentType; @@ -133,6 +139,21 @@ public class QInstanceEnricher { qInstance.getPossibleValueSources().values().forEach(this::enrichPossibleValueSource); } + + if(qInstance.getWidgets() != null) + { + qInstance.getWidgets().values().forEach(this::enrichWidget); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void enrichWidget(QWidgetMetaDataInterface widgetMetaData) + { + enrichPermissionRules(widgetMetaData); } @@ -175,6 +196,60 @@ public class QInstanceEnricher { table.setRecordLabelFormat(String.join(" ", Collections.nCopies(table.getRecordLabelFields().size(), "%s"))); } + + enrichPermissionRules(table); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void enrichPermissionRules(MetaDataWithPermissionRules metaDataWithPermissionRules) + { + /////////////////////////////////////////////////////////////////////// + // make sure there's a permissionsRule object in the metaData object // + /////////////////////////////////////////////////////////////////////// + if(metaDataWithPermissionRules.getPermissionRules() == null) + { + if(qInstance.getDefaultPermissionRules() != null) + { + metaDataWithPermissionRules.setPermissionRules(qInstance.getDefaultPermissionRules().clone()); + } + else + { + metaDataWithPermissionRules.setPermissionRules(QPermissionRules.defaultInstance().clone()); + } + } + + QPermissionRules permissionRules = metaDataWithPermissionRules.getPermissionRules(); + + ///////////////////////////////////////////////////////////////////////////////// + // now make sure the required fields are all set in the permissionRules object // + ///////////////////////////////////////////////////////////////////////////////// + if(permissionRules.getLevel() == null) + { + if(qInstance.getDefaultPermissionRules() != null && qInstance.getDefaultPermissionRules().getLevel() != null) + { + permissionRules.setLevel(qInstance.getDefaultPermissionRules().getLevel()); + } + else + { + permissionRules.setLevel(QPermissionRules.defaultInstance().getLevel()); + } + } + + if(permissionRules.getDenyBehavior() == null) + { + if(qInstance.getDefaultPermissionRules() != null && qInstance.getDefaultPermissionRules().getDenyBehavior() != null) + { + permissionRules.setDenyBehavior(qInstance.getDefaultPermissionRules().getDenyBehavior()); + } + else + { + permissionRules.setDenyBehavior(QPermissionRules.defaultInstance().getDenyBehavior()); + } + } } @@ -193,6 +268,8 @@ public class QInstanceEnricher { process.getStepList().forEach(this::enrichStep); } + + enrichPermissionRules(process); } @@ -323,6 +400,8 @@ public class QInstanceEnricher { enrichAppSection(section); } + + enrichPermissionRules(app); } @@ -411,6 +490,8 @@ public class QInstanceEnricher } } } + + enrichPermissionRules(report); } @@ -506,7 +587,9 @@ public class QInstanceEnricher .withName(processName) .withLabel(table.getLabel() + " Bulk Insert") .withTableName(table.getName()) - .withIsHidden(true); + .withIsHidden(true) + .withPermissionRules(qInstance.getDefaultPermissionRules().clone() + .withCustomPermissionChecker(new QCodeReference(BulkTableActionProcessPermissionChecker.class, QCodeUsage.CUSTOMIZER))); List editableFields = new ArrayList<>(); for(QFieldSection section : CollectionUtils.nonNullList(table.getSections())) @@ -568,7 +651,9 @@ public class QInstanceEnricher .withName(processName) .withLabel(table.getLabel() + " Bulk Edit") .withTableName(table.getName()) - .withIsHidden(true); + .withIsHidden(true) + .withPermissionRules(qInstance.getDefaultPermissionRules().clone() + .withCustomPermissionChecker(new QCodeReference(BulkTableActionProcessPermissionChecker.class, QCodeUsage.CUSTOMIZER))); List editableFields = table.getFields().values().stream() .filter(QFieldMetaData::getIsEditable) @@ -613,7 +698,9 @@ public class QInstanceEnricher .withName(processName) .withLabel(table.getLabel() + " Bulk Delete") .withTableName(table.getName()) - .withIsHidden(true); + .withIsHidden(true) + .withPermissionRules(qInstance.getDefaultPermissionRules().clone() + .withCustomPermissionChecker(new QCodeReference(BulkTableActionProcessPermissionChecker.class, QCodeUsage.CUSTOMIZER))); List tableFields = table.getFields().values().stream().toList(); process.getFrontendStep("review").setRecordListFields(tableFields); 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 213f37a1..ae0611e9 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 @@ -61,6 +61,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMeta import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView; +import com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock; +import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock; import com.kingsrook.qqq.backend.core.model.metadata.tables.AssociatedScript; import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; @@ -138,6 +140,7 @@ public class QInstanceValidator validatePossibleValueSources(qInstance); validateQueuesAndProviders(qInstance); validateJoins(qInstance); + validateSecurityKeyTypes(qInstance); validateUniqueTopLevelNames(qInstance); } @@ -156,6 +159,35 @@ public class QInstanceValidator + /******************************************************************************* + ** + *******************************************************************************/ + private void validateSecurityKeyTypes(QInstance qInstance) + { + Set usedNames = new HashSet<>(); + qInstance.getSecurityKeyTypes().forEach((name, securityKeyType) -> + { + 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); + usedNames.add(name); + if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName())) + { + assertCondition(!usedNames.contains(securityKeyType.getAllAccessKeyName()), "More than one SecurityKeyType with name (or allAccessKeyName) of: " + securityKeyType.getAllAccessKeyName()); + usedNames.add(securityKeyType.getAllAccessKeyName()); + } + + if(StringUtils.hasContent(securityKeyType.getPossibleValueSourceName())) + { + assertCondition(qInstance.getPossibleValueSource(securityKeyType.getPossibleValueSourceName()) != null, "Unrecognized possibleValueSourceName in securityKeyType: " + name); + } + } + }); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -387,17 +419,11 @@ public class QInstanceValidator } } - if(CollectionUtils.nullSafeHasContents(table.getFields())) + for(String fieldName : CollectionUtils.nonNullMap(table.getFields()).keySet()) { - for(String fieldName : table.getFields().keySet()) - { - assertCondition(fieldNamesInSections.contains(fieldName), "Table " + tableName + " field " + fieldName + " is not listed in any field sections."); - } + assertCondition(fieldNamesInSections.contains(fieldName), "Table " + tableName + " field " + fieldName + " is not listed in any field sections."); } - /////////////////////////////// - // validate the record label // - /////////////////////////////// if(table.getRecordLabelFields() != null && table.getFields() != null) { for(String recordLabelField : table.getRecordLabelFields()) @@ -406,51 +432,51 @@ public class QInstanceValidator } } - if(table.getCustomizers() != null) + for(Map.Entry entry : CollectionUtils.nonNullMap(table.getCustomizers()).entrySet()) { - for(Map.Entry entry : table.getCustomizers().entrySet()) - { - validateTableCustomizer(tableName, entry.getKey(), entry.getValue()); - } + validateTableCustomizer(tableName, entry.getKey(), entry.getValue()); } - ////////////////////////////////////// - // validate the table's automations // - ////////////////////////////////////// - if(table.getAutomationDetails() != null) - { - validateTableAutomationDetails(qInstance, table); - } - - ////////////////////////////////////// - // validate the table's unique keys // - ////////////////////////////////////// - if(table.getUniqueKeys() != null) - { - validateTableUniqueKeys(table); - } - - ///////////////////////////////////////////// - // validate the table's associated scripts // - ///////////////////////////////////////////// - if(table.getAssociatedScripts() != null) - { - validateAssociatedScripts(table); - } - - ////////////////////// - // validate cacheOf // - ////////////////////// - if(table.getCacheOf() != null) - { - validateTableCacheOf(qInstance, table); - } + validateTableAutomationDetails(qInstance, table); + validateTableUniqueKeys(table); + validateAssociatedScripts(table); + validateTableCacheOf(qInstance, table); + validateTableRecordSecurityLocks(qInstance, table); }); } } + /******************************************************************************* + ** + *******************************************************************************/ + private void validateTableRecordSecurityLocks(QInstance qInstance, QTableMetaData table) + { + String prefix = "Table " + table.getName() + " "; + + for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks())) + { + String securityKeyTypeName = recordSecurityLock.getSecurityKeyType(); + if(assertCondition(StringUtils.hasContent(securityKeyTypeName), prefix + "has a recordSecurityLock that is missing a securityKeyType")) + { + assertCondition(qInstance.getSecurityKeyType(securityKeyTypeName) != null, prefix + "has a recordSecurityLock with an unrecognized securityKeyType: " + securityKeyTypeName); + } + + prefix = "Table " + table.getName() + " recordSecurityLock (of key type " + securityKeyTypeName + ") "; + + String fieldName = recordSecurityLock.getFieldName(); + if(assertCondition(StringUtils.hasContent(fieldName), prefix + "is missing a fieldName")) + { + assertCondition(findField(qInstance, table, null, fieldName), prefix + "has an unrecognized fieldName: " + fieldName); + } + + assertCondition(recordSecurityLock.getNullValueBehavior() != null, prefix + "is missing a nullValueBehavior"); + } + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -465,16 +491,36 @@ public class QInstanceValidator "Unrecognized possibleValueSourceName " + field.getPossibleValueSourceName() + " in table " + tableName + " for field " + fieldName + "."); } + String prefix = "Field " + fieldName + " in table " + tableName + " "; + ValueTooLongBehavior behavior = field.getBehavior(qInstance, ValueTooLongBehavior.class); if(behavior != null && !behavior.equals(ValueTooLongBehavior.PASS_THROUGH)) { - assertCondition(field.getMaxLength() != null, "Field " + fieldName + " in table " + tableName + " specifies a ValueTooLongBehavior, but not a maxLength."); + assertCondition(field.getMaxLength() != null, prefix + "specifies a ValueTooLongBehavior, but not a maxLength."); } if(field.getMaxLength() != null) { - assertCondition(field.getMaxLength() > 0, "Field " + fieldName + " in table " + tableName + " has an invalid maxLength (" + field.getMaxLength() + ") - must be greater than 0."); - assertCondition(field.getType().isStringLike(), "Field " + fieldName + " in table " + tableName + " has maxLength, but is not of a supported type (" + field.getType() + ") - must be a string-like type."); + assertCondition(field.getMaxLength() > 0, prefix + "has an invalid maxLength (" + field.getMaxLength() + ") - must be greater than 0."); + assertCondition(field.getType().isStringLike(), prefix + "has maxLength, but is not of a supported type (" + field.getType() + ") - must be a string-like type."); + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // this condition doesn't make sense/apply - because the default value-too-long behavior is pass-through, so, idk, just omit // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // assertCondition(behavior != null, prefix + "specifies a maxLength, but no ValueTooLongBehavior."); + } + + FieldSecurityLock fieldSecurityLock = field.getFieldSecurityLock(); + if(fieldSecurityLock != null) + { + String securityKeyTypeName = fieldSecurityLock.getSecurityKeyType(); + if(assertCondition(StringUtils.hasContent(securityKeyTypeName), prefix + "has a fieldSecurityLock that is missing a securityKeyType")) + { + assertCondition(qInstance.getSecurityKeyType(securityKeyTypeName) != null, prefix + "has a fieldSecurityLock with an unrecognized securityKeyType: " + securityKeyTypeName); + } + + 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"); } } @@ -485,9 +531,14 @@ public class QInstanceValidator *******************************************************************************/ private void validateTableCacheOf(QInstance qInstance, QTableMetaData table) { - CacheOf cacheOf = table.getCacheOf(); - String prefix = "Table " + table.getName() + " cacheOf "; - String sourceTableName = cacheOf.getSourceTable(); + CacheOf cacheOf = table.getCacheOf(); + if(cacheOf == null) + { + return; + } + + String prefix = "Table " + table.getName() + " cacheOf "; + String sourceTableName = cacheOf.getSourceTable(); if(assertCondition(StringUtils.hasContent(sourceTableName), prefix + "is missing a sourceTable name")) { assertCondition(qInstance.getTable(sourceTableName) != null, prefix + "is referencing an unknown sourceTable: " + sourceTableName); @@ -519,7 +570,7 @@ public class QInstanceValidator private void validateAssociatedScripts(QTableMetaData table) { Set usedFieldNames = new HashSet<>(); - for(AssociatedScript associatedScript : table.getAssociatedScripts()) + for(AssociatedScript associatedScript : CollectionUtils.nonNullList(table.getAssociatedScripts())) { if(assertCondition(StringUtils.hasContent(associatedScript.getFieldName()), "Table " + table.getName() + " has an associatedScript without a fieldName")) { @@ -548,7 +599,7 @@ public class QInstanceValidator private void validateTableUniqueKeys(QTableMetaData table) { Set> ukSets = new HashSet<>(); - for(UniqueKey uniqueKey : table.getUniqueKeys()) + for(UniqueKey uniqueKey : CollectionUtils.nonNullList(table.getUniqueKeys())) { if(assertCondition(CollectionUtils.nullSafeHasContents(uniqueKey.getFieldNames()), table.getName() + " has a uniqueKey with no fields")) { @@ -573,11 +624,15 @@ public class QInstanceValidator *******************************************************************************/ private void validateTableAutomationDetails(QInstance qInstance, QTableMetaData table) { + QTableAutomationDetails automationDetails = table.getAutomationDetails(); + if(automationDetails == null) + { + return; + } + String tableName = table.getName(); String prefix = "Table " + tableName + " automationDetails "; - QTableAutomationDetails automationDetails = table.getAutomationDetails(); - ////////////////////////////////////// // validate the automation provider // ////////////////////////////////////// @@ -1028,7 +1083,7 @@ public class QInstanceValidator assertCondition(usedDataSourceNames.contains(view.getVarianceDataSourceName()), viewErrorPrefix + "has an unrecognized varianceDataSourceName: " + view.getVarianceDataSourceName()); } - boolean hasColumns = CollectionUtils.nullSafeHasContents(view.getColumns()); + boolean hasColumns = CollectionUtils.nullSafeHasContents(view.getColumns()); boolean hasViewCustomizer = view.getViewCustomizer() != null; assertCondition(hasColumns || hasViewCustomizer, viewErrorPrefix + "does not have any columns or a view customizer."); @@ -1092,7 +1147,7 @@ public class QInstanceValidator /******************************************************************************* ** Look for a field name in either a table, or the tables referenced in a list of query joins. *******************************************************************************/ - private static boolean findField(QInstance qInstance, QTableMetaData table, List queryJoins, String fieldName) + private boolean findField(QInstance qInstance, QTableMetaData table, List queryJoins, String fieldName) { boolean foundField = false; try @@ -1105,22 +1160,34 @@ public class QInstanceValidator if(fieldName.contains(".")) { String fieldNameAfterDot = fieldName.substring(fieldName.lastIndexOf(".") + 1); - for(QueryJoin queryJoin : CollectionUtils.nonNullList(queryJoins)) + + if(CollectionUtils.nullSafeHasContents(queryJoins)) { - QTableMetaData joinTable = qInstance.getTable(queryJoin.getJoinTable()); - if(joinTable != null) + for(QueryJoin queryJoin : CollectionUtils.nonNullList(queryJoins)) { - try + QTableMetaData joinTable = qInstance.getTable(queryJoin.getJoinTable()); + if(joinTable != null) { - joinTable.getField(fieldNameAfterDot); - foundField = true; - } - catch(Exception e2) - { - continue; + try + { + joinTable.getField(fieldNameAfterDot); + foundField = true; + } + catch(Exception e2) + { + continue; + } } } } + else + { + errors.add("QInstanceValidator does not yet support finding a field that looks like a join field, but isn't associated with a query."); + return (true); + // todo! for(QJoinMetaData join : CollectionUtils.nonNullMap(qInstance.getJoins()).values()) + // { + // } + } } } return foundField; diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/AbstractActionInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/AbstractActionInput.java index 9fd77e29..a771475d 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/AbstractActionInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/AbstractActionInput.java @@ -186,4 +186,27 @@ public class AbstractActionInput { this.asyncJobCallback = asyncJobCallback; } + + + + /******************************************************************************* + ** Fluent setter for instance + *******************************************************************************/ + public AbstractActionInput withInstance(QInstance instance) + { + this.instance = instance; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for session + *******************************************************************************/ + public AbstractActionInput withSession(QSession session) + { + this.session = session; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/AbstractTableActionInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/AbstractTableActionInput.java index fe6b7f02..74295d6a 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/AbstractTableActionInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/AbstractTableActionInput.java @@ -25,13 +25,14 @@ package com.kingsrook.qqq.backend.core.model.actions; 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.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.session.QSession; /******************************************************************************* ** Base class for input for any qqq action that works against a table. ** *******************************************************************************/ -public abstract class AbstractTableActionInput extends AbstractActionInput +public class AbstractTableActionInput extends AbstractActionInput { private String tableName; @@ -95,4 +96,28 @@ public abstract class AbstractTableActionInput extends AbstractActionInput { this.tableName = tableName; } + + + + /******************************************************************************* + ** Fluent setter for tableName + *******************************************************************************/ + public AbstractTableActionInput withTableName(String tableName) + { + this.tableName = tableName; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for session + *******************************************************************************/ + @Override + public AbstractTableActionInput withSession(QSession session) + { + super.withSession(session); + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/metadata/TableMetaDataInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/metadata/TableMetaDataInput.java index 55263dc2..e3ee9486 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/metadata/TableMetaDataInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/metadata/TableMetaDataInput.java @@ -22,7 +22,7 @@ package com.kingsrook.qqq.backend.core.model.actions.metadata; -import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; +import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; @@ -30,11 +30,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance; ** Input for meta-data for a table. ** *******************************************************************************/ -public class TableMetaDataInput extends AbstractActionInput +public class TableMetaDataInput extends AbstractTableActionInput { - private String tableName; - - /******************************************************************************* ** @@ -53,25 +50,4 @@ public class TableMetaDataInput extends AbstractActionInput super(instance); } - - - /******************************************************************************* - ** Getter for tableName - ** - *******************************************************************************/ - public String getTableName() - { - return tableName; - } - - - - /******************************************************************************* - ** Setter for tableName - ** - *******************************************************************************/ - public void setTableName(String tableName) - { - this.tableName = tableName; - } } 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 8110ba82..852a00ac 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 @@ -28,6 +28,7 @@ import java.util.Map; import com.kingsrook.qqq.backend.core.exceptions.QException; 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.RecordSecurityLock; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; @@ -69,6 +70,37 @@ public class JoinsContext } aliasToTableNameMap.put(tableNameOrAlias, joinTable.getName()); } + + /////////////////////////////////////////////////////////////// + // ensure any joins that contribute a recordLock are present // + /////////////////////////////////////////////////////////////// + for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(instance.getTable(tableName).getRecordSecurityLocks())) + { + for(String joinName : CollectionUtils.nonNullList(recordSecurityLock.getJoinChain())) + { + if(this.queryJoins.stream().anyMatch(qj -> qj.getJoinMetaData().getName().equals(joinName))) + { + /////////////////////////////////////////////////////// + // we're good - we're already joining on this table! // + /////////////////////////////////////////////////////// + } + else + { + this.queryJoins.add(new QueryJoin().withJoinMetaData(instance.getJoin(joinName)).withType(QueryJoin.Type.INNER)); // todo aliases? probably. + } + } + } + + /* todo!! + for(QueryJoin queryJoin : queryJoins) + { + QTableMetaData joinTable = instance.getTable(queryJoin.getJoinTable()); + for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(joinTable.getRecordSecurityLocks())) + { + // addCriteriaForRecordSecurityLock(instance, session, joinTable, securityCriteria, recordSecurityLock, joinsContext, queryJoin.getJoinTableOrItsAlias()); + } + } + */ } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QCriteriaOperator.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QCriteriaOperator.java index 5d0f21f7..abf3b81c 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QCriteriaOperator.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QCriteriaOperator.java @@ -32,6 +32,7 @@ public enum QCriteriaOperator NOT_EQUALS, IN, NOT_IN, + IS_NULL_OR_IN, STARTS_WITH, ENDS_WITH, CONTAINS, diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java index 9b02e1f8..ba90465c 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QQueryFilter.java @@ -340,7 +340,7 @@ public class QQueryFilter implements Serializable, Cloneable { for(QFilterCriteria criterion : CollectionUtils.nonNullList(criteria)) { - rs.append(criterion).append(" ").append(getBooleanOperator()); + rs.append(criterion).append(" ").append(getBooleanOperator()).append(" "); } for(QQueryFilter subFilter : CollectionUtils.nonNullList(subFilters)) 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 482ca631..9e5210af 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 @@ -25,8 +25,10 @@ package com.kingsrook.qqq.backend.core.model.metadata; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Set; import com.fasterxml.jackson.annotation.JsonIgnore; import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction; import com.kingsrook.qqq.backend.core.exceptions.QException; @@ -42,13 +44,16 @@ import com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNode; import com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNodeType; import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueMetaData; import com.kingsrook.qqq.backend.core.model.metadata.queues.QQueueProviderMetaData; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; +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.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import io.github.cdimascio.dotenv.Dotenv; import io.github.cdimascio.dotenv.DotenvEntry; @@ -73,20 +78,21 @@ public class QInstance //////////////////////////////////////////////////////////////////////////////////////////// // Important to use LinkedHashmap here, to preserve the order in which entries are added. // //////////////////////////////////////////////////////////////////////////////////////////// - private Map tables = new LinkedHashMap<>(); - private Map joins = new LinkedHashMap<>(); - private Map possibleValueSources = new LinkedHashMap<>(); - private Map processes = new LinkedHashMap<>(); - private Map apps = new LinkedHashMap<>(); - private Map reports = new LinkedHashMap<>(); - - private Map widgets = new LinkedHashMap<>(); - - private Map queueProviders = new LinkedHashMap<>(); - private Map queues = new LinkedHashMap<>(); + private Map tables = new LinkedHashMap<>(); + private Map joins = new LinkedHashMap<>(); + private Map possibleValueSources = new LinkedHashMap<>(); + private Map processes = new LinkedHashMap<>(); + private Map apps = new LinkedHashMap<>(); + private Map reports = new LinkedHashMap<>(); + private Map securityKeyTypes = new LinkedHashMap<>(); + private Map widgets = new LinkedHashMap<>(); + private Map queueProviders = new LinkedHashMap<>(); + private Map queues = new LinkedHashMap<>(); private Map environmentValues = new LinkedHashMap<>(); + private QPermissionRules defaultPermissionRules = QPermissionRules.defaultInstance(); + // todo - lock down the object (no more changes allowed) after it's been validated? @JsonIgnore @@ -249,16 +255,7 @@ public class QInstance *******************************************************************************/ public void addBackend(QBackendMetaData backend) { - addBackend(backend.getName(), backend); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - public void addBackend(String name, QBackendMetaData backend) - { + String name = backend.getName(); if(!StringUtils.hasContent(name)) { throw (new IllegalArgumentException("Attempted to add a backend without a name.")); @@ -309,16 +306,7 @@ public class QInstance *******************************************************************************/ public void addTable(QTableMetaData table) { - addTable(table.getName(), table); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - public void addTable(String name, QTableMetaData table) - { + String name = table.getName(); if(!StringUtils.hasContent(name)) { throw (new IllegalArgumentException("Attempted to add a table without a name.")); @@ -374,16 +362,7 @@ public class QInstance *******************************************************************************/ public void addJoin(QJoinMetaData join) { - addJoin(join.getName(), join); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - public void addJoin(String name, QJoinMetaData join) - { + String name = join.getName(); if(!StringUtils.hasContent(name)) { throw (new IllegalArgumentException("Attempted to add a join without a name.")); @@ -439,16 +418,7 @@ public class QInstance *******************************************************************************/ public void addPossibleValueSource(QPossibleValueSource possibleValueSource) { - this.addPossibleValueSource(possibleValueSource.getName(), possibleValueSource); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - public void addPossibleValueSource(String name, QPossibleValueSource possibleValueSource) - { + String name = possibleValueSource.getName(); if(!StringUtils.hasContent(name)) { throw (new IllegalArgumentException("Attempted to add a possibleValueSource without a name.")); @@ -515,16 +485,7 @@ public class QInstance *******************************************************************************/ public void addProcess(QProcessMetaData process) { - this.addProcess(process.getName(), process); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - public void addProcess(String name, QProcessMetaData process) - { + String name = process.getName(); if(!StringUtils.hasContent(name)) { throw (new IllegalArgumentException("Attempted to add a process without a name.")); @@ -575,19 +536,10 @@ public class QInstance *******************************************************************************/ public void addApp(QAppMetaData app) { - this.addApp(app.getName(), app); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - public void addApp(String name, QAppMetaData app) - { + String name = app.getName(); if(!StringUtils.hasContent(name)) { - throw (new IllegalArgumentException("Attempted to add an app without a name.")); + throw (new IllegalArgumentException("Attempted to add a app without a name.")); } if(this.apps.containsKey(name)) { @@ -635,19 +587,10 @@ public class QInstance *******************************************************************************/ public void addReport(QReportMetaData report) { - this.addReport(report.getName(), report); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - public void addReport(String name, QReportMetaData report) - { + String name = report.getName(); if(!StringUtils.hasContent(name)) { - throw (new IllegalArgumentException("Attempted to add an report without a name.")); + throw (new IllegalArgumentException("Attempted to add a report without a name.")); } if(this.reports.containsKey(name)) { @@ -693,9 +636,14 @@ public class QInstance /******************************************************************************* ** *******************************************************************************/ - public void addAutomationProvider(QAutomationProviderMetaData automationProvider) + public void addSecurityKeyType(QSecurityKeyType securityKeyType) { - this.addAutomationProvider(automationProvider.getName(), automationProvider); + String name = securityKeyType.getName(); + if(this.securityKeyTypes.containsKey(name)) + { + throw (new IllegalArgumentException("Attempted to add a second securityKeyType with name: " + name)); + } + this.securityKeyTypes.put(name, securityKeyType); } @@ -703,8 +651,41 @@ public class QInstance /******************************************************************************* ** *******************************************************************************/ - public void addAutomationProvider(String name, QAutomationProviderMetaData automationProvider) + public QSecurityKeyType getSecurityKeyType(String name) { + return (this.securityKeyTypes.get(name)); + } + + + + /******************************************************************************* + ** Getter for securityKeyTypes + ** + *******************************************************************************/ + public Map getSecurityKeyTypes() + { + return securityKeyTypes; + } + + + + /******************************************************************************* + ** Setter for securityKeyTypes + ** + *******************************************************************************/ + public void setSecurityKeyTypes(Map securityKeyTypes) + { + this.securityKeyTypes = securityKeyTypes; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void addAutomationProvider(QAutomationProviderMetaData automationProvider) + { + String name = automationProvider.getName(); if(this.automationProviders.containsKey(name)) { throw (new IllegalArgumentException("Attempted to add a second automationProvider with name: " + name)); @@ -839,16 +820,7 @@ public class QInstance *******************************************************************************/ public void addWidget(QWidgetMetaDataInterface widget) { - this.addWidget(widget.getName(), widget); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - public void addWidget(String name, QWidgetMetaDataInterface widget) - { + String name = widget.getName(); if(this.widgets.containsKey(name)) { throw (new IllegalArgumentException("Attempted to add a second widget with name: " + name)); @@ -873,19 +845,10 @@ public class QInstance *******************************************************************************/ public void addQueueProvider(QQueueProviderMetaData queueProvider) { - this.addQueueProvider(queueProvider.getName(), queueProvider); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - public void addQueueProvider(String name, QQueueProviderMetaData queueProvider) - { + String name = queueProvider.getName(); if(!StringUtils.hasContent(name)) { - throw (new IllegalArgumentException("Attempted to add an queueProvider without a name.")); + throw (new IllegalArgumentException("Attempted to add a queueProvider without a name.")); } if(this.queueProviders.containsKey(name)) { @@ -933,19 +896,10 @@ public class QInstance *******************************************************************************/ public void addQueue(QQueueMetaData queue) { - this.addQueue(queue.getName(), queue); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - public void addQueue(String name, QQueueMetaData queue) - { + String name = queue.getName(); if(!StringUtils.hasContent(name)) { - throw (new IllegalArgumentException("Attempted to add an queue without a name.")); + throw (new IllegalArgumentException("Attempted to add a queue without a name.")); } if(this.queues.containsKey(name)) { @@ -1008,4 +962,54 @@ public class QInstance this.environmentValues = environmentValues; } + + + /******************************************************************************* + ** Getter for defaultPermissionRules + *******************************************************************************/ + public QPermissionRules getDefaultPermissionRules() + { + return (this.defaultPermissionRules); + } + + + + /******************************************************************************* + ** Setter for defaultPermissionRules + *******************************************************************************/ + public void setDefaultPermissionRules(QPermissionRules defaultPermissionRules) + { + this.defaultPermissionRules = defaultPermissionRules; + } + + + + /******************************************************************************* + ** Fluent setter for defaultPermissionRules + *******************************************************************************/ + public QInstance withDefaultPermissionRules(QPermissionRules defaultPermissionRules) + { + this.defaultPermissionRules = defaultPermissionRules; + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public Set getAllowedSecurityKeyNames() + { + Set rs = new LinkedHashSet<>(); + for(QSecurityKeyType securityKeyType : CollectionUtils.nonNullMap(getSecurityKeyTypes()).values()) + { + rs.add(securityKeyType.getName()); + if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName())) + { + rs.add(securityKeyType.getAllAccessKeyName()); + } + } + return (rs); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/Auth0AuthenticationMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/Auth0AuthenticationMetaData.java index e0d46f5e..7f1999e6 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/Auth0AuthenticationMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/authentication/Auth0AuthenticationMetaData.java @@ -35,6 +35,7 @@ public class Auth0AuthenticationMetaData extends QAuthenticationMetaData { private String baseUrl; private String clientId; + private String audience; //////////////////////////////////////////////////////////////////////////////////////// // keep this secret, on the server - don't let it be serialized and sent to a client! // @@ -156,4 +157,36 @@ public class Auth0AuthenticationMetaData extends QAuthenticationMetaData { this.clientSecret = clientSecret; } + + + + /******************************************************************************* + ** Getter for audience + *******************************************************************************/ + public String getAudience() + { + return (this.audience); + } + + + + /******************************************************************************* + ** Setter for audience + *******************************************************************************/ + public void setAudience(String audience) + { + this.audience = audience; + } + + + + /******************************************************************************* + ** Fluent setter for audience + *******************************************************************************/ + public Auth0AuthenticationMetaData withAudience(String audience) + { + this.audience = audience; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/QWidgetMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/QWidgetMetaData.java index 6f50d022..31cb48d5 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/QWidgetMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/QWidgetMetaData.java @@ -28,6 +28,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules; /******************************************************************************* @@ -44,6 +45,8 @@ public class QWidgetMetaData implements QWidgetMetaDataInterface protected Integer gridColumns; protected QCodeReference codeReference; + private QPermissionRules permissionRules; + private List dropdowns; protected Map defaultValues = new LinkedHashMap<>(); @@ -387,4 +390,37 @@ public class QWidgetMetaData implements QWidgetMetaDataInterface return (this); } + + + /******************************************************************************* + ** Getter for permissionRules + *******************************************************************************/ + @Override + public QPermissionRules getPermissionRules() + { + return (this.permissionRules); + } + + + + /******************************************************************************* + ** Setter for permissionRules + *******************************************************************************/ + @Override + public void setPermissionRules(QPermissionRules permissionRules) + { + this.permissionRules = permissionRules; + } + + + + /******************************************************************************* + ** Fluent setter for permissionRules + *******************************************************************************/ + public QWidgetMetaData withPermissionRules(QPermissionRules permissionRules) + { + this.permissionRules = permissionRules; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/QWidgetMetaDataInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/QWidgetMetaDataInterface.java index 335a4480..30c987aa 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/QWidgetMetaDataInterface.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/dashboard/QWidgetMetaDataInterface.java @@ -25,13 +25,15 @@ package com.kingsrook.qqq.backend.core.model.metadata.dashboard; import java.io.Serializable; import java.util.Map; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules; /******************************************************************************* ** Interface for qqq widget meta data ** *******************************************************************************/ -public interface QWidgetMetaDataInterface +public interface QWidgetMetaDataInterface extends MetaDataWithPermissionRules { /******************************************************************************* ** Getter for name @@ -148,5 +150,17 @@ public interface QWidgetMetaDataInterface *******************************************************************************/ QWidgetMetaData withDefaultValue(String key, Serializable value); + + /******************************************************************************* + ** Getter for permissionRules + *******************************************************************************/ + QPermissionRules getPermissionRules(); + + + /******************************************************************************* + ** Setter for permissionRules + *******************************************************************************/ + void setPermissionRules(QPermissionRules permissionRules); + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java index b8c74cab..9f63aabb 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldMetaData.java @@ -34,6 +34,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.data.QField; import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.security.FieldSecurityLock; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; @@ -51,6 +52,8 @@ public class QFieldMetaData implements Cloneable private boolean isRequired = false; private boolean isEditable = true; + private FieldSecurityLock fieldSecurityLock; + /////////////////////////////////////////////////////////////////////////////////// // if we need "only edit on insert" or "only edit on update" in the future, // // propose doing that in a secondary field, e.g., "onlyEditableOn=insert|update" // @@ -689,4 +692,35 @@ public class QFieldMetaData implements Cloneable return (this); } + + /******************************************************************************* + ** Getter for fieldSecurityLock + *******************************************************************************/ + public FieldSecurityLock getFieldSecurityLock() + { + return (this.fieldSecurityLock); + } + + + + /******************************************************************************* + ** Setter for fieldSecurityLock + *******************************************************************************/ + public void setFieldSecurityLock(FieldSecurityLock fieldSecurityLock) + { + this.fieldSecurityLock = fieldSecurityLock; + } + + + + /******************************************************************************* + ** Fluent setter for fieldSecurityLock + *******************************************************************************/ + public QFieldMetaData withFieldSecurityLock(FieldSecurityLock fieldSecurityLock) + { + this.fieldSecurityLock = fieldSecurityLock; + return (this); + } + + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendAppMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendAppMetaData.java index 9471c95c..3bf1f5a8 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendAppMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendAppMetaData.java @@ -28,6 +28,7 @@ import java.util.List; import java.util.Map; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; @@ -55,7 +56,7 @@ public class QFrontendAppMetaData /******************************************************************************* ** *******************************************************************************/ - public QFrontendAppMetaData(QAppMetaData appMetaData) + public QFrontendAppMetaData(QAppMetaData appMetaData, MetaDataOutput metaDataOutput) { this.name = appMetaData.getName(); this.label = appMetaData.getLabel(); @@ -65,14 +66,31 @@ public class QFrontendAppMetaData this.iconName = appMetaData.getIcon().getName(); } - if(CollectionUtils.nullSafeHasContents(appMetaData.getWidgets())) + List filteredWidgets = CollectionUtils.nonNullList(appMetaData.getWidgets()).stream().filter(n -> metaDataOutput.getWidgets().containsKey(n)).toList(); + if(CollectionUtils.nullSafeHasContents(filteredWidgets)) { - this.widgets = appMetaData.getWidgets(); + this.widgets = filteredWidgets; } - if(CollectionUtils.nullSafeHasContents(appMetaData.getSections())) + List filteredSections = new ArrayList<>(); + for(QAppSection section : CollectionUtils.nonNullList(appMetaData.getSections())) { - this.sections = appMetaData.getSections(); + List filteredTables = CollectionUtils.nonNullList(section.getTables()).stream().filter(n -> metaDataOutput.getTables().containsKey(n)).toList(); + List filteredProcesses = CollectionUtils.nonNullList(section.getProcesses()).stream().filter(n -> metaDataOutput.getProcesses().containsKey(n)).toList(); + List filteredReports = CollectionUtils.nonNullList(section.getReports()).stream().filter(n -> metaDataOutput.getReports().containsKey(n)).toList(); + if(!filteredTables.isEmpty() || !filteredProcesses.isEmpty() || !filteredReports.isEmpty()) + { + QAppSection clonedSection = section.clone(); + clonedSection.setTables(filteredTables); + clonedSection.setProcesses(filteredProcesses); + clonedSection.setReports(filteredReports); + filteredSections.add(clonedSection); + } + } + + if(CollectionUtils.nullSafeHasContents(filteredSections)) + { + this.sections = filteredSections; } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendProcessMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendProcessMetaData.java index 182d1b9b..ba2bfa60 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendProcessMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendProcessMetaData.java @@ -27,6 +27,8 @@ import java.util.List; import java.util.stream.Collectors; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper; +import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; import com.kingsrook.qqq.backend.core.model.metadata.processes.QFrontendStepMetaData; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; @@ -49,6 +51,8 @@ public class QFrontendProcessMetaData private List frontendSteps; + private boolean hasPermission; + ////////////////////////////////////////////////////////////////////////////////// // do not add setters. take values from the source-object in the constructor!! // ////////////////////////////////////////////////////////////////////////////////// @@ -58,7 +62,7 @@ public class QFrontendProcessMetaData /******************************************************************************* ** *******************************************************************************/ - public QFrontendProcessMetaData(QProcessMetaData processMetaData, boolean includeSteps) + public QFrontendProcessMetaData(AbstractActionInput actionInput, QProcessMetaData processMetaData, boolean includeSteps) { this.name = processMetaData.getName(); this.label = processMetaData.getLabel(); @@ -84,6 +88,8 @@ public class QFrontendProcessMetaData { this.iconName = processMetaData.getIcon().getName(); } + + hasPermission = PermissionsHelper.hasProcessPermission(actionInput, name); } @@ -163,4 +169,15 @@ public class QFrontendProcessMetaData return iconName; } + + + /******************************************************************************* + ** Getter for hasPermission + ** + *******************************************************************************/ + public boolean getHasPermission() + { + return hasPermission; + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendReportMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendReportMetaData.java index dc0486a3..e2bf24d0 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendReportMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendReportMetaData.java @@ -24,6 +24,8 @@ package com.kingsrook.qqq.backend.core.model.metadata.frontend; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper; +import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; @@ -41,6 +43,8 @@ public class QFrontendReportMetaData private String iconName; + private boolean hasPermission; + ////////////////////////////////////////////////////////////////////////////////// // do not add setters. take values from the source-object in the constructor!! // ////////////////////////////////////////////////////////////////////////////////// @@ -50,7 +54,7 @@ public class QFrontendReportMetaData /******************************************************************************* ** *******************************************************************************/ - public QFrontendReportMetaData(QReportMetaData reportMetaData, boolean includeSteps) + public QFrontendReportMetaData(AbstractActionInput actionInput, QReportMetaData reportMetaData, boolean includeSteps) { this.name = reportMetaData.getName(); this.label = reportMetaData.getLabel(); @@ -60,6 +64,8 @@ public class QFrontendReportMetaData { this.iconName = reportMetaData.getIcon().getName(); } + + hasPermission = PermissionsHelper.hasReportPermission(actionInput, name); } @@ -106,4 +112,15 @@ public class QFrontendReportMetaData return iconName; } + + + /******************************************************************************* + ** Getter for hasPermission + ** + *******************************************************************************/ + public boolean getHasPermission() + { + return hasPermission; + } + } 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 56402053..c53cc933 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 @@ -30,6 +30,9 @@ import java.util.Set; import java.util.stream.Collectors; import com.fasterxml.jackson.annotation.JsonInclude; 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.model.actions.AbstractActionInput; import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability; @@ -55,10 +58,13 @@ public class QFrontendTableMetaData private Map fields; private List sections; - private List widgets; - private Set capabilities; + private boolean readPermission; + private boolean insertPermission; + private boolean editPermission; + private boolean deletePermission; + ////////////////////////////////////////////////////////////////////////////////// // do not add setters. take values from the source-object in the constructor!! // ////////////////////////////////////////////////////////////////////////////////// @@ -68,7 +74,7 @@ public class QFrontendTableMetaData /******************************************************************************* ** *******************************************************************************/ - public QFrontendTableMetaData(QBackendMetaData backendForTable, QTableMetaData tableMetaData, boolean includeFields) + public QFrontendTableMetaData(AbstractActionInput actionInput, QBackendMetaData backendForTable, QTableMetaData tableMetaData, boolean includeFields) { this.name = tableMetaData.getName(); this.label = tableMetaData.getLabel(); @@ -92,6 +98,11 @@ 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); } @@ -196,17 +207,6 @@ public class QFrontendTableMetaData - /******************************************************************************* - ** Getter for widgets - ** - *******************************************************************************/ - public List getWidgets() - { - return widgets; - } - - - /******************************************************************************* ** Getter for capabilities ** @@ -216,4 +216,47 @@ public class QFrontendTableMetaData return capabilities; } + + + /******************************************************************************* + ** Getter for readPermission + ** + *******************************************************************************/ + public boolean getReadPermission() + { + return readPermission; + } + + + + /******************************************************************************* + ** Getter for insertPermission + ** + *******************************************************************************/ + public boolean getInsertPermission() + { + return insertPermission; + } + + + + /******************************************************************************* + ** Getter for editPermission + ** + *******************************************************************************/ + public boolean getEditPermission() + { + return editPermission; + } + + + + /******************************************************************************* + ** Getter for deletePermission + ** + *******************************************************************************/ + public boolean getDeletePermission() + { + return deletePermission; + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendWidgetMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendWidgetMetaData.java index 0374a25e..47132a26 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendWidgetMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/frontend/QFrontendWidgetMetaData.java @@ -24,6 +24,8 @@ package com.kingsrook.qqq.backend.core.model.metadata.frontend; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper; +import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaDataInterface; @@ -42,6 +44,8 @@ public class QFrontendWidgetMetaData private final Integer gridColumns; private final boolean isCard; + private boolean hasPermission; + ////////////////////////////////////////////////////////////////////////////////// // do not add setters. take values from the source-object in the constructor!! // ////////////////////////////////////////////////////////////////////////////////// @@ -51,7 +55,7 @@ public class QFrontendWidgetMetaData /******************************************************************************* ** *******************************************************************************/ - public QFrontendWidgetMetaData(QWidgetMetaDataInterface widgetMetaData) + public QFrontendWidgetMetaData(AbstractActionInput actionInput, QWidgetMetaDataInterface widgetMetaData) { this.name = widgetMetaData.getName(); this.label = widgetMetaData.getLabel(); @@ -59,6 +63,8 @@ public class QFrontendWidgetMetaData this.icon = widgetMetaData.getIcon(); this.gridColumns = widgetMetaData.getGridColumns(); this.isCard = widgetMetaData.getIsCard(); + + hasPermission = PermissionsHelper.hasWidgetPermission(actionInput, name); } @@ -127,4 +133,15 @@ public class QFrontendWidgetMetaData return icon; } + + + /******************************************************************************* + ** Getter for hasPermission + ** + *******************************************************************************/ + public boolean getHasPermission() + { + return hasPermission; + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppMetaData.java index 542d2a79..fb1fb4e2 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppMetaData.java @@ -24,6 +24,8 @@ package com.kingsrook.qqq.backend.core.model.metadata.layout; import java.util.ArrayList; import java.util.List; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules; import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; @@ -34,11 +36,13 @@ import com.kingsrook.qqq.backend.core.utils.CollectionUtils; ** MetaData definition of an App - an entity that organizes tables & processes ** and can be arranged hierarchically (e.g, apps can contain other apps). *******************************************************************************/ -public class QAppMetaData implements QAppChildMetaData +public class QAppMetaData implements QAppChildMetaData, MetaDataWithPermissionRules { private String name; private String label; + private QPermissionRules permissionRules; + private List children; private String parentAppName; @@ -377,4 +381,35 @@ public class QAppMetaData implements QAppChildMetaData return (this); } + + + /******************************************************************************* + ** Getter for permissionRules + *******************************************************************************/ + public QPermissionRules getPermissionRules() + { + return (this.permissionRules); + } + + + + /******************************************************************************* + ** Setter for permissionRules + *******************************************************************************/ + public void setPermissionRules(QPermissionRules permissionRules) + { + this.permissionRules = permissionRules; + } + + + + /******************************************************************************* + ** Fluent setter for permissionRules + *******************************************************************************/ + public QAppMetaData withPermissionRules(QPermissionRules permissionRules) + { + this.permissionRules = permissionRules; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppSection.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppSection.java index b1d0c5b5..ed311341 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppSection.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/layout/QAppSection.java @@ -29,7 +29,7 @@ import java.util.List; /******************************************************************************* ** A section of apps/tables/processes - a logical grouping. *******************************************************************************/ -public class QAppSection +public class QAppSection implements Cloneable { private String name; private String label; @@ -65,6 +65,25 @@ public class QAppSection + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public QAppSection clone() + { + try + { + QAppSection clone = (QAppSection) super.clone(); + return clone; + } + catch(CloneNotSupportedException e) + { + throw new AssertionError(); + } + } + + + /******************************************************************************* ** Getter for name ** @@ -314,5 +333,4 @@ public class QAppSection this.icon = icon; return (this); } - } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/permissions/DenyBehavior.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/permissions/DenyBehavior.java new file mode 100644 index 00000000..a1849e6c --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/permissions/DenyBehavior.java @@ -0,0 +1,32 @@ +/* + * 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.permissions; + + +/******************************************************************************* + ** + *******************************************************************************/ +public enum DenyBehavior +{ + DISABLED, + HIDDEN +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/permissions/MetaDataWithName.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/permissions/MetaDataWithName.java new file mode 100644 index 00000000..f34c2439 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/permissions/MetaDataWithName.java @@ -0,0 +1,54 @@ +/* + * 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.permissions; + + +/******************************************************************************* + ** + *******************************************************************************/ +public interface MetaDataWithName +{ + + /******************************************************************************* + ** Getter for name + *******************************************************************************/ + String getName(); + + + /******************************************************************************* + ** Setter for name + *******************************************************************************/ + void setName(String name); + + + /******************************************************************************* + ** Getter for label + *******************************************************************************/ + String getLabel(); + + + /******************************************************************************* + ** Setter for label + *******************************************************************************/ + void setLabel(String label); + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/permissions/MetaDataWithPermissionRules.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/permissions/MetaDataWithPermissionRules.java new file mode 100644 index 00000000..9e0b2a24 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/permissions/MetaDataWithPermissionRules.java @@ -0,0 +1,41 @@ +/* + * 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.permissions; + + +/******************************************************************************* + ** + *******************************************************************************/ +public interface MetaDataWithPermissionRules extends MetaDataWithName +{ + + /******************************************************************************* + ** Getter for permissionRules + *******************************************************************************/ + QPermissionRules getPermissionRules(); + + /******************************************************************************* + ** Setter for permissionRules + *******************************************************************************/ + void setPermissionRules(QPermissionRules permissionRules); + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/permissions/PermissionLevel.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/permissions/PermissionLevel.java new file mode 100644 index 00000000..5ab890ca --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/permissions/PermissionLevel.java @@ -0,0 +1,34 @@ +/* + * 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.permissions; + + +/******************************************************************************* + ** + *******************************************************************************/ +public enum PermissionLevel +{ + NOT_PROTECTED, + HAS_ACCESS_PERMISSION, + READ_WRITE_PERMISSIONS, + READ_INSERT_EDIT_DELETE_PERMISSIONS, +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/permissions/QPermissionRules.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/permissions/QPermissionRules.java new file mode 100644 index 00000000..07dc0d55 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/permissions/QPermissionRules.java @@ -0,0 +1,194 @@ +/* + * 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.permissions; + + +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class QPermissionRules implements Cloneable +{ + private PermissionLevel level; + private DenyBehavior denyBehavior; + private String permissionBaseName; + + private QCodeReference customPermissionChecker; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static QPermissionRules defaultInstance() + { + return new QPermissionRules() + .withLevel(PermissionLevel.NOT_PROTECTED) + .withDenyBehavior(DenyBehavior.HIDDEN); + } + + + + /******************************************************************************* + ** Getter for level + *******************************************************************************/ + public PermissionLevel getLevel() + { + return (this.level); + } + + + + /******************************************************************************* + ** Setter for level + *******************************************************************************/ + public void setLevel(PermissionLevel level) + { + this.level = level; + } + + + + /******************************************************************************* + ** Fluent setter for level + *******************************************************************************/ + public QPermissionRules withLevel(PermissionLevel level) + { + this.level = level; + return (this); + } + + + + /******************************************************************************* + ** Getter for denyBehavior + *******************************************************************************/ + public DenyBehavior getDenyBehavior() + { + return (this.denyBehavior); + } + + + + /******************************************************************************* + ** Setter for denyBehavior + *******************************************************************************/ + public void setDenyBehavior(DenyBehavior denyBehavior) + { + this.denyBehavior = denyBehavior; + } + + + + /******************************************************************************* + ** Fluent setter for denyBehavior + *******************************************************************************/ + public QPermissionRules withDenyBehavior(DenyBehavior denyBehavior) + { + this.denyBehavior = denyBehavior; + return (this); + } + + + + /******************************************************************************* + ** Getter for permissionBaseName + *******************************************************************************/ + public String getPermissionBaseName() + { + return (this.permissionBaseName); + } + + + + /******************************************************************************* + ** Setter for permissionBaseName + *******************************************************************************/ + public void setPermissionBaseName(String permissionBaseName) + { + this.permissionBaseName = permissionBaseName; + } + + + + /******************************************************************************* + ** Fluent setter for permissionBaseName + *******************************************************************************/ + public QPermissionRules withPermissionBaseName(String permissionBaseName) + { + this.permissionBaseName = permissionBaseName; + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public QPermissionRules clone() + { + try + { + QPermissionRules clone = (QPermissionRules) super.clone(); + return clone; + } + catch(CloneNotSupportedException e) + { + throw new AssertionError(); + } + } + + + + /******************************************************************************* + ** Getter for customPermissionChecker + *******************************************************************************/ + public QCodeReference getCustomPermissionChecker() + { + return (this.customPermissionChecker); + } + + + + /******************************************************************************* + ** Setter for customPermissionChecker + *******************************************************************************/ + public void setCustomPermissionChecker(QCodeReference customPermissionChecker) + { + this.customPermissionChecker = customPermissionChecker; + } + + + + /******************************************************************************* + ** Fluent setter for customPermissionChecker + *******************************************************************************/ + public QPermissionRules withCustomPermissionChecker(QCodeReference customPermissionChecker) + { + this.customPermissionChecker = customPermissionChecker; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java index cc54e526..e2a75aea 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/processes/QProcessMetaData.java @@ -32,6 +32,8 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules; import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.QScheduleMetaData; import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullConfiguration; @@ -40,13 +42,14 @@ import com.kingsrook.qqq.backend.core.processes.implementations.basepull.Basepul ** Meta-Data to define a process in a QQQ instance. ** *******************************************************************************/ -public class QProcessMetaData implements QAppChildMetaData +public class QProcessMetaData implements QAppChildMetaData, MetaDataWithPermissionRules { private String name; private String label; private String tableName; private boolean isHidden = false; private BasepullConfiguration basepullConfiguration; + private QPermissionRules permissionRules; private List stepList; // these are the steps that are ran, by-default, in the order they are ran in private Map steps; // this is the full map of possible steps @@ -509,4 +512,35 @@ public class QProcessMetaData implements QAppChildMetaData return (this); } + + + /******************************************************************************* + ** Getter for permissionRules + *******************************************************************************/ + public QPermissionRules getPermissionRules() + { + return (this.permissionRules); + } + + + + /******************************************************************************* + ** Setter for permissionRules + *******************************************************************************/ + public void setPermissionRules(QPermissionRules permissionRules) + { + this.permissionRules = permissionRules; + } + + + + /******************************************************************************* + ** Fluent setter for permissionRules + *******************************************************************************/ + public QProcessMetaData withPermissionRules(QPermissionRules permissionRules) + { + this.permissionRules = permissionRules; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportMetaData.java index 772a0690..6d855cb5 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/reporting/QReportMetaData.java @@ -27,17 +27,21 @@ import java.util.List; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; /******************************************************************************* ** Meta-data definition of a report generated by QQQ *******************************************************************************/ -public class QReportMetaData implements QAppChildMetaData +public class QReportMetaData implements QAppChildMetaData, MetaDataWithPermissionRules { private String name; private String label; + private QPermissionRules permissionRules; + private String processName; private List inputFields; @@ -372,4 +376,35 @@ public class QReportMetaData implements QAppChildMetaData return (null); } + + + /******************************************************************************* + ** Getter for permissionRules + *******************************************************************************/ + public QPermissionRules getPermissionRules() + { + return (this.permissionRules); + } + + + + /******************************************************************************* + ** Setter for permissionRules + *******************************************************************************/ + public void setPermissionRules(QPermissionRules permissionRules) + { + this.permissionRules = permissionRules; + } + + + + /******************************************************************************* + ** Fluent setter for permissionRules + *******************************************************************************/ + public QReportMetaData withPermissionRules(QPermissionRules permissionRules) + { + this.permissionRules = permissionRules; + return (this); + } + } 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 new file mode 100644 index 00000000..298f6ad7 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/FieldSecurityLock.java @@ -0,0 +1,154 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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; + + +/******************************************************************************* + ** Define, for a field, a lock that controls if users can or cannot see the field. + *******************************************************************************/ +public class FieldSecurityLock +{ + private String securityKeyType; + private Behavior defaultBehavior = Behavior.DENY; + private List overrideValues; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public FieldSecurityLock() + { + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public enum Behavior + { + ALLOW, + DENY + } + + + + /******************************************************************************* + ** Getter for securityKeyType + *******************************************************************************/ + public String getSecurityKeyType() + { + return (this.securityKeyType); + } + + + + /******************************************************************************* + ** Setter for securityKeyType + *******************************************************************************/ + public void setSecurityKeyType(String securityKeyType) + { + this.securityKeyType = securityKeyType; + } + + + + /******************************************************************************* + ** Fluent setter for securityKeyType + *******************************************************************************/ + public FieldSecurityLock withSecurityKeyType(String securityKeyType) + { + this.securityKeyType = securityKeyType; + return (this); + } + + + + + /******************************************************************************* + ** Getter for defaultBehavior + *******************************************************************************/ + public Behavior getDefaultBehavior() + { + return (this.defaultBehavior); + } + + + + /******************************************************************************* + ** Setter for defaultBehavior + *******************************************************************************/ + public void setDefaultBehavior(Behavior defaultBehavior) + { + this.defaultBehavior = defaultBehavior; + } + + + + /******************************************************************************* + ** Fluent setter for defaultBehavior + *******************************************************************************/ + public FieldSecurityLock withDefaultBehavior(Behavior defaultBehavior) + { + this.defaultBehavior = defaultBehavior; + return (this); + } + + + + /******************************************************************************* + ** Getter for overrideValues + *******************************************************************************/ + public List getOverrideValues() + { + return (this.overrideValues); + } + + + + /******************************************************************************* + ** Setter for overrideValues + *******************************************************************************/ + public void setOverrideValues(List overrideValues) + { + this.overrideValues = overrideValues; + } + + + + /******************************************************************************* + ** Fluent setter for overrideValues + *******************************************************************************/ + public FieldSecurityLock withOverrideValues(List overrideValues) + { + this.overrideValues = overrideValues; + return (this); + } + + +} 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 new file mode 100644 index 00000000..a1404c75 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/QSecurityKeyType.java @@ -0,0 +1,137 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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; + + +/******************************************************************************* + ** Define a type of security key (e.g., a field associated with values), that + ** can be used to control access to records and/or fields + *******************************************************************************/ +public class QSecurityKeyType +{ + private String name; + private String allAccessKeyName; + private String possibleValueSourceName; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QSecurityKeyType() + { + } + + + + /******************************************************************************* + ** Getter for name + *******************************************************************************/ + public String getName() + { + return (this.name); + } + + + + /******************************************************************************* + ** Setter for name + *******************************************************************************/ + public void setName(String name) + { + this.name = name; + } + + + + /******************************************************************************* + ** Fluent setter for name + *******************************************************************************/ + public QSecurityKeyType withName(String name) + { + this.name = name; + return (this); + } + + + + /******************************************************************************* + ** Getter for allAccessKeyName + *******************************************************************************/ + public String getAllAccessKeyName() + { + return (this.allAccessKeyName); + } + + + + /******************************************************************************* + ** Setter for allAccessKeyName + *******************************************************************************/ + public void setAllAccessKeyName(String allAccessKeyName) + { + this.allAccessKeyName = allAccessKeyName; + } + + + + /******************************************************************************* + ** Fluent setter for allAccessKeyName + *******************************************************************************/ + public QSecurityKeyType withAllAccessKeyName(String allAccessKeyName) + { + this.allAccessKeyName = allAccessKeyName; + return (this); + } + + + + /******************************************************************************* + ** Getter for possibleValueSourceName + *******************************************************************************/ + public String getPossibleValueSourceName() + { + return (this.possibleValueSourceName); + } + + + + /******************************************************************************* + ** Setter for possibleValueSourceName + *******************************************************************************/ + public void setPossibleValueSourceName(String possibleValueSourceName) + { + this.possibleValueSourceName = possibleValueSourceName; + } + + + + /******************************************************************************* + ** Fluent setter for possibleValueSourceName + *******************************************************************************/ + public QSecurityKeyType withPossibleValueSourceName(String possibleValueSourceName) + { + this.possibleValueSourceName = possibleValueSourceName; + 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 new file mode 100644 index 00000000..cc08f098 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/security/RecordSecurityLock.java @@ -0,0 +1,185 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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; + + +/******************************************************************************* + ** Define (for a table) a lock that applies to records in the table - e.g., + ** a key type, and a field that has values for that key. + * + *******************************************************************************/ +public class RecordSecurityLock +{ + private String securityKeyType; + private String fieldName; + private List joinChain; // todo - add validation in validator!! + private NullValueBehavior nullValueBehavior = NullValueBehavior.DENY; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public RecordSecurityLock() + { + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public enum NullValueBehavior + { + ALLOW, + DENY + } + + + + /******************************************************************************* + ** Getter for securityKeyType + *******************************************************************************/ + public String getSecurityKeyType() + { + return (this.securityKeyType); + } + + + + /******************************************************************************* + ** Setter for securityKeyType + *******************************************************************************/ + public void setSecurityKeyType(String securityKeyType) + { + this.securityKeyType = securityKeyType; + } + + + + /******************************************************************************* + ** Fluent setter for securityKeyType + *******************************************************************************/ + public RecordSecurityLock withSecurityKeyType(String securityKeyType) + { + this.securityKeyType = securityKeyType; + return (this); + } + + + + /******************************************************************************* + ** Getter for fieldName + *******************************************************************************/ + public String getFieldName() + { + return (this.fieldName); + } + + + + /******************************************************************************* + ** Setter for fieldName + *******************************************************************************/ + public void setFieldName(String fieldName) + { + this.fieldName = fieldName; + } + + + + /******************************************************************************* + ** Fluent setter for fieldName + *******************************************************************************/ + public RecordSecurityLock withFieldName(String fieldName) + { + this.fieldName = fieldName; + return (this); + } + + + + /******************************************************************************* + ** Getter for nullValueBehavior + *******************************************************************************/ + public NullValueBehavior getNullValueBehavior() + { + return (this.nullValueBehavior); + } + + + + /******************************************************************************* + ** Setter for nullValueBehavior + *******************************************************************************/ + public void setNullValueBehavior(NullValueBehavior nullValueBehavior) + { + this.nullValueBehavior = nullValueBehavior; + } + + + + /******************************************************************************* + ** Fluent setter for nullValueBehavior + *******************************************************************************/ + public RecordSecurityLock withNullValueBehavior(NullValueBehavior nullValueBehavior) + { + this.nullValueBehavior = nullValueBehavior; + return (this); + } + + + /******************************************************************************* + ** Getter for joinChain + *******************************************************************************/ + public List getJoinChain() + { + return (this.joinChain); + } + + + + /******************************************************************************* + ** Setter for joinChain + *******************************************************************************/ + public void setJoinChain(List joinChain) + { + this.joinChain = joinChain; + } + + + + /******************************************************************************* + ** Fluent setter for joinChain + *******************************************************************************/ + public RecordSecurityLock withJoinChain(List joinChain) + { + this.joinChain = joinChain; + return (this); + } + + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java index d931f931..a3aac0c9 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java @@ -41,6 +41,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppChildMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.MetaDataWithPermissionRules; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules; +import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.QTableAutomationDetails; import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheOf; @@ -49,7 +52,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.cache.CacheOf; ** Meta-Data to define a table in a QQQ instance. ** *******************************************************************************/ -public class QTableMetaData implements QAppChildMetaData, Serializable +public class QTableMetaData implements QAppChildMetaData, Serializable, MetaDataWithPermissionRules { private String name; private String label; @@ -69,6 +72,9 @@ public class QTableMetaData implements QAppChildMetaData, Serializable private Map fields; private List uniqueKeys; + private List recordSecurityLocks; + private QPermissionRules permissionRules; + private QTableBackendDetails backendDetails; private QTableAutomationDetails automationDetails; @@ -1041,7 +1047,12 @@ public class QTableMetaData implements QAppChildMetaData, Serializable /******************************************************************************* + ** Test if a capability is enabled - checking both at the table level and + ** at the backend level. ** + ** If backend says disabled, then disable - UNLESS - the table says enable. + ** If backend either doesn't specify, or says enable, return what the table says (if it says). + ** else, return the default (of enabled). *******************************************************************************/ public boolean isCapabilityEnabled(QBackendMetaData backend, Capability capability) { @@ -1078,4 +1089,82 @@ public class QTableMetaData implements QAppChildMetaData, Serializable return (hasCapability); } + + + + /******************************************************************************* + ** Getter for recordSecurityLocks + *******************************************************************************/ + public List getRecordSecurityLocks() + { + return (this.recordSecurityLocks); + } + + + + /******************************************************************************* + ** Setter for recordSecurityLocks + *******************************************************************************/ + public void setRecordSecurityLocks(List recordSecurityLocks) + { + this.recordSecurityLocks = recordSecurityLocks; + } + + + + /******************************************************************************* + ** Fluent setter for recordSecurityLocks + *******************************************************************************/ + public QTableMetaData withRecordSecurityLocks(List recordSecurityLocks) + { + this.recordSecurityLocks = recordSecurityLocks; + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for recordSecurityLocks + *******************************************************************************/ + public QTableMetaData withRecordSecurityLock(RecordSecurityLock recordSecurityLock) + { + if(this.recordSecurityLocks == null) + { + this.recordSecurityLocks = new ArrayList<>(); + } + this.recordSecurityLocks.add(recordSecurityLock); + return (this); + } + + + + /******************************************************************************* + ** Getter for permissionRules + *******************************************************************************/ + public QPermissionRules getPermissionRules() + { + return (this.permissionRules); + } + + + + /******************************************************************************* + ** Setter for permissionRules + *******************************************************************************/ + public void setPermissionRules(QPermissionRules permissionRules) + { + this.permissionRules = permissionRules; + } + + + + /******************************************************************************* + ** Fluent setter for permissionRules + *******************************************************************************/ + public QTableMetaData withPermissionRules(QPermissionRules permissionRules) + { + this.permissionRules = permissionRules; + return (this); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/session/QSession.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/session/QSession.java index 5755b568..f362adb7 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/session/QSession.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/session/QSession.java @@ -23,9 +23,19 @@ package com.kingsrook.qqq.backend.core.model.session; import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.Set; import java.util.UUID; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; +import com.kingsrook.qqq.backend.core.utils.collections.MutableMap; /******************************************************************************* @@ -37,6 +47,9 @@ public class QSession implements Serializable private QUser user; private String uuid; + private Map> securityKeyValues; + private Set permissions; + // implementation-specific custom values private Map values; @@ -179,4 +192,264 @@ public class QSession implements Serializable { this.uuid = uuid; } + + + + /******************************************************************************* + ** Getter for securityKeyValues + *******************************************************************************/ + public Map> getSecurityKeyValues() + { + return (this.securityKeyValues); + } + + + + /******************************************************************************* + ** Getter for securityKeyValues - the list under a given key - never null. + *******************************************************************************/ + public List getSecurityKeyValues(String keyName) + { + if(securityKeyValues == null) + { + return (new ArrayList<>()); + } + + return (Objects.requireNonNullElseGet(securityKeyValues.get(keyName), ArrayList::new)); + } + + + + /******************************************************************************* + ** Getter for securityKeyValues - the list under a given key - as the expected tye - never null. + *******************************************************************************/ + public List getSecurityKeyValues(String keyName, QFieldType type) + { + if(securityKeyValues == null) + { + return (new ArrayList<>()); + } + + List rawValues = securityKeyValues.get(keyName); + if(rawValues == null) + { + return (new ArrayList<>()); + } + + List valuesAsType = new ArrayList<>(); + for(Serializable rawValue : rawValues) + { + valuesAsType.add(ValueUtils.getValueAsFieldType(type, rawValue)); + } + return (valuesAsType); + } + + + + /******************************************************************************* + ** Test if this session has a given value for a given key + *******************************************************************************/ + public boolean hasSecurityKeyValue(String keyName, Serializable value) + { + if(securityKeyValues == null) + { + return (false); + } + + if(!securityKeyValues.containsKey(keyName)) + { + return (false); + } + + List values = securityKeyValues.get(keyName); + return (values != null && values.contains(value)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public boolean hasSecurityKeyValue(String keyName, Serializable value, QFieldType fieldType) + { + if(securityKeyValues == null) + { + return (false); + } + + if(!securityKeyValues.containsKey(keyName)) + { + return (false); + } + + List values = securityKeyValues.get(keyName); + Serializable valueAsType = ValueUtils.getValueAsFieldType(fieldType, value); + for(Serializable keyValue : values) + { + Serializable keyValueAsType = ValueUtils.getValueAsFieldType(fieldType, keyValue); + if(keyValueAsType.equals(valueAsType)) + { + return (true); + } + } + + return (false); + } + + + + /******************************************************************************* + ** Setter for securityKeyValues + *******************************************************************************/ + public void setSecurityKeyValues(Map> securityKeyValues) + { + this.securityKeyValues = new MutableMap<>(securityKeyValues); + } + + + + /******************************************************************************* + ** Fluent setter for securityKeyValues - replaces the map. + *******************************************************************************/ + public QSession withSecurityKeyValues(Map> securityKeyValues) + { + this.securityKeyValues = new MutableMap<>(securityKeyValues); + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for securityKeyValues - add a list of values for 1 key + *******************************************************************************/ + public QSession withSecurityKeyValues(String keyName, List values) + { + if(values == null) + { + return (this); + } + + if(securityKeyValues == null) + { + securityKeyValues = new HashMap<>(); + } + + securityKeyValues.computeIfAbsent(keyName, (k) -> new ArrayList<>()); + + try + { + securityKeyValues.get(keyName).addAll(values); + } + catch(UnsupportedOperationException uoe) + { + securityKeyValues.put(keyName, new ArrayList<>(securityKeyValues.get(keyName))); + securityKeyValues.get(keyName).addAll(values); + } + + return (this); + } + + + + /******************************************************************************* + ** Fluent setter for securityKeyValues - add 1 value for 1 key. + *******************************************************************************/ + public QSession withSecurityKeyValue(String keyName, Serializable value) + { + return (withSecurityKeyValues(keyName, List.of(value))); + } + + + + /******************************************************************************* + ** Clear the map of security key values in the session. + *******************************************************************************/ + public void clearSecurityKeyValues() + { + if(securityKeyValues != null) + { + securityKeyValues.clear(); + } + } + + + + /******************************************************************************* + ** Setter for permissions + ** + *******************************************************************************/ + public void setPermissions(Set permissions) + { + this.permissions = permissions; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QSession withPermission(String permission) + { + if(this.permissions == null) + { + this.permissions = new HashSet<>(); + } + this.permissions.add(permission); + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QSession withPermissions(String... permissionNames) + { + if(this.permissions == null) + { + this.permissions = new HashSet<>(); + } + + Collections.addAll(this.permissions, permissionNames); + + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QSession withPermissions(Collection permissionNames) + { + if(this.permissions == null) + { + this.permissions = new HashSet<>(); + } + + this.permissions.addAll(permissionNames); + + return (this); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public boolean hasPermission(String permissionName) + { + return (permissions != null && permissions.contains(permissionName)); + } + + + + /******************************************************************************* + ** Getter for permissions + *******************************************************************************/ + public Set getPermissions() + { + return (this.permissions); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/session/QUser.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/session/QUser.java index 770dffe9..a6bf753b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/session/QUser.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/session/QUser.java @@ -73,4 +73,15 @@ public class QUser { this.fullName = fullName; } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public String toString() + { + return ("QUser{" + idReference + "," + fullName + "}"); + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleInterface.java index 75c1535f..d6dee291 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleInterface.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleInterface.java @@ -44,4 +44,14 @@ public interface QAuthenticationModuleInterface ** *******************************************************************************/ boolean isSessionValid(QInstance instance, QSession session); + + + /******************************************************************************* + ** + *******************************************************************************/ + default boolean usesSessionIdCookie() + { + return (false); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModule.java index 69ef1fe6..5560fb09 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModule.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModule.java @@ -28,8 +28,11 @@ import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Base64; +import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import com.auth0.client.auth.AuthAPI; import com.auth0.exception.Auth0Exception; import com.auth0.json.auth.TokenHolder; @@ -53,8 +56,10 @@ import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModu import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider; import com.kingsrook.qqq.backend.core.state.SimpleStateKey; import com.kingsrook.qqq.backend.core.state.StateProviderInterface; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.json.JSONArray; import org.json.JSONObject; @@ -70,11 +75,11 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface ///////////////////////////////////////////////////////////////////////////////////////////////////////////// public static final int ID_TOKEN_VALIDATION_INTERVAL_SECONDS = 1800; - public static final String AUTH0_ID_TOKEN_KEY = "sessionId"; - public static final String BASIC_AUTH_KEY = "basicAuthString"; + public static final String AUTH0_ACCESS_TOKEN_KEY = "sessionId"; + public static final String BASIC_AUTH_KEY = "basicAuthString"; - public static final String TOKEN_NOT_PROVIDED_ERROR = "Id Token was not provided"; - public static final String COULD_NOT_DECODE_ERROR = "Unable to decode id token"; + public static final String TOKEN_NOT_PROVIDED_ERROR = "Access Token was not provided"; + public static final String COULD_NOT_DECODE_ERROR = "Unable to decode access token"; public static final String EXPIRED_TOKEN_ERROR = "Token has expired"; public static final String INVALID_TOKEN_ERROR = "An invalid token was provided"; @@ -102,8 +107,8 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface // decode the credentials from the header auth // ///////////////////////////////////////////////// String base64Credentials = context.get(BASIC_AUTH_KEY).trim(); - String idToken = getIdTokenFromBase64BasicAuthCredentials(auth, base64Credentials); - context.put(AUTH0_ID_TOKEN_KEY, idToken); + String accessToken = getAccessTokenFromBase64BasicAuthCredentials(metaData, auth, base64Credentials); + context.put(AUTH0_ACCESS_TOKEN_KEY, accessToken); } catch(Auth0Exception e) { @@ -116,11 +121,11 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface } } - ////////////////////////////////////////////////// - // get the jwt id token from the context object // - ////////////////////////////////////////////////// - String idToken = context.get(AUTH0_ID_TOKEN_KEY); - if(idToken == null) + ////////////////////////////////////////////////////// + // get the jwt access token from the context object // + ////////////////////////////////////////////////////// + String accessToken = context.get(AUTH0_ACCESS_TOKEN_KEY); + if(accessToken == null) { LOG.warn(TOKEN_NOT_PROVIDED_ERROR); throw (new QAuthenticationException(TOKEN_NOT_PROVIDED_ERROR)); @@ -135,7 +140,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface // try to build session to see if still valid // // then call method to check more session validity // ///////////////////////////////////////////////////// - QSession qSession = buildQSessionFromToken(idToken); + QSession qSession = buildQSessionFromToken(accessToken, qInstance); if(isSessionValid(qInstance, qSession)) { return (qSession); @@ -145,7 +150,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface // if we make it here it means we have never validated this token or its been a long // // enough duration so we need to re-verify the token // /////////////////////////////////////////////////////////////////////////////////////// - qSession = revalidateToken(qInstance, idToken); + qSession = revalidateToken(qInstance, accessToken); //////////////////////////////////////////////////////////////////// // put now into state so we dont check until next interval passes // @@ -193,34 +198,34 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface /******************************************************************************* ** *******************************************************************************/ - private String getIdTokenFromBase64BasicAuthCredentials(AuthAPI auth, String base64Credentials) throws Auth0Exception + private String getAccessTokenFromBase64BasicAuthCredentials(Auth0AuthenticationMetaData metaData, AuthAPI auth, String base64Credentials) throws Auth0Exception { - //////////////////////////////////////////////////////////////////////////////// - // look for a fresh idToken in the state provider for this set of credentials // - //////////////////////////////////////////////////////////////////////////////// - SimpleStateKey idTokenStateKey = new SimpleStateKey<>(base64Credentials + ":idToken"); - SimpleStateKey timestampStateKey = new SimpleStateKey<>(base64Credentials + ":timestamp"); - StateProviderInterface stateProvider = getStateProvider(); - Optional cachedIdToken = stateProvider.get(String.class, idTokenStateKey); - Optional cachedTimestamp = stateProvider.get(Instant.class, timestampStateKey); - if(cachedIdToken.isPresent() && cachedTimestamp.isPresent()) + //////////////////////////////////////////////////////////////////////////////////// + // look for a fresh accessToken in the state provider for this set of credentials // + //////////////////////////////////////////////////////////////////////////////////// + SimpleStateKey accessTokenStateKey = new SimpleStateKey<>(base64Credentials + ":accessToken"); + SimpleStateKey timestampStateKey = new SimpleStateKey<>(base64Credentials + ":timestamp"); + StateProviderInterface stateProvider = getStateProvider(); + Optional cachedAccessToken = stateProvider.get(String.class, accessTokenStateKey); + Optional cachedTimestamp = stateProvider.get(Instant.class, timestampStateKey); + if(cachedAccessToken.isPresent() && cachedTimestamp.isPresent()) { if(cachedTimestamp.get().isAfter(Instant.now().minus(1, ChronoUnit.MINUTES))) { - return cachedIdToken.get(); + return cachedAccessToken.get(); } } - ////////////////////////////////////////////////////////////////////////////// - // not found in cache, make request to auth0 and cache the returned idToken // - ////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////////// + // not found in cache, make request to auth0 and cache the returned accessToken // + ////////////////////////////////////////////////////////////////////////////////// byte[] credDecoded = Base64.getDecoder().decode(base64Credentials); String credentials = new String(credDecoded, StandardCharsets.UTF_8); - String idToken = getIdTokenFromAuth0(auth, credentials); - stateProvider.put(idTokenStateKey, idToken); + String accessToken = getAccessTokenFromAuth0(metaData, auth, credentials); + stateProvider.put(accessTokenStateKey, accessToken); stateProvider.put(timestampStateKey, Instant.now()); - return (idToken); + return (accessToken); } @@ -228,16 +233,17 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface /******************************************************************************* ** *******************************************************************************/ - protected String getIdTokenFromAuth0(AuthAPI auth, String credentials) throws Auth0Exception + protected String getAccessTokenFromAuth0(Auth0AuthenticationMetaData metaData, AuthAPI auth, String credentials) throws Auth0Exception { ///////////////////////////////////// // call auth0 with a login request // ///////////////////////////////////// TokenHolder result = auth.login(credentials.split(":")[0], credentials.split(":")[1].toCharArray()) .setScope("openid email nickname") + .setAudience(metaData.getAudience()) .execute(); - return (result.getIdToken()); + return (result.getAccessToken()); } @@ -303,11 +309,11 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface ** makes request to check if a token is still valid and build new qSession if it is ** *******************************************************************************/ - private QSession revalidateToken(QInstance qInstance, String idToken) throws JwkException + private QSession revalidateToken(QInstance qInstance, String accessToken) throws JwkException { Auth0AuthenticationMetaData metaData = (Auth0AuthenticationMetaData) qInstance.getAuthentication(); - DecodedJWT jwt = JWT.decode(idToken); + DecodedJWT jwt = JWT.decode(accessToken); JwkProvider provider = new UrlJwkProvider(metaData.getBaseUrl()); Jwk jwk = provider.get(jwt.getKeyId()); Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) jwk.getPublicKey(), null); @@ -318,9 +324,9 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface /////////////////////////////////// // make call to verify the token // /////////////////////////////////// - verifier.verify(idToken); + verifier.verify(accessToken); - return (buildQSessionFromToken(idToken)); + return (buildQSessionFromToken(accessToken, qInstance)); } @@ -329,52 +335,181 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface ** extracts info from token creating a QSession ** *******************************************************************************/ - private QSession buildQSessionFromToken(String idToken) throws JwkException + private QSession buildQSessionFromToken(String accessToken, QInstance qInstance) throws JwkException { //////////////////////////////////// // decode and extract the payload // //////////////////////////////////// - DecodedJWT jwt = JWT.decode(idToken); + DecodedJWT jwt = JWT.decode(accessToken); Base64.Decoder decoder = Base64.getUrlDecoder(); String payloadString = new String(decoder.decode(jwt.getPayload())); JSONObject payload = new JSONObject(payloadString); + /////////////////////////////////////////////////////////////////////////////////////////////////////////// + // create user object. look for multiple possible keys in the jwt payload where the name & email may be // + /////////////////////////////////////////////////////////////////////////////////////////////////////////// QUser qUser = new QUser(); - if(payload.has("name")) + qUser.setFullName("Unknown"); + for(String key : List.of("name", "com.kingsrook.qqq.name")) { - qUser.setFullName(payload.getString("name")); - } - else - { - qUser.setFullName("Unknown"); - } - - if(payload.has("email")) - { - qUser.setIdReference(payload.getString("email")); - } - else - { - if(payload.has("sub")) + if(payload.has(key)) { - qUser.setIdReference(payload.getString("sub")); + qUser.setFullName(payload.getString(key)); + break; } } + for(String key : List.of("email", "com.kingsrook.qqq.email", "sub")) + { + if(payload.has(key)) + { + qUser.setIdReference(payload.getString(key)); + break; + } + } + + ///////////////////////////////////////////////////////// + // create session object - link to access token & user // + ///////////////////////////////////////////////////////// QSession qSession = new QSession(); - qSession.setIdReference(idToken); + qSession.setIdReference(accessToken); qSession.setUser(qUser); + ///////////////////////////////////////////////// + // set permissions in the session from the JWT // + ///////////////////////////////////////////////// + setPermissionsInSessionFromJwtPayload(payload, qSession); + + /////////////////////////////////////////////////// + // set security keys in the session from the JWT // + /////////////////////////////////////////////////// + setSecurityKeysInSessionFromJwtPayload(qInstance, payload, qSession); + return (qSession); } + /******************************************************************************* + ** + *******************************************************************************/ + static void setPermissionsInSessionFromJwtPayload(JSONObject payload, QSession qSession) + { + HashSet permissions = new HashSet<>(); + if(payload.has("permissions")) + { + try + { + JSONArray jwtPermissions = payload.getJSONArray("permissions"); + for(int i = 0; i < jwtPermissions.length(); i++) + { + permissions.add(jwtPermissions.optString(i)); + } + } + catch(Exception e) + { + LOG.error("Error getting permissions from JWT", e); + } + } + qSession.setPermissions(permissions); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + static void setSecurityKeysInSessionFromJwtPayload(QInstance qInstance, JSONObject payload, QSession qSession) + { + for(String payloadKey : List.of("com.kingsrook.qqq.app_metadata", "com.kingsrook.qqq.client_metadata")) + { + if(!payload.has(payloadKey)) + { + continue; + } + + try + { + JSONObject appMetadata = payload.getJSONObject(payloadKey); + Set allowedSecurityKeyNames = qInstance.getAllowedSecurityKeyNames(); + + ////////////////////////////////////////////////////////////////////////////////// + // for users, they will have a map of securityKeyValues (in their app_metadata) // + ////////////////////////////////////////////////////////////////////////////////// + JSONObject securityKeyValues = appMetadata.optJSONObject("securityKeyValues"); + if(securityKeyValues != null) + { + for(String keyName : securityKeyValues.keySet()) + { + setSecurityKeyValuesFromToken(allowedSecurityKeyNames, qSession, keyName, securityKeyValues, keyName); + } + } + else + { + ////////////////////////////////////////////////////////////////////////////////////////////////// + // for system-logins, there will be keys prefixed by securityKeyValues: (under client_metadata) // + ////////////////////////////////////////////////////////////////////////////////////////////////// + for(String appMetaDataKey : appMetadata.keySet()) + { + if(appMetaDataKey.startsWith("securityKeyValues:")) + { + String securityKeyName = appMetaDataKey.replace("securityKeyValues:", ""); + setSecurityKeyValuesFromToken(allowedSecurityKeyNames, qSession, securityKeyName, appMetadata, appMetaDataKey); + } + } + } + } + catch(Exception e) + { + LOG.error("Error getting securityKey values from app_metadata from JWT", e); + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void setSecurityKeyValuesFromToken(Set allowedSecurityKeyNames, QSession qSession, String securityKeyName, JSONObject securityKeyValues, String jsonKey) + { + if(!allowedSecurityKeyNames.contains(securityKeyName)) + { + QUser user = qSession.getUser(); + LOG.warn("Unrecognized security key name [" + securityKeyName + "] when creating session for user [" + user + "]. Allowed key names are: " + allowedSecurityKeyNames); + return; + } + + JSONArray valueArray = securityKeyValues.optJSONArray(jsonKey); + if(valueArray != null) + { + // todo - types? + for(int i = 0; i < valueArray.length(); i++) + { + Object optValue = valueArray.opt(i); + if(optValue != null) + { + qSession.withSecurityKeyValue(securityKeyName, ValueUtils.getValueAsString(optValue)); + } + } + } + else + { + String value = securityKeyValues.optString(jsonKey); + if(value != null) + { + qSession.withSecurityKeyValue(securityKeyName, value); + } + } + } + + + /******************************************************************************* ** Load an instance of the appropriate state provider ** *******************************************************************************/ - public static StateProviderInterface getStateProvider() + private static StateProviderInterface getStateProvider() { // TODO - read this from somewhere in meta data eh? return (InMemoryStateProvider.getInstance()); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModule.java index 14202fbc..b6e1207b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModule.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModule.java @@ -51,8 +51,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.authentication.TableBasedAu import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.model.session.QUser; import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleInterface; -import com.kingsrook.qqq.backend.core.state.AbstractStateKey; import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider; +import com.kingsrook.qqq.backend.core.state.SimpleStateKey; import com.kingsrook.qqq.backend.core.state.StateProviderInterface; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import org.apache.logging.log4j.LogManager; @@ -87,6 +87,17 @@ public class TableBasedAuthenticationModule implements QAuthenticationModuleInte + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public boolean usesSessionIdCookie() + { + return (true); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -203,7 +214,7 @@ public class TableBasedAuthenticationModule implements QAuthenticationModuleInte // put now into state so we dont check until next interval passes // /////////////////////////////////////////////////////////////////// StateProviderInterface spi = getStateProvider(); - SessionIdStateKey key = new SessionIdStateKey(qSession.getIdReference()); + SimpleStateKey key = new SimpleStateKey<>(qSession.getIdReference()); spi.put(key, Instant.now()); return (qSession); @@ -251,7 +262,7 @@ public class TableBasedAuthenticationModule implements QAuthenticationModuleInte } StateProviderInterface stateProvider = getStateProvider(); - SessionIdStateKey key = new SessionIdStateKey(session.getIdReference()); + SimpleStateKey key = new SimpleStateKey<>(session.getIdReference()); Optional lastTimeCheckedOptional = stateProvider.get(Instant.class, key); if(lastTimeCheckedOptional.isPresent()) { @@ -392,81 +403,6 @@ public class TableBasedAuthenticationModule implements QAuthenticationModuleInte - /******************************************************************************* - ** - *******************************************************************************/ - public static class SessionIdStateKey extends AbstractStateKey - { - private final String key; - - - - /******************************************************************************* - ** Constructor. - ** - *******************************************************************************/ - SessionIdStateKey(String key) - { - this.key = key; - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Override - public String toString() - { - return (this.key); - } - - - - /******************************************************************************* - ** Make the key give a unique string to identify itself. - * - *******************************************************************************/ - @Override - public String getUniqueIdentifier() - { - return (this.key); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Override - public boolean equals(Object o) - { - if(this == o) - { - return true; - } - if(o == null || getClass() != o.getClass()) - { - return false; - } - SessionIdStateKey that = (SessionIdStateKey) o; - return key.equals(that.key); - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - @Override - public int hashCode() - { - return key.hashCode(); - } - } - - - /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java index be82ec82..3b047c02 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryRecordStore.java @@ -146,6 +146,7 @@ public class MemoryRecordStore } BackendQueryFilterUtils.sortRecordList(input.getFilter(), records); + records = BackendQueryFilterUtils.applySkipAndLimit(input, records); return (records); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java index 949c5ff2..a6fd9422 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/utils/BackendQueryFilterUtils.java @@ -82,6 +82,7 @@ public class BackendQueryFilterUtils case IS_NOT_BLANK -> !testBlank(criterion, value); case CONTAINS -> testContains(criterion, fieldName, value); case NOT_CONTAINS -> !testContains(criterion, fieldName, value); + case IS_NULL_OR_IN -> testBlank(criterion, value) || testIn(criterion, value); case STARTS_WITH -> testStartsWith(criterion, fieldName, value); case NOT_STARTS_WITH -> !testStartsWith(criterion, fieldName, value); case ENDS_WITH -> testEndsWith(criterion, fieldName, value); @@ -438,13 +439,13 @@ public class BackendQueryFilterUtils { continue; } - else if(isGreaterThan(valueA, valueB) && orderBy.getIsAscending()) + else if(isGreaterThan(valueA, valueB)) { - return (-1); + return (orderBy.getIsAscending() ? -1 : 1); } - else + else // Less Than { - return (1); + return (orderBy.getIsAscending() ? 1 : -1); } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/collections/MutableList.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/collections/MutableList.java new file mode 100644 index 00000000..b43fd241 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/collections/MutableList.java @@ -0,0 +1,374 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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.utils.collections; + + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.function.Supplier; +import com.kingsrook.qqq.backend.core.utils.lambdas.VoidVoidMethod; + + +/******************************************************************************* + ** Object to wrap a List, so that in case a caller provided an immutable List, + ** you can safely perform mutating operations on it (in which case, it'll get + ** replaced by an actual mutable list). + *******************************************************************************/ +public class MutableList implements List +{ + private List sourceList; + private Class> mutableTypeIfNeeded; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public MutableList(List sourceList) + { + this(sourceList, (Class) ArrayList.class); + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public MutableList(List sourceList, Class> mutableTypeIfNeeded) + { + this.sourceList = sourceList; + this.mutableTypeIfNeeded = mutableTypeIfNeeded; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void replaceSourceListWithMutableCopy() + { + try + { + List replacementList = mutableTypeIfNeeded.getConstructor().newInstance(); + replacementList.addAll(sourceList); + sourceList = replacementList; + } + catch(Exception e) + { + throw (new IllegalStateException("The mutable type provided for this MutableList [" + mutableTypeIfNeeded.getName() + "] could not be instantiated.")); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private T doMutableOperationForValue(Supplier supplier) + { + try + { + return (supplier.get()); + } + catch(UnsupportedOperationException uoe) + { + replaceSourceListWithMutableCopy(); + return (supplier.get()); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void doMutableOperationForVoid(VoidVoidMethod method) + { + try + { + method.run(); + } + catch(UnsupportedOperationException uoe) + { + replaceSourceListWithMutableCopy(); + method.run(); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public int size() + { + return (sourceList.size()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public boolean isEmpty() + { + return (sourceList.isEmpty()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public boolean contains(Object o) + { + return (sourceList.contains(o)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Iterator iterator() + { + return (sourceList.iterator()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Object[] toArray() + { + return (sourceList.toArray()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public T1[] toArray(T1[] a) + { + return (sourceList.toArray(a)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public boolean add(T t) + { + return (doMutableOperationForValue(() -> sourceList.add(t))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public boolean remove(Object o) + { + return (doMutableOperationForValue(() -> sourceList.remove(o))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public boolean containsAll(Collection c) + { + return (sourceList.containsAll(c)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public boolean addAll(Collection c) + { + return (doMutableOperationForValue(() -> sourceList.addAll(c))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public boolean addAll(int index, Collection c) + { + return (doMutableOperationForValue(() -> sourceList.addAll(index, c))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public boolean removeAll(Collection c) + { + return (doMutableOperationForValue(() -> sourceList.removeAll(c))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public boolean retainAll(Collection c) + { + return (doMutableOperationForValue(() -> sourceList.retainAll(c))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void clear() + { + doMutableOperationForVoid(() -> sourceList.clear()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public T get(int index) + { + return (sourceList.get(index)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public T set(int index, T element) + { + return (doMutableOperationForValue(() -> sourceList.set(index, element))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void add(int index, T element) + { + doMutableOperationForVoid(() -> sourceList.add(index, element)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public T remove(int index) + { + return (doMutableOperationForValue(() -> sourceList.remove(index))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public int indexOf(Object o) + { + return (sourceList.indexOf(o)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public int lastIndexOf(Object o) + { + return (sourceList.lastIndexOf(o)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public ListIterator listIterator() + { + return (sourceList.listIterator()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public ListIterator listIterator(int index) + { + return (sourceList.listIterator(index)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public List subList(int fromIndex, int toIndex) + { + return (sourceList.subList(fromIndex, toIndex)); + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/collections/MutableMap.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/collections/MutableMap.java new file mode 100644 index 00000000..79102a07 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/collections/MutableMap.java @@ -0,0 +1,252 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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.utils.collections; + + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; +import com.kingsrook.qqq.backend.core.utils.lambdas.VoidVoidMethod; + + +/******************************************************************************* + ** Object to wrap a Map, so that in case a caller provided an immutable Map, + ** you can safely perform mutating operations on it (in which case, it'll get + ** replaced by an actual mutable Map). + *******************************************************************************/ +public class MutableMap implements Map +{ + private Map sourceMap; + private Class> mutableTypeIfNeeded; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public MutableMap(Map sourceMap) + { + this(sourceMap, (Class) HashMap.class); + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public MutableMap(Map sourceMap, Class> mutableTypeIfNeeded) + { + this.sourceMap = sourceMap; + this.mutableTypeIfNeeded = mutableTypeIfNeeded; + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void replaceSourceMapWithMutableCopy() + { + try + { + Map replacementMap = mutableTypeIfNeeded.getConstructor().newInstance(); + replacementMap.putAll(sourceMap); + sourceMap = replacementMap; + } + catch(Exception e) + { + throw (new IllegalStateException("The mutable type provided for this MutableMap [" + mutableTypeIfNeeded.getName() + "] could not be instantiated.")); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private T doMutableOperationForValue(Supplier supplier) + { + try + { + return (supplier.get()); + } + catch(UnsupportedOperationException uoe) + { + replaceSourceMapWithMutableCopy(); + return (supplier.get()); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void doMutableOperationForVoid(VoidVoidMethod method) + { + try + { + method.run(); + } + catch(UnsupportedOperationException uoe) + { + replaceSourceMapWithMutableCopy(); + method.run(); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public int size() + { + return (sourceMap.size()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public boolean isEmpty() + { + return (sourceMap.isEmpty()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public boolean containsKey(Object key) + { + return (sourceMap.containsKey(key)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public boolean containsValue(Object value) + { + return (sourceMap.containsValue(value)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public V get(Object key) + { + return (sourceMap.get(key)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public V put(K key, V value) + { + return (doMutableOperationForValue(() -> sourceMap.put(key, value))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public V remove(Object key) + { + return (doMutableOperationForValue(() -> sourceMap.remove(key))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void putAll(Map m) + { + doMutableOperationForVoid(() -> sourceMap.putAll(m)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void clear() + { + doMutableOperationForVoid(() -> sourceMap.clear()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Set keySet() + { + return (sourceMap.keySet()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Collection values() + { + return (sourceMap.values()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public Set> entrySet() + { + return (sourceMap.entrySet()); + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/lambdas/VoidVoidMethod.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/lambdas/VoidVoidMethod.java new file mode 100644 index 00000000..2d2d4800 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/utils/lambdas/VoidVoidMethod.java @@ -0,0 +1,37 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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.utils.lambdas; + + +/******************************************************************************* + ** + *******************************************************************************/ +@FunctionalInterface +public interface VoidVoidMethod +{ + + /******************************************************************************* + ** + *******************************************************************************/ + void run(); + +} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataActionTest.java index c22da8a5..53f571aa 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/metadata/MetaDataActionTest.java @@ -22,6 +22,7 @@ package com.kingsrook.qqq.backend.core.actions.metadata; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; @@ -30,13 +31,26 @@ import java.util.stream.Collectors; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataInput; import com.kingsrook.qqq.backend.core.model.actions.metadata.MetaDataOutput; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; import com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNode; +import com.kingsrook.qqq.backend.core.model.metadata.frontend.AppTreeNodeType; import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendAppMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendReportMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.frontend.QFrontendWidgetMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.DenyBehavior; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules; +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.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; /******************************************************************************* @@ -118,4 +132,217 @@ class MetaDataActionTest ///////////////////////////////////////////////////////////////////////////////// assertThat(greetingsAppUnderPeopleFromTree.get().getChildren()).isNotEmpty(); } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testHasAccessNoPermissionAndHide() throws QException + { + /////////////////////////////////////////////////////////////////////////////////////////// + // with 'hasAccess' set as the default instance rule, but no permissions in the session, // + // and the deny behavior as 'hide' we should have 0 of these // + /////////////////////////////////////////////////////////////////////////////////////////// + QInstance instance = TestUtils.defineInstance(); + instance.setDefaultPermissionRules(new QPermissionRules().withLevel(PermissionLevel.HAS_ACCESS_PERMISSION)); + MetaDataInput input = new MetaDataInput(instance); + input.setSession(new QSession()); + MetaDataOutput result = new MetaDataAction().execute(input); + + assertEquals(0, result.getTables().size()); + assertEquals(0, result.getProcesses().size()); + assertEquals(0, result.getReports().size()); + assertEquals(0, result.getWidgets().size()); + assertEquals(0, result.getApps().size()); + assertEquals(0, result.getAppTree().size()); + + //////////////////////////////////////////////////////////////////////////////////////////////////////// + // the only kinds of app meta data we should find are other apps - no tables, processes, reports, etc // + //////////////////////////////////////////////////////////////////////////////////////////////////////// + for(QFrontendAppMetaData appMetaData : result.getApps().values()) + { + assertThat(appMetaData.getClass()).isEqualTo(QFrontendAppMetaData.class); + for(AppTreeNode child : appMetaData.getChildren()) + { + assertEquals(AppTreeNodeType.APP, child.getType()); + } + } + + List toExplore = new ArrayList<>(result.getAppTree()); + while(!toExplore.isEmpty()) + { + AppTreeNode exploring = toExplore.remove(0); + if(exploring.getChildren() != null) + { + toExplore.addAll(exploring.getChildren()); + } + assertEquals(AppTreeNodeType.APP, exploring.getType()); + } + + // todo -- assert about sections in those apps not having stuff + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testHasAccessNoPermissionAndDisable() throws QException + { + ///////////////////////////////////////////////////////////////////////////////////////////////////// + // with 'hasAccess' set as the default instance rule, but no permissions in the session, // + // and the deny behavior as 'disable', we should have lots of things, but all with no permissions. // + ///////////////////////////////////////////////////////////////////////////////////////////////////// + QInstance instance = TestUtils.defineInstance(); + instance.setDefaultPermissionRules(new QPermissionRules().withLevel(PermissionLevel.HAS_ACCESS_PERMISSION).withDenyBehavior(DenyBehavior.DISABLED)); + MetaDataInput input = new MetaDataInput(instance); + input.setSession(new QSession()); + MetaDataOutput result = new MetaDataAction().execute(input); + + assertNotEquals(0, result.getTables().size()); + assertNotEquals(0, result.getProcesses().size()); + assertNotEquals(0, result.getReports().size()); + assertNotEquals(0, result.getWidgets().size()); + assertNotEquals(0, result.getApps().size()); + assertNotEquals(0, result.getAppTree().size()); + + assertTrue(result.getTables().values().stream().allMatch(t -> !t.getDeletePermission() && !t.getReadPermission() && !t.getInsertPermission() && !t.getEditPermission())); + assertTrue(result.getProcesses().values().stream().noneMatch(QFrontendProcessMetaData::getHasPermission)); + assertTrue(result.getReports().values().stream().noneMatch(QFrontendReportMetaData::getHasPermission)); + assertTrue(result.getWidgets().values().stream().noneMatch(QFrontendWidgetMetaData::getHasPermission)); + // todo ... apps... uh... + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testHasAccessSomePermissionsAndHide() throws QException + { + QInstance instance = TestUtils.defineInstance(); + instance.setDefaultPermissionRules(new QPermissionRules().withLevel(PermissionLevel.HAS_ACCESS_PERMISSION)); + MetaDataInput input = new MetaDataInput(instance); + input.setSession(new QSession().withPermissions( + "person.hasAccess", + "increaseBirthdate.hasAccess", + "runShapesPersonReport.hasAccess", + "shapesPersonReport.hasAccess", + "personJoinShapeReport.hasAccess", + "simplePersonReport.hasAccess", + "PersonsByCreateDateBarChart.hasAccess" + )); + MetaDataOutput result = new MetaDataAction().execute(input); + + ///////////////////////////////////////////////////////////////////////////////////////////////////////// + // with several permissions set, we should see some things, and they should have permissions turned on // + ///////////////////////////////////////////////////////////////////////////////////////////////////////// + assertEquals(Set.of("person"), result.getTables().keySet()); + assertEquals(Set.of("increaseBirthdate", "runShapesPersonReport"), result.getProcesses().keySet()); + assertEquals(Set.of("shapesPersonReport", "personJoinShapeReport", "simplePersonReport"), result.getReports().keySet()); + assertEquals(Set.of("PersonsByCreateDateBarChart"), result.getWidgets().keySet()); + + assertTrue(result.getTables().values().stream().allMatch(t -> t.getDeletePermission() && t.getReadPermission() && t.getInsertPermission() && t.getEditPermission())); + assertTrue(result.getProcesses().values().stream().allMatch(QFrontendProcessMetaData::getHasPermission)); + assertTrue(result.getReports().values().stream().allMatch(QFrontendReportMetaData::getHasPermission)); + assertTrue(result.getWidgets().values().stream().allMatch(QFrontendWidgetMetaData::getHasPermission)); + + // todo -- assert about apps & sections in those apps having just the right stuff + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testTableReadWritePermissions() throws QException + { + QInstance instance = TestUtils.defineInstance(); + instance.setDefaultPermissionRules(new QPermissionRules().withLevel(PermissionLevel.READ_WRITE_PERMISSIONS)); + MetaDataInput input = new MetaDataInput(instance); + input.setSession(new QSession().withPermissions( + "person.read", + "personFile.write", + "personMemory.read", + "personMemory.write", + "personMemoryCache.hasAccess", // this one should NOT come through. + "increaseBirthdate.hasAccess" + )); + MetaDataOutput result = new MetaDataAction().execute(input); + + assertEquals(Set.of("person", "personFile", "personMemory"), result.getTables().keySet()); + assertEquals(Set.of("increaseBirthdate"), result.getProcesses().keySet()); + assertEquals(Set.of(), result.getReports().keySet()); + assertEquals(Set.of(), result.getWidgets().keySet()); + + QFrontendTableMetaData personTable = result.getTables().get("person"); + assertTrue(personTable.getReadPermission()); + assertFalse(personTable.getInsertPermission()); + assertFalse(personTable.getEditPermission()); + assertFalse(personTable.getDeletePermission()); + + QFrontendTableMetaData personFileTable = result.getTables().get("personFile"); + assertFalse(personFileTable.getReadPermission()); + assertTrue(personFileTable.getInsertPermission()); + assertTrue(personFileTable.getEditPermission()); + assertTrue(personFileTable.getDeletePermission()); + + QFrontendTableMetaData personMemoryTable = result.getTables().get("personMemory"); + assertTrue(personMemoryTable.getReadPermission()); + assertTrue(personMemoryTable.getInsertPermission()); + assertTrue(personMemoryTable.getEditPermission()); + assertTrue(personMemoryTable.getDeletePermission()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testTableReadInsertEditDeletePermissions() throws QException + { + QInstance instance = TestUtils.defineInstance(); + instance.setDefaultPermissionRules(new QPermissionRules().withLevel(PermissionLevel.READ_INSERT_EDIT_DELETE_PERMISSIONS)); + MetaDataInput input = new MetaDataInput(instance); + input.setSession(new QSession().withPermissions( + "person.read", + "personFile.insert", + "personFile.edit", + "personMemory.read", + "personMemory.delete", + "personMemoryCache.hasAccess", // this one should NOT come through. + "increaseBirthdate.hasAccess" + )); + MetaDataOutput result = new MetaDataAction().execute(input); + + assertEquals(Set.of("person", "personFile", "personMemory"), result.getTables().keySet()); + assertEquals(Set.of("increaseBirthdate"), result.getProcesses().keySet()); + assertEquals(Set.of(), result.getReports().keySet()); + assertEquals(Set.of(), result.getWidgets().keySet()); + + QFrontendTableMetaData personTable = result.getTables().get("person"); + assertTrue(personTable.getReadPermission()); + assertFalse(personTable.getInsertPermission()); + assertFalse(personTable.getEditPermission()); + assertFalse(personTable.getDeletePermission()); + + QFrontendTableMetaData personFileTable = result.getTables().get("personFile"); + assertFalse(personFileTable.getReadPermission()); + assertTrue(personFileTable.getInsertPermission()); + assertTrue(personFileTable.getEditPermission()); + assertFalse(personFileTable.getDeletePermission()); + + QFrontendTableMetaData personMemoryTable = result.getTables().get("personMemory"); + assertTrue(personMemoryTable.getReadPermission()); + assertFalse(personMemoryTable.getInsertPermission()); + assertFalse(personMemoryTable.getEditPermission()); + assertTrue(personMemoryTable.getDeletePermission()); + } + } 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 new file mode 100644 index 00000000..ab2aebcc --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/permissions/PermissionsHelperTest.java @@ -0,0 +1,561 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.permissions; + + +import java.util.List; +import java.util.Set; +import com.kingsrook.qqq.backend.core.actions.processes.RunProcessTest; +import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException; +import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher; +import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; +import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +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.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.dashboard.QWidgetMetaData; +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.permissions.DenyBehavior; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QBackendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView; +import com.kingsrook.qqq.backend.core.model.metadata.reporting.ReportType; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/******************************************************************************* + ** Unit test for PermissionsHelper + *******************************************************************************/ +class PermissionsHelperTest +{ + private static final String TABLE_NAME = "testTable"; + private static final String PROCESS_NAME = "testProcess"; + private static final String REPORT_NAME = "testReport"; + private static final String WIDGET_NAME = "testWidget"; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTableNoPermissionsMetaDataMeansFullAccess() throws QPermissionDeniedException + { + QInstance instance = newQInstance(); + QSession session = new QSession(); + enrich(instance); + assertFullTableAccess(instance, session); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTableWithHasAccessLevelAndWithoutPermission() + { + QInstance instance = newQInstance(); + instance.getTable(TABLE_NAME).setPermissionRules(new QPermissionRules() + .withLevel(PermissionLevel.HAS_ACCESS_PERMISSION)); + enrich(instance); + + assertNoTableAccess(instance, new QSession()); + assertNoTableAccess(instance, new QSession().withPermission(TABLE_NAME + ".read")); + assertNoTableAccess(instance, new QSession().withPermission(TABLE_NAME + ".write")); + assertNoTableAccess(instance, new QSession().withPermission(TABLE_NAME + ".insert")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTableWithHasAccessLevelAndWithPermission() throws QPermissionDeniedException + { + QInstance instance = newQInstance(); + instance.getTable(TABLE_NAME).setPermissionRules(new QPermissionRules() + .withLevel(PermissionLevel.HAS_ACCESS_PERMISSION)); + enrich(instance); + + assertFullTableAccess(instance, new QSession().withPermission(TABLE_NAME + ".hasAccess")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTableWithReadWriteLevelAndWithoutPermission() + { + QInstance instance = newQInstance(); + instance.setDefaultPermissionRules(new QPermissionRules() + .withLevel(PermissionLevel.READ_WRITE_PERMISSIONS)); + enrich(instance); + + assertNoTableAccess(instance, new QSession()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTableWithReadWriteLevelWithLimitedPermissions() throws QPermissionDeniedException + { + QInstance instance = newQInstance(); + instance.setDefaultPermissionRules(new QPermissionRules() + .withLevel(PermissionLevel.READ_WRITE_PERMISSIONS)); + + { + QSession session = new QSession().withPermissions(TABLE_NAME + ".read"); + AbstractTableActionInput actionInput = new InsertInput(instance).withSession(session).withTableName(TABLE_NAME); + enrich(instance); + + 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)); + PermissionsHelper.checkTablePermissionThrowing(actionInput, TablePermissionSubType.READ); + assertThatThrownBy(() -> PermissionsHelper.checkTablePermissionThrowing(actionInput, TablePermissionSubType.INSERT)).isInstanceOf(QPermissionDeniedException.class); + assertThatThrownBy(() -> PermissionsHelper.checkTablePermissionThrowing(actionInput, TablePermissionSubType.EDIT)).isInstanceOf(QPermissionDeniedException.class); + assertThatThrownBy(() -> PermissionsHelper.checkTablePermissionThrowing(actionInput, TablePermissionSubType.DELETE)).isInstanceOf(QPermissionDeniedException.class); + assertEquals(PermissionCheckResult.ALLOW, PermissionsHelper.getPermissionCheckResult(actionInput, instance.getTable(TABLE_NAME))); + } + + { + QSession session = new QSession().withPermissions(TABLE_NAME + ".write"); + AbstractTableActionInput actionInput = new InsertInput(instance).withSession(session).withTableName(TABLE_NAME); + enrich(instance); + + 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)); + assertThatThrownBy(() -> PermissionsHelper.checkTablePermissionThrowing(actionInput, TablePermissionSubType.READ)).isInstanceOf(QPermissionDeniedException.class); + PermissionsHelper.checkTablePermissionThrowing(actionInput, TablePermissionSubType.INSERT); + PermissionsHelper.checkTablePermissionThrowing(actionInput, TablePermissionSubType.EDIT); + PermissionsHelper.checkTablePermissionThrowing(actionInput, TablePermissionSubType.DELETE); + assertEquals(PermissionCheckResult.ALLOW, PermissionsHelper.getPermissionCheckResult(actionInput, instance.getTable(TABLE_NAME))); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTableWithReadWriteLevelAndWithPermission() throws QPermissionDeniedException + { + QInstance instance = newQInstance(); + instance.setDefaultPermissionRules(new QPermissionRules() + .withLevel(PermissionLevel.READ_WRITE_PERMISSIONS)); + + assertFullTableAccess(instance, new QSession().withPermissions(TABLE_NAME + ".read", TABLE_NAME + ".write")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTableWithReadInsertEditDeleteLevelAndWithoutPermission() + { + QInstance instance = newQInstance(); + instance.getTable(TABLE_NAME).setPermissionRules(new QPermissionRules() + .withLevel(PermissionLevel.READ_INSERT_EDIT_DELETE_PERMISSIONS)); + + assertNoTableAccess(instance, new QSession()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTableWithReadInsertEditDeleteLevelAndWithPermission() throws QPermissionDeniedException + { + QInstance instance = newQInstance(); + instance.getTable(TABLE_NAME).setPermissionRules(new QPermissionRules() + .withLevel(PermissionLevel.READ_INSERT_EDIT_DELETE_PERMISSIONS)); + + assertFullTableAccess(instance, new QSession().withPermissions(TABLE_NAME + ".read", TABLE_NAME + ".insert", TABLE_NAME + ".edit", TABLE_NAME + ".delete")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testTableWithReadInsertEditDeleteLevelWithLimitedPermissions() throws QPermissionDeniedException + { + QInstance instance = newQInstance(); + instance.setDefaultPermissionRules(new QPermissionRules() + .withLevel(PermissionLevel.READ_INSERT_EDIT_DELETE_PERMISSIONS)); + + { + QSession session = new QSession().withPermissions(TABLE_NAME + ".read"); + AbstractTableActionInput actionInput = new InsertInput(instance).withSession(session).withTableName(TABLE_NAME); + enrich(instance); + + 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)); + PermissionsHelper.checkTablePermissionThrowing(actionInput, TablePermissionSubType.READ); + assertThatThrownBy(() -> PermissionsHelper.checkTablePermissionThrowing(actionInput, TablePermissionSubType.INSERT)).isInstanceOf(QPermissionDeniedException.class); + assertThatThrownBy(() -> PermissionsHelper.checkTablePermissionThrowing(actionInput, TablePermissionSubType.EDIT)).isInstanceOf(QPermissionDeniedException.class); + assertThatThrownBy(() -> PermissionsHelper.checkTablePermissionThrowing(actionInput, TablePermissionSubType.DELETE)).isInstanceOf(QPermissionDeniedException.class); + assertEquals(PermissionCheckResult.ALLOW, PermissionsHelper.getPermissionCheckResult(actionInput, instance.getTable(TABLE_NAME))); + } + + { + QSession session = new QSession().withPermissions(TABLE_NAME + ".insert"); + AbstractTableActionInput actionInput = new InsertInput(instance).withSession(session).withTableName(TABLE_NAME); + enrich(instance); + + 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)); + assertThatThrownBy(() -> PermissionsHelper.checkTablePermissionThrowing(actionInput, TablePermissionSubType.READ)).isInstanceOf(QPermissionDeniedException.class); + PermissionsHelper.checkTablePermissionThrowing(actionInput, TablePermissionSubType.INSERT); + assertThatThrownBy(() -> PermissionsHelper.checkTablePermissionThrowing(actionInput, TablePermissionSubType.EDIT)).isInstanceOf(QPermissionDeniedException.class); + assertThatThrownBy(() -> PermissionsHelper.checkTablePermissionThrowing(actionInput, TablePermissionSubType.DELETE)).isInstanceOf(QPermissionDeniedException.class); + assertEquals(PermissionCheckResult.ALLOW, PermissionsHelper.getPermissionCheckResult(actionInput, instance.getTable(TABLE_NAME))); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testProcessNoPermissionsMetaDataMeansFullAccess() throws QPermissionDeniedException + { + QInstance instance = newQInstance(); + QSession session = new QSession(); + AbstractActionInput actionInput = new AbstractActionInput(instance, session); + enrich(instance); + assertTrue(PermissionsHelper.hasProcessPermission(actionInput, PROCESS_NAME)); + PermissionsHelper.checkProcessPermissionThrowing(actionInput, PROCESS_NAME); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testProcessWithHasAccessLevelAndWithoutPermission() + { + QInstance instance = newQInstance(); + instance.getProcess(PROCESS_NAME) + .setPermissionRules(new QPermissionRules() + .withLevel(PermissionLevel.HAS_ACCESS_PERMISSION) + .withDenyBehavior(DenyBehavior.DISABLED) + ); + + QSession session = new QSession(); + AbstractActionInput actionInput = new AbstractActionInput(instance, session); + enrich(instance); + assertFalse(PermissionsHelper.hasProcessPermission(actionInput, PROCESS_NAME)); + assertThatThrownBy(() -> PermissionsHelper.checkProcessPermissionThrowing(actionInput, PROCESS_NAME)).isInstanceOf(QPermissionDeniedException.class); + assertEquals(PermissionCheckResult.DENY_DISABLE, PermissionsHelper.getPermissionCheckResult(actionInput, instance.getProcess(PROCESS_NAME))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testProcessWithHasAccessLevelAndWithPermission() throws QPermissionDeniedException + { + QInstance instance = newQInstance(); + instance.setDefaultPermissionRules(new QPermissionRules().withLevel(PermissionLevel.HAS_ACCESS_PERMISSION)); + instance.getProcess(PROCESS_NAME); + + QSession session = new QSession().withPermission(PROCESS_NAME + ".hasAccess"); + AbstractActionInput actionInput = new AbstractActionInput(instance, session); + enrich(instance); + assertTrue(PermissionsHelper.hasProcessPermission(actionInput, PROCESS_NAME)); + PermissionsHelper.checkProcessPermissionThrowing(actionInput, PROCESS_NAME); + assertEquals(PermissionCheckResult.ALLOW, PermissionsHelper.getPermissionCheckResult(actionInput, instance.getProcess(PROCESS_NAME))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testThatProcessesUseHasAccessPermissionIfInstanceDefaultIsLowerLevelTableDetails() throws QPermissionDeniedException + { + { + QInstance instance = newQInstance(); + instance.setDefaultPermissionRules(new QPermissionRules().withLevel(PermissionLevel.READ_INSERT_EDIT_DELETE_PERMISSIONS)); + instance.getProcess(PROCESS_NAME); + + QSession session = new QSession().withPermission(PROCESS_NAME + ".hasAccess"); + AbstractActionInput actionInput = new AbstractActionInput(instance, session); + enrich(instance); + assertTrue(PermissionsHelper.hasProcessPermission(actionInput, PROCESS_NAME)); + PermissionsHelper.checkProcessPermissionThrowing(actionInput, PROCESS_NAME); + assertEquals(PermissionCheckResult.ALLOW, PermissionsHelper.getPermissionCheckResult(actionInput, instance.getProcess(PROCESS_NAME))); + } + + { + QInstance instance = newQInstance(); + instance.setDefaultPermissionRules(new QPermissionRules().withLevel(PermissionLevel.READ_WRITE_PERMISSIONS)); + instance.getProcess(PROCESS_NAME); + + QSession session = new QSession(); + AbstractActionInput actionInput = new AbstractActionInput(instance, session); + enrich(instance); + assertFalse(PermissionsHelper.hasProcessPermission(actionInput, PROCESS_NAME)); + assertThatThrownBy(() -> PermissionsHelper.checkProcessPermissionThrowing(actionInput, PROCESS_NAME)).isInstanceOf(QPermissionDeniedException.class); + assertEquals(PermissionCheckResult.DENY_HIDE, PermissionsHelper.getPermissionCheckResult(actionInput, instance.getProcess(PROCESS_NAME))); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testProcessWithAlternatePermissionName() throws QPermissionDeniedException + { + QInstance instance = newQInstance(); + instance.getProcess(PROCESS_NAME) + .setPermissionRules(new QPermissionRules() + .withLevel(PermissionLevel.HAS_ACCESS_PERMISSION) + .withDenyBehavior(instance.getDefaultPermissionRules().getDenyBehavior()) + .withPermissionBaseName("someProcess") + ); + + { + ////////////////////////////////////////////////////// + // make sure we FAIL with the processName.hasAccess // + ////////////////////////////////////////////////////// + QSession session = new QSession(); + AbstractActionInput actionInput = new AbstractActionInput(instance, session); + enrich(instance); + assertFalse(PermissionsHelper.hasProcessPermission(actionInput, PROCESS_NAME)); + assertThatThrownBy(() -> PermissionsHelper.checkProcessPermissionThrowing(actionInput, PROCESS_NAME)).isInstanceOf(QPermissionDeniedException.class); + assertEquals(PermissionCheckResult.DENY_HIDE, PermissionsHelper.getPermissionCheckResult(actionInput, instance.getProcess(PROCESS_NAME))); + } + + { + //////////////////////////////////////////////////////////////////////// + // make sure we PASS with the override (permissionBaseName).hasAccess // + //////////////////////////////////////////////////////////////////////// + QSession session = new QSession().withPermission("someProcess.hasAccess"); + AbstractActionInput actionInput = new AbstractActionInput(instance, session); + enrich(instance); + assertTrue(PermissionsHelper.hasProcessPermission(actionInput, PROCESS_NAME)); + PermissionsHelper.checkProcessPermissionThrowing(actionInput, PROCESS_NAME); + assertEquals(PermissionCheckResult.ALLOW, PermissionsHelper.getPermissionCheckResult(actionInput, instance.getProcess(PROCESS_NAME))); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testReportWithHasAccessLevel() throws QPermissionDeniedException + { + QInstance instance = newQInstance(); + instance.getReport(REPORT_NAME) + .setPermissionRules(new QPermissionRules() + .withLevel(PermissionLevel.HAS_ACCESS_PERMISSION)); + + { + QSession session = new QSession(); + AbstractActionInput actionInput = new AbstractActionInput(instance, session); + enrich(instance); + assertFalse(PermissionsHelper.hasReportPermission(actionInput, REPORT_NAME)); + assertThatThrownBy(() -> PermissionsHelper.checkReportPermissionThrowing(actionInput, REPORT_NAME)).isInstanceOf(QPermissionDeniedException.class); + assertEquals(PermissionCheckResult.DENY_HIDE, PermissionsHelper.getPermissionCheckResult(actionInput, instance.getReport(REPORT_NAME))); + } + + { + QSession session = new QSession().withPermission(REPORT_NAME + ".hasAccess"); + AbstractActionInput actionInput = new AbstractActionInput(instance, session); + enrich(instance); + assertTrue(PermissionsHelper.hasReportPermission(actionInput, REPORT_NAME)); + PermissionsHelper.checkReportPermissionThrowing(actionInput, REPORT_NAME); + assertEquals(PermissionCheckResult.ALLOW, PermissionsHelper.getPermissionCheckResult(actionInput, instance.getReport(REPORT_NAME))); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testWidgetWithNoProtection() throws QPermissionDeniedException + { + QInstance instance = newQInstance(); + QSession session = new QSession(); + AbstractActionInput actionInput = new AbstractActionInput(instance, session); + enrich(instance); + + assertTrue(PermissionsHelper.hasWidgetPermission(actionInput, WIDGET_NAME)); + PermissionsHelper.checkWidgetPermissionThrowing(actionInput, WIDGET_NAME); + assertEquals(PermissionCheckResult.ALLOW, PermissionsHelper.getPermissionCheckResult(actionInput, instance.getWidget(WIDGET_NAME))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testGetAllAvailablePermissionNames() + { + { + QInstance instance = newQInstance(); + instance.setDefaultPermissionRules(new QPermissionRules() + .withLevel(PermissionLevel.NOT_PROTECTED)); + enrich(instance); + assertEquals(Set.of(), PermissionsHelper.getAllAvailablePermissionNames(instance)); + } + + { + QInstance instance = newQInstance(); + instance.setDefaultPermissionRules(new QPermissionRules() + .withLevel(PermissionLevel.HAS_ACCESS_PERMISSION)); + enrich(instance); + assertEquals(Set.of(TABLE_NAME + ".hasAccess", PROCESS_NAME + ".hasAccess", REPORT_NAME + ".hasAccess", WIDGET_NAME + ".hasAccess"), PermissionsHelper.getAllAvailablePermissionNames(instance)); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void enrich(QInstance instance) + { + new QInstanceEnricher(instance).enrich(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QInstance newQInstance() + { + QInstance qInstance = new QInstance(); + + qInstance.addBackend(new QBackendMetaData() + .withName("backend")); + + qInstance.addTable(new QTableMetaData() + .withName(TABLE_NAME) + .withBackendName("backend") + .withPrimaryKeyField("id") + .withField(new QFieldMetaData("id", QFieldType.INTEGER))); + + qInstance.addProcess(new QProcessMetaData() + .withName(PROCESS_NAME) + .withStepList(List.of(new QBackendStepMetaData() + .withCode(new QCodeReference(RunProcessTest.NoopBackendStep.class)) + .withName("noop") + ))); + + qInstance.addReport(new QReportMetaData() + .withName(REPORT_NAME) + .withDataSource(new QReportDataSource().withSourceTable(TABLE_NAME)) + .withView(new QReportView().withType(ReportType.TABLE).withColumn(new QReportField("id")))); + + qInstance.addWidget(new QWidgetMetaData() + .withName(WIDGET_NAME)); + + return (qInstance); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void assertFullTableAccess(QInstance instance, QSession session) throws QPermissionDeniedException + { + AbstractTableActionInput actionInput = new InsertInput(instance).withSession(session).withTableName(TABLE_NAME); + + for(TablePermissionSubType permissionSubType : TablePermissionSubType.values()) + { + assertTrue(PermissionsHelper.hasTablePermission(actionInput, TABLE_NAME, permissionSubType), "Expected to have permission " + TABLE_NAME + ":" + permissionSubType); + PermissionsHelper.checkTablePermissionThrowing(actionInput, permissionSubType); + } + + assertEquals(PermissionCheckResult.ALLOW, PermissionsHelper.getPermissionCheckResult(actionInput, instance.getTable(TABLE_NAME))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void assertNoTableAccess(QInstance instance, QSession session) + { + AbstractTableActionInput actionInput = new InsertInput(instance).withSession(session).withTableName(TABLE_NAME); + + for(TablePermissionSubType permissionSubType : TablePermissionSubType.values()) + { + assertFalse(PermissionsHelper.hasTablePermission(actionInput, TABLE_NAME, permissionSubType)); + assertThatThrownBy(() -> PermissionsHelper.checkTablePermissionThrowing(actionInput, permissionSubType)) + .isExactlyInstanceOf(QPermissionDeniedException.class); + } + + assertEquals(PermissionCheckResult.DENY_HIDE, PermissionsHelper.getPermissionCheckResult(actionInput, instance.getTable(TABLE_NAME))); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/permissions/ReportProcessPermissionCheckerTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/permissions/ReportProcessPermissionCheckerTest.java new file mode 100644 index 00000000..3db63bd4 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/permissions/ReportProcessPermissionCheckerTest.java @@ -0,0 +1,76 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.permissions; + + +import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException; +import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel; +import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +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.assertThrows; + + +/******************************************************************************* + ** Unit test for ReportProcessPermissionChecker + *******************************************************************************/ +class ReportProcessPermissionCheckerTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() throws Exception + { + QInstance qInstance = TestUtils.defineInstance(); + RunProcessInput runProcessInput = new RunProcessInput(qInstance); + runProcessInput.addValue("reportName", TestUtils.REPORT_NAME_SHAPES_PERSON); + + qInstance.getReport(TestUtils.REPORT_NAME_SHAPES_PERSON) + .withPermissionRules(new QPermissionRules().withLevel(PermissionLevel.HAS_ACCESS_PERMISSION)); + + QProcessMetaData process = new QProcessMetaData() + .withName("testProcess"); + qInstance.addProcess(process); + + new QInstanceValidator().validate(qInstance); + + /////////////////////////////////////////////////////// + // without permission in our session, we should fail // + /////////////////////////////////////////////////////// + runProcessInput.setSession(new QSession()); + assertThrows(QPermissionDeniedException.class, () -> new ReportProcessPermissionChecker().checkPermissionsThrowing(runProcessInput, process)); + + ///////////////////////////////////////////////////////////////////////// + // add the permission - assert that we have access (e.g., don't throw) // + ///////////////////////////////////////////////////////////////////////// + runProcessInput.setSession(new QSession().withPermission(TestUtils.REPORT_NAME_SHAPES_PERSON + ".hasAccess")); + new ReportProcessPermissionChecker().checkPermissionsThrowing(runProcessInput, process); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportActionTest.java index aa35430a..6c0ac44c 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/ExportActionTest.java @@ -40,6 +40,8 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.utils.TestUtils; import org.apache.commons.io.FileUtils; +import org.json.JSONArray; +import org.json.JSONObject; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -213,4 +215,26 @@ class ExportActionTest }); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + public void testJSON() throws Exception + { + int recordCount = 1000; + String filename = "/tmp/ReportActionTest.json"; + + runReport(recordCount, filename, ReportFormat.JSON, false); + + File file = new File(filename); + @SuppressWarnings("unchecked") + String fileContent = FileUtils.readFileToString(file, StandardCharsets.UTF_8.name()); + JSONArray jsonArray = new JSONArray(fileContent); + assertEquals(recordCount, jsonArray.length()); + JSONObject row0 = jsonArray.getJSONObject(0); + assertNotNull(row0.optString("lastName")); + } + } \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/AggregateActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/AggregateActionTest.java new file mode 100644 index 00000000..34de441a --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/tables/AggregateActionTest.java @@ -0,0 +1,52 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.tables; + + +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.AggregateInput; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertThrows; + + +/******************************************************************************* + ** Unit test for com.kingsrook.qqq.backend.core.actions.tables.AggregateAction + *******************************************************************************/ +class AggregateActionTest +{ + + /******************************************************************************* + ** At the core level, there isn't much that can be asserted, as it uses the + ** mock implementation - just confirming that all of the "wiring" works. + ** + *******************************************************************************/ + @Test + void test() throws QException + { + AggregateInput request = new AggregateInput(TestUtils.defineInstance()); + request.setSession(TestUtils.getMockSession()); + request.setTableName("person"); + assertThrows(IllegalStateException.class, () -> new AggregateAction().execute(request)); + } + +} \ No newline at end of file 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 83a1f3f3..c85f8174 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 @@ -27,6 +27,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.function.Consumer; +import java.util.function.Function; import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostQueryCustomizer; import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; @@ -46,6 +47,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; 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.fields.ValueTooLongBehavior; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppMetaData; import com.kingsrook.qqq.backend.core.model.metadata.layout.QAppSection; import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; @@ -57,6 +59,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.queues.SQSQueueProviderMeta import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportDataSource; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; +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.security.RecordSecurityLock; import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; @@ -69,6 +74,7 @@ import com.kingsrook.qqq.backend.core.processes.implementations.etl.streamedwith import com.kingsrook.qqq.backend.core.utils.TestUtils; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.fail; @@ -1505,6 +1511,123 @@ class QInstanceValidatorTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFieldValueTooLongBehavior() + { + Function fieldExtractor = qInstance -> qInstance.getTable(TestUtils.TABLE_NAME_PERSON).getField("firstName"); + + assertValidationFailureReasons((qInstance -> fieldExtractor.apply(qInstance).withBehavior(ValueTooLongBehavior.ERROR)), "specifies a ValueTooLongBehavior, but not a maxLength"); + assertValidationFailureReasons((qInstance -> fieldExtractor.apply(qInstance).withBehavior(ValueTooLongBehavior.TRUNCATE)), "specifies a ValueTooLongBehavior, but not a maxLength"); + assertValidationFailureReasons((qInstance -> fieldExtractor.apply(qInstance).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS)), "specifies a ValueTooLongBehavior, but not a maxLength"); + assertValidationSuccess((qInstance -> fieldExtractor.apply(qInstance).withBehavior(ValueTooLongBehavior.PASS_THROUGH))); + + assertValidationFailureReasons((qInstance -> fieldExtractor.apply(qInstance).withBehavior(ValueTooLongBehavior.ERROR).withMaxLength(0)), "invalid maxLength"); + assertValidationFailureReasons((qInstance -> fieldExtractor.apply(qInstance).withBehavior(ValueTooLongBehavior.ERROR).withMaxLength(-1)), "invalid maxLength"); + assertValidationSuccess((qInstance -> fieldExtractor.apply(qInstance).withBehavior(ValueTooLongBehavior.ERROR).withMaxLength(1))); + + Function idFieldExtractor = qInstance -> qInstance.getTable(TestUtils.TABLE_NAME_PERSON).getField("id"); + assertValidationFailureReasons((qInstance -> idFieldExtractor.apply(qInstance).withBehavior(ValueTooLongBehavior.ERROR).withMaxLength(1)), "maxLength, but is not of a supported type"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSecurityKeyTypes() + { + assertValidationFailureReasons((qInstance -> qInstance.addSecurityKeyType(new QSecurityKeyType())), + "Missing name for a securityKeyType"); + + assertValidationFailureReasons((qInstance -> qInstance.addSecurityKeyType(new QSecurityKeyType().withName(""))), + "Missing name for a securityKeyType"); + + assertThatThrownBy(() -> + { + QInstance qInstance = TestUtils.defineInstance(); + qInstance.addSecurityKeyType(new QSecurityKeyType().withName("clientId")); + qInstance.addSecurityKeyType(new QSecurityKeyType().withName("clientId")); + }).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("Attempted to add a second securityKeyType with name: clientId"); + + assertValidationFailureReasons((qInstance -> + { + QSecurityKeyType securityKeyType1 = new QSecurityKeyType().withName("clientId"); + QSecurityKeyType securityKeyType2 = new QSecurityKeyType().withName("notClientId"); + qInstance.addSecurityKeyType(securityKeyType1); + qInstance.addSecurityKeyType(securityKeyType2); + securityKeyType2.setName("clientId"); + }), "Inconsistent naming for securityKeyType"); + + assertValidationFailureReasons((qInstance -> + { + qInstance.addSecurityKeyType(new QSecurityKeyType().withName("clientId").withAllAccessKeyName("clientId")); + }), "More than one SecurityKeyType with name (or allAccessKeyName) 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"); + + 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"); + + assertValidationFailureReasons((qInstance -> qInstance.addSecurityKeyType(new QSecurityKeyType().withName("clientId").withPossibleValueSourceName("nonPVS"))), + "Unrecognized possibleValueSourceName in securityKeyType"); + + assertValidationSuccess((qInstance -> qInstance.addSecurityKeyType(new QSecurityKeyType().withName("clientId").withPossibleValueSourceName(TestUtils.POSSIBLE_VALUE_SOURCE_STATE)))); + assertValidationSuccess((qInstance -> qInstance.addSecurityKeyType(new QSecurityKeyType().withName("clientId").withAllAccessKeyName("clientAllAccess")))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testRecordSecurityLocks() + { + Function lockExtractor = qInstance -> qInstance.getTable(TestUtils.TABLE_NAME_ORDER).getRecordSecurityLocks().get(0); + + assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setSecurityKeyType(null)), "missing a securityKeyType"); + assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setSecurityKeyType(" ")), "missing a securityKeyType"); + assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setSecurityKeyType("notAKeyType")), "unrecognized securityKeyType"); + assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setFieldName(null)), "missing a fieldName"); + assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setFieldName("")), "missing a fieldName"); + assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setFieldName("notAField")), "unrecognized field"); + assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setNullValueBehavior(null)), "missing a nullValueBehavior"); + + // todo - remove once implemented + assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setFieldName("join.field")), "does not yet support finding a field that looks like a join field"); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testFieldSecurityLocks() + { + Function lockExtractor = qInstance -> qInstance.getTable(TestUtils.TABLE_NAME_ORDER).getField("total").getFieldSecurityLock(); + + assertValidationFailureReasons((qInstance -> lockExtractor.apply(qInstance).setSecurityKeyType(null)), "missing a securityKeyType"); + 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"); + } + + + /******************************************************************************* ** *******************************************************************************/ diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaDataTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaDataTest.java new file mode 100644 index 00000000..b9aabcee --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaDataTest.java @@ -0,0 +1,73 @@ +/* + * 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.tables; + + +import com.kingsrook.qqq.backend.core.model.metadata.QBackendMetaData; +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 QTableMetaData + *******************************************************************************/ +class QTableMetaDataTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testIsCapabilityEnabled() + { + Capability capability = Capability.TABLE_GET; + + // table:null & backend:null = true + assertTrue(new QTableMetaData().isCapabilityEnabled(new QBackendMetaData(), capability)); + + // table:null & backend:true = true + assertTrue(new QTableMetaData().isCapabilityEnabled(new QBackendMetaData().withCapability(capability), capability)); + + // table:null & backend:false = false + assertFalse(new QTableMetaData().isCapabilityEnabled(new QBackendMetaData().withoutCapability(capability), capability)); + + // table:true & backend:null = true + assertTrue(new QTableMetaData().withCapability(capability).isCapabilityEnabled(new QBackendMetaData(), capability)); + + // table:false & backend:null = false + assertFalse(new QTableMetaData().withoutCapability(capability).isCapabilityEnabled(new QBackendMetaData(), capability)); + + // table:true & backend:true = true + assertTrue(new QTableMetaData().withCapability(capability).isCapabilityEnabled(new QBackendMetaData().withCapability(capability), capability)); + + // table:true & backend:false = true + assertTrue(new QTableMetaData().withCapability(capability).isCapabilityEnabled(new QBackendMetaData().withoutCapability(capability), capability)); + + // table:false & backend:true = false + assertFalse(new QTableMetaData().withoutCapability(capability).isCapabilityEnabled(new QBackendMetaData().withCapability(capability), capability)); + + // table:false & backend:false = false + assertFalse(new QTableMetaData().withoutCapability(capability).isCapabilityEnabled(new QBackendMetaData().withoutCapability(capability), capability)); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/session/QSessionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/session/QSessionTest.java new file mode 100644 index 00000000..ea8de126 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/session/QSessionTest.java @@ -0,0 +1,112 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. 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.session; + + +import java.util.List; +import java.util.Map; +import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/******************************************************************************* + ** Unit test for QSession + *******************************************************************************/ +class QSessionTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSecurityKeys() + { + QSession session = new QSession().withSecurityKeyValues(Map.of( + "clientId", List.of(42, 47), + "warehouseId", List.of(1701) + )); + assertEquals(List.of(42, 47), session.getSecurityKeyValues("clientId")); + assertEquals(List.of(1701), session.getSecurityKeyValues("warehouseId")); + assertEquals(List.of(), session.getSecurityKeyValues("tenantId")); + + session.withSecurityKeyValues("clientId", List.of(256, 512)); + for(int i : List.of(42, 47, 256, 512)) + { + assertTrue(session.hasSecurityKeyValue("clientId", i), "Should contain: " + i); + assertTrue(session.hasSecurityKeyValue("clientId", String.valueOf(i), QFieldType.INTEGER), "Should contain: " + i); + } + + session.clearSecurityKeyValues(); + for(int i : List.of(42, 47, 256, 512)) + { + assertFalse(session.hasSecurityKeyValue("clientId", i), "Should no longer contain: " + i); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testMixedValueTypes() + { + QSession session = new QSession().withSecurityKeyValues(Map.of( + "storeId", List.of("100", "200", 300) + )); + + for(int i : List.of(100, 200, 300)) + { + assertTrue(session.hasSecurityKeyValue("storeId", i, QFieldType.INTEGER), "Should contain: " + i); + assertTrue(session.hasSecurityKeyValue("storeId", String.valueOf(i), QFieldType.INTEGER), "Should contain: " + i); + assertTrue(session.hasSecurityKeyValue("storeId", i, QFieldType.STRING), "Should contain: " + i); + assertTrue(session.hasSecurityKeyValue("storeId", String.valueOf(i), QFieldType.STRING), "Should contain: " + i); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testNullSafety() + { + QSession session = new QSession(); + assertFalse(session.hasSecurityKeyValue("any", 1)); + assertFalse(session.hasSecurityKeyValue("other", 1)); + assertFalse(session.hasSecurityKeyValue("other", 1, QFieldType.STRING)); + assertEquals(List.of(), session.getSecurityKeyValues("any")); + + session.withSecurityKeyValue("any", 1); + assertTrue(session.hasSecurityKeyValue("any", 1)); + assertTrue(session.hasSecurityKeyValue("any", 1, QFieldType.STRING)); + assertFalse(session.hasSecurityKeyValue("any", 2)); + assertFalse(session.hasSecurityKeyValue("other", 1)); + assertFalse(session.hasSecurityKeyValue("other", 1, QFieldType.STRING)); + assertEquals(List.of(), session.getSecurityKeyValues("other")); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModuleTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModuleTest.java index 918b57a2..39f9d3ef 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModuleTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0AuthenticationModuleTest.java @@ -26,6 +26,7 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Base64; import java.util.HashMap; +import java.util.List; import java.util.Map; import com.auth0.exception.Auth0Exception; import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; @@ -36,15 +37,18 @@ import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticat import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider; import com.kingsrook.qqq.backend.core.state.SimpleStateKey; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.json.JSONObject; import org.junit.jupiter.api.Test; -import static com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule.AUTH0_ID_TOKEN_KEY; +import static com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule.AUTH0_ACCESS_TOKEN_KEY; import static com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule.BASIC_AUTH_KEY; import static com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule.COULD_NOT_DECODE_ERROR; import static com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule.EXPIRED_TOKEN_ERROR; import static com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule.INVALID_TOKEN_ERROR; import static com.kingsrook.qqq.backend.core.modules.authentication.implementations.Auth0AuthenticationModule.TOKEN_NOT_PROVIDED_ERROR; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -139,7 +143,7 @@ public class Auth0AuthenticationModuleTest public void testInvalidToken() { Map context = new HashMap<>(); - context.put(AUTH0_ID_TOKEN_KEY, INVALID_TOKEN); + context.put(AUTH0_ACCESS_TOKEN_KEY, INVALID_TOKEN); try { @@ -163,7 +167,7 @@ public class Auth0AuthenticationModuleTest public void testUndecodableToken() { Map context = new HashMap<>(); - context.put(AUTH0_ID_TOKEN_KEY, UNDECODABLE_TOKEN); + context.put(AUTH0_ACCESS_TOKEN_KEY, UNDECODABLE_TOKEN); try { @@ -187,7 +191,7 @@ public class Auth0AuthenticationModuleTest public void testProperlyFormattedButExpiredToken() { Map context = new HashMap<>(); - context.put(AUTH0_ID_TOKEN_KEY, EXPIRED_TOKEN); + context.put(AUTH0_ACCESS_TOKEN_KEY, EXPIRED_TOKEN); try { @@ -232,7 +236,7 @@ public class Auth0AuthenticationModuleTest public void testNullToken() { Map context = new HashMap<>(); - context.put(AUTH0_ID_TOKEN_KEY, null); + context.put(AUTH0_ACCESS_TOKEN_KEY, null); try { @@ -257,11 +261,173 @@ public class Auth0AuthenticationModuleTest Map context = new HashMap<>(); context.put(BASIC_AUTH_KEY, encodeBasicAuth("darin.kelkhoff@gmail.com", "6-EQ!XzBJ!F*LRVDK6VZY__92!")); + QInstance qInstance = getQInstance(); + Auth0AuthenticationModule auth0Spy = spy(Auth0AuthenticationModule.class); - auth0Spy.createSession(getQInstance(), context); - auth0Spy.createSession(getQInstance(), context); - auth0Spy.createSession(getQInstance(), context); - verify(auth0Spy, times(1)).getIdTokenFromAuth0(any(), any()); + auth0Spy.createSession(qInstance, context); + auth0Spy.createSession(qInstance, context); + auth0Spy.createSession(qInstance, context); + verify(auth0Spy, times(1)).getAccessTokenFromAuth0(any(), any(), any()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSetPermissionsInSessionFromJwtPayload() + { + QSession qSession = new QSession(); + JSONObject payload = new JSONObject(""" + { + "com.kingsrook.qqq.client_metadata": { + "securityKeyValues:clientIdAllAccess": "true" + }, + "iss": "https://kingsrook.us.auth0.com/", + "sub": "LRuqVS2awusyOyqTzMH6oPC00XXKJj@clients", + "aud": "https://www.kingsrook.com", + "iat": 1673379451, + "exp": 1675971451, + "azp": "LRuqVS2awOyqTFwzMH6oPC00XXKJj", + "gty": "client-credentials", + "permissions": [ + "client.read", + "client.insert" + ] + } + """); + Auth0AuthenticationModule.setPermissionsInSessionFromJwtPayload(payload, qSession); + assertTrue(qSession.hasPermission("client.read")); + assertTrue(qSession.hasPermission("client.insert")); + assertEquals(2, qSession.getPermissions().size()); + + /////////////////////////////// + // test w/ empty permissions // + /////////////////////////////// + qSession = new QSession(); + payload = new JSONObject(""" + { + "iss": "https://kingsrook.us.auth0.com/", + "azp": "LRuqVS2awOyqTFwzMH6oPC00XXKJj", + "gty": "client-credentials", + "permissions": [] + } + """); + Auth0AuthenticationModule.setPermissionsInSessionFromJwtPayload(payload, qSession); + assertTrue(qSession.getPermissions().isEmpty()); + + ///////////////////////////////// + // test w/ missing permissions // + ///////////////////////////////// + qSession = new QSession(); + payload = new JSONObject(""" + { + "iss": "https://kingsrook.us.auth0.com/", + "azp": "LRuqVS2awOyqTFwzMH6oPC00XXKJj", + "gty": "client-credentials" + } + """); + Auth0AuthenticationModule.setPermissionsInSessionFromJwtPayload(payload, qSession); + assertTrue(qSession.getPermissions().isEmpty()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testSetSecurityKeysInSessionFromJwtPayload() + { + QInstance qInstance = getQInstance(); + QSession qSession = new QSession(); + JSONObject payload = new JSONObject(""" + { + "com.kingsrook.qqq.client_metadata": { + "securityKeyValues:storeAllAccess": "true" + }, + "iss": "https://kingsrook.us.auth0.com/", + "sub": "LRuqVS2awusyOyqTzMH6oPC00XXKJj@clients", + "iat": 1673379451 + } + """); + Auth0AuthenticationModule.setSecurityKeysInSessionFromJwtPayload(qInstance, payload, qSession); + assertEquals(List.of("true"), qSession.getSecurityKeyValues("storeAllAccess")); + + ///////////////////////////////////////////// + // app_metadata instead of client_metadata // + ///////////////////////////////////////////// + qSession = new QSession(); + payload = new JSONObject(""" + { + "com.kingsrook.qqq.app_metadata": { + "securityKeyValues": { + "store": 2 + } + }, + "iss": "https://kingsrook.us.auth0.com/", + "sub": "LRuqVS2awusyOyqTzMH6oPC00XXKJj@clients", + "iat": 1673379451 + } + """); + Auth0AuthenticationModule.setSecurityKeysInSessionFromJwtPayload(qInstance, payload, qSession); + assertEquals(List.of("2"), qSession.getSecurityKeyValues("store")); + + ////////////////////////// + // list of values // + // and, more than 1 key // + ////////////////////////// + qSession = new QSession(); + payload = new JSONObject(""" + { + "com.kingsrook.qqq.app_metadata": { + "securityKeyValues": { + "store": [3, 4, 5], + "internalOrExternal": "internal" + } + }, + "iss": "https://kingsrook.us.auth0.com/", + "sub": "LRuqVS2awusyOyqTzMH6oPC00XXKJj@clients", + "iat": 1673379451 + } + """); + Auth0AuthenticationModule.setSecurityKeysInSessionFromJwtPayload(qInstance, payload, qSession); + assertEquals(List.of("3", "4", "5"), qSession.getSecurityKeyValues("store")); + assertEquals(List.of("internal"), qSession.getSecurityKeyValues("internalOrExternal")); + + /////////////////////////////////////////// + // missing meta data -> no security keys // + /////////////////////////////////////////// + qSession = new QSession(); + payload = new JSONObject(""" + { + "iss": "https://kingsrook.us.auth0.com/", + "sub": "LRuqVS2awusyOyqTzMH6oPC00XXKJj@clients", + "iat": 1673379451 + } + """); + Auth0AuthenticationModule.setSecurityKeysInSessionFromJwtPayload(qInstance, payload, qSession); + assertTrue(CollectionUtils.nullSafeIsEmpty(qSession.getSecurityKeyValues())); + + ///////////////////////////////////////////////////// + // unrecognized security key -> no keys in session // + ///////////////////////////////////////////////////// + qSession = new QSession(); + payload = new JSONObject(""" + { + "com.kingsrook.qqq.app_metadata": { + "securityKeyValues": { + "notAKey": 47 + } + }, + "iss": "https://kingsrook.us.auth0.com/", + "sub": "LRuqVS2awusyOyqTzMH6oPC00XXKJj@clients", + "iat": 1673379451 + } + """); + Auth0AuthenticationModule.setSecurityKeysInSessionFromJwtPayload(qInstance, payload, qSession); + assertTrue(CollectionUtils.nullSafeIsEmpty(qSession.getSecurityKeyValues())); } @@ -275,11 +441,13 @@ public class Auth0AuthenticationModuleTest String auth0BaseUrl = new QMetaDataVariableInterpreter().interpret("${env.AUTH0_BASE_URL}"); String auth0ClientId = new QMetaDataVariableInterpreter().interpret("${env.AUTH0_CLIENT_ID}"); String auth0ClientSecret = new QMetaDataVariableInterpreter().interpret("${env.AUTH0_CLIENT_SECRET}"); + String auth0Audience = new QMetaDataVariableInterpreter().interpret("${env.AUTH0_AUDIENCE}"); QAuthenticationMetaData authenticationMetaData = new Auth0AuthenticationMetaData() .withBaseUrl(auth0BaseUrl) .withClientId(auth0ClientId) .withClientSecret(auth0ClientSecret) + .withAudience(auth0Audience) .withName("auth0"); QInstance qInstance = TestUtils.defineInstance(); diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModuleTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModuleTest.java index cd0976e2..b447b333 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModuleTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/TableBasedAuthenticationModuleTest.java @@ -42,6 +42,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.authentication.TableBasedAu import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryRecordStore; import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider; +import com.kingsrook.qqq.backend.core.state.SimpleStateKey; import com.kingsrook.qqq.backend.core.utils.TestUtils; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -191,7 +192,7 @@ public class TableBasedAuthenticationModuleTest QSession session = new QSession(); session.setIdReference(uuid); - InMemoryStateProvider.getInstance().put(new TableBasedAuthenticationModule.SessionIdStateKey(session.getIdReference()), Instant.now()); + InMemoryStateProvider.getInstance().put(new SimpleStateKey<>(session.getIdReference()), Instant.now()); assertTrue(new TableBasedAuthenticationModule().isSessionValid(qInstance, session)); } @@ -318,7 +319,7 @@ public class TableBasedAuthenticationModuleTest assertTrue(new TableBasedAuthenticationModule().isSessionValid(qInstance, session)); - InMemoryStateProvider.getInstance().put(new TableBasedAuthenticationModule.SessionIdStateKey(session.getIdReference()), Instant.now().minus(TableBasedAuthenticationModule.ID_TOKEN_VALIDATION_INTERVAL_SECONDS + 10, ChronoUnit.SECONDS)); + InMemoryStateProvider.getInstance().put(new SimpleStateKey<>(session.getIdReference()), Instant.now().minus(TableBasedAuthenticationModule.ID_TOKEN_VALIDATION_INTERVAL_SECONDS + 10, ChronoUnit.SECONDS)); MemoryRecordStore.setCollectStatistics(true); assertTrue(new TableBasedAuthenticationModule().isSessionValid(qInstance, session)); diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java index d2f70281..cff65e63 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/backend/implementations/memory/MemoryBackendModuleTest.java @@ -40,6 +40,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.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.QFilterOrderBy; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; @@ -339,6 +340,43 @@ class MemoryBackendModuleTest assertThat(queryShapes(qInstance, table, session, filter)).anyMatch(r -> r.getValueString("name").equals("Circle") && r.getValueInteger("id").equals(3)); } + ////////////////// + // skip & limit // + ////////////////// + { + QueryInput queryInput = new QueryInput(qInstance); + queryInput.setSession(session); + queryInput.setTableName(table.getName()); + queryInput.setLimit(2); + assertEquals(2, new QueryAction().execute(queryInput).getRecords().size()); + + queryInput.setLimit(1); + assertEquals(1, new QueryAction().execute(queryInput).getRecords().size()); + + queryInput.setSkip(4); + queryInput.setLimit(3); + assertEquals(0, new QueryAction().execute(queryInput).getRecords().size()); + } + + /////////// + // order // + /////////// + { + QueryInput queryInput = new QueryInput(qInstance); + queryInput.setSession(session); + queryInput.setTableName(table.getName()); + queryInput.setFilter(new QQueryFilter().withOrderBy(new QFilterOrderBy("name", true))); + assertEquals(List.of("Circle", "Square", "Triangle"), new QueryAction().execute(queryInput).getRecords().stream().map(r -> r.getValueString("name")).toList()); + + queryInput.setFilter(new QQueryFilter().withOrderBy(new QFilterOrderBy("name", false))); + assertEquals(List.of("Triangle", "Square", "Circle"), new QueryAction().execute(queryInput).getRecords().stream().map(r -> r.getValueString("name")).toList()); + + queryInput.setFilter(new QQueryFilter().withOrderBy(new QFilterOrderBy("id", true))); + assertEquals(List.of(1, 2, 3), new QueryAction().execute(queryInput).getRecords().stream().map(r -> r.getValueInteger("id")).toList()); + + queryInput.setFilter(new QQueryFilter().withOrderBy(new QFilterOrderBy("id", false))); + assertEquals(List.of(3, 2, 1), new QueryAction().execute(queryInput).getRecords().stream().map(r -> r.getValueInteger("id")).toList()); + } } @@ -353,6 +391,9 @@ class MemoryBackendModuleTest + /******************************************************************************* + ** + *******************************************************************************/ private List queryShapes(QInstance qInstance, QTableMetaData table, QSession session, QQueryFilter filter) throws QException { QueryInput queryInput = new QueryInput(qInstance); 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 2178c865..89c5983e 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 @@ -54,6 +54,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; 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.authentication.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.model.metadata.automation.PollingAutomationProviderMetaData; import com.kingsrook.qqq.backend.core.model.metadata.automation.QAutomationProviderMetaData; import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; @@ -85,6 +86,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportField; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportMetaData; import com.kingsrook.qqq.backend.core.model.metadata.reporting.QReportView; import com.kingsrook.qqq.backend.core.model.metadata.reporting.ReportType; +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.security.RecordSecurityLock; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import com.kingsrook.qqq.backend.core.model.metadata.tables.UniqueKey; import com.kingsrook.qqq.backend.core.model.metadata.tables.automation.AutomationStatusTracking; @@ -96,7 +100,6 @@ 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.model.session.QSession; import com.kingsrook.qqq.backend.core.modules.authentication.implementations.MockAuthenticationModule; -import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.modules.backend.implementations.memory.MemoryBackendModule; import com.kingsrook.qqq.backend.core.modules.backend.implementations.mock.MockBackendModule; import com.kingsrook.qqq.backend.core.processes.implementations.basepull.BasepullConfiguration; @@ -143,10 +146,12 @@ public class TestUtils public static final String REPORT_NAME_PERSON_SIMPLE = "simplePersonReport"; public static final String REPORT_NAME_PERSON_JOIN_SHAPE = "personJoinShapeReport"; - public static final String POSSIBLE_VALUE_SOURCE_STATE = "state"; // enum-type - public static final String POSSIBLE_VALUE_SOURCE_SHAPE = "shape"; // table-type - public static final String POSSIBLE_VALUE_SOURCE_CUSTOM = "custom"; // custom-type - public static final String POSSIBLE_VALUE_SOURCE_AUTOMATION_STATUS = "automationStatus"; + public static final String POSSIBLE_VALUE_SOURCE_STATE = "state"; // enum-type + public static final String POSSIBLE_VALUE_SOURCE_SHAPE = "shape"; // table-type + public static final String POSSIBLE_VALUE_SOURCE_CUSTOM = "custom"; // custom-type + public static final String POSSIBLE_VALUE_SOURCE_AUTOMATION_STATUS = "automationStatus"; + public static final String POSSIBLE_VALUE_SOURCE_STORE = "store"; + public static final String POSSIBLE_VALUE_SOURCE_INTERNAL_OR_EXTERNAL = "internalOrExternal"; public static final String POLLING_AUTOMATION = "polling"; public static final String DEFAULT_QUEUE_PROVIDER = "defaultQueueProvider"; @@ -154,6 +159,10 @@ public class TestUtils public static final String BASEPULL_KEY_FIELD_NAME = "processName"; public static final String BASEPULL_LAST_RUN_TIME_FIELD_NAME = "lastRunTime"; + 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_INTERNAL_OR_EXTERNAL = "internalOrExternal"; + /******************************************************************************* @@ -183,6 +192,11 @@ public class TestUtils qInstance.addPossibleValueSource(defineStatesPossibleValueSource()); qInstance.addPossibleValueSource(defineShapePossibleValueSource()); qInstance.addPossibleValueSource(defineCustomPossibleValueSource()); + qInstance.addPossibleValueSource(defineStorePossibleValueSource()); + qInstance.addPossibleValueSource(defineStorePossibleValueInternalOrExternal()); + + qInstance.addSecurityKeyType(defineStoreSecurityKeyType()); + qInstance.addSecurityKeyType(defineInternalOrExternalSecurityKeyType()); qInstance.addProcess(defineProcessGreetPeople()); qInstance.addProcess(defineProcessGreetPeopleInteractive()); @@ -398,6 +412,57 @@ public class TestUtils + /******************************************************************************* + ** + *******************************************************************************/ + private static QPossibleValueSource defineStorePossibleValueSource() + { + return new QPossibleValueSource() + .withName(POSSIBLE_VALUE_SOURCE_STORE) + .withType(QPossibleValueSourceType.ENUM) + .withEnumValues(List.of(new QPossibleValue<>(1, "Q-Mart"), new QPossibleValue<>(2, "Tar-que"))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static QPossibleValueSource defineStorePossibleValueInternalOrExternal() + { + return new QPossibleValueSource() + .withName(POSSIBLE_VALUE_SOURCE_INTERNAL_OR_EXTERNAL) + .withType(QPossibleValueSourceType.ENUM) + .withEnumValues(List.of(new QPossibleValue<>("internal", "Internal"), new QPossibleValue<>("external", "External"))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static QSecurityKeyType defineStoreSecurityKeyType() + { + return new QSecurityKeyType() + .withName(SECURITY_KEY_TYPE_STORE) + .withAllAccessKeyName(SECURITY_KEY_TYPE_STORE_ALL_ACCESS) + .withPossibleValueSourceName(POSSIBLE_VALUE_SOURCE_STORE); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static QSecurityKeyType defineInternalOrExternalSecurityKeyType() + { + return new QSecurityKeyType() + .withName(SECURITY_KEY_TYPE_INTERNAL_OR_EXTERNAL) + .withPossibleValueSourceName(POSSIBLE_VALUE_SOURCE_INTERNAL_OR_EXTERNAL); + } + + + /******************************************************************************* ** Define the authentication used in standard tests - using 'mock' type. ** @@ -472,11 +537,19 @@ public class TestUtils .withName(TABLE_NAME_ORDER) .withBackendName(MEMORY_BACKEND_NAME) .withPrimaryKeyField("id") + .withRecordSecurityLock(new RecordSecurityLock() + .withSecurityKeyType(SECURITY_KEY_TYPE_STORE) + .withFieldName("storeId")) .withField(new QFieldMetaData("id", QFieldType.INTEGER).withIsEditable(false)) .withField(new QFieldMetaData("createDate", QFieldType.DATE_TIME).withIsEditable(false)) .withField(new QFieldMetaData("modifyDate", QFieldType.DATE_TIME).withIsEditable(false)) .withField(new QFieldMetaData("orderDate", QFieldType.DATE)) - .withField(new QFieldMetaData("total", QFieldType.DECIMAL).withDisplayFormat(DisplayFormat.CURRENCY)); + .withField(new QFieldMetaData("storeId", QFieldType.INTEGER)) + .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")) + )); } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/collections/MutableListTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/collections/MutableListTest.java new file mode 100644 index 00000000..4fb73c6d --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/collections/MutableListTest.java @@ -0,0 +1,50 @@ +/* + * 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.utils.collections; + + +import java.util.List; +import org.junit.jupiter.api.Test; + + +/******************************************************************************* + ** Unit test for com.kingsrook.qqq.backend.core.utils.collections.MutableList + *******************************************************************************/ +class MutableListTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() + { + List list = new MutableList<>(List.of(1)); + list.add(2); + list.clear(); + + list = new MutableList<>(List.of(3)); + list.add(0, 4); + list.remove(0); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/collections/MutableMapTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/collections/MutableMapTest.java new file mode 100644 index 00000000..f002b67e --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/collections/MutableMapTest.java @@ -0,0 +1,50 @@ +/* + * 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.utils.collections; + + +import java.util.Map; +import org.junit.jupiter.api.Test; + + +/******************************************************************************* + ** Unit test for com.kingsrook.qqq.backend.core.utils.collections.MutableMap + *******************************************************************************/ +class MutableMapTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() + { + MutableMap map = new MutableMap<>(Map.of("a", 1)); + map.clear(); + map.put("b", 2); + + map = new MutableMap<>(Map.of("a", 1)); + map.remove("a"); + map.putAll(Map.of("c", 3, "d", 4)); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/lambdas/LambdasTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/lambdas/LambdasTest.java new file mode 100644 index 00000000..457347fe --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/utils/lambdas/LambdasTest.java @@ -0,0 +1,53 @@ +/* + * 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.utils.lambdas; + + +import org.junit.jupiter.api.Test; + + +/******************************************************************************* + ** Unit test for any simple lambdas we have (kinda just here to ensure test coverage metrics...) + *******************************************************************************/ +class LambdasTest +{ + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void test() + { + runVoidVoidMethod(() -> System.out.println("void!")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private void runVoidVoidMethod(VoidVoidMethod m) + { + m.run(); + } + +} \ No newline at end of file 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 211e09a4..9fb14f37 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 @@ -41,6 +41,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.GroupBy; import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.QFilterOrderByAggregate; import com.kingsrook.qqq.backend.core.model.actions.tables.aggregate.QFilterOrderByGroupBy; import com.kingsrook.qqq.backend.core.model.actions.tables.query.JoinsContext; +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.QFilterOrderBy; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; @@ -51,7 +52,10 @@ import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn; import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType; +import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.core.utils.ValueUtils; @@ -299,11 +303,35 @@ public abstract class AbstractRDBMSAction implements QActionInterface /******************************************************************************* - ** + ** method that sub-classes should call to make a full WHERE clause, including + ** security clauses. *******************************************************************************/ - protected String makeWhereClause(QInstance instance, QTableMetaData table, JoinsContext joinsContext, QQueryFilter filter, List params) throws IllegalArgumentException, QException + protected String makeWhereClause(QInstance instance, QSession session, QTableMetaData table, JoinsContext joinsContext, QQueryFilter filter, List params) throws IllegalArgumentException, QException { - String clause = makeSimpleWhereClause(instance, table, joinsContext, filter.getCriteria(), filter.getBooleanOperator(), params); + String whereClauseWithoutSecurity = makeWhereClauseWithoutSecurity(instance, table, joinsContext, filter, params); + QQueryFilter securityFilter = getSecurityFilter(instance, session, table, joinsContext); + if(securityFilter == null || CollectionUtils.nullSafeIsEmpty(securityFilter.getCriteria())) + { + return (whereClauseWithoutSecurity); + } + String securityWhereClause = getSqlWhereStringAndPopulateParamsListFromNonNestedFilter(instance, table, joinsContext, securityFilter.getCriteria(), QQueryFilter.BooleanOperator.AND, params); + return ("(" + whereClauseWithoutSecurity + ") AND (" + securityWhereClause + ")"); + } + + + + /******************************************************************************* + ** private method for making the part of a where clause that gets AND'ed to the + ** security clause. Recursively handles sub-clauses. + *******************************************************************************/ + private String makeWhereClauseWithoutSecurity(QInstance instance, QTableMetaData table, JoinsContext joinsContext, QQueryFilter filter, List params) throws IllegalArgumentException, QException + { + if(filter == null || !filter.hasAnyCriteria()) + { + return ("1 = 1"); + } + + String clause = getSqlWhereStringAndPopulateParamsListFromNonNestedFilter(instance, table, joinsContext, filter.getCriteria(), filter.getBooleanOperator(), params); if(!CollectionUtils.nullSafeHasContents(filter.getSubFilters())) { /////////////////////////////////////////////////////////////// @@ -322,7 +350,7 @@ public abstract class AbstractRDBMSAction implements QActionInterface } for(QQueryFilter subFilter : filter.getSubFilters()) { - String subClause = makeWhereClause(instance, table, joinsContext, subFilter, params); + String subClause = makeWhereClauseWithoutSecurity(instance, table, joinsContext, subFilter, params); if(StringUtils.hasContent(subClause)) { clauses.add("(" + subClause + ")"); @@ -333,10 +361,129 @@ public abstract class AbstractRDBMSAction implements QActionInterface + /******************************************************************************* + ** Build a QQueryFilter to apply record-level security to the query. + ** Note, it may be empty, if there are no lock fields, or all are all-access. + *******************************************************************************/ + private QQueryFilter getSecurityFilter(QInstance instance, QSession session, QTableMetaData table, JoinsContext joinsContext) + { + QQueryFilter newFilter = new QQueryFilter(); + newFilter.setBooleanOperator(QQueryFilter.BooleanOperator.AND); + List securityCriteria = new ArrayList<>(); + newFilter.setCriteria(securityCriteria); + + for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(table.getRecordSecurityLocks())) + { + addCriteriaForRecordSecurityLock(instance, session, table, securityCriteria, recordSecurityLock, joinsContext, table.getName()); + } + + for(QueryJoin queryJoin : CollectionUtils.nonNullList(joinsContext.getQueryJoins())) + { + QTableMetaData joinTable = instance.getTable(queryJoin.getJoinTable()); + for(RecordSecurityLock recordSecurityLock : CollectionUtils.nonNullList(joinTable.getRecordSecurityLocks())) + { + addCriteriaForRecordSecurityLock(instance, session, joinTable, securityCriteria, recordSecurityLock, joinsContext, queryJoin.getJoinTableOrItsAlias()); + } + } + + return (newFilter); + } + + + /******************************************************************************* ** *******************************************************************************/ - private String makeSimpleWhereClause(QInstance instance, QTableMetaData table, JoinsContext joinsContext, List criteria, QQueryFilter.BooleanOperator booleanOperator, List params) throws IllegalArgumentException + private static void addCriteriaForRecordSecurityLock(QInstance instance, QSession session, QTableMetaData table, List securityCriteria, RecordSecurityLock recordSecurityLock, JoinsContext joinsContext, String tableNameOrAlias) + { + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // check if the key type has an all-access key, and if so, if it's set to true for the current user/session // + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + QSecurityKeyType securityKeyType = instance.getSecurityKeyType(recordSecurityLock.getSecurityKeyType()); + if(StringUtils.hasContent(securityKeyType.getAllAccessKeyName())) + { + if(session.hasSecurityKeyValue(securityKeyType.getAllAccessKeyName(), true, QFieldType.BOOLEAN)) + { + /////////////////////////////////////////////////////////////////////////////// + // if we have all-access on this key, then we don't need a criterion for it. // + /////////////////////////////////////////////////////////////////////////////// + return; + } + } + + if(CollectionUtils.nullSafeHasContents(recordSecurityLock.getJoinChain())) + { + for(String joinName : recordSecurityLock.getJoinChain()) + { + QJoinMetaData joinMetaData = instance.getJoin(joinName); + + /* + for(QueryJoin queryJoin : joinsContext.getQueryJoins()) + { + if(queryJoin.getJoinMetaData().getName().equals(joinName)) + { + joinMetaData = queryJoin.getJoinMetaData(); + break; + } + } + */ + + if(joinMetaData == null) + { + throw (new RuntimeException("Could not find joinMetaData for recordSecurityLock with joinChain member [" + joinName + "]")); + } + + table = instance.getTable(joinMetaData.getRightTable()); + tableNameOrAlias = table.getName(); + } + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // else - get the key values from the session and decide what kind of criterion to build // + /////////////////////////////////////////////////////////////////////////////////////////// + List securityKeyValues = session.getSecurityKeyValues(recordSecurityLock.getSecurityKeyType(), table.getField(recordSecurityLock.getFieldName()).getType()); + String fieldName = tableNameOrAlias + "." + recordSecurityLock.getFieldName(); + if(CollectionUtils.nullSafeIsEmpty(securityKeyValues)) + { + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // 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())) + { + securityCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IS_BLANK)); + } + else + { + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // else, if no user/session values, and null-value behavior is deny, then setup a FALSE condition, to allow no rows. // + // todo - make some explicit contradiction here - maybe even avoid running the whole query - as you're not allowed ANY records // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + securityCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IN, Collections.emptyList())); + } + } + else + { + ////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // 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())) + { + securityCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IS_NULL_OR_IN, securityKeyValues)); + } + else + { + securityCriteria.add(new QFilterCriteria(fieldName, QCriteriaOperator.IN, securityKeyValues)); + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private String getSqlWhereStringAndPopulateParamsListFromNonNestedFilter(QInstance instance, QTableMetaData table, JoinsContext joinsContext, List criteria, QQueryFilter.BooleanOperator booleanOperator, List params) throws IllegalArgumentException { List clauses = new ArrayList<>(); for(QFilterCriteria criterion : criteria) @@ -366,10 +513,10 @@ public abstract class AbstractRDBMSAction implements QActionInterface { if(values.isEmpty()) { - ////////////////////////////////////////////////////////////////////////////////// - // if there are no values, then we want a false here - so say column != column. // - ////////////////////////////////////////////////////////////////////////////////// - clause += " != " + column; + /////////////////////////////////////////////////////// + // if there are no values, then we want a false here // + /////////////////////////////////////////////////////// + clause = " 0 = 1 "; } else { @@ -377,14 +524,24 @@ public abstract class AbstractRDBMSAction implements QActionInterface } break; } + case IS_NULL_OR_IN: + { + clause += " IS NULL "; + + if(!values.isEmpty()) + { + clause += " OR " + column + " IN (" + values.stream().map(x -> "?").collect(Collectors.joining(",")) + ")"; + } + break; + } case NOT_IN: { if(values.isEmpty()) { - ///////////////////////////////////////////////////////////////////////////////// - // if there are no values, then we want a true here - so say column == column. // - ///////////////////////////////////////////////////////////////////////////////// - clause += " = " + column; + ////////////////////////////////////////////////////// + // if there are no values, then we want a true here // + ////////////////////////////////////////////////////// + clause = " 1 = 1 "; } else { diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSAggregateAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSAggregateAction.java index d35557b9..5fe69135 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSAggregateAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSAggregateAction.java @@ -74,10 +74,7 @@ public class RDBMSAggregateAction extends AbstractRDBMSAction implements Aggrega QQueryFilter filter = aggregateInput.getFilter(); List params = new ArrayList<>(); - if(filter != null && filter.hasAnyCriteria()) - { - sql += " WHERE " + makeWhereClause(aggregateInput.getInstance(), table, joinsContext, filter, params); - } + sql += " WHERE " + makeWhereClause(aggregateInput.getInstance(), aggregateInput.getSession(), table, joinsContext, filter, params); if(CollectionUtils.nullSafeHasContents(aggregateInput.getGroupBys())) { diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java index c3ae3dea..e078cfee 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountAction.java @@ -64,10 +64,7 @@ public class RDBMSCountAction extends AbstractRDBMSAction implements CountInterf QQueryFilter filter = countInput.getFilter(); List params = new ArrayList<>(); - if(filter != null && filter.hasAnyCriteria()) - { - sql += " WHERE " + makeWhereClause(countInput.getInstance(), table, joinsContext, filter, params); - } + sql += " WHERE " + makeWhereClause(countInput.getInstance(), countInput.getSession(), table, joinsContext, filter, params); // todo sql customization - can edit sql and/or param list diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteAction.java index 2b45c05e..bf6a20b0 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSDeleteAction.java @@ -262,7 +262,7 @@ public class RDBMSDeleteAction extends AbstractRDBMSAction implements DeleteInte String tableName = getTableName(table); JoinsContext joinsContext = new JoinsContext(deleteInput.getInstance(), table.getName(), Collections.emptyList()); - String whereClause = makeWhereClause(deleteInput.getInstance(), table, joinsContext, filter, params); + String whereClause = makeWhereClause(deleteInput.getInstance(), deleteInput.getSession(), table, joinsContext, filter, params); // todo sql customization - can edit sql and/or param list? String sql = "DELETE FROM " diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java index 29342ade..f4a5665d 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java @@ -77,10 +77,7 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf QQueryFilter filter = queryInput.getFilter(); List params = new ArrayList<>(); - if(filter != null && filter.hasAnyCriteria()) - { - sql.append(" WHERE ").append(makeWhereClause(queryInput.getInstance(), table, joinsContext, filter, params)); - } + sql.append(" WHERE ").append(makeWhereClause(queryInput.getInstance(), queryInput.getSession(), table, joinsContext, filter, params)); if(filter != null && CollectionUtils.nullSafeHasContents(filter.getOrderBys())) { diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java index 84aff27d..48156e79 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/TestUtils.java @@ -27,6 +27,7 @@ import java.sql.Connection; import java.util.List; import com.kingsrook.qqq.backend.core.model.metadata.QAuthenticationType; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData; 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; @@ -35,8 +36,9 @@ import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.PVSValueFormatAndFields; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource; import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType; +import com.kingsrook.qqq.backend.core.model.metadata.security.QSecurityKeyType; +import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock; import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; -import com.kingsrook.qqq.backend.core.model.metadata.authentication.QAuthenticationMetaData; import com.kingsrook.qqq.backend.module.rdbms.actions.RDBMSActionTest; import com.kingsrook.qqq.backend.module.rdbms.jdbc.ConnectionManager; import com.kingsrook.qqq.backend.module.rdbms.jdbc.QueryManager; @@ -60,6 +62,8 @@ public class TestUtils public static final String TABLE_NAME_ITEM = "item"; public static final String TABLE_NAME_ORDER_LINE = "orderLine"; + public static final String SECURITY_KEY_STORE_ALL_ACCESS = "storeAllAccess"; + /******************************************************************************* @@ -219,21 +223,25 @@ public class TestUtils qInstance.addTable(defineBaseTable(TABLE_NAME_STORE, "store") .withRecordLabelFormat("%s") .withRecordLabelFields("name") + .withRecordSecurityLock(new RecordSecurityLock().withSecurityKeyType(TABLE_NAME_STORE).withFieldName("id")) .withField(new QFieldMetaData("name", QFieldType.STRING)) ); qInstance.addTable(defineBaseTable(TABLE_NAME_ORDER, "order") + .withRecordSecurityLock(new RecordSecurityLock().withSecurityKeyType(TABLE_NAME_STORE).withFieldName("storeId")) .withField(new QFieldMetaData("storeId", QFieldType.INTEGER).withBackendName("store_id").withPossibleValueSourceName(TABLE_NAME_STORE)) .withField(new QFieldMetaData("billToPersonId", QFieldType.INTEGER).withBackendName("bill_to_person_id").withPossibleValueSourceName(TABLE_NAME_PERSON)) .withField(new QFieldMetaData("shipToPersonId", QFieldType.INTEGER).withBackendName("ship_to_person_id").withPossibleValueSourceName(TABLE_NAME_PERSON)) ); qInstance.addTable(defineBaseTable(TABLE_NAME_ITEM, "item") + .withRecordSecurityLock(new RecordSecurityLock().withSecurityKeyType(TABLE_NAME_STORE).withFieldName("storeId")) .withField(new QFieldMetaData("sku", QFieldType.STRING)) .withField(new QFieldMetaData("storeId", QFieldType.INTEGER).withBackendName("store_id").withPossibleValueSourceName(TABLE_NAME_STORE)) ); qInstance.addTable(defineBaseTable(TABLE_NAME_ORDER_LINE, "order_line") + .withRecordSecurityLock(new RecordSecurityLock().withSecurityKeyType(TABLE_NAME_STORE).withFieldName("storeId")) .withField(new QFieldMetaData("orderId", QFieldType.INTEGER).withBackendName("order_id")) .withField(new QFieldMetaData("sku", QFieldType.STRING)) .withField(new QFieldMetaData("storeId", QFieldType.INTEGER).withBackendName("store_id").withPossibleValueSourceName(TABLE_NAME_STORE)) @@ -295,6 +303,11 @@ public class TestUtils .withTableName(TABLE_NAME_STORE) .withValueFormatAndFields(PVSValueFormatAndFields.LABEL_ONLY) ); + + qInstance.addSecurityKeyType(new QSecurityKeyType() + .withName(TABLE_NAME_STORE) + .withAllAccessKeyName(SECURITY_KEY_STORE_ALL_ACCESS) + .withPossibleValueSourceName(TABLE_NAME_STORE)); } diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSAggregateActionTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSAggregateActionTest.java index 9ca6b180..33ff54d2 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSAggregateActionTest.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSAggregateActionTest.java @@ -48,6 +48,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -318,7 +319,18 @@ public class RDBMSAggregateActionTest extends RDBMSActionTest AggregateOutput aggregateOutput = new RDBMSAggregateAction().execute(aggregateInput); AggregateResult aggregateResult = aggregateOutput.getResults().get(0); + assertNull(aggregateResult.getAggregateValue(sumOfQuantity)); + + aggregateInput.setSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true)); + aggregateOutput = new RDBMSAggregateAction().execute(aggregateInput); + aggregateResult = aggregateOutput.getResults().get(0); Assertions.assertEquals(43, aggregateResult.getAggregateValue(sumOfQuantity)); + + aggregateInput.setSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1)); + aggregateOutput = new RDBMSAggregateAction().execute(aggregateInput); + aggregateResult = aggregateOutput.getResults().get(0); + // note - this would be 33, except for that one order line that has a contradictory store id... + Assertions.assertEquals(32, aggregateResult.getAggregateValue(sumOfQuantity)); } @@ -340,7 +352,10 @@ public class RDBMSAggregateActionTest extends RDBMSActionTest aggregateInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ORDER, TestUtils.TABLE_NAME_ORDER_LINE)); AggregateOutput aggregateOutput = new RDBMSAggregateAction().execute(aggregateInput); - assertEquals(6, aggregateOutput.getResults().size()); + assertEquals(0, aggregateOutput.getResults().size()); + + aggregateInput.setSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true)); + aggregateOutput = new RDBMSAggregateAction().execute(aggregateInput); assertSkuQuantity("QM-1", 30, aggregateOutput.getResults(), groupBy); assertSkuQuantity("QM-2", 1, aggregateOutput.getResults(), groupBy); assertSkuQuantity("QM-3", 1, aggregateOutput.getResults(), groupBy); diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountActionTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountActionTest.java index ef49e90c..561ce70c 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountActionTest.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSCountActionTest.java @@ -35,6 +35,7 @@ import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.module.rdbms.TestUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -179,4 +180,26 @@ public class RDBMSCountActionTest extends RDBMSActionTest assertEquals(2, countOutput.getCount(), "Right Join count should find 2 rows"); } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testRecordSecurity() throws QException + { + CountInput countInput = new CountInput(); + countInput.setInstance(TestUtils.defineInstance()); + countInput.setTableName(TestUtils.TABLE_NAME_ORDER); + + countInput.setSession(new QSession()); + assertThat(new CountAction().execute(countInput).getCount()).isEqualTo(0); + + countInput.setSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true)); + assertThat(new CountAction().execute(countInput).getCount()).isEqualTo(8); + + countInput.setSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, List.of(2, 3))); + assertThat(new CountAction().execute(countInput).getCount()).isEqualTo(5); + } + } \ No newline at end of file diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java index 4a42ee15..14db43a7 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryActionTest.java @@ -22,8 +22,11 @@ package com.kingsrook.qqq.backend.module.rdbms.actions; +import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; @@ -38,6 +41,7 @@ import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryJoin; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.security.RecordSecurityLock; import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.utils.StringUtils; import com.kingsrook.qqq.backend.module.rdbms.TestUtils; @@ -605,6 +609,38 @@ public class RDBMSQueryActionTest extends RDBMSActionTest + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testNestedFilterAndTopLevelFilter() throws QException + { + QueryInput queryInput = initQueryRequest(); + queryInput.setFilter(new QQueryFilter() + .withCriteria(new QFilterCriteria("id", QCriteriaOperator.EQUALS, 3)) + .withBooleanOperator(QQueryFilter.BooleanOperator.AND) + .withSubFilters(List.of( + new QQueryFilter() + .withBooleanOperator(QQueryFilter.BooleanOperator.OR) + .withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, List.of("James"))) + .withCriteria(new QFilterCriteria("firstName", QCriteriaOperator.EQUALS, List.of("Tim"))), + new QQueryFilter() + .withBooleanOperator(QQueryFilter.BooleanOperator.OR) + .withCriteria(new QFilterCriteria("lastName", QCriteriaOperator.EQUALS, List.of("Kelkhoff"))) + .withCriteria(new QFilterCriteria("lastName", QCriteriaOperator.EQUALS, List.of("Chamberlain"))) + )) + ); + QueryOutput queryOutput = new QueryAction().execute(queryInput); + assertEquals(1, queryOutput.getRecords().size(), "Complex query should find 1 row"); + assertThat(queryOutput.getRecords()).anyMatch(r -> r.getValueInteger("id").equals(3) && r.getValueString("firstName").equals("Tim") && r.getValueString("lastName").equals("Chamberlain")); + + queryInput.getFilter().setCriteria(List.of(new QFilterCriteria("id", QCriteriaOperator.NOT_EQUALS, 3))); + queryOutput = new QueryAction().execute(queryInput); + assertEquals(0, queryOutput.getRecords().size(), "Next complex query should find 0 rows"); + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -712,7 +748,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest @Test void testFiveTableOmsJoinFindMismatchedStoreId() throws Exception { - QueryInput queryInput = new QueryInput(TestUtils.defineInstance(), new QSession()); + QueryInput queryInput = new QueryInput(TestUtils.defineInstance(), new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true)); queryInput.setTableName(TestUtils.TABLE_NAME_ORDER); queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ORDER, TestUtils.TABLE_NAME_STORE).withAlias("orderStore").withSelect(true)); queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ORDER, TestUtils.TABLE_NAME_ORDER_LINE).withSelect(true)); @@ -754,7 +790,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest orderLineCount.set(rs.getInt(1)); }); - QueryInput queryInput = new QueryInput(TestUtils.defineInstance(), new QSession()); + QueryInput queryInput = new QueryInput(TestUtils.defineInstance(), new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true)); queryInput.setTableName(TestUtils.TABLE_NAME_ORDER_LINE); queryInput.withQueryJoin(new QueryJoin(TestUtils.TABLE_NAME_ORDER).withSelect(true)); @@ -775,7 +811,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest void testOmsQueryByPersons() throws Exception { QInstance instance = TestUtils.defineInstance(); - QueryInput queryInput = new QueryInput(instance, new QSession()); + QueryInput queryInput = new QueryInput(instance, new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true)); queryInput.setTableName(TestUtils.TABLE_NAME_ORDER); ///////////////////////////////////////////////////// @@ -877,7 +913,7 @@ public class RDBMSQueryActionTest extends RDBMSActionTest void testOmsQueryByPersonsExtraKelkhoffOrder() throws Exception { QInstance instance = TestUtils.defineInstance(); - QueryInput queryInput = new QueryInput(instance, new QSession()); + QueryInput queryInput = new QueryInput(instance, new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true)); queryInput.setTableName(TestUtils.TABLE_NAME_ORDER); //////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -953,4 +989,330 @@ public class RDBMSQueryActionTest extends RDBMSActionTest .hasRootCauseMessage("Duplicate table name or alias: shipToPerson"); } + + + /******************************************************************************* + ** queries on the store table, where the primary key (id) is the security field + *******************************************************************************/ + @Test + void testRecordSecurityPrimaryKeyFieldNoFilters() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + QueryInput queryInput = new QueryInput(qInstance); + queryInput.setTableName(TestUtils.TABLE_NAME_STORE); + + queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true)); + assertThat(new QueryAction().execute(queryInput).getRecords()).hasSize(3); + + queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1)); + assertThat(new QueryAction().execute(queryInput).getRecords()) + .hasSize(1) + .anyMatch(r -> r.getValueInteger("id").equals(1)); + + queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 2)); + assertThat(new QueryAction().execute(queryInput).getRecords()) + .hasSize(1) + .anyMatch(r -> r.getValueInteger("id").equals(2)); + + queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 5)); + assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty(); + + queryInput.setSession(new QSession()); + assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty(); + + queryInput.setSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, null)); + assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty(); + + queryInput.setSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, Collections.emptyList())); + assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty(); + + queryInput.setSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, List.of(1, 3))); + assertThat(new QueryAction().execute(queryInput).getRecords()) + .hasSize(2) + .anyMatch(r -> r.getValueInteger("id").equals(1)) + .anyMatch(r -> r.getValueInteger("id").equals(3)); + } + + + + /******************************************************************************* + ** not really expected to be any different from where we filter on the primary key, + ** but just good to make sure + *******************************************************************************/ + @Test + void testRecordSecurityForeignKeyFieldNoFilters() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + QueryInput queryInput = new QueryInput(qInstance); + queryInput.setTableName(TestUtils.TABLE_NAME_ORDER); + + queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true)); + assertThat(new QueryAction().execute(queryInput).getRecords()).hasSize(8); + + queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1)); + assertThat(new QueryAction().execute(queryInput).getRecords()) + .hasSize(3) + .allMatch(r -> r.getValueInteger("storeId").equals(1)); + + queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 2)); + assertThat(new QueryAction().execute(queryInput).getRecords()) + .hasSize(2) + .allMatch(r -> r.getValueInteger("storeId").equals(2)); + + queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 5)); + assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty(); + + queryInput.setSession(new QSession()); + assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty(); + + queryInput.setSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, null)); + assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty(); + + queryInput.setSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, Collections.emptyList())); + assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty(); + + queryInput.setSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, List.of(1, 3))); + assertThat(new QueryAction().execute(queryInput).getRecords()) + .hasSize(6) + .allMatch(r -> r.getValueInteger("storeId").equals(1) || r.getValueInteger("storeId").equals(3)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testRecordSecurityWithFilters() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + QueryInput queryInput = new QueryInput(qInstance); + queryInput.setTableName(TestUtils.TABLE_NAME_ORDER); + + queryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.BETWEEN, List.of(2, 7)))); + queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true)); + assertThat(new QueryAction().execute(queryInput).getRecords()).hasSize(6); + + queryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.BETWEEN, List.of(2, 7)))); + queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1)); + assertThat(new QueryAction().execute(queryInput).getRecords()) + .hasSize(2) + .allMatch(r -> r.getValueInteger("storeId").equals(1)); + + queryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.BETWEEN, List.of(2, 7)))); + queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 5)); + assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty(); + + queryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.BETWEEN, List.of(2, 7)))); + queryInput.setSession(new QSession()); + assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty(); + + queryInput.setFilter(new QQueryFilter(new QFilterCriteria("storeId", QCriteriaOperator.IN, List.of(1, 2)))); + queryInput.setSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, List.of(1, 3))); + assertThat(new QueryAction().execute(queryInput).getRecords()) + .hasSize(3) + .allMatch(r -> r.getValueInteger("storeId").equals(1)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testRecordSecurityWithOrQueries() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + QueryInput queryInput = new QueryInput(qInstance); + queryInput.setTableName(TestUtils.TABLE_NAME_ORDER); + + queryInput.setFilter(new QQueryFilter( + new QFilterCriteria("billToPersonId", QCriteriaOperator.EQUALS, List.of(1)), + new QFilterCriteria("shipToPersonId", QCriteriaOperator.EQUALS, List.of(5)) + ).withBooleanOperator(QQueryFilter.BooleanOperator.OR)); + queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true)); + assertThat(new QueryAction().execute(queryInput).getRecords()) + .hasSize(5) + .allMatch(r -> Objects.equals(r.getValueInteger("billToPersonId"), 1) || Objects.equals(r.getValueInteger("shipToPersonId"), 5)); + + queryInput.setFilter(new QQueryFilter( + new QFilterCriteria("billToPersonId", QCriteriaOperator.EQUALS, List.of(1)), + new QFilterCriteria("shipToPersonId", QCriteriaOperator.EQUALS, List.of(5)) + ).withBooleanOperator(QQueryFilter.BooleanOperator.OR)); + queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 2)); + assertThat(new QueryAction().execute(queryInput).getRecords()) + .hasSize(1) + .allMatch(r -> r.getValueInteger("storeId").equals(2)) + .allMatch(r -> Objects.equals(r.getValueInteger("billToPersonId"), 1) || Objects.equals(r.getValueInteger("shipToPersonId"), 5)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testRecordSecurityWithSubFilters() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + QueryInput queryInput = new QueryInput(qInstance); + queryInput.setTableName(TestUtils.TABLE_NAME_ORDER); + + queryInput.setFilter(new QQueryFilter() + .withBooleanOperator(QQueryFilter.BooleanOperator.OR) + .withSubFilters(List.of( + new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.GREATER_THAN_OR_EQUALS, 2), new QFilterCriteria("billToPersonId", QCriteriaOperator.EQUALS, 1)), + new QQueryFilter(new QFilterCriteria("billToPersonId", QCriteriaOperator.IS_BLANK), new QFilterCriteria("shipToPersonId", QCriteriaOperator.IS_BLANK)).withBooleanOperator(QQueryFilter.BooleanOperator.OR) + ))); + Predicate p = r -> r.getValueInteger("billToPersonId") == null || r.getValueInteger("shipToPersonId") == null || (r.getValueInteger("id") >= 2 && r.getValueInteger("billToPersonId") == 1); + + queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true)); + assertThat(new QueryAction().execute(queryInput).getRecords()) + .hasSize(4) + .allMatch(p); + + queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1)); + assertThat(new QueryAction().execute(queryInput).getRecords()) + .hasSize(1) + .allMatch(r -> r.getValueInteger("storeId").equals(1)) + .allMatch(p); + + queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 3)); + assertThat(new QueryAction().execute(queryInput).getRecords()) + .hasSize(3) + .allMatch(r -> r.getValueInteger("storeId").equals(3)) + .allMatch(p); + + queryInput.setSession(new QSession()); + assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty(); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testRecordSecurityNullValues() throws Exception + { + runTestSql("INSERT INTO `order` (id, store_id, bill_to_person_id, ship_to_person_id) VALUES (9, NULL, 1, 6)", null); + runTestSql("INSERT INTO `order` (id, store_id, bill_to_person_id, ship_to_person_id) VALUES (10, NULL, 6, 5)", null); + + QInstance qInstance = TestUtils.defineInstance(); + QueryInput queryInput = new QueryInput(qInstance); + queryInput.setTableName(TestUtils.TABLE_NAME_ORDER); + + Predicate hasNullStoreId = r -> r.getValueInteger("storeId") == null; + + //////////////////////////////////////////// + // all-access user should get all 10 rows // + //////////////////////////////////////////// + queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true)); + assertThat(new QueryAction().execute(queryInput).getRecords()) + .hasSize(10) + .anyMatch(hasNullStoreId); + + ////////////////////////////////////////////////////////////////////////////////////////////////// + // no-values user should get 0 rows (given that default null-behavior on this key type is DENY) // + ////////////////////////////////////////////////////////////////////////////////////////////////// + queryInput.setSession(new QSession()); + assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty(); + + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + // user with list of all ids shouldn't see the nulls (given that default null-behavior on this key type is DENY) // + /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + queryInput.setSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, List.of(1, 2, 3, 4, 5))); + assertThat(new QueryAction().execute(queryInput).getRecords()) + .hasSize(8) + .noneMatch(hasNullStoreId); + + ////////////////////////////////////////////////////////////////////////// + // specifically set the null behavior to deny - repeat the last 2 tests // + ////////////////////////////////////////////////////////////////////////// + qInstance.getTable(TestUtils.TABLE_NAME_ORDER).getRecordSecurityLocks().get(0).setNullValueBehavior(RecordSecurityLock.NullValueBehavior.DENY); + + queryInput.setSession(new QSession()); + assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty(); + + queryInput.setSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, List.of(1, 2, 3, 4, 5))); + assertThat(new QueryAction().execute(queryInput).getRecords()) + .hasSize(8) + .noneMatch(hasNullStoreId); + + /////////////////////////////////// + // change null behavior to ALLOW // + /////////////////////////////////// + qInstance.getTable(TestUtils.TABLE_NAME_ORDER).getRecordSecurityLocks().get(0).setNullValueBehavior(RecordSecurityLock.NullValueBehavior.ALLOW); + + ///////////////////////////////////////////// + // all-access user should still get all 10 // + ///////////////////////////////////////////// + queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true)); + assertThat(new QueryAction().execute(queryInput).getRecords()) + .hasSize(10) + .anyMatch(hasNullStoreId); + + ///////////////////////////////////////////////////// + // no-values user should only get the rows w/ null // + ///////////////////////////////////////////////////// + queryInput.setSession(new QSession()); + assertThat(new QueryAction().execute(queryInput).getRecords()) + .hasSize(2) + .allMatch(hasNullStoreId); + + //////////////////////////////////////////////////// + // user with list of all ids should see the nulls // + //////////////////////////////////////////////////// + queryInput.setSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, List.of(1, 2, 3, 4, 5))); + assertThat(new QueryAction().execute(queryInput).getRecords()) + .hasSize(10) + .anyMatch(hasNullStoreId); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testRecordSecurityWithLockFromJoinTable() throws QException + { + QInstance qInstance = TestUtils.defineInstance(); + QueryInput queryInput = new QueryInput(qInstance); + queryInput.setTableName(TestUtils.TABLE_NAME_ORDER); + + ///////////////////////////////////////////////////////////////////////////////////////////////// + // remove the normal lock on the order table - replace it with one from the joined store table // + ///////////////////////////////////////////////////////////////////////////////////////////////// + qInstance.getTable(TestUtils.TABLE_NAME_ORDER).getRecordSecurityLocks().clear(); + qInstance.getTable(TestUtils.TABLE_NAME_ORDER).withRecordSecurityLock(new RecordSecurityLock() + .withSecurityKeyType(TestUtils.TABLE_NAME_STORE) + .withJoinChain(List.of("orderJoinStore")) + .withFieldName("id")); + + queryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.BETWEEN, List.of(2, 7)))); + queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true)); + assertThat(new QueryAction().execute(queryInput).getRecords()).hasSize(6); + + queryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.BETWEEN, List.of(2, 7)))); + queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 1)); + assertThat(new QueryAction().execute(queryInput).getRecords()) + .hasSize(2) + .allMatch(r -> r.getValueInteger("storeId").equals(1)); + + queryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.BETWEEN, List.of(2, 7)))); + queryInput.setSession(new QSession().withSecurityKeyValue(TestUtils.TABLE_NAME_STORE, 5)); + assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty(); + + queryInput.setFilter(new QQueryFilter(new QFilterCriteria("id", QCriteriaOperator.BETWEEN, List.of(2, 7)))); + queryInput.setSession(new QSession()); + assertThat(new QueryAction().execute(queryInput).getRecords()).isEmpty(); + + queryInput.setFilter(new QQueryFilter(new QFilterCriteria("storeId", QCriteriaOperator.IN, List.of(1, 2)))); + queryInput.setSession(new QSession().withSecurityKeyValues(TestUtils.TABLE_NAME_STORE, List.of(1, 3))); + assertThat(new QueryAction().execute(queryInput).getRecords()) + .hasSize(3) + .allMatch(r -> r.getValueInteger("storeId").equals(1)); + } + } \ No newline at end of file diff --git a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/reporting/GenerateReportActionRDBMSTest.java b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/reporting/GenerateReportActionRDBMSTest.java index 3386d340..b70553c6 100644 --- a/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/reporting/GenerateReportActionRDBMSTest.java +++ b/qqq-backend-module-rdbms/src/test/java/com/kingsrook/qqq/backend/module/rdbms/reporting/GenerateReportActionRDBMSTest.java @@ -196,7 +196,7 @@ public class GenerateReportActionRDBMSTest extends RDBMSActionTest private String runReport(QInstance qInstance) throws QException { ReportInput reportInput = new ReportInput(qInstance); - reportInput.setSession(new QSession()); + reportInput.setSession(new QSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_STORE_ALL_ACCESS, true)); reportInput.setReportName(TEST_REPORT); reportInput.setReportFormat(ReportFormat.CSV); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index 76717424..a0b302d0 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -41,6 +41,9 @@ import com.kingsrook.qqq.backend.core.actions.dashboard.RenderWidgetAction; import com.kingsrook.qqq.backend.core.actions.metadata.MetaDataAction; import com.kingsrook.qqq.backend.core.actions.metadata.ProcessMetaDataAction; import com.kingsrook.qqq.backend.core.actions.metadata.TableMetaDataAction; +import com.kingsrook.qqq.backend.core.actions.permissions.PermissionCheckResult; +import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper; +import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType; import com.kingsrook.qqq.backend.core.actions.reporting.ExportAction; import com.kingsrook.qqq.backend.core.actions.tables.CountAction; import com.kingsrook.qqq.backend.core.actions.tables.DeleteAction; @@ -54,6 +57,7 @@ import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; import com.kingsrook.qqq.backend.core.exceptions.QInstanceValidationException; import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException; import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; +import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; import com.kingsrook.qqq.backend.core.exceptions.QValueException; import com.kingsrook.qqq.backend.core.instances.QInstanceValidator; @@ -86,6 +90,7 @@ import com.kingsrook.qqq.backend.core.model.actions.widgets.RenderWidgetOutput; 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.fields.QFieldMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; 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.modules.authentication.QAuthenticationModuleDispatcher; @@ -331,7 +336,6 @@ public class QJavalinImplementation QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher(); QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(input.getAuthenticationMetaData()); - boolean needToSetSessionIdCookie = false; try { Map authenticationContext = new HashMap<>(); @@ -345,7 +349,6 @@ public class QJavalinImplementation // first, look for a sessionId cookie // //////////////////////////////////////// authenticationContext.put(SESSION_ID_COOKIE_NAME, sessionIdCookieValue); - needToSetSessionIdCookie = true; } else if(authorizationHeaderValue != null) { @@ -360,7 +363,6 @@ public class QJavalinImplementation { authorizationHeaderValue = authorizationHeaderValue.replaceFirst(basicPrefix, ""); authenticationContext.put(BASIC_AUTH_NAME, authorizationHeaderValue); - needToSetSessionIdCookie = true; } else if(authorizationHeaderValue.startsWith(bearerPrefix)) { @@ -383,7 +385,7 @@ public class QJavalinImplementation ///////////////////////////////////////////////////////////////////////////////// // if we got a session id cookie in, then send it back with updated cookie age // ///////////////////////////////////////////////////////////////////////////////// - if(needToSetSessionIdCookie) + if(authenticationModule.usesSessionIdCookie()) { context.cookie(SESSION_ID_COOKIE_NAME, session.getIdReference(), SESSION_COOKIE_AGE); } @@ -395,7 +397,7 @@ public class QJavalinImplementation //////////////////////////////////////////////////////////////////////////////// // if exception caught, clear out the cookie so the frontend will reauthorize // //////////////////////////////////////////////////////////////////////////////// - if(needToSetSessionIdCookie) + if(authenticationModule.usesSessionIdCookie()) { context.removeCookie(SESSION_ID_COOKIE_NAME); } @@ -446,6 +448,8 @@ public class QJavalinImplementation deleteInput.setTableName(table); deleteInput.setPrimaryKeys(primaryKeys); + PermissionsHelper.checkTablePermissionThrowing(deleteInput, TablePermissionSubType.DELETE); + DeleteAction deleteAction = new DeleteAction(); DeleteOutput deleteResult = deleteAction.execute(deleteInput); @@ -466,7 +470,14 @@ public class QJavalinImplementation { try { - String table = context.pathParam("table"); + String table = context.pathParam("table"); + + UpdateInput updateInput = new UpdateInput(qInstance); + setupSession(context, updateInput); + updateInput.setTableName(table); + + PermissionsHelper.checkTablePermissionThrowing(updateInput, TablePermissionSubType.EDIT); + List recordList = new ArrayList<>(); QRecord record = new QRecord(); record.setTableName(table); @@ -494,12 +505,8 @@ public class QJavalinImplementation } QTableMetaData tableMetaData = qInstance.getTable(table); - record.setValue(tableMetaData.getPrimaryKeyField(), context.pathParam("primaryKey")); - UpdateInput updateInput = new UpdateInput(qInstance); - setupSession(context, updateInput); - updateInput.setTableName(table); updateInput.setRecords(recordList); UpdateAction updateAction = new UpdateAction(); @@ -522,7 +529,13 @@ public class QJavalinImplementation { try { - String table = context.pathParam("table"); + String table = context.pathParam("table"); + InsertInput insertInput = new InsertInput(qInstance); + setupSession(context, insertInput); + insertInput.setTableName(table); + + PermissionsHelper.checkTablePermissionThrowing(insertInput, TablePermissionSubType.INSERT); + List recordList = new ArrayList<>(); QRecord record = new QRecord(); record.setTableName(table); @@ -536,10 +549,6 @@ public class QJavalinImplementation record.setValue(String.valueOf(entry.getKey()), (Serializable) entry.getValue()); } } - - InsertInput insertInput = new InsertInput(qInstance); - setupSession(context, insertInput); - insertInput.setTableName(table); insertInput.setRecords(recordList); InsertAction insertAction = new InsertAction(); @@ -577,6 +586,8 @@ public class QJavalinImplementation getInput.setShouldGenerateDisplayValues(true); getInput.setShouldTranslatePossibleValues(true); + PermissionsHelper.checkTablePermissionThrowing(getInput, TablePermissionSubType.READ); + // todo - validate that the primary key is of the proper type (e.g,. not a string for an id field) // and throw a 400-series error (tell the user bad-request), rather than, we're doing a 500 (server error) @@ -624,6 +635,8 @@ public class QJavalinImplementation setupSession(context, countInput); countInput.setTableName(context.pathParam("table")); + PermissionsHelper.checkTablePermissionThrowing(countInput, TablePermissionSubType.READ); + String filter = stringQueryParam(context, "filter"); if(!StringUtils.hasContent(filter)) { @@ -673,6 +686,8 @@ public class QJavalinImplementation queryInput.setSkip(integerQueryParam(context, "skip")); queryInput.setLimit(integerQueryParam(context, "limit")); + PermissionsHelper.checkTablePermissionThrowing(queryInput, TablePermissionSubType.READ); + String filter = stringQueryParam(context, "filter"); if(!StringUtils.hasContent(filter)) { @@ -727,7 +742,22 @@ public class QJavalinImplementation { TableMetaDataInput tableMetaDataInput = new TableMetaDataInput(qInstance); setupSession(context, tableMetaDataInput); - tableMetaDataInput.setTableName(context.pathParam("table")); + + String tableName = context.pathParam("table"); + QTableMetaData table = qInstance.getTable(tableName); + if(table == null) + { + throw (new QNotFoundException("Table [" + tableName + "] was not found.")); + } + + PermissionCheckResult permissionCheckResult = PermissionsHelper.getPermissionCheckResult(tableMetaDataInput, table); + if(permissionCheckResult.equals(PermissionCheckResult.DENY_HIDE)) + { + // not found? or permission denied... hmm + throw (new QNotFoundException("Table [" + tableName + "] was not found.")); + } + + tableMetaDataInput.setTableName(tableName); TableMetaDataAction tableMetaDataAction = new TableMetaDataAction(); TableMetaDataOutput tableMetaDataOutput = tableMetaDataAction.execute(tableMetaDataInput); @@ -750,7 +780,16 @@ public class QJavalinImplementation { ProcessMetaDataInput processMetaDataInput = new ProcessMetaDataInput(qInstance); setupSession(context, processMetaDataInput); - processMetaDataInput.setProcessName(context.pathParam("processName")); + + String processName = context.pathParam("processName"); + QProcessMetaData process = qInstance.getProcess(processName); + if(process == null) + { + throw (new QNotFoundException("Process [" + processName + "] was not found.")); + } + PermissionsHelper.checkProcessPermissionThrowing(processMetaDataInput, processName); + + processMetaDataInput.setProcessName(processName); ProcessMetaDataAction processMetaDataAction = new ProcessMetaDataAction(); ProcessMetaDataOutput processMetaDataOutput = processMetaDataAction.execute(processMetaDataInput); @@ -778,6 +817,8 @@ public class QJavalinImplementation .withSession(insertInput.getSession()) .withWidgetMetaData(qInstance.getWidget(context.pathParam("name"))); + // todo permission? + ////////////////////////// // process query string // ////////////////////////// @@ -856,6 +897,8 @@ public class QJavalinImplementation exportInput.setFilename(filename); exportInput.setLimit(limit); + PermissionsHelper.checkTablePermissionThrowing(exportInput, TablePermissionSubType.READ); + String fields = stringQueryParam(context, "fields"); if(StringUtils.hasContent(fields)) { @@ -1137,6 +1180,12 @@ public class QJavalinImplementation return; } + if(e instanceof QPermissionDeniedException) + { + respondWithError(context, HttpStatus.Code.FORBIDDEN, e.getMessage()); // 403 + return; + } + //////////////////////////////// // default exception handling // //////////////////////////////// diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java index 3cba884e..3af29a35 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinProcessHandler.java @@ -45,6 +45,7 @@ import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager; import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState; import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus; import com.kingsrook.qqq.backend.core.actions.async.JobGoingAsyncException; +import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper; import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallback; import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction; import com.kingsrook.qqq.backend.core.actions.reporting.GenerateReportAction; @@ -53,7 +54,9 @@ import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QModuleDispatchException; import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; +import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException; import com.kingsrook.qqq.backend.core.exceptions.QUserFacingException; +import com.kingsrook.qqq.backend.core.model.actions.AbstractActionInput; import com.kingsrook.qqq.backend.core.model.actions.processes.ProcessState; import com.kingsrook.qqq.backend.core.model.actions.processes.QUploadedFile; import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; @@ -185,6 +188,8 @@ public class QJavalinProcessHandler ///////////////////////////////////////////// ReportInput reportInput = new ReportInput(QJavalinImplementation.qInstance); QJavalinImplementation.setupSession(context, reportInput); + PermissionsHelper.checkReportPermissionThrowing(reportInput, reportName); + reportInput.setReportFormat(reportFormat); reportInput.setReportName(reportName); reportInput.setInputValues(null); // todo! @@ -293,12 +298,19 @@ public class QJavalinProcessHandler RunProcessInput runProcessInput = new RunProcessInput(QJavalinImplementation.qInstance); QJavalinImplementation.setupSession(context, runProcessInput); + runProcessInput.setProcessName(processName); runProcessInput.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.BREAK); runProcessInput.setProcessUUID(processUUID); runProcessInput.setStartAfterStep(startAfterStep); populateRunProcessRequestWithValuesFromContext(context, runProcessInput); + ////////////////////////////////////////////////////////////////////////////////////////////////// + // important to do this check AFTER the runProcessInput is populated with values from context - // + // e.g., in case things like a reportName are set in here // + ////////////////////////////////////////////////////////////////////////////////////////////////// + PermissionsHelper.checkProcessPermissionThrowing(runProcessInput, processName); + //////////////////////////////////////// // run the process as an async action // //////////////////////////////////////// @@ -321,6 +333,10 @@ public class QJavalinProcessHandler { resultForCaller.put("jobUUID", jgae.getJobUUID()); } + catch(QPermissionDeniedException pde) + { + QJavalinImplementation.handleException(context, pde); + } catch(Exception e) { ////////////////////////////////////////////////////////////////////////////// @@ -549,55 +565,68 @@ public class QJavalinProcessHandler *******************************************************************************/ public static void processStatus(Context context) { - String processUUID = context.pathParam("processUUID"); - String jobUUID = context.pathParam("jobUUID"); - Map resultForCaller = new HashMap<>(); - resultForCaller.put("processUUID", processUUID); - LOG.debug("Request for status of process " + processUUID + ", job " + jobUUID); - Optional optionalJobStatus = new AsyncJobManager().getJobStatus(jobUUID); - if(optionalJobStatus.isEmpty()) + try { - serializeRunProcessExceptionForCaller(resultForCaller, new RuntimeException("Could not find status of process step job")); - } - else - { - AsyncJobStatus jobStatus = optionalJobStatus.get(); + AbstractActionInput input = new AbstractActionInput(QJavalinImplementation.qInstance); + QJavalinImplementation.setupSession(context, input); - resultForCaller.put("jobStatus", jobStatus); - LOG.debug("Job status is " + jobStatus.getState() + " for " + jobUUID); + // todo... get process values? PermissionsHelper.checkProcessPermissionThrowing(input, context.pathParam("processName")); - if(jobStatus.getState().equals(AsyncJobState.COMPLETE)) + String processUUID = context.pathParam("processUUID"); + String jobUUID = context.pathParam("jobUUID"); + + resultForCaller.put("processUUID", processUUID); + + LOG.debug("Request for status of process " + processUUID + ", job " + jobUUID); + Optional optionalJobStatus = new AsyncJobManager().getJobStatus(jobUUID); + if(optionalJobStatus.isEmpty()) { - /////////////////////////////////////////////////////////////////////////////////////// - // if the job is complete, get the process result from state provider, and return it // - // this output should look like it did if the job finished synchronously!! // - /////////////////////////////////////////////////////////////////////////////////////// - Optional processState = RunProcessAction.getState(processUUID); - if(processState.isPresent()) + serializeRunProcessExceptionForCaller(resultForCaller, new RuntimeException("Could not find status of process step job")); + } + else + { + AsyncJobStatus jobStatus = optionalJobStatus.get(); + + resultForCaller.put("jobStatus", jobStatus); + LOG.debug("Job status is " + jobStatus.getState() + " for " + jobUUID); + + if(jobStatus.getState().equals(AsyncJobState.COMPLETE)) { - RunProcessOutput runProcessOutput = new RunProcessOutput(processState.get()); - serializeRunProcessResultForCaller(resultForCaller, runProcessOutput); + /////////////////////////////////////////////////////////////////////////////////////// + // if the job is complete, get the process result from state provider, and return it // + // this output should look like it did if the job finished synchronously!! // + /////////////////////////////////////////////////////////////////////////////////////// + Optional processState = RunProcessAction.getState(processUUID); + if(processState.isPresent()) + { + RunProcessOutput runProcessOutput = new RunProcessOutput(processState.get()); + serializeRunProcessResultForCaller(resultForCaller, runProcessOutput); + } + else + { + serializeRunProcessExceptionForCaller(resultForCaller, new RuntimeException("Could not find results for process " + processUUID)); + } } - else + else if(jobStatus.getState().equals(AsyncJobState.ERROR)) { - serializeRunProcessExceptionForCaller(resultForCaller, new RuntimeException("Could not find results for process " + processUUID)); + /////////////////////////////////////////////////////////////////////////////////////////////////////////// + // if the job had an error (e.g., a process step threw), "nicely" serialize its exception for the caller // + /////////////////////////////////////////////////////////////////////////////////////////////////////////// + if(jobStatus.getCaughtException() != null) + { + serializeRunProcessExceptionForCaller(resultForCaller, jobStatus.getCaughtException()); + } } } - else if(jobStatus.getState().equals(AsyncJobState.ERROR)) - { - /////////////////////////////////////////////////////////////////////////////////////////////////////////// - // if the job had an error (e.g., a process step threw), "nicely" serialize its exception for the caller // - /////////////////////////////////////////////////////////////////////////////////////////////////////////// - if(jobStatus.getCaughtException() != null) - { - serializeRunProcessExceptionForCaller(resultForCaller, jobStatus.getCaughtException()); - } - } - } - context.result(JsonUtils.toJson(resultForCaller)); + context.result(JsonUtils.toJson(resultForCaller)); + } + catch(Exception e) + { + serializeRunProcessExceptionForCaller(resultForCaller, e); + } } @@ -609,6 +638,10 @@ public class QJavalinProcessHandler { try { + AbstractActionInput input = new AbstractActionInput(QJavalinImplementation.qInstance); + QJavalinImplementation.setupSession(context, input); + // todo - need process values? PermissionsHelper.checkProcessPermissionThrowing(input, context.pathParam("processName")); + String processUUID = context.pathParam("processUUID"); Integer skip = Objects.requireNonNullElse(QJavalinImplementation.integerQueryParam(context, "skip"), 0); Integer limit = Objects.requireNonNullElse(QJavalinImplementation.integerQueryParam(context, "limit"), 20); diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinScriptsHandler.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinScriptsHandler.java index d8a32404..e920b657 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinScriptsHandler.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinScriptsHandler.java @@ -29,10 +29,13 @@ import java.util.List; import java.util.Map; import java.util.Optional; import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; +import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper; +import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType; import com.kingsrook.qqq.backend.core.actions.scripts.StoreAssociatedScriptAction; import com.kingsrook.qqq.backend.core.actions.scripts.TestScriptActionInterface; import com.kingsrook.qqq.backend.core.actions.tables.GetAction; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException; import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher; import com.kingsrook.qqq.backend.core.model.actions.scripts.StoreAssociatedScriptInput; @@ -75,6 +78,7 @@ public class QJavalinScriptsHandler *******************************************************************************/ public static void defineRecordRoutes() { + // todo - do we want some generic "developer mode" permission?? get("/developer", QJavalinScriptsHandler::getRecordDeveloperMode); post("/developer/associatedScript/{fieldName}", QJavalinScriptsHandler::storeRecordAssociatedScript); get("/developer/associatedScript/{fieldName}/{scriptRevisionId}/logs", QJavalinScriptsHandler::getAssociatedScriptLogs); @@ -100,6 +104,8 @@ public class QJavalinScriptsHandler getInput.setShouldGenerateDisplayValues(true); getInput.setShouldTranslatePossibleValues(true); + PermissionsHelper.checkTablePermissionThrowing(getInput, TablePermissionSubType.READ); + // todo - validate that the primary key is of the proper type (e.g,. not a string for an id field) // and throw a 400-series error (tell the user bad-request), rather than, we're doing a 500 (server error) @@ -217,6 +223,8 @@ public class QJavalinScriptsHandler { try { + getReferencedRecordToEnsureAccess(context); + String scriptRevisionId = context.pathParam("scriptRevisionId"); QueryInput queryInput = new QueryInput(QJavalinImplementation.qInstance); @@ -247,6 +255,40 @@ public class QJavalinScriptsHandler + /******************************************************************************* + ** + *******************************************************************************/ + private static void getReferencedRecordToEnsureAccess(Context context) throws QException + { + ///////////////////////////////////////////////////////////////////////////////// + // make sure user can get the record they're trying to do a related action for // + ///////////////////////////////////////////////////////////////////////////////// + String tableName = context.pathParam("table"); + QTableMetaData table = QJavalinImplementation.qInstance.getTable(tableName); + GetInput getInput = new GetInput(QJavalinImplementation.qInstance); + getInput.setTableName(tableName); + QJavalinImplementation.setupSession(context, getInput); + PermissionsHelper.checkTablePermissionThrowing(getInput, TablePermissionSubType.READ); + + String primaryKey = context.pathParam("primaryKey"); + getInput.setPrimaryKey(primaryKey); + + GetAction getAction = new GetAction(); + GetOutput getOutput = getAction.execute(getInput); + + /////////////////////////////////////////////////////// + // throw a not found error if the record isn't found // + /////////////////////////////////////////////////////// + QRecord record = getOutput.getRecord(); + if(record == null) + { + throw (new QNotFoundException("Could not find " + table.getLabel() + " with " + + table.getFields().get(table.getPrimaryKeyField()).getLabel() + " of " + primaryKey)); + } + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -258,12 +300,15 @@ public class QJavalinScriptsHandler { StoreAssociatedScriptInput input = new StoreAssociatedScriptInput(QJavalinImplementation.qInstance); QJavalinImplementation.setupSession(context, input); + input.setCode(context.formParam("contents")); input.setCommitMessage(context.formParam("commitMessage")); input.setFieldName(context.pathParam("fieldName")); input.setTableName(context.pathParam("table")); input.setRecordPrimaryKey(context.pathParam("primaryKey")); + PermissionsHelper.checkTablePermissionThrowing(input, TablePermissionSubType.EDIT); // todo ... is this enough?? + StoreAssociatedScriptOutput output = new StoreAssociatedScriptOutput(); StoreAssociatedScriptAction storeAssociatedScriptAction = new StoreAssociatedScriptAction(); @@ -288,6 +333,8 @@ public class QJavalinScriptsHandler try { + getReferencedRecordToEnsureAccess(context); + TestScriptInput input = new TestScriptInput(QJavalinImplementation.qInstance); QJavalinImplementation.setupSession(context, input);