AEM User Mappings: Enforcing Cybersecurity Through the Principle of Least Privilege
Aug 23, 2023
Maintaining a secure environment and protecting systems from the threat of malicious actors is typically achieved by following certain principles. One such principle is the Principle of Least Privilege.
What is the principle of least privilege?
Here are some definitions:
A security architecture should be designed so that each entity is granted the minimum system resources and authorizations needed to perform its function.
— NIST Glossary
This principle is based on the idea that limiting access to resources, reduces the risk of unauthorized access, use, or disclosure.
— Identity Management Institute
If we think of an enterprise organization, the principle of least privilege is a concept where user access rights are limited to only what is necessary to do their job.
In AEM Services (OSGi components), a common requirement is that access must be granted to the repository to perform data manipulation and also trigger specific Sling events, like replication. To do so, a valid user session is required. In the past, that was accomplished by using an admin session and admin resource resolver through ResourceresolverFactory, but that option has been deprecated since AEM 6.0 per security concerns.
AEM has introduced a new way to grant Back End services access to the repository, based on Service Users being mapped to bundles. This way, specific services can use the user mapping to access the repo.
In this post, we will cover:
-
How to create a SubSystem Identifier (SSI) inside a ServiceUserMapper Configuration Amendment (SUMCA) to assign a Service User to an OSGi bundle.
-
How to use that SUMCA in an OSGi component.
AEM Version: AEM 6.5.4+ and AEM as a Cloud Service
Main concept: ServiceUserMapper Configuration Amendment Factory: Allows the creation of user mapping configurations. Every mapping definition follows the format:
User Mapping
user.mapping=[BUNDLE-ARTIFACT_ID:SUBSYSTEM_IDENTIFIER=[SERVICE_USER_NAME,...]]
This creates a SubSystem Identifier (SSI) that maps a bundle to one or more ServiceUser (SU). The final objective is to map only the user(s) with no more privileges than those required by one or more services (OSGi components) in the mapped OSGi bundle, enforcing the principle of least privilege. This allows services inside the mapped bundle to use the SSI to get a ResourceResolver which can be adapted to a session having only the privileges of the mapped Service User.
Example requirement: Authors want to be able to activate individual AEM pages by adding an "activate" extension right after the page name and before the .html. So if they want to activate a page like [root-site-path]/my-page-name.html
they can hit the same URL just adding the "activate" extension, just like this: [root-site-path]/my-page-name.activate.html
Example solution: We will use a servlet associated with pages created using a specific page component and the "activate" extension to accomplish this. When a page created with the corresponding page component and having the activate extension is requested, the servlet will activate the current page, allowing it to be executed only from the author instance for security reasons.
Steps:
0. Define your Service User
Identify or create a Service User having only the privileges required to perform the required task, in this case, replication. Please check our blog AEM Service Users Creation Using Sling Repo Initializer for more details about how to create this kind of user.
1. Map the Service User to a Bundle using a SubSystem Identifier (SSI).
Configurations are stored under ui.config/src/main/content/jcr_root/apps/YOUR_SITE_PATH/osgiconfig/config/[SPECIFIC_RUNMODE]
. In this case, the configuration wants to be applied to all Run Modes, so we will keep it inside the main /config directory:
Configs Path
ui.config/src/main/content/jcr_root/apps/oshynDemo/osgiconfig/config/
The (SUMCA) factory requires you to name your file starting with the full class name org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl
and a dash-separated meaningful suffix of your choice. In our example we will use:
SUMCA File Name Example
org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended-activation-service.xml
Now, let's use the Service User Mappings format:
Service User Mapping
user.mapping=[BUNDLE-ARTIFACT_ID:SUBSYSTEM_IDENTIFIER=[SERVICE_USER_NAME]]
In this case, the required mapping definition looks like this:
org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended-activation-service.xml
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0" xmlns:jcr="http://www.jcp.org/jcr/1.0" jcr:primaryType="sling:OsgiConfig" user.mapping="[oshynDemo.core:activation_service=[oshynDemo-replication-service-user]]"/>
You can use CRXDELite to create the configuration file(sling:OsgiConfig)
and modify those properties values in an user-friendly interface:
The main idea is to keep those files in the codebase, so they are automatically deployed and applied to every server. As such it’s important to remember to sync back any change you make in AEM directly.
The Service User is already mapped to a Service Identifier that any OSGi component can use.
2. Using the Service User Mapping in an OSGi component.
Now that we have the required configurations, we can use them in our servlet to access the repository. Now we’ll see how to get a valid session from the Service Mapping:
1. You need a ResourceResolverFactory:
ResourceResolverFactory
@Reference private ResourceResolverFactory resourceResolverFactory;
2. You try to get a ResourceResolver
from the ResourceResolverFactory
, passing it as a parameter with the Service Identifier from your SUMCA.
Get ResourceResolver
private static String SERVICE_ID = "activation_service";...
Map<String, Object> paramsMap = new HashMap<>();
paramsMap.put(ResourceResolverFactory.SUBSERVICE, SERVICE_ID);
try (ResourceResolver resourceResolver = resourceResolverFactory.getServiceResourceResolver(paramsMap)) { ...
3. Your ResourceResolver
can be adapted to a Session that has the same privileges defined to your System User that was mapped to the Service Identifier.
Get Session From ResourceResolver
Session session = resourceResolver.adaptTo(Session.class);
After these steps, the acquired session can perform the actions allowed in the repository to the mapped Service User. In this case, replication of the current page.
Activating Page
@Reference
private Replicator replicator;
...
replicator.replicate(session, ReplicationActionType.ACTIVATE, pagePath);
This is the complete code of servlet:
Activation Servlet
//REGULAR IMPORTS GOES HERE...
/**
* Servlet that:
* - is mounted for all resources of a specific Sling resource type (oshynDemo/components/page)
* - and when an extension(activate) is added to the url
* - uses a system user and a user mapping to get access to a ResourceResolver
* - then tries to activate current page and writes a message into the response
* The {@link SlingSafeMethodsServlet} shall be used for HTTP methods that are idempotent.
* For write operations use the {@link SlingAllMethodsServlet}.
*/
@Component(service = { Servlet.class })
@SlingServletResourceTypes(
resourceTypes="oshynDemo/components/page",
methods=HttpConstants.METHOD_GET,
extensions="activate")
@ServiceDescription("Activation Servlet")
public class ActivationServlet extends SlingSafeMethodsServlet {
public static final String AUTHOR = "author";
public static final String ACTIVATION_MESSAGE = "The page has been sent to the replication queue and will be activated/published soon!. Please check your web site in a couple of minutes and remember to delete browser cache!";
public static final String EXCEPTION_MESSAGE = "An unexpected error happened while trying to activate the page! Please check system logs for further details.";
private final Logger logger = LoggerFactory.getLogger(getClass());
private static final long serialVersionUID = 1L;
private static String SERVICE_ID = "activation_service";
@Reference
private ResourceResolverFactory resourceResolverFactory;
@Reference
private Replicator replicator;
@Reference
SlingSettingsService slingSettingsService;
@Override
protected void doGet(final SlingHttpServletRequest req,
final SlingHttpServletResponse resp) throws ServletException, IOException {
final Resource resource = req.getResource();//this resource provides the jcr_root node
String pagePath = Objects.requireNonNull(resource.getParent()).getPath();//we need the cq:page resource
String message;
if(slingSettingsService.getRunModes().contains(AUTHOR)) {
Map<String, Object> paramsMap = new HashMap<>();
paramsMap.put(ResourceResolverFactory.SUBSERVICE, SERVICE_ID);
try (ResourceResolver resourceResolver = resourceResolverFactory.getServiceResourceResolver(paramsMap)) {
Session session = resourceResolver.adaptTo(Session.class);
replicator.replicate(session, ReplicationActionType.ACTIVATE, pagePath);
message = ACTIVATION_MESSAGE;
logger.info("========> [" + pagePath + "] : " + message);
} catch (Exception e) {
message = EXCEPTION_MESSAGE;
logger.error("========> Unable to activate page at " + pagePath + " | Exception: " + e.toString());
}
} else {
message = " Unable to activate page at " + pagePath + " from Publish Environment! ";
logger.error(message);
logger.error("========> ["+pagePath+"] Invalid action invoked from: host:" + req.getRemoteHost() + " | address: " + req.getRemoteAddr());
}
resp.setContentType("text/html");
resp.getWriter().write("<html><body><h1>Trying to activate page at: " + pagePath + "</h1>"
+ "<p>" + message + "</p></body></html>");
}
}
3. Using the new functionality to publish a page.
Wrapping Up
Every company today should follow cybersecurity best practices. AEM OOTB enforces the principle of least privilege through System User Mappings, allowing specific users to be mapped to particular groups of services(bundles). But cybersecurity also depends on how well best practices are followed by companies. In this case, companies need to invest time to set the correct definition of privileges to system users and avoid assigning full access to resources when not required.
Related Insights
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.