AEM Cloud Service - Asset Link InDesign relink local links to AEM using InDesign Server

Goal

Asset Link in InDesign supports creating AEM Links (Link is an AEM path, not local file path). Documents created with AEM Links can be opened on other designers desktops with No Missing Links Warning in Links Panel

The following solution could be useful when legacy documents with local fs links (or documents created by design agencies with no access to AEM) are uploaded to AEM and a user expects AEM to automatically convert the local links to AEM links

1) Design agency creates documents with local links (placing images from file system, not AEM)

2) Documents are uploaded to local AEM SDK or any AEM with access to InDesign Server

3) Media Extraction Step of DAM Update Asset Workflow is updated with Relink Script /apps/settings/dam/indesign/scripts/local-to-aem-relink.jsx

4) getAEMPaths() of local-to-aem-relink.jsx, executing in InDesign Server makes a Query Builder call to AEM, fetch the respective paths for local links...

5) If AEM host/domain is provided in script local-to-aem-relink.jsx (RELINK_HOST_PREFIX), links are rewritten using the specified AEM host, else the AEM host initiating InDesign server request is used in link path...

6) Relinked document is uploaded to AEM as a rendition with suffix -with-aem-links.indd

7) A Post processing workflow step can create new indesign document by moving/copying the rendition and delete the document with local links or keep them both, based on the necessity....

Demo | Package Install | Github



Solution

1) Add the Relink Script in CRXDe /apps/settings/dam/indesign/scripts/local-to-aem-relink.jsx

(function () {
var RELINK_HOST_PREFIX = "",
QUERY_BUILDER_PATH = "/bin/querybuilder.json?path=/content/dam&type=dam:Asset",
AEM_RELINKED_DOC_SUFFIX = "-with-aem-links.indd",
AEMS_PROTOCOL = "aems://";

try{
app.consoleout('Checking if relinking to AEM links is required for : ' + resourcePath);

var links = document.links,
hostPrefix = RELINK_HOST_PREFIX;

if(!hostPrefix){
hostPrefix = AEMS_PROTOCOL + host;
}

if(!hasAEMLinks(links)){
app.consoleout('Document contains local links, AEM relinking required : ' + resourcePath);

var aemPaths = getAEMPaths(links);

relinkLocalToAEM(links, aemPaths, hostPrefix);

var fileName = resourcePath.substring(resourcePath.lastIndexOf ('/') + 1, resourcePath.lastIndexOf ('.') ),
relinkedFileName = fileName + AEM_RELINKED_DOC_SUFFIX,
relinkedFile = new File( sourceFolder.fullName + "/" + relinkedFileName);

document.save(relinkedFile);

app.consoleout('Uploading relinked doc to : ' + target + '/jcr:content/renditions' + ", as : " + relinkedFileName);

putResource(host, credentials, relinkedFile, relinkedFileName, "application/x-indesign", target);
}else{
app.consoleout('Document contains AEM links, relinking to AEM NOT required : ' + resourcePath);
}
}catch(err){
app.consoleout("Error relinking document : " + resourcePath + ", error : " + err);
}

function hasAEMLinks(links){
var containsAEMLinks = false;

for(var i = 0; i < links.length; i++ ){
if(links[i].linkResourceURI.indexOf(AEMS_PROTOCOL) == 0){
containsAEMLinks = true;
break;
}
}

return containsAEMLinks;
}

function relinkLocalToAEM(links, aemPaths, hostPrefix){
for(var i = 0; i < links.length; i++ ){
var link = links[i],
aemPath = aemPaths[link.name];

if(!aemPath){
app.consoleout("Could not find aem path for file : " + link.name);
continue;
}

var aemLink = hostPrefix + aemPath;

try{
app.consoleout("Relinking to aem with uri : " + aemLink);
link.reinitLink(aemLink );
}catch(err){
app.consoleout("error relinking : " + err);
return;
}
}
}

function getInDesignDocRealAEMPath(){
var fileName = resourcePath.substring(resourcePath.lastIndexOf ('/') + 1 ),
query = QUERY_BUILDER_PATH + "&nodename=" + fileName;

var hitMap = fetchJSONObjectByGET(host, credentials, query);

if(!hitMap || (hitMap.hits.length == 0)){
return;
}

var aemPath;

for(var i = 0; i < hitMap.hits.length; i++ ){
aemPath = hitMap.hits[i]["path"];
}

return aemPath;
}

function getAEMPaths(links){
var query = QUERY_BUILDER_PATH + "&group.p.or=true",
aemPaths = {};

for(var i = 0; i < links.length; i++ ){
query = query + "&group." + (i + 1) + "_nodename=" + links[i].name;
}

var hitMap = fetchJSONObjectByGET(host, credentials, query);

if(!hitMap || (hitMap.hits.length == 0)){
app.consoleout('No Query results....');
return;
}

for(var i = 0; i < hitMap.hits.length; i++ ){
aemPaths[hitMap.hits[i]["name"]] = hitMap.hits[i]["path"];
}

return aemPaths;
}
}());


