From 1b98197d06b679a26e9f338cb1d8bee38f565639 Mon Sep 17 00:00:00 2001 From: Jose Castro Date: Thu, 19 Oct 2023 13:14:58 -0600 Subject: [PATCH] feat(core): Add db layer to our config to allow on-the-fly (no restart) configuration #25184 * feat(system table) : Adding missing requirements from Code Review #25184 * Enable System Table by default. --- .../SystemTable.postman_collection.json | 209 +++--------------- .../api/v1/system/SystemTableResource.java | 99 +++++---- .../com/dotcms/util/CollectionsUtils.java | 36 ++- .../java/com/dotmarketing/util/Config.java | 2 +- 4 files changed, 133 insertions(+), 213 deletions(-) diff --git a/dotCMS/src/curl-test/SystemTable.postman_collection.json b/dotCMS/src/curl-test/SystemTable.postman_collection.json index 11924f7f1784..b3927e9d622e 100644 --- a/dotCMS/src/curl-test/SystemTable.postman_collection.json +++ b/dotCMS/src/curl-test/SystemTable.postman_collection.json @@ -1,76 +1,23 @@ { "info": { - "_postman_id": "268d46df-9822-4de1-b0ff-c3d03e715902", + "_postman_id": "66446a0d-a484-449f-8b78-a69e14929ad1", "name": "SystemTable", + "description": "This Postman test verifies that CRUD operations executed on the System Table API work as expected. This API allows users and developers to store global properties in the application.", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "_exporter_id": "781456" + "_exporter_id": "5403727", + "_collection_link": "https://cloudy-robot-285072.postman.co/workspace/5bfa586e-54db-429b-b7d5-c4ff997e3a0d/collection/5403727-66446a0d-a484-449f-8b78-a69e14929ad1?action=share&source=collection_link&creator=5403727" }, "item": [ { - "name": "GetInvalidKey", + "name": "Getting an Non-Existing Key", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test(\"400\", function () {", - " pm.response.to.have.status(400);", - "});", - "", - "", - "", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "GET", - "header": [], - "url": { - "raw": "{{serverURL}}/api/v1/system-table/non-valid-key", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "system-table", - "non-valid-key" - ] - } - }, - "response": [] - }, - { - "name": "Get404", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"404\", function () {", + "pm.test(\"Expecting a Status 404 when asking for a non-existing key\", function () {", " pm.response.to.have.status(404);", "});", - "", - "", - "", "" ], "type": "text/javascript" @@ -96,7 +43,7 @@ "method": "GET", "header": [], "url": { - "raw": "{{serverURL}}/api/v1/system-table/DOT_404-key", + "raw": "{{serverURL}}/api/v1/system-table/key-404", "host": [ "{{serverURL}}" ], @@ -104,85 +51,22 @@ "api", "v1", "system-table", - "DOT_404-key" + "key-404" ] } }, "response": [] }, { - "name": "TryAddInvalidKey", + "name": "Saving a New Key", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test(\"400\", function () {", - " pm.response.to.have.status(400);", - "});", - "", - "", - "", - "" - ], - "type": "text/javascript" - } - } - ], - "request": { - "auth": { - "type": "basic", - "basic": [ - { - "key": "password", - "value": "admin", - "type": "string" - }, - { - "key": "username", - "value": "admin@dotcms.com", - "type": "string" - } - ] - }, - "method": "POST", - "header": [], - "body": { - "mode": "raw", - "raw": "{\n \"key\":\"non-valid\",\n \"value\":\"non-valid\"\n}", - "options": { - "raw": { - "language": "json" - } - } - }, - "url": { - "raw": "{{serverURL}}/api/v1/system-table", - "host": [ - "{{serverURL}}" - ], - "path": [ - "api", - "v1", - "system-table" - ] - } - }, - "response": [] - }, - { - "name": "SaveKey", - "event": [ - { - "listen": "test", - "script": { - "exec": [ - "pm.test(\"200\", function () {", + "pm.test(\"Saving a new key in the System Table\", function () {", " pm.response.to.have.status(200);", "});", - "", - "", - "", "" ], "type": "text/javascript" @@ -209,7 +93,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"key\":\"DOT_KEY1\",\n \"value\":\"value1\"\n}", + "raw": "{\n \"key\":\"MY_KEY1\",\n \"value\":\"value1\"\n}", "options": { "raw": { "language": "json" @@ -231,33 +115,25 @@ "response": [] }, { - "name": "GetRecentAdded", + "name": "Getting the Previously Added Key", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test(\"200\", function () {", + "pm.test(\"Expecting a Status 200 response\", function () {", " pm.response.to.have.status(200);", "});", "", - "pm.test(\"No errors\", function () {", - " ", + "pm.test(\"No errors should be thrown\", function () {", " var jsonData = pm.response.json();", " pm.expect(jsonData.errors.length).to.eql(0);", "});", "", - "pm.test(\"Information Saved Correctly\", function () {", - " ", + "pm.test(\"Expecting the correct value of the previously added key\", function () {", " var jsonData = pm.response.json();", " pm.expect(jsonData.entity).to.eql(\"value1\");", - " ", "});", - "", - "", - "", - "", - "", "" ], "type": "text/javascript" @@ -283,7 +159,7 @@ "method": "GET", "header": [], "url": { - "raw": "{{serverURL}}/api/v1/system-table/DOT_KEY1", + "raw": "{{serverURL}}/api/v1/system-table/MY_KEY1", "host": [ "{{serverURL}}" ], @@ -291,25 +167,22 @@ "api", "v1", "system-table", - "DOT_KEY1" + "MY_KEY1" ] } }, "response": [] }, { - "name": "UpdateKey", + "name": "Updating the Existing Key", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test(\"200\", function () {", + "pm.test(\"Updating the existing key in the System Table\", function () {", " pm.response.to.have.status(200);", "});", - "", - "", - "", "" ], "type": "text/javascript" @@ -336,7 +209,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"key\":\"DOT_KEY1\",\n \"value\":\"value2\"\n}", + "raw": "{\n \"key\":\"MY_KEY1\",\n \"value\":\"value2\"\n}", "options": { "raw": { "language": "json" @@ -358,33 +231,25 @@ "response": [] }, { - "name": "GetRecentUpdated", + "name": "Getting the Previously Updated Key", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test(\"200\", function () {", + "pm.test(\"Expecting a Status 200 response\", function () {", " pm.response.to.have.status(200);", "});", "", - "pm.test(\"No errors\", function () {", - " ", + "pm.test(\"No errors should be thrown\", function () {", " var jsonData = pm.response.json();", " pm.expect(jsonData.errors.length).to.eql(0);", "});", "", - "pm.test(\"Information Saved Correctly\", function () {", - " ", + "pm.test(\"Expecting the correct updated value of the key\", function () {", " var jsonData = pm.response.json();", " pm.expect(jsonData.entity).to.eql(\"value2\");", - " ", "});", - "", - "", - "", - "", - "", "" ], "type": "text/javascript" @@ -410,7 +275,7 @@ "method": "GET", "header": [], "url": { - "raw": "{{serverURL}}/api/v1/system-table/DOT_KEY1", + "raw": "{{serverURL}}/api/v1/system-table/MY_KEY1", "host": [ "{{serverURL}}" ], @@ -418,22 +283,23 @@ "api", "v1", "system-table", - "DOT_KEY1" + "MY_KEY1" ] } }, "response": [] }, { - "name": "Delete", + "name": "Deleting the Key", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test(\"200\", function () {", + "pm.test(\"Deleting the existing key\", function () {", " pm.response.to.have.status(200);", - "});" + "});", + "" ], "type": "text/javascript" } @@ -458,7 +324,7 @@ "method": "DELETE", "header": [], "url": { - "raw": "{{serverURL}}/api/v1/system-table/DOT_KEY1", + "raw": "{{serverURL}}/api/v1/system-table/MY_KEY1", "host": [ "{{serverURL}}" ], @@ -466,25 +332,22 @@ "api", "v1", "system-table", - "DOT_KEY1" + "MY_KEY1" ] } }, "response": [] }, { - "name": "Get404RecentDeleted", + "name": "Getting a 404 for the Previously Deleted Key", "event": [ { "listen": "test", "script": { "exec": [ - "pm.test(\"404\", function () {", + "pm.test(\"Retrieving the key after it was deleted\", function () {", " pm.response.to.have.status(404);", "});", - "", - "", - "", "" ], "type": "text/javascript" @@ -510,7 +373,7 @@ "method": "GET", "header": [], "url": { - "raw": "{{serverURL}}/api/v1/system-table/DOT_KEY1", + "raw": "{{serverURL}}/api/v1/system-table/MY_KEY1", "host": [ "{{serverURL}}" ], @@ -518,7 +381,7 @@ "api", "v1", "system-table", - "DOT_KEY1" + "MY_KEY1" ] } }, diff --git a/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/SystemTableResource.java b/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/SystemTableResource.java index 76cab242f6ab..ca3bf7668ace 100644 --- a/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/SystemTableResource.java +++ b/dotCMS/src/main/java/com/dotcms/rest/api/v1/system/SystemTableResource.java @@ -5,6 +5,7 @@ import com.dotcms.rest.ResponseEntityView; import com.dotcms.rest.WebResource.InitBuilder; import com.dotcms.rest.annotation.NoCache; +import com.dotcms.util.CollectionsUtils; import com.dotcms.util.WhiteBlackList; import com.dotmarketing.business.APILocator; import com.dotmarketing.business.Role; @@ -12,6 +13,7 @@ import com.dotmarketing.exception.DotDuplicateDataException; import com.dotmarketing.util.Config; import com.dotmarketing.util.Logger; +import com.liferay.util.StringPool; import org.glassfish.jersey.server.JSONP; import javax.servlet.http.HttpServletRequest; @@ -26,7 +28,6 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; -import java.io.IOException; import java.io.Serializable; import java.util.Map; import java.util.Optional; @@ -39,31 +40,30 @@ * @author jsanca */ @Path("/v1/system-table") -@SuppressWarnings("serial") public class SystemTableResource implements Serializable { private final SystemTable systemTable = APILocator.getSystemAPI().getSystemTable(); + private static final String[] DEFAULT_BLACKLISTED_PROPS = new String[]{"DOTCMS_CLUSTER_ID", "DOTCMS_CLUSTER_SALT"}; private final WhiteBlackList whiteBlackList = new WhiteBlackList.Builder() .addWhitePatterns(Config.getStringArrayProperty("SYSTEM_TABLE_WHITELISTED_KEYS", - new String[]{"^DOT_.*"})) - .addBlackPatterns(Config.getStringArrayProperty("SYSTEM_TABLE_BLACKLISTED_KEYS", - new String[]{"SYSTEM_TABLE_BLACKLISTED_KEYS","SYSTEM_TABLE_WHITELISTED_KEYS"})).build(); + new String[]{StringPool.BLANK})) + .addBlackPatterns(CollectionsUtils.concat(Config.getStringArrayProperty( + "SYSTEM_TABLE_BLACKLISTED_KEYS", new String[]{}), DEFAULT_BLACKLISTED_PROPS)).build(); /** * Returns all entries in the system table. - * @param request - * @param response - * @param key - * @return - * @throws IOException + * + * @param request The current instance of the {@link HttpServletRequest}. + * @param response The current instance of the {@link HttpServletResponse}. + * + * @return A {@link Map} containing all entries in the system table. */ @GET @JSONP @NoCache @Produces({MediaType.APPLICATION_JSON, "application/javascript"}) public final ResponseEntityView> getAll(@Context final HttpServletRequest request, - @Context final HttpServletResponse response) - throws IllegalAccessException { + @Context final HttpServletResponse response) { this.init(request, response); final Map allEntries = this.systemTable.all(); @@ -72,16 +72,20 @@ public final ResponseEntityView> getAll(@Context final HttpSe .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); Logger.debug(this, ()-> "Getting all system table values"); - return new ResponseEntityView>(filteredEntries); + return new ResponseEntityView<>(filteredEntries); } /** - * Find a value in the system table by key, 404 if not found - * @param request - * @param response - * @param key - * @return - * @throws IOException + * Returns the value of a key in the System Table, or returns a 404 if not found. + * + * @param request The current instance of the {@link HttpServletRequest}. + * @param response The current instance of the {@link HttpServletResponse}. + * @param key The key to search for. + * + * @return The value for the key. + * + * @throws IllegalArgumentException If the key is blacklisted. + * @throws DoesNotExistException The key was not found. */ @Path("/{key}") @GET @@ -106,6 +110,14 @@ public final ResponseEntityStringView get(@Context final HttpServletRequest requ throw new DoesNotExistException("Key not found: " + key); } + /** + * Defines the required information that must be available in the request for this REST Endpoint + * to provide the expected information. In this case, the User in the request must have back-end + * access permissions, and must be a CMS Administrator. + * + * @param request The current instance of the {@link HttpServletRequest}. + * @param response The current instance of the {@link HttpServletResponse}. + */ private void init(final HttpServletRequest request, final HttpServletResponse response) { new InitBuilder(request, response) .requiredBackendUser(true) @@ -115,6 +127,13 @@ private void init(final HttpServletRequest request, final HttpServletResponse re .init(); } + /** + * Checks if the key is blacklisted. + * + * @param key The key to check. + * + * @throws IllegalArgumentException If the key is blacklisted. + */ private void checkBlackList(final String key) throws IllegalArgumentException { if (!this.whiteBlackList.isAllowed(key)) { @@ -124,12 +143,16 @@ private void checkBlackList(final String key) throws IllegalArgumentException { } /** - * Saves a value to the system table - * @param request - * @param response - * @param form - * @return - * @throws IllegalAccessException + * Saves a value to the system table. + * + * @param request The current instance of the {@link HttpServletRequest}. + * @param response The current instance of the {@link HttpServletResponse}. + * @param form The {@link KeyValueForm} object containing the key and value to save. + * + * @return The key that was saved. + * + * @throws IllegalArgumentException If the key is blacklisted. + * @throws DotDuplicateDataException The key already exists. */ @POST @JSONP @@ -154,11 +177,13 @@ public ResponseEntityStringView save( } /** - * Updates a value in the system table - * 404 if the value to update does not exist + * Updates a value in the System Table, or returns a 404 if the key doesn't exist. + * + * @param request The current instance of the {@link HttpServletRequest}. * - * @param request - * @return + * @return The key that was updated. + * + * @throws IllegalArgumentException If the key is blacklisted. */ @PUT @JSONP @@ -174,8 +199,7 @@ public ResponseEntityStringView update( this.checkBlackList(form.getKey()); final Optional valueOpt = this.systemTable.get(form.getKey()); - if (!valueOpt.isPresent()) { - + if (valueOpt.isEmpty()) { throw new DoesNotExistException("Key not found: " + form.getKey()); } @@ -186,11 +210,13 @@ public ResponseEntityStringView update( } /** - * Deletes a value in the system table - * 404 if the value to update does not exist + * Deletes a value from the System Table, or returns a 404 if the key doesn't exist. + * + * @param request The current instance of the {@link HttpServletRequest}. * - * @param request - * @return + * @return The key that was deleted. + * + * @throws IllegalArgumentException If the key is blacklisted. */ @DELETE @Path("/{key}") @@ -207,8 +233,7 @@ public ResponseEntityStringView delete( this.checkBlackList(key); final Optional valueOpt = this.systemTable.get(key); - if (!valueOpt.isPresent()) { - + if (valueOpt.isEmpty()) { throw new DoesNotExistException("Key not found: " + key); } diff --git a/dotCMS/src/main/java/com/dotcms/util/CollectionsUtils.java b/dotCMS/src/main/java/com/dotcms/util/CollectionsUtils.java index 275afa105706..092247182140 100644 --- a/dotCMS/src/main/java/com/dotcms/util/CollectionsUtils.java +++ b/dotCMS/src/main/java/com/dotcms/util/CollectionsUtils.java @@ -7,9 +7,28 @@ import org.elasticsearch.common.collect.MapBuilder; import java.io.Serializable; -import java.util.*; -import java.util.function.*; +import java.lang.reflect.Array; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BiFunction; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.Supplier; import java.util.stream.Collector; +import java.util.stream.Stream; /** * This utility class provides common use methods for creating and interacting @@ -1099,6 +1118,19 @@ public static BinaryOperator compare(final Comparator comparator) { return (current,last) -> comparator.compare(current,last) >= 0 ? current : last; } + } + /** + * Concatenates the contents of one array into the other one. + * + * @param array1 The base array used in the concatenation. + * @param array2 The array that will be added to the first array. + * + * @return The resulting array + */ + public static T[] concat(final T[] array1, final T[] array2) { + return Stream.concat(Arrays.stream(array1), Arrays.stream(array2)) + .toArray(size -> (T[]) Array.newInstance(array1.getClass().getComponentType(), size)); } + } diff --git a/dotCMS/src/main/java/com/dotmarketing/util/Config.java b/dotCMS/src/main/java/com/dotmarketing/util/Config.java index cb9ff49e7832..9a757e030dcb 100644 --- a/dotCMS/src/main/java/com/dotmarketing/util/Config.java +++ b/dotCMS/src/main/java/com/dotmarketing/util/Config.java @@ -53,7 +53,7 @@ public class Config { private static SystemTableConfigSource systemTableConfigSource = null; @VisibleForTesting - public static boolean enableSystemTableConfigSource = "true".equalsIgnoreCase(EnvironmentVariablesService.getInstance().getenv().getOrDefault("DOT_ENABLE_SYSTEM_TABLE_CONFIG_SOURCE", "false")); + public static boolean enableSystemTableConfigSource = "true".equalsIgnoreCase(EnvironmentVariablesService.getInstance().getenv().getOrDefault("DOT_ENABLE_SYSTEM_TABLE_CONFIG_SOURCE", "true")); public static void initSystemTableConfigSource() { systemTableConfigSource = new SystemTableConfigSource();