Skip to content

Commit

Permalink
Dedicated OSGi configuration for granting access to UI
Browse files Browse the repository at this point in the history
No longer leverage PID SlingWebConsoleSecurityProvider as
defaults no longer reasonably set in AEMaaCS.
Disable action buttons depending on permissions.

Clean up HistoryUtils to leverage JcrUtils

This closes #781
  • Loading branch information
kwin committed Feb 4, 2025
1 parent 9d1ab3d commit 9638820
Show file tree
Hide file tree
Showing 9 changed files with 161 additions and 157 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ There are also some [advanced configuration options](docs/AdvancedFeatures.md) s

# User Interface

There is a Felix Web Console plugin (at `/system/console/actool`) as well as a Touch UI console (at `/mnt/overlay/netcentric/actool/content/overview.html`) to apply configurations and to inspect previous executions of the tool. Additionally there is a [JMX interface](docs/Jmx.md) for some advanced use cases.
There is a [Felix Web Console plugin (at `/system/console/actool`)](docs/ApplyConfig.md#web-console) as well as a [Touch UI console (at `/mnt/overlay/netcentric/actool/content/overview.html`)](docs/ApplyConfig.md#touch-ui) to apply configurations and to inspect previous executions of the tool. Additionally there is a [JMX interface](docs/Jmx.md) for some advanced use cases. Further information in []

# Applying AC Tool Configurations

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,12 @@ public class HistoryUtils {

public static final String HISTORY_NODE_NAME_PREFIX = "history_";
public static final String NODETYPE_NT_UNSTRUCTURED = "nt:unstructured";
public static final String ACHISTORY_ROOT_NODE = "achistory";
public static final String STATISTICS_ROOT_NODE = "var/statistics";
public static final String ACHISTORY_PATH = "/"+ HistoryUtils.STATISTICS_ROOT_NODE + "/" + HistoryUtils.ACHISTORY_ROOT_NODE;
public static final String ACHISTORY_ROOT_NODE_NAME = "achistory";
public static final String STATISTICS_ROOT_NODE_PATH = "/var/statistics";
public static final String ACHISTORY_PATH = HistoryUtils.STATISTICS_ROOT_NODE_PATH + "/" + HistoryUtils.ACHISTORY_ROOT_NODE_NAME;

private static final String AC_ROOT_PATH_IN_APPS = "/apps/netcentric";
public static final String AC_HISTORY_PATH_IN_APPS = AC_ROOT_PATH_IN_APPS + "/" + ACHISTORY_ROOT_NODE;
public static final String AC_HISTORY_PATH_IN_APPS = AC_ROOT_PATH_IN_APPS + "/" + ACHISTORY_ROOT_NODE_NAME;

public static final String PROPERTY_TIMESTAMP = "timestamp";
private static final String PROPERTY_MESSAGES = "messages";
Expand All @@ -85,10 +85,8 @@ public class HistoryUtils {

public static Node getAcHistoryRootNode(final Session session)
throws RepositoryException {
final Node rootNode = session.getRootNode();
Node statisticsRootNode = safeGetNode(rootNode, STATISTICS_ROOT_NODE, NODETYPE_NT_UNSTRUCTURED);
Node acHistoryRootNode = safeGetNode(statisticsRootNode, ACHISTORY_ROOT_NODE, "sling:OrderedFolder");
return acHistoryRootNode;
Node statisticsRootNode = JcrUtils.getOrCreateByPath(STATISTICS_ROOT_NODE_PATH, NODETYPE_NT_UNSTRUCTURED, session);
return JcrUtils.getOrAddNode(statisticsRootNode, ACHISTORY_ROOT_NODE_NAME, "sling:OrderedFolder");
}

/**
Expand Down Expand Up @@ -139,7 +137,7 @@ public static Node persistHistory(final Session session,
}
name += AcToolExecutionImpl.TRIGGER_SEPARATOR_IN_NODE_NAME + trigger;

Node newHistoryNode = safeGetNode(acHistoryRootNode, name, NODETYPE_NT_UNSTRUCTURED);
Node newHistoryNode = JcrUtils.getOrAddNode(acHistoryRootNode, name, NODETYPE_NT_UNSTRUCTURED);
String path = newHistoryNode.getPath();
setHistoryNodeProperties(newHistoryNode, installLog, trigger);
saveLogs(newHistoryNode, installLog);
Expand Down Expand Up @@ -177,17 +175,6 @@ private static boolean isInStrackTracke(StackTraceElement[] stackTrace, String c
return false;
}

private static Node safeGetNode(final Node baseNode, final String name,
final String typeToCreate) throws RepositoryException {
if (!baseNode.hasNode(name)) {
LOG.debug("create node: {}", name);
return baseNode.addNode(name, typeToCreate);

} else {
return baseNode.getNode(name);
}
}

public static void setHistoryNodeProperties(final Node historyNode,
PersistableInstallationLogger installLog, String trigger) throws ValueFormatException,
VersionException, LockException, ConstraintViolationException,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
Expand All @@ -34,21 +33,25 @@
import java.util.stream.Collectors;

import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Value;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.felix.webconsole.WebConsoleConstants;
import org.apache.jackrabbit.api.security.user.Group;
import org.apache.jackrabbit.api.JackrabbitSession;
import org.apache.jackrabbit.api.security.user.User;
import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal;
import org.apache.sling.api.SlingHttpServletRequest;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferencePolicyOption;
import org.osgi.service.metatype.annotations.AttributeDefinition;
import org.osgi.service.metatype.annotations.Designate;
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -64,6 +67,7 @@
import biz.netcentric.cq.tools.actool.user.UserProcessor;

@Component(service = { AcToolUiService.class })
@Designate(ocd=biz.netcentric.cq.tools.actool.ui.AcToolUiService.Configuration.class)
public class AcToolUiService {

private static final Logger LOG = LoggerFactory.getLogger(AcToolUiService.class);
Expand All @@ -88,15 +92,27 @@ public class AcToolUiService {
@Reference(policyOption = ReferencePolicyOption.GREEDY)
AcInstallationServiceInternal acInstallationService;

@Reference(policyOption = ReferencePolicyOption.GREEDY)
private WebConsoleConfigTracker webConsoleConfig;

@Reference(policyOption = ReferencePolicyOption.GREEDY)
private AcHistoryService acHistoryService;

@ObjectClassDefinition(name = "AC Tool UI Service",
description="Service that allows to apply AC Tool configuration and gather status of users/groups and permissions from a Web UI (either Touch UI or Web Console Plugin).")
protected static @interface Configuration {

@AttributeDefinition(name="Read access", description="Principal names allowed to export all users/groups and permissions in the system. Only leveraged for Touch UI but not for Web Console Plugin.")
String[] readAccessPrincipalNames() default { "administrators", "admin" };

@AttributeDefinition(name="Write access", description="Principal names allowed to modify users/groups and permissions in the system via ACTool configuration files. Only leveraged for Touch UI but not for Web Console Plugin.")
String[] writeAccessPrincipalNames() default { "administrators", "admin" };
}

private final Map<String, String> countryCodePerName;

public AcToolUiService() {
private final Configuration config;

@Activate
public AcToolUiService(Configuration config) {
this.config = config;
countryCodePerName = new HashMap<>();
for (String iso : Locale.getISOCountries()) {
Locale l = new Locale(Locale.ENGLISH.getLanguage(), iso);
Expand All @@ -108,16 +124,17 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp, String pa
throws ServletException, IOException {

if (req.getRequestURI().endsWith(SUFFIX_DUMP_YAML)) {
callWhenAuthorized(req, resp, this::streamDumpToResponse);
callWhenReadAccessGranted(req, resp, this::streamDumpToResponse);
} else if (req.getRequestURI().endsWith(SUFFIX_USERS_CSV)) {
callWhenAuthorized(req, resp, this::streamUsersCsvToResponse);
callWhenReadAccessGranted(req, resp, this::streamUsersCsvToResponse);
} else {
// everyone is allows to see the UI in general
renderUi(req, resp, path, isTouchUi);
}
}

private void callWhenAuthorized(HttpServletRequest req, HttpServletResponse resp, Consumer<HttpServletResponse> responseConsumer) throws IOException {
if (!hasAccessToFelixWebConsole(req)) {
private void callWhenReadAccessGranted(HttpServletRequest req, HttpServletResponse resp, Consumer<HttpServletResponse> responseConsumer) throws IOException, ServletException {
if (!isOneOfPrincipalNamesBound(req, config.readAccessPrincipalNames())) {
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "You do not have sufficent permissions to export users/groups/permissions");
return;
}
Expand All @@ -127,12 +144,13 @@ private void callWhenAuthorized(HttpServletRequest req, HttpServletResponse resp
throw e.getCause();
}
}

@SuppressWarnings(/* SonarCloud false positive */ {
"javasecurity:S5131" /* response is sent as text/plain, it's not interpreted */,
"javasecurity:S5145" /* logging the path is fine */ })
protected void doPost(final HttpServletRequest req, final HttpServletResponse resp) throws IOException, ServletException {

if (!hasAccessToFelixWebConsole(req)) {
if (!isOneOfPrincipalNamesBound(req, config.writeAccessPrincipalNames())) {
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "You do not have sufficent permissions to apply the configuration");
return;
}
Expand All @@ -157,45 +175,31 @@ protected void doPost(final HttpServletRequest req, final HttpServletResponse re
}

/**
* Replicates the logic of the <a href="https://sling.apache.org/documentation/bundles/web-console-extensions.html#authentication-handling">Sling Web Console Security Provider</a>.
* Similar to the logic of the <a href="https://sling.apache.org/documentation/bundles/web-console-extensions.html#authentication-handling">Sling Web Console Security Provider</a> but acting on principal names
* @param req the request
* @return {@code true} if the user bound to the given request may also access the Felix Web Console or if we are outside of Sling, {@code false} otherwise
* @param principalNames the principal names to check against
* @return {@code true} if the session bound to the given request is bound to any of the given principal names
* @throws ServletException
* @throws RepositoryException
*/
private boolean hasAccessToFelixWebConsole(HttpServletRequest req) {

private boolean isOneOfPrincipalNamesBound(HttpServletRequest req, String[] principalNames) throws ServletException {
if (!(req instanceof SlingHttpServletRequest)) {
// outside Sling this is only called by the Felix Web Console, which has its own security layer
LOG.debug("Outside Sling no additional security checks are performed!");
return true;
}
try {
User requestUser = SlingHttpServletRequest.class.cast(req).getResourceResolver().adaptTo(User.class);
if (requestUser != null) {
if (StringUtils.equals(requestUser.getID(), "admin")) {
LOG.debug("Admin user is allowed to apply AC Tool");
return true;
}

if (ArrayUtils.contains(webConsoleConfig.getAllowedUsers(), requestUser.getID())) {
LOG.debug("User {} is allowed to apply AC Tool (allowed users: {})", requestUser.getID(), ArrayUtils.toString(webConsoleConfig.getAllowedUsers()));
return true;
}

Iterator<Group> memberOfIt = requestUser.memberOf();
Session session = SlingHttpServletRequest.class.cast(req).getResourceResolver().adaptTo(Session.class);
return isOneOfPrincipalNamesBound(JackrabbitSession.class.cast(session), principalNames);
}

while (memberOfIt.hasNext()) {
Group memberOfGroup = memberOfIt.next();
if (ArrayUtils.contains(webConsoleConfig.getAllowedGroups(), memberOfGroup.getID())) {
LOG.debug("Group {} is allowed to apply AC Tool (allowed groups: {})", memberOfGroup.getID(), ArrayUtils.toString(webConsoleConfig.getAllowedGroups()));
return true;
}
}
}
LOG.debug("Could not get associated user for Sling request");
return false;
} catch (Exception e) {
throw new IllegalStateException("Could not check if user may apply AC Tool configuration: " + e, e);
private boolean isOneOfPrincipalNamesBound(JackrabbitSession session, String[] principalNames) throws ServletException {
BoundPrincipals boundPrincipals;
try {
boundPrincipals = new BoundPrincipals(JackrabbitSession.class.cast(session));
} catch (RepositoryException e) {
throw new ServletException("Could not determine bound principals", e);
}
return boundPrincipals.containsOneOf(Arrays.asList(principalNames));
}

public String getWebConsoleRoot(HttpServletRequest req) {
Expand All @@ -210,8 +214,8 @@ private void renderUi(HttpServletRequest req, HttpServletResponse resp, String p

printCss(isTouchUi, writer);
printVersion(writer);
printImportSection(writer, reqParams, path, isTouchUi, getWebConsoleRoot(req));
printExportSection(writer, reqParams, path, isTouchUi, getWebConsoleRoot(req));
printImportSection(writer, reqParams, path, isTouchUi, getWebConsoleRoot(req), isOneOfPrincipalNamesBound(req, config.writeAccessPrincipalNames()));
printExportSection(writer, reqParams, path, isTouchUi, getWebConsoleRoot(req), isOneOfPrincipalNamesBound(req, config.readAccessPrincipalNames()));

try {
printInstallationLogsSection(writer, reqParams, isTouchUi);
Expand Down Expand Up @@ -425,7 +429,7 @@ private String getExecutionStatusHtml(AcToolExecution acToolExecution) {
return acToolExecution.isSuccess() ? "SUCCESS" : "<span style='color:red;font-weight: bold;'>FAILED</span>";
}

private void printImportSection(final HtmlWriter writer, RequestParameters reqParams, String path, boolean isTouchUI, String webConsoleRoot) throws IOException {
private void printImportSection(final HtmlWriter writer, RequestParameters reqParams, String path, boolean isTouchUI, String webConsoleRoot, boolean hasWritePermission) throws IOException {

writer.print("<form id='acForm' action='" + path + "'>");
writer.openTable("acFormTable");
Expand Down Expand Up @@ -473,7 +477,7 @@ private void printImportSection(final HtmlWriter writer, RequestParameters reqPa
writer.openTd();
String onClick = "var as=$('#applySpinner');as.show(); var b=$('#applyButton');b.prop('disabled', true); oldL = b.text();b.text(' Applying AC Tool Configuration... ');var f=$('#acForm');var fd=f.serialize();$.post(f.attr('action'), fd).done(function(text){alert(text)}).fail(function(xhr){alert(xhr.status===403?'Permission Denied':'Config could not be applied - check log for errors')}).always(function(text) { "
+ "as.hide();b.text(oldL);b.prop('disabled', false);location.href='" + PAGE_NAME + "?'+fd; });return false";
writer.println("<button " + getCoralButtonAtts(isTouchUI) + " id='applyButton' onclick=\"" + onClick + "\"> Apply AC Tool Configuration </button>");
writer.println("<button " + getCoralButtonAtts(isTouchUI) + (!hasWritePermission ? " disabled" : "") + " id='applyButton' onclick=\"" + onClick + "\"> Apply AC Tool Configuration </button>");
writer.closeTd();
writer.openTd();
writer.println("<div id='applySpinner' style='display:none' class='spinner'><div></div><div></div><div></div></div>");
Expand All @@ -487,15 +491,15 @@ private void printImportSection(final HtmlWriter writer, RequestParameters reqPa
}


private void printExportSection(final HtmlWriter writer, RequestParameters reqParams, String path, boolean isTouchUI, String webConsoleRoot) throws IOException {
private void printExportSection(final HtmlWriter writer, RequestParameters reqParams, String path, boolean isTouchUI, String webConsoleRoot, boolean hasReadPermission) throws IOException {
writer.openTable("acExportTable");
writer.tableHeader("Export", 2);
writer.tr();
writer.openTd();
writer.print("Export in AC Tool YAML format. This includes groups and permissions (in form of ACEs).");
writer.closeTd();
writer.openTd();
writer.println("<button " + getCoralButtonAtts(isTouchUI) + " id='downloadDumpButton' onclick=\"window.open('" + path + ".html/"
writer.println("<button " + getCoralButtonAtts(isTouchUI) + (!hasReadPermission ? " disabled" : "") + " id='downloadDumpButton' onclick=\"window.open('" + path + ".html/"
+ SUFFIX_DUMP_YAML + "', '_blank');return false;\"> Download YAML </button>");
writer.closeTd();
writer.closeTr();
Expand All @@ -504,7 +508,7 @@ private void printExportSection(final HtmlWriter writer, RequestParameters reqPa
writer.print("Export Users in Admin Console CSV format. This includes non-system users, their profiles and their direct group memberships.");
writer.closeTd();
writer.openTd();
writer.println("<button " + getCoralButtonAtts(isTouchUI) + " id='downloadCsvButton' onclick=\"window.open('" + path + ".html/"
writer.println("<button " + getCoralButtonAtts(isTouchUI) + (!hasReadPermission ? " disabled" : "") + " id='downloadCsvButton' onclick=\"window.open('" + path + ".html/"
+ SUFFIX_USERS_CSV + "', '_blank');return false;\"> Download CSV </button>");
writer.closeTd();
writer.closeTr();
Expand Down
Loading

0 comments on commit 9638820

Please sign in to comment.