diff --git a/README.adoc b/README.adoc index 3ee3da9..9bf4389 100644 --- a/README.adoc +++ b/README.adoc @@ -37,7 +37,7 @@ image:https://api.bintray.com/packages/twcable/aem/Grabbit/images/download.svg[t * Grabbit Development -** link:{docsDir}/Prerequisites.adoc[Prerequisites] +** link:{docsDir}/GettingStarted.adoc[Getting Started] ** link:{docsDir}/Building.adoc[Building from Source] ** link:{docsDir}/RELEASING.adoc[Releasing A New Version] diff --git a/docs/Prerequisites.adoc b/docs/GettingStarted.adoc similarity index 84% rename from docs/Prerequisites.adoc rename to docs/GettingStarted.adoc index 73c7cf1..dda7642 100644 --- a/docs/Prerequisites.adoc +++ b/docs/GettingStarted.adoc @@ -1,3 +1,7 @@ +== Getting Started + +The JCR 2.0 Specification (JSR-283) is included under docs for quick reference + == Prerequisites === Installing Protocol Buffers Compiler diff --git a/docs/Running.adoc b/docs/Running.adoc index c8f01b0..ca5ab33 100644 --- a/docs/Running.adoc +++ b/docs/Running.adoc @@ -182,3 +182,19 @@ 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/docs/jcr-spec.pdf b/docs/jcr-spec.pdf new file mode 100644 index 0000000..1152363 Binary files /dev/null and b/docs/jcr-spec.pdf differ diff --git a/gradle.properties b/gradle.properties index d937a75..25a2624 100644 --- a/gradle.properties +++ b/gradle.properties @@ -33,6 +33,7 @@ jcr_version = 2.0 jms_version = 3.1.1 jsr305_version = 2.0.0 logback_version = 1.0.4 +oak_version = 1.2.2 objenesis_version = 2.1 protobuf_gradle_plugin_version = 0.9.1 protobuf_version = 2.6.1 @@ -40,6 +41,7 @@ scr_annotations_version = 1.7.0 servlet_api_version = 2.5 slf4j_version = 1.7.6 sling_api_version = 2.9.0 +sling_base_version = 2.2.2 sling_commons_testing_version = 2.0.12 sling_commons_version = 2.2.0 sling_event_version = 3.1.4 diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index 332bf76..5b72953 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -19,6 +19,7 @@ dependencies { // Apache Sling libraries compile "org.apache.sling:org.apache.sling.api:${sling_api_version}" + compile "org.apache.sling:org.apache.sling.jcr.base:${sling_base_version}" compile "org.apache.sling:org.apache.sling.jcr.resource:${sling_jcr_resource_version}" // Apache Felix libraries @@ -33,6 +34,8 @@ dependencies { // Working with the JCR compile "javax.jcr:jcr:${jcr_version}" compile "org.apache.jackrabbit:jackrabbit-jcr-commons:${jackrabbit_version}" + compile "org.apache.jackrabbit:jackrabbit-api:${jackrabbit_version}" + compile "org.apache.jackrabbit:oak-core:${oak_version}" compile "org.apache.sling:org.apache.sling.jcr.api:${sling_commons_version}" // Logging diff --git a/gradle/packageExclusions.gradle b/gradle/packageExclusions.gradle index 5adfb2a..42267a3 100644 --- a/gradle/packageExclusions.gradle +++ b/gradle/packageExclusions.gradle @@ -29,11 +29,14 @@ configurations.cq_package { exclude group: 'org.apache.felix', module: 'org.osgi.compendium' exclude group: 'org.apache.commons', module: 'commons-lang3' - exclude group: 'org.apache.jackrabbit', module:'jackrabbit-jcr-commons' + exclude group: 'org.apache.jackrabbit', module: 'jackrabbit-jcr-commons' + exclude group: 'org.apache.jackrabbit', module: 'jackrabbit-api' + exclude group: 'org.apache.jackrabbit', module: 'oak-core' exclude group: 'commons-io', module: 'commons-io' //Exclude Apache Sling Libraries exclude group: 'org.apache.sling', module: 'org.apache.sling.api' + exclude group: 'org.apache.sling', module: 'org.apache.sling.jcr.base' exclude group: 'org.apache.sling', module:'org.apache.sling.jcr.resource' exclude group: 'org.apache.sling', module: 'org.apache.sling.jcr.api' diff --git a/src/main/content/SLING-INF/content/apps/grabbit/config/org.apache.sling.commons.log.LogManager.factory.config-com.twcable.grabbit.client.batch.xml b/src/main/content/SLING-INF/content/apps/grabbit/config/org.apache.sling.commons.log.LogManager.factory.config-com.twcable.grabbit.client.batch.xml index e7739e2..e2557f2 100644 --- a/src/main/content/SLING-INF/content/apps/grabbit/config/org.apache.sling.commons.log.LogManager.factory.config-com.twcable.grabbit.client.batch.xml +++ b/src/main/content/SLING-INF/content/apps/grabbit/config/org.apache.sling.commons.log.LogManager.factory.config-com.twcable.grabbit.client.batch.xml @@ -14,7 +14,7 @@ org.apache.sling.commons.log.names - com.twcable.grabbit.client.batch + com.twcable.grabbit.client String diff --git a/src/main/content/SLING-INF/content/apps/grabbit/config/org.apache.sling.commons.log.LogManager.factory.config-com.twcable.grabbit.server.batch.xml b/src/main/content/SLING-INF/content/apps/grabbit/config/org.apache.sling.commons.log.LogManager.factory.config-com.twcable.grabbit.server.batch.xml index 9fe6384..b5750d7 100644 --- a/src/main/content/SLING-INF/content/apps/grabbit/config/org.apache.sling.commons.log.LogManager.factory.config-com.twcable.grabbit.server.batch.xml +++ b/src/main/content/SLING-INF/content/apps/grabbit/config/org.apache.sling.commons.log.LogManager.factory.config-com.twcable.grabbit.server.batch.xml @@ -14,7 +14,7 @@ org.apache.sling.commons.log.names - com.twcable.grabbit.server.batch + com.twcable.grabbit.server String diff --git a/src/main/groovy/com/twcable/grabbit/client/batch/steps/jcrnodes/JcrNodesReader.groovy b/src/main/groovy/com/twcable/grabbit/client/batch/steps/jcrnodes/JcrNodesReader.groovy index d33a9b1..b5ed0ba 100644 --- a/src/main/groovy/com/twcable/grabbit/client/batch/steps/jcrnodes/JcrNodesReader.groovy +++ b/src/main/groovy/com/twcable/grabbit/client/batch/steps/jcrnodes/JcrNodesReader.groovy @@ -18,6 +18,7 @@ package com.twcable.grabbit.client.batch.steps.jcrnodes import com.twcable.grabbit.client.batch.ClientBatchJobContext import com.twcable.grabbit.proto.NodeProtos +import com.twcable.grabbit.proto.NodeProtos.Node as ProtoNode import groovy.transform.CompileStatic import groovy.util.logging.Slf4j import org.springframework.batch.item.ItemReader @@ -32,11 +33,11 @@ import org.springframework.batch.item.UnexpectedInputException @Slf4j @CompileStatic @SuppressWarnings("GrMethodMayBeStatic") -class JcrNodesReader implements ItemReader { +class JcrNodesReader implements ItemReader { @Override NodeProtos.Node read() throws Exception, UnexpectedInputException, ParseException, NonTransientResourceException { - NodeProtos.Node nodeProto = NodeProtos.Node.parseDelimitedFrom(theInputStream()) + ProtoNode nodeProto = ProtoNode.parseDelimitedFrom(theInputStream()) if (!nodeProto) { log.info "Received all data from Server" return null diff --git a/src/main/groovy/com/twcable/grabbit/client/batch/steps/jcrnodes/JcrNodesWriter.groovy b/src/main/groovy/com/twcable/grabbit/client/batch/steps/jcrnodes/JcrNodesWriter.groovy index 85fe218..8d62167 100644 --- a/src/main/groovy/com/twcable/grabbit/client/batch/steps/jcrnodes/JcrNodesWriter.groovy +++ b/src/main/groovy/com/twcable/grabbit/client/batch/steps/jcrnodes/JcrNodesWriter.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.JcrNodeDecorator +import com.twcable.grabbit.jcr.JCRNodeDecorator import com.twcable.grabbit.jcr.ProtoNodeDecorator import com.twcable.grabbit.proto.NodeProtos.Node as ProtoNode import groovy.transform.CompileStatic @@ -88,14 +88,8 @@ class JcrNodesWriter implements ItemWriter, ItemWriteListener { } private static void writeToJcr(ProtoNode nodeProto, Session session) { - JcrNodeDecorator jcrNode = new ProtoNodeDecorator(nodeProto).writeToJcr(session) + JCRNodeDecorator jcrNode = ProtoNodeDecorator.createFrom(nodeProto).writeToJcr(session) jcrNode.setLastModified() - // This will processed all mandatory child nodes only - if(nodeProto.mandatoryChildNodeList && nodeProto.mandatoryChildNodeList.size() > 0) { - for(ProtoNode childNode: nodeProto.mandatoryChildNodeList) { - writeToJcr(childNode, session) - } - } } private Session theSession() { diff --git a/src/main/groovy/com/twcable/grabbit/jcr/AuthorizableProtoNodeDecorator.groovy b/src/main/groovy/com/twcable/grabbit/jcr/AuthorizableProtoNodeDecorator.groovy new file mode 100644 index 0000000..09f13e3 --- /dev/null +++ b/src/main/groovy/com/twcable/grabbit/jcr/AuthorizableProtoNodeDecorator.groovy @@ -0,0 +1,266 @@ +/* + * Copyright 2015 Time Warner Cable, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.twcable.grabbit.jcr + +import com.twcable.grabbit.proto.NodeProtos.Node as ProtoNode +import com.twcable.grabbit.security.AuthorizablePrincipal +import com.twcable.grabbit.security.InsufficientGrabbitPrivilegeException +import com.twcable.grabbit.util.CryptoUtil +import groovy.transform.CompileStatic +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 +import org.apache.jackrabbit.api.security.user.Group +import org.apache.jackrabbit.api.security.user.User +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. + */ +@CompileStatic +@Slf4j +class AuthorizableProtoNodeDecorator extends ProtoNodeDecorator { + + + protected AuthorizableProtoNodeDecorator(@Nonnull ProtoNode node, @Nonnull Collection protoProperties) { + this.innerProtoNode = node + this.protoProperties = protoProperties + } + + + @Override + JCRNodeDecorator writeToJcr(@Nonnull Session session) { + if(!checkSecurityPermissions()) { + 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) + Authorizable newAuthorizable = existingAuthorizable ? updateAuthorizable(existingAuthorizable, session) : createNewAuthorizable(session) + writeAuthorizablePieces(newAuthorizable, session) + return new JCRNodeDecorator(session.getNode(newAuthorizable.getPath())) + } + + + /** + * @return a new authorizable from this serialized node + */ + private Authorizable createNewAuthorizable(final Session session) { + final UserManager userManager = getUserManager(session) + if(isUserType()) { + //We set a temporary password for now, and then set the real password later in setPasswordForUser(). See the method for why. + final newUser = userManager.createUser(authorizableID, Long.toString(CryptoUtil.generateNextId()), new AuthorizablePrincipal(authorizableID), getIntermediateAuthorizablePath()) + //This is a special protected property for disabling user access + if(hasProperty('rep:disabled')) { + newUser.disable(getStringValueFrom('rep:disabled')) + } + //AEM writes this property directly on the user node for some reason. One known use is for setting leads on MCM campaigns. + final authorizableCategory = 'cq:authorizableCategory' + if(hasProperty(authorizableCategory)) { + newUser.setProperty(authorizableCategory, new StringValue(getStringValueFrom(authorizableCategory))) + } + session.save() + //Special users may not have passwords, such as anonymous users + if(hasProperty('rep:password')) { + setPasswordForUser(newUser, session) + } + return newUser + } + final Group group = userManager.createGroup(authorizableID, new AuthorizablePrincipal(authorizableID), getIntermediateAuthorizablePath()) + session.save() + return group + } + + + /** + * From a client API perspective, there is really no way to truly update an existing authorizable node. All of the node properties are protected, and there is no + * known way to update them. Here we remove the existing authorizable as denoted by the authorizableID, and recreate it. + * @return new instance of updated authorizable + */ + private Authorizable updateAuthorizable(final Authorizable authorizable, final Session session) { + 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) + } + session.save() + } + + + private Authorizable findAuthorizable(final Session session) { + final UserManager userManager = getUserManager(session) + return userManager.getAuthorizable(getAuthorizableID()) + } + + + private String getAuthorizableID() { + return protoProperties.find { it.isAuthorizableIDType() }.stringValue + } + + + private String getIntermediateAuthorizablePath() { + final pathTokens = getName().tokenize('/') + //remove last index, as this is the Authorizable node name + pathTokens.remove(pathTokens.size() - 1) + return "/${pathTokens.join('/')}" + } + + + private boolean isUserType() { + return protoProperties.any { it.userType } + } + + + /** + * 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 + * @return true if we can sync this Authorizable + */ + private boolean checkSecurityPermissions() { + final SecurityManager securityManager = getSecurityManager() + //If no security manager is present, then we are in the clear; otherwise, we need to check certain permissions + if(!securityManager){ + log.debug "No SecurityManager found on this JVM. Sync of Users/Groups can continue" + return true + } + final issues = [] + final badPermissions = false + log.debug "SecurityManager found on this JVM. Checking permissions.." + try { + //Needed to reflect on members for which this class does not normally have access to + securityManager.checkPermission(new ReflectPermission('suppressAccessChecks')) + } + catch(SecurityException ex) { + issues << 'suppressAccessChecks' + badPermissions = true + } + try { + //Needed to access all declared members of a class, including protected or private + securityManager.checkPermission(new RuntimePermission('accessDeclaredMembers')) + } + catch(SecurityException ex) { + issues << 'accessDeclaredMembers' + badPermissions = true + } + try { + //Needed to access classes directly within a potentially system protected package + securityManager.checkPermission(new RuntimePermission('accessClassInPackage.{org.apache.jackrabbit.oak.security.user}')) + } + catch(SecurityException ex) { + issues << 'accessClassInPackage.{org.apache.jackrabbit.oak.security.user}' + badPermissions = true + } + if(badPermissions) { + log.warn "A SecurityManager is enabled for this JVM, and permissions are not sufficient for Grabbit to sync Authorizables (Users/Groups). You must enable ${issues.join(', ')} permissions in your SecurityManager to use this functionality" + + "Check https://docs.oracle.com/javase/7/docs/api/java/lang/RuntimePermission.html and https://docs.oracle.com/javase/7/docs/api/java/lang/reflect/ReflectPermission.html to see what these permissions enable" + return false + } + else { + log.debug "Permissions check successful" + return true + } + } + + /** + * Mostly for ease of mocking/testing + * @return the system's security manager, or null if one is not present + */ + SecurityManager getSecurityManager() { + return System.getSecurityManager() + } + + /** + * Mostly for ease of mocking/testing + */ + UserManager getUserManager(final Session session) { + return AccessControlUtil.getUserManager(session) + } + + + /** + * Normally we would call org.apache.jackrabbit.oak.jcr.delegate.UserDelegator.changePassword(String password) to change a password (this is what is publicly available through the Jackrabbit API) + * However, this method ALWAYS rehashes the password argument which is of no use to us, since we are trying to transfer an already hashed password. + * + * Internally, org.apache.jackrabbit.oak.jcr.delegate.UserDelegator calls it's delegate's org.apache.jackrabbit.oak.security.user.UserImpl.changePassword(String password) + * which calls org.apache.jackrabbit.oak.security.user.UserManagerImpl.setPassword(Tree tree, String userId, String password, boolean forceHash) with forceHash always set to true + * We really need forcehash set to false for our case, but this isn't publicly available. Here, we access internal objects to do this manipulation. org.apache.jackrabbit.oak.security.user.UserManagerImpl + * simply ensures that forcehash is false, and that the password is not plain text, and it sets the password as-is. + * + * @throws IllegalStateException if security permissions required to run this are not there. @{code checkSecurityPermissions()} should be called before calling this method + **/ + void setPasswordForUser(final User user, final Session session) { + if(!checkSecurityPermissions()) throw new IllegalStateException("Security check failed for Grabbit. Can not set user passwords") + //As a consumer we have access to org.apache.jackrabbit.oak.jcr.delegate.UserManagerDelegator below + final userManager = getUserManager(session) + Class userManagerDelegatorClass = userManager.getClass() + //Reach into the class of this delegator, and grab the core Jackrabbit object we delegate to + Field userManagerDelegateField = userManagerDelegatorClass.getDeclaredField('userManagerDelegate') + //The delegate field is private, so we need to make it accessible. Security checks above are imperative for this to work + userManagerDelegateField.setAccessible(true) + //Here we have a handle to the internal class org.apache.jackrabbit.oak.security.user.UserManagerImpl + final userManagerDelegate = userManagerDelegateField.get(userManager) + final userManagerDelegateClass = userManagerDelegate.getClass() + //We need to set the 'setPassword' method as accessible. Again, security checks above are imperative for this to work + Method setPasswordMethod = userManagerDelegateClass.getDeclaredMethod('setPassword', Class.forName('org.apache.jackrabbit.oak.api.Tree', true, userManagerDelegateClass.getClassLoader()), String, String, boolean) + setPasswordMethod.setAccessible(true) + /** + * Step two. We need access to the internal Authorizable object's tree in order to call the internal setPassword method + * User is an instance of org.apache.jackrabbit.oak.jcr.delegate.UserDelegator. We need to get the delegate off of this class's super class org.apache.jackrabbit.oak.jcr.delegate.AuthorizableDelegator + */ + Class authorizableDelegateClass = user.getClass().getSuperclass() + Field authorizableDelegateField = authorizableDelegateClass.getDeclaredField('delegate') + authorizableDelegateField.setAccessible(true) + final authorizable = authorizableDelegateField.get(user) + //Internal org.apache.jackrabbit.oak.security.user.AuthorizableImpl object. We can access the protected tree here + Method getTreeMethod = authorizable.getClass().getSuperclass().getDeclaredMethod('getTree') + getTreeMethod.setAccessible(true) + + /** + * The last argument where we are passing in 'false' in the secret sauce we need. This parameter is forceHash. As long as forceHash is false, and the password is not + * clear-text, which it isn't since we got it from another Jackrabbit instance, we can set the password as-is. + */ + setPasswordMethod.invoke(userManagerDelegate, getTreeMethod.invoke(authorizable), getAuthorizableID(), getStringValueFrom('rep:password'), false) + session.save() + } + + + /** + * 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 new file mode 100644 index 0000000..64635e0 --- /dev/null +++ b/src/main/groovy/com/twcable/grabbit/jcr/DefaultProtoNodeDecorator.groovy @@ -0,0 +1,110 @@ +/* + * Copyright 2015 Time Warner Cable, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.twcable.grabbit.jcr + +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 org.apache.jackrabbit.commons.JcrUtils + + +import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES +import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE + +@CompileStatic +@Slf4j +class DefaultProtoNodeDecorator extends ProtoNodeDecorator { + + private final String nameOverride + + protected DefaultProtoNodeDecorator(@Nonnull ProtoNode node, @Nonnull Collection protoProperties, String nameOverride) { + this.innerProtoNode = node + this.protoProperties = protoProperties + this.nameOverride = nameOverride + } + + + @Override + JCRNodeDecorator writeToJcr(@Nonnull Session session) { + final jcrNode = getOrCreateNode(session) + //Write mixin types first to avoid InvalidConstraintExceptions + final mixinProperty = getMixinProperty() + 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) + } + } + return new JCRNodeDecorator(jcrNode) + } + + + private ProtoPropertyDecorator getMixinProperty() { + protoProperties.find { it.isMixinType() } + } + + + private Collection getWritableProperties() { + protoProperties.findAll { !(it.name in [JCR_PRIMARYTYPE, JCR_MIXINTYPES]) } + } + + + @Override + String getName() { + nameOverride ?: innerProtoNode.getName() + } + + + /** + * This method is rather succinct, but helps isolate this JcrUtils static method call + * so that we can get better test coverage. + * @param session to create or get the node path for + * @return the newly created, or found node + */ + JCRNode getOrCreateNode(Session session) { + JcrUtils.getOrCreateByPath(getName(), primaryType.getStringValue(), session) + } + + + /** + * If a property can be added as a mixin, adds it to the given node + * @param property + * @param node + */ + private static void addMixins(ProtoPropertyDecorator property, JCRNode node) { + property.valuesList.each { ProtoValue value -> + if (node.canAddMixin(value.stringValue)) { + node.addMixin(value.stringValue) + log.debug "Added mixin ${value.stringValue} for : ${node.name}." + } + else { + log.warn "Encountered invalid mixin type while unmarshalling for Proto value : ${value}" + } + } + } + +} diff --git a/src/main/groovy/com/twcable/grabbit/jcr/JCRNodeDecorator.groovy b/src/main/groovy/com/twcable/grabbit/jcr/JCRNodeDecorator.groovy index d3be775..0aa6e27 100644 --- a/src/main/groovy/com/twcable/grabbit/jcr/JCRNodeDecorator.groovy +++ b/src/main/groovy/com/twcable/grabbit/jcr/JCRNodeDecorator.groovy @@ -19,49 +19,74 @@ 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 groovy.transform.CompileStatic - import groovy.util.logging.Slf4j -import org.apache.jackrabbit.value.DateValue - import javax.annotation.Nonnull import javax.annotation.Nullable +import javax.jcr.ItemNotFoundException import javax.jcr.Node as JCRNode -import javax.jcr.Property +import javax.jcr.PathNotFoundException import javax.jcr.Property as JcrProperty import javax.jcr.RepositoryException import javax.jcr.nodetype.ItemDefinition +import org.apache.jackrabbit.commons.flat.TreeTraverser +import org.apache.jackrabbit.value.DateValue -import static org.apache.jackrabbit.JcrConstants.* + +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.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 @CompileStatic @Slf4j -class JcrNodeDecorator { +class JCRNodeDecorator { @Delegate JCRNode innerNode + private final Collection properties //Evaluated in a lazy fashion - private Collection immediateChildNodes + private Collection immediateChildNodes + private List childNodeList - JcrNodeDecorator(@Nonnull JCRNode node) { + JCRNodeDecorator(@Nonnull JCRNode node) { if(!node) throw new IllegalArgumentException("node must not be null!") this.innerNode = node + Collection innerProperties = node.properties.toList() + this.properties = innerProperties.collect { JcrProperty property -> + new JcrPropertyDecorator(property, this) + } } /** * @return this node's immediate children, empty if none */ - Collection getImmediateChildNodes() { + Collection getImmediateChildNodes() { if(!immediateChildNodes) { - immediateChildNodes = (getNodes().collect { JCRNode node -> new JcrNodeDecorator(node) } ?: []) as Collection + immediateChildNodes = (getNodes().collect { JCRNode node -> new JCRNodeDecorator(node) } ?: []) as Collection } return immediateChildNodes } + List getChildNodeList() { + if(!childNodeList) { + childNodeList = (getChildNodeIterator().collect { JCRNode node -> new JCRNodeDecorator(node) } ?: []) as List + } + return childNodeList + } + + + Iterator getChildNodeIterator() { + return TreeTraverser.nodeIterator(innerNode, ErrorHandler.IGNORE, new NoRootInclusionPolicy(this)) + } + + void setLastModified() { final lastModified = new DateValue(Calendar.instance) try { @@ -77,18 +102,25 @@ class JcrNodeDecorator { } - String getPrimaryType() { - innerNode.getProperty(JCR_PRIMARYTYPE).string - } - - /** * Identify all required child nodes - * @return list of immediate required child nodes that must exist with this node, or null if no children + * @return list of immediate required child nodes that must be transported with this node, or an empty collection if no required nodes */ @Nullable - Collection getRequiredChildNodes() { - return hasMandatoryChildNodes() ? getImmediateChildNodes().findAll{ JcrNodeDecorator childJcrNode -> childJcrNode.isRequiredNode() } : null + Collection getRequiredChildNodes() { + if(isAuthorizableType()){ + return getChildNodeList().findAll { JCRNodeDecorator childJcrNode -> !childJcrNode.isLoginToken() && !childJcrNode.isACType() } + } + return getMandatoryChildren() + } + + + /** + * Some nodes must be saved together, per node definition + */ + @Nonnull + private Collection getMandatoryChildren() { + return hasMandatoryChildNodes() ? getImmediateChildNodes().findAll{ JCRNodeDecorator childJcrNode -> childJcrNode.isMandatoryNode() } : [] } @@ -102,10 +134,10 @@ class JcrNodeDecorator { /** - * This node is a required node of some parent node definition + * This node is a mandatory required node of some parent node definition * @return true if mandatory, false if not */ - boolean isRequiredNode() { + boolean isMandatoryNode() { return definition.isMandatory() } @@ -117,7 +149,7 @@ class JcrNodeDecorator { final ProtoNodeBuilder protoNodeBuilder = ProtoNode.newBuilder() protoNodeBuilder.setName(path) protoNodeBuilder.addAllProperties(getProtoProperties()) - requiredChildNodes?.each { + requiredChildNodes.each { protoNodeBuilder.addMandatoryChildNode(it.toProtoNode()) } return protoNodeBuilder.build() @@ -128,11 +160,8 @@ class JcrNodeDecorator { */ @Nonnull private Collection getProtoProperties() { - final List properties = properties.toList() - return properties.findResults { JcrProperty jcrProperty -> - JcrPropertyDecorator decoratedProperty = new JcrPropertyDecorator(jcrProperty) - decoratedProperty.isTransferable() ? decoratedProperty.toProtoProperty() : null - } + final Collection transferableProperties = properties.findAll{ it.isTransferable() } + return transferableProperties.collect{ it.toProtoProperty() } } /** @@ -156,6 +185,43 @@ class JcrNodeDecorator { } + /** + * Authorizable nodes can be unique from server to server, so associated profiles, preferences, etc need to be sent with. + * @return true if this node lives under an authorizable + */ + boolean isAuthorizablePart() { + try { + JCRNodeDecorator parent = new JCRNodeDecorator(getParent()) + while(!parent.isAuthorizableType()) { + parent = new JCRNodeDecorator(parent.getParent()) + } + return true + } catch(PathNotFoundException | ItemNotFoundException ex) { + return false + } + } + + + String getPrimaryType() { + innerNode.getProperty(JCR_PRIMARYTYPE).string + } + + + boolean isAuthorizableType() { + return primaryType == 'rep:User' || primaryType == 'rep:Group' + } + + boolean isACType() { + AC_NODETYPE_NAMES.contains(primaryType) + } + + + boolean isLoginToken() { + final primaryType = getPrimaryType() + return (primaryType == 'rep:Unstructured' && name.tokenize('/')[-1] == '.tokens') || primaryType == 'rep:Token' + } + + Object asType(Class clazz) { if(clazz == JCRNode) { return innerNode @@ -164,4 +230,37 @@ class JcrNodeDecorator { super.asType(clazz) } } + + @Override + boolean equals(Object obj) { + if (this.is(obj)) return true + if (getClass() != obj.class) return false + + JCRNodeDecorator that = (JCRNodeDecorator)obj + + return this.hashCode() == that.hashCode() + } + + @Override + int hashCode() { + return innerNode.getName().hashCode() + } + + @CompileStatic + private static class NoRootInclusionPolicy implements InclusionPolicy { + + final JCRNodeDecorator rootNode + + NoRootInclusionPolicy(JCRNode rootNode) { + this.rootNode = new JCRNodeDecorator(rootNode) + } + + + @Override + boolean include(JCRNode node) { + final JCRNodeDecorator candidateNode = new JCRNodeDecorator(node) + //Don't include the root, and dont' include mandatory nodes as they are held within their parent + return (!rootNode.equals(candidateNode)) && (!candidateNode.isMandatoryNode()) + } + } } diff --git a/src/main/groovy/com/twcable/grabbit/jcr/JcrPropertyDecorator.groovy b/src/main/groovy/com/twcable/grabbit/jcr/JcrPropertyDecorator.groovy index af667dc..b258db8 100644 --- a/src/main/groovy/com/twcable/grabbit/jcr/JcrPropertyDecorator.groovy +++ b/src/main/groovy/com/twcable/grabbit/jcr/JcrPropertyDecorator.groovy @@ -24,7 +24,6 @@ import groovy.transform.CompileStatic import groovy.util.logging.Slf4j import javax.annotation.Nonnull -import javax.jcr.Property import javax.jcr.Property as JCRProperty import javax.jcr.Value @@ -38,8 +37,11 @@ class JcrPropertyDecorator { @Delegate JCRProperty innerProperty - JcrPropertyDecorator(Property property) { + private final JCRNodeDecorator nodeOwner + + JcrPropertyDecorator(JCRProperty property, JCRNodeDecorator nodeOwner) { this.innerProperty = property + this.nodeOwner = nodeOwner } /** @@ -58,7 +60,7 @@ class JcrPropertyDecorator { return true } - !definition.isProtected() + return nodeOwner.isAuthorizableType() ?: !definition.isProtected() } /** diff --git a/src/main/groovy/com/twcable/grabbit/jcr/ProtoNodeDecorator.groovy b/src/main/groovy/com/twcable/grabbit/jcr/ProtoNodeDecorator.groovy index 750eab3..24d65a8 100644 --- a/src/main/groovy/com/twcable/grabbit/jcr/ProtoNodeDecorator.groovy +++ b/src/main/groovy/com/twcable/grabbit/jcr/ProtoNodeDecorator.groovy @@ -13,93 +13,46 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.twcable.grabbit.jcr 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 org.apache.jackrabbit.commons.JcrUtils import javax.annotation.Nonnull -import javax.jcr.Node as JCRNode import javax.jcr.Session -import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES -import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE - @CompileStatic -@Slf4j -class ProtoNodeDecorator { +abstract class ProtoNodeDecorator { @Delegate - ProtoNode innerProtoNode + protected ProtoNode innerProtoNode - Collection protoProperties + protected Collection protoProperties + abstract JCRNodeDecorator writeToJcr(@Nonnull Session session) - ProtoNodeDecorator(@Nonnull ProtoNode node) { + static ProtoNodeDecorator createFrom(@Nonnull ProtoNode node, String nameOverride = null) { if(!node) throw new IllegalArgumentException("node must not be null!") - this.innerProtoNode = node - this.protoProperties = node.propertiesList.collect { new ProtoPropertyDecorator(it) } - } - - - String getPrimaryType() { - protoProperties.find { it.isPrimaryType() }.value.stringValue - } - - - ProtoPropertyDecorator getMixinProperty() { - protoProperties.find { it.isMixinType() } - } - - - Collection getWritableProperties() { - protoProperties.findAll { !(it.name in [JCR_PRIMARYTYPE, JCR_MIXINTYPES]) } - } - - - JcrNodeDecorator writeToJcr(@Nonnull Session session) { - final jcrNode = getOrCreateNode(session) - //Write mixin types first to avoid InvalidConstraintExceptions - final mixinProperty = getMixinProperty() - if(mixinProperty) { - addMixins(mixinProperty, jcrNode) + final protoProperties = node.propertiesList.collect { new ProtoPropertyDecorator(it) } + final primaryType = protoProperties.find { it.primaryType } + if(primaryType.isUserType() || primaryType.isGroupType()) { + return new AuthorizableProtoNodeDecorator(node, protoProperties) } - //Then add other properties - writableProperties.each { it.writeToNode(jcrNode) } + return new DefaultProtoNodeDecorator(node, protoProperties, nameOverride) + } - return new JcrNodeDecorator(jcrNode) + boolean hasProperty(String propertyName) { + propertiesList.any{ it.name == propertyName } } - /** - * This method is rather succinct, but helps isolate this JcrUtils static method call - * so that we can get better test coverage. - * @param session to create or get the node path for - * @return the newly created, or found node - */ - JCRNode getOrCreateNode(Session session) { - JcrUtils.getOrCreateByPath(innerProtoNode.name, primaryType, session) + protected ProtoPropertyDecorator getPrimaryType() { + protoProperties.find { it.isPrimaryType() } } - /** - * If a property can be added as a mixin, adds it to the given node - * @param property - * @param node - */ - private static void addMixins(ProtoPropertyDecorator property, JCRNode node) { - property.valuesList.each { ProtoValue value -> - if (node.canAddMixin(value.stringValue)) { - node.addMixin(value.stringValue) - log.debug "Added mixin ${value.stringValue} for : ${node.name}." - } - else { - log.warn "Encountered invalid mixin type while unmarshalling for Proto value : ${value}" - } - } + protected String getStringValueFrom(String propertyName) { + protoProperties.find { it.name == propertyName }.stringValue } - } diff --git a/src/main/groovy/com/twcable/grabbit/jcr/ProtoPropertyDecorator.groovy b/src/main/groovy/com/twcable/grabbit/jcr/ProtoPropertyDecorator.groovy index eeb7a4e..a7aa862 100644 --- a/src/main/groovy/com/twcable/grabbit/jcr/ProtoPropertyDecorator.groovy +++ b/src/main/groovy/com/twcable/grabbit/jcr/ProtoPropertyDecorator.groovy @@ -38,6 +38,7 @@ class ProtoPropertyDecorator { @Delegate ProtoProperty innerProtoProperty + ProtoPropertyDecorator(@Nonnull ProtoProperty protoProperty) { this.innerProtoProperty = protoProperty } @@ -82,7 +83,28 @@ class ProtoPropertyDecorator { innerProtoProperty.name == JCR_MIXINTYPES } - ProtoValue getValue() { + + boolean isUserType() { + getStringValue() == 'rep:User' + } + + + boolean isGroupType() { + getStringValue() == 'rep:Group' + } + + + boolean isAuthorizableIDType() { + innerProtoProperty.name == 'rep:authorizableId' + } + + + String getStringValue() { + getValue().stringValue + } + + + private ProtoValue getValue() { innerProtoProperty.valuesList.first() } diff --git a/src/main/groovy/com/twcable/grabbit/security/AuthorizablePrincipal.groovy b/src/main/groovy/com/twcable/grabbit/security/AuthorizablePrincipal.groovy new file mode 100644 index 0000000..101ba2b --- /dev/null +++ b/src/main/groovy/com/twcable/grabbit/security/AuthorizablePrincipal.groovy @@ -0,0 +1,49 @@ +/* + * Copyright 2015 Time Warner Cable, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.twcable.grabbit.security + +import groovy.transform.CompileStatic + +import javax.annotation.Nonnull +import java.security.Principal + + +@CompileStatic +class AuthorizablePrincipal implements Principal { + + final private String principalName + + AuthorizablePrincipal(@Nonnull final String principalName) { + this.principalName = principalName + } + + @Override + boolean equals(Object other) { + if(other == null) return false + return this.hashCode() == other.hashCode() + } + + @Override + String getName() { + return principalName + } + + @Override + int hashCode() { + return principalName.hashCode() + } +} diff --git a/src/main/groovy/com/twcable/grabbit/security/InsufficientGrabbitPrivilegeException.groovy b/src/main/groovy/com/twcable/grabbit/security/InsufficientGrabbitPrivilegeException.groovy new file mode 100644 index 0000000..a397d40 --- /dev/null +++ b/src/main/groovy/com/twcable/grabbit/security/InsufficientGrabbitPrivilegeException.groovy @@ -0,0 +1,23 @@ +/* + * Copyright 2015 Time Warner Cable, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.twcable.grabbit.security + +import groovy.transform.CompileStatic +import groovy.transform.InheritConstructors + +@InheritConstructors +@CompileStatic +class InsufficientGrabbitPrivilegeException extends RuntimeException {} 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 76051e6..7f4bb65 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 @@ -17,7 +17,7 @@ package com.twcable.grabbit.server.batch.steps.jcrnodes import com.twcable.grabbit.DateUtil -import com.twcable.grabbit.jcr.JcrNodeDecorator +import com.twcable.grabbit.jcr.JCRNodeDecorator import com.twcable.grabbit.proto.NodeProtos.Node as ProtoNode import groovy.transform.CompileStatic import groovy.util.logging.Slf4j @@ -47,10 +47,10 @@ class JcrNodesProcessor implements ItemProcessor { @Nullable ProtoNode process(JcrNode jcrNode) throws Exception { - JcrNodeDecorator decoratedNode = new JcrNodeDecorator(jcrNode) + JCRNodeDecorator decoratedNode = new JCRNodeDecorator(jcrNode) //TODO: Access Control Lists nodes are not supported right now. - if (decoratedNode.path.contains("rep:policy")) { + if (decoratedNode.isACType()) { log.info "Ignoring current node ${decoratedNode.innerNode}" return null } @@ -66,7 +66,7 @@ class JcrNodesProcessor implements ItemProcessor { } // Skip this node because it has already been processed by its parent - if(decoratedNode.isRequiredNode()) { + if(decoratedNode.isMandatoryNode() || decoratedNode.isAuthorizablePart()) { return null } else { // Build parent node diff --git a/src/main/groovy/com/twcable/grabbit/server/services/RootNodeWithMandatoryIterator.groovy b/src/main/groovy/com/twcable/grabbit/server/services/RootNodeWithMandatoryIterator.groovy index 063dd52..e882259 100644 --- a/src/main/groovy/com/twcable/grabbit/server/services/RootNodeWithMandatoryIterator.groovy +++ b/src/main/groovy/com/twcable/grabbit/server/services/RootNodeWithMandatoryIterator.groovy @@ -16,7 +16,7 @@ package com.twcable.grabbit.server.services -import com.twcable.grabbit.jcr.JcrNodeDecorator +import com.twcable.grabbit.jcr.JCRNodeDecorator import groovy.transform.CompileStatic import groovy.transform.TailRecursive @@ -31,13 +31,13 @@ import javax.jcr.Node as JcrNode final class RootNodeWithMandatoryIterator implements Iterator { private boolean doneRoot - private JcrNodeDecorator rootNode - private Iterator immediateChildren - private Iterator mandatoryWriteNodes + private JCRNodeDecorator rootNode + private Iterator immediateChildren + private Iterator mandatoryWriteNodes public RootNodeWithMandatoryIterator(JcrNode root) { - this.rootNode = new JcrNodeDecorator(root) + this.rootNode = new JCRNodeDecorator(root) this.doneRoot = false //Get all immediate children that are not mandatory write nodes. We will handle those by iterating over mandatoryWriteNodes immediateChildren = getNonMandatoryChildren(this.rootNode).iterator() @@ -76,20 +76,20 @@ final class RootNodeWithMandatoryIterator implements Iterator { } - private static Collection getNonMandatoryChildren(final JcrNodeDecorator node) { - node.getImmediateChildNodes().findAll { !it.isRequiredNode() } + private static Collection getNonMandatoryChildren(final JCRNodeDecorator node) { + node.getImmediateChildNodes().findAll { !it.isMandatoryNode() } } @TailRecursive - private static Collection getMandatoryChildren(final JcrNodeDecorator currentNode, final Collection nodesToAdd) { + private static Collection getMandatoryChildren(final JCRNodeDecorator currentNode, final Collection nodesToAdd) { if(!currentNode.hasMandatoryChildNodes()) { return nodesToAdd } final mandatoryNodes = currentNode.getRequiredChildNodes() - return mandatoryNodes.collectMany { JcrNodeDecorator mandatoryNode -> + return mandatoryNodes.collectMany { JCRNodeDecorator mandatoryNode -> return getMandatoryChildren(mandatoryNode, (nodesToAdd << mandatoryNode)) } } diff --git a/src/test/groovy/com/twcable/grabbit/client/GrabbitJobServletSpec.groovy b/src/test/groovy/com/twcable/grabbit/client/GrabbitJobServletSpec.groovy index 86dc95d..25fad75 100644 --- a/src/test/groovy/com/twcable/grabbit/client/GrabbitJobServletSpec.groovy +++ b/src/test/groovy/com/twcable/grabbit/client/GrabbitJobServletSpec.groovy @@ -32,10 +32,9 @@ import spock.lang.Specification import spock.lang.Subject import spock.lang.Unroll -import javax.annotation.Nonnull -import javax.servlet.ServletInputStream import static com.twcable.grabbit.client.servlets.GrabbitJobServlet.ALL_JOBS_ID +import static com.twcable.grabbit.testutil.StubInputStream.inputStream import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST import static javax.servlet.http.HttpServletResponse.SC_OK @@ -186,7 +185,7 @@ class GrabbitJobServletSpec extends Specification { def "Can create a new job successfully from configuration"() { given: File configFile = new File(this.class.getResource("test_config.yaml").getFile()) - final inputStream = new StubServletInputStream(configFile) + final inputStream = inputStream(configFile) final clientService = Mock(ClientService) { initiateGrab(_, _) >> [123L] } @@ -230,36 +229,7 @@ class GrabbitJobServletSpec extends Specification { 1 * response.setStatus(SC_BAD_REQUEST) where: - inputStream << [new StubServletInputStream(" "), new StubServletInputStream("foo: 'foo'")] + inputStream << [inputStream(" "), inputStream("foo: 'foo'")] //One causes SnakeYAML to produce a null config map, and the other does not pass our validations (missing values) } - - class StubServletInputStream extends ServletInputStream { - - - private final int byte_length - private final byte[] bytes - private int byte_index = 0 - - StubServletInputStream(@Nonnull final String data) { - bytes = data as byte[] - byte_length = bytes.length - } - - StubServletInputStream(@Nonnull final File fromFile) { - bytes = fromFile.readBytes() - byte_length = bytes.length - } - - @Override - int read() throws IOException { - if (byte_index <= byte_length - 1) { - final thisByte = bytes[byte_index] as int - byte_index++ - return thisByte - } - return -1 - } - } - } diff --git a/src/test/groovy/com/twcable/grabbit/jcr/AuthorizableProtoNodeDecoratorSpec.groovy b/src/test/groovy/com/twcable/grabbit/jcr/AuthorizableProtoNodeDecoratorSpec.groovy new file mode 100644 index 0000000..e858d71 --- /dev/null +++ b/src/test/groovy/com/twcable/grabbit/jcr/AuthorizableProtoNodeDecoratorSpec.groovy @@ -0,0 +1,339 @@ +/* + * Copyright 2015 Time Warner Cable, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.twcable.grabbit.jcr + +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 com.twcable.grabbit.proto.NodeProtos.Value as ProtoValue +import com.twcable.grabbit.security.AuthorizablePrincipal +import com.twcable.grabbit.security.InsufficientGrabbitPrivilegeException +import java.lang.reflect.ReflectPermission +import javax.jcr.Node +import javax.jcr.Property +import javax.jcr.PropertyIterator +import javax.jcr.Session +import org.apache.jackrabbit.api.security.user.Authorizable +import org.apache.jackrabbit.api.security.user.Group +import org.apache.jackrabbit.api.security.user.User +import org.apache.jackrabbit.api.security.user.UserManager +import org.apache.jackrabbit.value.StringValue +import spock.lang.Specification +import spock.lang.Unroll + + +import static javax.jcr.PropertyType.STRING +import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE + + +class AuthorizableProtoNodeDecoratorSpec extends Specification { + + AuthorizableProtoNodeDecorator theProtoNodeDecorator(boolean forUser, boolean hasProfile, boolean hasPreferences, Closure configuration = null){ + ProtoNodeBuilder nodeBuilder = ProtoNode.newBuilder() + nodeBuilder.setName(forUser ? "/home/users/u/user1" : "/home/groups/g/group1") + + ProtoProperty primaryTypeProperty = ProtoProperty + .newBuilder() + .setName(JCR_PRIMARYTYPE) + .setType(STRING) + .setMultiple(false) + .addValues(ProtoValue.newBuilder().setStringValue(forUser ? 'rep:User' : 'rep:Group')) + .build() + nodeBuilder.addProperties(primaryTypeProperty) + + ProtoProperty disabledProperty = ProtoProperty + .newBuilder() + .setName('rep:disabled') + .setType(STRING) + .setMultiple(false) + .addValues(ProtoValue.newBuilder().setStringValue('Reason for disabling')) + .build() + nodeBuilder.addProperties(disabledProperty) + + ProtoProperty authorizableIdProperty = ProtoProperty + .newBuilder() + .setName('rep:authorizableId') + .setType(STRING) + .setMultiple(false) + .addValues(ProtoValue.newBuilder().setStringValue('authorizableID')) + .build() + nodeBuilder.addProperties(authorizableIdProperty) + + ProtoProperty authorizableCategory = ProtoProperty + .newBuilder() + .setName('cq:authorizableCategory') + .setType(STRING) + .setMultiple(false) + .addValues(ProtoValue.newBuilder().setStringValue('mcm')) + .build() + nodeBuilder.addProperties(authorizableCategory) + + ProtoProperty simplePrimaryType = ProtoProperty + .newBuilder() + .setName(JCR_PRIMARYTYPE) + .setType(STRING) + .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() + nodeBuilder.addMandatoryChildNode(preferenceNode) + } + if(hasProfile) { + ProtoNode profileNode = ProtoNode + .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) + } + + + def "Throws an InsufficientGrabbitPrivilegeException if JVM permissions are not present"() { + when: + final protoNodeDecorator = theProtoNodeDecorator(false, false, false) { + it.getSecurityManager() >> Mock(SecurityManager) { + it.checkPermission(permission) >> { + throw new SecurityException() + } + } + } + + protoNodeDecorator.writeToJcr(Mock(Session)) + + then: + thrown(InsufficientGrabbitPrivilegeException) + + where: + permission << [new ReflectPermission('suppressAccessChecks'), new RuntimePermission('accessDeclaredMembers'), new RuntimePermission('accessClassInPackage.{org.apache.jackrabbit.oak.security.user}')] + } + + + def "Passes security check if all JVM permissions are present"() { + when: + final session = Mock(Session) { + it.getNode('authorizablePath') >> Mock(Node) { + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + it.getString() >> 'nt:unstructured' + } + getProperties() >> Mock(PropertyIterator) { + it.toList() >> [] + } + } + } + final protoNodeDecorator = theProtoNodeDecorator(false, false, false) { + it.getSecurityManager() >> Mock(SecurityManager) { + it.checkPermission(permission) >> { + return + } + } + it.getUserManager(session) >> Mock(UserManager) { + it.createGroup(_, _, _) >> Mock(Group) { + it.getPath() >> 'authorizablePath' + } + } + } + + protoNodeDecorator.writeToJcr(session) + + then: + notThrown(InsufficientGrabbitPrivilegeException) + + where: + permission << [new ReflectPermission('suppressAccessChecks'), new RuntimePermission('accessDeclaredMembers'), new RuntimePermission('accessClassInPackage.{org.apache.jackrabbit.oak.security.user}')] + } + + + def "Passes security check if no SecurityManager is found on the JVM"() { + when: + final session = Mock(Session) { + it.getNode('authorizablePath') >> Mock(Node) { + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + it.getString() >> 'nt:unstructured' + } + getProperties() >> Mock(PropertyIterator) { + it.toList() >> [] + } + } + } + final protoNodeDecorator = theProtoNodeDecorator(false, false, false) { + it.getSecurityManager() >> null + it.getUserManager(session) >> Mock(UserManager) { + it.createGroup(_, _, _) >> Mock(Group) { + it.getPath() >> 'authorizablePath' + } + } + } + + protoNodeDecorator.writeToJcr(session) + + then: + notThrown(InsufficientGrabbitPrivilegeException) + } + + + def "getSecurityManager() will retrieve the JVM's security manager"() { + given: + final protoNodeDecorator = theProtoNodeDecorator(false, false, false) + + expect: + System.getSecurityManager() == protoNodeDecorator.getSecurityManager() + } + + + def "writeToJcr() will create a new underlying User, and return it's node within a JcrNodeDecorator"(){ + given: + final node = Mock(Node) { + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + it.getString() >> 'nt:unstructured' + } + getProperties() >> Mock(PropertyIterator) { + it.toList() >> [] + } + } + final session = Mock(Session) { + it.getNode('newUserPath') >> node + } + final newUser = Mock(User) { + 1 * it.disable('Reason for disabling') + 1 * it.setProperty('cq:authorizableCategory', new StringValue('mcm')) + it.getPath() >> 'newUserPath' + } + final protoNodeDecorator = theProtoNodeDecorator(true, false, false) { + it.getName() >> '/home/users/auth_folder/user' + it.getSecurityManager() >> null + it.setPasswordForUser(newUser, session) >> { + return + } + it.getUserManager(session) >> Mock(UserManager) { + it.getAuthorizable('authorizableID') >> null + 1 * it.createUser('authorizableID', _, new AuthorizablePrincipal('authorizableID'), '/home/users/auth_folder') >> newUser + } + } + + when: + final userNode = protoNodeDecorator.writeToJcr(session) + + then: + userNode.getInnerNode() == node + } + + + def "writeToJcr() will create a new underlying Group, and return it's node within a JcrNodeDecorator"(){ + given: + final node = Mock(Node) { + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + it.getString() >> 'nt:unstructured' + } + getProperties() >> Mock(PropertyIterator) { + it.toList() >> [] + } + } + final session = Mock(Session) { + it.getNode('newGroupPath') >> node + } + final newGroup = Mock(Group) { + it.getPath() >> 'newGroupPath' + } + final protoNodeDecorator = theProtoNodeDecorator(false, false, false) { + 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 + } + } + + when: + final groupNode = protoNodeDecorator.writeToJcr(session) + + then: + groupNode.getInnerNode() == node + } + + @Unroll + def "Updates profile on an authorizable if it exists. Exists: #exists"() { + when: + final node = Mock(Node) { + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + it.getString() >> 'rep:User' + } + getProperties() >> Mock(PropertyIterator) { + it.toList() >> [] + } + } + final session = Mock(Session) { + it.getNode('/home/users/u/newuser') >> node + } + final protoNodeDecorator = theProtoNodeDecorator(false, exists, false) { + it.getSecurityManager() >> null + it.getUserManager(session) >> Mock(UserManager) { + it.getAuthorizable('authorizableID') >> Mock(Authorizable) + it.createGroup('authorizableID', _, _) >> Mock(Group) { + it.getPath() >> '/home/users/u/newuser' + } + } + (exists ? 1 : 0) * it.createFrom(_ as ProtoNode, '/home/users/u/newuser/profile') >> Mock(ProtoNodeDecorator) + } + + then: + protoNodeDecorator.writeToJcr(session) + + + where: + exists << [false, true] + } + + @Unroll + def "Updates preferences on an authorizable if it exists. Exists: #exists"() { + when: + final node = Mock(Node) { + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + it.getString() >> 'rep:User' + } + getProperties() >> Mock(PropertyIterator) { + it.toList() >> [] + } + } + final session = Mock(Session) { + it.getNode('/home/users/u/newuser') >> node + } + final protoNodeDecorator = theProtoNodeDecorator(false, false, exists) { + it.getSecurityManager() >> null + it.getUserManager(session) >> Mock(UserManager) { + it.getAuthorizable('authorizableID') >> Mock(Authorizable) + it.createGroup('authorizableID', _, _) >> Mock(Group) { + it.getPath() >> '/home/users/u/newuser' + } + } + (exists ? 1 : 0) * it.createFrom(_ as ProtoNode, '/home/users/u/newuser/preferences') >> Mock(ProtoNodeDecorator) + } + + then: + protoNodeDecorator.writeToJcr(session) + + + where: + exists << [false, true] + } +} diff --git a/src/test/groovy/com/twcable/grabbit/jcr/DefaultProtoNodeDecoratorSpec.groovy b/src/test/groovy/com/twcable/grabbit/jcr/DefaultProtoNodeDecoratorSpec.groovy new file mode 100644 index 0000000..2724778 --- /dev/null +++ b/src/test/groovy/com/twcable/grabbit/jcr/DefaultProtoNodeDecoratorSpec.groovy @@ -0,0 +1,145 @@ +/* + * Copyright 2015 Time Warner Cable, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.twcable.grabbit.jcr + +import com.day.cq.commons.jcr.JcrConstants +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 spock.lang.Specification + +import javax.jcr.Node +import javax.jcr.Property +import javax.jcr.PropertyIterator +import javax.jcr.Session + +import static javax.jcr.PropertyType.STRING +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 + + def setup() { + ProtoNodeBuilder nodeBuilder = ProtoNode.newBuilder() + nodeBuilder.setName("somenode") + + ProtoProperty primaryTypeProperty = NodeProtos.Property + .newBuilder() + .setName(JCR_PRIMARYTYPE) + .setType(STRING) + .setMultiple(false) + .addValues(NodeProtos.Value.newBuilder().setStringValue(JcrConstants.NT_UNSTRUCTURED)) + .build() + nodeBuilder.addProperties(primaryTypeProperty) + + mixinProperty = NodeProtos.Property + .newBuilder() + .setName(JCR_MIXINTYPES) + .setType(STRING) + .setMultiple(true) + .addAllValues( + [ + NodeProtos.Value.newBuilder().setStringValue("somemixintype").build(), + NodeProtos.Value.newBuilder().setStringValue("unwritablemixin").build() + ] + ) + .build() + nodeBuilder.addProperties(mixinProperty) + + + someOtherProperty = NodeProtos.Property + .newBuilder() + .setName("someproperty") + .setType(STRING) + .setMultiple(false) + .addValues(NodeProtos.Value.newBuilder().setStringValue("somevalue")) + .build() + nodeBuilder.addProperties(someOtherProperty) + + decoratedProtoNode = nodeBuilder.build() + } + + + def "Can get primary type"() { + when: + final protoNodeDecorator = DefaultProtoNodeDecorator.createFrom(decoratedProtoNode) + + then: + protoNodeDecorator.getPrimaryType().getStringValue() == JcrConstants.NT_UNSTRUCTURED + } + + + def "can get mixin property"() { + given: + final protoNodeDecorator = DefaultProtoNodeDecorator.createFrom(decoratedProtoNode) + + when: + final property = protoNodeDecorator.getMixinProperty() + + then: + property.valuesCount == 2 + property.name == JCR_MIXINTYPES + } + + + def "Can get just writable properties"() { + given: + final protoNodeDecorator = DefaultProtoNodeDecorator.createFrom(decoratedProtoNode) + + when: + final Collection properties = protoNodeDecorator.getWritableProperties() + + then: + properties.size() == 1 + properties[0].stringValue == "somevalue" + } + + def "Can write this ProtoNodeDecorator to the JCR"() { + given: + final session = Mock(Session) + final jcrNodeRepresentation = Mock(Node) { + canAddMixin('somemixintype') >> true + canAddMixin('unwritablemixin') >> false + 1 * addMixin('somemixintype') + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + getString() >> 'nt:unstructured' + } + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } + } + final protoPropertyDecorators = [ + new ProtoPropertyDecorator(mixinProperty), + new ProtoPropertyDecorator(someOtherProperty) + ] + final protoNodeDecorator = Spy(DefaultProtoNodeDecorator, constructorArgs: [decoratedProtoNode, protoPropertyDecorators, null]) { + getOrCreateNode(session) >> { + return jcrNodeRepresentation + } + } + + final jcrNodeDecorator = protoNodeDecorator.writeToJcr(session) + + expect: + jcrNodeDecorator != null + } +} diff --git a/src/test/groovy/com/twcable/grabbit/jcr/JCRNodeDecoratorSpec.groovy b/src/test/groovy/com/twcable/grabbit/jcr/JCRNodeDecoratorSpec.groovy index 6b5904e..b83b375 100644 --- a/src/test/groovy/com/twcable/grabbit/jcr/JCRNodeDecoratorSpec.groovy +++ b/src/test/groovy/com/twcable/grabbit/jcr/JCRNodeDecoratorSpec.groovy @@ -15,18 +15,28 @@ */ package com.twcable.grabbit.jcr -import com.day.cq.commons.jcr.JcrConstants -import spock.lang.Shared -import spock.lang.Specification - +import com.twcable.grabbit.proto.NodeProtos.Node as ProtoNode +import javax.jcr.Binary +import javax.jcr.ItemNotFoundException import javax.jcr.Node import javax.jcr.NodeIterator import javax.jcr.Property +import javax.jcr.PropertyIterator import javax.jcr.RepositoryException +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.commons.iterator.PropertyIteratorAdapter +import spock.lang.Shared +import spock.lang.Specification +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 org.apache.jackrabbit.JcrConstants.JCR_CREATED import static org.apache.jackrabbit.JcrConstants.JCR_LASTMODIFIED import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE @@ -46,93 +56,189 @@ class JCRNodeDecoratorSpec extends Specification { def "null nodes are not allowed for JCRNodeDecorator construction"() { when: - new JcrNodeDecorator(null) + new JCRNodeDecorator(null) then: thrown(IllegalArgumentException) } - - def "setLastModified() when last modified can be set"() { + def "toProtoNode()"() { given: Node node = Mock(Node) { + getPath() >> "/some/path" getPrimaryNodeType() >> Mock(NodeType) { - canSetProperty(JCR_LASTMODIFIED, _) >> { true } + getChildNodeDefinitions() >> [ + Mock(NodeDefinition) { + isMandatory() >> true + } + ].toArray() + } + getNodes() >> Mock(NodeIterator) { + hasNext() >>> true >> false + next() >> + Mock(Node) { + getDefinition() >> Mock(NodeDefinition) { + isMandatory() >> true + } + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } + getPath() >> "path" + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + getString() >> "nt:unstructured" + } + getPrimaryNodeType() >> Mock(NodeType) { + getChildNodeDefinitions() >> [].toArray() + } + } } + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + getString() >> "rep:SystemUser" + } + getProperties() >> new PropertyIteratorAdapter( + [ + Mock(Property) { + getName() >> JCR_PRIMARYTYPE + getString() >> "rep:SystemUser" + getDefinition() >> Mock(PropertyDefinition) { + isProtected() >> true + } + getType() >> BINARY + isMultiple() >> false + getValue() >> Mock(Value) { + getBinary() >> Mock(Binary) { + getStream() >> inputStream("test data") + } + } + }, + Mock(Property) { + getName() >> JCR_LASTMODIFIED + getString() >> "lastModified" + getDefinition() >> Mock(PropertyDefinition) { + isProtected() >> false + } + getType() >> BINARY + isMultiple() >> false + getValue() >> Mock(Value) { + getBinary() >> Mock(Binary) { + getStream() >> inputStream("test data") + } } + }, + Mock(Property) { + getName() >> "protectedProperty" + getString() >> "protectedPropertyValue" + getDefinition() >> Mock(PropertyDefinition) { + isProtected() >> true + } + getType() >> BINARY + isMultiple() >> false + getValue() >> Mock(Value) { + getBinary() >> Mock(Binary) { + getStream() >> inputStream("test data") + } + } + } + ].iterator() + ) } when: - final nodeDecorator = new JcrNodeDecorator(node) - nodeDecorator.setLastModified() + final ProtoNode protoNode = new JCRNodeDecorator(node).toProtoNode() then: - 1 * node.setProperty(JCR_LASTMODIFIED, _) - notThrown(RepositoryException) + protoNode.getName() == '/some/path' + protoNode.getPropertiesCount() == 1 } - def "setLastModified() when last modified can not be set"() { + def "setLastModified() when last modified can be set"() { given: Node node = Mock(Node) { getPrimaryNodeType() >> Mock(NodeType) { - canSetProperty(JCR_LASTMODIFIED, _) >> { false } + canSetProperty(JCR_LASTMODIFIED, _) >> { true } + } + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + getString() >> 'nt:unstructured' + } + getProperties() >> Mock(PropertyIterator) { + toList() >> [] } } when: - final nodeDecorator = new JcrNodeDecorator(node) + final nodeDecorator = new JCRNodeDecorator(node) nodeDecorator.setLastModified() then: - 0 * node.setProperty(JCR_LASTMODIFIED, _) + 1 * node.setProperty(JCR_LASTMODIFIED, _) notThrown(RepositoryException) } - def "During setLastModified() when something goes wrong with getPrimaryNodeType() we handle this case gracefully"() { + def "setLastModified() when last modified can not be set"() { given: Node node = Mock(Node) { - getPrimaryNodeType() >> { throw new RepositoryException() } + getPrimaryNodeType() >> Mock(NodeType) { + canSetProperty(JCR_LASTMODIFIED, _) >> { false } + } + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + getString() >> 'nt:unstructured' + } + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } } when: - final nodeDecorator = new JcrNodeDecorator(node) + final nodeDecorator = new JCRNodeDecorator(node) nodeDecorator.setLastModified() then: + 0 * node.setProperty(JCR_LASTMODIFIED, _) notThrown(RepositoryException) } - def "getPrimaryType()"() { + def "During setLastModified() when something goes wrong with getPrimaryNodeType() we handle this case gracefully"() { given: Node node = Mock(Node) { + getPrimaryNodeType() >> { throw new RepositoryException() } getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { - getString() >> { JcrConstants.NT_FILE } + getString() >> 'nt:unstructured' + } + getProperties() >> Mock(PropertyIterator) { + toList() >> [] } } when: - final nodeDecorator = new JcrNodeDecorator(node) + final nodeDecorator = new JCRNodeDecorator(node) + nodeDecorator.setLastModified() then: - nodeDecorator.getPrimaryType() == JcrConstants.NT_FILE + notThrown(RepositoryException) } - def "isRequiredNode()"() { + def "isMandatoryNode()"() { given: Node node = Mock(Node) { getDefinition() >> Mock(NodeDefinition) { isMandatory() >> isMandatory } + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + getString() >> 'nt:unstructured' + } + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } } when: - final nodeDecorator = new JcrNodeDecorator(node) + final nodeDecorator = new JCRNodeDecorator(node) then: - nodeDecorator.isRequiredNode() == isMandatory + nodeDecorator.isMandatoryNode() == isMandatory where: isMandatory << [true, false] @@ -148,10 +254,16 @@ class JCRNodeDecoratorSpec extends Specification { Mock(ItemDefinition) { isMandatory() >> secondDefinition } as NodeDefinition ] } + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + getString() >> 'nt:unstructured' + } + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } } when: - final nodeDecorator = new JcrNodeDecorator(node) + final nodeDecorator = new JCRNodeDecorator(node) then: nodeDecorator.hasMandatoryChildNodes() == hasMandatoryChildNodes @@ -166,44 +278,132 @@ class JCRNodeDecoratorSpec extends Specification { def "getRequiredChildNodes()"() { given: - Node node = Mock(Node) { + Node nodeWithMandatoryChildren = Mock(Node) { getNodes() >> Mock(NodeIterator) { hasNext() >>> true >> false next() >> Mock(Node) { + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + getString() >> 'nt:resource' + } + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } getDefinition() >> Mock(NodeDefinition) { isMandatory() >> true } } } + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + getString() >> 'nt:file' + } + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } } - when: "The node has children" - final nodeDecorator = Spy(JcrNodeDecorator, constructorArgs: [node]) { + Node authorizableNode = Mock(Node) { + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + getString() >> 'rep:User' + } + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } + } + + when: "The node has mandatory children" + final nodeDecorator = Spy(JCRNodeDecorator, constructorArgs: [nodeWithMandatoryChildren]) { hasMandatoryChildNodes() >> true + isAuthorizableType() >> false } then: nodeDecorator.getRequiredChildNodes().size() == 1 - and: "If no child nodes, getRequiredChildNodes() returns null" + and: "The node has authorizable pieces" + + when: + final otherNodeDecorator = Spy(JCRNodeDecorator, constructorArgs: [authorizableNode]) { + hasMandatoryChildNodes() >> false + isAuthorizableType() >> true + getChildNodeIterator() >> Mock(Iterator) { + hasNext() >>> true >> true >> true >> true >> false + next() >>> + Mock(Node) { + getName() >> "/home/users/u/user/preferences" + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + getString() >> 'nt:unstructured' + } + getProperty('sling:resourceType') >> Mock(Property) { + getString() >> 'cq:Preferences' + } + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } + } >> + Mock(Node) { + getName() >> "/home/users/u/user/profile" + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + getString() >> 'nt:unstructured' + } + getProperty('sling:resourceType') >> Mock(Property) { + getString() >> 'cq/security/components/profile' + } + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } + } >> + Mock(Node) { + getName() >> "/home/users/u/user/.tokens" + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + getString() >> 'rep:Unstructured' + } + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } + } >> + Mock(Node) { + getName() >> "/home/users/u/user/rep:policy" + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + getString() >> 'rep:ACL' + } + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } + } + } + } + + then: + otherNodeDecorator.getRequiredChildNodes().size() == 2 + + and: "If no child nodes, getRequiredChildNodes() returns an empty collection" when: - final otherNodeDecorator = Spy(JcrNodeDecorator, constructorArgs: [Mock(Node)]) { + final yetAnotherNodeDecorator = Spy(JCRNodeDecorator, constructorArgs: [nodeWithMandatoryChildren]) { hasMandatoryChildNodes() >> false + isAuthorizableType() >> false } then: - otherNodeDecorator.getRequiredChildNodes() == null + yetAnotherNodeDecorator.getRequiredChildNodes() == [] } def "Can adapt the decorator back to the wrapped node"() { given: - final node = Mock(Node) - final nodeDecorator = new JcrNodeDecorator(node) + final node = Mock(Node) { + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + getString() >> 'nt:unstructured' + } + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } + } + final nodeDecorator = new JCRNodeDecorator(node) expect: (nodeDecorator as Node) == node + (nodeDecorator as JCRNodeDecorator) == nodeDecorator } def "Get modified date for a node"() { @@ -221,8 +421,14 @@ class JCRNodeDecoratorSpec extends Specification { getProperty(JCR_CREATED) >> Mock(Property) { getDate() >> jcrCreatedDate } + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + getString() >> 'nt:unstructured' + } + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } } - final nodeDecorator = new JcrNodeDecorator(node) + final nodeDecorator = new JCRNodeDecorator(node) expect: nodeDecorator.getModifiedOrCreatedDate() == modifiedDate @@ -234,4 +440,189 @@ class JCRNodeDecoratorSpec extends Specification { false | false | true | jcrCreatedDate.time false | false | false | null } + + def "isAuthorizablePart()"() { + given: + final nodeThatIsPart = Mock(Node) { + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } + getParent() >> Mock(Node) { + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + getString() >> "nt:unstructured" + } + getParent() >> Mock(Node) { + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + getString() >> "rep:User" + } + } + } + } + + final nodeThatIsNotPart = Mock(Node) { + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } + getParent() >> Mock(Node) { + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + getString() >> "nt:unstructured" + } + getParent() >> { throw new ItemNotFoundException() } + } + } + + when: + final JCRNodeDecorator jcrNodeDecorator = new JCRNodeDecorator(nodeThatIsPart) + + then: + jcrNodeDecorator.isAuthorizablePart() + + and: "!isAuthorizablePart()" + + when: + final JCRNodeDecorator jcrNodeDecoratorTwo = new JCRNodeDecorator(nodeThatIsNotPart) + + then: + !jcrNodeDecoratorTwo.isAuthorizablePart() + } + + @Unroll + def "isAuthorizableType() for primary type #primaryType is expected #expected"() { + given: + final node = Mock(Node) { + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + getString() >> primaryType + } + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } + } + + when: + final JCRNodeDecorator jcrNodeDecorator = new JCRNodeDecorator(node) + + then: + jcrNodeDecorator.isAuthorizableType() == expected + + where: + primaryType | expected + 'rep:User' | true + 'rep:Group' | true + 'unknown' | false + } + + + @Unroll + def "isLoginToken() for primary type #primaryType and name #name is expected #expected"() { + given: + final node = Mock(Node) { + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + getString() >> primaryType + } + getName() >> name + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } + } + + when: + final JCRNodeDecorator jcrNodeDecorator = new JCRNodeDecorator(node) + + then: + jcrNodeDecorator.isLoginToken() == expected + + where: + primaryType | name | expected + 'rep:Unstructured' | '/home/users/u/user/.tokens' | true + 'nt:unstructured' | '/home/users/u/user/.tokens' | false + 'rep:Unstructured' | '/home/users/u/user/other' | false + 'rep:Token' | '/home/users/u/user/.tokens/token' | true + 'unknown' | 'unknown' | false + } + + + def "equals()"() { + given: + final JCRNodeDecorator decoratorOne = new JCRNodeDecorator(Mock(Node){ + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } + getName() >> 'decoratorOne' + }) + + final JCRNodeDecorator otherDecoratorOneInstance = new JCRNodeDecorator(Mock(Node){ + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } + getName() >> 'decoratorOne' + }) + + final JCRNodeDecorator decoratorTwo = new JCRNodeDecorator(Mock(Node){ + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } + getName() >> 'decoratorTwo' + }) + + expect: + decoratorOne.equals(decoratorOne) + decoratorOne.equals(otherDecoratorOneInstance) + !decoratorOne.equals(decoratorTwo) + !decoratorOne.equals(Mock(Node)) + } + + + def "NoRootInclusionPolicy behavior"() { + given: + final rootNode = Mock(Node) { + getName() >> "/path/root" + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } + } + + when: + final NoRootInclusionPolicy policy = new NoRootInclusionPolicy(rootNode) + + then: + policy.include(node) == expectedValue + + where: + node << [ + Mock(Node) { + getName() >> "/path/root" + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } + }, + Mock(Node) { + getName() >> "/path/root/node" + getDefinition() >> Mock(NodeDefinition) { + isMandatory() >> false + } + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } + }, + Mock(Node) { + getName() >> "/path/root/node" + getDefinition() >> Mock(NodeDefinition) { + isMandatory() >> true + } + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } + } + ] + expectedValue << [false, true, false] + } } diff --git a/src/test/groovy/com/twcable/grabbit/jcr/JcrPropertyDecoratorSpec.groovy b/src/test/groovy/com/twcable/grabbit/jcr/JcrPropertyDecoratorSpec.groovy index b2c110b..ca7e326 100644 --- a/src/test/groovy/com/twcable/grabbit/jcr/JcrPropertyDecoratorSpec.groovy +++ b/src/test/groovy/com/twcable/grabbit/jcr/JcrPropertyDecoratorSpec.groovy @@ -16,10 +16,11 @@ package com.twcable.grabbit.jcr -import spock.lang.Specification - import javax.jcr.Property import javax.jcr.nodetype.PropertyDefinition +import spock.lang.Specification +import spock.lang.Unroll + import static org.apache.jackrabbit.JcrConstants.JCR_LASTMODIFIED import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES @@ -28,6 +29,7 @@ import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE @SuppressWarnings("GroovyAssignabilityCheck") class JcrPropertyDecoratorSpec extends Specification { + @Unroll def "check if property is transferable"() { given: Property property = Mock(Property) { @@ -36,19 +38,26 @@ class JcrPropertyDecoratorSpec extends Specification { isProtected() >> protectedFlag } } + final nodeOwner = Mock(JCRNodeDecorator) { + isAuthorizableType() >> authorizableType + } when: - final propertyDecorator = new JcrPropertyDecorator(property) + final propertyDecorator = new JcrPropertyDecorator(property, nodeOwner) then: expectedOutput == propertyDecorator.isTransferable() where: - propertyName | protectedFlag | expectedOutput - JCR_LASTMODIFIED | true | false - JCR_PRIMARYTYPE | false | true - JCR_MIXINTYPES | false | true - "otherProperty" | true | false - "otherProperty" | false | true + propertyName | protectedFlag | expectedOutput | authorizableType + JCR_LASTMODIFIED | true | false | false + JCR_PRIMARYTYPE | false | true | false + JCR_MIXINTYPES | false | true | false + 'otherProperty' | true | false | false + 'otherProperty' | false | true | false + 'protectedProperty' | true | true | true + 'protectedProperty' | true | true | true + + } } diff --git a/src/test/groovy/com/twcable/grabbit/jcr/ProtoNodeDecoratorSpec.groovy b/src/test/groovy/com/twcable/grabbit/jcr/ProtoNodeDecoratorSpec.groovy index 4b1c3dd..3ce6735 100644 --- a/src/test/groovy/com/twcable/grabbit/jcr/ProtoNodeDecoratorSpec.groovy +++ b/src/test/groovy/com/twcable/grabbit/jcr/ProtoNodeDecoratorSpec.groovy @@ -15,146 +15,80 @@ */ package com.twcable.grabbit.jcr -import com.day.cq.commons.jcr.JcrConstants +import com.twcable.grabbit.proto.NodeProtos import com.twcable.grabbit.proto.NodeProtos.Node as ProtoNode - -/* - * Copyright 2015 Time Warner Cable, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ import com.twcable.grabbit.proto.NodeProtos.Node.Builder as ProtoNodeBuilder -import com.twcable.grabbit.proto.NodeProtos.Property as ProtoProperty -import com.twcable.grabbit.proto.NodeProtos.Value as ProtoValue import spock.lang.Specification -import javax.jcr.Node -import javax.jcr.Session import static javax.jcr.PropertyType.STRING -import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE @SuppressWarnings("GroovyAccessibility") class ProtoNodeDecoratorSpec extends Specification { - ProtoNode decoratedProtoNode + def "ProtoNodeDecorator can not be constructed with a null ProtoNode"() { + when: + ProtoNodeDecorator.createFrom(null) + then: + thrown(IllegalArgumentException) + } - def setup() { - ProtoNodeBuilder nodeBuilder = ProtoNode.newBuilder() - nodeBuilder.setName("somenode") - ProtoProperty primaryTypeProperty = ProtoProperty + def "Can create a regular DefaultProtoNodeDecorator"() { + given: + ProtoNodeBuilder nodeBuilder = ProtoNode.newBuilder() + nodeBuilder.setName("user") + NodeProtos.Property primaryTypeProperty = NodeProtos.Property .newBuilder() .setName(JCR_PRIMARYTYPE) .setType(STRING) .setMultiple(false) - .addValues(ProtoValue.newBuilder().setStringValue(JcrConstants.NT_UNSTRUCTURED)) + .addValues(NodeProtos.Value.newBuilder().setStringValue('nt:unstructured')) .build() nodeBuilder.addProperties(primaryTypeProperty) + final protoNodeDecorator = ProtoNodeDecorator.createFrom(nodeBuilder.build()) - ProtoProperty mixinTypeProperty = ProtoProperty - .newBuilder() - .setName(JCR_MIXINTYPES) - .setType(STRING) - .setMultiple(true) - .addAllValues( - [ - ProtoValue.newBuilder().setStringValue("somemixintype").build(), - ProtoValue.newBuilder().setStringValue("unwritablemixin").build() - ] - ) - .build() - nodeBuilder.addProperties(mixinTypeProperty) + expect: + protoNodeDecorator instanceof DefaultProtoNodeDecorator + } - ProtoProperty someOtherProperty = ProtoProperty + def "Can create an AuthorizableProtoNodeDecorator with wrapped User node"() { + given: + NodeProtos.Node.Builder nodeBuilder = NodeProtos.Node.newBuilder() + nodeBuilder.setName("user") + NodeProtos.Property userProperty = NodeProtos.Property .newBuilder() - .setName("someproperty") + .setName(JCR_PRIMARYTYPE) .setType(STRING) .setMultiple(false) - .addValues(ProtoValue.newBuilder().setStringValue("somevalue")) + .addValues(NodeProtos.Value.newBuilder().setStringValue('rep:User')) .build() - nodeBuilder.addProperties(someOtherProperty) + nodeBuilder.addProperties(userProperty) + final protoNodeDecorator = DefaultProtoNodeDecorator.createFrom(nodeBuilder.build()) - decoratedProtoNode = nodeBuilder.build() + expect: + protoNodeDecorator instanceof AuthorizableProtoNodeDecorator } - def "ProtoNodeDecorator can not be constructed with a null ProtoNode"() { - when: - new ProtoNodeDecorator(null) - - then: - thrown(IllegalArgumentException) - } - - - def "Can get primary type"() { - when: - final protoNodeDecorator = new ProtoNodeDecorator(decoratedProtoNode) - - then: - protoNodeDecorator.getPrimaryType() == JcrConstants.NT_UNSTRUCTURED - } - - - def "can get mixin property"() { - given: - final protoNodeDecorator = new ProtoNodeDecorator(decoratedProtoNode) - - when: - final property = protoNodeDecorator.getMixinProperty() - - then: - property.valuesCount == 2 - property.name == JCR_MIXINTYPES - } - - - def "Can get just writable properties"() { + def "Can create an AuthorizableProtoNodeDecorator with wrapped Group node"() { given: - final protoNodeDecorator = new ProtoNodeDecorator(decoratedProtoNode) - - when: - final properties = protoNodeDecorator.getWritableProperties() - - then: - properties.size() == 1 - properties[0].value.stringValue == "somevalue" - } - - - def "Can write the decorated node to the JCR"() { - given: - final session = Mock(Session) - final node = Mock(Node) { - canAddMixin("somemixintype") >> { true } - canAddMixin("unwritablemixin") >> { false } - } - - final protoNodeDecorator = Spy(ProtoNodeDecorator, constructorArgs: [decoratedProtoNode]) { - getOrCreateNode(session) >> { node } - } - - when: - protoNodeDecorator.writeToJcr(session) + NodeProtos.Node.Builder nodeBuilder = NodeProtos.Node.newBuilder() + nodeBuilder.setName("group") + NodeProtos.Property groupProperty = NodeProtos.Property + .newBuilder() + .setName(JCR_PRIMARYTYPE) + .setType(STRING) + .setMultiple(false) + .addValues(NodeProtos.Value.newBuilder().setStringValue('rep:Group')) + .build() + nodeBuilder.addProperties(groupProperty) + final protoNodeDecorator = DefaultProtoNodeDecorator.createFrom(nodeBuilder.build()) - then: - //Only one mixin should be valid - 1 * node.addMixin("somemixintype") - //Only one other property that needs to be written - 1 * node.setProperty(_, _, _) + expect: + protoNodeDecorator instanceof AuthorizableProtoNodeDecorator } } diff --git a/src/test/groovy/com/twcable/grabbit/security/AuthorizablePrincipalSpec.groovy b/src/test/groovy/com/twcable/grabbit/security/AuthorizablePrincipalSpec.groovy new file mode 100644 index 0000000..abc9b16 --- /dev/null +++ b/src/test/groovy/com/twcable/grabbit/security/AuthorizablePrincipalSpec.groovy @@ -0,0 +1,36 @@ +/* + * Copyright 2015 Time Warner Cable, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.twcable.grabbit.security + +import spock.lang.Specification + +class AuthorizablePrincipalSpec extends Specification { + + def "Can get the name of an AuthorizablePrincipal"() { + given: + final authorizablePrincipal = new AuthorizablePrincipal('principalName') + + expect: + authorizablePrincipal.getName() == 'principalName' + } + + def "One AuthorizablePrincipal is equal to another"() { + expect: + assert new AuthorizablePrincipal('one').equals(new AuthorizablePrincipal('one')) + assert !(new AuthorizablePrincipal('two').equals(new AuthorizablePrincipal('three'))) + assert !(new AuthorizablePrincipal('two').equals(null)) + } +} diff --git a/src/test/groovy/com/twcable/grabbit/server/batch/steps/jcrnodes/JcrNodesProcessorSpec.groovy b/src/test/groovy/com/twcable/grabbit/server/batch/steps/jcrnodes/JcrNodesProcessorSpec.groovy index e951d42..780ee67 100644 --- a/src/test/groovy/com/twcable/grabbit/server/batch/steps/jcrnodes/JcrNodesProcessorSpec.groovy +++ b/src/test/groovy/com/twcable/grabbit/server/batch/steps/jcrnodes/JcrNodesProcessorSpec.groovy @@ -19,12 +19,14 @@ package com.twcable.grabbit.server.batch.steps.jcrnodes import com.twcable.grabbit.proto.NodeProtos import com.twcable.jackalope.NodeBuilder as FakeNodeBuilder import com.twcable.jackalope.impl.jcr.ValueImpl +import javax.jcr.ItemNotFoundException import org.apache.jackrabbit.JcrConstants import org.apache.jackrabbit.commons.iterator.NodeIteratorAdapter import org.apache.jackrabbit.commons.iterator.PropertyIteratorAdapter import spock.lang.Specification import spock.lang.Subject import javax.jcr.Node as JcrNode +import javax.jcr.Property import javax.jcr.Property as JcrProperty import javax.jcr.PropertyIterator import javax.jcr.nodetype.NodeDefinition @@ -39,6 +41,8 @@ import javax.jcr.NodeIterator import spock.lang.Shared import spock.lang.Unroll +import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE + @Subject(JcrNodesProcessor) class JcrNodesProcessorSpec extends Specification { @@ -157,6 +161,10 @@ class JcrNodesProcessorSpec extends Specification { getPrimaryNodeType() >> Mock(NodeType) { getChildNodeDefinitions() >> childDefinitions.toArray() } + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + getString() >> primaryType + } + getParent() >> { throw new ItemNotFoundException() } } children.each { JcrNode child -> @@ -201,6 +209,7 @@ class JcrNodesProcessorSpec extends Specification { JcrNode node = Mock(JcrNode) { //mocks a parent with updated lastModified property getPath() >> "testParent" + getParent() >> { throw new ItemNotFoundException() } getProperties() >> propertyIterator(primaryTypeProperty(JcrConstants.NT_FILE)) hasProperty(JcrConstants.JCR_LASTMODIFIED) >> true getProperty(JcrConstants.JCR_LASTMODIFIED) >> Mock(JcrProperty){ @@ -214,6 +223,9 @@ class JcrNodesProcessorSpec extends Specification { isMandatory() >> true //has mandatory child } } + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + getString() >> 'nt:file' + } getNodes() >> Mock(NodeIterator) { hasNext() >>> true >> false @@ -228,6 +240,9 @@ class JcrNodesProcessorSpec extends Specification { isMandatory() >> false //has no children } } + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + getString() >> 'nt:unstructured' + } } } } diff --git a/src/test/groovy/com/twcable/grabbit/server/services/RootNodeWithMandatoryIteratorSpec.groovy b/src/test/groovy/com/twcable/grabbit/server/services/RootNodeWithMandatoryIteratorSpec.groovy index d35c857..79b64bc 100644 --- a/src/test/groovy/com/twcable/grabbit/server/services/RootNodeWithMandatoryIteratorSpec.groovy +++ b/src/test/groovy/com/twcable/grabbit/server/services/RootNodeWithMandatoryIteratorSpec.groovy @@ -23,11 +23,14 @@ import spock.lang.Subject import javax.jcr.Node import javax.jcr.Node as JcrNode import javax.jcr.NodeIterator +import javax.jcr.Property +import javax.jcr.PropertyIterator import javax.jcr.nodetype.NodeDefinition import javax.jcr.nodetype.NodeType import static com.twcable.jackalope.JCRBuilder.node import static com.twcable.jackalope.JCRBuilder.property +import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE @Subject(RootNodeWithMandatoryIterator) class RootNodeWithMandatoryIteratorSpec extends Specification { @@ -89,6 +92,12 @@ class RootNodeWithMandatoryIteratorSpec extends Specification { isMandatory() >> true } getPrimaryNodeType() >> nodeTypeNoMandatory + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + getString() >> 'nt:unstructured' + } + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } } final child1 = Mock(Node) { @@ -100,12 +109,24 @@ class RootNodeWithMandatoryIteratorSpec extends Specification { hasNext() >>> [true, false] next() >> child3 } + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + getString() >> 'nt:unstructured' + } + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } } final child2 = Mock(Node) { getDefinition() >> Mock(NodeDefinition) { isMandatory() >> false } + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + getString() >> 'nt:unstructured' + } + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } } final rootNode = Mock(Node) { @@ -114,6 +135,12 @@ class RootNodeWithMandatoryIteratorSpec extends Specification { next() >>> [child1, child2] } getPrimaryNodeType() >> nodeTypeWithMandatory + getProperty(JCR_PRIMARYTYPE) >> Mock(Property) { + getString() >> 'nt:unstructured' + } + getProperties() >> Mock(PropertyIterator) { + toList() >> [] + } } when: diff --git a/src/test/groovy/com/twcable/grabbit/testutil/StubInputStream.groovy b/src/test/groovy/com/twcable/grabbit/testutil/StubInputStream.groovy new file mode 100644 index 0000000..00ce366 --- /dev/null +++ b/src/test/groovy/com/twcable/grabbit/testutil/StubInputStream.groovy @@ -0,0 +1,55 @@ +/* + * Copyright 2015 Time Warner Cable, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.twcable.grabbit.testutil + +import javax.annotation.Nonnull +import javax.servlet.ServletInputStream + +class StubInputStream extends ServletInputStream { + + + private final int byte_length + private final byte[] bytes + private int byte_index = 0 + + private StubInputStream(@Nonnull final String data) { + bytes = data as byte[] + byte_length = bytes.length + } + + private StubInputStream(@Nonnull final File fromFile) { + bytes = fromFile.readBytes() + byte_length = bytes.length + } + + static InputStream inputStream(@Nonnull final String data) { + return new StubInputStream(data) + } + + static InputStream inputStream(@Nonnull final File fromFile) { + return new StubInputStream(fromFile) + } + + @Override + int read() throws IOException { + if (byte_index <= byte_length - 1) { + final thisByte = bytes[byte_index] as int + byte_index++ + return thisByte + } + return -1 + } +}