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 extends List> mutableTypeIfNeeded;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public MutableList(List sourceList)
+ {
+ this(sourceList, (Class) ArrayList.class);
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public MutableList(List sourceList, Class extends List> 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 extends T> c)
+ {
+ return (doMutableOperationForValue(() -> sourceList.addAll(c)));
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ @Override
+ public boolean addAll(int index, Collection extends T> 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 extends Map> mutableTypeIfNeeded;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public MutableMap(Map sourceMap)
+ {
+ this(sourceMap, (Class) HashMap.class);
+ }
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public MutableMap(Map sourceMap, Class extends Map> 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 extends K, ? extends V> 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);