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

This closes #781
  • Loading branch information
kwin committed Jan 31, 2025
1 parent 9d1ab3d commit ecea5eb
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 126 deletions.
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,23 @@
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.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.ObjectClassDefinition;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -88,15 +89,26 @@ 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 from the UI and gather some status about the current ACLs of a system")
protected static @interface Configuration {

@AttributeDefinition(name="Allowed to read", description="Principal names allowed to read status about the current acls maintained in the system")
String[] allowReadPrincipalNames() default { "admin" };

@AttributeDefinition(name="Allowed to write", description="Principal names allowed to modify the ACLs in the system via ACTool configuration files")
String[] allowWritePrincipalNames() default { "administrators", "admin" };
}

private final Map<String, String> countryCodePerName;

public AcToolUiService() {
private final Configuration config;

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 +120,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.allowReadPrincipalNames())) {
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "You do not have sufficent permissions to export users/groups/permissions");
return;
}
Expand All @@ -127,12 +140,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.allowWritePrincipalNames())) {
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "You do not have sufficent permissions to apply the configuration");
return;
}
Expand All @@ -157,45 +171,27 @@ 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;
}
Session session = SlingHttpServletRequest.class.cast(req).getResourceResolver().adaptTo(Session.class);
BoundPrincipals boundPrincipals;
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();

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);
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 Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package biz.netcentric.cq.tools.actool.ui;

/*-
* #%L
* Access Control Tool Bundle
* %%
* Copyright (C) 2015 - 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
* #L%
*/

import java.security.Principal;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import javax.jcr.RepositoryException;

import org.apache.jackrabbit.api.JackrabbitSession;
import org.apache.jackrabbit.api.security.user.Authorizable;
import org.apache.jackrabbit.api.security.user.AuthorizableTypeException;
import org.apache.jackrabbit.api.security.user.Group;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
* Encapsulates all principals bound to a given session.
* Natively exposed from Oak 1.40 onwards (see OAK-8611).
*/
class BoundPrincipals {

private static final Logger log = LoggerFactory.getLogger(BoundPrincipals.class);

private Set<Principal> boundPrincipals;

BoundPrincipals(@NotNull JackrabbitSession session) throws RepositoryException {
final String userId = session.getUserID();
// newer Oak versions expose bound principals via session attribute (https://issues.apache.org/jira/browse/OAK-9415)
boundPrincipals = (Set<Principal>)session.getAttribute("oak.bound-principals");
if (boundPrincipals == null) {
boundPrincipals = new HashSet<>();
Authorizable authorizable = session.getUserManager().getAuthorizable(userId);
if (authorizable == null) {
throw new AuthorizableTypeException("Could not find authorizable for session's user ID " + userId);
}
boundPrincipals.add(authorizable.getPrincipal());

Iterator<Group> groupIterator = authorizable.memberOf();
while (groupIterator.hasNext()) {
boundPrincipals.add(groupIterator.next().getPrincipal());
}
}
}

public boolean containsOneOf(@NotNull Collection<String> principalNames) {
for (Principal principal : boundPrincipals) {
if (principalNames.contains(principal.getName())) {
return true;
}
}
return false;
}

}

This file was deleted.

0 comments on commit ecea5eb

Please sign in to comment.