Service Users
Learn how to create and use Service Users in your AEM code to provide controlled, programmatic access to the AEM repository.
Transcript
Let’s take a look at how we can create and use service-users in AEM to access the repository with specific, well-defined set of permissions. To help illustrate this, we’ll use an example content statistics OSGI service, whose job is to count the number of dam assets in the AEM repository and provide us a count. So I have my AEM Maven project open, and I’ve already stubbed out the content statistics OSGI service implementation in the core bundle here. I’ve already implemented a private method, query and count assets. And this just uses the past and resourceResolver to query/content/dam, for dam:asset nodes and then just counts up the number of results.
I’ve also stubbed out two methods that we’ll implement in this video. The first getAssetsCount, takes a resourceResolver and uses that resourceResolver to query AEM for assets. And the second, is getAllAssetsCount, which will use a specifically permissioned service-users resourceResolver to query for AEM Assets. We’ll invoke the first method, getAssetsCounts, using our logged in AEM users resourceResolver, and then we’ll juxtapose those results against the second method, which will use this service resolver’s resourceResolver and its permissions. So let’s quickly implement the first method as this one’s quite simple. We’ll just call it our private method and pass through the provided resourceResolver. Now, when we invoke getAssetsCounts with our logged in AEM users resourceResolver that will pass in the security context scopes, what the dam assets query can read and thus the asset count it will return.
Okay, now for the fun stuff. Let’s implement the getAllAssetsCount method, which will query and count all assets, no matter by whom or how it’s invoked. Before we write any Java code, we need to make sure that we have a service-user. So let’s define one. And for this we’ll use sling repository initialization, or repoinit. We’ll use a repoinit script defined in an OSGI configuration that declares our service-user and its permissions. And since all OSGI configuration should be defined in the project’s ui.config Maven subproject, let’s head over there. So we need to select the right config run mode folder to place this in. You’ll want to think about when you’ll need the service-user based on your specific use case. But for our use case, let’s say that we only need the content statistics functionality, to run on AEM author. So we’ll go ahead and make our OSGI configuration under config.author. We’ll create a new OSGI configuration file for the sling repository initializer and this follows the usual conventions. So the file name will start off with org.Apache.sling.JCR.repoinit.repositoryInitializer, and since this is a factory configuration, we’ll follow that up with a tilde, and then postfix this configuration with a unique name. Let’s say wknd-examples-statistics. And finally we’ll add the .config extension. So, a few things to know here. While you can include all your repoinit scripts in a single repoinit OSGI config file, like the one here that’s been auto-generated using the AEM Maven archetype. I like to maintain discreet repoinits for each logical piece of functionality, as this makes it easy to understand what the repoinit instruction dependencies for the specific functionality are. And this is why we’re creating a new OSGI configuration file, rather than lumping it in with a repoinit script that the Maven archetype generated for us. But you could certainly add it there as well. Next, I want to call out that we use the tilde as the separator in the file name, and just be aware, that earlier versions of AEM 6, don’t support this character. So if you’re on an earlier version of AEM 6, you should of course update to the latest version of AEM. But, if you can’t, use a dash or hyphen instead of the tilde. And lastly, you might’ve noticed that we’re using the .config extension rather than the recommended .cfg, .json extension. The reason for this is that repoinit scripts are almost always multi-lined and json does not have a good multi-line support, whereas .config format does. So, it’s more comprehensible to write and maintain your repoinit scripts, especially as they grow large, in .config files, rather than trying to maintain them on a single line in the json. You can consider this the exception to the rule of using .cfg json format OSGI configuration files. Repoinit scripts are defined in these scripts property. So, this is a multi value string property, and each string in the multi value can contain one or more repoinit instructions. And typically, the scripts array has a single string value, that is the entire repoinit script. So, let’s go ahead and define this. First, we’ll create the service-user using the create service-user instruction. Next, we’ll provide our service-user name. You can name these anything you want. I like to prefix it with the app name. So it’s very clear where the user is coming from and it’s been defined, and then post fix it with -service, indicating that it’s a service-user, rather than a regular AEM user. The name between this prefix and suffix should be semantic, so for us, statistics is a good choice. After the service name, we use the with forced path directive and provide a path somewhere under system/cq:services, and we’ll use systems/cqservices/wknd-examples. I like to use the app name as the final path folder or segment here. So all the apps service-users are nicely grouped and it makes them easy to check on in the AEM repository. It’s worth noting that AEM puts its service-users in system/cqservices/internal. So, never put yours in there, since as the name implies, it’s considered an internal folder that only AEM should write to. Next, let’s grant the service-user some permissions. Keep in mind, you always want to grant your service-user the least amount of privileges it requires to do its job. And you can always update your repoinit scripts later if the scope of its work evolves. So, since our service-user needs to be able to read all dam asset nodes under /contentdam in order to count them, let’s give them read permissions there. So we use set principal ACL instruction, followed by the service-user’s name. And set principle ACL is a block, which allows us to put some sub instructions. In here, we can specify all the permissions the service-user should have. So, let’s put allow jcr read on /content/dam, and then we can end this block. If the service-user needed other permissions, we could add those instructions on new lines within this block.
Do note that we are setting principal ACL’s, instead of regular repository ACL’s. And what this does, is it sets the ACL’s for /content/dam under the service-user’s node, rather than polluting the AEM repository’s /content dam rep policy node with more allow and deny roles. Also, in order to use the set principal ACL, the user must be created under system/cq:services. If it’s not under system/cq:services, the principal ACL’s will not be set. And after watching this video, check out the repoinit docs if you need more complex permissioning, as it has quite extensive support. Okay, so we’re done with defining our service-users as well as its permissions. Next, let’s map the service-user with sling, so it can be referenceable under Java code. For this, we need another OSGI config, and we’ll place this config.author as well, since that’s where the service-user’s defined. This’ll be an OSGI config for sling’s service-user mapper. So, let’s go ahead and name the file org.Apache.sling.ServiceUserMapping.impl.
ServiceUserMapperImpl.amended, and we’ll follow that by the tilde again and a descriptive name. And the descriptive name is usually just the application name, and we’ll finish it off with the recommended .cfg.json format. Again, you may need to use the dash instead of the tilde, for older versions of AEM 6. Since this OSGI configuration files in the cfg.json format, we’ll create a json object with the key user.mapping, whose value is a string array. In each element in the array, we’ll map a service and sub-service ID to a set of service-users. The format of each mapping is the service name, which is the OSGI bundle symbolic name that will use this service-user, and is usually the core project’s Maven artifact ID. So for us, wknd-examples.core and this scopes this mapping so it will only work for this bundle. So for example, if another bundle is deployed to AEM, it wouldn’t be allowed to use this service-user mapping to gain repository access using our service-user. We’ll follow the service name with a colon, and then provide a unique sub-service ID of our choosing. So, like the service-user itself, wknd-examples-statistics, is a good name for us here. Now, we’ll map the service and sub-service ID combo to actual AEM service-users that provide the permissions to AEM’s repository. So, we’ll put an equal sign and then we’ll list out the service-users that provide the permissions. And in our case, wknd-examples-statistics-service. And since we’re using principal ACL’s for our service-user, we must wrap the service-user in square brackets. A few quick things I want to mention here. One, you can set multiple AEM users for the sub-service ID mapping. However, this requires a high level of diligence to maintain and ensure that the changes to service-user permissions don’t over time adversely impact the map sub-services. For this reason, I tend to keep a one-to-one relationship between the sub-service and the service-user. However, if you do find yourself with a lot of overlap between your service-user permissions, you can look into leveraging this approach. It’s just that it might be a little bit more difficult to maintain. The other thing to note, is you can specify a mapping that provides a default service-user for the service or OSGI bundle, and leave off the sub-service ID entirely. You rarely want to do this though, as it usually results in a single overpowered service-user, that has the aggregate permissions needed for everything in the bundle. And it is much better to map sub-services to the service-users required for each logical set of functionality and giving each sub-service the least amount of privileges it needs to do its job. Okay, so our service-user, wknd-examples-statistics-service, is mapped to a referenceable sub-service ID, or, wknd-examples-statistics, that we’ll be able to reference and use in our OSGI service. So, let’s go ahead and do that. We’ll jump back over to ContentStatisticsImpl in our core project. And the first thing that we need to do is make this an OSGI component, so it has access to other OSGI services, which we’ll need to gain that reference to the service-user. So let’s annotate this class with component, and since it already implements an interface, it will automatically register against that interface as an OSGI Service. So we can call it from scripts or other OSGI services. Next, let’s use the OSGI reference annotation to inject the resourceResolverFactory OSGI service, provided by sling.
Okay, now we’re ready to write the code to get the resourceResolver associated with our service-user, which will in turn, let us access the AEM repository as that service-user.
First, we’ll create a map used to identify the sub-service we want to retrieve.
And the value for the sub-service key is the subsurface ID we defined on the left side of the mapping in the sling service-user mapping OSGI config And then we’ll pass this map to the resourceResolver factory to get the matching resourceResolver. And since resourceResolvers are autoposable, we’ll use the try with resources syntax here. If you use the traditional try catch finally syntax, make sure to null check and close the service resourceResolver in the finally block since the resourceResolver rule of, if you create it, you must close it, applies to service resourceResolver as well. So the call to get service resourceResolver with our authInfo map that specifies the service ID, will return the service-user’s resourceResolver object that is mapped for that sub-service ID. Since this can throw a log-in exception, let’s go ahead and catch that.
And in the try block, we have access to the service-user’s resourceResolver, which acts as a security context into AEM repository, so we can simply pass that to our private method that performs a query and counts the results. Remember to perform all work that requires access to the AEM repository in this try block, as the service-user resolver will auto close when it’s passed. So for example, if you were to retrieve a list of resource objects, with the service resourceResolver, and then try to use those outside of the try block, their underlying access to AEM repository would have been closed, and those resource objects would no longer work. All right, the last thing we want to do, and this is optional, however, I do recommend it, especially if the service-user plays an integral part in the OSGI component’s execution, is to make the OSGI component dependent on the existence of the service-user. This prevents this OSGI component from activating until the service-user is available to the system. This is done via reference to the sling service-user mapped OSGI configuration service, and providing a target filter, specifying the sub-service ID we want to wait for.
And since references require a name, let’s provide one. This can be anything, but I recommend just making it the subsurface ID for clarity. This also has the added benefit of controlling when your OSGI component can become active. If you remember, we created the repoinit and sling user mapping OSGI configurations in the config.author run mode folder, in the UI config project. So these OSGI configurations will only resolve on AEM author and never on AEM publish. Without this reference dependency on service-user mapped, this OSGI component would be active and available for execution on AEM publish, even though the service-user itself wasn’t available. But since we added this dependency, our content statistics service will be inactive on AEM publish.
The last thing we’ll do before we test this out in AEM, is invoke the, getAllAssetsCount method, which uses the service-user in the OSGI components activate method, and log the results. And this is just to show that if you need access to the AEM repository outside the context of a sling request or any other context that doesn’t have AEM repository access, we can do it using service-users.
All right, we should be good to go. Let’s go ahead and build and deploy the project to AEM author.
Okay. It looks like the build was successful. So let’s jump over to AEM author. I’ve logged in here as the admin user. And, as you can see, I have five assets’ folders, and one of them is the activities folder. I’ll now impersonate a test user I’ve made that has access to all the dam folders, except the activities folder.
And now that I’m logged in as our test user, you can see the activities folder no longer is visible to me. Let’s jump over to our test harness page, which executes our two methods that we implemented on our content’s statistics OSGI service. And as we can see, the one where we pass the active user’s resourceResolver, which is this test user I’m logged in as, that cannot see the activities folder or its contents, shows fewer assets than the method invocation that uses the service-user’s resourceResolver which we permissioned to have read access to all assets, including those in the activities folder. Before we go, let’s check out the AEM author’s air log output.
Let’s look up in the logs to when our application was installed the AEM. And we can see log messages stating that the repoinit scripts ran, as well as what they did. So they were able to create our service-user as well as apply our principal ACL’s.
Looking down a little further, we can see when our content statistics OSGI component activated, and we can see our log message from the activate method, displaying the asset count.
And then below that, we can see some log messages from when we invoked the OSGI service from the test web harness. The first show is the invocation using the wknd-examples user’s resourceResolver that I was logged in as, which doesn’t have access to the activity folders or its assets, and, thus, shows the lower asset count. And the second log message shows the invocation with the service-user’s resourceResolver, which has access to all assets, including the assets folder and, thus, the higher count. Okay, I think that’s about it for how you can define and use service-users in AEM. -
Resources
Code
ContentStatisticsImpl.java
/core/src/main/java/com/adobe/aem/wknd/examples/core/statistics/impl/ContentStatisticsImpl.java
package com.adobe.aem.wknd.examples.core.statistics.impl;
import com.adobe.aem.wknd.examples.core.statistics.ContentStatistics;
import org.apache.sling.api.resource.LoginException;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.serviceusermapping.ServiceUserMapped;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.query.Query;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
@Component(
reference = {
@Reference(
name = SERVICE_USER_SUB_SERVICE_ID,
service = ServiceUserMapped.class,
target = "(subServiceName=wknd-examples-statistics)"
)
}
)
public class ContentStatisticsImpl implements ContentStatistics {
private static final Logger log = LoggerFactory.getLogger(ContentStatisticsImpl.class);
@Reference
private ResourceResolverFactory resourceResolverFactory;
@Override
public int getAssetsCount(final ResourceResolver resourceResolver) {
return getNumberOfAssets(resourceResolver);
}
@Override
public int getAllAssetsCount() {
// Create the Map that specifies the SubService ID
final Map<String, Object> authInfo = Collections.singletonMap(
ResourceResolverFactory.SUBSERVICE,
"wknd-examples-statistics");
// Get the auto-closing Service resource resolver
// Remember, any ResourceResolver you get, you must ensure is closed!
try (ResourceResolver serviceResolver = resourceResolverFactory.getServiceResourceResolver(authInfo)) {
// Do some work with the service user's resource resolver and underlying resources.
// Make sure to do all work that relies on the AEM repository access in the try-block, since the serviceResolver will auto-close when it's left
return queryAndCountAssets(serviceResolver);
} catch (LoginException e) {
log.error("Login Exception when obtaining a User for the Bundle Service: {} ", e);
}
return -1;
}
private int queryAndCountAssets(ResourceResolver resourceResolver) {
final String ASSETS_QUERY = "SELECT * FROM [dam:Asset] WHERE isdescendantnode(\"/content/dam\")";
Iterator<Resource> resources = resourceResolver.findResources(ASSETS_QUERY, Query.JCR_SQL2);
int count = 0;
while (resources.hasNext()) { count++; resources.next(); }
log.info("User [ {} ] found [ {} ] assets", resourceResolver.getUserID(), count);
return count;
}
@Activate
protected void activate() {
// We can use service users in the context's where no natural Sling security context is available to us,
// which is usually outside the context of a Sling HTTP request.
log.debug("Begin calling getAllAssetsCount() from the @Activate method");
int count = getAllAssetsCount();
log.debug("Finished calling getAllAssetsCount() from the @Activate method with count [ {} ]", count);
}
}
org.apache.sling.jcr.repoinit.RepositoryInitializer-wknd-examples-statistics.config
/ui.config/src/main/content/jcr_root/apps/wknd-examples/osgiconfig/config.author/org.apache.sling.jcr.repoinit.RepositoryInitializer-wknd-examples-statistics.config
scripts=["
create service user wknd-examples-statistics-service with forced path system/cq:services/wknd-examples
# When using principal ACLs, the service user MUST be created under system/cq:services
set principal ACL for wknd-examples-statistics-service
allow jcr:read on /content/dam
end
"]
org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended-wknd-examples.cfg.json
/ui.config/src/main/content/jcr_root/apps/wknd-examples/osgiconfig/config.author/org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended-wknd-examples.cfg.json
{
"user.mapping": [
"wknd-examples.core:wknd-examples-statistics=[wknd-examples-statistics-service]"
]
}
recommendation-more-help
4859a77c-7971-4ac9-8f5c-4260823c6f69