Skip to content

Commit d7e74c3

Browse files
committed
Dedicated OSGi configuration for granting access to UI
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
1 parent 9d1ab3d commit d7e74c3

File tree

9 files changed

+160
-160
lines changed

9 files changed

+160
-160
lines changed

README.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,6 @@ You need to setup [Yaml configuration files](docs/Configuration.md) to specify y
4848

4949
There are also some [advanced configuration options](docs/AdvancedFeatures.md) supported such as loops, conditional statements and permissions for anonymous.
5050

51-
# User Interface
52-
53-
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.
54-
5551
# Applying AC Tool Configurations
5652

5753
Best practice is to apply AC Tool Configurations using the install hook (or startup hook for Cloud Service) during your project's software package installation. See [applying the ACL entries](docs/ApplyConfig.md) for a full list of options.

accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/history/impl/HistoryUtils.java

Lines changed: 7 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,12 @@ public class HistoryUtils {
5858

5959
public static final String HISTORY_NODE_NAME_PREFIX = "history_";
6060
public static final String NODETYPE_NT_UNSTRUCTURED = "nt:unstructured";
61-
public static final String ACHISTORY_ROOT_NODE = "achistory";
62-
public static final String STATISTICS_ROOT_NODE = "var/statistics";
63-
public static final String ACHISTORY_PATH = "/"+ HistoryUtils.STATISTICS_ROOT_NODE + "/" + HistoryUtils.ACHISTORY_ROOT_NODE;
61+
public static final String ACHISTORY_ROOT_NODE_NAME = "achistory";
62+
public static final String STATISTICS_ROOT_NODE_PATH = "/var/statistics";
63+
public static final String ACHISTORY_PATH = HistoryUtils.STATISTICS_ROOT_NODE_PATH + "/" + HistoryUtils.ACHISTORY_ROOT_NODE_NAME;
6464

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

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

8686
public static Node getAcHistoryRootNode(final Session session)
8787
throws RepositoryException {
88-
final Node rootNode = session.getRootNode();
89-
Node statisticsRootNode = safeGetNode(rootNode, STATISTICS_ROOT_NODE, NODETYPE_NT_UNSTRUCTURED);
90-
Node acHistoryRootNode = safeGetNode(statisticsRootNode, ACHISTORY_ROOT_NODE, "sling:OrderedFolder");
91-
return acHistoryRootNode;
88+
Node statisticsRootNode = JcrUtils.getOrCreateByPath(STATISTICS_ROOT_NODE_PATH, NODETYPE_NT_UNSTRUCTURED, session);
89+
return JcrUtils.getOrAddNode(statisticsRootNode, ACHISTORY_ROOT_NODE_NAME, "sling:OrderedFolder");
9290
}
9391

9492
/**
@@ -139,7 +137,7 @@ public static Node persistHistory(final Session session,
139137
}
140138
name += AcToolExecutionImpl.TRIGGER_SEPARATOR_IN_NODE_NAME + trigger;
141139

142-
Node newHistoryNode = safeGetNode(acHistoryRootNode, name, NODETYPE_NT_UNSTRUCTURED);
140+
Node newHistoryNode = JcrUtils.getOrAddNode(acHistoryRootNode, name, NODETYPE_NT_UNSTRUCTURED);
143141
String path = newHistoryNode.getPath();
144142
setHistoryNodeProperties(newHistoryNode, installLog, trigger);
145143
saveLogs(newHistoryNode, installLog);
@@ -177,17 +175,6 @@ private static boolean isInStrackTracke(StackTraceElement[] stackTrace, String c
177175
return false;
178176
}
179177

180-
private static Node safeGetNode(final Node baseNode, final String name,
181-
final String typeToCreate) throws RepositoryException {
182-
if (!baseNode.hasNode(name)) {
183-
LOG.debug("create node: {}", name);
184-
return baseNode.addNode(name, typeToCreate);
185-
186-
} else {
187-
return baseNode.getNode(name);
188-
}
189-
}
190-
191178
public static void setHistoryNodeProperties(final Node historyNode,
192179
PersistableInstallationLogger installLog, String trigger) throws ValueFormatException,
193180
VersionException, LockException, ConstraintViolationException,

accesscontroltool-bundle/src/main/java/biz/netcentric/cq/tools/actool/ui/AcToolUiService.java

Lines changed: 53 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import java.text.SimpleDateFormat;
2424
import java.util.Arrays;
2525
import java.util.HashMap;
26-
import java.util.Iterator;
2726
import java.util.LinkedHashMap;
2827
import java.util.LinkedList;
2928
import java.util.List;
@@ -34,21 +33,25 @@
3433
import java.util.stream.Collectors;
3534

3635
import javax.jcr.RepositoryException;
36+
import javax.jcr.Session;
3737
import javax.jcr.Value;
3838
import javax.servlet.ServletException;
3939
import javax.servlet.http.HttpServletRequest;
4040
import javax.servlet.http.HttpServletResponse;
4141

42-
import org.apache.commons.lang3.ArrayUtils;
4342
import org.apache.commons.lang3.StringUtils;
4443
import org.apache.felix.webconsole.WebConsoleConstants;
45-
import org.apache.jackrabbit.api.security.user.Group;
44+
import org.apache.jackrabbit.api.JackrabbitSession;
4645
import org.apache.jackrabbit.api.security.user.User;
4746
import org.apache.jackrabbit.oak.spi.security.principal.EveryonePrincipal;
4847
import org.apache.sling.api.SlingHttpServletRequest;
48+
import org.osgi.service.component.annotations.Activate;
4949
import org.osgi.service.component.annotations.Component;
5050
import org.osgi.service.component.annotations.Reference;
5151
import org.osgi.service.component.annotations.ReferencePolicyOption;
52+
import org.osgi.service.metatype.annotations.AttributeDefinition;
53+
import org.osgi.service.metatype.annotations.Designate;
54+
import org.osgi.service.metatype.annotations.ObjectClassDefinition;
5255
import org.slf4j.Logger;
5356
import org.slf4j.LoggerFactory;
5457

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

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

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

91-
@Reference(policyOption = ReferencePolicyOption.GREEDY)
92-
private WebConsoleConfigTracker webConsoleConfig;
93-
9495
@Reference(policyOption = ReferencePolicyOption.GREEDY)
9596
private AcHistoryService acHistoryService;
9697

98+
@ObjectClassDefinition(name = "AC Tool UI Service",
99+
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).")
100+
protected static @interface Configuration {
101+
102+
@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.")
103+
String[] readAccessPrincipalNames() default { "administrators", "admin" };
104+
105+
@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.")
106+
String[] writeAccessPrincipalNames() default { "administrators", "admin" };
107+
}
108+
97109
private final Map<String, String> countryCodePerName;
98110

99-
public AcToolUiService() {
111+
private final Configuration config;
112+
113+
@Activate
114+
public AcToolUiService(Configuration config) {
115+
this.config = config;
100116
countryCodePerName = new HashMap<>();
101117
for (String iso : Locale.getISOCountries()) {
102118
Locale l = new Locale(Locale.ENGLISH.getLanguage(), iso);
@@ -108,16 +124,17 @@ protected void doGet(HttpServletRequest req, HttpServletResponse resp, String pa
108124
throws ServletException, IOException {
109125

110126
if (req.getRequestURI().endsWith(SUFFIX_DUMP_YAML)) {
111-
callWhenAuthorized(req, resp, this::streamDumpToResponse);
127+
callWhenReadAccessGranted(req, resp, this::streamDumpToResponse);
112128
} else if (req.getRequestURI().endsWith(SUFFIX_USERS_CSV)) {
113-
callWhenAuthorized(req, resp, this::streamUsersCsvToResponse);
129+
callWhenReadAccessGranted(req, resp, this::streamUsersCsvToResponse);
114130
} else {
131+
// everyone is allows to see the UI in general
115132
renderUi(req, resp, path, isTouchUi);
116133
}
117134
}
118135

119-
private void callWhenAuthorized(HttpServletRequest req, HttpServletResponse resp, Consumer<HttpServletResponse> responseConsumer) throws IOException {
120-
if (!hasAccessToFelixWebConsole(req)) {
136+
private void callWhenReadAccessGranted(HttpServletRequest req, HttpServletResponse resp, Consumer<HttpServletResponse> responseConsumer) throws IOException, ServletException {
137+
if (!isOneOfPrincipalNamesBound(req, config.readAccessPrincipalNames())) {
121138
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "You do not have sufficent permissions to export users/groups/permissions");
122139
return;
123140
}
@@ -127,12 +144,13 @@ private void callWhenAuthorized(HttpServletRequest req, HttpServletResponse resp
127144
throw e.getCause();
128145
}
129146
}
147+
130148
@SuppressWarnings(/* SonarCloud false positive */ {
131149
"javasecurity:S5131" /* response is sent as text/plain, it's not interpreted */,
132150
"javasecurity:S5145" /* logging the path is fine */ })
133151
protected void doPost(final HttpServletRequest req, final HttpServletResponse resp) throws IOException, ServletException {
134152

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

159177
/**
160-
* 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>.
178+
* 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
161179
* @param req the request
162-
* @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
180+
* @param principalNames the principal names to check against
181+
* @return {@code true} if the session bound to the given request is bound to any of the given principal names
182+
* @throws ServletException
183+
* @throws RepositoryException
163184
*/
164-
private boolean hasAccessToFelixWebConsole(HttpServletRequest req) {
165-
185+
private boolean isOneOfPrincipalNamesBound(HttpServletRequest req, String[] principalNames) throws ServletException {
166186
if (!(req instanceof SlingHttpServletRequest)) {
167187
// outside Sling this is only called by the Felix Web Console, which has its own security layer
168188
LOG.debug("Outside Sling no additional security checks are performed!");
169189
return true;
170190
}
171-
try {
172-
User requestUser = SlingHttpServletRequest.class.cast(req).getResourceResolver().adaptTo(User.class);
173-
if (requestUser != null) {
174-
if (StringUtils.equals(requestUser.getID(), "admin")) {
175-
LOG.debug("Admin user is allowed to apply AC Tool");
176-
return true;
177-
}
178-
179-
if (ArrayUtils.contains(webConsoleConfig.getAllowedUsers(), requestUser.getID())) {
180-
LOG.debug("User {} is allowed to apply AC Tool (allowed users: {})", requestUser.getID(), ArrayUtils.toString(webConsoleConfig.getAllowedUsers()));
181-
return true;
182-
}
183-
184-
Iterator<Group> memberOfIt = requestUser.memberOf();
191+
Session session = SlingHttpServletRequest.class.cast(req).getResourceResolver().adaptTo(Session.class);
192+
return isOneOfPrincipalNamesBound(JackrabbitSession.class.cast(session), principalNames);
193+
}
185194

186-
while (memberOfIt.hasNext()) {
187-
Group memberOfGroup = memberOfIt.next();
188-
if (ArrayUtils.contains(webConsoleConfig.getAllowedGroups(), memberOfGroup.getID())) {
189-
LOG.debug("Group {} is allowed to apply AC Tool (allowed groups: {})", memberOfGroup.getID(), ArrayUtils.toString(webConsoleConfig.getAllowedGroups()));
190-
return true;
191-
}
192-
}
193-
}
194-
LOG.debug("Could not get associated user for Sling request");
195-
return false;
196-
} catch (Exception e) {
197-
throw new IllegalStateException("Could not check if user may apply AC Tool configuration: " + e, e);
195+
private boolean isOneOfPrincipalNamesBound(JackrabbitSession session, String[] principalNames) throws ServletException {
196+
BoundPrincipals boundPrincipals;
197+
try {
198+
boundPrincipals = new BoundPrincipals(JackrabbitSession.class.cast(session));
199+
} catch (RepositoryException e) {
200+
throw new ServletException("Could not determine bound principals", e);
198201
}
202+
return boundPrincipals.containsOneOf(Arrays.asList(principalNames));
199203
}
200204

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

211215
printCss(isTouchUi, writer);
212216
printVersion(writer);
213-
printImportSection(writer, reqParams, path, isTouchUi, getWebConsoleRoot(req));
214-
printExportSection(writer, reqParams, path, isTouchUi, getWebConsoleRoot(req));
217+
printImportSection(writer, reqParams, path, isTouchUi, getWebConsoleRoot(req), isOneOfPrincipalNamesBound(req, config.writeAccessPrincipalNames()));
218+
printExportSection(writer, reqParams, path, isTouchUi, getWebConsoleRoot(req), isOneOfPrincipalNamesBound(req, config.readAccessPrincipalNames()));
215219

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

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

430434
writer.print("<form id='acForm' action='" + path + "'>");
431435
writer.openTable("acFormTable");
@@ -473,7 +477,7 @@ private void printImportSection(final HtmlWriter writer, RequestParameters reqPa
473477
writer.openTd();
474478
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) { "
475479
+ "as.hide();b.text(oldL);b.prop('disabled', false);location.href='" + PAGE_NAME + "?'+fd; });return false";
476-
writer.println("<button " + getCoralButtonAtts(isTouchUI) + " id='applyButton' onclick=\"" + onClick + "\"> Apply AC Tool Configuration </button>");
480+
writer.println("<button " + getCoralButtonAtts(isTouchUI) + (!hasWritePermission ? " disabled" : "") + " id='applyButton' onclick=\"" + onClick + "\"> Apply AC Tool Configuration </button>");
477481
writer.closeTd();
478482
writer.openTd();
479483
writer.println("<div id='applySpinner' style='display:none' class='spinner'><div></div><div></div><div></div></div>");
@@ -487,15 +491,15 @@ private void printImportSection(final HtmlWriter writer, RequestParameters reqPa
487491
}
488492

489493

490-
private void printExportSection(final HtmlWriter writer, RequestParameters reqParams, String path, boolean isTouchUI, String webConsoleRoot) throws IOException {
494+
private void printExportSection(final HtmlWriter writer, RequestParameters reqParams, String path, boolean isTouchUI, String webConsoleRoot, boolean hasReadPermission) throws IOException {
491495
writer.openTable("acExportTable");
492496
writer.tableHeader("Export", 2);
493497
writer.tr();
494498
writer.openTd();
495499
writer.print("Export in AC Tool YAML format. This includes groups and permissions (in form of ACEs).");
496500
writer.closeTd();
497501
writer.openTd();
498-
writer.println("<button " + getCoralButtonAtts(isTouchUI) + " id='downloadDumpButton' onclick=\"window.open('" + path + ".html/"
502+
writer.println("<button " + getCoralButtonAtts(isTouchUI) + (!hasReadPermission ? " disabled" : "") + " id='downloadDumpButton' onclick=\"window.open('" + path + ".html/"
499503
+ SUFFIX_DUMP_YAML + "', '_blank');return false;\"> Download YAML </button>");
500504
writer.closeTd();
501505
writer.closeTr();
@@ -504,7 +508,7 @@ private void printExportSection(final HtmlWriter writer, RequestParameters reqPa
504508
writer.print("Export Users in Admin Console CSV format. This includes non-system users, their profiles and their direct group memberships.");
505509
writer.closeTd();
506510
writer.openTd();
507-
writer.println("<button " + getCoralButtonAtts(isTouchUI) + " id='downloadCsvButton' onclick=\"window.open('" + path + ".html/"
511+
writer.println("<button " + getCoralButtonAtts(isTouchUI) + (!hasReadPermission ? " disabled" : "") + " id='downloadCsvButton' onclick=\"window.open('" + path + ".html/"
508512
+ SUFFIX_USERS_CSV + "', '_blank');return false;\"> Download CSV </button>");
509513
writer.closeTd();
510514
writer.closeTr();

0 commit comments

Comments
 (0)