Skip to content

Commit 875b576

Browse files
author
Jeff Bornemann
committed
Support for writing authorizable nodes
1 parent c21b547 commit 875b576

24 files changed

Lines changed: 1203 additions & 255 deletions

gradle.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ scr_annotations_version = 1.7.0
4040
servlet_api_version = 2.5
4141
slf4j_version = 1.7.6
4242
sling_api_version = 2.9.0
43+
sling_base_version = 2.2.2
4344
sling_commons_testing_version = 2.0.12
4445
sling_commons_version = 2.2.0
4546
sling_event_version = 3.1.4

gradle/dependencies.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ dependencies {
1919

2020
// Apache Sling libraries
2121
compile "org.apache.sling:org.apache.sling.api:${sling_api_version}"
22+
compile "org.apache.sling:org.apache.sling.jcr.base:${sling_base_version}"
2223
compile "org.apache.sling:org.apache.sling.jcr.resource:${sling_jcr_resource_version}"
2324

2425
// Apache Felix libraries
@@ -33,6 +34,7 @@ dependencies {
3334
// Working with the JCR
3435
compile "javax.jcr:jcr:${jcr_version}"
3536
compile "org.apache.jackrabbit:jackrabbit-jcr-commons:${jackrabbit_version}"
37+
compile "org.apache.jackrabbit:jackrabbit-api:${jackrabbit_version}"
3638
compile "org.apache.sling:org.apache.sling.jcr.api:${sling_commons_version}"
3739

3840
// Logging

gradle/packageExclusions.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,12 @@ configurations.cq_package {
3030

3131
exclude group: 'org.apache.commons', module: 'commons-lang3'
3232
exclude group: 'org.apache.jackrabbit', module:'jackrabbit-jcr-commons'
33+
exclude group: 'org.apache.jackrabbit', module:'jackrabbit-api'
3334
exclude group: 'commons-io', module: 'commons-io'
3435

3536
//Exclude Apache Sling Libraries
3637
exclude group: 'org.apache.sling', module: 'org.apache.sling.api'
38+
exclude group: 'org.apache.sling', module: 'org.apache.sling.jcr.base'
3739
exclude group: 'org.apache.sling', module:'org.apache.sling.jcr.resource'
3840
exclude group: 'org.apache.sling', module: 'org.apache.sling.jcr.api'
3941

src/main/content/SLING-INF/content/apps/grabbit/config/org.apache.sling.commons.log.LogManager.factory.config-com.twcable.grabbit.client.batch.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
<property>
1616
<name>org.apache.sling.commons.log.names</name>
17-
<value>com.twcable.grabbit.client.batch</value>
17+
<value>com.twcable.grabbit.client</value>
1818
<type>String</type>
1919
</property>
2020

src/main/content/SLING-INF/content/apps/grabbit/config/org.apache.sling.commons.log.LogManager.factory.config-com.twcable.grabbit.server.batch.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
<property>
1616
<name>org.apache.sling.commons.log.names</name>
17-
<value>com.twcable.grabbit.server.batch</value>
17+
<value>com.twcable.grabbit.server</value>
1818
<type>String</type>
1919
</property>
2020

src/main/groovy/com/twcable/grabbit/client/batch/steps/jcrnodes/JcrNodesWriter.groovy

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ class JcrNodesWriter implements ItemWriter<ProtoNode>, ItemWriteListener {
8888
}
8989

9090
private static void writeToJcr(ProtoNode nodeProto, Session session) {
91-
JcrNodeDecorator jcrNode = new ProtoNodeDecorator(nodeProto).writeToJcr(session)
91+
JcrNodeDecorator jcrNode = ProtoNodeDecorator.createFrom(nodeProto).writeToJcr(session)
9292
jcrNode.setLastModified()
9393
// This will processed all mandatory child nodes only
9494
if(nodeProto.mandatoryChildNodeList && nodeProto.mandatoryChildNodeList.size() > 0) {
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
package com.twcable.grabbit.jcr
2+
3+
import com.twcable.grabbit.proto.NodeProtos.Node as ProtoNode
4+
import com.twcable.grabbit.security.AuthorizablePrincipal
5+
import com.twcable.grabbit.security.InsufficientGrabbitPrivilegeException
6+
import groovy.transform.CompileStatic
7+
import groovy.util.logging.Slf4j
8+
import org.apache.jackrabbit.api.security.user.Authorizable
9+
import org.apache.jackrabbit.api.security.user.User
10+
import org.apache.jackrabbit.api.security.user.UserManager
11+
import org.apache.jackrabbit.value.StringValue
12+
import org.apache.sling.jcr.base.util.AccessControlUtil
13+
14+
import javax.annotation.Nonnull
15+
import javax.jcr.Session
16+
import java.lang.reflect.Field
17+
import java.lang.reflect.Method
18+
import java.lang.reflect.ReflectPermission
19+
20+
/*
21+
* Copyright 2015 Time Warner Cable, Inc.
22+
*
23+
* Licensed under the Apache License, Version 2.0 (the "License");
24+
* you may not use this file except in compliance with the License.
25+
* You may obtain a copy of the License at
26+
*
27+
* http://www.apache.org/licenses/LICENSE-2.0
28+
*
29+
* Unless required by applicable law or agreed to in writing, software
30+
* distributed under the License is distributed on an "AS IS" BASIS,
31+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
32+
* See the License for the specific language governing permissions and
33+
* limitations under the License.
34+
*/
35+
36+
/**
37+
* This class wraps a serialized node that represents an Authorizable. Authorizables are special system protected nodes, that can only be written under certain
38+
* trees, and can not be written directly by a client.
39+
*/
40+
@CompileStatic
41+
@Slf4j
42+
class AuthorizableProtoNodeDecorator extends ProtoNodeDecorator {
43+
44+
45+
protected AuthorizableProtoNodeDecorator(@Nonnull ProtoNode node, @Nonnull Collection<ProtoPropertyDecorator> protoProperties) {
46+
this.innerProtoNode = node
47+
this.protoProperties = protoProperties
48+
}
49+
50+
51+
@Override
52+
JcrNodeDecorator writeToJcr(@Nonnull Session session) {
53+
if(!checkSecurityPermissions()) {
54+
throw new InsufficientGrabbitPrivilegeException("JVM Permissions needed by Grabbit to sync Users/Groups were not found. See log for specific permissions needed, and add these to your security manager; or do not sync users and groups." +
55+
"Unfortunately, the way Jackrabbit goes about certain things requires us to do a bit of hacking in order to sync Authorizables securely, and efficiently.")
56+
}
57+
Authorizable authorizable = findAuthorizable(session)
58+
if(!authorizable) {
59+
authorizable = createNewAuthorizable(session)
60+
}
61+
else {
62+
updateAuthorizable(authorizable, session)
63+
}
64+
session.save()
65+
return new JcrNodeDecorator(session.getNode(authorizable.getPath()))
66+
}
67+
68+
69+
/**
70+
* @return a new authorizable from this serialized node
71+
*/
72+
private Authorizable createNewAuthorizable(final Session session) {
73+
final UserManager userManager = getUserManager(session)
74+
if(isUserType()) {
75+
//We set a temporary password for now, and then set the real password later in setPasswordForUser(). See the method for why.
76+
final newUser = userManager.createUser(authorizableID, 'temp', new AuthorizablePrincipal(authorizableID), getIntermediateAuthorizablePath())
77+
//This is a special protected property for disabling user access
78+
if(hasProperty('rep:disabled')) {
79+
newUser.disable(getStringValueFrom('rep:disabled'))
80+
}
81+
//AEM writes this property directly on the user node for some reason. One known use is for setting leads on MCM campaigns.
82+
final authorizableCategory = 'cq:authorizableCategory'
83+
if(hasProperty(authorizableCategory)) {
84+
newUser.setProperty(authorizableCategory, new StringValue(getStringValueFrom(authorizableCategory)))
85+
}
86+
//Special users may not have passwords, such as anonymous users
87+
if(hasProperty('rep:password')) {
88+
setPasswordForUser(newUser, session)
89+
}
90+
return newUser
91+
}
92+
return userManager.createGroup(authorizableID, new AuthorizablePrincipal(authorizableID), getIntermediateAuthorizablePath())
93+
}
94+
95+
96+
/**
97+
* From a client API perspective, there is really no way to truely update an existing authorizable node. All of the properties are protected, and there is no
98+
* known way to update them. What we do here is essentially remove the existing authorizable as denoted by the authorizableID, and recreate it.
99+
*/
100+
private void updateAuthorizable(final Authorizable authorizable, final Session session) {
101+
authorizable.remove()
102+
session.save()
103+
createNewAuthorizable(session)
104+
}
105+
106+
107+
private Authorizable findAuthorizable(final Session session) {
108+
final UserManager userManager = getUserManager(session)
109+
return userManager.getAuthorizable(getAuthorizableID())
110+
}
111+
112+
113+
private String getAuthorizableID() {
114+
return protoProperties.find { it.isAuthorizableIDType() }.stringValue
115+
}
116+
117+
118+
private String getIntermediateAuthorizablePath() {
119+
final pathTokens = getName().tokenize('/')
120+
//remove last index, as this is the Authorizable node name
121+
pathTokens.remove(pathTokens.size() - 1)
122+
return "/${pathTokens.join('/')}"
123+
}
124+
125+
126+
private boolean isUserType() {
127+
return protoProperties.any { it.userType }
128+
}
129+
130+
131+
/**
132+
* Some JVM's have a SecurityManager set, which based on configuration, can potentially inhibit our hack {@code setPasswordForUser(User, Session)} from working.
133+
* We need to check security permissions before proceeding
134+
* @return true if we can sync this Authorizable
135+
*/
136+
private boolean checkSecurityPermissions() {
137+
final SecurityManager securityManager = getSecurityManager()
138+
//If no security manager is present, then we are in the clear; otherwise, we need to check certain permissions
139+
if(!securityManager){
140+
log.debug "No SecurityManager found on this JVM. Sync of Users/Groups can continue"
141+
return true
142+
}
143+
final issues = []
144+
final badPermissions = false
145+
log.debug "SecurityManager found on this JVM. Checking permissions.."
146+
try {
147+
//Needed to reflect on members for which this class does not normally have access to
148+
securityManager.checkPermission(new ReflectPermission('suppressAccessChecks'))
149+
}
150+
catch(SecurityException ex) {
151+
issues << 'suppressAccessChecks'
152+
badPermissions = true
153+
}
154+
try {
155+
//Needed to access all declared members of a class, including protected or private
156+
securityManager.checkPermission(new RuntimePermission('accessDeclaredMembers'))
157+
}
158+
catch(SecurityException ex) {
159+
issues << 'accessDeclaredMembers'
160+
badPermissions = true
161+
}
162+
try {
163+
//Needed to access classes directly within a potentially system protected package
164+
securityManager.checkPermission(new RuntimePermission('accessClassInPackage.{org.apache.jackrabbit.oak.security.user}'))
165+
}
166+
catch(SecurityException ex) {
167+
issues << 'accessClassInPackage.{org.apache.jackrabbit.oak.security.user}'
168+
badPermissions = true
169+
}
170+
if(badPermissions) {
171+
log.warn "A SecurityManager is enabled for this JVM, and permissions are not sufficient for Grabbit to sync Authorizables (Users/Groups). You must enable ${issues.join(', ')} permissions in your SecurityManager to use this functionality" +
172+
"Check https://docs.oracle.com/javase/7/docs/api/java/lang/RuntimePermission.html and https://docs.oracle.com/javase/7/docs/api/java/lang/reflect/ReflectPermission.html to see what these permissions enable"
173+
return false
174+
}
175+
else {
176+
log.debug "Permissions check successful"
177+
return true
178+
}
179+
}
180+
181+
/**
182+
* @return the system's security manager, or null if one is not present
183+
*/
184+
SecurityManager getSecurityManager() {
185+
return System.getSecurityManager()
186+
}
187+
188+
189+
UserManager getUserManager(final Session session) {
190+
return AccessControlUtil.getUserManager(session)
191+
}
192+
193+
194+
/**
195+
* Normally we would call org.apache.jackrabbit.oak.jcr.delegate.UserDelegator.changePassword(String password) to change a password (this is what is publicly available through the Jackrabbit API)
196+
* However, this method ALWAYS rehashes the password argument which is of no use to us, since we are trying to transfer an already hashed password.
197+
*
198+
* Internally, org.apache.jackrabbit.oak.jcr.delegate.UserDelegator calls it's delegate's org.apache.jackrabbit.oak.security.user.UserImpl.changePassword(String password)
199+
* which calls org.apache.jackrabbit.oak.security.user.UserManagerImpl.setPassword(Tree tree, String userId, String password, boolean forceHash) with forceHash always set to true
200+
* We really need forcehash set to false for our case, but this isn't publicly available. Here, we access internal objects to do this manipulation. org.apache.jackrabbit.oak.security.user.UserManagerImpl
201+
* simply ensures that forcehash is false, and that the password is not plain text, and it sets the password as-is.
202+
*
203+
* @throws IllegalStateException if security permissions required to run this are not there. @{code checkSecurityPermissions()} should be called before calling this method
204+
**/
205+
void setPasswordForUser(final User user, final Session session) {
206+
if(!checkSecurityPermissions()) throw new IllegalStateException("Security check failed for Grabbit. Can not set user passwords")
207+
//As a consumer we have access to org.apache.jackrabbit.oak.jcr.delegate.UserManagerDelegator below
208+
final userManager = getUserManager(session)
209+
Class userManagerDelegatorClass = userManager.getClass()
210+
//Reach into the class of this delegator, and grab the core Jackrabbit object we delegate to
211+
Field userManagerDelegateField = userManagerDelegatorClass.getDeclaredField('userManagerDelegate')
212+
//The delegate field is private, so we need to make it accessible. Security checks above are imperative for this to work
213+
userManagerDelegateField.setAccessible(true)
214+
//Here we have a handle to the internal class org.apache.jackrabbit.oak.security.user.UserManagerImpl
215+
final userManagerDelegate = userManagerDelegateField.get(userManager)
216+
final userManagerDelegateClass = userManagerDelegate.getClass()
217+
//We need to set the 'setPassword' method as accessible. Again, security checks above are imperative for this to work
218+
Method setPasswordMethod = userManagerDelegateClass.getDeclaredMethod('setPassword', Class.forName('org.apache.jackrabbit.oak.api.Tree', true, userManagerDelegateClass.getClassLoader()), String, String, boolean)
219+
setPasswordMethod.setAccessible(true)
220+
/**
221+
* Step two. We need access to the internal Authorizable object's tree in order to call the internal setPassword method
222+
* User is an instance of org.apache.jackrabbit.oak.jcr.delegate.UserDelegator. We need to get the delegate off of this class's super class org.apache.jackrabbit.oak.jcr.delegate.AuthorizableDelegator
223+
*/
224+
Class authorizableDelegateClass = user.getClass().getSuperclass()
225+
Field authorizableDelegateField = authorizableDelegateClass.getDeclaredField('delegate')
226+
authorizableDelegateField.setAccessible(true)
227+
final authorizable = authorizableDelegateField.get(user)
228+
//Internal org.apache.jackrabbit.oak.security.user.AuthorizableImpl object. We can access the protected tree here
229+
Method getTreeMethod = authorizable.getClass().getSuperclass().getDeclaredMethod('getTree')
230+
getTreeMethod.setAccessible(true)
231+
232+
/**
233+
* The last argument where we are passing in 'false' in the secret sauce we need. This parameter is forceHash. As long as forceHash is false, and the password is not
234+
* clear-text, which it isn't since we got it from another Jackrabbit instance, we can set the password as-is.
235+
*/
236+
setPasswordMethod.invoke(userManagerDelegate, getTreeMethod.invoke(authorizable), getAuthorizableID(), getStringValueFrom('rep:password'), false)
237+
}
238+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright 2015 Time Warner Cable, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.twcable.grabbit.jcr
17+
18+
import com.twcable.grabbit.proto.NodeProtos.Node as ProtoNode
19+
import com.twcable.grabbit.proto.NodeProtos.Value as ProtoValue
20+
import groovy.transform.CompileStatic
21+
import groovy.util.logging.Slf4j
22+
import org.apache.jackrabbit.commons.JcrUtils
23+
24+
import javax.annotation.Nonnull
25+
import javax.jcr.Node as JCRNode
26+
import javax.jcr.Session
27+
28+
import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES
29+
import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE
30+
31+
@CompileStatic
32+
@Slf4j
33+
class DefaultProtoNodeDecorator extends ProtoNodeDecorator {
34+
35+
36+
protected DefaultProtoNodeDecorator(@Nonnull ProtoNode node, @Nonnull Collection<ProtoPropertyDecorator> protoProperties) {
37+
this.innerProtoNode = node
38+
this.protoProperties = protoProperties
39+
}
40+
41+
42+
@Override
43+
JcrNodeDecorator writeToJcr(@Nonnull Session session) {
44+
final jcrNode = getOrCreateNode(session)
45+
//Write mixin types first to avoid InvalidConstraintExceptions
46+
final mixinProperty = getMixinProperty()
47+
if(mixinProperty) {
48+
addMixins(mixinProperty, jcrNode)
49+
}
50+
//Then add other properties
51+
writableProperties.each { it.writeToNode(jcrNode) }
52+
53+
return new JcrNodeDecorator(jcrNode)
54+
}
55+
56+
57+
private ProtoPropertyDecorator getMixinProperty() {
58+
protoProperties.find { it.isMixinType() }
59+
}
60+
61+
62+
private Collection<ProtoPropertyDecorator> getWritableProperties() {
63+
protoProperties.findAll { !(it.name in [JCR_PRIMARYTYPE, JCR_MIXINTYPES]) }
64+
}
65+
66+
67+
/**
68+
* This method is rather succinct, but helps isolate this JcrUtils static method call
69+
* so that we can get better test coverage.
70+
* @param session to create or get the node path for
71+
* @return the newly created, or found node
72+
*/
73+
JCRNode getOrCreateNode(Session session) {
74+
JcrUtils.getOrCreateByPath(innerProtoNode.name, primaryType.getStringValue(), session)
75+
}
76+
77+
78+
/**
79+
* If a property can be added as a mixin, adds it to the given node
80+
* @param property
81+
* @param node
82+
*/
83+
private static void addMixins(ProtoPropertyDecorator property, JCRNode node) {
84+
property.valuesList.each { ProtoValue value ->
85+
if (node.canAddMixin(value.stringValue)) {
86+
node.addMixin(value.stringValue)
87+
log.debug "Added mixin ${value.stringValue} for : ${node.name}."
88+
}
89+
else {
90+
log.warn "Encountered invalid mixin type while unmarshalling for Proto value : ${value}"
91+
}
92+
}
93+
}
94+
95+
}

0 commit comments

Comments
 (0)