Skip to content

Commit ccb8beb

Browse files
author
Jeff Bornemann
committed
Grabbit client work for writing Authorizable nodes
1 parent 3826544 commit ccb8beb

12 files changed

Lines changed: 349 additions & 16 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: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
package com.twcable.grabbit.client.jcr
2+
3+
import com.twcable.grabbit.jcr.JcrNodeDecorator
4+
import com.twcable.grabbit.jcr.ProtoNodeDecorator
5+
import com.twcable.grabbit.security.AuthorizablePrincipal
6+
import com.twcable.grabbit.security.InsufficientGrabbitPrivilegeException
7+
import groovy.transform.CompileStatic
8+
import groovy.transform.InheritConstructors
9+
import groovy.util.logging.Slf4j
10+
import org.apache.jackrabbit.api.security.user.Authorizable
11+
import org.apache.jackrabbit.api.security.user.User
12+
import org.apache.jackrabbit.api.security.user.UserManager
13+
import org.apache.jackrabbit.value.StringValue
14+
import org.apache.sling.jcr.base.util.AccessControlUtil
15+
16+
import javax.annotation.Nonnull
17+
import javax.jcr.Session
18+
import java.lang.reflect.Field
19+
import java.lang.reflect.Method
20+
import java.lang.reflect.ReflectPermission
21+
22+
/*
23+
* Copyright 2015 Time Warner Cable, Inc.
24+
*
25+
* Licensed under the Apache License, Version 2.0 (the "License");
26+
* you may not use this file except in compliance with the License.
27+
* You may obtain a copy of the License at
28+
*
29+
* http://www.apache.org/licenses/LICENSE-2.0
30+
*
31+
* Unless required by applicable law or agreed to in writing, software
32+
* distributed under the License is distributed on an "AS IS" BASIS,
33+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
34+
* See the License for the specific language governing permissions and
35+
* limitations under the License.
36+
*/
37+
38+
/**
39+
* This class wraps a serialized node that represents an Authorizable. Authorizables are special system protected nodes, that can only be written under certain
40+
* trees, and can not be written directly by a client.
41+
*/
42+
@CompileStatic
43+
@InheritConstructors
44+
@Slf4j
45+
class AuthorizableProtoNodeDecorator extends ProtoNodeDecorator {
46+
47+
@Override
48+
JcrNodeDecorator writeToJcr(@Nonnull Session session) {
49+
if(!checkSecurityPermissions()) {
50+
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." +
51+
"Unfortunately, the way Jackrabbit goes about certain things requires us to do a bit of hacking in order to sync Authorizables securely, and efficiently.")
52+
}
53+
Authorizable authorizable = findAuthorizable(session)
54+
if(!authorizable) {
55+
authorizable = createNewAuthorizable(session)
56+
}
57+
else {
58+
updateAuthorizable(authorizable, session)
59+
}
60+
session.save()
61+
return new JcrNodeDecorator(session.getNode(authorizable.getPath()))
62+
}
63+
64+
/**
65+
* @return a new authorizable from this serialized node
66+
*/
67+
private Authorizable createNewAuthorizable(final Session session) {
68+
final UserManager userManager = AccessControlUtil.getUserManager(session)
69+
if(isUserType()) {
70+
//When set a temporary password for now, and then set the real password later in setPasswordForUser(). See the method for why.
71+
final newUser = userManager.createUser(authorizableID, 'temp', new AuthorizablePrincipal(authorizableID), getIntermediateAuthorizablePath())
72+
//This is a special protected property for disabling user access
73+
if(hasProperty('rep:disabled')) {
74+
newUser.disable(getStringValueFrom('rep:disabled'))
75+
}
76+
//AEM writes this property directly on the user node for some reason. One known use is for setting leads on MCM campaigns.
77+
final authorizableCategory = 'cq:authorizableCategory'
78+
if(hasProperty(authorizableCategory)) {
79+
newUser.setProperty(authorizableCategory, new StringValue(getStringValueFrom(authorizableCategory)))
80+
}
81+
setPasswordForUser(newUser, session)
82+
return newUser
83+
}
84+
return userManager.createGroup(authorizableID, new AuthorizablePrincipal(authorizableID), getIntermediateAuthorizablePath())
85+
}
86+
87+
/**
88+
* 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
89+
* known way to update them. What we do here is essentially remove the existing authorizable as denoted by the authorizableID, and recreate it.
90+
*/
91+
private void updateAuthorizable(final Authorizable authorizable, final Session session) {
92+
authorizable.remove()
93+
session.save()
94+
createNewAuthorizable(session)
95+
}
96+
97+
98+
private Authorizable findAuthorizable(final Session session) {
99+
final UserManager userManager = AccessControlUtil.getUserManager(session)
100+
return userManager.getAuthorizable(getAuthorizableID())
101+
}
102+
103+
104+
private String getAuthorizableID() {
105+
return protoProperties.find { it.isAuthorizableIDType() }.stringValue
106+
}
107+
108+
109+
private String getIntermediateAuthorizablePath() {
110+
return getName() - "/${getName().tokenize('/').last()}"
111+
}
112+
113+
114+
private boolean isUserType() {
115+
return protoProperties.any { it.userType }
116+
}
117+
118+
119+
/**
120+
* Some JVM's have a SecurityManager set, which based on configuration, can potentially inhibit our hack {@code setPasswordForUser(User, Session)} from working.
121+
* We need to check security permissions before proceeding
122+
* @return true if we can sync this Authorizable
123+
*/
124+
private boolean checkSecurityPermissions() {
125+
final SecurityManager securityManager = System.getSecurityManager()
126+
//If no security manager is present, then we are in the clear; otherwise, we need to check certain permissions
127+
if(!securityManager){
128+
log.debug "No SecurityManager found on this JVM. Sync of Users/Groups can continue"
129+
return true
130+
}
131+
final issues = []
132+
final badPermissions = false
133+
log.debug "SecurityManager found on this JVM. Checking permissions.."
134+
try {
135+
//Needed to reflect on members for which this class does not normally have access to
136+
securityManager.checkPermission(new ReflectPermission('suppressAccessChecks'))
137+
}
138+
catch(SecurityException ex) {
139+
issues << 'suppressAccessChecks'
140+
badPermissions = true
141+
}
142+
try {
143+
//Needed to access all declared members of a class, including protected or private
144+
securityManager.checkPermission(new RuntimePermission('accessDeclaredMembers'))
145+
}
146+
catch(SecurityException ex) {
147+
issues << 'accessDeclaredMembers'
148+
badPermissions = true
149+
}
150+
try {
151+
//Needed to access classes directly within a potentially system protected package
152+
securityManager.checkPermission(new RuntimePermission('accessClassInPackage.{org.apache.jackrabbit.oak.security.user}'))
153+
}
154+
catch(SecurityException ex) {
155+
issues << 'accessClassInPackage.{org.apache.jackrabbit.oak.security.user}'
156+
badPermissions = true
157+
}
158+
if(badPermissions) {
159+
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" +
160+
"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"
161+
return false
162+
}
163+
else {
164+
log.debug "Permissions check successful"
165+
return true
166+
}
167+
}
168+
169+
170+
/**
171+
* 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)
172+
* 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.
173+
*
174+
* Internally, org.apache.jackrabbit.oak.jcr.delegate.UserDelegator calls it's delegate's org.apache.jackrabbit.oak.security.user.UserImpl.changePassword(String password)
175+
* which calls org.apache.jackrabbit.oak.security.user.UserManagerImpl.setPassword(Tree tree, String userId, String password, boolean forceHash) with forceHash always set to true
176+
* 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
177+
* simply ensures that forcehash is false, and that the password is not plain text, and it sets the password as-is.
178+
*
179+
* @throws IllegalStateException if security permissions required to run this are not there. @{code checkSecurityPermissions()} should be called before calling this method
180+
**/
181+
private void setPasswordForUser(final User user, final Session session) {
182+
if(!checkSecurityPermissions()) throw new IllegalStateException("Security check failed for Grabbit. Can not set user passwords")
183+
//As a consumer we have access to org.apache.jackrabbit.oak.jcr.delegate.UserManagerDelegator below
184+
final userManager = AccessControlUtil.getUserManager(session)
185+
Class userManagerDelegatorClass = userManager.getClass()
186+
//Reach into the class of this delegator, and grab the core Jackrabbit object we delegate to
187+
Field userManagerDelegateField = userManagerDelegatorClass.getDeclaredField('userManagerDelegate')
188+
//The delegate field is private, so we need to make it accessible. Security checks above are imperative for this to work
189+
userManagerDelegateField.setAccessible(true)
190+
//Here we have a handle to the internal class org.apache.jackrabbit.oak.security.user.UserManagerImpl
191+
final userManagerDelegate = userManagerDelegateField.get(userManager)
192+
final userManagerDelegateClass = userManagerDelegate.getClass()
193+
//We need to set the 'setPassword' method as accessible. Again, security checks above are imperative for this to work
194+
Method setPasswordMethod = userManagerDelegateClass.getDeclaredMethod('setPassword', Class.forName('org.apache.jackrabbit.oak.api.Tree', true, userManagerDelegateClass.getClassLoader()), String, String, boolean)
195+
setPasswordMethod.setAccessible(true)
196+
/**
197+
* Step two. We need access to the internal Authorizable object's tree in order to call the internal setPassword method
198+
* 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
199+
*/
200+
Class authorizableDelegateClass = user.getClass().getSuperclass()
201+
Field authorizableDelegateField = authorizableDelegateClass.getDeclaredField('delegate')
202+
authorizableDelegateField.setAccessible(true)
203+
final authorizable = authorizableDelegateField.get(user)
204+
//Internal org.apache.jackrabbit.oak.security.user.AuthorizableImpl object. We can access the protected tree here
205+
Method getTreeMethod = authorizable.getClass().getSuperclass().getDeclaredMethod('getTree')
206+
getTreeMethod.setAccessible(true)
207+
208+
/**
209+
* 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
210+
* clear-text, which it isn't since we got it from another Jackrabbit instance, we can set the password as-is.
211+
*/
212+
setPasswordMethod.invoke(userManagerDelegate, getTreeMethod.invoke(authorizable), getAuthorizableID(), getStringValueFrom('rep:password'), false)
213+
}
214+
}

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

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package com.twcable.grabbit.jcr
1717

