Skip to content

Commit fdd8ded

Browse files
Jeff Bornemannjbornemann
Jeff Bornemann
authored andcommitted
issue-157 follow references support
Automatically avoid modifying admin user Extract parent existence logic into JCRUtil Clarification in log message within ACL decorator
1 parent 4bdfc87 commit fdd8ded

27 files changed

+753
-246
lines changed

docs/Running.adoc

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -180,20 +180,3 @@ Invalid:
180180
]
181181
}
182182
```
183-
184-
=== Syncing Users and Groups
185-
186-
Grabbit has support for syncing users and groups. One *important note* about syncing users is that you must take care to avoid syncing the _admin user_.
187-
Jackrabbit will not allow modification of the admin user, so Grabbit will fail on a job that attempts to do so. You can find the path of your admin user
188-
on your data-warehouse instance or other source instance; and add it as an exclude path as so:
189-
190-
```
191-
pathConfigurations :
192-
-
193-
path : /home/groups
194-
-
195-
path : /home/users
196-
excludePaths:
197-
- k/ki9zhpzfe #Admin user
198-
```
199-

src/main/groovy/com/twcable/grabbit/client/batch/ClientBatchJobExecutionListener.groovy

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
package com.twcable.grabbit.client.batch
1818

19-
import com.twcable.grabbit.jcr.JcrUtil
19+
import com.twcable.grabbit.jcr.JCRUtil
2020
import groovy.transform.CompileStatic
2121
import groovy.util.logging.Slf4j
2222
import org.apache.sling.jcr.api.SlingRepository
@@ -58,7 +58,7 @@ class ClientBatchJobExecutionListener implements JobExecutionListener {
5858
void beforeJob(JobExecution jobExecution) {
5959
log.debug "SlingRepository : ${slingRepository}"
6060
final clientUsername = jobExecution.jobParameters.getString(ClientBatchJob.CLIENT_USERNAME)
61-
final Session session = JcrUtil.getSession(slingRepository, clientUsername)
61+
final Session session = JCRUtil.getSession(slingRepository, clientUsername)
6262

6363
ClientBatchJobContext.setSession(session)
6464
log.info "Starting job : ${jobExecution}\n\n"

src/main/groovy/com/twcable/grabbit/client/batch/steps/validation/ValidJobDecider.groovy

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,15 @@ package com.twcable.grabbit.client.batch.steps.validation
1717

1818
import com.twcable.grabbit.client.batch.ClientBatchJob
1919
import com.twcable.grabbit.client.batch.ClientBatchJobContext
20+
import com.twcable.grabbit.jcr.JCRUtil
2021
import groovy.transform.CompileStatic
2122
import groovy.util.logging.Slf4j
23+
import javax.jcr.Session
2224
import org.springframework.batch.core.JobExecution
2325
import org.springframework.batch.core.StepExecution
2426
import org.springframework.batch.core.job.flow.FlowExecutionStatus
2527
import org.springframework.batch.core.job.flow.JobExecutionDecider
2628

27-
import javax.jcr.PathNotFoundException
28-
import javax.jcr.RepositoryException
29-
import javax.jcr.Session
30-
3129
/**
3230
* This class serves as a validation gate for jobs in-flight. It should be the first step on the client when running a job to determine
3331
* if the job is job that is safe, and valid to execute.
@@ -57,29 +55,14 @@ class ValidJobDecider implements JobExecutionDecider {
5755
@Override
5856
FlowExecutionStatus decide(JobExecution jobExecution, StepExecution stepExecution) {
5957
String jobPath = jobExecution.jobParameters.getString(ClientBatchJob.PATH)
60-
//For processing, remove trailing /
61-
jobPath = jobPath.replaceFirst(/\/$/, '')
62-
//Get the parent's path (if applicable) and determine if it exists already
63-
final parts = jobPath.split('/')
64-
//No parent, so nothing to worry about
65-
if(parts.length <= 2) return JOB_VALID
66-
67-
final parentPath = parts[0..-2].join('/')
68-
final Session session = theSession()
69-
try {
70-
session.getNode(parentPath)
71-
} catch(PathNotFoundException pathException) {
72-
log.warn "${jobPath} is not a valid job path. Make sure a parent is synched or created before this job is run"
73-
log.debug pathException.toString()
74-
return JOB_INVALID
58+
if(JCRUtil.writingToExistingNode(jobPath, theSession())) {
59+
log.debug "${ValidJobDecider.class.canonicalName} Job determined to be valid for job path ${jobPath}"
60+
return JOB_VALID
7561
}
76-
catch(RepositoryException repoException) {
77-
log.error "${RepositoryException.class.canonicalName} Something went wrong when accessing the repository at ${this.class.canonicalName} for job path ${jobPath}!"
78-
log.error repoException.toString()
62+
else {
63+
log.warn "${jobPath} is not a valid job path. Make sure a parent is synched or created before this job is run"
7964
return JOB_INVALID
8065
}
81-
log.debug "${ValidJobDecider.class.canonicalName} Job determined to be valid for job path ${jobPath}"
82-
return JOB_VALID
8366
}
8467

8568
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ class ACLProtoNodeDecorator extends ProtoNodeDecorator {
105105
final String principalName = getPrincipalName(aceNode)
106106
Principal principal = getPrincipal(session, principalName)
107107
if(principal == null) {
108-
log.warn "Principal for name ${principalName} does not exist, or is not accessible. Can not write ACE/ACL information."
108+
log.warn "Principal for name ${principalName} does not exist, or is not accessible. Can not write ACE/ACL information for this principal on ${getParentPath()}. If this principal is currently being synched, it may not be accessible."
109109
return
110110
}
111111
Privilege[] privileges = getPrivilegeNames(aceNode).collect { String privilegeName ->
@@ -166,7 +166,7 @@ class ACLProtoNodeDecorator extends ProtoNodeDecorator {
166166
final ProtoPropertyDecorator privilegeProperty = node.propertiesList.collect { new ProtoPropertyDecorator(it) }.find { ProtoPropertyDecorator property ->
167167
property.isPrivilege()
168168
}
169-
return privilegeProperty.getPropertyValues().collect { Value value -> value.string }.toArray() as String[]
169+
return privilegeProperty.getStringValues().toArray() as String[]
170170
}
171171

172172

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

Lines changed: 34 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import groovy.util.logging.Slf4j
2525
import java.lang.reflect.Field
2626
import java.lang.reflect.Method
2727
import java.lang.reflect.ReflectPermission
28-
import java.util.regex.Pattern
2928
import javax.annotation.Nonnull
3029
import javax.jcr.Session
3130
import org.apache.jackrabbit.api.security.user.Authorizable
@@ -35,7 +34,6 @@ import org.apache.jackrabbit.api.security.user.UserManager
3534
import org.apache.jackrabbit.value.StringValue
3635
import org.apache.sling.jcr.base.util.AccessControlUtil
3736

38-
3937
/**
4038
* This class wraps a serialized node that represents an Authorizable. Authorizables are special system protected nodes, that can only be written under certain
4139
* trees, and can not be written directly by a client.
@@ -57,10 +55,14 @@ class AuthorizableProtoNodeDecorator extends ProtoNodeDecorator {
5755
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." +
5856
"Unfortunately, the way Jackrabbit goes about certain things requires us to do a bit of hacking in order to sync Authorizables securely, and efficiently.")
5957
}
60-
Authorizable existingAuthorizable = findAuthorizable(session)
58+
//the administrator is a special user that Jackrabbit will not let us mess with.
59+
if(getAuthorizableID() == 'admin') {
60+
return new JCRNodeDecorator(session.getNode(findAuthorizable(session, 'admin').getPath()))
61+
}
62+
63+
Authorizable existingAuthorizable = findAuthorizable(session, getAuthorizableID())
6164
Authorizable newAuthorizable = existingAuthorizable ? updateAuthorizable(existingAuthorizable, session) : createNewAuthorizable(session)
62-
writeAuthorizablePieces(newAuthorizable, session)
63-
return new JCRNodeDecorator(session.getNode(newAuthorizable.getPath()))
65+
return new JCRNodeDecorator(session.getNode(newAuthorizable.getPath()), getID())
6466
}
6567

6668

@@ -86,11 +88,20 @@ class AuthorizableProtoNodeDecorator extends ProtoNodeDecorator {
8688
setPasswordForUser(newUser, session)
8789
}
8890
session.save()
91+
writeMandatoryPieces(session, newUser.getPath())
8992
return newUser
9093
}
91-
final Group group = userManager.createGroup(authorizableID, new AuthorizablePrincipal(authorizableID), getParentPath())
94+
final Group newGroup = userManager.createGroup(authorizableID, new AuthorizablePrincipal(authorizableID), getParentPath())
95+
/*
96+
* Write all mandatory pieces, and find those that are authorizables. We then need to see if any of them have membership in this group, and add them.
97+
*/
98+
final Collection<JCRNodeDecorator> authorizablePieces = writeMandatoryPieces(session, newGroup.getPath()).findAll { it.isAuthorizableType() }
99+
final Collection<JCRNodeDecorator> members = authorizablePieces.findAll { getMembershipIDs().contains(it.getTransferredID()) }
100+
members.each { JCRNodeDecorator member ->
101+
newGroup.addMember(member as Authorizable)
102+
}
92103
session.save()
93-
return group
104+
return newGroup
94105
}
95106

96107

@@ -100,28 +111,25 @@ class AuthorizableProtoNodeDecorator extends ProtoNodeDecorator {
100111
* @return new instance of updated authorizable
101112
*/
102113
private Authorizable updateAuthorizable(final Authorizable authorizable, final Session session) {
114+
//We get all the declared groups of this authorizable so that we can add them back to the new, updated authorizable
115+
final Collection<Group> declaredGroups = authorizable.declaredMemberOf().toList()
116+
for(Group group : declaredGroups) {
117+
group.removeMember(authorizable)
118+
}
103119
authorizable.remove()
104120
session.save()
105-
createNewAuthorizable(session)
106-
}
107-
108-
109-
/**
110-
* Authorizable pieces (nodes that live under Authorizables - profile, preferences, etc) get sent with the authorizable node instead of streamed independently because we do not know the client's new
111-
* authorizable UUID node name at runtime. In other words, authorizables can live under different node names from server to server
112-
*/
113-
private void writeAuthorizablePieces(final Authorizable authorizable, final Session session) {
114-
innerProtoNode.mandatoryChildNodeList.each {
115-
//We replace the incoming server authorizable path, with the new authorizable path
116-
createFrom(it, it.name.replaceFirst(Pattern.quote(getName()), authorizable.getPath())).writeToJcr(session)
121+
final Authorizable newAuthorizable = createNewAuthorizable(session)
122+
for(Group group: declaredGroups) {
123+
group.addMember(newAuthorizable)
117124
}
118125
session.save()
126+
return newAuthorizable
119127
}
120128

121129

122-
private Authorizable findAuthorizable(final Session session) {
130+
private Authorizable findAuthorizable(final Session session, final String authorizableID) {
123131
final UserManager userManager = getUserManager(session)
124-
return userManager.getAuthorizable(getAuthorizableID())
132+
return userManager.getAuthorizable(authorizableID)
125133
}
126134

127135

@@ -135,6 +143,11 @@ class AuthorizableProtoNodeDecorator extends ProtoNodeDecorator {
135143
}
136144

137145

146+
private Collection<String> getMembershipIDs() {
147+
return hasProperty('rep:members') ? getStringValuesFrom('rep:members') : []
148+
}
149+
150+
138151
/**
139152
* Some JVM's have a SecurityManager set, which based on configuration, can potentially inhibit our hack {@code setPasswordForUser(User, Session)} from working.
140153
* We need to check security permissions before proceeding
@@ -245,13 +258,4 @@ class AuthorizableProtoNodeDecorator extends ProtoNodeDecorator {
245258
*/
246259
setPasswordMethod.invoke(userManagerDelegate, getTreeMethod.invoke(authorizable), getAuthorizableID(), getStringValueFrom('rep:password'), false)
247260
}
248-
249-
250-
/**
251-
* An instance wrapper for ease of mocking
252-
* @see super.createFrom
253-
*/
254-
ProtoNodeDecorator createFrom(final ProtoNode protoNode, final String nameOverride) {
255-
super.createFrom(protoNode, nameOverride)
256-
}
257261
}

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

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,12 @@ import com.twcable.grabbit.proto.NodeProtos.Node as ProtoNode
1919
import com.twcable.grabbit.proto.NodeProtos.Value as ProtoValue
2020
import groovy.transform.CompileStatic
2121
import groovy.util.logging.Slf4j
22-
import java.util.regex.Pattern
2322
import javax.annotation.Nonnull
2423
import javax.jcr.Node as JCRNode
2524
import javax.jcr.Session
25+
import javax.jcr.Value
2626
import org.apache.jackrabbit.commons.JcrUtils
27+
import org.apache.jackrabbit.value.StringValue
2728

2829

2930
import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES
@@ -49,16 +50,34 @@ class DefaultProtoNodeDecorator extends ProtoNodeDecorator {
4950
if(mixinProperty) {
5051
addMixins(mixinProperty, jcrNode)
5152
}
52-
//Then add other properties
53-
writableProperties.each { it.writeToNode(jcrNode) }
5453

55-
if(innerProtoNode.mandatoryChildNodeList && innerProtoNode.mandatoryChildNodeList.size() > 0) {
56-
for(ProtoNode childNode: innerProtoNode.mandatoryChildNodeList) {
57-
//Mandatory children must inherit any name overrides from their parent (if they exist)
58-
createFrom(childNode, childNode.getName().replaceFirst(Pattern.quote(innerProtoNode.name), getName())).writeToJcr(session)
54+
final Collection<ProtoPropertyDecorator> referenceTypeProperties = writableProperties.findAll { it.isReferenceType() }
55+
final Collection<ProtoPropertyDecorator> nonReferenceTypeProperties = writableProperties.findAll { !it.isReferenceType() }
56+
57+
//These can be written now. Reference properties can be written after we write the referenced nodes
58+
nonReferenceTypeProperties.each { it.writeToNode(jcrNode) }
59+
60+
final Collection<JCRNodeDecorator> referenceables = writeMandatoryPieces(session, getName()).findAll { it.isReferenceable() }
61+
62+
/*
63+
* Nodes that are referenced from reference properties are written above in writeMandatoryPieces(). We can now map each
64+
* reference pointer to a transferred id from a node above, and write the pointer with a mapped nodes new identifier.
65+
* The transferred id is what the identifier was on sending server, and the current identifier is what it is now that it is
66+
* written to this server. Identifiers only apply to referenceable nodes (i.e nodes with mix:referenceable)
67+
*/
68+
referenceTypeProperties.each { ProtoPropertyDecorator property ->
69+
final Collection<Value> newReferenceIDValues = property.getStringValues().findResults { String referenceID ->
70+
final JCRNodeDecorator match = referenceables.find { it.transferredID == referenceID }
71+
if(match) {
72+
return new StringValue(match.getIdentifier())
73+
}
74+
} as Collection<Value>
75+
if(!newReferenceIDValues.isEmpty()) {
76+
property.writeToNode(jcrNode, (newReferenceIDValues.toArray() as Value[]))
5977
}
6078
}
61-
return new JCRNodeDecorator(jcrNode)
79+
80+
return new JCRNodeDecorator(jcrNode, getID())
6281
}
6382

6483

0 commit comments

Comments
 (0)