Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
e8367f2
Authorized execution
krystian-panek-vmltech Oct 30, 2025
708933c
Console permissions
krystian-panek-vmltech Oct 30, 2025
4c2efc5
Compile fix
krystian-panek-vmltech Oct 30, 2025
f095f2a
Minor
krystian-panek-vmltech Oct 30, 2025
7391341
Perms by feature nodes
krystian-panek-vmltech Oct 30, 2025
d881609
Merge remote-tracking branch 'origin/main' into authorized-execution
krystian-panek-vmltech Nov 13, 2025
0d0e571
Features
krystian-panek-vmltech Nov 13, 2025
b085798
Docs
krystian-panek-vmltech Nov 13, 2025
26aee12
XLS imprs
krystian-panek-vmltech Nov 13, 2025
40369b4
Minor
krystian-panek-vmltech Nov 13, 2025
4011f14
Feature node based authorization
krystian-panek-vmltech Nov 14, 2025
64c6d0d
Script and console execute feature
krystian-panek-vmltech Nov 14, 2025
d1e623f
Minors
krystian-panek-vmltech Nov 14, 2025
1c958f9
Feature perms
krystian-panek-vmltech Nov 14, 2025
c35e423
SPA build moved
krystian-panek-vmltech Nov 14, 2025
5b3e227
Doc
krystian-panek-vmltech Nov 14, 2025
029c5f0
Doc
krystian-panek-vmltech Nov 14, 2025
052ceaa
Minors
krystian-panek-vmltech Nov 14, 2025
3cac4ab
Dashboard not protected
krystian-panek-vmltech Nov 14, 2025
ff5fae8
Tool permissions test
krystian-panek-vmltech Nov 17, 2025
e5fe152
Tool access e2e spec
krystian-panek-vmltech Nov 17, 2025
ac0754e
Minor
krystian-panek-vmltech Nov 17, 2025
816efdd
Test fix
krystian-panek-vmltech Nov 17, 2025
c013c0e
Test green
krystian-panek-vmltech Nov 17, 2025
dbee482
Refactoring
krystian-panek-vmltech Nov 17, 2025
72f8748
Test fix
krystian-panek-vmltech Nov 17, 2025
bf17de4
Test fix
krystian-panek-vmltech Nov 17, 2025
4bc4d1c
Permission fixes
krystian-panek-vmltech Nov 17, 2025
4cd0f70
Strict path permission
krystian-panek-vmltech Nov 17, 2025
ff8d8c2
Path strict + test fix
krystian-panek-vmltech Nov 17, 2025
09e8876
Minor
krystian-panek-vmltech Nov 17, 2025
27ff804
Content example fix
krystian-panek-vmltech Nov 17, 2025
2e37971
Minor
krystian-panek-vmltech Nov 17, 2025
c859e7e
Less LOC
krystian-panek-vmltech Nov 17, 2025
33955fb
Minor
krystian-panek-vmltech Nov 17, 2025
055d710
Clean up
krystian-panek-vmltech Nov 17, 2025
a3b62d7
Hardening
krystian-panek-vmltech Nov 17, 2025
c75bfbd
Prettier ignore
krystian-panek-vmltech Nov 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
4 changes: 3 additions & 1 deletion core/src/main/java/dev/vml/es/acm/core/code/Executable.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() + "/")) {
Expand All @@ -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) {
Expand Down
18 changes: 18 additions & 0 deletions core/src/main/java/dev/vml/es/acm/core/code/Executor.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
16 changes: 16 additions & 0 deletions core/src/main/java/dev/vml/es/acm/core/state/Permissions.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
10 changes: 9 additions & 1 deletion core/src/main/java/dev/vml/es/acm/core/state/State.java
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -42,4 +46,8 @@ public InstanceSettings getInstanceSettings() {
public SpaSettings getSpaSettings() {
return spaSettings;
}

public Permissions getPermissions() {
return permissions;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ public static ServletResult<Void> badRequest(String message) {
return new ServletResult<>(HttpServletResponse.SC_BAD_REQUEST, message);
}

public static ServletResult<Void> forbidden(String message) {
return new ServletResult<>(HttpServletResponse.SC_FORBIDDEN, message);
}

public static ServletResult<Void> error(String message) {
return new ServletResult<>(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, message);
}
Expand Down
4 changes: 1 addition & 3 deletions ui.apps/src/main/content/jcr_root/apps/acm/api/.content.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:nt="http://www.jcp.org/jcr/nt/1.0" xmlns:rep="internal"
jcr:mixinTypes="[rep:AccessControllable]"
jcr:primaryType="nt:unstructured">
<execute-script/>
</jcr:root>
jcr:primaryType="nt:unstructured"/>

This file was deleted.

3 changes: 3 additions & 0 deletions ui.frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ function App() {
role: InstanceRole.AUTHOR,
type: InstanceType.CLOUD_CONTAINER,
},
permissions: {
console: false,
},
});

const isFetching = useRef(false);
Expand Down
17 changes: 11 additions & 6 deletions ui.frontend/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Flex justifyContent="center" gap="size-100" marginBottom="size-200">
Expand All @@ -18,12 +21,14 @@ const Header = () => {
<Home />
</Button>
</AppLink>
<AppLink to="/console">
<Button variant={location.pathname.startsWith('/console') ? 'accent' : 'primary'} style="outline">
<Draft />
<Text>Console</Text>
</Button>
</AppLink>
<Toggle when={state.permissions.console}>
<AppLink to="/console">
<Button variant={location.pathname.startsWith('/console') ? 'accent' : 'primary'} style="outline">
<Draft />
<Text>Console</Text>
</Button>
</AppLink>
</Toggle>
<AppLink to="/scripts">
<Button variant={location.pathname.startsWith('/scripts') ? 'accent' : 'primary'} style="outline">
<FileCode />
Expand Down
5 changes: 5 additions & 0 deletions ui.frontend/src/types/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export type State = {
healthStatus: HealthStatus;
mockStatus: MockStatus;
instanceSettings: InstanceSettings;
permissions: Permissions;
};

export type SpaSettings = {
Expand All @@ -65,6 +66,10 @@ export type MockStatus = {
enabled: boolean;
};

export type Permissions = {
console: boolean;
};

export enum ExecutionFormat {
SUMMARY = 'SUMMARY',
FULL = 'FULL',
Expand Down