2) Add the relink script path in the Media Extraction Step of DAM Update Asset workflow http://localhost:4502/editor.html/conf/global/settings/workflow/models/dam/update_asset.html


3) Sample InDesign document with local links....


4) On upload, processed by InDesign Server, document with AEM links gets created as rendition of original document....



5) Saved AEM Links document (as rendition) in CRXDe




AEM Cloud Service - Permission Sensitive Caching for Protected Pages


This Adobe documentation very well explains the procedure for Caching Secure Content. The following post just puts together the code, dispatcher changes and screenshots to implement and verify dispatcher cache hits..

1) Add a servlet apps.eaem.sites.core.servlets.AuthCheckerServlet accepting HEAD requests for checking user access to protected pages (using SAML Authentication Handler on Publish)
package apps.eaem.sites.core.servlets;

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingSafeMethodsServlet;
import org.osgi.service.component.annotations.Component;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Session;
import javax.servlet.Servlet;

@Component(
immediate = true,
service = Servlet.class,
property = {
"sling.servlet.methods=HEAD",
"sling.servlet.paths=/bin/experience-aem/permissioncheck"
}
)
public class AuthCheckerServlet extends SlingSafeMethodsServlet {
private Logger logger = LoggerFactory.getLogger(this.getClass());

public void doHead(SlingHttpServletRequest request, SlingHttpServletResponse response) {
String uri = request.getParameter("uri");

try{
Session session = request.getResourceResolver().adaptTo(Session.class);

try {
session.checkPermission(uri, Session.ACTION_READ);

logger.info("READ access APPROVED for uri " + uri + ", user : " + session.getUserID());

response.setStatus(SlingHttpServletResponse.SC_OK);
} catch(Exception e) {
logger.info("READ access DENIED! to uri " + uri);
response.setStatus(SlingHttpServletResponse.SC_FORBIDDEN);
}
}catch(Exception oe){
logger.error("Error checking permissions for uri :" + uri, oe);
}
}
}

2) Add necessary configuration in farm file dispatcher\src\conf.dispatcher.d\available_farms\eaem.farm for the dispatcher to call above servlet, check user access to page...

/publishfarm {
....
/cache {
/docroot "${DOCROOT}/content/marketing-hub"
....
/rules {
/0000 { /glob "*" /type "deny" }
/0001 { /glob "*.html" /type "allow" }
}
....
}
/auth_checker {
/url "/bin/experience-aem/permissioncheck"

/filter
{
/0000
{
/glob "*"
/type "deny"
}
/0001
{
/glob "/content/marketing-hub/*.html"
/type "allow"
}
}
/headers
{
/0000
{
/glob "*"
/type "deny"
}
/0001
{
/glob "Set-Cookie:*"
/type "allow"
}
}
}
}

3) Internal Dispatcher request to the publish url /bin/experience-aem/permissioncheck can be debugged in IDE (on both author and publish this safe servlet call doesn't need authentication)



4) Local dispatcher log shows the hits...



5) Opening the Docker Dispatcher Container terminal, you can find the cached protected pages in docroot eg.  /mnt/var/www/html/content/marketing-hub




6) Download the dispatcher log from cloud manager and you can observe the cache hit log statements....

[20/Oct/2022:21:14:26 +0000] [I] [cm-pxxxxx-exxxxx-aem-publish-xxxxxxx-xxxxxx] "GET /content/marketing-hub/en/website-management/aem-handling/adobe-sreek-test.html" -139ms [hub/-] [actionhit] dev.mywebsite.com

7) In the downloaded publish log, auth checker servlet leaves traces of dispatcher calls for checking user permissions....

2022-10-20 21:14:26.065 *INFO* [107.204.55.186 [1666300466054] HEAD /bin/experience-aem/permissioncheck HTTP/1.0] apps.eaem.sites.core.servlets.AuthCheckerServlet READ access APPROVED for uri /content/marketing-hub/en/website-management/aem-handling/adobe-sreek-test.html, user : xxxxxxxx/xxxxxxx

AEM Cloud Service - Debug SAML Assertions redirecting from Customer's IDP to Local Publish SDK


