AEM CQ 6 - MSM, Setting JCR Permissions on Live Copy Components

Goal


This post is for MSM (Multi Site Manager) administrators who need more control on live copy components. A live copy author can click Cancel Inheritance on page components to break the inheritance relation of component with blue print site.

Using jcr:write privileges on the component nodes at JCR level, we can restrict live copy authors from breaking the relation and modifying content. Check Demo, Download Source Code and Package Install

The package was tested and works on 561; For a different approach (does not provide detail component level permissions) on dealing with the same problem check this post

Before we move to the solution part, here is how it works

1) Create a live copy Geo One (/content/geometrixx-outdoors1) for site Geometrixx Outdoors Site (/content/geometrixx-outdoors)

2) Blueprint site admin open's the page (eg. http://localhost:4502/cf#/content/geometrixx-outdoors/en/toolbar/about-us.html) in author environment for providing permissions on live copy components. Click the component menu option Set Cancel Inheritance. This option is added by the extension



3) Set Cancel Inheritance menu option opens the following dialog. Here you select live copy, user/group on which the privilege jcr:write needs to be set



   Click ok and a notification appears on the top right of screen




4) So for live copy page /content/geometrixx-outdoors1/en/toolbar/about-us, component  /jcr:content/par/text_8d9e user author is denied the jcr:write permission



5) Login to CRXDE Lite http://localhost:4502/crx/de and browse to node of live copy Geo One node /content/geometrixx-outdoors1/en/toolbar/about-us/jcr:content/par/text_8d9e. The jcr:write permission is denied to author



6) Login as author and try to click Cancel Inheritance of the component, on live copy Geo One page http://localhost:4502/cf#/content/geometrixx-outdoors1/en/toolbar/about-us.html



   Error is shown on top of screen



7) When user author clicks on the Cancel Inheritance of dialog, dialog becomes editable but any content added is not saved and UI shows error

Dialog becomes editable on clicking cancel inheritance lock icon



Click ok and error pops



8) So using the extension a blueprint admin can control which users/groups can modify the components of a live copy page.

Even without the extension, a blueprint site admin can set the permissions on page component nodes by visiting CRXDE, but setting privileges in the context of page is more intuitive which is what this extension provides

Solution


We need a servlet to handle the get/set of privileges and clientlib for the UI parts. The package install above is created using the multimodule-content-package-archetype, for detailed notes on how to create a package using multimodule-content-package-archetype in Intellij check this post

Servlet


The doPost method in below servlet creates jcr:write privilege on the live copy component node and doGet returns the privilege on live copy node for a principal, as json

package apps.experienceaem.msm;

import org.apache.commons.lang3.StringUtils;
import org.apache.felix.scr.annotations.*;
import org.apache.felix.scr.annotations.Properties;
import org.apache.jackrabbit.api.JackrabbitSession;
import org.apache.jackrabbit.api.security.JackrabbitAccessControlEntry;
import org.apache.jackrabbit.api.security.JackrabbitAccessControlList;
import org.apache.jackrabbit.api.security.JackrabbitAccessControlPolicy;
import org.apache.jackrabbit.api.security.user.Authorizable;
import org.apache.jackrabbit.api.security.user.UserManager;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.apache.sling.commons.json.JSONArray;
import org.apache.sling.commons.json.JSONException;
import org.apache.sling.commons.json.JSONObject;
import org.apache.sling.commons.json.io.JSONWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.Session;
import javax.jcr.security.*;
import javax.servlet.ServletException;
import java.io.IOException;

@Component(metatype = false)
@Service
@Properties({
    @Property(name = "sling.servlet.paths", value = "/bin/experience-aem/msm/acl"),
    @Property(name = "sling.servlet.methods", value = { "GET", "POST" } )
})
public class AccessControl extends SlingAllMethodsServlet {

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

    /**
     * Returns the ACL of path
     *
     * @param session
     * @param path
     * @return
     * @throws Exception
     */
    private JackrabbitAccessControlList getACL(Session session, String path) throws Exception{
        AccessControlManager acMgr = session.getAccessControlManager();

        JackrabbitAccessControlList acl = null;
        AccessControlPolicyIterator app = acMgr.getApplicablePolicies(path);

        while (app.hasNext()) {
            AccessControlPolicy pol = app.nextAccessControlPolicy();

            if (pol instanceof JackrabbitAccessControlPolicy) {
                acl = (JackrabbitAccessControlList) pol;
                break;
            }
        }

        if(acl == null){
            for (AccessControlPolicy pol : acMgr.getPolicies(path)) {
                if (pol instanceof JackrabbitAccessControlPolicy) {
                    acl = (JackrabbitAccessControlList) pol;
                    break;
                }
            }
        }

        return acl;
    }

