diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/helper/UncheckedRepositoryException.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/helper/UncheckedRepositoryException.java
new file mode 100644
index 00000000..b5726e92
--- /dev/null
+++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/helper/UncheckedRepositoryException.java
@@ -0,0 +1,46 @@
+/*
+ * (C) Copyright 2024 Cognizant Netcentric.
+ *
+ * All rights reserved. This program and the accompanying materials
+ * are made available under the terms of the Eclipse Public License v1.0
+ * which accompanies this distribution, and is available at
+ * http://www.eclipse.org/legal/epl-v10.html
+ */
+package biz.netcentric.cq.tools.actool.helper;
+
+import java.util.Objects;
+
+import javax.jcr.RepositoryException;
+
+/**
+ * Wraps a {@link RepositoryException} with an unchecked exception.
+ * This is useful for usage within lambdas.
+ *
+ */
+public class UncheckedRepositoryException extends RuntimeException {
+
+ private static final long serialVersionUID = 2727436608772501551L;
+
+ /**
+ * Constructs an instance of this class.
+ *
+ * @param cause
+ * the {@code RepositoryException}
+ *
+ * @throws NullPointerException
+ * if the cause is {@code null}
+ */
+ public UncheckedRepositoryException(RepositoryException cause) {
+ super(Objects.requireNonNull(cause));
+ }
+
+ /**
+ * Returns the cause of this exception.
+ *
+ * @return the {@code RepositoryException} which is the cause of this exception.
+ */
+ @Override
+ public synchronized RepositoryException getCause() {
+ return (RepositoryException) super.getCause();
+ }
+}
diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ui/AcToolTouchUiServlet.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ui/AcToolTouchUiServlet.java
index f25778a2..ffdf8da0 100644
--- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ui/AcToolTouchUiServlet.java
+++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ui/AcToolTouchUiServlet.java
@@ -37,66 +37,24 @@ public class AcToolTouchUiServlet extends SlingAllMethodsServlet {
private static final Logger LOG = LoggerFactory.getLogger(AcToolTouchUiServlet.class);
- @Reference(policyOption = ReferencePolicyOption.GREEDY)
- private WebConsoleConfigTracker webConsoleConfigTracker;
-
@Reference(policyOption = ReferencePolicyOption.GREEDY)
private AcToolUiService acToolUiService;
@Override
protected void doGet(SlingHttpServletRequest req, SlingHttpServletResponse resp) throws ServletException, IOException {
-
if (StringUtils.isBlank(req.getRequestPathInfo().getSuffix())) {
String targetUrl = req.getResourceResolver().resolve(req.getPathInfo()).getPath() + ".html/" + AcToolUiService.PAGE_NAME;
resp.getWriter().println("");
return;
}
-
acToolUiService.doGet(req, resp, req.getRequestPathInfo().getResourcePath(), true);
-
}
@Override
protected void doPost(SlingHttpServletRequest req, SlingHttpServletResponse resp) throws ServletException, IOException {
-
- if (!mayApplyConfig(req.getResourceResolver().adaptTo(User.class))) {
- resp.sendError(HttpServletResponse.SC_FORBIDDEN, "You do not have sufficent permissions to apply the configuration");
- return;
- }
-
acToolUiService.doPost(req, resp);
LOG.debug("Applied AC tool config via Touch UI by user {}", req.getUserPrincipal());
}
- private boolean mayApplyConfig(User requestUser) {
-
- try {
-
- if (requestUser != null) {
- if (StringUtils.equals(requestUser.getID(), "admin")) {
- LOG.debug("Admin user is allowed to apply AC Tool");
- return true;
- }
-
- if (ArrayUtils.contains(webConsoleConfigTracker.getAllowedUsers(), requestUser.getID())) {
- LOG.debug("User {} is allowed to apply AC Tool (allowed users: {})", requestUser.getID(), ArrayUtils.toString(webConsoleConfigTracker.getAllowedUsers()));
- return true;
- }
-
- Iterator memberOfIt = requestUser.memberOf();
-
- while (memberOfIt.hasNext()) {
- Group memberOfGroup = memberOfIt.next();
- if (ArrayUtils.contains(webConsoleConfigTracker.getAllowedGroups(), memberOfGroup.getID())) {
- LOG.debug("Group {} is allowed to apply AC Tool (allowed groups: {})", memberOfGroup.getID(), ArrayUtils.toString(webConsoleConfigTracker.getAllowedGroups()));
- return true;
- }
- }
- }
- return false;
- } catch (Exception e) {
- throw new IllegalStateException("Could not check if user may apply AC Tool configuration: " + e, e);
- }
- }
}
diff --git a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ui/AcToolUiService.java b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ui/AcToolUiService.java
index 8dc42827..17422d60 100644
--- a/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ui/AcToolUiService.java
+++ b/accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ui/AcToolUiService.java
@@ -4,18 +4,33 @@
import java.io.IOException;
import java.io.PrintWriter;
+import java.io.UncheckedIOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+import javax.jcr.RepositoryException;
+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.security.user.User;
+import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal;
+import org.apache.sling.api.SlingHttpServletRequest;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferencePolicyOption;
@@ -26,10 +41,12 @@
import biz.netcentric.cq.tools.actool.api.InstallationLog;
import biz.netcentric.cq.tools.actool.api.InstallationResult;
import biz.netcentric.cq.tools.actool.dumpservice.ConfigDumpService;
+import biz.netcentric.cq.tools.actool.helper.UncheckedRepositoryException;
import biz.netcentric.cq.tools.actool.history.AcHistoryService;
import biz.netcentric.cq.tools.actool.history.AcToolExecution;
import biz.netcentric.cq.tools.actool.impl.AcInstallationServiceImpl;
import biz.netcentric.cq.tools.actool.impl.AcInstallationServiceInternal;
+import biz.netcentric.cq.tools.actool.user.UserProcessor;
@Component(service = { AcToolUiService.class })
public class AcToolUiService {
@@ -44,11 +61,15 @@ public class AcToolUiService {
public static final String PAGE_NAME = "actool";
- static final String PATH_SEGMENT_DUMP = "dump.yaml";
+ static final String SUFFIX_DUMP_YAML = "dump.yaml";
+ static final String SUFFIX_USERS_CSV = "users.csv";
@Reference(policyOption = ReferencePolicyOption.GREEDY)
private ConfigDumpService dumpService;
+ @Reference(policyOption = ReferencePolicyOption.GREEDY)
+ private UserProcessor userProcessor;
+
@Reference(policyOption = ReferencePolicyOption.GREEDY)
AcInstallationServiceInternal acInstallationService;
@@ -58,21 +79,48 @@ public class AcToolUiService {
@Reference(policyOption = ReferencePolicyOption.GREEDY)
private AcHistoryService acHistoryService;
- protected void doGet(HttpServletRequest req, HttpServletResponse resp, String postPath, boolean isTouchUi)
+ private final Map countryCodePerName;
+
+ public AcToolUiService() {
+ countryCodePerName = new HashMap<>();
+ for (String iso : Locale.getISOCountries()) {
+ Locale l = new Locale(Locale.ENGLISH.getLanguage(), iso);
+ countryCodePerName.put(l.getDisplayCountry(), iso);
+ }
+ }
+
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp, String path, boolean isTouchUi)
throws ServletException, IOException {
- if (req.getRequestURI().endsWith(PATH_SEGMENT_DUMP)) {
- streamDumpToResponse(resp);
+ if (req.getRequestURI().endsWith(SUFFIX_DUMP_YAML)) {
+ callWhenAuthorized(req, resp, this::streamDumpToResponse);
+ } else if (req.getRequestURI().endsWith(SUFFIX_USERS_CSV)) {
+ callWhenAuthorized(req, resp, this::streamUsersCsvToResponse);
} else {
- renderUi(req, resp, postPath, isTouchUi);
+ renderUi(req, resp, path, isTouchUi);
}
}
+ private void callWhenAuthorized(HttpServletRequest req, HttpServletResponse resp, Consumer responseConsumer) throws IOException {
+ if (!hasAccessToFelixWebConsole(req)) {
+ resp.sendError(HttpServletResponse.SC_FORBIDDEN, "You do not have sufficent permissions to export users/groups/permissions");
+ return;
+ }
+ try {
+ responseConsumer.accept(resp);
+ } catch (UncheckedIOException e) {
+ 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)) {
+ resp.sendError(HttpServletResponse.SC_FORBIDDEN, "You do not have sufficent permissions to apply the configuration");
+ return;
+ }
RequestParameters reqParams = RequestParameters.fromRequest(req, acInstallationService);
LOG.info("Received POST request to apply AC Tool config with configurationRootPath={} basePaths={}", reqParams.configurationRootPath, reqParams.basePaths);
@@ -93,18 +141,64 @@ protected void doPost(final HttpServletRequest req, final HttpServletResponse re
}
}
+ /**
+ * Replicates the logic of the https://sling.apache.org/documentation/bundles/web-console-extensions.html#authentication-handling.
+ * @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
+ */
+ private boolean hasAccessToFelixWebConsole(HttpServletRequest req) {
+
+ 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 memberOfIt = requestUser.memberOf();
+
+ 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);
+ }
+ }
+
public String getWebConsoleRoot(HttpServletRequest req) {
return (String) req.getAttribute(WebConsoleConstants.ATTR_APP_ROOT);
}
-
- private void renderUi(HttpServletRequest req, HttpServletResponse resp, String postPath, boolean isTouchUi) throws IOException {
+
+ private void renderUi(HttpServletRequest req, HttpServletResponse resp, String path, boolean isTouchUi) throws IOException {
RequestParameters reqParams = RequestParameters.fromRequest(req, acInstallationService);
final PrintWriter out = resp.getWriter();
+ final HtmlWriter writer = new HtmlWriter(out, isTouchUi);
+
+ printCss(isTouchUi, writer);
+ printVersion(writer);
+ printImportSection(writer, reqParams, path, isTouchUi, getWebConsoleRoot(req));
+ printExportSection(writer, reqParams, path, isTouchUi, getWebConsoleRoot(req));
- printForm(out, reqParams, postPath, isTouchUi, getWebConsoleRoot(req));
-
- printInstallationLogsSection(out, reqParams, isTouchUi);
+ printInstallationLogsSection(writer, reqParams, isTouchUi);
if(!isTouchUi) {
String jmxUrl = getWebConsoleRoot(req) + "/jmx/"
@@ -113,20 +207,112 @@ private void renderUi(HttpServletRequest req, HttpServletResponse resp, String p
}
}
- void streamDumpToResponse(final HttpServletResponse resp) throws IOException {
+ void streamDumpToResponse(final HttpServletResponse resp) {
resp.setContentType("application/x-yaml");
resp.setHeader("Content-Disposition", "inline; filename=\"actool-dump.yaml\"");
String dumpAsString = dumpService.getCompletePrincipalBasedDumpsAsString();
- PrintWriter out = resp.getWriter();
- out.println(dumpAsString);
- out.flush();
+ try {
+ PrintWriter out;
+ out = resp.getWriter();
+ out.println(dumpAsString);
+ out.flush();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
}
- private void printInstallationLogsSection(PrintWriter out, RequestParameters reqParams, boolean isTouchUi) {
+ private void streamUsersCsvToResponse(HttpServletResponse resp) {
+ resp.setContentType("text/csv");
+ resp.setHeader("Content-Disposition", "inline; filename=\"users.csv\"");
+ try {
+ PrintWriter out = resp.getWriter();
+ out.println("Identity Type,Username,Domain,Email,First Name,Last Name,Country Code,ID,Product Configurations,Admin Roles,Product Configurations Administered,User Groups,User Groups Administered,Products Administered,Developer Access");
+ try {
+ userProcessor.forEachNonSystemUser(u -> {
+ try {
+ out.println(String.format(",%s,,%s,%s,%s,%s,,,,,%s", u.getID(),
+ escapeAsCsvValue(getUserPropertyAsString(u, "profile/email")),
+ escapeAsCsvValue(getUserPropertyAsString(u, "profile/givenName")),
+ escapeAsCsvValue(getUserPropertyAsString(u, "profile/familyName")),
+ escapeAsCsvValue(getCountyCodeFromName(getUserPropertyAsString(u, "profile/country"))),
+ escapeAsCsvValue(getDeclaredMemberOfAsStrings(u))));
+ } catch (RepositoryException e) {
+ throw new UncheckedRepositoryException(e);
+ }
+ });
+ } catch (UncheckedRepositoryException|RepositoryException e) {
+ throw new IOException("Could not access users or their properties", e);
+ }
+ out.println();
+ out.flush();
+ } catch (IOException e) {
+ throw new UncheckedIOException(e);
+ }
+ }
+
+ private String getCountyCodeFromName(String countryName) {
+ String countryCode = countryCodePerName.get(countryName);
+ return countryCode != null ? countryCode : "";
+ }
+
+ private static String escapeAsCsvValue(String text) {
+ if (text.contains(",")) {
+ return "\"" + text.replace("\"", "\"\"") + "\"";
+ } else {
+ return text;
+ }
+ }
+
+ private static String getDeclaredMemberOfAsStrings(User user) throws RepositoryException {
+ List groupNames = new LinkedList<>();
+ try {
+ user.declaredMemberOf().forEachRemaining(g -> {
+ try {
+ if (!EveryonePrincipal.NAME.equals(g.getID())) {
+ groupNames.add(g.getID());
+ }
+ } catch (RepositoryException e) {
+ throw new UncheckedRepositoryException(e);
+ }
+ });
+ } catch (UncheckedRepositoryException e) {
+ throw e.getCause();
+ }
+ return String.join(",", groupNames);
+ }
+
+ private static String getUserPropertyAsString(User user, String propertyName) throws RepositoryException {
+ Value[] values = user.getProperty(propertyName);
+ if (values == null) {
+ return "";
+ }
+ try {
+ return Arrays.stream(values).map(t -> {
+ try {
+ return t.getString();
+ } catch (RepositoryException e) {
+ throw new UncheckedRepositoryException(new RepositoryException("Could not convert property \"" + propertyName + "\" of user \"" + user + " to string", e));
+ }
+ }).collect(Collectors.joining(", "));
+ } catch (UncheckedRepositoryException e) {
+ throw e.getCause();
+ }
+ }
+
+ private void printVersion(HtmlWriter writer) {
+ writer.openTable("version");
+ writer.tableHeader("Version", 1);
+ writer.tr();
+ writer.td("v" + acInstallationService.getVersion());
+ writer.closeTd();
+ writer.closeTr();
+ writer.closeTable();
+ }
+
+ private void printInstallationLogsSection(HtmlWriter writer, RequestParameters reqParams, boolean isTouchUi) {
List acToolExecutions = acHistoryService.getAcToolExecutions();
- final HtmlWriter writer = new HtmlWriter(out, isTouchUi);
writer.openTable("previousLogs");
writer.tableHeader("Previous Logs", 5);
@@ -208,14 +394,11 @@ private String getExecutionStatusHtml(AcToolExecution acToolExecution) {
return acToolExecution.isSuccess() ? "SUCCESS" : "FAILED";
}
- private void printForm(final PrintWriter out, RequestParameters reqParams, String postPath, boolean isTouchUI, String webConsoleRoot) throws IOException {
- final HtmlWriter writer = new HtmlWriter(out, isTouchUI);
+ private void printImportSection(final HtmlWriter writer, RequestParameters reqParams, String path, boolean isTouchUI, String webConsoleRoot) throws IOException {
- printCss(isTouchUI, writer);
-
- writer.print("