For authenticating end users AEM Publish can be configured with SAML2.0 connecting to an IDP to authenticate users and create authenticated session in AEM, for more info check documentation. This post is on investigating SAML connection/assertions issues by redirecting responses from customer's IDP to local publish AEM sdk (instead of cloud services publish instance). Investigating assertions on a local instance by attaching debugger can be much faster compared to downloading logs from cloud manager where the issue exists...

1) As all AEM cloud services instances are on https, enable the local dispatcher sdk to run on https. For more details check https://experience-aem.blogspot.com/2022/10/aem-cloud-service-setup-local-sdk-docker-https-ssl.html

2) Get the SAML specific values from Environment Configuration of customer's publish instance...



3) Add the environment variables in publish start script. Attach a debugger to local AEM SDK Publisher...

REM DEV
set SAML_AEM_ID=https://publish-pxxxxx-exxxxx.adobeaemcloud.com/
set SAML_IDP_CERT_ALIAS=certalias___999999999999999

set SAML_IDP_URL=https://login.microsoftonline.com/xxxx-xxxx-xxx-xxx-xxxxxxxx/saml2
set SAML_IDP_REFERRER=login.microsoftonline.com
C:/Progra~1/Java/jdk-11.0.6/bin/java -Xdebug -Xrunjdwp:transport=dt_socket,address=6006,suspend=n,
server=y -XX:MaxPermSize=512m -Xmx1024M -Doak.queryLimitInMemory=500000 -Doak.queryLimitReads=500000
-Dsling.run.modes=publish -jar aem-sdk-quickstart-2022.9.8630.20220905T184657Z-220800-p4503.jar -nofork


4) In Local Publish AEM SAML Authentication handler configuration (com.adobe.granite.auth.saml.SamlAuthenticationHandler~eaem.cfg.json) set useEncryption to false (so we only need the truststore configuration and not private key configuration of authentication-service user making it a simpler setup...)

........
........
"idpUrl": "$[env:SAML_IDP_URL;default=https://myapps.microsoft.com/signin/xxx-xxx-xxx-xxx-xxxxxxxx?tenantId=xxxx-xxx-xxx-xxx-xxxxxxx]",
"serviceProviderEntityId": "$[env:SAML_AEM_ID;default=https://publish-pxxxxx-exxxxx.adobeaemcloud.com]",
"useEncryption": false,
"createUser": true,
........
........

5) Get the trust store from customer's author instance crxde (generally the same certificate is activated to publish). Package it up on customer's author with filter /etc/truststore and install the package on local AEM publish...



6)  The encrypted keystorePassword is unique to each AEM instance (encrypted/decrypted using the hmac and master keys available on file system of bundle Adobe Granite Crypto Bundle Key Provider), so the keystorePassword exported from customer's AEM instance cannot be decrypted using the crypto keys of local AEM so we need to encrypt the keystorePassword using local AEM keys and add it in /etc/truststore

7) Connect the IDE debugger to local publish instance, execute a simple POST call to http://localhost:4503/content/marketing-hub/saml_login and debug method extractStorePassword() of class com.adobe.granite.keystore.internal.KeyStoreServiceImpl; use cryptoSupport.protect("plainPasswordOfTrustStore") code statement to generate the local AEM specific encrypted keystorePassword 



8) Once you have the keystorePassword of trust store encrypted using local AEM keys, open crxde and add it in /etc/truststore@keystorePassword, save..

9) Next step is to spoof the requests that are originally for customer's AEM env. If you are on windows open the hosts file c:\Windows\System32\Drivers\etc\hosts and add the customer's AEM domain entry with your local machine's IP, save and flush the DNS cache using command prompt ipconfig /flushdns



10) Configuration is now complete and you can try the local AEM publish to customer's IDP authentication flow, accessing local AEM on https using customer's AEM domain name...




11) To read the SAML Assertions put a debug point in parse() method of com.adobe.granite.auth.saml.util.SamlReader




12) With successful IDP authentication you should see the SAML protected page on local AEM publish...










AEM Cloud Service - Fix Publish Dispatcher SAML Login Loop with IDP


If you are experiencing a login (authentication) loop with IDP while integrating AEM Publish/Dispatcher with an IDP like Azure AD using SAML2, the following fix might help...


The Login Loop

1) /en.html has the following redirect configured in dispatcher vhost file eg. dispatcher\src\conf.d\available_vhosts\eaem.vhost, rewriting the url /en.html to /content/marketing-hub/en.html when sent to publish aem...

RewriteRule ^/en(.*) /content/marketing-hub/en$1 [PT,L]


2) Page structure /content/marketing-hub in Publish is protected using SAML authentication configured in ui.config\src\main\content\jcr_root\apps\eaem-mkthub-sample\osgiconfig\config\com.adobe.granite.auth.saml.SamlAuthenticationHandler~eaem.cfg.json

