diff --git a/core/src/main/java/dev/vml/es/acm/core/code/Conditions.java b/core/src/main/java/dev/vml/es/acm/core/code/Conditions.java index 136ab0eda..2f7bd3274 100644 --- a/core/src/main/java/dev/vml/es/acm/core/code/Conditions.java +++ b/core/src/main/java/dev/vml/es/acm/core/code/Conditions.java @@ -209,7 +209,7 @@ public InstanceInfo instanceInfo() { // Executable-based public boolean isConsole() { - return Executable.ID_CONSOLE.equals(executableId()); + return Executable.CONSOLE_ID.equals(executableId()); } public boolean isAutomaticScript() { diff --git a/core/src/main/java/dev/vml/es/acm/core/code/Executable.java b/core/src/main/java/dev/vml/es/acm/core/code/Executable.java index f2741ac10..9bfa0af12 100644 --- a/core/src/main/java/dev/vml/es/acm/core/code/Executable.java +++ b/core/src/main/java/dev/vml/es/acm/core/code/Executable.java @@ -5,7 +5,9 @@ public interface Executable extends Serializable { - String ID_CONSOLE = "console"; + String CONSOLE_ID = "console"; + + String CONSOLE_SCRIPT_PATH = "/conf/acm/settings/script/template/core/console.groovy"; String getId(); diff --git a/core/src/main/java/dev/vml/es/acm/core/code/ExecutableUtils.java b/core/src/main/java/dev/vml/es/acm/core/code/ExecutableUtils.java index 0b27d4b9d..702ff85ae 100644 --- a/core/src/main/java/dev/vml/es/acm/core/code/ExecutableUtils.java +++ b/core/src/main/java/dev/vml/es/acm/core/code/ExecutableUtils.java @@ -13,7 +13,7 @@ private ExecutableUtils() { } public static String nameById(String id) { - if (Executable.ID_CONSOLE.equals(id)) { + if (Executable.CONSOLE_ID.equals(id)) { return "Console"; } if (StringUtils.startsWith(id, ScriptType.AUTOMATIC.root() + "/")) { @@ -30,7 +30,7 @@ public static String nameById(String id) { } public static boolean isIdExplicit(String id) { - return Executable.ID_CONSOLE.equals(id) || StringUtils.startsWith(id, ScriptRepository.ROOT + "/"); + return Executable.CONSOLE_ID.equals(id) || StringUtils.startsWith(id, ScriptRepository.ROOT + "/"); } public static boolean isUserExplicit(String userId) { diff --git a/core/src/main/java/dev/vml/es/acm/core/code/Executor.java b/core/src/main/java/dev/vml/es/acm/core/code/Executor.java index e818aedb4..6778109e9 100644 --- a/core/src/main/java/dev/vml/es/acm/core/code/Executor.java +++ b/core/src/main/java/dev/vml/es/acm/core/code/Executor.java @@ -13,6 +13,9 @@ import dev.vml.es.acm.core.osgi.InstanceInfo; import dev.vml.es.acm.core.osgi.OsgiContext; import dev.vml.es.acm.core.repo.Locker; +import dev.vml.es.acm.core.script.Script; +import dev.vml.es.acm.core.script.ScriptRepository; +import dev.vml.es.acm.core.script.ScriptType; import dev.vml.es.acm.core.util.DateUtils; import dev.vml.es.acm.core.util.ResolverUtils; import dev.vml.es.acm.core.util.StringUtil; @@ -130,6 +133,21 @@ public void onEvent(Event event) { } } + public boolean authorize(Executable executable, String userId) { + return ResolverUtils.queryContentResolver(resolverFactory, userId, resolver -> { + return authorize(executable, resolver); + }); + } + + public boolean authorize(Executable executable, ResourceResolver resolver) { + String scriptPath = executable.getId(); + if (Executable.CONSOLE_ID.equals(executable.getId())) { + scriptPath = Executable.CONSOLE_SCRIPT_PATH; + } + ScriptRepository repository = new ScriptRepository(resolver); + return repository.read(scriptPath).isPresent(); + } + public ExecutionContext createContext( String id, String userId, diff --git a/core/src/main/java/dev/vml/es/acm/core/servlet/ExecuteCodeServlet.java b/core/src/main/java/dev/vml/es/acm/core/servlet/ExecuteCodeServlet.java index deb88bb35..48ae3ece5 100644 --- a/core/src/main/java/dev/vml/es/acm/core/servlet/ExecuteCodeServlet.java +++ b/core/src/main/java/dev/vml/es/acm/core/servlet/ExecuteCodeServlet.java @@ -50,6 +50,10 @@ protected void doPost(SlingHttpServletRequest request, SlingHttpServletResponse } Code code = input.getCode(); + if (!executor.authorize(code, request.getResourceResolver())) { + respondJson(response, forbidden(String.format("Code from '%s' is not authorized!", code.getId()))); + return; + } ExecutionMode mode = ExecutionMode.of(input.getMode()).orElse(null); if (mode == null) { diff --git a/core/src/main/java/dev/vml/es/acm/core/servlet/ExecuteScriptServlet.java b/core/src/main/java/dev/vml/es/acm/core/servlet/ExecuteScriptServlet.java deleted file mode 100644 index f1cd0580a..000000000 --- a/core/src/main/java/dev/vml/es/acm/core/servlet/ExecuteScriptServlet.java +++ /dev/null @@ -1,72 +0,0 @@ -package dev.vml.es.acm.core.servlet; - -import static dev.vml.es.acm.core.util.ServletResult.*; -import static dev.vml.es.acm.core.util.ServletUtils.respondJson; -import static dev.vml.es.acm.core.util.ServletUtils.stringParam; - -import dev.vml.es.acm.core.code.*; -import dev.vml.es.acm.core.script.Script; -import dev.vml.es.acm.core.script.ScriptRepository; -import java.io.IOException; -import javax.servlet.Servlet; -import org.apache.sling.api.SlingHttpServletRequest; -import org.apache.sling.api.SlingHttpServletResponse; -import org.apache.sling.api.servlets.ServletResolverConstants; -import org.apache.sling.api.servlets.SlingAllMethodsServlet; -import org.osgi.service.component.annotations.Component; -import org.osgi.service.component.annotations.Reference; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -@Component( - immediate = true, - service = Servlet.class, - property = { - ServletResolverConstants.SLING_SERVLET_METHODS + "=POST", - ServletResolverConstants.SLING_SERVLET_EXTENSIONS + "=json", - ServletResolverConstants.SLING_SERVLET_RESOURCE_TYPES + "=" + ExecuteScriptServlet.RT - }) -public class ExecuteScriptServlet extends SlingAllMethodsServlet { - - public static final String RT = "acm/api/execute-script"; - - private static final Logger LOG = LoggerFactory.getLogger(ExecuteScriptServlet.class); - - private static final String PATH_PARAM = "path"; - - @Reference - private transient Executor executor; - - @Override - protected void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException { - String path = stringParam(request, PATH_PARAM); - - try { - Script script = new ScriptRepository(request.getResourceResolver()) - .read(path) - .orElse(null); - if (script == null) { - respondJson(response, badRequest(String.format("Script at path '%s' not found!", path))); - return; - } - - try (ExecutionContext context = executor.createContext( - ExecutionId.generate(), - request.getResourceResolver().getUserID(), - ExecutionMode.RUN, - script, - new InputValues(), - request.getResourceResolver(), - new CodeOutputMemory())) { - Execution execution = executor.execute(context); - - respondJson(response, ok(String.format("Script at path '%s' executed successfully", path), execution)); - } - } catch (Exception e) { - LOG.error("Cannot execute script at path '{}'", path, e); - respondJson( - response, - error(String.format("Script at path '%s' cannot be executed. Error: %s", path, e.getMessage()))); - } - } -} diff --git a/core/src/main/java/dev/vml/es/acm/core/servlet/QueueCodeServlet.java b/core/src/main/java/dev/vml/es/acm/core/servlet/QueueCodeServlet.java index d0f43c745..4a9b2b33f 100644 --- a/core/src/main/java/dev/vml/es/acm/core/servlet/QueueCodeServlet.java +++ b/core/src/main/java/dev/vml/es/acm/core/servlet/QueueCodeServlet.java @@ -55,6 +55,10 @@ protected void doPost(SlingHttpServletRequest request, SlingHttpServletResponse } Code code = input.getCode(); + if (!executor.authorize(code, request.getResourceResolver())) { + respondJson(response, forbidden(String.format("Code from '%s' is not authorized!", code.getId()))); + return; + } try (ExecutionContext context = executor.createContext( ExecutionId.generate(), diff --git a/core/src/main/java/dev/vml/es/acm/core/servlet/StateServlet.java b/core/src/main/java/dev/vml/es/acm/core/servlet/StateServlet.java index f06cb14f8..b8eb72a86 100644 --- a/core/src/main/java/dev/vml/es/acm/core/servlet/StateServlet.java +++ b/core/src/main/java/dev/vml/es/acm/core/servlet/StateServlet.java @@ -4,13 +4,17 @@ import static dev.vml.es.acm.core.util.ServletResult.ok; import static dev.vml.es.acm.core.util.ServletUtils.respondJson; +import dev.vml.es.acm.core.code.Code; +import dev.vml.es.acm.core.code.Executable; import dev.vml.es.acm.core.code.ExecutionQueue; +import dev.vml.es.acm.core.code.Executor; import dev.vml.es.acm.core.gui.SpaSettings; import dev.vml.es.acm.core.instance.HealthChecker; import dev.vml.es.acm.core.instance.HealthStatus; import dev.vml.es.acm.core.mock.MockHttpFilter; import dev.vml.es.acm.core.mock.MockStatus; import dev.vml.es.acm.core.osgi.InstanceInfo; +import dev.vml.es.acm.core.state.Permissions; import dev.vml.es.acm.core.state.State; import java.io.IOException; import javax.servlet.Servlet; @@ -51,12 +55,16 @@ public class StateServlet extends SlingAllMethodsServlet { @Reference private transient SpaSettings spaSettings; + @Reference + private transient Executor executor; + @Override protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException { try { HealthStatus healthStatus = healthChecker.checkStatus(); MockStatus mockStatus = mockHttpFilter.checkStatus(); - State state = new State(spaSettings, healthStatus, mockStatus, instanceInfo.getSettings()); + Permissions permissions = new Permissions(executor.authorize(Code.consoleMinimal(), request.getResourceResolver())); + State state = new State(spaSettings, healthStatus, mockStatus, instanceInfo.getSettings(), permissions); respondJson(response, ok("State read successfully", state)); } catch (Exception e) { diff --git a/core/src/main/java/dev/vml/es/acm/core/state/Permissions.java b/core/src/main/java/dev/vml/es/acm/core/state/Permissions.java new file mode 100644 index 000000000..f8939179d --- /dev/null +++ b/core/src/main/java/dev/vml/es/acm/core/state/Permissions.java @@ -0,0 +1,16 @@ +package dev.vml.es.acm.core.state; + +import java.io.Serializable; + +public class Permissions implements Serializable { + + private boolean console; + + public Permissions(boolean console) { + this.console = console; + } + + public boolean isConsole() { + return console; + } +} diff --git a/core/src/main/java/dev/vml/es/acm/core/state/State.java b/core/src/main/java/dev/vml/es/acm/core/state/State.java index ed3f0a80e..77cb9c8ca 100644 --- a/core/src/main/java/dev/vml/es/acm/core/state/State.java +++ b/core/src/main/java/dev/vml/es/acm/core/state/State.java @@ -16,15 +16,19 @@ public class State implements Serializable { private final SpaSettings spaSettings; + private final Permissions permissions; + public State( SpaSettings spaSettings, HealthStatus healthStatus, MockStatus mockStatus, - InstanceSettings instanceSettings) { + InstanceSettings instanceSettings, + Permissions permissions) { this.spaSettings = spaSettings; this.healthStatus = healthStatus; this.mockStatus = mockStatus; this.instanceSettings = instanceSettings; + this.permissions = permissions; } public HealthStatus getHealthStatus() { @@ -42,4 +46,8 @@ public InstanceSettings getInstanceSettings() { public SpaSettings getSpaSettings() { return spaSettings; } + + public Permissions getPermissions() { + return permissions; + } } diff --git a/core/src/main/java/dev/vml/es/acm/core/util/ServletResult.java b/core/src/main/java/dev/vml/es/acm/core/util/ServletResult.java index 07e6a987b..161956eeb 100644 --- a/core/src/main/java/dev/vml/es/acm/core/util/ServletResult.java +++ b/core/src/main/java/dev/vml/es/acm/core/util/ServletResult.java @@ -35,6 +35,10 @@ public static ServletResult badRequest(String message) { return new ServletResult<>(HttpServletResponse.SC_BAD_REQUEST, message); } + public static ServletResult forbidden(String message) { + return new ServletResult<>(HttpServletResponse.SC_FORBIDDEN, message); + } + public static ServletResult error(String message) { return new ServletResult<>(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, message); } diff --git a/ui.apps/src/main/content/jcr_root/apps/acm/api/.content.xml b/ui.apps/src/main/content/jcr_root/apps/acm/api/.content.xml index 71edada66..d3ee0e8e1 100644 --- a/ui.apps/src/main/content/jcr_root/apps/acm/api/.content.xml +++ b/ui.apps/src/main/content/jcr_root/apps/acm/api/.content.xml @@ -1,6 +1,4 @@ - - + jcr:primaryType="nt:unstructured"/> diff --git a/ui.apps/src/main/content/jcr_root/apps/acm/api/execute-script/.content.xml b/ui.apps/src/main/content/jcr_root/apps/acm/api/execute-script/.content.xml deleted file mode 100644 index 103161902..000000000 --- a/ui.apps/src/main/content/jcr_root/apps/acm/api/execute-script/.content.xml +++ /dev/null @@ -1,5 +0,0 @@ - - diff --git a/ui.frontend/src/App.tsx b/ui.frontend/src/App.tsx index 5fe3d9262..9fcb9c3f6 100644 --- a/ui.frontend/src/App.tsx +++ b/ui.frontend/src/App.tsx @@ -34,6 +34,9 @@ function App() { role: InstanceRole.AUTHOR, type: InstanceType.CLOUD_CONTAINER, }, + permissions: { + console: false, + }, }); const isFetching = useRef(false); diff --git a/ui.frontend/src/components/Header.tsx b/ui.frontend/src/components/Header.tsx index 27186724e..ba2cceab7 100644 --- a/ui.frontend/src/components/Header.tsx +++ b/ui.frontend/src/components/Header.tsx @@ -7,9 +7,12 @@ import Home from '@spectrum-icons/workflow/Home'; import Maintenance from '@spectrum-icons/workflow/Settings'; import { useLocation } from 'react-router-dom'; import { AppLink } from '../AppLink.tsx'; +import { useAppState } from '../hooks/app.ts'; +import Toggle from './Toggle.tsx'; const Header = () => { const location = useLocation(); + const state = useAppState(); return ( @@ -18,12 +21,14 @@ const Header = () => { - - - + + + + +