Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
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
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"
}
86 changes: 65 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,65 @@ 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 a group for script viewers with 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-viewers
set ACL for acm-script-viewers
allow jcr:read on /apps/cq/core/content/nav/tools/acm
allow jcr:read on /apps/acm/gui

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

allow jcr:read on /apps/acm/feature/execution/view
end
"]
```

#### API Permissions

In addition to UI features, you can also control access to ACM's REST API endpoints. The API permissions are managed through nodes under */apps/acm/api*. For a complete list of available API endpoints, see the [ACM API directory](https://github.com/wttech/acm/tree/main/ui.apps/src/main/content/jcr_root/apps/acm/api).

**Important:** API access alone may not be sufficient for code execution. Features are evaluated on both feature and API levels. For example, granting only */apps/acm/api/execute-code* permission will not allow code execution from console or scripts unless the user also has access to the corresponding features:

```ini
set ACL for acm-automation-user
# API access
allow jcr:read on /apps/acm/api/queue-code
allow jcr:read on /apps/acm/api/execute-code
# Feature access
allow jcr:read on /apps/acm/feature/scripts/execute
# Scripts access
allow jcr:read on /conf/acm/settings/script/manual/acme
end
```

This two-layer permission model ensures that both API and feature-level authorizations are properly enforced, preventing unauthorized code execution even when API access is granted.

## Compatibility

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
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
30 changes: 29 additions & 1 deletion 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,8 @@
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.ScriptRepository;
import dev.vml.es.acm.core.state.Permissions;
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 +132,32 @@ 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) {
return isFeatureEnabled(executable, resolver) && isExecutableAvailable(executable, resolver);
}

private boolean isFeatureEnabled(Executable executable, ResourceResolver resolver) {
if (Executable.CONSOLE_ID.equals(executable.getId())) {
return Permissions.check(Permissions.Feature.CONSOLE_EXECUTE, resolver);
}
return Permissions.check(Permissions.Feature.SCRIPT_EXECUTE, resolver);
}

private boolean isExecutableAvailable(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 Expand Up @@ -223,7 +251,7 @@ private ContextualExecution executeInternal(ExecutionContext context) {
if (config.logPrintingEnabled()) {
context.getOut().fromSelfLogger();
context.getOut().fromLoggers(config.logPrintingNames());
context.getOut().withLoggerTimestamps(config.logPrintingTimestamps());
context.getOut().setLoggerTimestamps(config.logPrintingTimestamps());
}
contentScript.run();

Expand Down
4 changes: 3 additions & 1 deletion core/src/main/java/dev/vml/es/acm/core/gui/Spa.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import dev.vml.es.acm.core.AcmConstants;

@Model(adaptables = SlingHttpServletRequest.class)
public class Spa {

private static final Logger LOG = LoggerFactory.getLogger(Spa.class);

private static final String ASSETS_ROOT = "/apps/acm/spa/assets";
private static final String ASSETS_ROOT = AcmConstants.APPS_ROOT + "/gui/spa/build/assets";

@Self
private SlingHttpServletRequest request;
Expand Down
12 changes: 0 additions & 12 deletions core/src/main/java/dev/vml/es/acm/core/gui/SpaSettings.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ public class SpaSettings implements Serializable {

private long scriptStatsLimit;

private boolean scriptManagementEnabled;

@Activate
@Modified
protected void activate(Config config) {
Expand All @@ -32,7 +30,6 @@ protected void activate(Config config) {
this.executionCodeOutputChunkSize = config.executionCodeOutputChunkSize();
this.executionFileOutputChunkSize = config.executionFileOutputChunkSize();
this.scriptStatsLimit = config.scriptStatsLimit();
this.scriptManagementEnabled = config.scriptManagementEnabled();
}

public long getAppStateInterval() {
Expand All @@ -55,10 +52,6 @@ public long getScriptStatsLimit() {
return scriptStatsLimit;
}

public boolean isScriptManagementEnabled() {
return scriptManagementEnabled;
}

@ObjectClassDefinition(name = "AEM Content Manager - SPA Settings")
public @interface Config {

Expand All @@ -83,10 +76,5 @@ public boolean isScriptManagementEnabled() {
description =
"Limit for the number of historical executions to be considered to calculate the average duration.")
long scriptStatsLimit() default 10;

@AttributeDefinition(
name = "Script Management Enabled",
description = "Enable or disable script management features (delete, save, sync, etc).")
boolean scriptManagementEnabled() default true;
}
}
Loading
Loading