    @Override
    protected void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response)
                        throws ServletException, IOException {
        String liveCopies = request.getParameter("liveCopies");
        String path = request.getParameter("path");
        String principal = request.getParameter("principal");
        String type = request.getParameter("type");

        if(StringUtils.isEmpty(liveCopies) || StringUtils.isEmpty(path) || StringUtils.isEmpty(principal)){
            throw new RuntimeException("Required parameters missing");
        }

        if(StringUtils.isEmpty(type)){
            type = "ALLOW";
        }

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

            for(String copy: liveCopies.split(",")){
                String compPath = copy + path;
                JackrabbitAccessControlList acl = getACL(session, compPath);

                if(acl == null){
                    throw new RuntimeException("ACL not found for  path: " + compPath);
                }

                UserManager uMgr = ((JackrabbitSession) session).getUserManager();
                Authorizable authorizable = uMgr.getAuthorizable(principal);

                Privilege[] p = new Privilege[]{ acMgr.privilegeFromName(Privilege.JCR_WRITE) };
                acl.addEntry(authorizable.getPrincipal(), p, type.equalsIgnoreCase("ALLOW"));

                acMgr.setPolicy(compPath, acl);
            }

            session.save();

            JSONWriter jw = new JSONWriter(response.getWriter());
            jw.object().key("success").value("success").endObject();
        }catch(Exception e){
            log.error("Error adding acl in path - " + path + ", for - " + liveCopies, e);
            throw new ServletException(e);
        }
    }

    @Override
    protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response)
            throws ServletException, IOException {
        response.setContentType("application/json");
        response.setCharacterEncoding("utf-8");

        String path = request.getParameter("path");
        String privilege = request.getParameter("privilege");

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

            Privilege privileges[] = null; String pName = null;
            JSONObject map = null, allow = new JSONObject(), deny = new JSONObject();
            JSONArray arr = null;

            JackrabbitAccessControlList acl = getACL(session, path);
            AccessControlEntry entries[] = acl.getAccessControlEntries();

            for(AccessControlEntry entry : entries){
                privileges = entry.getPrivileges();

                map = ((JackrabbitAccessControlEntry)entry).isAllow() ? allow : deny;

                for(Privilege p : privileges){
                    pName = p.getName();

                    if(StringUtils.isNotEmpty(privilege) && !privilege.equals(pName)){
                        continue;
                    }

                    try{
                        arr = (JSONArray)map.get(pName);
                    }catch(JSONException je){
                        arr = new JSONArray();
                        map.put(pName, arr);
                    }

                    arr.put(entry.getPrincipal().getName());
                }
            }

            JSONWriter jw = new JSONWriter(response.getWriter());
            jw.object().key("allow").value(allow).key("deny").value(deny).endObject();
        }catch(Exception e){
            log.error("Error getting privileges for path - " + path, e);
            throw new ServletException(e);
        }
    }
}


UI Client lib


We need a clientlib with necessary JS code for adding Set Cancel Inheritance menu option and subsequent dialog

1) Login to CRXDE Lite (http://localhost:4502/crx/de) and create folder /apps/msm-disable-cancel-inheritance-users

2) Create node /apps/msm-disable-cancel-inheritance-users/clientlib of type cq:ClientLibraryFolder and add a String property categories with value cq.widgets

3) Create file (nt:file) /apps/msm-disable-cancel-inheritance-users/clientlib/js.txt and add

                       disable-cancel-inheritance-for-users.js

4) Create file (nt:file) /apps/msm-disable-cancel-inheritance-users/clientlib/disable-cancel-inheritance-for-users.js and add the following code

CQ.Ext.ns("ExperienceAEM.MSM");

