diff --git a/cicd/docker/dotcms-compose.yml b/cicd/docker/dotcms-compose.yml index 684a2146e548..f5cacc1b4b0f 100644 --- a/cicd/docker/dotcms-compose.yml +++ b/cicd/docker/dotcms-compose.yml @@ -38,6 +38,7 @@ services: WAIT_FOR_DEPS: JVM_ENDPOINT_TEST_PASS: DOT_ANALYTICS_IDP_URL: http://localhost:61111/realms/dotcms/protocol/openid-connect/token + DOT_ENABLE_SCRIPTING: "true" depends_on: - database - elasticsearch diff --git a/dotCMS/src/curl-test/VelocitySecrets.postman_collection.json b/dotCMS/src/curl-test/VelocitySecrets.postman_collection.json index 8d70163d60b5..377cd244ad12 100644 --- a/dotCMS/src/curl-test/VelocitySecrets.postman_collection.json +++ b/dotCMS/src/curl-test/VelocitySecrets.postman_collection.json @@ -1,11 +1,68 @@ { "info": { - "_postman_id": "c55a23bc-a33b-42ab-a63b-083e43af4572", + "_postman_id": "e2ac4397-5a79-464d-a0d9-243f72420eff", "name": "VelocitySecrets", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", "_exporter_id": "781456" }, "item": [ + { + "name": "CreateNewScriptingUser", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code should be 200\", function () {", + " pm.response.to.have.status(200);", + "});", + "" + ], + "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 \"firstName\":\"Scripting User\",\n \"lastName\":\"Scripting User\",\n \"email\":\"scripting@dotcms.com\",\n \"password\":[\"d\",\"o\",\"t\",\"c\",\"m\",\"s\",\"1\",\"2\",\"3\",\"4\",\"5\",\"6\",\"7\",\"8\"],\n \"roles\":[\"DOTCMS_BACK_END_USER\",\"Scripting Developer\"]\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{serverURL}}/api/v1/users", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "users" + ] + } + }, + "response": [] + }, { "name": "Add Config to System Host", "event": [ @@ -125,7 +182,39 @@ "response": [] }, { - "name": "Test Velocity Secrets", + "name": "invalidateSession", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{serverURL}}/api/v1/logout", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "logout" + ] + } + }, + "response": [] + }, + { + "name": "Test Velocity Secrets With Scripting User", "event": [ { "listen": "test", @@ -177,12 +266,12 @@ "basic": [ { "key": "password", - "value": "admin", + "value": "dotcms12345678", "type": "string" }, { "key": "username", - "value": "admin@dotcms.com", + "value": "scripting@dotcms.com", "type": "string" } ] @@ -212,6 +301,38 @@ } }, "response": [] + }, + { + "name": "invalidateSessionAgain", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "pm.test(\"Status code is 200\", function () {", + " pm.response.to.have.status(200);", + "});" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{serverURL}}/api/v1/logout", + "host": [ + "{{serverURL}}" + ], + "path": [ + "api", + "v1", + "logout" + ] + } + }, + "response": [] } ] } \ No newline at end of file diff --git a/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/SecretTool.java b/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/SecretTool.java index b20dbca01344..970bfc72765d 100644 --- a/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/SecretTool.java +++ b/dotCMS/src/main/java/com/dotcms/rendering/velocity/viewtools/SecretTool.java @@ -3,9 +3,21 @@ import com.dotcms.api.web.HttpServletRequestThreadLocal; import com.dotcms.rendering.velocity.viewtools.secrets.DotVelocitySecretAppConfig; import com.dotmarketing.business.APILocator; +import com.dotmarketing.business.Role; +import com.dotmarketing.business.UserAPI; +import com.dotmarketing.business.web.WebAPILocator; +import com.dotmarketing.portlets.contentlet.model.Contentlet; +import com.dotmarketing.util.Config; +import com.dotmarketing.util.Logger; +import com.dotmarketing.util.UtilMethods; +import com.liferay.portal.model.User; +import org.apache.velocity.context.Context; +import org.apache.velocity.context.InternalContextAdapterImpl; +import org.apache.velocity.tools.view.context.ViewContext; import org.apache.velocity.tools.view.tools.ViewTool; import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; import java.util.Optional; /** @@ -15,8 +27,16 @@ */ public class SecretTool implements ViewTool { + private InternalContextAdapterImpl internalContextAdapter; + private Context context; + private HttpServletRequest request; + @Override - public void init(Object initData) { + public void init(final Object initData) { + + final ViewContext context = (ViewContext) initData; + this.request = context.getRequest(); + this.context = context.getVelocityContext(); } /** @@ -26,6 +46,8 @@ public void init(Object initData) { */ public Object get(final String key) { + canUserEvaluate(); + final HttpServletRequest request = HttpServletRequestThreadLocal.INSTANCE.getRequest(); final Optional config = DotVelocitySecretAppConfig.config(request); return config.isPresent()? config.get().getStringOrNull(key) : null; @@ -51,12 +73,14 @@ public Object getSystemSecret (final String key) { public Object getSystemSecret (final String key, final Object defaultValue) { + canUserEvaluate(); final Optional config = DotVelocitySecretAppConfig.config(APILocator.systemHost()); return config.isPresent()? config.get().getStringOrNull(key, null!= defaultValue? defaultValue.toString():null) : defaultValue; } public char[] getCharArray(final String key) { + canUserEvaluate(); final HttpServletRequest request = HttpServletRequestThreadLocal.INSTANCE.getRequest(); final Optional config = DotVelocitySecretAppConfig.config(request); return config.isPresent()? config.get().getCharArrayOrNull(key) : null; @@ -70,7 +94,58 @@ public char[] getCharArraySystemSecret (final String key) { public char[] getCharArraySystemSecret (final String key, final char[] defaultValue) { + canUserEvaluate(); final Optional config = DotVelocitySecretAppConfig.config(APILocator.systemHost()); return config.isPresent()? config.get().getCharArrayOrNull(key, defaultValue) : defaultValue; } + + private static final boolean ENABLE_SCRIPTING = Config.getBooleanProperty("ENABLE_SCRIPTING", false); + + /** + * Test 2 things. + * 1) see if the user has the scripting role + * 2) otherwise check if the last modified user has the scripting role + * @return boolean + */ + protected void canUserEvaluate() { + + if(!ENABLE_SCRIPTING) { + + Logger.warn(this.getClass(), "Scripting called and ENABLE_SCRIPTING set to false"); + throw new SecurityException("External scripting is disabled in your dotcms instance."); + } + + try { + + boolean hasScriptingRole = false; + final Role scripting = APILocator.getRoleAPI().loadRoleByKey(Role.SCRIPTING_DEVELOPER); + + this.internalContextAdapter = new InternalContextAdapterImpl(context); + final String fieldResourceName = this.internalContextAdapter.getCurrentTemplateName(); + if (UtilMethods.isSet(fieldResourceName)) { + final String contentletFileAssetInode = fieldResourceName.substring(fieldResourceName.indexOf("/") + 1, fieldResourceName.indexOf("_")); + final Contentlet contentlet = APILocator.getContentletAPI().find(contentletFileAssetInode, APILocator.systemUser(), true); + final User lastModifiedUser = APILocator.getUserAPI().loadUserById(contentlet.getModUser(), APILocator.systemUser(), true); + hasScriptingRole = APILocator.getRoleAPI().doesUserHaveRole(lastModifiedUser, scripting); + } + + if (!hasScriptingRole) { + final User user = WebAPILocator.getUserWebAPI().getUser(this.request); + // try with the current user + if (null != user) { + + hasScriptingRole = APILocator.getRoleAPI().doesUserHaveRole(user, scripting); + } + } + + if (!hasScriptingRole) { + + throw new SecurityException("External scripting is disabled in your dotcms instance."); + } + } catch(Exception e) { + + Logger.warn(this.getClass(), "Scripting called with error" + e); + throw new SecurityException("External scripting is disabled in your dotcms instance.", e); + } + } // canUserEvaluate. } diff --git a/dotCMS/src/main/java/com/dotmarketing/business/Role.java b/dotCMS/src/main/java/com/dotmarketing/business/Role.java index b28466e8c795..98d9667f993d 100644 --- a/dotCMS/src/main/java/com/dotmarketing/business/Role.java +++ b/dotCMS/src/main/java/com/dotmarketing/business/Role.java @@ -34,6 +34,8 @@ public class Role implements Serializable,Comparable { public static final String CMS_OWNER_ROLE = "CMS Owner"; public static final String CMS_ADMINISTRATOR_ROLE = "CMS Administrator"; + public static final String SCRIPTING_DEVELOPER = "Scripting Developer"; + public static final String DOTCMS_BACK_END_USER = "DOTCMS_BACK_END_USER"; diff --git a/dotCMS/src/main/webapp/WEB-INF/toolbox.xml b/dotCMS/src/main/webapp/WEB-INF/toolbox.xml index e0d37913a20e..0f98392511e1 100644 --- a/dotCMS/src/main/webapp/WEB-INF/toolbox.xml +++ b/dotCMS/src/main/webapp/WEB-INF/toolbox.xml @@ -36,7 +36,7 @@ dotsecrets - application + request com.dotcms.rendering.velocity.viewtools.SecretTool