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
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ package be.orbinson.aem.groovy.console.configuration.impl
import be.orbinson.aem.groovy.console.configuration.ConfigurationService
import groovy.transform.Synchronized
import groovy.util.logging.Slf4j
import org.apache.jackrabbit.api.JackrabbitSession
import org.apache.jackrabbit.api.security.user.User
import org.apache.jackrabbit.api.security.user.UserManager
import org.apache.sling.api.SlingHttpServletRequest
import javax.jcr.Session
import org.apache.sling.api.resource.ResourceResolverFactory
import org.apache.sling.serviceusermapping.ServiceUserMapped
import org.osgi.framework.BundleContext
import org.osgi.service.component.annotations.Activate
import org.osgi.service.component.annotations.Component
Expand All @@ -22,6 +25,9 @@ class DefaultConfigurationService implements ConfigurationService {
@Reference
private ResourceResolverFactory resourceResolverFactory

@Reference(target = "(!(subServiceName=*))")
private ServiceUserMapped serviceUserMapped

private boolean emailEnabled

private Set<String> emailRecipients
Expand Down Expand Up @@ -100,20 +106,42 @@ class DefaultConfigurationService implements ConfigurationService {
if (bundleContext.getProperty("sling.run.modes") != null) {
author = bundleContext.getProperty("sling.run.modes").contains("author")
}
if (properties.addAemCloudProductAdministrators()) {
def aemCloudAdministrators = System.getenv("aemCloudAdministrators")
if (aemCloudAdministrators) {
LOG.debug("Adding AEM Cloud product administrators group '{}' to allowed groups", aemCloudAdministrators)
allowedGroups = allowedGroups + [aemCloudAdministrators] as Set
allowedScheduledJobsGroups = allowedScheduledJobsGroups + [aemCloudAdministrators] as Set
}
}
}

private boolean isAdminOrAllowedGroupMember(SlingHttpServletRequest request, Set<String> groupIds) {
resourceResolverFactory.getServiceResourceResolver(null).withCloseable { resourceResolver ->
def userManager = resourceResolver.adaptTo(UserManager);
def userManager = resourceResolver.adaptTo(UserManager)
if (userManager == null) {
// AEM provides a ResourceResolver -> UserManager adapter factory; Sling Starter does not.
// Fall back to obtaining the UserManager directly from the JCR session.
def session = resourceResolver.adaptTo(Session)
if (session instanceof JackrabbitSession) {
userManager = ((JackrabbitSession) session).userManager
}
}
if (userManager != null) {
def user = resourceResolver.adaptTo(UserManager).getAuthorizable(request.userPrincipal) as User
def principal = request.getUserPrincipal()
def user = (principal != null ? userManager.getAuthorizable(principal) : null) as User
?: userManager.getAuthorizable(request.getResourceResolver().getUserID()) as User
if (user == null) {
LOG.debug("Could not find user in UserManager, denying access")
return false
}
def memberOfGroupIds = user.memberOf()*.ID

LOG.debug("member of group IDs : {}, allowed group IDs : {}", memberOfGroupIds, groupIds)

user.admin || (groupIds ? memberOfGroupIds.intersect(groupIds as Iterable) : false)
} else {
LOG.debug("UserManager not available, probably in a Sling Based application, falling back to is admin check")
LOG.debug("UserManager not available, falling back to is admin check")
return request.getResourceResolver().getUserID() == "admin"
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,9 @@
description = "If enabled, a script will be able to be replicated from an author and executed on all default replication agents."
)
boolean distributedExecutionEnabled() default false;

@AttributeDefinition(name = "Add AEM Cloud product administrators?",
description = "If enabled, the group referenced by the 'aemCloudAdministrators' environment variable (set by AEM as a Cloud Service) will automatically be added to the allowed groups. Has no effect if the environment variable is not set (e.g. on AEM 6.5)."
)
boolean addAemCloudProductAdministrators() default true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import be.orbinson.aem.groovy.console.response.impl.DefaultRunScriptResponse
import io.wcm.testing.mock.aem.junit5.AemContext
import io.wcm.testing.mock.aem.junit5.AemContextExtension
import org.apache.commons.lang3.exception.ExceptionUtils
import org.apache.sling.serviceusermapping.ServiceUserMapped
import org.apache.sling.testing.mock.sling.ResourceResolverType
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
Expand All @@ -26,6 +27,7 @@ class DefaultAuditServiceTest {
@BeforeEach
void beforeEach() {
context.build().resource("/var/groovyconsole").commit();
context.registerService(ServiceUserMapped, {} as ServiceUserMapped)
context.registerInjectActivateService(new DefaultConfigurationService())
auditService = context.registerInjectActivateService(new DefaultAuditService())
}
Expand Down
4 changes: 4 additions & 0 deletions it.tests/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@
<!-- <value>-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5000</value>-->
<!--</vmOptions>-->
</launcherArguments>
<environmentVariables>
<!-- Simulates the AEM Cloud aemCloudAdministrators environment variable -->
<aemCloudAdministrators>it-test-cloud-group</aemCloudAdministrators>
</environmentVariables>
<startTimeoutSeconds>180</startTimeoutSeconds>
</launch>
</launches>
Expand Down
9 changes: 9 additions & 0 deletions it.tests/src/main/features/groovyconsole-it.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"repoinit:TEXT|true": [
"create user it-test-unprivileged with password ItTest1234",
"create group it-test-cloud-group",
"create user it-test-cloud-user with password ItTest1234",
"add it-test-cloud-user to group it-test-cloud-group",
"set ACL for it-test-cloud-group\n allow jcr:read on /\nend"
]
}
2 changes: 1 addition & 1 deletion it.tests/src/main/features/groovyconsole.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"content-packages:ARTIFACTS|required":[
"content-packages:ARTIFACTS|required": [
{
"id": "be.orbinson.aem:aem-groovy-console-all:zip:${project.version}"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ class GroovyConsoleServiceIT {
private static final String BASE_URL = "http://localhost:" + SLING_PORT;
private static final String AUTH_HEADER = "Basic " + Base64.encodeBase64String("admin:admin".getBytes(StandardCharsets.UTF_8));

// Users and groups created via repoinit in groovyconsole-it.json feature model
private static final String UNPRIVILEGED_USER = "it-test-unprivileged";
private static final String CLOUD_USER = "it-test-cloud-user";
private static final String TEST_PASSWORD = "ItTest1234";

private static CloseableHttpClient httpClient;

@BeforeAll
Expand Down Expand Up @@ -175,14 +180,68 @@ void testGroovyJsonStarImport() throws Exception {
assertEquals("{\"a\":1}", response.get("result").getAsString());
}

@Test
void testUnauthenticatedUserCannotExecuteScript() throws Exception {
HttpPost post = new HttpPost(BASE_URL + "/bin/groovyconsole/post");
List<BasicNameValuePair> params = new ArrayList<>();
params.add(new BasicNameValuePair("script", "return 1"));
post.setEntity(new UrlEncodedFormEntity(params, StandardCharsets.UTF_8));
// No Authorization header

try (CloseableHttpResponse response = httpClient.execute(post)) {
int status = response.getStatusLine().getStatusCode();
assertTrue(status == 401 || status == 403,
"Expected 401 or 403 for unauthenticated request, got " + status);
}
}

/**
* Verifies that a user without any allowed group membership cannot execute scripts.
* The 'it-test-unprivileged' user is created at startup via repoinit in groovyconsole.json.
*/
@Test
void testNonPrivilegedUserCannotExecuteScript() throws Exception {
int status = executeScriptStatus("return 1", UNPRIVILEGED_USER, TEST_PASSWORD);
assertTrue(status == 401 || status == 403,
"Expected 401 or 403 for non-privileged user, got " + status);
}

/**
* Verifies that a user in a group referenced by the 'aemCloudAdministrators' system property
* (simulating the AEM Cloud environment variable) is automatically granted access.
*
* The Sling JVM is started with environmentVariable aemCloudAdministrators=it-test-cloud-group (see pom.xml),
* and 'it-test-cloud-user' is added to 'it-test-cloud-group' via repoinit in groovyconsole-it.json.
*/
@Test
void testAemCloudProductAdministratorsGroupGrantsAccess() throws Exception {
JsonObject response = executeScript("return 'cloud-access-granted'", CLOUD_USER, TEST_PASSWORD);
assertNotNull(response, "Could not get response from API");
assertEquals("", response.get("exceptionStackTrace").getAsString());
assertEquals("cloud-access-granted", response.get("result").getAsString());
}

private static boolean isHealthy() {
try {
HttpGet healthCheck = new HttpGet(BASE_URL + "/system/health.json?tags=systemalive,groovyconsole");
healthCheck.addHeader("Authorization", AUTH_HEADER);
try (CloseableHttpResponse response = httpClient.execute(healthCheck)) {
String body = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
JsonObject jsonResponse = JsonParser.parseString(body).getAsJsonObject();
return "OK".equals(jsonResponse.get("overallResult").getAsString());
if (!"OK".equals(jsonResponse.get("overallResult").getAsString())) {
return false;
}
}
// Also verify the ScriptPostServlet is actually registered and available.
// The health check may pass before the servlet resource providers are bound
// (returning 0 groovyconsole-tagged results counts as OK). A GET to the
// endpoint returns 405 when the servlet is up, 404 when it is not yet ready.
HttpGet servletCheck = new HttpGet(BASE_URL + "/bin/groovyconsole/post");
servletCheck.addHeader("Authorization", AUTH_HEADER);
try (CloseableHttpResponse response = httpClient.execute(servletCheck)) {
int status = response.getStatusLine().getStatusCode();
EntityUtils.consume(response.getEntity());
return status != 404;
}
} catch (Exception e) {
return false;
Expand All @@ -207,17 +266,35 @@ private static JsonObject doGet(String path) throws IOException {
}
}

private static JsonObject executeScript(String script) throws IOException {
private static int executeScriptStatus(String script, String user, String password) throws IOException {
HttpPost post = new HttpPost(BASE_URL + "/bin/groovyconsole/post");
List<BasicNameValuePair> params = new java.util.ArrayList<>();
post.addHeader("Authorization", authHeader(user, password));
List<BasicNameValuePair> params = new ArrayList<>();
params.add(new BasicNameValuePair("script", script));
post.setEntity(new UrlEncodedFormEntity(params, StandardCharsets.UTF_8));
post.addHeader("Authorization", AUTH_HEADER);
try (CloseableHttpResponse response = httpClient.execute(post)) {
EntityUtils.consume(response.getEntity());
return response.getStatusLine().getStatusCode();
}
}

private static JsonObject executeScript(String script) throws IOException {
return executeScript(script, AUTH_HEADER);
}

private static JsonObject executeScript(String script, String user, String password) throws IOException {
return executeScript(script, authHeader(user, password));
}

private static JsonObject executeScript(String script, String authHeader) throws IOException {
HttpPost post = new HttpPost(BASE_URL + "/bin/groovyconsole/post");
post.addHeader("Authorization", authHeader);
List<BasicNameValuePair> params = new ArrayList<>();
params.add(new BasicNameValuePair("script", script));
post.setEntity(new UrlEncodedFormEntity(params, StandardCharsets.UTF_8));
try (CloseableHttpResponse response = httpClient.execute(post)) {
assertEquals(200, response.getStatusLine().getStatusCode(),
"Expected HTTP 200 but got " + response.getStatusLine());

String body = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);
try {
return JsonParser.parseString(body).getAsJsonObject();
Expand All @@ -227,4 +304,8 @@ private static JsonObject executeScript(String script) throws IOException {
}
}
}

private static String authHeader(String user, String password) {
return "Basic " + Base64.encodeBase64String((user + ":" + password).getBytes(StandardCharsets.UTF_8));
}
}
Loading