diff --git a/docs/Running.adoc b/docs/Running.adoc index 33be156..0d5b206 100644 --- a/docs/Running.adoc +++ b/docs/Running.adoc @@ -180,20 +180,3 @@ Invalid: ] } ``` - -=== Syncing Users and Groups - -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_. -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 -on your data-warehouse instance or other source instance; and add it as an exclude path as so: - -``` - pathConfigurations : - - - path : /home/groups - - - path : /home/users - excludePaths: - - k/ki9zhpzfe #Admin user -``` - diff --git a/src/main/groovy/com/twcable/grabbit/client/batch/ClientBatchJobExecutionListener.groovy b/src/main/groovy/com/twcable/grabbit/client/batch/ClientBatchJobExecutionListener.groovy index 7305aa8..5497f5d 100644 --- a/src/main/groovy/com/twcable/grabbit/client/batch/ClientBatchJobExecutionListener.groovy +++ b/src/main/groovy/com/twcable/grabbit/client/batch/ClientBatchJobExecutionListener.groovy @@ -16,7 +16,7 @@ package com.twcable.grabbit.client.batch -import com.twcable.grabbit.jcr.JcrUtil +import com.twcable.grabbit.jcr.JCRUtil import groovy.transform.CompileStatic import groovy.util.logging.Slf4j import org.apache.sling.jcr.api.SlingRepository @@ -58,7 +58,7 @@ class ClientBatchJobExecutionListener implements JobExecutionListener { void beforeJob(JobExecution jobExecution) { log.debug "SlingRepository : ${slingRepository}" final clientUsername = jobExecution.jobParameters.getString(ClientBatchJob.CLIENT_USERNAME) - final Session session = JcrUtil.getSession(slingRepository, clientUsername) + final Session session = JCRUtil.getSession(slingRepository, clientUsername) ClientBatchJobContext.setSession(session) log.info "Starting job : ${jobExecution}\n\n" diff --git a/src/main/groovy/com/twcable/grabbit/client/batch/steps/validation/ValidJobDecider.groovy b/src/main/groovy/com/twcable/grabbit/client/batch/steps/validation/ValidJobDecider.groovy index 5921c61..e911c5f 100644 --- a/src/main/groovy/com/twcable/grabbit/client/batch/steps/validation/ValidJobDecider.groovy +++ b/src/main/groovy/com/twcable/grabbit/client/batch/steps/validation/ValidJobDecider.groovy @@ -17,17 +17,15 @@ package com.twcable.grabbit.client.batch.steps.validation import com.twcable.grabbit.client.batch.ClientBatchJob import com.twcable.grabbit.client.batch.ClientBatchJobContext +import com.twcable.grabbit.jcr.JCRUtil import groovy.transform.CompileStatic import groovy.util.logging.Slf4j +import javax.jcr.Session import org.springframework.batch.core.JobExecution import org.springframework.batch.core.StepExecution import org.springframework.batch.core.job.flow.FlowExecutionStatus import org.springframework.batch.core.job.flow.JobExecutionDecider -import javax.jcr.PathNotFoundException -import javax.jcr.RepositoryException -import javax.jcr.Session - /** * 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 * if the job is job that is safe, and valid to execute. @@ -57,29 +55,14 @@ class ValidJobDecider implements JobExecutionDecider { @Override FlowExecutionStatus decide(JobExecution jobExecution, StepExecution stepExecution) { String jobPath = jobExecution.jobParameters.getString(ClientBatchJob.PATH) - //For processing, remove trailing / - jobPath = jobPath.replaceFirst(/\/$/, '') - //Get the parent's path (if applicable) and determine if it exists already - final parts = jobPath.split('/') - //No parent, so nothing to worry about - if(parts.length <= 2) return JOB_VALID - - final parentPath = parts[0..-2].join('/') - final Session session = theSession() - try { - session.getNode(parentPath) - } catch(PathNotFoundException pathException) { - log.warn "${jobPath} is not a valid job path. Make sure a parent is synched or created before this job is run" - log.debug pathException.toString() - return JOB_INVALID + if(JCRUtil.writingToExistingNode(jobPath, theSession())) { + log.debug "${ValidJobDecider.class.canonicalName} Job determined to be valid for job path ${jobPath}" + return JOB_VALID } - catch(RepositoryException repoException) { - log.error "${RepositoryException.class.canonicalName} Something went wrong when accessing the repository at ${this.class.canonicalName} for job path ${jobPath}!" - log.error repoException.toString() + else { + log.warn "${jobPath} is not a valid job path. Make sure a parent is synched or created before this job is run" return JOB_INVALID } - log.debug "${ValidJobDecider.class.canonicalName} Job determined to be valid for job path ${jobPath}" - return JOB_VALID } } diff --git a/src/main/groovy/com/twcable/grabbit/jcr/ACLProtoNodeDecorator.groovy b/src/main/groovy/com/twcable/grabbit/jcr/ACLProtoNodeDecorator.groovy index d53fd33..369d632 100644 --- a/src/main/groovy/com/twcable/grabbit/jcr/ACLProtoNodeDecorator.groovy +++ b/src/main/groovy/com/twcable/grabbit/jcr/ACLProtoNodeDecorator.groovy @@ -105,7 +105,7 @@ class ACLProtoNodeDecorator extends ProtoNodeDecorator { final String principalName = getPrincipalName(aceNode) Principal principal = getPrincipal(session, principalName) if(principal == null) { - log.warn "Principal for name ${principalName} does not exist, or is not accessible. Can not write ACE/ACL information." + 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." return } Privilege[] privileges = getPrivilegeNames(aceNode).collect { String privilegeName -> @@ -166,7 +166,7 @@ class ACLProtoNodeDecorator extends ProtoNodeDecorator { final ProtoPropertyDecorator privilegeProperty = node.propertiesList.collect { new ProtoPropertyDecorator(it) }.find { ProtoPropertyDecorator property -> property.isPrivilege() } - return privilegeProperty.getPropertyValues().collect { Value value -> value.string }.toArray() as String[] + return privilegeProperty.getStringValues().toArray() as String[] } diff --git a/src/main/groovy/com/twcable/grabbit/jcr/AuthorizableProtoNodeDecorator.groovy b/src/main/groovy/com/twcable/grabbit/jcr/AuthorizableProtoNodeDecorator.groovy index c264bfb..1f54a0f 100644 --- a/src/main/groovy/com/twcable/grabbit/jcr/AuthorizableProtoNodeDecorator.groovy +++ b/src/main/groovy/com/twcable/grabbit/jcr/AuthorizableProtoNodeDecorator.groovy @@ -25,7 +25,6 @@ import groovy.util.logging.Slf4j import java.lang.reflect.Field import java.lang.reflect.Method import java.lang.reflect.ReflectPermission -import java.util.regex.Pattern import javax.annotation.Nonnull import javax.jcr.Session import org.apache.jackrabbit.api.security.user.Authorizable @@ -35,7 +34,6 @@ import org.apache.jackrabbit.api.security.user.UserManager import org.apache.jackrabbit.value.StringValue import org.apache.sling.jcr.base.util.AccessControlUtil - /** * This class wraps a serialized node that represents an Authorizable. Authorizables are special system protected nodes, that can only be written under certain * trees, and can not be written directly by a client. @@ -57,10 +55,14 @@ class AuthorizableProtoNodeDecorator extends ProtoNodeDecorator { 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." + "Unfortunately, the way Jackrabbit goes about certain things requires us to do a bit of hacking in order to sync Authorizables securely, and efficiently.") } - Authorizable existingAuthorizable = findAuthorizable(session) + //the administrator is a special user that Jackrabbit will not let us mess with. + if(getAuthorizableID() == 'admin') { + return new JCRNodeDecorator(session.getNode(findAuthorizable(session, 'admin').getPath())) + } + + Authorizable existingAuthorizable = findAuthorizable(session, getAuthorizableID()) Authorizable newAuthorizable = existingAuthorizable ? updateAuthorizable(existingAuthorizable, session) : createNewAuthorizable(session) - writeAuthorizablePieces(newAuthorizable, session) - return new JCRNodeDecorator(session.getNode(newAuthorizable.getPath())) + return new JCRNodeDecorator(session.getNode(newAuthorizable.getPath()), getID()) } @@ -86,11 +88,20 @@ class AuthorizableProtoNodeDecorator extends ProtoNodeDecorator { setPasswordForUser(newUser, session) } session.save() + writeMandatoryPieces(session, newUser.getPath()) return newUser } - final Group group = userManager.createGroup(authorizableID, new AuthorizablePrincipal(authorizableID), getParentPath()) + final Group newGroup = userManager.createGroup(authorizableID, new AuthorizablePrincipal(authorizableID), getParentPath()) + /* + * 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. + */ + final Collection<JCRNodeDecorator> authorizablePieces = writeMandatoryPieces(session, newGroup.getPath()).findAll { it.isAuthorizableType() } + final Collection<JCRNodeDecorator> members = authorizablePieces.findAll { getMembershipIDs().contains(it.getTransferredID()) } + members.each { JCRNodeDecorator member -> + newGroup.addMember(member as Authorizable) + } session.save() - return group + return newGroup } @@ -100,28 +111,25 @@ class AuthorizableProtoNodeDecorator extends ProtoNodeDecorator { * @return new instance of updated authorizable */ private Authorizable updateAuthorizable(final Authorizable authorizable, final Session session) { + //We get all the declared groups of this authorizable so that we can add them back to the new, updated authorizable + final Collection<Group> declaredGroups = authorizable.declaredMemberOf().toList() + for(Group group : declaredGroups) { + group.removeMember(authorizable) + } authorizable.remove() session.save() - createNewAuthorizable(session) - } - - - /** - * 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 - * authorizable UUID node name at runtime. In other words, authorizables can live under different node names from server to server - */ - private void writeAuthorizablePieces(final Authorizable authorizable, final Session session) { - innerProtoNode.mandatoryChildNodeList.each { - //We replace the incoming server authorizable path, with the new authorizable path - createFrom(it, it.name.replaceFirst(Pattern.quote(getName()), authorizable.getPath())).writeToJcr(session) + final Authorizable newAuthorizable = createNewAuthorizable(session) + for(Group group: declaredGroups) { + group.addMember(newAuthorizable) } session.save() + return newAuthorizable } - private Authorizable findAuthorizable(final Session session) { + private Authorizable findAuthorizable(final Session session, final String authorizableID) { final UserManager userManager = getUserManager(session) - return userManager.getAuthorizable(getAuthorizableID()) + return userManager.getAuthorizable(authorizableID) } @@ -135,6 +143,11 @@ class AuthorizableProtoNodeDecorator extends ProtoNodeDecorator { } + private Collection<String> getMembershipIDs() { + return hasProperty('rep:members') ? getStringValuesFrom('rep:members') : [] + } + + /** * Some JVM's have a SecurityManager set, which based on configuration, can potentially inhibit our hack {@code setPasswordForUser(User, Session)} from working. * We need to check security permissions before proceeding @@ -245,13 +258,4 @@ class AuthorizableProtoNodeDecorator extends ProtoNodeDecorator { */ setPasswordMethod.invoke(userManagerDelegate, getTreeMethod.invoke(authorizable), getAuthorizableID(), getStringValueFrom('rep:password'), false) } - - - /** - * An instance wrapper for ease of mocking - * @see super.createFrom - */ - ProtoNodeDecorator createFrom(final ProtoNode protoNode, final String nameOverride) { - super.createFrom(protoNode, nameOverride) - } } diff --git a/src/main/groovy/com/twcable/grabbit/jcr/DefaultProtoNodeDecorator.groovy b/src/main/groovy/com/twcable/grabbit/jcr/DefaultProtoNodeDecorator.groovy index 2d6dc8f..7817000 100644 --- a/src/main/groovy/com/twcable/grabbit/jcr/DefaultProtoNodeDecorator.groovy +++ b/src/main/groovy/com/twcable/grabbit/jcr/DefaultProtoNodeDecorator.groovy @@ -19,11 +19,12 @@ import com.twcable.grabbit.proto.NodeProtos.Node as ProtoNode import com.twcable.grabbit.proto.NodeProtos.Value as ProtoValue import groovy.transform.CompileStatic import groovy.util.logging.Slf4j -import java.util.regex.Pattern import javax.annotation.Nonnull import javax.jcr.Node as JCRNode import javax.jcr.Session +import javax.jcr.Value import org.apache.jackrabbit.commons.JcrUtils +import org.apache.jackrabbit.value.StringValue import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES @@ -49,16 +50,34 @@ class DefaultProtoNodeDecorator extends ProtoNodeDecorator { if(mixinProperty) { addMixins(mixinProperty, jcrNode) } - //Then add other properties - writableProperties.each { it.writeToNode(jcrNode) } - if(innerProtoNode.mandatoryChildNodeList && innerProtoNode.mandatoryChildNodeList.size() > 0) { - for(ProtoNode childNode: innerProtoNode.mandatoryChildNodeList) { - //Mandatory children must inherit any name overrides from their parent (if they exist) - createFrom(childNode, childNode.getName().replaceFirst(Pattern.quote(innerProtoNode.name), getName())).writeToJcr(session) + final Collection<ProtoPropertyDecorator> referenceTypeProperties = writableProperties.findAll { it.isReferenceType() } + final Collection<ProtoPropertyDecorator> nonReferenceTypeProperties = writableProperties.findAll { !it.isReferenceType() } + + //These can be written now. Reference properties can be written after we write the referenced nodes + nonReferenceTypeProperties.each { it.writeToNode(jcrNode) } + + final Collection<JCRNodeDecorator> referenceables = writeMandatoryPieces(session, getName()).findAll { it.isReferenceable() } + + /* + * Nodes that are referenced from reference properties are written above in writeMandatoryPieces(). We can now map each + * reference pointer to a transferred id from a node above, and write the pointer with a mapped nodes new identifier. + * The transferred id is what the identifier was on sending server, and the current identifier is what it is now that it is + * written to this server. Identifiers only apply to referenceable nodes (i.e nodes with mix:referenceable) + */ + referenceTypeProperties.each { ProtoPropertyDecorator property -> + final Collection<Value> newReferenceIDValues = property.getStringValues().findResults { String referenceID -> + final JCRNodeDecorator match = referenceables.find { it.transferredID == referenceID } + if(match) { + return new StringValue(match.getIdentifier()) + } + } as Collection<Value> + if(!newReferenceIDValues.isEmpty()) { + property.writeToNode(jcrNode, (newReferenceIDValues.toArray() as Value[])) } } - return new JCRNodeDecorator(jcrNode) + + return new JCRNodeDecorator(jcrNode, getID()) } diff --git a/src/main/groovy/com/twcable/grabbit/jcr/JCRNodeDecorator.groovy b/src/main/groovy/com/twcable/grabbit/jcr/JCRNodeDecorator.groovy index 61aa8be..138a2b9 100644 --- a/src/main/groovy/com/twcable/grabbit/jcr/JCRNodeDecorator.groovy +++ b/src/main/groovy/com/twcable/grabbit/jcr/JCRNodeDecorator.groovy @@ -27,14 +27,21 @@ import javax.jcr.Node as JCRNode import javax.jcr.PathNotFoundException import javax.jcr.Property as JcrProperty import javax.jcr.RepositoryException +import javax.jcr.Session +import javax.jcr.Value as JcrValue import javax.jcr.nodetype.ItemDefinition +import org.apache.jackrabbit.api.security.user.Authorizable +import org.apache.jackrabbit.api.security.user.UserManager import org.apache.jackrabbit.commons.flat.TreeTraverser import org.apache.jackrabbit.value.DateValue +import org.apache.sling.jcr.base.util.AccessControlUtil +import org.slf4j.Logger import static org.apache.jackrabbit.JcrConstants.JCR_CREATED import static org.apache.jackrabbit.JcrConstants.JCR_LASTMODIFIED import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE +import static org.apache.jackrabbit.JcrConstants.MIX_REFERENCEABLE import static org.apache.jackrabbit.commons.flat.TreeTraverser.ErrorHandler import static org.apache.jackrabbit.commons.flat.TreeTraverser.InclusionPolicy import static org.apache.jackrabbit.oak.spi.security.authorization.accesscontrol.AccessControlConstants.AC_NODETYPE_NAMES @@ -47,25 +54,42 @@ class JCRNodeDecorator { @Delegate JCRNode innerNode - private final Collection<JcrPropertyDecorator> properties + private final Collection<JCRPropertyDecorator> wrappedProperties //Evaluated in a lazy fashion private Collection<JCRNodeDecorator> immediateChildNodes private List<JCRNodeDecorator> childNodeList + private final String transferredID + JCRNodeDecorator(@Nonnull JCRNode node) { + this(node, null) + } + + + JCRNodeDecorator(@Nonnull JCRNode node, @Nullable String transferredID) { if(!node) throw new IllegalArgumentException("node must not be null!") this.innerNode = node Collection<JcrProperty> innerProperties = node.properties.toList() - this.properties = innerProperties.collect { JcrProperty property -> - new JcrPropertyDecorator(property, this) + this.wrappedProperties = innerProperties.collect { JcrProperty property -> + new JCRPropertyDecorator(property, this) } + this.transferredID = transferredID } + private Collection<JCRPropertyDecorator> getWrappedProperties() { return wrappedProperties } + + /** - * @return this node's immediate children, empty if none + * @return an identifier for this node on a sending server. Not all wrapped nodes will have this. Will return null if it does not exist + */ + String getTransferredID() { return transferredID } + + + /** + * @return this node's immediate children (depth 1), empty if none */ Collection<JCRNodeDecorator> getImmediateChildNodes() { if(!immediateChildNodes) { @@ -74,7 +98,9 @@ class JCRNodeDecorator { return immediateChildNodes } - + /** + * @return this node's children via pre-order traversal. + */ List<JCRNodeDecorator> getChildNodeList() { if(!childNodeList) { childNodeList = (getChildNodeIterator().collect { JCRNode node -> new JCRNodeDecorator(node) } ?: []) as List<JCRNodeDecorator> @@ -83,10 +109,6 @@ class JCRNodeDecorator { } - Iterator<JCRNode> getChildNodeIterator() { - return TreeTraverser.nodeIterator(innerNode, ErrorHandler.IGNORE, new NoRootInclusionPolicy(this)) - } - void setLastModified() { final lastModified = new DateValue(Calendar.instance) @@ -104,19 +126,25 @@ class JCRNodeDecorator { /** - * Identify all required child nodes + * Identify all required child nodes. This may include any mandatory nodes per definition, associated nodes that are required to write this node info on another server, + * and nodes that are referenced via this node's weak/strong reference pointers * @return list of immediate required child nodes that must be transported with this node, or an empty collection if no required nodes */ @Nullable Collection<JCRNodeDecorator> getRequiredChildNodes() { + final Collection<JCRNodeDecorator> requiredNodes = [] if(isAuthorizableType()){ - return getChildNodeList().findAll { JCRNodeDecorator childJcrNode -> !childJcrNode.isLoginToken() && !childJcrNode.isACType() } + requiredNodes.addAll(getChildNodeList().findAll { JCRNodeDecorator childJcrNode -> !childJcrNode.isLoginToken() && !childJcrNode.isACType() }) } else if(isRepACLType()) { //Send all ACE parts underneath the ACL as required nodes - return getChildNodeList() + requiredNodes.addAll(getChildNodeList()) + } + else { + requiredNodes.addAll(getMandatoryChildren()) } - return getMandatoryChildren() + requiredNodes.addAll(referencedNodesFrom(requiredNodes + this)) + return requiredNodes } @@ -125,6 +153,30 @@ class JCRNodeDecorator { } + /** + * Find any weak or strong references on this collection of nodes, and attempt to find their nodes + * @return any nodes we can find through references + */ + private Collection<JCRNodeDecorator> referencedNodesFrom(Collection<JCRNodeDecorator> nodes) { + final Collection<JCRPropertyDecorator> referenceProperties = nodes.collectMany { JCRNodeDecorator node -> + /** + * Find all references that are transferable. We don't want to send referenced nodes that belong to a protected property that are not being transferred back to the client, + * such as version storage entries + */ + return node.getWrappedProperties().findAll { JCRPropertyDecorator property -> property.isReferenceType() && property.isTransferable() } + } + return referenceProperties.collectMany { JCRPropertyDecorator property -> + property.values.toList().findResults { JcrValue value -> + try { + return new JCRNodeDecorator(getSession().getNodeByIdentifier(value.string)) + } catch(ItemNotFoundException ex) { + _log().info "Tried following reference ${value.string} from ${getInnerNode().path}, but this node does not exist any longer. Skipping" + } + } + } as Collection<JCRNodeDecorator> + } + + /** * Some nodes must be saved together, per node definition */ @@ -151,13 +203,14 @@ class JCRNodeDecorator { return definition.isMandatory() } - /** - * Build node and "only" mandatory child nodes - */ + @Nonnull ProtoNode toProtoNode() { final ProtoNodeBuilder protoNodeBuilder = ProtoNode.newBuilder() protoNodeBuilder.setName(path) + if(isNodeType(MIX_REFERENCEABLE)) { + protoNodeBuilder.setIdentifier(getIdentifier()) + } protoNodeBuilder.addAllProperties(getProtoProperties()) requiredChildNodes.each { protoNodeBuilder.addMandatoryChildNode(it.toProtoNode()) @@ -165,15 +218,14 @@ class JCRNodeDecorator { return protoNodeBuilder.build() } - /** - * @return resulting collection of transferable proto properties collected from jcrNode - */ + @Nonnull private Collection<ProtoProperty> getProtoProperties() { - final Collection<JcrPropertyDecorator> transferableProperties = properties.findAll{ it.isTransferable() } + final Collection<JCRPropertyDecorator> transferableProperties = getWrappedProperties().findAll{ it.isTransferable() } return transferableProperties.collect{ it.toProtoProperty() } } + /** * Returns the "jcr:lastModified", "cq:lastModified" or "jcr:created" date property * for current Jcr Node @@ -243,15 +295,41 @@ class JCRNodeDecorator { } + boolean isReferenceable() { + return isNodeType(MIX_REFERENCEABLE) + } + + + /** + * For ease of mocking. Simply delegates to static accessor in AccessControlUtil + */ + UserManager getUserManager(final Session session) { + return AccessControlUtil.getUserManager(session) + } + + + /** + * For ease of mocking + */ + Iterator<JCRNode> getChildNodeIterator() { + return TreeTraverser.nodeIterator(innerNode, ErrorHandler.IGNORE, new NoRootInclusionPolicy(this)) + } + + Object asType(Class clazz) { if(clazz == JCRNode) { return innerNode } + else if(clazz == Authorizable) { + if(!isAuthorizableType()) throw new ClassCastException('This class is not an Authorizable type. Check isAuthorizableType() before casting.') + return getUserManager(session).getAuthorizableByPath(getPath()) + } else { super.asType(clazz) } } + @Override boolean equals(Object obj) { if (this.is(obj)) return true @@ -262,11 +340,20 @@ class JCRNodeDecorator { return this.hashCode() == that.hashCode() } + @Override int hashCode() { return innerNode.getName().hashCode() } + /** + * @SL4J generated log property, and @Delegate conflict on accessing log sometimes within closures. This is to get around that + */ + private static Logger _log() { + return log + } + + @CompileStatic private static class NoRootInclusionPolicy implements InclusionPolicy<JCRNode> { diff --git a/src/main/groovy/com/twcable/grabbit/jcr/JcrPropertyDecorator.groovy b/src/main/groovy/com/twcable/grabbit/jcr/JcrPropertyDecorator.groovy index b5aa8fa..3892e85 100644 --- a/src/main/groovy/com/twcable/grabbit/jcr/JcrPropertyDecorator.groovy +++ b/src/main/groovy/com/twcable/grabbit/jcr/JcrPropertyDecorator.groovy @@ -22,28 +22,39 @@ import com.twcable.grabbit.proto.NodeProtos.Value as ProtoValue import com.twcable.grabbit.proto.NodeProtos.Value.Builder as ProtoValueBuilder import groovy.transform.CompileStatic import groovy.util.logging.Slf4j - import javax.annotation.Nonnull import javax.jcr.Property as JCRProperty import javax.jcr.Value + import static javax.jcr.PropertyType.BINARY -import static org.apache.jackrabbit.JcrConstants.* +import static javax.jcr.PropertyType.REFERENCE +import static javax.jcr.PropertyType.WEAKREFERENCE +import static org.apache.jackrabbit.JcrConstants.JCR_LASTMODIFIED +import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES +import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE @CompileStatic @Slf4j -class JcrPropertyDecorator { +class JCRPropertyDecorator { @Delegate JCRProperty innerProperty private final JCRNodeDecorator nodeOwner - JcrPropertyDecorator(JCRProperty property, JCRNodeDecorator nodeOwner) { + + JCRPropertyDecorator(JCRProperty property, JCRNodeDecorator nodeOwner) { this.innerProperty = property this.nodeOwner = nodeOwner } + + boolean isReferenceType() { + return type == REFERENCE || type == WEAKREFERENCE + } + + /** * Determines if JCR Property object can be "rewritten" to the JCR. For example, we can not rewrite a node's * primary type; That is forbidden by the JCR spec. @@ -67,6 +78,13 @@ class JcrPropertyDecorator { return !definition.isProtected() } + + @Override + Value[] getValues() { + return (multiple ? innerProperty.values : [innerProperty.value]) as Value[] + } + + /** * Marshalls current Jcr Property to a ProtoProperty */ @@ -81,7 +99,6 @@ class JcrPropertyDecorator { } else { //Other property types can potentially have multiple values - final Value[] values = multiple ? values : [value] as Value[] values.each { Value value -> propertyBuilder.addValues(valueBuilder.setStringValue(value.string)) } diff --git a/src/main/groovy/com/twcable/grabbit/jcr/JcrUtil.groovy b/src/main/groovy/com/twcable/grabbit/jcr/JcrUtil.groovy index 74eaad2..61c7837 100644 --- a/src/main/groovy/com/twcable/grabbit/jcr/JcrUtil.groovy +++ b/src/main/groovy/com/twcable/grabbit/jcr/JcrUtil.groovy @@ -18,6 +18,8 @@ package com.twcable.grabbit.jcr import groovy.transform.CompileStatic import groovy.util.logging.Slf4j +import javax.jcr.PathNotFoundException +import javax.jcr.RepositoryException import org.apache.sling.api.resource.ResourceResolver import org.apache.sling.api.resource.ResourceResolverFactory import org.apache.sling.jcr.api.SlingRepository @@ -31,7 +33,7 @@ import javax.jcr.SimpleCredentials */ @Slf4j @CompileStatic -public class JcrUtil { +public class JCRUtil { /** * This creates a JCR Session with the rights of the given user (by default @@ -64,7 +66,7 @@ public class JcrUtil { * This handles creating the ResourceResolver with the rights of the given user (by default * that is 'anonymous') and does the needed clean up when the code block is finished. * <p/> - * <code>use(JcrUtil) {<br/> + * <code>use(JCRUtil) {<br/> *   slingRepository.withSession{Session session -><br/> *     // do something with the Session<br/> *   }<br/> @@ -112,7 +114,7 @@ public class JcrUtil { * This handles creating the ResourceResolver with the rights of the given user (by default * that is 'anonymous') and does the needed clean up when the code block is finished. * <p/> - * <code>use(JcrUtil) {<br/> + * <code>use(JCRUtil) {<br/> *   resourceResolverFactory.withResourceResolver {ResourceResolver resourceResolver -><br/> *     // do something with the ResourceResolver<br/> *   }<br/> @@ -146,7 +148,7 @@ public class JcrUtil { * This handles creating the ResourceResolver with the rights of the given user (by default * that is 'anonymous') and does the needed clean up when the code block is finished. * <p/> - * <code>use(JcrUtil) {<br/> + * <code>use(JCRUtil) {<br/> *   resourceResolverFactory.manageResourceResolver {ResourceResolver resourceResolver -><br/> *     // do something with the ResourceResolver<br/> *   }<br/> @@ -164,4 +166,30 @@ public class JcrUtil { return withResourceResolver(resolverFactory, "admin", closure) } + /** + * @return true if the node at the path being written is being written to an existing node + */ + static boolean writingToExistingNode(@Nonnull final String pathBeingWritten, @Nonnull final Session session) { + //For processing, remove trailing / + final String thePath = pathBeingWritten.replaceFirst(/\/$/, '') + //Get the parent's path (if applicable) and determine if it exists already + final parts = thePath.split('/') + //No parent, so nothing to worry about + if(parts.length <= 2) return true + + final String parentPath = parts[0..-2].join('/') + try { + session.getNode(parentPath) + } catch(PathNotFoundException pathException) { + log.debug pathException.toString() + return false + } + catch(RepositoryException repoException) { + log.error "${RepositoryException.class.canonicalName} Something went wrong when accessing the repository at ${this.class.canonicalName} for path ${pathBeingWritten}!" + log.error repoException.toString() + return false + } + return true + } + } diff --git a/src/main/groovy/com/twcable/grabbit/jcr/ProtoNodeDecorator.groovy b/src/main/groovy/com/twcable/grabbit/jcr/ProtoNodeDecorator.groovy index 9cd437d..059713f 100644 --- a/src/main/groovy/com/twcable/grabbit/jcr/ProtoNodeDecorator.groovy +++ b/src/main/groovy/com/twcable/grabbit/jcr/ProtoNodeDecorator.groovy @@ -18,7 +18,7 @@ package com.twcable.grabbit.jcr import com.twcable.grabbit.proto.NodeProtos.Node as ProtoNode import groovy.transform.CompileStatic - +import java.util.regex.Pattern import javax.annotation.Nonnull import javax.jcr.Session @@ -69,6 +69,17 @@ abstract class ProtoNodeDecorator { protoProperties.find { it.name == propertyName }.stringValue } + + protected Collection<String> getStringValuesFrom(String propertyName) { + protoProperties.find { it.name == propertyName }.valuesList.collect { it.stringValue } + } + + + protected String getID() { + return innerProtoNode.getIdentifier() + } + + protected String getParentPath() { final pathTokens = getName().tokenize('/') //remove last index, as this is the Authorizable node name @@ -76,6 +87,23 @@ abstract class ProtoNodeDecorator { return "/${pathTokens.join('/')}" } + + protected Collection<JCRNodeDecorator> writeMandatoryPieces(final Session session, final String pathOverride) { + final Collection<JCRNodeDecorator> mandatoryPieces = innerProtoNode.mandatoryChildNodeList.collect { + //Mandatory nodes, if children of this node, must inherit any name overrides (if they exist) + _createFrom(it, it.getName().replaceFirst(Pattern.quote(getName()), pathOverride)).writeToJcr(session) + } + session.save() + return mandatoryPieces + } + + + //An instance wrapper for ease of mocking + ProtoNodeDecorator _createFrom(ProtoNode node, String nameOverride) { + return createFrom(node, nameOverride) + } + + @Override String getName() { nameOverride ?: innerProtoNode.getName() diff --git a/src/main/groovy/com/twcable/grabbit/jcr/ProtoPropertyDecorator.groovy b/src/main/groovy/com/twcable/grabbit/jcr/ProtoPropertyDecorator.groovy index ab809a5..b6568de 100644 --- a/src/main/groovy/com/twcable/grabbit/jcr/ProtoPropertyDecorator.groovy +++ b/src/main/groovy/com/twcable/grabbit/jcr/ProtoPropertyDecorator.groovy @@ -28,6 +28,8 @@ import javax.jcr.ValueFormatException import org.apache.jackrabbit.value.ValueFactoryImpl +import static javax.jcr.PropertyType.REFERENCE +import static javax.jcr.PropertyType.WEAKREFERENCE import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE import static org.apache.jackrabbit.oak.spi.security.authorization.accesscontrol.AccessControlConstants.AC_NODETYPE_NAMES @@ -52,13 +54,18 @@ class ProtoPropertyDecorator { void writeToNode(@Nonnull JCRNode node) { + writeToNode(node, getPropertyValues()) + } + + + void writeToNode(final JCRNode node, Value[] values) { if(primaryType || mixinType) throw new IllegalStateException("Refuse to write jcr:primaryType or jcr:mixinType as normal properties. These are not allowed") try { if (multiple) { - node.setProperty(this.name, getPropertyValues(), this.type) + node.setProperty(this.name, values, this.type) } else { - node.setProperty(this.name, getPropertyValue(), this.type) + node.setProperty(this.name, values.first(), this.type) } } catch (ValueFormatException ex) { @@ -71,7 +78,7 @@ class ProtoPropertyDecorator { if(existingProperty.type != this.type || existingProperty.multiple ^ this.multiple) { existingProperty.remove() node.session.save() - this.writeToNode(node) + this.writeToNode(node, values) log.debug "Resolve successful..." } else { @@ -141,18 +148,13 @@ class ProtoPropertyDecorator { } - String getStringValue() { - getValue().stringValue - } - - - private ProtoValue getValue() { - innerProtoProperty.valuesList.first() + boolean isReferenceType() { + innerProtoProperty.type == REFERENCE || innerProtoProperty.type == WEAKREFERENCE } Value getPropertyValue() throws ValueFormatException { - getJCRValueFromProtoValue(getValue()) + getPropertyValues().first() } @@ -161,6 +163,16 @@ class ProtoPropertyDecorator { } + String getStringValue() { + getPropertyValue().string + } + + + Collection<String> getStringValues() { + getPropertyValues().collect { Value value -> value.string } + } + + private Value getJCRValueFromProtoValue(ProtoValue value) throws ValueFormatException { final valueFactory = ValueFactoryImpl.getInstance() diff --git a/src/main/groovy/com/twcable/grabbit/server/GrabbitContentPullServlet.groovy b/src/main/groovy/com/twcable/grabbit/server/GrabbitContentPullServlet.groovy index 4b1455a..bda4892 100644 --- a/src/main/groovy/com/twcable/grabbit/server/GrabbitContentPullServlet.groovy +++ b/src/main/groovy/com/twcable/grabbit/server/GrabbitContentPullServlet.groovy @@ -82,7 +82,10 @@ class GrabbitContentPullServlet extends SlingSafeMethodsServlet { //This user will be used to connect to JCR. //If the User is null, 'anonymous' will be used to connect to JCR. final serverUsername = request.remoteUser - serverService.getContentForRootPath(serverUsername, decodedPath, decodedExcludePaths ?: null, - afterDateString ?: null, response.outputStream) + serverService.getContentForRootPath(serverUsername, + decodedPath, + decodedExcludePaths ?: null, + afterDateString ?: null, + response.outputStream) } } diff --git a/src/main/groovy/com/twcable/grabbit/server/batch/ServerBatchJob.groovy b/src/main/groovy/com/twcable/grabbit/server/batch/ServerBatchJob.groovy index 91e5705..6e23152 100644 --- a/src/main/groovy/com/twcable/grabbit/server/batch/ServerBatchJob.groovy +++ b/src/main/groovy/com/twcable/grabbit/server/batch/ServerBatchJob.groovy @@ -75,7 +75,8 @@ class ServerBatchJob { PathBuilder andConfiguration(Iterator<Map.Entry<String, String>> namespacesIterator, - Iterator<JcrNode> nodeIterator, ServletOutputStream servletOutputStream) { + Iterator<JcrNode> nodeIterator, + ServletOutputStream servletOutputStream) { if (namespacesIterator == null) throw new IllegalArgumentException("namespacesIterator == null") if (nodeIterator == null) throw new IllegalArgumentException("nodeIterator == null") if (servletOutputStream == null) throw new IllegalArgumentException("servletOutputStream == null") diff --git a/src/main/groovy/com/twcable/grabbit/server/batch/steps/jcrnodes/JcrNodesProcessor.groovy b/src/main/groovy/com/twcable/grabbit/server/batch/steps/jcrnodes/JcrNodesProcessor.groovy index 42814c3..06f6867 100644 --- a/src/main/groovy/com/twcable/grabbit/server/batch/steps/jcrnodes/JcrNodesProcessor.groovy +++ b/src/main/groovy/com/twcable/grabbit/server/batch/steps/jcrnodes/JcrNodesProcessor.groovy @@ -35,10 +35,12 @@ class JcrNodesProcessor implements ItemProcessor<JcrNode, ProtoNode> { private String contentAfterDate + void setContentAfterDate(String contentAfterDate) { this.contentAfterDate = contentAfterDate } + /** * Converts a JCR Node to a {@link ProtoNode} object. * Returns null if current node does not need to be processed diff --git a/src/main/groovy/com/twcable/grabbit/server/services/ServerService.groovy b/src/main/groovy/com/twcable/grabbit/server/services/ServerService.groovy index 5bcf9f4..b368e17 100644 --- a/src/main/groovy/com/twcable/grabbit/server/services/ServerService.groovy +++ b/src/main/groovy/com/twcable/grabbit/server/services/ServerService.groovy @@ -25,5 +25,9 @@ interface ServerService { * @param path * @param servletOutputStream */ - void getContentForRootPath(String serverUsername, String path, Collection<String> excludePaths, String afterDateString, ServletOutputStream servletOutputStream) + void getContentForRootPath(String serverUsername, + String path, + Collection<String> excludePaths, + String afterDateString, + ServletOutputStream servletOutputStream) } diff --git a/src/main/groovy/com/twcable/grabbit/server/services/impl/DefaultServerService.groovy b/src/main/groovy/com/twcable/grabbit/server/services/impl/DefaultServerService.groovy index acbae61..f154569 100644 --- a/src/main/groovy/com/twcable/grabbit/server/services/impl/DefaultServerService.groovy +++ b/src/main/groovy/com/twcable/grabbit/server/services/impl/DefaultServerService.groovy @@ -16,7 +16,7 @@ package com.twcable.grabbit.server.services.impl -import com.twcable.grabbit.jcr.JcrUtil +import com.twcable.grabbit.jcr.JCRUtil import com.twcable.grabbit.server.batch.ServerBatchJob import com.twcable.grabbit.server.services.ExcludePathNodeIterator import com.twcable.grabbit.server.services.RootNodeWithMandatoryIterator @@ -51,17 +51,19 @@ class DefaultServerService implements ServerService { ConfigurableApplicationContext configurableApplicationContext + @Override void getContentForRootPath( @Nullable String serverUsername, @Nonnull String path, @Nullable Collection<String> excludePaths, - @Nullable String afterDateString, @Nonnull ServletOutputStream servletOutputStream) { + @Nullable String afterDateString, + @Nonnull ServletOutputStream servletOutputStream) { if (path == null) throw new IllegalStateException("path == null") if (excludePaths == null) excludePaths = (Collection<String>) Collections.EMPTY_LIST if (servletOutputStream == null) throw new IllegalStateException("servletOutputStream == null") - JcrUtil.withSession(slingRepository, serverUsername) { Session session -> + JCRUtil.withSession(slingRepository, serverUsername) { Session session -> Iterator<JcrNode> nodeIterator //If the path is of type "/a/b/.", that means we should not do a recursive search of b's children diff --git a/src/main/groovy/com/twcable/grabbit/spring/batch/repository/JcrGrabbitExecutionContextDao.groovy b/src/main/groovy/com/twcable/grabbit/spring/batch/repository/JcrGrabbitExecutionContextDao.groovy index f12e06a..90d6bf9 100644 --- a/src/main/groovy/com/twcable/grabbit/spring/batch/repository/JcrGrabbitExecutionContextDao.groovy +++ b/src/main/groovy/com/twcable/grabbit/spring/batch/repository/JcrGrabbitExecutionContextDao.groovy @@ -16,7 +16,7 @@ package com.twcable.grabbit.spring.batch.repository -import com.twcable.grabbit.jcr.JcrUtil +import com.twcable.grabbit.jcr.JCRUtil import com.twcable.grabbit.util.CryptoUtil import groovy.transform.CompileStatic import groovy.util.logging.Slf4j @@ -159,7 +159,7 @@ class JcrGrabbitExecutionContextDao extends AbstractJcrDao implements GrabbitExe */ @Override protected void ensureRootResource() { - JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + JCRUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> if (!getOrCreateResource(resolver, JOB_EXECUTION_CONTEXT_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true)) { //create the Root Resource throw new IllegalStateException("Cannot get or create RootResource for : ${JOB_EXECUTION_CONTEXT_ROOT}") @@ -190,7 +190,7 @@ class JcrGrabbitExecutionContextDao extends AbstractJcrDao implements GrabbitExe if (!executionId) throw new IllegalArgumentException("executionId == null") if (!executionContext) throw new IllegalArgumentException("executionContext == null") - JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + JCRUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> final rootResource = getOrCreateResource(resolver, rootResourceName, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true) final existingResource = rootResource.children.asList().find { Resource resource -> @@ -258,7 +258,7 @@ class JcrGrabbitExecutionContextDao extends AbstractJcrDao implements GrabbitExe if (!rootResourceName) throw new IllegalArgumentException("rootResourceName == null") if (!executionId) throw new IllegalArgumentException("executionId == null") - JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + JCRUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> final rootResource = getOrCreateResource(resolver, rootResourceName, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true) //find resource from the "rootResource" where the executionId property matches "executionid" @@ -307,7 +307,7 @@ class JcrGrabbitExecutionContextDao extends AbstractJcrDao implements GrabbitExe @Override Collection<String> getJobExecutionContextPaths(Collection<String> jobExecutionResourcePaths) { - JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + JCRUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> Collection<String> jobExecutionContextPathsToRemove = [] jobExecutionResourcePaths.each { String jobExecutionResourcePath -> Resource jobExecutionResource = resolver.getResource(jobExecutionResourcePath) @@ -332,7 +332,7 @@ class JcrGrabbitExecutionContextDao extends AbstractJcrDao implements GrabbitExe @Override Collection<String> getStepExecutionContextPaths(Collection<String> stepExecutionResourcePaths) { - JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + JCRUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> Collection<String> stepExecutionContextPathsToRemove = [] stepExecutionResourcePaths.each { String stepExecutionResourcePath -> Resource stepExecutionResource = resolver.getResource(stepExecutionResourcePath) diff --git a/src/main/groovy/com/twcable/grabbit/spring/batch/repository/JcrGrabbitJobExecutionDao.groovy b/src/main/groovy/com/twcable/grabbit/spring/batch/repository/JcrGrabbitJobExecutionDao.groovy index a0d4be8..9f12a2f 100644 --- a/src/main/groovy/com/twcable/grabbit/spring/batch/repository/JcrGrabbitJobExecutionDao.groovy +++ b/src/main/groovy/com/twcable/grabbit/spring/batch/repository/JcrGrabbitJobExecutionDao.groovy @@ -18,7 +18,7 @@ package com.twcable.grabbit.spring.batch.repository import com.twcable.grabbit.DateUtil import com.twcable.grabbit.client.batch.ClientBatchJob -import com.twcable.grabbit.jcr.JcrUtil +import com.twcable.grabbit.jcr.JCRUtil import com.twcable.grabbit.util.CryptoUtil import groovy.transform.CompileStatic import groovy.util.logging.Slf4j @@ -75,7 +75,7 @@ class JcrGrabbitJobExecutionDao extends AbstractJcrDao implements GrabbitJobExec //Create a new resource for the jobExecution (with the id) jobExecution.incrementVersion() - JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + JCRUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> final id = CryptoUtil.generateNextId() jobExecution.id = id Resource rootResource = getOrCreateResource(resolver, JOB_EXECUTION_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true) @@ -126,7 +126,7 @@ class JcrGrabbitJobExecutionDao extends AbstractJcrDao implements GrabbitJobExec if (!jobExecution) throw new IllegalArgumentException("jobExecution == null") if (!jobExecution.id) throw new IllegalArgumentException("jobExecution must have an id") - JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + JCRUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> final rootResource = getOrCreateResource(resolver, JOB_EXECUTION_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true) //Get the Execution with jobExecution.id from jobExecution root resource @@ -157,7 +157,7 @@ class JcrGrabbitJobExecutionDao extends AbstractJcrDao implements GrabbitJobExec List<JobExecution> findJobExecutions(@Nonnull final JobInstance jobInstance) { if (!jobInstance) throw new IllegalArgumentException("jobInstance == null") - JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + JCRUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> final Resource jobExecutionRootResource = getOrCreateResource(resolver, JOB_EXECUTION_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true) List<Resource> resources = jobExecutionRootResource?.children?.asList() if (!resources) return Collections.EMPTY_LIST @@ -255,7 +255,7 @@ class JcrGrabbitJobExecutionDao extends AbstractJcrDao implements GrabbitJobExec Set<JobExecution> findRunningJobExecutions(@Nonnull final String jobName) { if (!jobName) throw new IllegalArgumentException("jobName == null") - JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + JCRUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> final jobExecutionRoot = getOrCreateResource(resolver, JOB_EXECUTION_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true) //find jobExecution root, then find all jobExecutions, then find the jobexecution @@ -290,7 +290,7 @@ class JcrGrabbitJobExecutionDao extends AbstractJcrDao implements GrabbitJobExec if (!executionId) throw new IllegalArgumentException("executionId == null") //find jobExecution root, then find jobExecution with node name : "jobExecution${executionId}" - JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + JCRUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> final jobExecutionRoot = getOrCreateResource(resolver, JOB_EXECUTION_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true) final jobExecutionResource = jobExecutionRoot?.children?.asList()?.find { Resource resource -> final properties = resource.adaptTo(ValueMap) @@ -370,7 +370,7 @@ class JcrGrabbitJobExecutionDao extends AbstractJcrDao implements GrabbitJobExec */ @Override protected void ensureRootResource() { - JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + JCRUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> if (!getOrCreateResource(resolver, JOB_EXECUTION_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true)) { //create the Root Resource throw new IllegalStateException("Cannot get or create RootResource for : ${JOB_EXECUTION_ROOT}") @@ -385,7 +385,7 @@ class JcrGrabbitJobExecutionDao extends AbstractJcrDao implements GrabbitJobExec @Override Collection<String> getJobExecutions(Collection<BatchStatus> batchStatuses) { String statusPredicate = batchStatuses.collect { "s.status = '${it}'" }.join(' or ') - JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + JCRUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> String jobExecutionsQuery = "select * from [nt:unstructured] as s where " + "ISDESCENDANTNODE(s,'${JOB_EXECUTION_ROOT}') AND ( ${statusPredicate} )" Collection<String> jobExecutions = resolver.findResources(jobExecutionsQuery, "JCR-SQL2") @@ -400,7 +400,7 @@ class JcrGrabbitJobExecutionDao extends AbstractJcrDao implements GrabbitJobExec @Override Collection<String> getJobExecutions(int hours, Collection<String> jobExecutions) { - JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + JCRUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> //Create a Date object that is "hours" ago from now Calendar olderThanHours = Calendar.getInstance() log.info "Current time: ${olderThanHours.time}" diff --git a/src/main/groovy/com/twcable/grabbit/spring/batch/repository/JcrGrabbitJobInstanceDao.groovy b/src/main/groovy/com/twcable/grabbit/spring/batch/repository/JcrGrabbitJobInstanceDao.groovy index 160e253..7a90a10 100644 --- a/src/main/groovy/com/twcable/grabbit/spring/batch/repository/JcrGrabbitJobInstanceDao.groovy +++ b/src/main/groovy/com/twcable/grabbit/spring/batch/repository/JcrGrabbitJobInstanceDao.groovy @@ -16,7 +16,7 @@ package com.twcable.grabbit.spring.batch.repository -import com.twcable.grabbit.jcr.JcrUtil +import com.twcable.grabbit.jcr.JCRUtil import groovy.transform.CompileStatic import groovy.util.logging.Slf4j import org.apache.sling.api.resource.Resource @@ -69,7 +69,7 @@ class JcrGrabbitJobInstanceDao extends AbstractJcrDao implements GrabbitJobInsta if (!jobParameters) throw new IllegalArgumentException("jobParameters == null") if (getJobInstance(jobName, jobParameters)) throw new IllegalStateException("A JobInstance for jobName: ${jobName} must not already exist") - JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + JCRUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> //TODO : This will need to be a "incrementing ID" after all and NOT use a UUID (for other APIs like //getJobInstances which require result to be sorted backwards by "id". If we use UUID, then the possibility of @@ -115,7 +115,7 @@ class JcrGrabbitJobInstanceDao extends AbstractJcrDao implements GrabbitJobInsta final jobKey = new DefaultJobKeyGenerator().generateKey(jobParameters) //Find a resource under "/jobInstances" for the jobKey above - JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + JCRUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> final rootResource = getOrCreateResource(resolver, JOB_INSTANCE_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true) final jobInstanceResource = rootResource?.children?.asList()?.find { Resource resource -> final properties = resource.adaptTo(ValueMap) @@ -141,7 +141,7 @@ class JcrGrabbitJobInstanceDao extends AbstractJcrDao implements GrabbitJobInsta JobInstance getJobInstance(@Nonnull final Long instanceId) { if (!instanceId) throw new IllegalArgumentException("instanceId == null") //Get the "instanceId" node from JOB_INSTANCE_ROOT - JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + JCRUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> final rootResource = getOrCreateResource(resolver, JOB_INSTANCE_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true) final jobInstanceResource = rootResource?.children?.asList()?.find { Resource resource -> final properties = resource.adaptTo(ValueMap) @@ -184,7 +184,7 @@ class JcrGrabbitJobInstanceDao extends AbstractJcrDao implements GrabbitJobInsta //Get All jobInstances in JOB_INSTANCE_ROOT with jobName property = $jobName //Map jobInstances nodes to JobInstance object - JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + JCRUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> final rootResource = getOrCreateResource(resolver, JOB_INSTANCE_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true) final jobInstanceResource = rootResource?.children?.asList()?.findAll { Resource resource -> final properties = resource.adaptTo(ValueMap) @@ -222,7 +222,7 @@ class JcrGrabbitJobInstanceDao extends AbstractJcrDao implements GrabbitJobInsta */ @Override List<String> getJobNames() { - JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + JCRUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> final jobInstanceResources = getOrCreateResource(resolver, JOB_INSTANCE_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true)?.children?.asList() if (!jobInstanceResources) return Collections.EMPTY_LIST @@ -240,7 +240,7 @@ class JcrGrabbitJobInstanceDao extends AbstractJcrDao implements GrabbitJobInsta */ @Override protected void ensureRootResource() { - JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + JCRUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> if (!getOrCreateResource(resolver, JOB_INSTANCE_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true)) { //create the Root Resource throw new IllegalStateException("Cannot get or create RootResource for : ${JOB_INSTANCE_ROOT}") @@ -276,7 +276,7 @@ class JcrGrabbitJobInstanceDao extends AbstractJcrDao implements GrabbitJobInsta @Override Collection<String> getJobInstancePaths(Collection<String> jobExecutionResourcePaths) { - JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + JCRUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> Collection<String> jobInstancesToRemove = [] jobExecutionResourcePaths.each { String jobExecutionResourcePath -> Resource jobExecutionResource = resolver.getResource(jobExecutionResourcePath) diff --git a/src/main/groovy/com/twcable/grabbit/spring/batch/repository/JcrGrabbitStepExecutionDao.groovy b/src/main/groovy/com/twcable/grabbit/spring/batch/repository/JcrGrabbitStepExecutionDao.groovy index d8b56e7..779b83e 100644 --- a/src/main/groovy/com/twcable/grabbit/spring/batch/repository/JcrGrabbitStepExecutionDao.groovy +++ b/src/main/groovy/com/twcable/grabbit/spring/batch/repository/JcrGrabbitStepExecutionDao.groovy @@ -17,7 +17,7 @@ package com.twcable.grabbit.spring.batch.repository import com.twcable.grabbit.DateUtil -import com.twcable.grabbit.jcr.JcrUtil +import com.twcable.grabbit.jcr.JCRUtil import com.twcable.grabbit.util.CryptoUtil import groovy.transform.CompileStatic import groovy.util.logging.Slf4j @@ -129,7 +129,7 @@ public class JcrGrabbitStepExecutionDao extends AbstractJcrDao implements Grabbi if (!jobExecution) throw new IllegalArgumentException("jobExecution == null") if (!stepExecutionId) throw new IllegalArgumentException("stepExecutionId == null") - JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + JCRUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> final Resource rootResource = getOrCreateResource(resolver, STEP_EXECUTION_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true) final stepExecutionResource = rootResource?.children?.asList()?.find { Resource resource -> final properties = resource.adaptTo(ValueMap) @@ -156,7 +156,7 @@ public class JcrGrabbitStepExecutionDao extends AbstractJcrDao implements Grabbi if (!jobExecution) throw new IllegalArgumentException("jobExecution == null") //TODO : JOB_EXECUTION's ID must exist - JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + JCRUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> final rootResource = getOrCreateResource(resolver, STEP_EXECUTION_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true) final stepExecutionResources = rootResource.children.asList().findAll { Resource resource -> final properties = resource.adaptTo(ValueMap) @@ -189,7 +189,7 @@ public class JcrGrabbitStepExecutionDao extends AbstractJcrDao implements Grabbi if (!executionId) throw new IllegalArgumentException("executionId == null") if (!execution) throw new IllegalArgumentException("execution == null") - JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + JCRUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> final rootResource = getOrCreateResource(resolver, STEP_EXECUTION_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true) final existingResource = resolver.getResource(rootResource, executionId) @@ -286,7 +286,7 @@ public class JcrGrabbitStepExecutionDao extends AbstractJcrDao implements Grabbi */ @Override protected void ensureRootResource() { - JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + JCRUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> //ResourceUtil.getOrCreateResource() if (!getOrCreateResource(resolver, STEP_EXECUTION_ROOT, NT_UNSTRUCTURED, NT_UNSTRUCTURED, true)) { //create the Root Resource @@ -301,7 +301,7 @@ public class JcrGrabbitStepExecutionDao extends AbstractJcrDao implements Grabbi @Override Collection<String> getStepExecutionPaths(Collection<String> jobExecutionResourcePaths) { - JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + JCRUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> Collection<String> stepExecutionsToRemove = [] jobExecutionResourcePaths.each { String jobExecutionResourcePath -> Resource jobExecutionResource = resolver.getResource(jobExecutionResourcePath) diff --git a/src/main/groovy/com/twcable/grabbit/spring/batch/repository/services/impl/DefaultCleanJobRepository.groovy b/src/main/groovy/com/twcable/grabbit/spring/batch/repository/services/impl/DefaultCleanJobRepository.groovy index 1de38ae..2d18288 100644 --- a/src/main/groovy/com/twcable/grabbit/spring/batch/repository/services/impl/DefaultCleanJobRepository.groovy +++ b/src/main/groovy/com/twcable/grabbit/spring/batch/repository/services/impl/DefaultCleanJobRepository.groovy @@ -16,7 +16,7 @@ package com.twcable.grabbit.spring.batch.repository.services.impl -import com.twcable.grabbit.jcr.JcrUtil +import com.twcable.grabbit.jcr.JCRUtil import com.twcable.grabbit.spring.batch.repository.JcrJobRepositoryFactoryBean import com.twcable.grabbit.spring.batch.repository.services.CleanJobRepository import groovy.transform.CompileStatic @@ -66,7 +66,7 @@ class DefaultCleanJobRepository implements CleanJobRepository { Collection<String> jobExecutionContextsToRemove = jobRepositoryFactoryBean.executionContextDao.getJobExecutionContextPaths(olderThanHoursJobExecutions) Collection<String> stepExecutionContextsToRemove = jobRepositoryFactoryBean.executionContextDao.getStepExecutionContextPaths(stepExecutionsToRemove) - JcrUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> + JCRUtil.manageResourceResolver(resourceResolverFactory) { ResourceResolver resolver -> log.debug "jobInstancesToRemove: $jobInstancesToRemove, size: ${jobInstancesToRemove.size()}" log.debug "jobExecutionsToRemove: $olderThanHoursJobExecutions, size: ${olderThanHoursJobExecutions.size()}" diff --git a/src/main/proto/node.proto b/src/main/proto/node.proto index eecb447..a419d18 100644 --- a/src/main/proto/node.proto +++ b/src/main/proto/node.proto @@ -7,6 +7,7 @@ message Node { required string name = 1; repeated Property properties = 2; repeated Node mandatoryChildNode = 3; + optional string identifier = 4; } message Property { diff --git a/src/test/groovy/com/twcable/grabbit/client/batch/steps/jcrnodes/JcrNodesWriterSpec.groovy b/src/test/groovy/com/twcable/grabbit/client/batch/steps/jcrnodes/JcrNodesWriterSpec.groovy index 09742e4..4491b0e 100644 --- a/src/test/groovy/com/twcable/grabbit/client/batch/steps/jcrnodes/JcrNodesWriterSpec.groovy +++ b/src/test/groovy/com/twcable/grabbit/client/batch/steps/jcrnodes/JcrNodesWriterSpec.groovy @@ -17,7 +17,7 @@ package com.twcable.grabbit.client.batch.steps.jcrnodes import com.twcable.grabbit.client.batch.ClientBatchJobContext -import com.twcable.grabbit.jcr.JcrUtil +import com.twcable.grabbit.jcr.JCRUtil import com.twcable.grabbit.proto.NodeProtos import com.twcable.grabbit.proto.NodeProtos.Node as ProtoNode import com.twcable.grabbit.proto.NodeProtos.Node.Builder as ProtoNodeBuilder @@ -88,7 +88,7 @@ class JcrNodesWriterSpec extends Specification { NodeProtos.Node nodeProto = nodeBuilder.build() - Session session = JcrUtil.getSession(repository().build(), "admin") + Session session = JCRUtil.getSession(repository().build(), "admin") when: ClientBatchJobContext.setSession(session) @@ -133,7 +133,7 @@ class JcrNodesWriterSpec extends Specification { ProtoNode protoNode = nodeBuilder.build() - Session session = JcrUtil.getSession(repository().build(), "admin") + Session session = JCRUtil.getSession(repository().build(), "admin") when: ClientBatchJobContext.setSession(session) @@ -155,7 +155,7 @@ class JcrNodesWriterSpec extends Specification { getNodeProto("/foo/bar/foo", "jcr:primaryType", "nt:file") ] - Session session = JcrUtil.getSession(repository().build(), "admin") + Session session = JCRUtil.getSession(repository().build(), "admin") when: ClientBatchJobContext.setSession(session) diff --git a/src/test/groovy/com/twcable/grabbit/jcr/AuthorizableProtoNodeDecoratorSpec.groovy b/src/test/groovy/com/twcable/grabbit/jcr/AuthorizableProtoNodeDecoratorSpec.groovy index 0486a54..5b9b32d 100644 --- a/src/test/groovy/com/twcable/grabbit/jcr/AuthorizableProtoNodeDecoratorSpec.groovy +++ b/src/test/groovy/com/twcable/grabbit/jcr/AuthorizableProtoNodeDecoratorSpec.groovy @@ -42,16 +42,17 @@ import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE class AuthorizableProtoNodeDecoratorSpec extends Specification { - AuthorizableProtoNodeDecorator theProtoNodeDecorator(boolean forUser, boolean hasProfile, boolean hasPreferences, Closure configuration = null){ + + AuthorizableProtoNodeDecorator theProtoNodeDecoratorForUser(boolean hasProfile, boolean hasPreferences, String id, Closure configuration = null){ ProtoNodeBuilder nodeBuilder = ProtoNode.newBuilder() - nodeBuilder.setName(forUser ? "/home/users/u/user1" : "/home/groups/g/group1") + nodeBuilder.setName('/home/users/u/user1') ProtoProperty primaryTypeProperty = ProtoProperty .newBuilder() .setName(JCR_PRIMARYTYPE) .setType(STRING) .setMultiple(false) - .addValues(ProtoValue.newBuilder().setStringValue(forUser ? 'rep:User' : 'rep:Group')) + .addValues(ProtoValue.newBuilder().setStringValue('rep:User')) .build() nodeBuilder.addProperties(primaryTypeProperty) @@ -69,7 +70,7 @@ class AuthorizableProtoNodeDecoratorSpec extends Specification { .setName('rep:authorizableId') .setType(STRING) .setMultiple(false) - .addValues(ProtoValue.newBuilder().setStringValue('authorizableID')) + .addValues(ProtoValue.newBuilder().setStringValue(id)) .build() nodeBuilder.addProperties(authorizableIdProperty) @@ -89,30 +90,85 @@ class AuthorizableProtoNodeDecoratorSpec extends Specification { .setMultiple(false) .addValues(ProtoValue.newBuilder().setStringValue('nt:unstructured')) .build() - nodeBuilder.addProperties(authorizableCategory) + if(hasPreferences) { - ProtoNode preferenceNode = ProtoNode.newBuilder() - .setName("${nodeBuilder.getName()}/preferences") - .addProperties(simplePrimaryType) - .build() + ProtoNode preferenceNode = ProtoNode + .newBuilder() + .setName("${nodeBuilder.getName()}/preferences") + .addProperties(simplePrimaryType) + .build() nodeBuilder.addMandatoryChildNode(preferenceNode) } if(hasProfile) { ProtoNode profileNode = ProtoNode - .newBuilder() - .setName("${nodeBuilder.getName()}/profile") - .addProperties(simplePrimaryType) - .build() + .newBuilder() + .setName("${nodeBuilder.getName()}/profile") + .addProperties(simplePrimaryType) + .build() nodeBuilder.addMandatoryChildNode(profileNode) } + final properties = [new ProtoPropertyDecorator(primaryTypeProperty), new ProtoPropertyDecorator(disabledProperty), new ProtoPropertyDecorator(authorizableIdProperty), new ProtoPropertyDecorator(authorizableCategory)] return GroovySpy(AuthorizableProtoNodeDecorator, constructorArgs: [nodeBuilder.build(), properties], configuration) } + AuthorizableProtoNodeDecorator theProtoNodeDecoratorForGroup(Closure configuration = null){ + ProtoNodeBuilder nodeBuilder = ProtoNode.newBuilder() + nodeBuilder.setName('/home/groups/g/group1') + + ProtoProperty primaryTypeProperty = ProtoProperty + .newBuilder() + .setName(JCR_PRIMARYTYPE) + .setType(STRING) + .setMultiple(false) + .addValues(ProtoValue.newBuilder().setStringValue('rep:Group')) + .build() + nodeBuilder.addProperties(primaryTypeProperty) + + ProtoProperty authorizableIdProperty = ProtoProperty + .newBuilder() + .setName('rep:authorizableId') + .setType(STRING) + .setMultiple(false) + .addValues(ProtoValue.newBuilder().setStringValue('authorizableID')) + .build() + nodeBuilder.addProperties(authorizableIdProperty) + + ProtoProperty membershipProperty = ProtoProperty + .newBuilder() + .setName('rep:members') + .setType(STRING) + .setMultiple(false) + .addValues(ProtoValue.newBuilder().setStringValue('member-group-id')) + .build() + nodeBuilder.addProperties(membershipProperty) + + + ProtoNodeBuilder memberNodeBuilder = ProtoNode.newBuilder() + memberNodeBuilder.setName('/home/groups/g/anothergroup') + + ProtoProperty userPrimaryTypeProperty = ProtoProperty + .newBuilder() + .setName(JCR_PRIMARYTYPE) + .setType(STRING) + .setMultiple(false) + .addValues(ProtoValue.newBuilder().setStringValue('rep:Group')) + .build() + memberNodeBuilder.addProperties(userPrimaryTypeProperty) + + memberNodeBuilder.setIdentifier('member-group-id') + + nodeBuilder.addMandatoryChildNode(memberNodeBuilder.build()) + + final properties = [new ProtoPropertyDecorator(primaryTypeProperty), new ProtoPropertyDecorator(authorizableIdProperty), new ProtoPropertyDecorator(membershipProperty)] + return GroovySpy(AuthorizableProtoNodeDecorator, constructorArgs: [nodeBuilder.build(), properties], configuration) + } + + def "Throws an InsufficientGrabbitPrivilegeException if JVM permissions are not present"() { when: - final protoNodeDecorator = theProtoNodeDecorator(false, false, false) { + final protoNodeDecorator = theProtoNodeDecoratorForGroup { it.getSecurityManager() >> Mock(SecurityManager) { it.checkPermission(permission) >> { throw new SecurityException() @@ -130,6 +186,42 @@ class AuthorizableProtoNodeDecoratorSpec extends Specification { } + def "Does not attempt to modify the administrator"() { + when: + final adminNode = Mock(Node) { + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + it.getString() >> 'rep:User' + } + getProperties() >> Mock(PropertyIterator) { + it.toList() >> [] + } + it.getPrimaryNodeType() >> Mock(NodeType) { + it.canSetProperty(_, _) >> false + } + } + final session = Mock(Session) { + it.getNode('/home/users/m/a') >> adminNode + } + final protoNodeDecorator = theProtoNodeDecoratorForUser(false, false, 'admin') { + it.getSecurityManager() >> Mock(SecurityManager) { + it.checkPermission(_) >> { + return + } + } + it.getUserManager(session) >> Mock(UserManager) { + it.getAuthorizable('admin') >> Mock(Authorizable) { + it.getPath() >> '/home/users/m/a' + } + } + } + + final JCRNodeDecorator resultNode = protoNodeDecorator.writeToJcr(session) + + then: + (resultNode as Node) == adminNode + } + + def "Passes security check if all JVM permissions are present"() { when: final session = Mock(Session) { @@ -145,7 +237,7 @@ class AuthorizableProtoNodeDecoratorSpec extends Specification { } } } - final protoNodeDecorator = theProtoNodeDecorator(false, false, false) { + final protoNodeDecorator = theProtoNodeDecoratorForGroup { it.getSecurityManager() >> Mock(SecurityManager) { it.checkPermission(permission) >> { return @@ -156,6 +248,9 @@ class AuthorizableProtoNodeDecoratorSpec extends Specification { it.getPath() >> 'authorizablePath' } } + it.writeMandatoryPieces(_, _) >> [ + Mock(JCRNodeDecorator) + ] } protoNodeDecorator.writeToJcr(session) @@ -183,13 +278,16 @@ class AuthorizableProtoNodeDecoratorSpec extends Specification { } } } - final protoNodeDecorator = theProtoNodeDecorator(false, false, false) { + final protoNodeDecorator = theProtoNodeDecoratorForGroup { it.getSecurityManager() >> null it.getUserManager(session) >> Mock(UserManager) { it.createGroup(_, _, _) >> Mock(Group) { it.getPath() >> 'authorizablePath' } } + it.writeMandatoryPieces(_, _) >> [ + Mock(JCRNodeDecorator) + ] } protoNodeDecorator.writeToJcr(session) @@ -201,7 +299,7 @@ class AuthorizableProtoNodeDecoratorSpec extends Specification { def "getSecurityManager() will retrieve the JVM's security manager"() { given: - final protoNodeDecorator = theProtoNodeDecorator(false, false, false) + final protoNodeDecorator = theProtoNodeDecoratorForGroup() expect: System.getSecurityManager() == protoNodeDecorator.getSecurityManager() @@ -229,7 +327,7 @@ class AuthorizableProtoNodeDecoratorSpec extends Specification { 1 * it.setProperty('cq:authorizableCategory', new StringValue('mcm')) it.getPath() >> 'newUserPath' } - final protoNodeDecorator = theProtoNodeDecorator(true, false, false) { + final protoNodeDecorator = theProtoNodeDecoratorForUser(false, false, 'authorizableID') { it.getName() >> '/home/users/auth_folder/user' it.getSecurityManager() >> null it.setPasswordForUser(newUser, session) >> { @@ -265,16 +363,25 @@ class AuthorizableProtoNodeDecoratorSpec extends Specification { final session = Mock(Session) { it.getNode('newGroupPath') >> node } + final member = Mock(Authorizable) final newGroup = Mock(Group) { it.getPath() >> 'newGroupPath' + 1 * it.addMember(member) } - final protoNodeDecorator = theProtoNodeDecorator(false, false, false) { + final protoNodeDecorator = theProtoNodeDecoratorForGroup { it.getName() >> '/home/groups/auth_folder/group' it.getSecurityManager() >> null it.getUserManager(session) >> Mock(UserManager) { it.getAuthorizable('authorizableID') >> null 1 * it.createGroup('authorizableID', new AuthorizablePrincipal('authorizableID'), '/home/groups/auth_folder') >> newGroup } + it.writeMandatoryPieces(session, 'newGroupPath') >> [ + Mock(JCRNodeDecorator) { + it.isAuthorizableType() >> true + it.getTransferredID() >> 'member-group-id' + it.asType(Authorizable) >> member + } + ] } when: @@ -301,15 +408,18 @@ class AuthorizableProtoNodeDecoratorSpec extends Specification { final session = Mock(Session) { it.getNode('/home/users/u/newuser') >> node } - final protoNodeDecorator = theProtoNodeDecorator(false, exists, false) { + final protoNodeDecorator = theProtoNodeDecoratorForUser(exists, false, 'authorizableID') { it.getSecurityManager() >> null it.getUserManager(session) >> Mock(UserManager) { - it.getAuthorizable('authorizableID') >> Mock(Authorizable) - it.createGroup('authorizableID', _, _) >> Mock(Group) { + it.getAuthorizable('authorizableID') >> Mock(Authorizable) { + it.declaredMemberOf() >> [].iterator() + } + it.createUser('authorizableID', _, _, _) >> Mock(User) { it.getPath() >> '/home/users/u/newuser' + it.declaredMemberOf() >> [].iterator() } } - (exists ? 1 : 0) * it.createFrom(_ as ProtoNode, '/home/users/u/newuser/profile') >> Mock(ProtoNodeDecorator) + (exists ? 1 : 0) * it._createFrom(_ as ProtoNode, '/home/users/u/newuser/profile') >> Mock(ProtoNodeDecorator) } then: @@ -337,15 +447,18 @@ class AuthorizableProtoNodeDecoratorSpec extends Specification { final session = Mock(Session) { it.getNode('/home/users/u/newuser') >> node } - final protoNodeDecorator = theProtoNodeDecorator(false, false, exists) { + final protoNodeDecorator = theProtoNodeDecoratorForUser(false, exists, 'authorizableID') { it.getSecurityManager() >> null it.getUserManager(session) >> Mock(UserManager) { - it.getAuthorizable('authorizableID') >> Mock(Authorizable) - it.createGroup('authorizableID', _, _) >> Mock(Group) { + it.getAuthorizable('authorizableID') >> Mock(Authorizable) { + it.declaredMemberOf() >> [].iterator() + } + it.createUser('authorizableID', _, _, _) >> Mock(User) { it.getPath() >> '/home/users/u/newuser' + it.declaredMemberOf() >> [].iterator() } } - (exists ? 1 : 0) * it.createFrom(_ as ProtoNode, '/home/users/u/newuser/preferences') >> Mock(ProtoNodeDecorator) + (exists ? 1 : 0) * it._createFrom(_ as ProtoNode, '/home/users/u/newuser/preferences') >> Mock(ProtoNodeDecorator) } then: @@ -355,4 +468,41 @@ class AuthorizableProtoNodeDecoratorSpec extends Specification { where: exists << [false, true] } + + def "When updating an authorizable, remove old authorizable from declared groups, and add updated authorizable back to groups"() { + when: + final node = Mock(Node) { + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + it.getString() >> 'rep:User' + } + getProperties() >> Mock(PropertyIterator) { + it.toList() >> [] + } + it.getPrimaryNodeType() >> Mock(NodeType) { + it.canSetProperty(_, _) >> false + } + } + final session = Mock(Session) { + it.getNode('/home/users/u/newuser') >> node + } + final group = Mock(Group) { + 1 * it.removeMember(_) + 1 * it.addMember(_) + } + final protoNodeDecorator = theProtoNodeDecoratorForUser(false, false, 'authorizableID') { + it.getUserManager(session) >> Mock(UserManager) { + it.getAuthorizable('authorizableID') >> Mock(Authorizable) { + it.declaredMemberOf() >> [group].iterator() + } + it.createUser('authorizableID', _, _, _) >> Mock(User) { + it.getPath() >> '/home/users/u/newuser' + it.declaredMemberOf() >> [].iterator() + } + } + } + + then: + protoNodeDecorator.writeToJcr(session) + } + } diff --git a/src/test/groovy/com/twcable/grabbit/jcr/DefaultProtoNodeDecoratorSpec.groovy b/src/test/groovy/com/twcable/grabbit/jcr/DefaultProtoNodeDecoratorSpec.groovy index 07e9ec9..def8131 100644 --- a/src/test/groovy/com/twcable/grabbit/jcr/DefaultProtoNodeDecoratorSpec.groovy +++ b/src/test/groovy/com/twcable/grabbit/jcr/DefaultProtoNodeDecoratorSpec.groovy @@ -20,62 +20,81 @@ import com.twcable.grabbit.proto.NodeProtos import com.twcable.grabbit.proto.NodeProtos.Node as ProtoNode import com.twcable.grabbit.proto.NodeProtos.Node.Builder as ProtoNodeBuilder import com.twcable.grabbit.proto.NodeProtos.Property as ProtoProperty -import javax.jcr.nodetype.NodeType -import spock.lang.Specification - import javax.jcr.Node import javax.jcr.Property import javax.jcr.PropertyIterator import javax.jcr.Session +import javax.jcr.nodetype.NodeType +import spock.lang.Specification + +import static javax.jcr.PropertyType.REFERENCE import static javax.jcr.PropertyType.STRING +import static javax.jcr.PropertyType.WEAKREFERENCE import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE - class DefaultProtoNodeDecoratorSpec extends Specification { ProtoNode decoratedProtoNode ProtoProperty mixinProperty ProtoProperty someOtherProperty + ProtoProperty referenceProperty + ProtoProperty referencePropertyTwo - def setup() { - ProtoNodeBuilder nodeBuilder = ProtoNode.newBuilder() - nodeBuilder.setName("somenode") + def setup() { ProtoProperty primaryTypeProperty = NodeProtos.Property - .newBuilder() - .setName(JCR_PRIMARYTYPE) - .setType(STRING) - .setMultiple(false) - .addValues(NodeProtos.Value.newBuilder().setStringValue(JcrConstants.NT_UNSTRUCTURED)) - .build() - nodeBuilder.addProperties(primaryTypeProperty) + .newBuilder() + .setName(JCR_PRIMARYTYPE) + .setType(STRING) + .setMultiple(false) + .addValues(NodeProtos.Value.newBuilder().setStringValue(JcrConstants.NT_UNSTRUCTURED)) + .build() mixinProperty = NodeProtos.Property - .newBuilder() - .setName(JCR_MIXINTYPES) - .setType(STRING) - .setMultiple(true) - .addAllValues( + .newBuilder() + .setName(JCR_MIXINTYPES) + .setType(STRING) + .setMultiple(true) + .addAllValues( [ - NodeProtos.Value.newBuilder().setStringValue("somemixintype").build(), - NodeProtos.Value.newBuilder().setStringValue("unwritablemixin").build() + NodeProtos.Value.newBuilder().setStringValue("somemixintype").build(), + NodeProtos.Value.newBuilder().setStringValue("unwritablemixin").build() ] - ) - .build() - nodeBuilder.addProperties(mixinProperty) - + ).build() someOtherProperty = NodeProtos.Property + .newBuilder() + .setName("someproperty") + .setType(STRING) + .setMultiple(false) + .addValues(NodeProtos.Value.newBuilder().setStringValue("somevalue")) + .build() + + referenceProperty = NodeProtos.Property + .newBuilder() + .setName('jcr:uuid') + .setType(WEAKREFERENCE) + .setMultiple(false) + .addValues(NodeProtos.Value.newBuilder().setStringValue('21232f29-7a57-35a7-8389-4a0e4a801fc3')) + .build() + + referencePropertyTwo = NodeProtos.Property .newBuilder() - .setName("someproperty") - .setType(STRING) + .setName('jcr:uuid') + .setType(REFERENCE) .setMultiple(false) - .addValues(NodeProtos.Value.newBuilder().setStringValue("somevalue")) + .addValues(NodeProtos.Value.newBuilder().setStringValue('41232f29-7a57-35a7-8389-4a0e4a801fc6')) .build() - nodeBuilder.addProperties(someOtherProperty) + ProtoNodeBuilder nodeBuilder = ProtoNode.newBuilder() + nodeBuilder.setName("somenode") + nodeBuilder.addProperties(primaryTypeProperty) + nodeBuilder.addProperties(mixinProperty) + nodeBuilder.addProperties(someOtherProperty) + nodeBuilder.addProperties(referenceProperty) + nodeBuilder.addProperties(referencePropertyTwo) decoratedProtoNode = nodeBuilder.build() } @@ -110,8 +129,7 @@ class DefaultProtoNodeDecoratorSpec extends Specification { final Collection<ProtoPropertyDecorator> properties = protoNodeDecorator.getWritableProperties() then: - properties.size() == 1 - properties[0].stringValue == "somevalue" + properties.size() == 3 } def "Can write this ProtoNodeDecorator to the JCR"() { @@ -133,12 +151,21 @@ class DefaultProtoNodeDecoratorSpec extends Specification { } final protoPropertyDecorators = [ new ProtoPropertyDecorator(mixinProperty), - new ProtoPropertyDecorator(someOtherProperty) + new ProtoPropertyDecorator(someOtherProperty), + new ProtoPropertyDecorator(referenceProperty), + new ProtoPropertyDecorator(referencePropertyTwo) ] final protoNodeDecorator = Spy(DefaultProtoNodeDecorator, constructorArgs: [decoratedProtoNode, protoPropertyDecorators, null]) { getOrCreateNode(session) >> { return jcrNodeRepresentation } + writeMandatoryPieces(session, 'somenode') >> [ + Mock(JCRNodeDecorator) { + isReferenceable() >> true + getTransferredID() >> '21232f29-7a57-35a7-8389-4a0e4a801fc3' + getIdentifier() >> '31232f29-7a57-35a7-8389-4a0e4a801fc4' + } + ] } final jcrNodeDecorator = protoNodeDecorator.writeToJcr(session) diff --git a/src/test/groovy/com/twcable/grabbit/jcr/JCRNodeDecoratorSpec.groovy b/src/test/groovy/com/twcable/grabbit/jcr/JCRNodeDecoratorSpec.groovy index 5d886a7..04b05c0 100644 --- a/src/test/groovy/com/twcable/grabbit/jcr/JCRNodeDecoratorSpec.groovy +++ b/src/test/groovy/com/twcable/grabbit/jcr/JCRNodeDecoratorSpec.groovy @@ -23,11 +23,14 @@ import javax.jcr.NodeIterator import javax.jcr.Property import javax.jcr.PropertyIterator import javax.jcr.RepositoryException +import javax.jcr.Session import javax.jcr.Value import javax.jcr.nodetype.ItemDefinition import javax.jcr.nodetype.NodeDefinition import javax.jcr.nodetype.NodeType import javax.jcr.nodetype.PropertyDefinition +import org.apache.jackrabbit.api.security.user.Authorizable +import org.apache.jackrabbit.api.security.user.UserManager import org.apache.jackrabbit.commons.iterator.PropertyIteratorAdapter import spock.lang.Shared import spock.lang.Specification @@ -37,9 +40,12 @@ import spock.lang.Unroll import static com.twcable.grabbit.jcr.JCRNodeDecorator.NoRootInclusionPolicy import static com.twcable.grabbit.testutil.StubInputStream.inputStream import static javax.jcr.PropertyType.BINARY +import static javax.jcr.PropertyType.REFERENCE +import static javax.jcr.PropertyType.WEAKREFERENCE import static org.apache.jackrabbit.JcrConstants.JCR_CREATED import static org.apache.jackrabbit.JcrConstants.JCR_LASTMODIFIED import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE +import static org.apache.jackrabbit.JcrConstants.MIX_REFERENCEABLE import static org.apache.jackrabbit.oak.spi.security.authorization.accesscontrol.AccessControlConstants.NT_REP_ACL import static org.apache.jackrabbit.oak.spi.security.authorization.accesscontrol.AccessControlConstants.NT_REP_GRANT_ACE @@ -94,6 +100,8 @@ class JCRNodeDecoratorSpec extends Specification { } } } + isNodeType(MIX_REFERENCEABLE) >> true + getIdentifier() >> "node_id" getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { getString() >> "rep:SystemUser" } @@ -149,6 +157,7 @@ class JCRNodeDecoratorSpec extends Specification { then: protoNode.getName() == '/some/path' + protoNode.getIdentifier() == "node_id" protoNode.getPropertiesCount() == 1 } @@ -278,7 +287,7 @@ class JCRNodeDecoratorSpec extends Specification { } - def "getRequiredChildNodes()"() { + def "getRequiredChildNodes() when the node has mandatory children"() { given: Node nodeWithMandatoryChildren = Mock(Node) { getNodes() >> Mock(NodeIterator) { @@ -303,16 +312,7 @@ class JCRNodeDecoratorSpec extends Specification { } } - Node authorizableNode = Mock(Node) { - getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { - getString() >> 'rep:User' - } - getProperties() >> Mock(PropertyIterator) { - toList() >> [] - } - } - - when: "The node has mandatory children" + when: final nodeDecorator = Spy(JCRNodeDecorator, constructorArgs: [nodeWithMandatoryChildren]) { hasMandatoryChildNodes() >> true isAuthorizableType() >> false @@ -320,11 +320,22 @@ class JCRNodeDecoratorSpec extends Specification { then: nodeDecorator.getRequiredChildNodes().size() == 1 + } + - and: "The node has authorizable pieces" + def "getRequiredChildNodes() when the node has authorizable pieces"() { + given: + Node authorizableNode = Mock(Node) { + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + getString() >> 'rep:User' + } + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } + } when: - final otherNodeDecorator = Spy(JCRNodeDecorator, constructorArgs: [authorizableNode]) { + final nodeDecorator = Spy(JCRNodeDecorator, constructorArgs: [authorizableNode]) { hasMandatoryChildNodes() >> false isAuthorizableType() >> true getChildNodeIterator() >> Mock(Iterator) { @@ -362,7 +373,7 @@ class JCRNodeDecoratorSpec extends Specification { getProperties() >> Mock(PropertyIterator) { toList() >> [] } - } >> + } >> Mock(Node) { getName() >> "/home/users/u/user/rep:policy" getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { @@ -385,13 +396,22 @@ class JCRNodeDecoratorSpec extends Specification { } then: - otherNodeDecorator.getRequiredChildNodes().size() == 2 + nodeDecorator.getRequiredChildNodes().size() == 2 + } + - and: "If no child nodes, getRequiredChildNodes() returns an empty collection" + def "getRequiredChildNodes() if no child nodes, returns an empty collection"() { + given: + final node = Mock(Node) { + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } + } when: - final yetAnotherNodeDecorator = Spy(JCRNodeDecorator, constructorArgs: [nodeWithMandatoryChildren]) { + final yetAnotherNodeDecorator = Spy(JCRNodeDecorator, constructorArgs: [node]) { hasMandatoryChildNodes() >> false + isRepACLType() >> false isAuthorizableType() >> false } @@ -400,6 +420,97 @@ class JCRNodeDecoratorSpec extends Specification { } + def "getRequiredChildNodes() returns referenced nodes"() { + given: + final referenceNode = Mock(Node) { + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } + } + final node = Mock(Node) { + getPath() >> "/some/node/path" + getSession() >> Mock(Session) { + getNodeByIdentifier({["reference_uuid_one", "reference_uuid_two", "reference_uuid_four"].contains(it)}) >> referenceNode + getNodeByIdentifier({!["reference_uuid_one", "reference_uuid_two", "reference_uuid_four"].contains(it)}) >> { + throw new ItemNotFoundException("Node not found") + } + } + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + getString() >> 'nt:unstructured' + } + getPrimaryNodeType() >> Mock(NodeType) { + getChildNodeDefinitions() >> [ + Mock(NodeDefinition) { + isMandatory() >> true + } + ].toArray() + } + getProperties() >> Mock(PropertyIterator) { + hasNext() >>> true >> true >> false + next() >> + Mock(Property) { + getType() >> WEAKREFERENCE + isMultiple() >> true + getDefinition() >> Mock(PropertyDefinition) { + isProtected() >> false + } + getValues() >> ([ + Mock(Value) { + getString() >> "reference_uuid_one" + }, + Mock(Value) { + getString() >> "reference_uuid_two" + }, + Mock(Value) { + getString() >> "reference_uuid_three" + } + ].toArray() as Value[]) + } >> + Mock(Property) { + getType() >> REFERENCE + isMultiple() >> false + getDefinition() >> Mock(PropertyDefinition) { + isProtected() >> true + } + getValue() >> Mock(Value) { + getString() >> "reference_uuid_five" + } + } + } + getNodes() >> Mock(NodeIterator) { + hasNext() >>> true >> false + next() >> Mock(Node) { + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + getString() >> 'nt:unstructured' + } + getProperties() >> Mock(PropertyIterator) { + hasNext() >>> true >> false + next() >> Mock(Property) { + getType() >> REFERENCE + isMultiple() >> false + getDefinition() >> Mock(PropertyDefinition) { + isProtected() >> false + } + getValue() >> Mock(Value) { + getString() >> "reference_uuid_four" + } + } + } + getDefinition() >> Mock(NodeDefinition) { + isMandatory() >> true + } + } + } + } + + when: + final nodeDecorator = new JCRNodeDecorator(node) + + then: + nodeDecorator.getRequiredChildNodes().size() == 4 + } + + def "Can adapt the decorator back to the wrapped node"() { given: final node = Mock(Node) { @@ -417,6 +528,30 @@ class JCRNodeDecoratorSpec extends Specification { (nodeDecorator as JCRNodeDecorator) == nodeDecorator } + + def "Can adapt the decorator to an authorizable if appropriate"() { + given: + final node = Mock(Node) { + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + getString() >> 'rep:User' + } + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } + getPath() >> '/home/users/user' + } + final returnedAuthorizable = Mock(Authorizable) + final nodeDecorator = Spy(JCRNodeDecorator, constructorArgs: [node]) { + getUserManager(_) >> Mock(UserManager) { + 1 * getAuthorizableByPath('/home/users/user') >> returnedAuthorizable + } + } + + expect: + (nodeDecorator as Authorizable) == returnedAuthorizable + } + + def "Get modified date for a node"() { given: final node = Mock(Node) { diff --git a/src/test/groovy/com/twcable/grabbit/jcr/JcrPropertyDecoratorSpec.groovy b/src/test/groovy/com/twcable/grabbit/jcr/JcrPropertyDecoratorSpec.groovy index 8b031d4..40e87f3 100644 --- a/src/test/groovy/com/twcable/grabbit/jcr/JcrPropertyDecoratorSpec.groovy +++ b/src/test/groovy/com/twcable/grabbit/jcr/JcrPropertyDecoratorSpec.groovy @@ -16,23 +16,26 @@ package com.twcable.grabbit.jcr -import javax.jcr.Property +import javax.jcr.Property as JCRProperty import javax.jcr.nodetype.PropertyDefinition import spock.lang.Specification import spock.lang.Unroll +import static javax.jcr.PropertyType.BINARY +import static javax.jcr.PropertyType.REFERENCE +import static javax.jcr.PropertyType.WEAKREFERENCE import static org.apache.jackrabbit.JcrConstants.JCR_LASTMODIFIED import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE @SuppressWarnings("GroovyAssignabilityCheck") -class JcrPropertyDecoratorSpec extends Specification { +class JCRPropertyDecoratorSpec extends Specification { @Unroll def "check if property is transferable"() { given: - Property property = Mock(Property) { + JCRProperty property = Mock(JCRProperty) { getName() >> propertyName getDefinition() >> Mock(PropertyDefinition) { isProtected() >> protectedFlag @@ -44,7 +47,7 @@ class JcrPropertyDecoratorSpec extends Specification { } when: - final propertyDecorator = new JcrPropertyDecorator(property, nodeOwner) + final propertyDecorator = new JCRPropertyDecorator(property, nodeOwner) then: expectedOutput == propertyDecorator.isTransferable() @@ -61,4 +64,22 @@ class JcrPropertyDecoratorSpec extends Specification { 'rep:privileges' | true | true | false | true } + + def "isReferenceType()"() { + when: + final JCRProperty property = Mock(JCRProperty) { + getType() >> type + } + + final jcrPropertyDecorator = new JCRPropertyDecorator(property, Mock(JCRNodeDecorator)) + + then: + jcrPropertyDecorator.isReferenceType() == expectedValue + + where: + type | expectedValue + REFERENCE | true + WEAKREFERENCE | true + BINARY | false + } }