Initial commit for solr 6.x plugin

git-svn-id: https://svn.apache.org/repos/asf/manifoldcf/integration/solr-6.x/trunk@1737771 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/CHANGES.txt b/CHANGES.txt
new file mode 100644
index 0000000..03c4536
--- /dev/null
+++ b/CHANGES.txt
@@ -0,0 +1,7 @@
+Apache ManifoldCF Plugin for Apache Solr 5.x change Log
+$Id$
+
+======================= 2.3-dev ======================
+
+Initial commit.
+(Karl Wrigh)
diff --git a/DEPENDENCIES.txt b/DEPENDENCIES.txt
new file mode 100644
index 0000000..eba48be
--- /dev/null
+++ b/DEPENDENCIES.txt
@@ -0,0 +1,11 @@
+Apache ManifoldCF Plugin for Apache Solr 6.x requires
+---------------------------------------------------
+* JRE 1.8 or above
+* mvn 3.0 or higher
+
+For building and running Apache ManifoldCF Plugin for Apache Solr 6.x:
+------------------------------------------------------------------
+
+* Look at README.txt
+
+
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..66a27ec
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,177 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
diff --git a/NOTICE.txt b/NOTICE.txt
new file mode 100644
index 0000000..8263e38
--- /dev/null
+++ b/NOTICE.txt
@@ -0,0 +1,5 @@
+Apache ManifoldCF Plugin for Apache Solr 6.x
+Copyright 2010-2016 The Apache Software Foundation
+
+This product includes software developed by
+The Apache Software Foundation (http://www.apache.org/).
diff --git a/README.txt b/README.txt
new file mode 100644
index 0000000..901277f
--- /dev/null
+++ b/README.txt
@@ -0,0 +1,231 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+
+Compatibility
+------------
+
+This version of this component is fully functional with Apache ManifoldCF 1.6 and
+above.  It is backwards compatible with earlier versions as well, except for the fact
+that two additional Solr fields are required for this plugin to work.
+
+
+Instructions for Building Apache ManifoldCF Plugin for Apache Solr 6.x from Source
+------------------------------------------------------------------------------
+
+1. Download the Java SE 8 JDK (Java Development Kit), or greater, from
+   http://www.oracle.com/technetwork/java/index.html.
+   You will need the JDK installed, and the %JAVA_HOME%\bin directory included
+   on your command path.  To test this, issue a "java -version" command from your
+   shell and verify that the Java version is 1.8 or greater.
+
+2. Download and install Maven 3.0 or later.  Maven installation and configuration
+   instructions can be found here:  http://maven.apache.org
+
+3. Building distribution assemblies
+
+   Execute the following command in order to build the distribution assemblies
+
+   mvn package assembly:assembly
+
+   The JAR package can be found in the target folder:
+
+   target/apache-manifoldcf-solr-6.x-plugin-<VERSION>.jar where <VERSION> is the release version
+
+
+Getting Started
+---------------
+
+There are two ways to hook up security to Solr in this package.  The first is using
+a Query Parser plugin.  The second is using a Search Component.  In both cases,
+the first step is to have ManifoldCF installed and running.  See:
+http://manifoldcf.apache.org/release/trunk/en_US/how-to-build-and-deploy.html
+
+Then, in order to store the document authorization information you need to add
+'allow' and 'deny' fields for documents, parents, and shares to your Solr
+index. Depending on your schemaFactory setting in solrconfig.xml you have to 
+use either the schema.xml file for 'ClassicIndexSchemaFactory' or the schema
+API for 'ManagedIndexSchemaFactory'. 
+See section 'Managed Schema Definition in SolrConfig' in the Solr Reference Gude
+https://www.apache.org/dyn/closer.cgi/lucene/solr/ref-guide/apache-solr-ref-guide-5.0.pdf
+
+For schema.xml simply add the following field definitions to the <schema>
+section:
+
+  <field name="allow_token_document" type="string" indexed="true" stored="true"
+    multiValued="true" required="true" default="__nosecurity__"/>
+  <field name="allow_token_parent" type="string" indexed="true" stored="true"
+    multiValued="true" required="true" default="__nosecurity__"/>
+  <field name="allow_token_share" type="string" indexed="true" stored="true"
+    multiValued="true" required="true" default="__nosecurity__"/>
+  <field name="deny_token_document" type="string" indexed="true" stored="true"
+    multiValued="true" required="true" default="__nosecurity__"/>
+  <field name="deny_token_parent" type="string" indexed="true" stored="true"
+    multiValued="true" required="true" default="__nosecurity__"/>
+  <field name="deny_token_share" type="string" indexed="true" stored="true"
+    multiValued="true" required="true" default="__nosecurity__"/>
+
+To define the fields via schema API use the curl command instead:
+
+curl -X POST -H 'Content-type:application/json' --data-binary '{ 
+  "add-field" : [
+  { "name":"allow_token_document", "type":"string", "indexed":"true",
+    "stored":"true", "multiValued":"true", "required":"true",
+    "default":"__nosecurity__" },
+  { "name":"allow_token_parent",   "type":"string", "indexed":"true",
+    "stored":"true", "multiValued":"true", "required":"true",
+    "default":"__nosecurity__" },
+  { "name":"allow_token_share",    "type":"string", "indexed":"true",
+    "stored":"true", "multiValued":"true", "required":"true", 
+    "default":"__nosecurity__" },
+  { "name":"deny_token_document",  "type":"string", "indexed":"true",
+    "stored":"true", "multiValued":"true", "required":"true",
+    "default":"__nosecurity__" },
+  { "name":"deny_token_parent",    "type":"string", "indexed":"true",
+    "stored":"true", "multiValued":"true", "required":"true",
+    "default":"__nosecurity__" },
+  { "name":"deny_token_share",     "type":"string", "indexed":"true",
+    "stored":"true", "multiValued":"true", "required":"true",
+    "default":"__nosecurity__"}
+]}' http://localhost:8983/solr/<collection_name>/schema
+
+Replace <collection_name> with your core or collection name respectively.
+
+The default value of "__nosecurity__" is manadatory because the queries will be
+rewritten to use all of these 6 fields. If a field is missing in the index then
+you will get no results for your search.
+
+Check the field definitions with
+
+curl http://localhost:8983/solr/<collection_name>/schema/fields
+
+
+Using the Query Parser Plugin
+-----------------------------
+
+To set up the query parser plugin, modify your solrconfig.xml to add the query parser:
+
+  <!-- ManifoldCF document security enforcement component -->
+  <queryParser name="manifoldCFSecurity"
+    class="org.apache.solr.mcf.ManifoldCFQParserPlugin">
+    <str name="AuthorityServiceBaseURL">http://localhost:8345/mcf-authority-service</str>
+    <int name="ConnectionPoolSize">50</int>
+  </queryParser>
+
+Hook up the search component in the solrconfig.xml file wherever you want it, e.g.:
+
+<requestHandler name="/select" class="solr.SearchHandler">
+  <lst name="appends">
+    <str name="fq">{!manifoldCFSecurity}</str>
+  </lst>
+  ...
+</requestHandler>
+
+
+Using the Search Component
+--------------------------
+
+To set up the search component, modify your solrconfig.xml to add the search component:
+
+  <!-- ManifoldCF document security enforcement component -->
+  <searchComponent name="manifoldCFSecurity"
+    class="org.apache.solr.mcf.ManifoldCFSearchComponent">
+    <str name="AuthorityServiceBaseURL">http://localhost:8345/mcf-authority-service</str>
+    <int name="ConnectionPoolSize">50</int>
+  </searchComponent>
+
+Hook up the search component in the solrconfig.xml file wherever you want it, e.g.:
+
+<requestHandler name="/select" class="solr.SearchHandler">
+  <arr name="last-components">
+    <str>manifoldCFSecurity</str>
+  </arr>
+  ...
+</requestHandler>
+
+
+Supplying authenticated usernames and domains
+----------------------------------------------
+
+This component looks for the following parameters in the Solr request object:
+
+AuthenticatedUserName
+AuthenticatedUserDomain
+AuthenticatedUserName_XX
+AuthenticatedUserDomain_XX
+
+At a minimum, AuthenticatedUserName must be present in order for the component to communicate with
+the ManifoldCF Authority Service and obtain user access tokens.  In that case, the user identity will consist
+of one user and one authorization domain.  If AuthenticatedUserDomain is not set, then the authorization domain
+chosen will be the standard default domain, an empty string.
+
+If you need multiple user/domain tuples for the user identity, you may pass these as parameter pairs starting with
+AuthenticatedUserName_0 and AuthenticatedUserDomain_0, and counting up as high as you like.
+
+Operation in conjunction with mod-authz-annotate
+------------------------------------------------
+
+An optional component of ManifoldCF can be built and deployed as part of Apache - mod-authz-annotate.  The
+mod-authz-annotate module obtains the Kerberos principal from the Kerberos tickets present if mod-auth-kerb is used, and uses
+the MCF Authority Service to look up the appropriate access tokens.  If you choose to use that architecture,
+you will still need to use this Solr component to modify the user query.  All you have to do is the following:
+
+- Make sure you do not set AuthenticatedUserName or AuthenticatedUserName_0 in the request
+- Make sure the HTTP request from Apache to Solr translates all AAAGRP header values into "UserToken" parameters
+   for the Solr request
+
+
+
+Licensing
+---------
+
+Apache ManifoldCF Plugin for Apache Solr 6.x is licensed under the
+Apache License 2.0. See the files called LICENSE.txt and NOTICE.txt
+for more information.
+
+Cryptographic Software Notice
+-----------------------------
+
+This distribution may include software that has been designed for use
+with cryptographic software. The country in which you currently reside
+may have restrictions on the import, possession, use, and/or re-export
+to another country, of encryption software. BEFORE using any encryption
+software, please check your country's laws, regulations and policies
+concerning the import, possession, or use, and re-export of encryption
+software, to see if this is permitted. See <http://www.wassenaar.org/>
+for more information.
+
+The U.S. Government Department of Commerce, Bureau of Industry and
+Security (BIS), has classified this software as Export Commodity
+Control Number (ECCN) 5D002.C.1, which includes information security
+software using or performing cryptographic functions with asymmetric
+algorithms. The form and manner of this Apache Software Foundation
+distribution makes it eligible for export under the License Exception
+ENC Technology Software Unrestricted (TSU) exception (see the BIS
+Export Administration Regulations, Section 740.13) for both object
+code and source code.
+
+The following provides more details on the included software that
+may be subject to export controls on cryptographic software:
+
+  The Apache ManifoldCF Solr 6.x Plugin does not include any
+  implementation or usage of cryptographic software at this time.
+  
+Contact
+-------
+
+  o For general information visit the main project site at
+    http://manifoldcf.apache.org
+
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..910c8be
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,125 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements.  See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+
+  <modelVersion>4.0.0</modelVersion>
+  <groupId>org.apache.manifoldcf.solr</groupId>
+
+  <name>ManifoldCF Solr 6.x Plugin</name>
+  <artifactId>apache-manifoldcf-solr-6.x-plugin</artifactId>
+  <version>2.3-SNAPSHOT</version>
+  <packaging>jar</packaging>
+  <description>ManifoldCF Plugin for Apache Solr 6.x</description>
+  <inceptionYear>2015</inceptionYear>
+
+  <organization>
+    <name>The Apache Software Foundation</name>
+    <url>http://www.apache.org/</url>
+  </organization>
+
+  <properties>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+    <solr.version>6.0.0</solr.version>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.solr</groupId>
+      <artifactId>solr-test-framework</artifactId>
+      <version>${solr.version}</version>
+      <type>jar</type>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.solr</groupId>
+      <artifactId>solr-core</artifactId>
+      <version>${solr.version}</version>
+      <type>jar</type>
+      <scope>compile</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>commons-logging</groupId>
+      <artifactId>commons-logging</artifactId>
+      <version>1.1.1</version>
+      <type>jar</type>
+      <scope>test</scope>
+    </dependency>
+
+  </dependencies>
+  
+  <build>
+
+    <plugins>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-surefire-plugin</artifactId>
+        <version>2.18.1</version>
+        <configuration>
+          <argLine>-Xmx1024m</argLine>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <version>3.2</version>
+        <configuration>
+          <encoding>utf-8</encoding>
+          <source>1.7</source>
+          <target>1.7</target>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <artifactId>maven-jar-plugin</artifactId>
+        <version>2.5</version>
+        <configuration>
+          <archive>
+            <manifestEntries>
+              <Specification-Title>${project.name}</Specification-Title>
+              <Specification-Version>${project.version}</Specification-Version>
+              <Specification-Vendor>The Apache Software Foundation</Specification-Vendor>
+              <Implementation-Title>${project.name}</Implementation-Title>
+              <Implementation-Version>${project.version}</Implementation-Version>
+              <Implementation-Vendor>The Apache Software Foundation</Implementation-Vendor>
+              <Implementation-Vendor-Id>org.apache</Implementation-Vendor-Id>
+              <url>${project.url}</url>
+            </manifestEntries>
+          </archive>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <artifactId>maven-assembly-plugin</artifactId>
+        <version>2.5.3</version>
+        <configuration>
+          <descriptors>
+            <descriptor>src/main/assembly/bin.xml</descriptor>
+            <descriptor>src/main/assembly/src.xml</descriptor>
+          </descriptors>
+          <tarLongFileMode>gnu</tarLongFileMode>
+        </configuration>
+      </plugin>
+
+    </plugins>
+  </build>
+
+</project>
\ No newline at end of file
diff --git a/src/main/assembly/bin.xml b/src/main/assembly/bin.xml
new file mode 100644
index 0000000..8988d4b
--- /dev/null
+++ b/src/main/assembly/bin.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements.  See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You 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.
+-->
+<assembly>
+    <id>bin</id>
+    <formats>
+        <format>tar.gz</format>
+        <format>zip</format>
+    </formats>
+    
+    <fileSets>
+        
+        <fileSet>
+            <directory>target</directory>
+            <outputDirectory/>
+            <fileMode>644</fileMode>
+            <directoryMode>755</directoryMode>
+            <includes>
+                <include>${project.artifactId}-${project.version}.jar</include>
+            </includes>
+        </fileSet>
+        
+        <fileSet>
+            <directory>.</directory>
+            <outputDirectory/>
+            <fileMode>644</fileMode>
+            <directoryMode>755</directoryMode>
+            <includes>
+                <include>README.txt</include>
+                <include>LICENSE.txt</include>
+                <include>NOTICE.txt</include>
+                <include>CHANGES.txt</include>
+                <include>DEPENDENCIES.txt</include>
+            </includes>
+        </fileSet>
+    </fileSets>
+</assembly>
diff --git a/src/main/assembly/src.xml b/src/main/assembly/src.xml
new file mode 100644
index 0000000..d540555
--- /dev/null
+++ b/src/main/assembly/src.xml
@@ -0,0 +1,44 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements.  See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You 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.
+-->
+<assembly>
+    <id>src</id>
+    <formats>
+        <format>tar.gz</format>
+        <format>zip</format>
+    </formats>
+    <fileSets>
+        <!-- Release materials -->
+        <fileSet>
+            <directory>.</directory>
+            <outputDirectory/>
+            <fileMode>644</fileMode>
+            <directoryMode>755</directoryMode>
+            <excludes>
+                <exclude>**/.*</exclude>
+                <exclude>**/.*/**</exclude>
+                <exclude>**/*.iml/**</exclude>
+                <exclude>**/*.patch/**</exclude>
+                <exclude>**/*.sh/**</exclude>
+                <exclude>**/*.bat/**</exclude>
+                <exclude>**/target/**</exclude>
+                <exclude>**/lib/**</exclude>
+                <exclude>**/data/**</exclude>
+            </excludes>
+        </fileSet>
+    </fileSets>
+</assembly>
diff --git a/src/main/java/org/apache/solr/mcf/ManifoldCFQParserPlugin.java b/src/main/java/org/apache/solr/mcf/ManifoldCFQParserPlugin.java
new file mode 100644
index 0000000..53a16da
--- /dev/null
+++ b/src/main/java/org/apache/solr/mcf/ManifoldCFQParserPlugin.java
@@ -0,0 +1,431 @@
+/**
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright ownership.
+* The ASF licenses this file to You 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 org.apache.solr.mcf;
+
+import org.apache.http.ParseException;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.config.SocketConfig;
+import org.apache.http.entity.ContentType;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.apache.lucene.index.*;
+import org.apache.lucene.search.*;
+import org.apache.solr.search.QParserPlugin;
+import org.apache.solr.search.QParser;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.request.SolrQueryRequest;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.core.CloseHook;
+import org.apache.solr.core.SolrCore;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.HttpResponse;
+import org.apache.http.util.EntityUtils;
+import org.apache.http.impl.client.DefaultRedirectStrategy;
+import org.slf4j.*;
+
+import java.io.*;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.net.*;
+
+/**
+* Query parser plugin for ManifoldCF-specific document-level access control.
+*/
+public class ManifoldCFQParserPlugin extends QParserPlugin
+{
+  /** The parameter that is supposed to contain the authenticated user name, possibly including the AD domain */
+  static final public String AUTHENTICATED_USER_NAME = "AuthenticatedUserName";
+  /** The parameter that is supposed to contain the MCF authorization domain, if any */
+  static final public String AUTHENTICATED_USER_DOMAIN = "AuthenticatedUserDomain";
+  /** If there are more than one user/domain, this prefix will allow us to get the users... */
+  static final public String AUTHENTICATED_USER_NAME_PREFIX = "AuthenticatedUserName_";
+  /** If there are more than one user/domain, this prefix will allow us to get the authorization domains... */
+  static final public String AUTHENTICATED_USER_DOMAIN_PREFIX = "AuthenticatedUserDomain_";
+
+  /** This parameter is an array of strings, which contain the tokens to use if there is no authenticated user name.
+   * It's meant to work with mod_authz_annotate,
+   * running under Apache */
+  static final public String USER_TOKENS = "UserTokens";
+  
+  /** Special token for null security fields */
+  static final public String NOSECURITY_TOKEN = "__nosecurity__";
+
+  /** A logger we can use */
+  private static final Logger LOG = LoggerFactory.getLogger(ManifoldCFQParserPlugin.class);
+
+  // Member variables
+  String authorityBaseURL = null;
+  String fieldAllowDocument = null;
+  String fieldDenyDocument = null;
+  String fieldAllowShare = null;
+  String fieldDenyShare = null;
+  String fieldAllowParent = null;
+  String fieldDenyParent = null;
+  int connectionTimeOut;
+  int socketTimeOut;
+  final Integer connectionManagerSynchronizer = 0;
+  PoolingHttpClientConnectionManager httpConnectionManager = null;
+  HttpClient client = null;
+  int poolSize;
+  
+  public ManifoldCFQParserPlugin()
+  {
+    super();
+  }
+
+  @Override
+  public void init(NamedList args)
+  {
+    authorityBaseURL = (String)args.get("AuthorityServiceBaseURL");
+    if (authorityBaseURL == null)
+      authorityBaseURL = "http://localhost:8345/mcf-authority-service";
+    Integer cTimeOut = (Integer)args.get("ConnectionTimeOut");
+    connectionTimeOut = cTimeOut == null ? 60000 : cTimeOut;
+    Integer timeOut = (Integer)args.get("SocketTimeOut");
+    socketTimeOut = timeOut == null ? 300000 : timeOut;
+    String allowAttributePrefix = (String)args.get("AllowAttributePrefix");
+    String denyAttributePrefix = (String)args.get("DenyAttributePrefix");
+    if (allowAttributePrefix == null)
+      allowAttributePrefix = "allow_token_";
+    if (denyAttributePrefix == null)
+      denyAttributePrefix = "deny_token_";
+    fieldAllowDocument = allowAttributePrefix+"document";
+    fieldDenyDocument = denyAttributePrefix+"document";
+    fieldAllowShare = allowAttributePrefix+"share";
+    fieldDenyShare = denyAttributePrefix+"share";
+    fieldAllowParent = allowAttributePrefix+"parent";
+    fieldDenyParent = denyAttributePrefix+"parent";
+    Integer connectionPoolSize = (Integer)args.get("ConnectionPoolSize");
+    poolSize = (connectionPoolSize==null)?50:connectionPoolSize;
+  }
+  
+  protected void initializeClient(SolrCore core)
+  {
+    synchronized (connectionManagerSynchronizer)
+    {
+      if (client == null)
+      {
+        // Initialize the connection pool
+        httpConnectionManager = new PoolingHttpClientConnectionManager();
+        httpConnectionManager.setMaxTotal(poolSize);
+        httpConnectionManager.setDefaultMaxPerRoute(poolSize);
+        httpConnectionManager.setDefaultSocketConfig(SocketConfig.custom()
+                .setTcpNoDelay(true)
+                .setSoTimeout(socketTimeOut)
+                .build());
+
+        RequestConfig.Builder requestBuilder = RequestConfig.custom()
+                .setCircularRedirectsAllowed(true)
+                .setSocketTimeout(socketTimeOut)
+                .setStaleConnectionCheckEnabled(true)
+                .setExpectContinueEnabled(true)
+                .setConnectTimeout(connectionTimeOut)
+                .setConnectionRequestTimeout(socketTimeOut);
+
+        HttpClientBuilder clientBuilder = HttpClients.custom()
+                .setConnectionManager(httpConnectionManager)
+                .disableAutomaticRetries()
+                .setDefaultRequestConfig(requestBuilder.build())
+                .setRedirectStrategy(new DefaultRedirectStrategy());
+
+        client = clientBuilder.build();
+
+        core.addCloseHook(new CloseHandler());
+      }
+    }
+  }
+
+  @Override
+  public QParser createParser(String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest req)
+  {
+    initializeClient(req.getCore());
+    return new ManifoldCFQueryParser(qstr,localParams,params,req);
+  }
+
+  protected class ManifoldCFQueryParser extends QParser
+  {
+    public ManifoldCFQueryParser(String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest req)
+    {
+      super(qstr,localParams,params,req);
+    }
+        
+    @Override
+    /** Create and return the <code>Query</code> object represented by <code>qstr</code>
+    * @see #getQuery()
+    **/
+    public Query parse()
+    {
+      SolrParams params = req.getParams();
+
+      List<String> userAccessTokens;
+      
+      // Map from domain to user
+      Map<String,String> domainMap = new HashMap<>();
+      
+      // Get the authenticated user name from the parameters
+      String authenticatedUserName = params.get(AUTHENTICATED_USER_NAME);
+      if (authenticatedUserName != null)
+      {
+        String authenticatedUserDomain = params.get(AUTHENTICATED_USER_DOMAIN);
+        if (authenticatedUserDomain == null)
+          authenticatedUserDomain = "";
+        domainMap.put(authenticatedUserDomain, authenticatedUserName);
+      }
+      else
+      {
+        // Look for user names/domains using the prefix
+        int i = 0;
+        while (true)
+        {
+          String userName = params.get(AUTHENTICATED_USER_NAME_PREFIX+i);
+          String domain = params.get(AUTHENTICATED_USER_DOMAIN_PREFIX+i);
+          if (userName == null)
+            break;
+          if (domain == null)
+            domain = "";
+          domainMap.put(domain,userName);
+          i++;
+        }
+      }
+      
+      // If this parameter is empty or does not exist, we have to presume this is a guest, and treat them accordingly
+      if (domainMap.size() == 0)
+      {
+        // No authenticated user name.
+        // mod_authz_annotate may be in use upstream, so look for tokens from it.
+        userAccessTokens = new ArrayList<>();
+        String[] passedTokens = params.getParams(USER_TOKENS);
+        if (passedTokens == null)
+        {
+          // Only return 'public' documents (those with no security tokens at all)
+          LOG.info("Default no-user response (open documents only)");
+        }
+        else
+        {
+          // Only return 'public' documents (those with no security tokens at all)
+          LOG.info("Group tokens received from caller");
+          userAccessTokens.addAll(Arrays.asList(passedTokens));
+        }
+      }
+      else
+      {
+        if(LOG.isInfoEnabled()){
+          StringBuilder sb = new StringBuilder("[");
+          boolean first = true;
+          for (String domain : domainMap.keySet())
+          {
+            if (!first)
+              sb.append(",");
+            else
+              first = false;
+            sb.append(domain).append(":").append(domainMap.get(domain));
+          }
+          sb.append("]");
+          LOG.info("Trying to match docs for user '"+sb.toString()+"'");
+        }
+        // Valid authenticated user name.  Look up access tokens for the user.
+        // Check the configuration arguments for validity
+        if (authorityBaseURL == null)
+        {
+          throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error initializing ManifoldCFSecurityFilter component: 'AuthorityServiceBaseURL' init parameter required");
+        }
+        try
+        {
+          userAccessTokens = getAccessTokens(domainMap);
+        }
+        catch (IOException e)
+        {
+          LOG.error("IO exception communicating with MCF authority service: "+e.getMessage(),e);
+          throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "IO exception communicating with MCF authority service: "+e.getMessage());
+        }
+      }
+
+      BooleanQuery bq = new BooleanQuery();
+      //bf.setMaxClauseCount(100000);
+      
+      Query allowShareOpen = new TermQuery(new Term(fieldAllowShare,NOSECURITY_TOKEN));
+      Query denyShareOpen = new TermQuery(new Term(fieldDenyShare,NOSECURITY_TOKEN));
+      Query allowParentOpen = new TermQuery(new Term(fieldAllowParent,NOSECURITY_TOKEN));
+      Query denyParentOpen = new TermQuery(new Term(fieldDenyParent,NOSECURITY_TOKEN));
+      Query allowDocumentOpen = new TermQuery(new Term(fieldAllowDocument,NOSECURITY_TOKEN));
+      Query denyDocumentOpen = new TermQuery(new Term(fieldDenyDocument,NOSECURITY_TOKEN));
+      
+      if (userAccessTokens.size() == 0)
+      {
+        // Only open documents can be included.
+        // That query is:
+        // (fieldAllowShare is empty AND fieldDenyShare is empty AND fieldAllowDocument is empty AND fieldDenyDocument is empty)
+        // We're trying to map to:  -(fieldAllowShare:*) , which should be pretty efficient in Solr because it is negated.  If this turns out not to be so, then we should
+        // have the SolrConnector inject a special token into these fields when they otherwise would be empty, and we can trivially match on that token.
+        bq.add(allowShareOpen,BooleanClause.Occur.MUST);
+        bq.add(denyShareOpen,BooleanClause.Occur.MUST);
+        bq.add(allowParentOpen,BooleanClause.Occur.MUST);
+        bq.add(denyParentOpen,BooleanClause.Occur.MUST);
+        bq.add(allowDocumentOpen,BooleanClause.Occur.MUST);
+        bq.add(denyDocumentOpen,BooleanClause.Occur.MUST);
+      }
+      else
+      {
+        // Extend the query appropriately for each user access token.
+        bq.add(calculateCompleteSubquery(fieldAllowShare,fieldDenyShare,allowShareOpen,denyShareOpen,userAccessTokens),
+          BooleanClause.Occur.MUST);
+        bq.add(calculateCompleteSubquery(fieldAllowParent,fieldDenyParent,allowParentOpen,denyParentOpen,userAccessTokens),
+          BooleanClause.Occur.MUST);
+        bq.add(calculateCompleteSubquery(fieldAllowDocument,fieldDenyDocument,allowDocumentOpen,denyDocumentOpen,userAccessTokens),
+          BooleanClause.Occur.MUST);
+      }
+
+      return new ConstantScoreQuery(bq);
+    }
+
+    /** Calculate a complete subclause, representing something like:
+    * ((fieldAllowShare is empty AND fieldDenyShare is empty) OR fieldAllowShare HAS token1 OR fieldAllowShare HAS token2 ...)
+    *     AND fieldDenyShare DOESN'T_HAVE token1 AND fieldDenyShare DOESN'T_HAVE token2 ...
+    */
+    protected Query calculateCompleteSubquery(String allowField, String denyField, Query allowOpen, Query denyOpen, List<String> userAccessTokens)
+    {
+      BooleanQuery bq = new BooleanQuery();
+      BooleanQuery.setMaxClauseCount(1000000);
+      
+      // Add the empty-acl case
+      BooleanQuery subUnprotectedClause = new BooleanQuery();
+      subUnprotectedClause.add(allowOpen,BooleanClause.Occur.MUST);
+      subUnprotectedClause.add(denyOpen,BooleanClause.Occur.MUST);
+      bq.add(subUnprotectedClause,BooleanClause.Occur.SHOULD);
+      for (String accessToken : userAccessTokens)
+      {
+        bq.add(new TermQuery(new Term(allowField,accessToken)),BooleanClause.Occur.SHOULD);
+        bq.add(new TermQuery(new Term(denyField,accessToken)),BooleanClause.Occur.MUST_NOT);
+      }
+      return bq;
+    }
+    
+    // Protected methods
+    
+    /** Get access tokens given a username */
+    protected List<String> getAccessTokens(Map<String,String> domainMap)
+      throws IOException
+    {
+      // We can make this more complicated later, with support for https etc., but this is enough to demonstrate how it all should work.
+      StringBuilder urlBuffer = new StringBuilder(authorityBaseURL);
+      urlBuffer.append("/UserACLs");
+      int i = 0;
+      for (String domain : domainMap.keySet())
+      {
+        if (i == 0)
+          urlBuffer.append("?");
+        else
+          urlBuffer.append("&");
+        // For backwards compatibility, handle the singleton case specially
+        if (domainMap.size() == 1 && domain.length() == 0)
+        {
+          urlBuffer.append("username=").append(URLEncoder.encode(domainMap.get(domain),"utf-8"));
+        }
+        else
+        {
+          urlBuffer.append("username_").append(Integer.toString(i)).append("=").append(URLEncoder.encode(domainMap.get(domain),"utf-8")).append("&")
+            .append("domain_").append(Integer.toString(i)).append("=").append(URLEncoder.encode(domain,"utf-8"));
+        }
+        i++;
+      }
+      String theURL = urlBuffer.toString();
+        
+      HttpGet method = new HttpGet(theURL);
+      try
+      {
+        HttpResponse httpResponse = client.execute(method);
+        int rval = httpResponse.getStatusLine().getStatusCode();
+        if (rval != 200)
+        {
+          String response = EntityUtils.toString(httpResponse.getEntity(), StandardCharsets.UTF_8);
+          throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,"Couldn't fetch user's access tokens from ManifoldCF authority service: "+Integer.toString(rval)+"; "+response);
+        }
+
+        try(InputStream is = httpResponse.getEntity().getContent())
+        {
+          Charset charSet;
+          try
+            {
+              ContentType ct = ContentType.get(httpResponse.getEntity());
+              if (ct == null)
+                charSet = StandardCharsets.UTF_8;
+              else
+                charSet = ct.getCharset();
+            }catch (ParseException e){
+                charSet = StandardCharsets.UTF_8;
+            }
+
+          try( Reader r = new InputStreamReader(is,charSet); BufferedReader br = new BufferedReader(r)) {
+              // Read the tokens, one line at a time.  If any authorities are down, we have no current way to note that, but someday we will.
+              List<String> tokenList = new ArrayList<>();
+              while (true) {
+                String line = br.readLine();
+                if (line == null)
+                  break;
+                if (line.startsWith("TOKEN:")) {
+                  tokenList.add(line.substring("TOKEN:".length()));
+                } else {
+                   // It probably says something about the state of the authority(s) involved, so log it
+                  LOG.info("Saw authority response " + line);
+                }
+              }
+              return tokenList;
+          }
+        }
+      }
+      finally
+      {
+        method.abort();
+      }
+    }
+  }
+
+  /** CloseHook implementation.
+  */
+  protected class CloseHandler extends CloseHook
+  {
+    public CloseHandler()
+    {
+    }
+    
+    @Override
+    public void preClose(SolrCore core)
+    {
+    }
+    
+    @Override
+    public void postClose(SolrCore core)
+    {
+      synchronized (connectionManagerSynchronizer)
+      {
+        // Close the connection pool
+        if (httpConnectionManager != null)
+        {
+          httpConnectionManager.shutdown();
+          httpConnectionManager = null;
+          client = null;
+        }
+      }
+    }
+    
+  }
+
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/solr/mcf/ManifoldCFSearchComponent.java b/src/main/java/org/apache/solr/mcf/ManifoldCFSearchComponent.java
new file mode 100644
index 0000000..4a9a854
--- /dev/null
+++ b/src/main/java/org/apache/solr/mcf/ManifoldCFSearchComponent.java
@@ -0,0 +1,472 @@
+/**
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright ownership.
+* The ASF licenses this file to You 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 org.apache.solr.mcf;
+
+import org.apache.http.ParseException;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.config.SocketConfig;
+import org.apache.http.entity.ContentType;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
+import org.apache.lucene.index.*;
+import org.apache.lucene.search.*;
+import org.apache.solr.common.SolrException;
+import org.apache.solr.common.params.CommonParams;
+import org.apache.solr.common.params.ShardParams;
+import org.apache.solr.common.params.SolrParams;
+import org.apache.solr.common.util.NamedList;
+import org.apache.solr.handler.component.ResponseBuilder;
+import org.apache.solr.handler.component.SearchComponent;
+import org.apache.solr.core.CloseHook;
+import org.apache.solr.util.plugin.SolrCoreAware;
+import org.apache.solr.core.SolrCore;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.HttpResponse;
+import org.apache.http.util.EntityUtils;
+import org.apache.http.impl.client.DefaultRedirectStrategy;
+import org.slf4j.*;
+
+import java.io.*;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.net.*;
+
+/**
+* SearchComponent plugin for ManifoldCF-specific document-level access control.
+* Configuration is under the SolrACLSecurity name.
+*/
+public class ManifoldCFSearchComponent extends SearchComponent implements SolrCoreAware
+{
+  /** The component name */
+  static final public String COMPONENT_NAME = "mcf";
+  /** The parameter that is supposed to contain the authenticated user name, possibly including the AD domain */
+  static final public String AUTHENTICATED_USER_NAME = "AuthenticatedUserName";
+  /** The parameter that is supposed to contain the MCF authorization domain, if any */
+  static final public String AUTHENTICATED_USER_DOMAIN = "AuthenticatedUserDomain";
+  /** If there are more than one user/domain, this prefix will allow us to get the users... */
+  static final public String AUTHENTICATED_USER_NAME_PREFIX = "AuthenticatedUserName_";
+  /** If there are more than one user/domain, this prefix will allow us to get the authorization domains... */
+  static final public String AUTHENTICATED_USER_DOMAIN_PREFIX = "AuthenticatedUserDomain_";
+
+  /** This parameter is an array of strings, which contain the tokens to use if there is no authenticated user name.
+   * It's meant to work with mod_authz_annotate,
+   * running under Apache */
+  static final public String USER_TOKENS = "UserTokens";
+  
+  /** Special token for null security fields */
+  static final public String NOSECURITY_TOKEN = "__nosecurity__";
+  
+  /** The queries that we will not attempt to interfere with */
+  static final private String[] globalAllowed = { "solrpingquery" };
+  
+  /** A logger we can use */
+  private static final Logger LOG = LoggerFactory.getLogger(ManifoldCFSearchComponent.class);
+
+  // Member variables
+  String authorityBaseURL = null;
+  String fieldAllowDocument = null;
+  String fieldDenyDocument = null;
+  String fieldAllowShare = null;
+  String fieldDenyShare = null;
+  String fieldAllowParent = null;
+  String fieldDenyParent = null;
+  int connectionTimeOut;
+  int socketTimeOut;
+  PoolingHttpClientConnectionManager httpConnectionManager = null;
+  HttpClient client = null;
+  int poolSize;
+  
+  public ManifoldCFSearchComponent()
+  {
+    super();
+  }
+
+  @Override
+  public void init(NamedList args)
+  {
+    super.init(args);
+    authorityBaseURL = (String)args.get("AuthorityServiceBaseURL");
+    if (authorityBaseURL == null)
+    {
+      LOG.info("USING DEFAULT BASE URL!!");
+      authorityBaseURL = "http://localhost:8345/mcf-authority-service";
+    }
+    Integer cTimeOut = (Integer)args.get("ConnectionTimeOut");
+    connectionTimeOut = cTimeOut == null ? 60000 : cTimeOut;
+    Integer timeOut = (Integer)args.get("SocketTimeOut");
+    socketTimeOut = timeOut == null ? 300000 : timeOut;
+    String allowAttributePrefix = (String)args.get("AllowAttributePrefix");
+    String denyAttributePrefix = (String)args.get("DenyAttributePrefix");
+    if (allowAttributePrefix == null)
+      allowAttributePrefix = "allow_token_";
+    if (denyAttributePrefix == null)
+      denyAttributePrefix = "deny_token_";
+    fieldAllowDocument = allowAttributePrefix+"document";
+    fieldDenyDocument = denyAttributePrefix+"document";
+    fieldAllowShare = allowAttributePrefix+"share";
+    fieldDenyShare = denyAttributePrefix+"share";
+    fieldAllowParent = allowAttributePrefix+"parent";
+    fieldDenyParent = denyAttributePrefix+"parent";
+    Integer connectionPoolSize = (Integer)args.get("ConnectionPoolSize");
+    poolSize = (connectionPoolSize==null)?50:connectionPoolSize;
+
+    // Initialize the connection pool
+    httpConnectionManager = new PoolingHttpClientConnectionManager();
+    httpConnectionManager.setMaxTotal(poolSize);
+    httpConnectionManager.setDefaultMaxPerRoute(poolSize);
+    httpConnectionManager.setDefaultSocketConfig(SocketConfig.custom()
+            .setTcpNoDelay(true)
+            .setSoTimeout(socketTimeOut)
+            .build());
+
+    RequestConfig.Builder requestBuilder = RequestConfig.custom()
+            .setCircularRedirectsAllowed(true)
+            .setSocketTimeout(socketTimeOut)
+            .setStaleConnectionCheckEnabled(true)
+            .setExpectContinueEnabled(true)
+            .setConnectTimeout(connectionTimeOut)
+            .setConnectionRequestTimeout(socketTimeOut);
+
+    HttpClientBuilder clientBuilder = HttpClients.custom()
+            .setConnectionManager(httpConnectionManager)
+            .disableAutomaticRetries()
+            .setDefaultRequestConfig(requestBuilder.build())
+            .setRedirectStrategy(new DefaultRedirectStrategy());           
+
+    client = clientBuilder.build();
+
+  }
+
+  @Override
+  public void prepare(ResponseBuilder rb) throws IOException
+  {
+    SolrParams params = rb.req.getParams();
+    if (!params.getBool(COMPONENT_NAME, true) || params.get(ShardParams.SHARDS) != null)
+      return;
+    
+    // Log that we got here
+    //LOG.info("prepare() entry params:\n" + params + "\ncontext: " + rb.req.getContext());
+		
+    String qry = params.get(CommonParams.Q);
+    if (qry != null)
+    {
+      //Check global allowed searches
+      for (String ga : globalAllowed)
+      {
+        if (qry.equalsIgnoreCase(ga.trim()))
+          // Allow this query through unchanged
+          return;
+      }
+    }
+
+    List<String> userAccessTokens;
+    
+    // Map from domain to user
+    Map<String,String> domainMap = new HashMap<>();
+      
+    // Get the authenticated user name from the parameters
+    String authenticatedUserName = params.get(AUTHENTICATED_USER_NAME);
+    if (authenticatedUserName != null)
+    {
+      String authenticatedUserDomain = params.get(AUTHENTICATED_USER_DOMAIN);
+      if (authenticatedUserDomain == null)
+        authenticatedUserDomain = "";
+      domainMap.put(authenticatedUserDomain, authenticatedUserName);
+    }
+    else
+    {
+      // Look for user names/domains using the prefix
+      int i = 0;
+      while (true)
+      {
+        String userName = params.get(AUTHENTICATED_USER_NAME_PREFIX+i);
+        String domain = params.get(AUTHENTICATED_USER_DOMAIN_PREFIX+i);
+        if (userName == null)
+          break;
+        if (domain == null)
+          domain = "";
+        domainMap.put(domain,userName);
+        i++;
+      }
+    }
+      
+    // If this parameter is empty or does not exist, we have to presume this is a guest, and treat them accordingly
+    if (domainMap.size() == 0)
+    {
+      // No authenticated user name.
+      // mod_authz_annotate may be in use upstream, so look for tokens from it.
+      userAccessTokens = new ArrayList<>();
+      String[] passedTokens = params.getParams(USER_TOKENS);
+      if (passedTokens == null)
+      {
+        // Only return 'public' documents (those with no security tokens at all)
+        LOG.info("Default no-user response (open documents only)");
+      }
+      else
+      {
+        // Only return 'public' documents (those with no security tokens at all)
+        LOG.info("Group tokens received from caller");
+        userAccessTokens.addAll(Arrays.asList(passedTokens));
+      }
+    }
+    else
+    {
+      if(LOG.isInfoEnabled()){
+        StringBuilder sb = new StringBuilder("[");
+        boolean first = true;
+        for (String domain : domainMap.keySet())
+        {
+          if (!first)
+            sb.append(",");
+          else
+            first = false;
+          sb.append(domain).append(":").append(domainMap.get(domain));
+        }
+        sb.append("]");
+        LOG.info("Trying to match docs for user '"+sb.toString()+"'");
+      }
+      // Valid authenticated user name.  Look up access tokens for the user.
+      // Check the configuration arguments for validity
+      if (authorityBaseURL == null)
+      {
+        throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error initializing ManifoldCFSecurityFilter component: 'AuthorityServiceBaseURL' init parameter required");
+      }
+      userAccessTokens = getAccessTokens(domainMap);
+    }
+
+    BooleanQuery bq = new BooleanQuery();
+    //bf.setMaxClauseCount(100000);
+    
+    Query allowShareOpen = new TermQuery(new Term(fieldAllowShare,NOSECURITY_TOKEN));
+    Query denyShareOpen = new TermQuery(new Term(fieldDenyShare,NOSECURITY_TOKEN));
+    Query allowParentOpen = new TermQuery(new Term(fieldAllowParent,NOSECURITY_TOKEN));
+    Query denyParentOpen = new TermQuery(new Term(fieldDenyParent,NOSECURITY_TOKEN));
+    Query allowDocumentOpen = new TermQuery(new Term(fieldAllowDocument,NOSECURITY_TOKEN));
+    Query denyDocumentOpen = new TermQuery(new Term(fieldDenyDocument,NOSECURITY_TOKEN));
+    
+    if (userAccessTokens.size() == 0)
+    {
+      // Only open documents can be included.
+      // That query is:
+      // (fieldAllowShare is empty AND fieldDenyShare is empty AND fieldAllowDocument is empty AND fieldDenyDocument is empty)
+      // We're trying to map to:  -(fieldAllowShare:*) , which should be pretty efficient in Solr because it is negated.  If this turns out not to be so, then we should
+      // have the SolrConnector inject a special token into these fields when they otherwise would be empty, and we can trivially match on that token.
+      bq.add(allowShareOpen,BooleanClause.Occur.MUST);
+      bq.add(denyShareOpen,BooleanClause.Occur.MUST);
+      bq.add(allowParentOpen,BooleanClause.Occur.MUST);
+      bq.add(denyParentOpen,BooleanClause.Occur.MUST);
+      bq.add(allowDocumentOpen,BooleanClause.Occur.MUST);
+      bq.add(denyDocumentOpen,BooleanClause.Occur.MUST);
+    }
+    else
+    {
+      // Extend the query appropriately for each user access token.
+      bq.add(calculateCompleteSubquery(fieldAllowShare,fieldDenyShare,allowShareOpen,denyShareOpen,userAccessTokens),
+        BooleanClause.Occur.MUST);
+      bq.add(calculateCompleteSubquery(fieldAllowParent,fieldDenyParent,allowParentOpen,denyParentOpen,userAccessTokens),
+        BooleanClause.Occur.MUST);
+      bq.add(calculateCompleteSubquery(fieldAllowDocument,fieldDenyDocument,allowDocumentOpen,denyDocumentOpen,userAccessTokens),
+        BooleanClause.Occur.MUST);
+    }
+
+    // Concatenate with the user's original query.
+    List<Query> list = rb.getFilters();
+    if (list == null)
+    {
+      list = new ArrayList<>();
+      rb.setFilters(list);
+    }
+    list.add(new ConstantScoreQuery(bq));
+  }
+
+  @Override
+  public void process(ResponseBuilder rb) throws IOException
+  {
+    //LOG.info("process() called");
+  }
+
+  /** Calculate a complete subclause, representing something like:
+  * ((fieldAllowShare is empty AND fieldDenyShare is empty) OR fieldAllowShare HAS token1 OR fieldAllowShare HAS token2 ...)
+  *     AND fieldDenyShare DOESN'T_HAVE token1 AND fieldDenyShare DOESN'T_HAVE token2 ...
+  */
+  protected Query calculateCompleteSubquery(String allowField, String denyField, Query allowOpen, Query denyOpen, List<String> userAccessTokens)
+  {
+    BooleanQuery bq = new BooleanQuery();
+    BooleanQuery.setMaxClauseCount(1000000);
+    
+    // Add the empty-acl case
+    BooleanQuery subUnprotectedClause = new BooleanQuery();
+    subUnprotectedClause.add(allowOpen,BooleanClause.Occur.MUST);
+    subUnprotectedClause.add(denyOpen,BooleanClause.Occur.MUST);
+    bq.add(subUnprotectedClause,BooleanClause.Occur.SHOULD);
+    for (String accessToken : userAccessTokens)
+    {
+      bq.add(new TermQuery(new Term(allowField,accessToken)),BooleanClause.Occur.SHOULD);
+      bq.add(new TermQuery(new Term(denyField,accessToken)),BooleanClause.Occur.MUST_NOT);
+    }
+    return bq;
+  }
+  
+  //---------------------------------------------------------------------------------
+  // SolrInfoMBean
+  //---------------------------------------------------------------------------------
+  @Override
+  public String getDescription()
+  {
+    return "ManifoldCF Solr security enforcement plugin";
+  }
+
+  @Override
+  public String getVersion()
+  {
+    return "$Revision$";
+  }
+
+  @Override
+  public Category getCategory()
+  {
+    return Category.QUERYHANDLER;
+  }
+
+  @Override
+  public String getSource()
+  {
+    return "$URL$";
+  }
+
+  @Override
+  public void inform(SolrCore core)
+  {
+    core.addCloseHook(new CloseHandler());
+  }
+  
+  // Protected methods
+  
+  /** Get access tokens given a username */
+  protected List<String> getAccessTokens(Map<String,String> domainMap)
+    throws IOException
+  {
+    // We can make this more complicated later, with support for https etc., but this is enough to demonstrate how it all should work.
+    StringBuilder urlBuffer = new StringBuilder(authorityBaseURL);
+    urlBuffer.append("/UserACLs");
+    int i = 0;
+    for (String domain : domainMap.keySet())
+    {
+      if (i == 0)
+        urlBuffer.append("?");
+      else
+        urlBuffer.append("&");
+      // For backwards compatibility, handle the singleton case specially
+      if (domainMap.size() == 1 && domain.length() == 0)
+      {
+        urlBuffer.append("username=").append(URLEncoder.encode(domainMap.get(domain),"utf-8"));
+      }
+      else
+      {
+        urlBuffer.append("username_").append(Integer.toString(i)).append("=").append(URLEncoder.encode(domainMap.get(domain),"utf-8")).append("&")
+          .append("domain_").append(Integer.toString(i)).append("=").append(URLEncoder.encode(domain,"utf-8"));
+      }
+      i++;
+    }
+    String theURL = urlBuffer.toString();
+        
+    HttpGet method = new HttpGet(theURL);
+    try
+    {
+      HttpResponse httpResponse = client.execute(method);
+      int rval = httpResponse.getStatusLine().getStatusCode();
+      if (rval != 200)
+      {
+        String response = EntityUtils.toString(httpResponse.getEntity(), StandardCharsets.UTF_8);
+        throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,"Couldn't fetch user's access tokens from ManifoldCF authority service: "+Integer.toString(rval)+"; "+response);
+      }
+
+      try(InputStream is = httpResponse.getEntity().getContent())
+      {
+        Charset charSet;
+        try
+        {
+          ContentType ct = ContentType.get(httpResponse.getEntity());
+          if (ct == null)
+            charSet = StandardCharsets.UTF_8;
+          else
+            charSet = ct.getCharset();
+        }catch (ParseException e){
+          charSet = StandardCharsets.UTF_8;
+        }
+
+
+        try(Reader r = new InputStreamReader(is,charSet); BufferedReader br = new BufferedReader(r))
+        {
+            // Read the tokens, one line at a time.  If any authorities are down, we have no current way to note that, but someday we will.
+            List<String> tokenList = new ArrayList<>();
+            while (true)
+            {
+              String line = br.readLine();
+              if (line == null)
+                break;
+              if (line.startsWith("TOKEN:"))
+              {
+                tokenList.add(line.substring("TOKEN:".length()));
+              }
+              else
+              {
+                // It probably says something about the state of the authority(s) involved, so log it
+                LOG.info("Saw authority response "+line);
+              }
+            }
+            return tokenList;
+          }
+      }
+    }
+    finally
+    {
+      method.abort();
+    }
+  }
+  
+  /** CloseHook implementation.
+  */
+  protected class CloseHandler extends CloseHook
+  {
+    public CloseHandler()
+    {
+    }
+    
+    @Override
+    public void preClose(SolrCore core)
+    {
+    }
+    
+    @Override
+    public void postClose(SolrCore core)
+    {
+      // Close the connection pool
+      if (httpConnectionManager != null)
+      {
+        httpConnectionManager.shutdown();
+        httpConnectionManager = null;
+        client = null;
+      }
+    }
+    
+  }
+  
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/solr/mcf/ManifoldCFQParserPluginTest.java b/src/test/java/org/apache/solr/mcf/ManifoldCFQParserPluginTest.java
new file mode 100644
index 0000000..e6cbf97
--- /dev/null
+++ b/src/test/java/org/apache/solr/mcf/ManifoldCFQParserPluginTest.java
@@ -0,0 +1,184 @@
+/**
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright ownership.
+* The ASF licenses this file to You 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 org.apache.solr.mcf;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.solr.SolrTestCaseJ4;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.eclipse.jetty.server.handler.ContextHandlerCollection;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+
+public class ManifoldCFQParserPluginTest extends SolrTestCaseJ4 {
+  
+  static MockMCFAuthorityService service;
+
+  @BeforeClass
+  public static void beforeClass() throws Exception {
+    initCore("solrconfig-auth-qparser.xml","schema-auth.xml");
+    service = new MockMCFAuthorityService();
+    service.start();
+
+    //             |     share    |   document
+    //             |--------------|--------------
+    //             | allow | deny | allow | deny
+    // ------------+-------+------+-------+------
+    // da12        |       |      | 1, 2  |
+    // ------------+-------+------+-------+------
+    // da13-dd3    |       |      | 1,3   | 3
+    // ------------+-------+------+-------+------
+    // sa123-sd13  | 1,2,3 | 1, 3 |       |
+    // ------------+-------+------+-------+------
+    // sa3-sd1-da23| 3     | 1    | 2,3   |
+    // ------------+-------+------+-------+------
+    // notoken     |       |      |       |
+    // ------------+-------+------+-------+------
+    //
+    assertU(adoc("id", "da12", "allow_token_document", "token1", "allow_token_document", "token2"));
+    assertU(adoc("id", "da13-dd3", "allow_token_document", "token1", "allow_token_document", "token3", "deny_token_document", "token3"));
+    assertU(adoc("id", "sa123-sd13", "allow_token_share", "token1", "allow_token_share", "token2", "allow_token_share", "token3", "deny_token_share", "token1", "deny_token_share", "token3"));
+    assertU(adoc("id", "sa3-sd1-da23", "allow_token_document", "token2", "allow_token_document", "token3", "allow_token_share", "token3", "deny_token_share", "token1"));
+    assertU(adoc("id", "notoken"));
+    assertU(commit());
+  }
+
+  @AfterClass
+  public static void afterClass() throws Exception {
+    service.stop();
+  }
+  
+  @Test
+  public void testNullUsers() throws Exception {
+    assertQ(req("qt", "/mcf", "q", "*:*", "fl", "id"),
+        "//*[@numFound='1']",
+        "//result/doc[1]/str[@name='id'][.='notoken']");
+    assertQ(req("qt", "/mcf", "q", "*:*", "fl", "id", "AuthenticatedUserName", "anonymous"),
+        "//*[@numFound='1']",
+        "//result/doc[1]/str[@name='id'][.='notoken']");
+  }
+
+  // da12
+  // da13-dd3
+  // sa123-sd13
+  // sa3-sd1-da23
+  // notoken
+  @Test
+  public void testAuthUsers() throws Exception {
+    assertQ(req("qt", "/mcf", "q", "*:*", "fl", "id", "AuthenticatedUserName", "user1"),
+        "//*[@numFound='3']",
+        "//result/doc[1]/str[@name='id'][.='da12']",
+        "//result/doc[2]/str[@name='id'][.='da13-dd3']",
+        "//result/doc[3]/str[@name='id'][.='notoken']");
+
+    assertQ(req("qt", "/mcf", "q", "*:*", "fl", "id", "AuthenticatedUserName", "user2"),
+        "//*[@numFound='3']",
+        "//result/doc[1]/str[@name='id'][.='da12']",
+        "//result/doc[2]/str[@name='id'][.='da13-dd3']",
+        "//result/doc[3]/str[@name='id'][.='notoken']");
+
+    assertQ(req("qt", "/mcf", "q", "*:*", "fl", "id", "AuthenticatedUserName", "user3"),
+        "//*[@numFound='2']",
+        "//result/doc[1]/str[@name='id'][.='da12']",
+        "//result/doc[2]/str[@name='id'][.='notoken']");
+  }
+
+  // da12
+  // da13-dd3
+  // sa123-sd13
+  // sa3-sd1-da23
+  // notoken
+  @Test
+  public void testUserTokens() throws Exception {
+
+    assertQ(req("qt", "/mcf", "q", "*:*", "fl", "id", "UserTokens", "token1"),
+        "//*[@numFound='3']",
+        "//result/doc[1]/str[@name='id'][.='da12']",
+        "//result/doc[2]/str[@name='id'][.='da13-dd3']",
+        "//result/doc[3]/str[@name='id'][.='notoken']");
+
+    assertQ(req("qt", "/mcf", "q", "*:*", "fl", "id", "UserTokens", "token2"),
+        "//*[@numFound='3']",
+        "//result/doc[1]/str[@name='id'][.='da12']",
+        "//result/doc[2]/str[@name='id'][.='sa123-sd13']",
+        "//result/doc[3]/str[@name='id'][.='notoken']");
+
+    assertQ(req("qt", "/mcf", "q", "*:*", "fl", "id", "UserTokens", "token3"),
+        "//*[@numFound='2']",
+        "//result/doc[1]/str[@name='id'][.='sa3-sd1-da23']",
+        "//result/doc[2]/str[@name='id'][.='notoken']");
+
+    assertQ(req("qt", "/mcf", "q", "*:*", "fl", "id", "UserTokens", "token2", "UserTokens", "token3"),
+        "//*[@numFound='3']",
+        "//result/doc[1]/str[@name='id'][.='da12']",
+        "//result/doc[2]/str[@name='id'][.='sa3-sd1-da23']",
+        "//result/doc[3]/str[@name='id'][.='notoken']");
+  }
+  
+  static class MockMCFAuthorityService {
+    
+    Server server;
+    
+    public MockMCFAuthorityService() {
+      server = new Server(8347);
+      ContextHandlerCollection contexts = new ContextHandlerCollection();
+      server.setHandler(contexts);
+
+      ServletContextHandler asContext = new ServletContextHandler(contexts,"/mcf-as",ServletContextHandler.SESSIONS);
+      asContext.addServlet(new ServletHolder(new UserACLServlet()), "/UserACLs");
+      contexts.addHandler(asContext);
+    }
+    
+    public void start() throws Exception {
+      server.start();
+    }
+    
+    public void stop() throws Exception {
+      server.stop();
+    }
+
+    // username | tokens rewarded
+    // ---------+-------------------------------
+    // null     | (no tokens)
+    // user1    | token1
+    // user2    | token1, token2
+    // user3    | token1, token2, token3
+    public static class UserACLServlet extends HttpServlet {
+      @Override
+      public void service(HttpServletRequest req, HttpServletResponse res)
+          throws IOException {
+        String user = req.getParameter("username");
+        res.setStatus(HttpServletResponse.SC_OK);
+        if(user.equals("user1") || user.equals("user2") || user.equals("user3"))
+          res.getWriter().printf("TOKEN:token1\n");
+        if(user.equals("user2") || user.equals("user3"))
+          res.getWriter().printf("TOKEN:token2\n");
+        if(user.equals("user3"))
+          res.getWriter().printf("TOKEN:token3\n");
+      }
+    }
+  }
+}
diff --git a/src/test/java/org/apache/solr/mcf/ManifoldCFSCLoadTest.java b/src/test/java/org/apache/solr/mcf/ManifoldCFSCLoadTest.java
new file mode 100644
index 0000000..02417e7
--- /dev/null
+++ b/src/test/java/org/apache/solr/mcf/ManifoldCFSCLoadTest.java
@@ -0,0 +1,150 @@
+/**
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright ownership.
+* The ASF licenses this file to You 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 org.apache.solr.mcf;
+
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.solr.SolrTestCaseJ4;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.eclipse.jetty.server.handler.ContextHandlerCollection;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+
+public class ManifoldCFSCLoadTest extends SolrTestCaseJ4 {
+  
+  static MockMCFAuthorityService service;
+
+  @BeforeClass
+  public static void beforeClass() throws Exception {
+    initCore("solrconfig-auth-load.xml","schema-auth.xml");
+    service = new MockMCFAuthorityService();
+    service.start();
+
+    //             |     share    |   document
+    //             |--------------|--------------
+    //             | allow | deny | allow | deny
+    // ------------+-------+------+-------+------
+    // da12        |       |      | 1, 2  |
+    // ------------+-------+------+-------+------
+    // da13-dd3    |       |      | 1,3   | 3
+    // ------------+-------+------+-------+------
+    // sa123-sd13  | 1,2,3 | 1, 3 |       |
+    // ------------+-------+------+-------+------
+    // sa3-sd1-da23| 3     | 1    | 2,3   |
+    // ------------+-------+------+-------+------
+    // notoken     |       |      |       |
+    // ------------+-------+------+-------+------
+    //
+    int i = 0;
+    while (i < 1000)
+    {
+      assertU(adoc("id", "da12-"+i, "allow_token_document", "token1", "allow_token_document", "token2"));
+      assertU(adoc("id", "da13-dd3-"+i, "allow_token_document", "token1", "allow_token_document", "token3", "deny_token_document", "token3"));
+      assertU(adoc("id", "sa123-sd13-"+i, "allow_token_share", "token1", "allow_token_share", "token2", "allow_token_share", "token3", "deny_token_share", "token1", "deny_token_share", "token3"));
+      assertU(adoc("id", "sa3-sd1-da23-"+i, "allow_token_document", "token2", "allow_token_document", "token3", "allow_token_share", "token3", "deny_token_share", "token1"));
+      assertU(adoc("id", "notoken-"+i));
+      i++;
+    }
+    assertU(optimize());
+    assertU(commit());
+  }
+
+  @AfterClass
+  public static void afterClass() throws Exception {
+    service.stop();
+  }
+  
+  @Test
+  public void testTimeQueries() throws Exception {
+    int i = 0;
+    long startTime = System.nanoTime();
+    while (i < 1000)
+    {
+      assertQ(req("qt", "/mcf", "q", "*:*", "fl", "id", "AuthenticatedUserName", "user1"),
+          "//*[@numFound='3000']");
+
+      assertQ(req("qt", "/mcf", "q", "*:*", "fl", "id", "AuthenticatedUserName", "user2"),
+          "//*[@numFound='3000']");
+
+      assertQ(req("qt", "/mcf", "q", "*:*", "fl", "id", "AuthenticatedUserName", "user3"),
+          "//*[@numFound='2000']");
+      
+      i++;
+    }
+    System.out.println("Query time (milliseconds) = " +  TimeUnit.NANOSECONDS.toMillis(System.nanoTime()-startTime));
+  }
+
+  static class MockMCFAuthorityService {
+    
+    Server server;
+    
+    public MockMCFAuthorityService() {
+      server = new Server(8346);
+      ContextHandlerCollection contexts = new ContextHandlerCollection();
+      server.setHandler(contexts);
+
+      ServletContextHandler asContext = new ServletContextHandler(contexts,"/mcf-as",ServletContextHandler.SESSIONS);
+      asContext.addServlet(new ServletHolder(new UserACLServlet()), "/UserACLs");
+      contexts.addHandler(asContext);
+    }
+    
+    public void start() throws Exception {
+      server.start();
+    }
+    
+    public void stop() throws Exception {
+      server.stop();
+    }
+
+    // username | tokens rewarded
+    // ---------+-------------------------------
+    // null     | (no tokens)
+    // user1    | token1
+    // user2    | token1, token2
+    // user3    | token1, token2, token3
+    public static class UserACLServlet extends HttpServlet {
+      @Override
+      public void service(HttpServletRequest req, HttpServletResponse res)
+          throws IOException {
+        String user = req.getParameter("username");
+        res.setStatus(HttpServletResponse.SC_OK);
+        if(user.equals("user1") || user.equals("user2") || user.equals("user3"))
+          res.getWriter().printf("TOKEN:token1\n");
+        if(user.equals("user2") || user.equals("user3"))
+          res.getWriter().printf("TOKEN:token2\n");
+        if(user.equals("user3"))
+          res.getWriter().printf("TOKEN:token3\n");
+        int i = 0;
+        while (i < 100)
+        {
+          res.getWriter().printf("TOKEN:dummy"+i+"\n");
+          i++;
+        }
+      }
+    }
+  }
+}
diff --git a/src/test/java/org/apache/solr/mcf/ManifoldCFSearchComponentTest.java b/src/test/java/org/apache/solr/mcf/ManifoldCFSearchComponentTest.java
new file mode 100644
index 0000000..b0e14aa
--- /dev/null
+++ b/src/test/java/org/apache/solr/mcf/ManifoldCFSearchComponentTest.java
@@ -0,0 +1,195 @@
+/**
+* Licensed to the Apache Software Foundation (ASF) under one or more
+* contributor license agreements. See the NOTICE file distributed with
+* this work for additional information regarding copyright ownership.
+* The ASF licenses this file to You 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 org.apache.solr.mcf;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.solr.SolrTestCaseJ4;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import org.eclipse.jetty.server.handler.ContextHandlerCollection;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.servlet.ServletHolder;
+
+public class ManifoldCFSearchComponentTest extends SolrTestCaseJ4 {
+  
+  static MockMCFAuthorityService service;
+
+  @BeforeClass
+  public static void beforeClass() throws Exception {
+    initCore("solrconfig-auth.xml","schema-auth.xml");
+    service = new MockMCFAuthorityService();
+    service.start();
+
+    //             |     share    |   document
+    //             |--------------|--------------
+    //             | allow | deny | allow | deny
+    // ------------+-------+------+-------+------
+    // da12        |       |      | 1, 2  |
+    // ------------+-------+------+-------+------
+    // da13-dd3    |       |      | 1,3   | 3
+    // ------------+-------+------+-------+------
+    // sa123-sd13  | 1,2,3 | 1, 3 |       |
+    // ------------+-------+------+-------+------
+    // sa3-sd1-da23| 3     | 1    | 2,3   |
+    // ------------+-------+------+-------+------
+    // notoken     |       |      |       |
+    // ------------+-------+------+-------+------
+    //
+    assertU(adoc("id", "da12", "allow_token_document", "token1", "allow_token_document", "token2"));
+    assertU(adoc("id", "da13-dd3", "allow_token_document", "token1", "allow_token_document", "token3", "deny_token_document", "token3"));
+    assertU(adoc("id", "sa123-sd13", "allow_token_share", "token1", "allow_token_share", "token2", "allow_token_share", "token3", "deny_token_share", "token1", "deny_token_share", "token3"));
+    assertU(adoc("id", "sa3-sd1-da23", "allow_token_document", "token2", "allow_token_document", "token3", "allow_token_share", "token3", "deny_token_share", "token1"));
+    assertU(adoc("id", "notoken"));
+    assertU(commit());
+  }
+
+  @AfterClass
+  public static void afterClass() throws Exception {
+    service.stop();
+  }
+  
+  @Test
+  public void testParameters() throws Exception {
+    ManifoldCFSearchComponent mcfFilter = (ManifoldCFSearchComponent)h.getCore().getSearchComponent("mcf-param");
+    assertEquals("http://localhost:8345/mcf-as", mcfFilter.authorityBaseURL);
+    assertEquals(3000, mcfFilter.socketTimeOut);
+    assertEquals("aap-document", mcfFilter.fieldAllowDocument);
+    assertEquals("dap-document", mcfFilter.fieldDenyDocument);
+    assertEquals("aap-share", mcfFilter.fieldAllowShare);
+    assertEquals("dap-share", mcfFilter.fieldDenyShare);
+  }
+
+  @Test
+  public void testNullUsers() throws Exception {
+    assertQ(req("qt", "/mcf", "q", "*:*", "fl", "id"),
+        "//*[@numFound='1']",
+        "//result/doc[1]/str[@name='id'][.='notoken']");
+    assertQ(req("qt", "/mcf", "q", "*:*", "fl", "id", "AuthenticatedUserName", "anonymous"),
+        "//*[@numFound='1']",
+        "//result/doc[1]/str[@name='id'][.='notoken']");
+  }
+
+  // da12
+  // da13-dd3
+  // sa123-sd13
+  // sa3-sd1-da23
+  // notoken
+  @Test
+  public void testAuthUsers() throws Exception {
+    assertQ(req("qt", "/mcf", "q", "*:*", "fl", "id", "AuthenticatedUserName", "user1"),
+        "//*[@numFound='3']",
+        "//result/doc[1]/str[@name='id'][.='da12']",
+        "//result/doc[2]/str[@name='id'][.='da13-dd3']",
+        "//result/doc[3]/str[@name='id'][.='notoken']");
+
+    assertQ(req("qt", "/mcf", "q", "*:*", "fl", "id", "AuthenticatedUserName", "user2"),
+        "//*[@numFound='3']",
+        "//result/doc[1]/str[@name='id'][.='da12']",
+        "//result/doc[2]/str[@name='id'][.='da13-dd3']",
+        "//result/doc[3]/str[@name='id'][.='notoken']");
+
+    assertQ(req("qt", "/mcf", "q", "*:*", "fl", "id", "AuthenticatedUserName", "user3"),
+        "//*[@numFound='2']",
+        "//result/doc[1]/str[@name='id'][.='da12']",
+        "//result/doc[2]/str[@name='id'][.='notoken']");
+  }
+
+  // da12
+  // da13-dd3
+  // sa123-sd13
+  // sa3-sd1-da23
+  // notoken
+  @Test
+  public void testUserTokens() throws Exception {
+
+    assertQ(req("qt", "/mcf", "q", "*:*", "fl", "id", "UserTokens", "token1"),
+        "//*[@numFound='3']",
+        "//result/doc[1]/str[@name='id'][.='da12']",
+        "//result/doc[2]/str[@name='id'][.='da13-dd3']",
+        "//result/doc[3]/str[@name='id'][.='notoken']");
+
+    assertQ(req("qt", "/mcf", "q", "*:*", "fl", "id", "UserTokens", "token2"),
+        "//*[@numFound='3']",
+        "//result/doc[1]/str[@name='id'][.='da12']",
+        "//result/doc[2]/str[@name='id'][.='sa123-sd13']",
+        "//result/doc[3]/str[@name='id'][.='notoken']");
+
+    assertQ(req("qt", "/mcf", "q", "*:*", "fl", "id", "UserTokens", "token3"),
+        "//*[@numFound='2']",
+        "//result/doc[1]/str[@name='id'][.='sa3-sd1-da23']",
+        "//result/doc[2]/str[@name='id'][.='notoken']");
+
+    assertQ(req("qt", "/mcf", "q", "*:*", "fl", "id", "UserTokens", "token2", "UserTokens", "token3"),
+        "//*[@numFound='3']",
+        "//result/doc[1]/str[@name='id'][.='da12']",
+        "//result/doc[2]/str[@name='id'][.='sa3-sd1-da23']",
+        "//result/doc[3]/str[@name='id'][.='notoken']");
+  }
+  
+  static class MockMCFAuthorityService {
+    
+    Server server;
+    
+    public MockMCFAuthorityService() {
+      server = new Server(8345);
+      ContextHandlerCollection contexts = new ContextHandlerCollection();
+      server.setHandler(contexts);
+
+      ServletContextHandler asContext = new ServletContextHandler(contexts,"/mcf-authority-service",ServletContextHandler.SESSIONS);
+      asContext.addServlet(new ServletHolder(new UserACLServlet()), "/UserACLs");
+      contexts.addHandler(asContext);
+    }
+    
+    public void start() throws Exception {
+      server.start();
+    }
+    
+    public void stop() throws Exception {
+      server.stop();
+    }
+
+    // username | tokens rewarded
+    // ---------+-------------------------------
+    // null     | (no tokens)
+    // user1    | token1
+    // user2    | token1, token2
+    // user3    | token1, token2, token3
+    public static class UserACLServlet extends HttpServlet {
+      @Override
+      public void service(HttpServletRequest req, HttpServletResponse res)
+          throws IOException {
+        String user = req.getParameter("username");
+        res.setStatus(HttpServletResponse.SC_OK);
+        if(user.equals("user1") || user.equals("user2") || user.equals("user3"))
+          res.getWriter().printf("TOKEN:token1\n");
+        if(user.equals("user2") || user.equals("user3"))
+          res.getWriter().printf("TOKEN:token2\n");
+        if(user.equals("user3"))
+          res.getWriter().printf("TOKEN:token3\n");
+      }
+    }
+  }
+}