diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0PermissionsHelper.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0PermissionsHelper.java
new file mode 100644
index 00000000..4bd5acc0
--- /dev/null
+++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/implementations/Auth0PermissionsHelper.java
@@ -0,0 +1,516 @@
+/*
+ * 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.modules.authentication.implementations;
+
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
+import com.kingsrook.qqq.backend.core.exceptions.QException;
+import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPatch;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.util.EntityUtils;
+import org.json.JSONArray;
+import org.json.JSONObject;
+
+
+/*******************************************************************************
+ **
+ *******************************************************************************/
+public class Auth0PermissionsHelper
+{
+ private String baseUrl;
+ private String apiName;
+ private String token;
+
+
+
+ /*******************************************************************************
+ ** Constructor
+ **
+ *******************************************************************************/
+ public Auth0PermissionsHelper(String baseUrl, String apiName, String token)
+ {
+ this.baseUrl = baseUrl;
+ this.apiName = apiName;
+ this.token = token;
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public Set getCurrentAuth0Permissions() throws QException
+ {
+ Set rs = new LinkedHashSet<>();
+ try(CloseableHttpClient httpClient = HttpClientBuilder.create().build())
+ {
+ HttpGet request = new HttpGet(baseUrl + "/resource-servers/" + URLEncoder.encode(apiName, StandardCharsets.UTF_8));
+ request.addHeader("Authorization", "Bearer " + token);
+ request.addHeader("Content-Type", "application/json");
+
+ try(CloseableHttpResponse response = httpClient.execute(request))
+ {
+ logResponseStatus(response);
+
+ String body = EntityUtils.toString(response.getEntity());
+ JSONObject json = new JSONObject(body);
+ JSONArray scopes = json.getJSONArray("scopes");
+ for(int i = 0; i < scopes.length(); i++)
+ {
+ JSONObject scope = scopes.getJSONObject(i);
+ rs.add(scope.getString("value"));
+ }
+ }
+ }
+ catch(Exception e)
+ {
+ e.printStackTrace();
+ throw (new QException("Error getting current auth0 permissions", e));
+ }
+
+ return (rs);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public Set getRoles() throws QException
+ {
+ Set rs = new LinkedHashSet<>();
+ try(CloseableHttpClient httpClient = HttpClientBuilder.create().build())
+ {
+ HttpGet request = new HttpGet(baseUrl + "roles");
+ request.addHeader("Authorization", "Bearer " + token);
+ request.addHeader("Content-Type", "application/json");
+
+ try(CloseableHttpResponse response = httpClient.execute(request))
+ {
+ logResponseStatus(response);
+
+ String body = EntityUtils.toString(response.getEntity());
+ JSONArray json = new JSONArray(body);
+ for(int i = 0; i < json.length(); i++)
+ {
+ JSONObject role = json.getJSONObject(i);
+ rs.add(role.getString("id"));
+ }
+ }
+ }
+ catch(Exception e)
+ {
+ e.printStackTrace();
+ throw (new QException("Error getting auth0 roles", e));
+ }
+
+ return (rs);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public Set getPermissionsForRole(String roleId) throws QException
+ {
+ Set rs = new LinkedHashSet<>();
+ try(CloseableHttpClient httpClient = HttpClientBuilder.create().build())
+ {
+ for(int page = 0; ; page++)
+ {
+ HttpGet request = new HttpGet(baseUrl + "roles/" + roleId + "/permissions?page=" + page);
+ request.addHeader("Authorization", "Bearer " + token);
+ request.addHeader("Content-Type", "application/json");
+
+ try(CloseableHttpResponse response = httpClient.execute(request))
+ {
+ logResponseStatus(response);
+
+ String body = EntityUtils.toString(response.getEntity());
+ JSONArray json = new JSONArray(body);
+ if(json.isEmpty())
+ {
+ break;
+ }
+
+ for(int i = 0; i < json.length(); i++)
+ {
+ JSONObject permission = json.getJSONObject(i);
+ rs.add(permission.getString("permission_name"));
+ }
+ }
+ }
+ }
+ catch(Exception e)
+ {
+ e.printStackTrace();
+ throw (new QException("Error getting auth0 permissions for role", e));
+ }
+
+ return (rs);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void addPermissionsToRole(String roleId, Set permissionsToAdd) throws QException
+ {
+ try(CloseableHttpClient httpClient = HttpClientBuilder.create().build())
+ {
+ HttpPost request = new HttpPost(baseUrl + "/roles/" + roleId + "/permissions");
+ request.addHeader("Authorization", "Bearer " + token);
+ request.addHeader("Content-Type", "application/json");
+
+ JSONArray permissions = new JSONArray();
+ for(String permissionName : permissionsToAdd)
+ {
+ JSONObject permission = new JSONObject();
+ permissions.put(permission);
+ permission.put("resource_server_identifier", apiName);
+ permission.put("permission_name", permissionName);
+ }
+ JSONObject body = new JSONObject();
+ body.put("permissions", permissions);
+
+ request.setEntity(new StringEntity(body.toString()));
+
+ try(CloseableHttpResponse response = httpClient.execute(request))
+ {
+ logResponseStatus(response);
+ }
+ }
+ catch(Exception e)
+ {
+ e.printStackTrace();
+ throw (new QException("Error storing permissions for role in auth0", e));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void removePermissionsFromRole(String roleId, Set permissionsToRemove) throws QException
+ {
+ try(CloseableHttpClient httpClient = HttpClientBuilder.create().build())
+ {
+ HttpDeleteWithBody request = new HttpDeleteWithBody(baseUrl + "/roles/" + roleId + "/permissions");
+ request.addHeader("Authorization", "Bearer " + token);
+ request.addHeader("Content-Type", "application/json");
+
+ JSONArray permissions = new JSONArray();
+ for(String permissionName : permissionsToRemove)
+ {
+ JSONObject permission = new JSONObject();
+ permissions.put(permission);
+ permission.put("resource_server_identifier", apiName);
+ permission.put("permission_name", permissionName);
+ }
+ JSONObject body = new JSONObject();
+ body.put("permissions", permissions);
+
+ request.setEntity(new StringEntity(body.toString()));
+
+ try(CloseableHttpResponse response = httpClient.execute(request))
+ {
+ logResponseStatus(response);
+ }
+ }
+ catch(Exception e)
+ {
+ e.printStackTrace();
+ throw (new QException("Error storing permissions for role in auth0", e));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public Set getPermissionsInInstanceButNotInAuth0(QInstance qInstance, Collection permissionsCurrentlyInAuth0)
+ {
+ List allInstancePermissions = new ArrayList<>(PermissionsHelper.getAllAvailablePermissionNames(qInstance).stream().filter(p -> !p.contains(".bulk")).toList());
+ allInstancePermissions.removeAll(permissionsCurrentlyInAuth0);
+ return new TreeSet<>(allInstancePermissions);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public Set getPermissionsInAuth0ButNotInInstance(QInstance qInstance, Set currentAuth0Permissions)
+ {
+ List allInstancePermissions = new ArrayList<>(PermissionsHelper.getAllAvailablePermissionNames(qInstance).stream().filter(p -> !p.contains(".bulk")).toList());
+ Set rs = new TreeSet<>(currentAuth0Permissions);
+ allInstancePermissions.forEach(rs::remove);
+ return (rs);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void addPermissionsToAuth0(QInstance qInstance, Collection permissionsCurrentlyInAuth0, Collection permissionsToAddToAuth0) throws QException
+ {
+ Set permissions = new TreeSet<>();
+ permissions.addAll(permissionsCurrentlyInAuth0);
+ permissions.addAll(permissionsToAddToAuth0);
+ storePermissionsInAuth0(qInstance, permissions);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void removePermissionsToAuth0(QInstance qInstance, Collection permissionsCurrentlyInAuth0, Collection permissionsToRemoveFromAuth0) throws QException
+ {
+ Set permissions = new TreeSet<>(permissionsCurrentlyInAuth0);
+ permissions.removeAll(permissionsToRemoveFromAuth0);
+ storePermissionsInAuth0(qInstance, permissions);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public void replaceAuth0PermissionsWithAllFromInstance(QInstance qInstance) throws QException
+ {
+ List permissions = PermissionsHelper.getAllAvailablePermissionNames(qInstance).stream().filter(p -> !p.contains(".bulk")).toList();
+ storePermissionsInAuth0(qInstance, permissions);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private void storePermissionsInAuth0(QInstance qInstance, Collection permissions) throws QException
+ {
+ try(CloseableHttpClient httpClient = HttpClientBuilder.create().build())
+ {
+ HttpPatch request = new HttpPatch(baseUrl + "/resource-servers/" + URLEncoder.encode(apiName, StandardCharsets.UTF_8));
+ request.addHeader("Authorization", "Bearer " + token);
+ request.addHeader("Content-Type", "application/json");
+
+ buildRequestBodyWithScopesFromPermissions(qInstance, permissions, request);
+
+ try(CloseableHttpResponse response = httpClient.execute(request))
+ {
+ logResponseStatus(response);
+ }
+ }
+ catch(Exception e)
+ {
+ e.printStackTrace();
+ throw (new QException("Error storing permissions in auth0", e));
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static void logResponseStatus(CloseableHttpResponse response) throws IOException
+ {
+ if(response.getStatusLine() != null)
+ {
+ Integer statusCode = response.getStatusLine().getStatusCode();
+ String statusReasonPhrase = response.getStatusLine().getReasonPhrase();
+ System.out.println("Result: " + statusCode + " : " + statusReasonPhrase);
+
+ if(statusCode > 299)
+ {
+ System.out.println(EntityUtils.toString(response.getEntity()));
+ throw (new IllegalStateException("Bad status code: " + statusCode));
+ }
+ }
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ private static void buildRequestBodyWithScopesFromPermissions(QInstance qInstance, Collection permissions, HttpPatch request) throws UnsupportedEncodingException
+ {
+ JSONArray scopes = new JSONArray();
+ for(String permissionName : permissions)
+ {
+ String[] parts = permissionName.split("\\.", 2);
+ String name = parts[0];
+ String permission = parts[1];
+
+ String object = name;
+ if(qInstance.getTable(name) != null)
+ {
+ object = qInstance.getTable(name).getLabel() + " table";
+ }
+ else if(qInstance.getProcess(name) != null)
+ {
+ object = qInstance.getProcess(name).getLabel() + " process";
+ }
+ else if(qInstance.getReport(name) != null)
+ {
+ object = qInstance.getReport(name).getLabel() + " report";
+ }
+ else if(qInstance.getWidget(name) != null)
+ {
+ object = qInstance.getWidget(name).getLabel() + " widget";
+ }
+ else if(qInstance.getApp(name) != null)
+ {
+ object = qInstance.getApp(name).getLabel() + " app";
+ }
+ String verb = permission.equals("hasAccess") ? "access" : permission;
+
+ JSONObject scopeObject = new JSONObject();
+ scopeObject.put("value", permissionName);
+ scopeObject.put("description", "Permission to " + verb + " the " + object);
+ scopes.put(scopeObject);
+ }
+
+ JSONObject body = new JSONObject();
+ body.put("scopes", scopes);
+
+ request.setEntity(new StringEntity(body.toString()));
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for baseUrl
+ *******************************************************************************/
+ public String getBaseUrl()
+ {
+ return (this.baseUrl);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for baseUrl
+ *******************************************************************************/
+ public void setBaseUrl(String baseUrl)
+ {
+ this.baseUrl = baseUrl;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for baseUrl
+ *******************************************************************************/
+ public Auth0PermissionsHelper withUrl(String baseUrl)
+ {
+ this.baseUrl = baseUrl;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ ** Getter for token
+ *******************************************************************************/
+ public String getToken()
+ {
+ return (this.token);
+ }
+
+
+
+ /*******************************************************************************
+ ** Setter for token
+ *******************************************************************************/
+ public void setToken(String token)
+ {
+ this.token = token;
+ }
+
+
+
+ /*******************************************************************************
+ ** Fluent setter for token
+ *******************************************************************************/
+ public Auth0PermissionsHelper withToken(String token)
+ {
+ this.token = token;
+ return (this);
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ class HttpDeleteWithBody extends HttpEntityEnclosingRequestBase
+ {
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public String getMethod()
+ {
+ return "DELETE";
+ }
+
+
+
+ /*******************************************************************************
+ **
+ *******************************************************************************/
+ public HttpDeleteWithBody(final String uri)
+ {
+ super();
+ setURI(URI.create(uri));
+ }
+
+ }
+
+}