18+
import com.twcable.grabbit.client.jcr.AuthorizableProtoNodeDecorator
1819
import com.twcable.grabbit.proto.NodeProtos.Node as ProtoNode
1920
import com.twcable.grabbit.proto.NodeProtos.Value as ProtoValue
2021
import groovy.transform.CompileStatic
@@ -38,24 +39,43 @@ class ProtoNodeDecorator {
3839
Collection<ProtoPropertyDecorator> protoProperties
3940

4041

41-
ProtoNodeDecorator(@Nonnull ProtoNode node) {
42+
static ProtoNodeDecorator createFrom(@Nonnull ProtoNode node) {
4243
if(!node) throw new IllegalArgumentException("node must not be null!")
44+
final protoProperties = node.propertiesList.collect { new ProtoPropertyDecorator(it) }
45+
if(protoProperties.any { it.userType || it.groupType }) {
46+
return new AuthorizableProtoNodeDecorator(node, protoProperties)
47+
}
48+
return new ProtoNodeDecorator(node, protoProperties)
49+
}
50+
51+
52+
protected ProtoNodeDecorator(@Nonnull ProtoNode node, @Nonnull Collection<ProtoPropertyDecorator> protoProperties) {
4353
this.innerProtoNode = node
44-
this.protoProperties = node.propertiesList.collect { new ProtoPropertyDecorator(it) }
54+
this.protoProperties = protoProperties
55+
}
56+
57+
58+
boolean hasProperty(String propertyName) {
59+
propertiesList.any{ it.name == propertyName }
60+
}
61+
62+
63+
protected String getStringValueFrom(String propertyName) {
64+
protoProperties.find { it.name == propertyName }.stringValue
4565
}
4666

4767

48-
String getPrimaryType() {
49-
protoProperties.find { it.isPrimaryType() }.value.stringValue
68+
private String getPrimaryType() {
69+
protoProperties.find { it.isPrimaryType() }.stringValue
5070
}
5171

5272

53-
ProtoPropertyDecorator getMixinProperty() {
73+
private ProtoPropertyDecorator getMixinProperty() {
5474
protoProperties.find { it.isMixinType() }
5575
}
5676

5777

58-
Collection<ProtoPropertyDecorator> getWritableProperties() {
78+
private Collection<ProtoPropertyDecorator> getWritableProperties() {
5979
protoProperties.findAll { !(it.name in [JCR_PRIMARYTYPE, JCR_MIXINTYPES]) }
6080
}
6181

@@ -80,7 +100,7 @@ class ProtoNodeDecorator {
80100
* @param session to create or get the node path for
81101
* @return the newly created, or found node
82102
*/
83-
JCRNode getOrCreateNode(Session session) {
103+
private JCRNode getOrCreateNode(Session session) {
84104
JcrUtils.getOrCreateByPath(innerProtoNode.name, primaryType, session)
85105
}
86106

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

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class ProtoPropertyDecorator {
3838
@Delegate
3939
ProtoProperty innerProtoProperty
4040

41+
4142
ProtoPropertyDecorator(@Nonnull ProtoProperty protoProperty) {
4243
this.innerProtoProperty = protoProperty
4344
}
@@ -82,7 +83,28 @@ class ProtoPropertyDecorator {
8283
innerProtoProperty.name == JCR_MIXINTYPES
8384
}
8485

85-
ProtoValue getValue() {
86+
87+
boolean isUserType() {
88+
innerProtoProperty.name == 'rep:User'
89+
}
90+
91+
92+
boolean isGroupType() {
93+
innerProtoProperty.name == 'rep:Group'
94+
}
95+
96+
97+
boolean isAuthorizableIDType() {
98+
innerProtoProperty.name == 'rep:authorizableId'
99+
}
100+
101+
102+
String getStringValue() {
103+
getValue().stringValue
104+
}
105+
106+
107+
private ProtoValue getValue() {
86108
innerProtoProperty.valuesList.first()
87109
}
88110

0 commit comments

Comments
 (0)