Skip to content

Commit

Permalink
Merge branch 'develop' of git@github.com:Netcentric/accesscontroltool…
Browse files Browse the repository at this point in the history
….git into develop
  • Loading branch information
kwin committed Feb 4, 2025
2 parents 192bd1b + 892a0c3 commit f28db29
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.

# 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 f28db29

Please sign in to comment.