ExperienceAEM.MSM.Blueprint = {
    liveCopies: [],

    //the dialog for user to choose the live copy, user/group
    getCancelInheritanceDialog: function(path){
        path = path.substring(path.indexOf("/jcr:content"));

        var allow = new CQ.Ext.form.Label( { text: "Allow : Select a live copy" });
        var deny = new CQ.Ext.form.Label( { text: "Deny : Select a live copy" });

        var getLiveCopies = function(lBox){
            var lCopies = lBox.getValue();

            if(lCopies == "ALL"){
                var items = lBox.getStore().data.items;
                lCopies = [];

                CQ.Ext.each(items, function(item){
                    if(item.id == "ALL"){
                        return;
                    }

                    lCopies.push(item.id);
                });

                lCopies = lCopies.join(",");
            }

            return lCopies;
        };

        //updates the labels with users/groups allow/deny the jcr:write permission
        var showPrivileges = function(lBox){
            if(lBox.getValue() == "ALL"){
                allow.setText("Allow : Select a live copy");
                deny.setText("Deny : Select a live copy");
                return;
            }

            //get the allow/deny permissions as json
            $.ajax({ url: "/bin/experience-aem/msm/acl", dataType: "json",
                data: { path : lBox.getValue() + path, privilege: "jcr:write" },
                success: function(data){
                    var privs = data["allow"];

                    if(privs && !CQ.Ext.isEmpty(privs["jcr:write"])){
                        allow.setText("Allow : " + privs["jcr:write"].join(" "));
                    }else{
                        allow.setText("Allow : None set");
                    }

                    privs = data["deny"];

                    if(privs && privs["jcr:write"]){
                        deny.setText("Deny : " + privs["jcr:write"].join(" "));
                    }else{
                        deny.setText("Deny : None set");
                    }
                },
                type: "GET"
            });
        };

        var dialogConfig = {
            "jcr:primaryType": "cq:Dialog",
            title: "Set Inheritance Options - " + path,
            modal: true,
            width: 600,
            height: 300,
            items: [{
                xtype: "panel",
                layout: "form",
                bodyStyle :"padding: 20px",
                items: [{
                            xtype: "panel",
                            border: false,
                            bodyStyle :"margin-bottom: 10px",
                            items: allow
                        },{
                            xtype: "panel",
                            border: false,
                            bodyStyle :"margin-bottom: 25px",
                            items: deny
                        },{
                            anchor: "95%",
                            xtype: "combo",
                            style: "margin-bottom: 20px",
                            mode: 'local',
                            fieldLabel: "Select Live Copy",
                            store: new CQ.Ext.data.ArrayStore({
                                id: 0,
                                fields: [ 'id', 'text' ],
                                data: this.liveCopies
                            }),
                            valueField: 'id',
                            displayField: 'text',
                            triggerAction: "all",
                            listeners:{
                                scope: this,
                                'select': function(combo){
                                    //when a livecopy is selected, make an ajax call to get the jcr:write permission 
                                    showPrivileges(combo);
                                }
                            }
                        },{
                            valueField: "id",
                            displayField: "name",
                            fieldLabel: "Select User/Group",
                            style: "margin-bottom: 20px",
                            autoSelect: true,
                            xtype: "authselection"
                        },{
                            xtype: 'radiogroup',
                            columns: 6,
                            fieldLabel: "Cancel Inheritance ",
                            items: [{
                                boxLabel: ' Allow',
                                name: 'type',
                                value: 'ALLOW',
                                checked: true
                            },{
                                name: 'type',
                                boxLabel: ' Deny',
                                value: 'DENY',
                                checked: false
                        }]
                    }]
            }],
            ok: function () {
                var lBox = this.findByType("combo")[0];
                var uBox = this.findByType("authselection")[0];
                var tBox = this.findByType("radiogroup")[0];

                var options = {
                    path: path,
                    liveCopies: getLiveCopies(lBox),
                    principal: uBox.getValue(),
                    type: tBox.getValue().value
                };

                this.close();

                //save the user/group allow/deny privileges on the live copy component
                $.ajax({
                    url: "/bin/experience-aem/msm/acl",
                    dataType: "json",
                    data: options,
                    success: function(){
                        CQ.Notification.notify("Cancel Inheritance","Access controls set for " + options.principal);
                    },
                    error: function(){
                        CQ.Notification.notify("Cancel Inheritance","Error setting access controls for " + options.principal);
                    },
                    type: 'POST'
                });
            }
        };

        return CQ.WCM.getDialog(dialogConfig);
    },

    //get the livecopies for a blueprint. If the site has not live copies "Set Cancel Inheritance" menu option is not shown
    readLiveCopies: function(){
        var sk = CQ.WCM.getSidekick();

        $.ajax({ url: sk.getPath() + "/jcr:content.blueprint.json", async: false, dataType: "json",
            success: function(data){
                if(!data){
                    return;
                }

                var liveCopies = data["msm:targets"];

                //return if there are no live copies
                if(CQ.Ext.isEmpty(liveCopies)){
                    return;
                }

                this.liveCopies.push( [ "ALL", "All" ] );

                CQ.Ext.each(liveCopies, function(lCopy){
                    this.liveCopies.push([ lCopy, lCopy ])
                }, this);
            }.createDelegate(this)
        });
    },

    //browse editables and add the Set Cancel Inheritance menu option
    addSetCancelInheritance: function () {
        this.readLiveCopies();

        if(CQ.Ext.isEmpty(this.liveCopies)){
            return;
        }

        var editables = CQ.utils.WCM.getEditables();

        CQ.Ext.iterate(editables, function (path, editable) {
            if(!editable.addElementEventListener){
                return;
            }

            editable.addElementEventListener(editable.element.dom, "contextmenu", function () {
                var component = this.element.linkedEditComponent;

                if (!component || !component.menuComponent) {
                    return;
                }

                var menu = component.menuComponent;

                if (menu.cancelInheritanceSet) {
                    return;
                }

                menu.addSeparator();

                menu.add({
                    text: "Set Cancel Inheritance",
                    handler: function () {
                        var dialog = ExperienceAEM.MSM.Blueprint.getCancelInheritanceDialog(path);
                        dialog.show();
                    }
                });

                menu.cancelInheritanceSet = true;
            }, true, editable);
        });
    }
};

(function() {
    var E = ExperienceAEM.MSM.Blueprint;

    if (( window.location.pathname == "/cf" ) || ( window.location.pathname.indexOf("/content") == 0)) {
        if (CQ.WCM.isEditMode()) {
            CQ.WCM.on("editablesready", E.addSetCancelInheritance, E);
        }
    }
})();




No comments:

Post a Comment