Skip to content
Open
Show file tree
Hide file tree
Changes from all 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"
}
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: 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
3 changes: 2 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
@@ -1,5 +1,6 @@
package dev.vml.es.acm.core.gui;

import dev.vml.es.acm.core.AcmConstants;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand All @@ -15,7 +16,7 @@ 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