Skip to content

Commit

Permalink
Add functionality to export users in CSV format
Browse files Browse the repository at this point in the history
This format is compatible with
https://helpx.adobe.com/enterprise/using/bulk-upload-users.html#csv-format

Fix Web Console deployment with sling-m-p

This closes #703
This closes #443
  • Loading branch information
kwin committed May 3, 2024
1 parent ba3c803 commit a20c4c5
Show file tree
Hide file tree
Showing 5 changed files with 276 additions and 30 deletions.
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,28 @@

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.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
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.StringUtils;
import org.apache.felix.webconsole.WebConsoleConstants;
import org.apache.jackrabbit.api.security.user.User;
import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.osgi.service.component.annotations.ReferencePolicyOption;
Expand All @@ -26,10 +36,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 {
Expand All @@ -44,11 +56,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 userExporter;

@Reference(policyOption = ReferencePolicyOption.GREEDY)
AcInstallationServiceInternal acInstallationService;

Expand All @@ -58,13 +74,25 @@ public class AcToolUiService {
@Reference(policyOption = ReferencePolicyOption.GREEDY)
private AcHistoryService acHistoryService;

protected void doGet(HttpServletRequest req, HttpServletResponse resp, String postPath, boolean isTouchUi)
private final Map<String, String> 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)) {
if (req.getRequestURI().endsWith(SUFFIX_DUMP_YAML)) {
streamDumpToResponse(resp);
} else if (req.getRequestURI().endsWith(SUFFIX_USERS_CSV)) {
streamUsersCsvToResponse(resp);
} else {
renderUi(req, resp, postPath, isTouchUi);
renderUi(req, resp, path, isTouchUi);
}
}

Expand Down Expand Up @@ -97,14 +125,18 @@ 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/"
Expand All @@ -122,11 +154,94 @@ void streamDumpToResponse(final HttpServletResponse resp) throws IOException {
out.flush();
}

private void printInstallationLogsSection(PrintWriter out, RequestParameters reqParams, boolean isTouchUi) {
private void streamUsersCsvToResponse(HttpServletResponse resp) throws IOException {
resp.setContentType("text/csv");
resp.setHeader("Content-Disposition", "inline; filename=\"users.csv\"");
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 {
userExporter.forEachNonSystemUsers(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();
}

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<String> 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<AcToolExecution> acToolExecutions = acHistoryService.getAcToolExecutions();

final HtmlWriter writer = new HtmlWriter(out, isTouchUi);
writer.openTable("previousLogs");
writer.tableHeader("Previous Logs", 5);

Expand Down Expand Up @@ -208,14 +323,11 @@ private String getExecutionStatusHtml(AcToolExecution acToolExecution) {
return acToolExecution.isSuccess() ? "SUCCESS" : "<span style='color:red;font-weight: bold;'>FAILED</span>";
}

private void printForm(final PrintWriter out, RequestParameters reqParams, String postPath, boolean isTouchUI, String webConsoleRoot) throws IOException {
final HtmlWriter writer = new HtmlWriter(out, isTouchUI);

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

writer.print("<form id='acForm' action='" + postPath + "'>");
writer.print("<form id='acForm' action='" + path + "'>");
writer.openTable("acFormTable");
writer.tableHeader("AC Tool v" + acInstallationService.getVersion(), 3);
writer.tableHeader("Import", 2);

writer.tr();
writer.openTd();
Expand All @@ -237,10 +349,6 @@ private void printForm(final PrintWriter out, RequestParameters reqParams, Strin
+ (reqParams.applyOnlyIfChanged ? " checked='checked'" : "") + " /> apply only if config changed");
writer.closeTd();

writer.openTd();
writer.println("<button " + getCoralButtonAtts(isTouchUI) + " id='downloadDumpButton' onclick=\"window.open('" + postPath + ".html/"
+ PATH_SEGMENT_DUMP + "', '_blank');return false;\"> Download Dump </button>");
writer.closeTd();

writer.closeTr();

Expand All @@ -257,10 +365,6 @@ private void printForm(final PrintWriter out, RequestParameters reqParams, Strin
writer.println("' class='input' size='70'>");
writer.closeTd();

writer.openTd();
writer.println("");
writer.closeTd();

writer.closeTr();

writer.tr();
Expand All @@ -272,8 +376,6 @@ private void printForm(final PrintWriter out, RequestParameters reqParams, Strin
writer.openTd();
writer.println("<div id='applySpinner' style='display:none' class='spinner'><div></div><div></div><div></div></div>");

writer.openTd();
writer.println("");
writer.closeTd();

writer.closeTr();
Expand All @@ -283,6 +385,31 @@ private void printForm(final PrintWriter out, RequestParameters reqParams, Strin
}


private void printExportSection(final HtmlWriter writer, RequestParameters reqParams, String path, boolean isTouchUI, String webConsoleRoot) 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/"
+ SUFFIX_DUMP_YAML + "', '_blank');return false;\"> Download YAML </button>");
writer.closeTd();
writer.closeTr();
writer.tr();
writer.openTd();
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/"
+ SUFFIX_USERS_CSV + "', '_blank');return false;\"> Download CSV </button>");
writer.closeTd();
writer.closeTr();

writer.closeTable();
}

private void printCss(boolean isTouchUI, final HtmlWriter writer) {
StringBuilder css = new StringBuilder();
// spinner css
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package biz.netcentric.cq.tools.actool.user;

import java.util.function.Consumer;

import javax.jcr.RepositoryException;

import org.apache.jackrabbit.api.security.user.User;

public interface UserProcessor {

void forEachNonSystemUsers(Consumer<User> userConsumer) throws RepositoryException;

}
Loading

0 comments on commit a20c4c5

Please sign in to comment.