Dev: So we can share everything in the content repo and trust the app to hide stuff, right?
Me: That is how you wake up on call at three in the morning. Put the locks in the repository. Let the app ride on top.
Admin: Which locks? LDAP knows groups. The repo knows nodes. Where do they meet?
Me: In JCR, they meet in the AccessControlManager. That is where access rules live close to the content. No secret flag in your app. No if in your servlet.
Evidence that JCR access control works in real projects
JSR two eight three gave us a standard way to ask and set permissions. You get Privilege objects like jcr:read, jcr:write, jcr:readAccessControl, and friends. With AccessControlPolicy and AccessControlList you can attach rules to a node path. Apache Jackrabbit two.x ships this today. CQ and other products that sit on Jackrabbit lean on the same API. ModeShape is in that game too.
In Jackrabbit, policies usually show up as a child node named rep:policy below your content. That node holds Access Control Entries or ACEs. Each ACE binds a Principal which is a user or a group to a set of privileges and Jackrabbit also lets you flag an entry as allow or deny. The standard API stays neutral on allow or deny, but Jackrabbit exposes it with an extra interface. This is not theory. This is what you get when you crack open the repo and look.
Last bit of proof. When the repo enforces rules, the same protection applies to queries, WebDAV, RMI, and any app that logs in with a JCR session. One rule. Many doors.
Build notes for Access Control in JCR
Here is the smallest round trip. Ask for the supported privileges, create or edit an ACL, then save the session.
Session session = repository.login(new SimpleCredentials("admin", "admin".toCharArray()));
AccessControlManager acm = session.getAccessControlManager();
// Inspect what the repo supports at a path
Privilege[] supported = acm.getSupportedPrivileges("/content/site");
// Pick a basic set
Privilege read = acm.privilegeFromName(Privilege.JCR_READ);
Privilege write = acm.privilegeFromName(Privilege.JCR_WRITE);
// Get or create a policy at the path
AccessControlPolicy[] policies = acm.getPolicies("/content/site");
AccessControlList acl;
if (policies.length == 0) {
AccessControlPolicyIterator it = acm.getApplicablePolicies("/content/site");
acl = (AccessControlList) it.nextAccessControlPolicy();
} else {
acl = (AccessControlList) policies[0];
}
// Bind a group to read only
Principal authors = new Principal() { public String getName() { return "authors"; } };
acl.addAccessControlEntry(authors, new Privilege[]{ read });
// Apply and persist
acm.setPolicy("/content/site", acl);
session.save();If you need deny entries or path based restrictions, Jackrabbit gives you an extended ACL. The next snippet shows a deny on write under a subtree while still letting the same group read.
import org.apache.jackrabbit.api.security.JackrabbitAccessControlList;
JackrabbitAccessControlList jacl = (JackrabbitAccessControlList) acl;
// Allow read everywhere under the path
jacl.addEntry(authors, new Privilege[]{ read }, true, null);
// Deny write under a subtree using a glob restriction
Map<String, Value> restrictions = new HashMap<>();
restrictions.put("rep:glob", session.getValueFactory().createValue("/news/*"));
jacl.addEntry(authors, new Privilege[]{ write }, false, restrictions);
acm.setPolicy("/content/site", jacl);
session.save();Plumbing tips that save time:
- Wire principals to LDAP. JCR cares about principals, not how they came to be. Let JAAS logins pull users and groups from your directory. The ACLs stay clean.
- Stay close to the tree. Put rules at the nearest stable folder. Do not set a rule at the root and hope the deny will carve it later. Keep it simple.
- Favor primitive privileges. Aggregate sets like
jcr:allare handy but hide what is going on. Use read, write, add child nodes, remove child nodes when you can. - Test with a clean session. After changes, open a new session for checks so you see what real users will see.
Risks and gotchas you will want to avoid
Too many ACEs. Every extra entry at a path has a cost. A thousand tiny rules sprinkled across deep trees hurts both reads and writes. Batch by folder. Use groups.
Relying on app only checks. If your servlet hides a node but the repo still serves it to a query, you have a hole. Always let the repo enforce the final say.
Deny logic surprises. The standard API does not define how allow and deny should be resolved. Jackrabbit has a clear story but other repos may not. If you need deny, stick to one engine and test the order you add entries. Keep allow only if you aim for portability.
Privilege confusion. Write is not the same as add child nodes. A user might edit properties but not add a new page. Check the spec names. They are precise.
Security through naming. A clever path like /tmp that you hope users will not find is not a rule. Put a real ACL on it or move it out of reach.
Forgetting to save. Policy edits are transient like any other change set in JCR. No save, no effect.
Graceful exit
Keep the rules where the content lives. Use AccessControlManager to write them once and let every entry point respect them. Start with three moves: define groups in your directory, place read and write at the nearest stable folders, and script your ACLs so you can replay them in any env. When the next app shows up and logs in, you will sleep well. The repo will do the heavy lifting.