Skip to content

Commit 60df08c

Browse files
author
Jeff Bornemann
committed
Support for writing rep:policy trees
1 parent 9d3134b commit 60df08c

14 files changed

+964
-86
lines changed
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
/*
2+
*
3+
* * Copyright 2015 Time Warner Cable, Inc.
4+
* *
5+
* * Licensed under the Apache License, Version 2.0 (the "License");
6+
* * you may not use this file except in compliance with the License.
7+
* * You may obtain a copy of the License at
8+
* *
9+
* * http://www.apache.org/licenses/LICENSE-2.0
10+
* *
11+
* * Unless required by applicable law or agreed to in writing, software
12+
* * distributed under the License is distributed on an "AS IS" BASIS,
13+
* * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* * See the License for the specific language governing permissions and
15+
* * limitations under the License.
16+
*
17+
*/
18+
19+
package com.twcable.grabbit.jcr
20+
21+
import groovy.transform.CompileStatic
22+
import groovy.util.logging.Slf4j
23+
import java.security.AccessControlException
24+
import java.security.Principal
25+
import javax.annotation.Nonnull
26+
import javax.jcr.PathNotFoundException
27+
import javax.jcr.Session
28+
import javax.jcr.Value
29+
import javax.jcr.security.AccessControlEntry
30+
import javax.jcr.security.AccessControlManager
31+
import javax.jcr.security.AccessControlPolicy
32+
import javax.jcr.security.AccessControlPolicyIterator
33+
import javax.jcr.security.Privilege
34+
import org.apache.jackrabbit.api.security.JackrabbitAccessControlList
35+
import org.apache.jackrabbit.api.security.principal.PrincipalManager
36+
import org.apache.sling.jcr.base.util.AccessControlUtil
37+
38+
39+
import static com.twcable.grabbit.proto.NodeProtos.Node as ProtoNode
40+
41+
/**
42+
* Wraps a rep:policy (rep:ACL) node, providing the ability to write it, and it's child ACE, and restriction nodes
43+
*/
44+
@CompileStatic
45+
@Slf4j
46+
class ACLProtoNodeDecorator extends ProtoNodeDecorator {
47+
48+
49+
protected ACLProtoNodeDecorator(@Nonnull ProtoNode repACLNode, @Nonnull Collection<ProtoPropertyDecorator> protoProperties, String nameOverride) {
50+
this.innerProtoNode = repACLNode
51+
this.protoProperties = protoProperties
52+
this.nameOverride = nameOverride
53+
}
54+
55+
56+
@Override
57+
JCRNodeDecorator writeToJcr(@Nonnull Session session) {
58+
/**
59+
* We don't write the rep:policy node directly. Rather, we find the rep:policy node's ACE(s) and add them to the
60+
* owner's existing policy; or we add them to a new policy.
61+
*/
62+
clearExistingEntries(session)
63+
innerProtoNode.mandatoryChildNodeList.each { ProtoNode node ->
64+
if(isGrantACEType(node)) {
65+
writeGrantACE(session, node)
66+
}
67+
else if(isDenyACEType(node)) {
68+
writeDenyACE(session, node)
69+
}
70+
}
71+
session.save()
72+
try {
73+
return new JCRNodeDecorator(session.getNode(getName()))
74+
} catch(PathNotFoundException ex) {
75+
//We may not have been able to write the policy node if for example the principal does not exist
76+
return new JCRNodeDecorator(session.getNode(getParentPath()))
77+
}
78+
}
79+
80+
81+
private void clearExistingEntries(final Session session) {
82+
final JackrabbitAccessControlList acl = getAccessControlList(session)
83+
if(acl.accessControlEntries.length != 0) {
84+
acl.accessControlEntries.each { AccessControlEntry entry ->
85+
acl.removeAccessControlEntry(entry)
86+
}
87+
getAccessControlManager(session).setPolicy(getParentPath(), acl)
88+
session.save()
89+
}
90+
}
91+
92+
93+
private void writeGrantACE(final Session session, ProtoNode grantACENode) {
94+
writeACE(session, grantACENode, true)
95+
}
96+
97+
98+
private void writeDenyACE(final Session session, ProtoNode denyACENode) {
99+
writeACE(session, denyACENode, false)
100+
}
101+
102+
103+
private void writeACE(final Session session, ProtoNode aceNode, boolean grant) {
104+
JackrabbitAccessControlList acl = getAccessControlList(session)
105+
final String principalName = getPrincipalName(aceNode)
106+
Principal principal = getPrincipal(session, principalName)
107+
if(principal == null) {
108+
log.warn "Principal for name ${principalName} does not exist, or is not accessible. Can not write ACE/ACL information."
109+
return
110+
}
111+
Privilege[] privileges = getPrivilegeNames(aceNode).collect { String privilegeName ->
112+
if(isSupportedPrivilege(session, privilegeName)) {
113+
return getPrivilege(session, privilegeName)
114+
}
115+
else {
116+
throw new IllegalStateException("${privilegeName} is not a supported privilege on this JCR implementation. Bailing out")
117+
}
118+
}
119+
Map<String, Value> restrictionMap
120+
if(hasRestrictions(aceNode)) {
121+
ProtoNode restrictionNode = getRestrictionNode(aceNode)
122+
restrictionMap = extractRestrictionMapFrom(restrictionNode)
123+
}
124+
if(restrictionMap != null) {
125+
acl.addEntry(principal, privileges, grant, restrictionMap)
126+
}
127+
else {
128+
acl.addEntry(principal, privileges, grant)
129+
}
130+
getAccessControlManager(session).setPolicy(getParentPath(), acl)
131+
}
132+
133+
134+
private boolean isGrantACEType(ProtoNode node) {
135+
return node.propertiesList.any { new ProtoPropertyDecorator(it).isGrantACEType() }
136+
}
137+
138+
139+
private boolean isDenyACEType(ProtoNode node) {
140+
return node.propertiesList.any { new ProtoPropertyDecorator(it).isDenyACEType() }
141+
}
142+
143+
144+
private boolean hasRestrictions(ProtoNode aceNode) {
145+
return aceNode.mandatoryChildNodeList.any { ProtoNode childNode ->
146+
childNode.propertiesList.any { new ProtoPropertyDecorator(it).isRepRestrictionType() }
147+
}
148+
}
149+
150+
151+
private ProtoNode getRestrictionNode(ProtoNode aceNode) {
152+
return aceNode.mandatoryChildNodeList.find { ProtoNode childNode ->
153+
childNode.propertiesList.any { new ProtoPropertyDecorator(it).isRepRestrictionType() }
154+
}
155+
}
156+
157+
158+
private String getPrincipalName(ProtoNode node) {
159+
node.propertiesList.collect { new ProtoPropertyDecorator(it) }.find { ProtoPropertyDecorator property ->
160+
property.isPrincipalName()
161+
}.getStringValue()
162+
}
163+
164+
165+
private String[] getPrivilegeNames(ProtoNode node) {
166+
final ProtoPropertyDecorator privilegeProperty = node.propertiesList.collect { new ProtoPropertyDecorator(it) }.find { ProtoPropertyDecorator property ->
167+
property.isPrivilege()
168+
}
169+
return privilegeProperty.getPropertyValues().collect { Value value -> value.string }.toArray() as String[]
170+
}
171+
172+
173+
private Map<String, Value> extractRestrictionMapFrom(ProtoNode restrictionNode) {
174+
final Collection<ProtoPropertyDecorator> restrictionProperties = restrictionNode.propertiesList.findResults {
175+
final property = new ProtoPropertyDecorator(it)
176+
return !property.isPrimaryType() ? property : null
177+
}
178+
return restrictionProperties.collectEntries { ProtoPropertyDecorator property ->
179+
[(property.getName()) : property.getPropertyValue()]
180+
}
181+
}
182+
183+
184+
private JackrabbitAccessControlList getAccessControlList(final Session session) {
185+
final AccessControlManager manager = getAccessControlManager(session)
186+
final String ownershipNodePath = getParentPath()
187+
//Check to see if existing policies exist for this node
188+
final AccessControlPolicy[] existingPolicies = manager.getPolicies(ownershipNodePath)
189+
//If no policies, we need to create a new policy
190+
if(existingPolicies.length == 0) {
191+
final AccessControlPolicyIterator policyIterator = manager.getApplicablePolicies(ownershipNodePath)
192+
//For Jackrabbit, the only policy type we are interested in is the JackrabbitAccessControlList
193+
while (policyIterator.hasNext()) {
194+
final AccessControlPolicy thisPolicy = policyIterator.nextAccessControlPolicy()
195+
if (thisPolicy instanceof JackrabbitAccessControlList) {
196+
return (JackrabbitAccessControlList)thisPolicy
197+
}
198+
}
199+
}
200+
//We have an existing policy. We just need to dig it out
201+
else {
202+
return existingPolicies.findResult { AccessControlPolicy policy ->
203+
if(policy instanceof JackrabbitAccessControlList) {
204+
return (JackrabbitAccessControlList)policy
205+
}
206+
} as JackrabbitAccessControlList
207+
}
208+
throw new IllegalStateException("Something went wrong when trying to find a ${JackrabbitAccessControlList.class.canonicalName} for this session on node ${ownershipNodePath}")
209+
}
210+
211+
212+
private Principal getPrincipal(final Session session, final String principalName) {
213+
return getPrincipalManager(session).getPrincipal(principalName)
214+
}
215+
216+
217+
private Privilege getPrivilege(final Session session, final String privilegeName) {
218+
try {
219+
return getAccessControlManager(session).privilegeFromName(privilegeName)
220+
} catch(AccessControlException ex) {
221+
log.error "Something went wrong while getting privilege ${privilegeName}. isSupportedPrivilege() may not be producing expected behavior "
222+
throw ex
223+
}
224+
}
225+
226+
227+
private boolean isSupportedPrivilege(final Session session, final String privilegeName) {
228+
final Privilege[] supportedPrivileges = getAccessControlManager(session).getSupportedPrivileges(getParentPath())
229+
return supportedPrivileges.any { Privilege privilege -> privilege.getName() == privilegeName }
230+
}
231+
232+
233+
PrincipalManager getPrincipalManager(final Session session) {
234+
return AccessControlUtil.getPrincipalManager(session)
235+
}
236+
237+
238+
AccessControlManager getAccessControlManager(final Session session) {
239+
return AccessControlUtil.getAccessControlManager(session)
240+
}
241+
242+
}

