Skip to content
Merged
Show file tree
Hide file tree
Changes from 35 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
22 changes: 21 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,13 @@ jobs:
path: all/target/*.zip
retention-days: 3

- name: Upload ACM Content Example Package Artifact
uses: actions/upload-artifact@v4
with:
name: acm-content-example
path: ui.content.example/target/*.zip
retention-days: 3

test:
name: 'Test ACM Package'
runs-on: ubuntu-latest
Expand Down Expand Up @@ -91,6 +98,12 @@ jobs:
name: acm-package
path: ./aem/home/lib/acm

- name: Download ACM Content Example Artifact
uses: actions/download-artifact@v4
with:
name: acm-content-example
path: ./aem/home/lib/acm-content-example

- name: Setup AEM Instance
run: |
set -e
Expand All @@ -113,6 +126,13 @@ jobs:
sh aemw instance restart
echo "Deployed ACM Package '$PACKAGE_PATH'"

- name: Deploy ACM Content Example Package
run: |
PACKAGE_PATH=$(find ./aem/home/lib/acm-content-example -name "*.zip" | head -1)
echo "Deploying ACM Content Example Package '$PACKAGE_PATH'"
sh aemw pkg deploy --file "$PACKAGE_PATH"
echo "Deployed ACM Content Example Package '$PACKAGE_PATH'"

- name: Run Playwright Tests
working-directory: ./test/e2e
run: npx playwright test
Expand All @@ -121,6 +141,6 @@ jobs:
uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
name: acm-playwright-report
path: ./test/e2e/playwright-report/
retention-days: 30
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
### Project-specific

ui.apps/src/main/content/jcr_root/apps/acm/spa
ui.apps/src/main/content/jcr_root/apps/acm/gui/spa/build/
/var

# Created by https://www.gitignore.io/api/eclipse,java,maven
Expand Down
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
"search.exclude": {
"**/aem/home": true,
"**/node": true
}
},
"java.configuration.updateBuildConfiguration": "interactive"
}
83 changes: 62 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ It works seamlessly across AEM on-premise, AMS, and AEMaaCS environments.
- [Permissions Management](#permissions-management)
- [Data Imports \& Exports](#data-imports--exports)
- [Installation](#installation)
- [Package Installation](#package-installation)
- [Tools Access Configuration](#tools-access-configuration)
- [Feature Permissions](#feature-permissions)
- [API Permissions](#api-permissions)
- [Compatibility](#compatibility)
- [Documentation](#documentation)
- [Usage](#usage)
Expand Down Expand Up @@ -107,6 +111,8 @@ By simplifying data import implementation, ACM allows developers to focus more o

## Installation

### Package Installation

The ready-to-install AEM packages are available on:

- [GitHub releases](https://github.com/wttech/acm/releases).
Expand Down Expand Up @@ -155,27 +161,62 @@ Adjust file 'all/pom.xml':

Repeat the same for [ui.content.example](https://central.sonatype.com/artifact/dev.vml.es/acm.ui.content.example) package if you want to install demonstrative ACM scripts to get you started quickly.

3. Consider refining the ACL settings

The default settings are defined in the [repo init OSGi config](https://github.com/wttech/acm/blob/main/ui.config/src/main/content/jcr_root/apps/acm-config/osgiconfig/config/org.apache.sling.jcr.repoinit.RepositoryInitializer~acmcore.config), which effectively restrict access to the tool and script execution to administrators only - a recommended practice for production environments.
If you require further customization, you can create your own repo init OSGi config to override or extend the default configuration.

For example:
```ini
service.ranking=I"100"
scripts=["
set ACL for everyone
deny jcr:read on /apps/acm
deny jcr:read on /apps/cq/core/content/nav/tools/acm
end

create group acm-users
set ACL for acm-users
allow jcr:read on /apps/acm
allow jcr:read on /apps/cq/core/content/nav/tools/acm
end
"]
```
### Tools Access Configuration

The default settings are defined in the [repo init OSGi config](https://github.com/wttech/acm/blob/main/ui.config/src/main/content/jcr_root/apps/acm-config/osgiconfig/config/org.apache.sling.jcr.repoinit.RepositoryInitializer~acmcore.config), which effectively restrict access to the tool and script execution to administrators only - a recommended practice for production environments.

If you require further customization, you can create your own repo init OSGi config to override or extend the default configuration.

#### Feature Permissions

ACM supports fine-grained permission control through individual features. This allows you to grant specific capabilities to different user groups without providing full access to ACM tool. For a complete list of available features, see the [ACM features directory](https://github.com/wttech/acm/tree/main/ui.apps/src/main/content/jcr_root/apps/acm/feature).

**Example: Create groups for full and limited access:**

```ini
service.ranking=I"100"
scripts=["
set ACL for everyone
deny jcr:read on /apps/cq/core/content/nav/tools/acm
deny jcr:read on /apps/acm
end

create group acm-admins
set ACL for acm-admins
allow jcr:read on /apps/cq/core/content/nav/tools/acm
allow jcr:read on /apps/acm
end

create group acm-script-users
set ACL for acm-script-users
allow jcr:read on /apps/cq/core/content/nav/tools/acm
allow jcr:read on /apps/acm/gui
allow jcr:read on /apps/acm/api

allow jcr:read on /apps/acm/feature/script/list
allow jcr:read on /apps/acm/feature/script/view
allow jcr:read on /apps/acm/feature/execution/view

allow jcr:read on /conf/acm/settings/script
end
"]
```

Later on when AEM is running, just assign users to the created groups (`acm-admins` or `acm-script-users`) to grant them the corresponding access.

#### API Permissions

Access to ACM's REST API endpoints is controlled through nodes under `/apps/acm/api`. For a complete list of available endpoints, see the [ACM API directory](https://github.com/wttech/acm/tree/main/ui.apps/src/main/content/jcr_root/apps/acm/api).

**Important:** Code execution requires authorization at three levels: API endpoint, feature, and e.g. script path. Example:

```ini
set ACL for acm-automation-user
allow jcr:read on /apps/acm/api
allow jcr:read on /apps/acm/feature
allow jcr:read on /conf/acm/settings/script
end
```

## Compatibility

Expand Down
2 changes: 1 addition & 1 deletion Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ tasks:
desc: build & deploy frontend to instances
cmds:
- (cd ui.frontend && npm install && npm run build)
- sh aemw content push --dir 'ui.apps/src/main/content/jcr_root/apps/acm/spa'
- sh aemw content push --dir 'ui.apps/src/main/content/jcr_root/apps/acm/gui/spa/build'

develop:frontend:dev:
desc: build & deploy frontend to instances in development mode
Expand Down
2 changes: 2 additions & 0 deletions core/src/main/java/dev/vml/es/acm/core/AcmConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ public class AcmConstants {

public static final String NOTIFIER_ID = "acm";

public static final String APPS_ROOT = "/apps/acm";

public static final String SETTINGS_ROOT = "/conf/acm/settings";

public static final String VAR_ROOT = "/var/acm";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@

public class PermissionsOptions {

public static final String GLOB_STRICT = "strict";

private static final String GLOB_STRICT_EFFECTIVE = "";

private String path;

private List<String> permissions = Collections.emptyList();
Expand All @@ -33,6 +37,11 @@ public void setPath(String path) {
this.path = path;
}

public void setPathStrict(String path) {
this.path = path;
this.glob = GLOB_STRICT_EFFECTIVE;
}

public List<String> getPermissions() {
return permissions;
}
Expand All @@ -46,7 +55,7 @@ public String getGlob() {
}

public void setGlob(String glob) {
this.glob = glob;
this.glob = GLOB_STRICT.equalsIgnoreCase(glob) ? GLOB_STRICT_EFFECTIVE : glob;
}

public List<String> getTypes() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@

public class PermissionsOptions extends AuthorizableOptions {

public static final String GLOB_STRICT = "strict";

private static final String GLOB_STRICT_EFFECTIVE = "";

private String path;

private List<String> permissions = Collections.emptyList();
Expand All @@ -31,6 +35,11 @@ public void setPath(String path) {
this.path = path;
}

public void setPathStrict(String path) {
this.path = path;
this.glob = GLOB_STRICT_EFFECTIVE;
}

public List<String> getPermissions() {
return permissions;
}
Expand All @@ -44,7 +53,7 @@ public String getGlob() {
}

public void setGlob(String glob) {
this.glob = glob;
this.glob = GLOB_STRICT.equalsIgnoreCase(glob) ? GLOB_STRICT_EFFECTIVE : glob;
}

public List<String> getTypes() {
Expand Down
52 changes: 34 additions & 18 deletions core/src/main/java/dev/vml/es/acm/core/code/CodePrintStream.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public class CodePrintStream extends PrintStream {
public static final String[] LOGGER_NAMES = {LOGGER_NAME_ACL, LOGGER_NAME_REPO};

// have to match pattern in 'monaco/log.ts'
private static final DateTimeFormatter LOGGER_TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");
private static final DateTimeFormatter TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");

private final Logger logger;

Expand All @@ -40,6 +40,8 @@ public class CodePrintStream extends PrintStream {

private final LogAppender logAppender;

private boolean printerTimestamps;

public CodePrintStream(OutputStream output, String id) {
super(output);

Expand All @@ -48,6 +50,8 @@ public CodePrintStream(OutputStream output, String id) {
this.loggerTimestamps = true;
this.logger = loggerContext.getLogger(id);
this.logAppender = new LogAppender();

this.printerTimestamps = true;
}

private class LogAppender extends AppenderBase<ILoggingEvent> {
Expand All @@ -60,7 +64,7 @@ protected void append(ILoggingEvent event) {
if (loggerTimestamps) {
LocalDateTime eventTime = LocalDateTime.ofInstant(
Instant.ofEpochMilli(event.getTimeStamp()), ZoneId.systemDefault());
String timestamp = eventTime.format(LOGGER_TIMESTAMP_FORMATTER);
String timestamp = eventTime.format(TIMESTAMP_FORMATTER);
println(timestamp + " [" + level + "] " + event.getFormattedMessage());
} else {
println('[' + level + "] " + event.getFormattedMessage());
Expand Down Expand Up @@ -116,7 +120,7 @@ public boolean isLoggerTimestamps() {
return loggerTimestamps;
}

public void withLoggerTimestamps(boolean flag) {
public void setLoggerTimestamps(boolean flag) {
this.loggerTimestamps = flag;
}

Expand Down Expand Up @@ -147,33 +151,45 @@ public void fromLoggers(List<String> loggerNames) {
loggerNames.forEach(this::fromLogger);
}

public void setPrinterTimestamps(boolean flag) {
this.printerTimestamps = flag;
}

public boolean isPrinterTimestamps() {
return printerTimestamps;
}

public void printTimestamped(String level, String message) {
printTimestamped(CodePrintLevel.of(level), message);
}

public void printTimestamped(CodePrintLevel level, String message) {
if (printerTimestamps) {
LocalDateTime now = LocalDateTime.now();
String timestamp = now.format(TIMESTAMP_FORMATTER);
println(timestamp + " [" + level + "] " + message);
} else {
println("[" + level + "] " + message);
}
}

public void success(String message) {
printStamped(CodePrintLevel.SUCCESS, message);
printTimestamped(CodePrintLevel.SUCCESS, message);
}

public void info(String message) {
printStamped(CodePrintLevel.INFO, message);
printTimestamped(CodePrintLevel.INFO, message);
}

public void error(String message) {
printStamped(CodePrintLevel.ERROR, message);
printTimestamped(CodePrintLevel.ERROR, message);
}

public void warn(String message) {
printStamped(CodePrintLevel.WARN, message);
printTimestamped(CodePrintLevel.WARN, message);
}

public void debug(String message) {
printStamped(CodePrintLevel.DEBUG, message);
}

public void printStamped(String level, String message) {
printStamped(CodePrintLevel.of(level), message);
}

public void printStamped(CodePrintLevel level, String message) {
LocalDateTime now = LocalDateTime.now();
String timestamp = now.format(LOGGER_TIMESTAMP_FORMATTER);
println(timestamp + " [" + level + "] " + message);
printTimestamped(CodePrintLevel.DEBUG, message);
}
}
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
Loading