{
"path": [ "/content/marketing-hub" ],
"idpCertAlias": "$[env:SAML_IDP_CERT_ALIAS;default=certalias___11111]",
"spPrivateKeyAlias": "$[env:SAML_PRIVATE_KEY_ALIAS;default=certalias___1111]",
"keyStorePassword": "password",
"idpIdentifier": "$[env:SAML_IDP_ID;default=https://sts.windows.net/xxxxx/]",
"idpUrl": "$[env:SAML_IDP_URL;default=https://myapps.microsoft.com/signin/xxx-xx-xxx-xxx-xxxx?tenantId=xxxx-xxx-xx-xx-xxxxx]",
"serviceProviderEntityId": "$[env:SAML_AEM_ID;default=https://publish-p99999-e999999.adobeaemcloud.com]",
"useEncryption": false,
"createUser": true,
"userIntermediatePath": "",
"synchronizeAttributes":[
"uid=userPrincipalName"
],
"defaultRedirectUrl": "/content/marketing-hub/en.html",
"addGroupMemberships": true,
"defaultGroups": [
"contributor"
]
}


3) User makes request to a SSO protected page eg. https://dev.experinceaem.com/en.html, internally gets rewritten to /content/marketing-hub/en.html, activating SAML authentication handler on publish, redirecting to the IDP login page...


4) User enters credentials and after successful authentication, IDP posts the SAML assertion to AEM Dispatcher url */saml_login


5) AEM validates the SAML assertion (after some decryption process) and redirects to user requested page /eh.html


6) At this point, since the response of /en.html was cached at CDN, when request was made in step 3 and the cached response is a request to IDP login page, user is redirected back to IDP login page with response header x-cache:HIT, resulting in a loop (until the cached response at CDN expires with default TTL set, may be 5 mts)...



Fix the Loop

1) DoNot cache protected pages at Fastly CDN using the proprietary header Surrogate-Control

<LocationMatch "^/content/marketing-hub">
Header unset Surrogate-Control
Header always set Surrogate-Control "no-store, no-cache"
</LocationMatch>


2)  With the Surrogate-Control header set for requests starting with /content/marketing-hub, responses of these pages are not cached at CDN resulting in a x-cache:MISS and requests are passed to dispatcher and subsequently to Publish (assuming Permission-Sensitive Caching was not implemented)






AEM Cloud Service - Setup Local SDK Docker Dispatcher https ssl using self-signed certs


Setup up Local sdk Dispatcher with Https/SSL, so the author/publish/dispatcher flow can be tested with https (unlike live sites where the ssl terminates at load balancer, it terminates at dispatcher here...) 

1) Download SDK Dispatcher tools and extract aem-sdk-dispatcher-tools-x.x.x-windows.zip to a local drive

2) copy paste dispatcher\bin\docker_run.cmd as dispatcher\bin\docker_run_https.cmd and change docker run command (directing docker to pass-on requests to port 8443)

                   docker run --rm -p %localport%:80 %volumes% %envvars% %imageurl%

                    to

                    docker run --rm -p %localport%:8443 %volumes% %envvars% %imageurl%

3) Generate self-signed public/private key certs using OpenSSL

                 set OPENSSL_CONF=C:/dev/install/OpenSSL-Win64/bin/openssl.cfg

                 openssl req -new -newkey rsa:4096 -x509 -sha256 -days 365 -nodes -out eaem.crt -keyout eaem.key



4) Place the generated eaem.crt and eaem.key files in project dispatcher conf.d folder eg. eaem-ssl-dsipatcher-sample\dispatcher\src\conf.d

5) In your project vhost file eg. eaem-ssl-dsipatcher-sample\dispatcher\src\conf.d\available_vhosts\eaem.vhost add a virtual host configuration for ssl on port 8443; provide the path to public cert and key files added above...

LoadModule ssl_module modules/mod_ssl.so

Listen 8443
<VirtualHost *:8443>
SSLEngine on
SSLProtocol -all +TLSv1 +TLSv1.1 +TLSv1.2
SSLCertificateFile conf.d/eaem.crt
SSLCertificateKeyFile conf.d/eaem.key

ServerName "publish"
# Put names of which domains are used for your published site/content here
ServerAlias "*"
....
....
....
</VirtualHost>

6) Start Docker using docker_run_https.cmd command (running the following command Docker listens on default https port 443 and forwards the request to apache container listening on port 8443 for https)

                  bin\docker_run_https C:/dev/projects/eaem-ssl-dsipatcher-sample/dispatcher/src host.docker.internal:4503 443

7) Access a sample page using https eg. https://localhost/content/ eaem-ssl-dsipatcher-sample/us/en/home.html