src/main/groovy/com/twcable/grabbit/jcr/AuthorizableProtoNodeDecorator.groovy

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ class AuthorizableProtoNodeDecorator extends ProtoNodeDecorator {
7171
final UserManager userManager = getUserManager(session)
7272
if(isUserType()) {
7373
//We set a temporary password for now, and then set the real password later in setPasswordForUser(). See the method for why.
74-
final newUser = userManager.createUser(authorizableID, Long.toString(CryptoUtil.generateNextId()), new AuthorizablePrincipal(authorizableID), getIntermediateAuthorizablePath())
74+
final newUser = userManager.createUser(authorizableID, Long.toString(CryptoUtil.generateNextId()), new AuthorizablePrincipal(authorizableID), getParentPath())
7575
//This is a special protected property for disabling user access
7676
if(hasProperty('rep:disabled')) {
7777
newUser.disable(getStringValueFrom('rep:disabled'))
@@ -81,14 +81,14 @@ class AuthorizableProtoNodeDecorator extends ProtoNodeDecorator {
8181
if(hasProperty(authorizableCategory)) {
8282
newUser.setProperty(authorizableCategory, new StringValue(getStringValueFrom(authorizableCategory)))
8383
}
84-
session.save()
8584
//Special users may not have passwords, such as anonymous users
8685
if(hasProperty('rep:password')) {
8786
setPasswordForUser(newUser, session)
8887
}
88+
session.save()
8989
return newUser
9090
}
91-
final Group group = userManager.createGroup(authorizableID, new AuthorizablePrincipal(authorizableID), getIntermediateAuthorizablePath())
91+
final Group group = userManager.createGroup(authorizableID, new AuthorizablePrincipal(authorizableID), getParentPath())
9292
session.save()
9393
return group
9494
}
@@ -130,14 +130,6 @@ class AuthorizableProtoNodeDecorator extends ProtoNodeDecorator {
130130
}
131131

132132

133-
private String getIntermediateAuthorizablePath() {
134-
final pathTokens = getName().tokenize('/')
135-
//remove last index, as this is the Authorizable node name
136-
pathTokens.remove(pathTokens.size() - 1)
137-
return "/${pathTokens.join('/')}"
138-
}
139-
140-
141133
private boolean isUserType() {
142134
return protoProperties.any { it.userType }
143135
}
@@ -252,7 +244,6 @@ class AuthorizableProtoNodeDecorator extends ProtoNodeDecorator {
252244
* clear-text, which it isn't since we got it from another Jackrabbit instance, we can set the password as-is.
253245
*/
254246
setPasswordMethod.invoke(userManagerDelegate, getTreeMethod.invoke(authorizable), getAuthorizableID(), getStringValueFrom('rep:password'), false)
255-
session.save()
256247
}
257248

258249

src/main/groovy/com/twcable/grabbit/jcr/DefaultProtoNodeDecorator.groovy

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE
3333
@Slf4j
3434
class DefaultProtoNodeDecorator extends ProtoNodeDecorator {
3535

36-
private final String nameOverride
3736

3837
protected DefaultProtoNodeDecorator(@Nonnull ProtoNode node, @Nonnull Collection<ProtoPropertyDecorator> protoProperties, String nameOverride) {
3938
this.innerProtoNode = node
@@ -72,13 +71,6 @@ class DefaultProtoNodeDecorator extends ProtoNodeDecorator {
7271
protoProperties.findAll { !(it.name in [JCR_PRIMARYTYPE, JCR_MIXINTYPES]) }
7372
}
7473

75-
76-
@Override
77-
String getName() {
78-
nameOverride ?: innerProtoNode.getName()
79-
}
80-
81-
8274
/**
8375
* This method is rather succinct, but helps isolate this JcrUtils static method call
8476
* so that we can get better test coverage.

src/main/groovy/com/twcable/grabbit/jcr/JCRNodeDecorator.groovy

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE
3838
import static org.apache.jackrabbit.commons.flat.TreeTraverser.ErrorHandler
3939
import static org.apache.jackrabbit.commons.flat.TreeTraverser.InclusionPolicy
4040
import static org.apache.jackrabbit.oak.spi.security.authorization.accesscontrol.AccessControlConstants.AC_NODETYPE_NAMES
41+
import static org.apache.jackrabbit.oak.spi.security.authorization.accesscontrol.AccessControlConstants.NT_REP_ACL
4142

4243
@CompileStatic
4344
@Slf4j
@@ -111,10 +112,19 @@ class JCRNodeDecorator {
111112
if(isAuthorizableType()){
112113
return getChildNodeList().findAll { JCRNodeDecorator childJcrNode -> !childJcrNode.isLoginToken() && !childJcrNode.isACType() }
113114
}
115+
else if(isRepACLType()) {
116+
//Send all ACE parts underneath the ACL as required nodes
117+
return getChildNodeList()
118+
}
114119
return getMandatoryChildren()
115120
}
116121

117122

123+
String getPrimaryType() {
124+
innerNode.getProperty(JCR_PRIMARYTYPE).string
125+
}
126+
127+
118128
/**
119129
* Some nodes must be saved together, per node definition
120130
*/
@@ -201,18 +211,29 @@ class JCRNodeDecorator {
201211
}
202212
}
203213

204-
205-
String getPrimaryType() {
206-
innerNode.getProperty(JCR_PRIMARYTYPE).string
214+
/**
215+
* @return is part of a rep:ACL tree, such as rep:GrantACE, or rep:restrictions. These nodes are transported with the rep:ACL parent, and
216+
* are not written independently.
217+
*/
218+
boolean isACPart() {
219+
final Collection<String> theACNodeTypeNames = new ArrayList(AC_NODETYPE_NAMES)
220+
theACNodeTypeNames.remove(NT_REP_ACL)
221+
return theACNodeTypeNames.contains(primaryType)
207222
}
208223

209224

210-
boolean isAuthorizableType() {
211-
return primaryType == 'rep:User' || primaryType == 'rep:Group'
225+
boolean isRepACLType() {
226+
return primaryType == NT_REP_ACL
212227
}
213228

229+
214230
boolean isACType() {
215-
AC_NODETYPE_NAMES.contains(primaryType)
231+
return AC_NODETYPE_NAMES.contains(primaryType)
232+
}
233+
234+
235+
boolean isAuthorizableType() {
236+
return primaryType == 'rep:User' || primaryType == 'rep:Group'
216237
}
217238

218239

src/main/groovy/com/twcable/grabbit/jcr/JcrPropertyDecorator.groovy

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,11 @@ class JcrPropertyDecorator {
6060
return true
6161
}
6262

63-
return nodeOwner.isAuthorizableType() ?: !definition.isProtected()
63+
if(nodeOwner.isAuthorizableType() || nodeOwner.isACType()) {
64+
return true
65+
}
66+
67+
return !definition.isProtected()
6468
}
6569

6670
/**

0 commit comments

Comments
 (0)