Documentation (some of it), API, test fixes

git-svn-id: https://svn.apache.org/repos/asf/manifoldcf/branches/CONNECTORS-792@1535980 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/connectors/hdfs/connector/src/main/java/org/apache/manifoldcf/crawler/connectors/hdfs/HDFSSession.java b/connectors/hdfs/connector/src/main/java/org/apache/manifoldcf/crawler/connectors/hdfs/HDFSSession.java
index a4e63c4..ef1d754 100644
--- a/connectors/hdfs/connector/src/main/java/org/apache/manifoldcf/crawler/connectors/hdfs/HDFSSession.java
+++ b/connectors/hdfs/connector/src/main/java/org/apache/manifoldcf/crawler/connectors/hdfs/HDFSSession.java
@@ -61,11 +61,12 @@
     info.put("Name Node", nameNode);
     info.put("Config", config.toString());
     info.put("User", user);
+    // Commented much of this out because each timeout is too long if there's no connection
     info.put("Canonical Service Name", fileSystem.getCanonicalServiceName());
-    info.put("Default Block Size", Long.toString(fileSystem.getDefaultBlockSize()));
-    info.put("Default Replication", Short.toString(fileSystem.getDefaultReplication()));
-    info.put("Home Directory", fileSystem.getHomeDirectory().toUri().toString());
-    info.put("Working Directory", fileSystem.getWorkingDirectory().toUri().toString());
+    //info.put("Default Block Size", Long.toString(fileSystem.getDefaultBlockSize()));
+    //info.put("Default Replication", Short.toString(fileSystem.getDefaultReplication()));
+    //info.put("Home Directory", fileSystem.getHomeDirectory().toUri().toString());
+    //info.put("Working Directory", fileSystem.getWorkingDirectory().toUri().toString());
     return info;
   }
 
diff --git a/framework/pull-agent/src/main/java/org/apache/manifoldcf/crawler/system/ManifoldCF.java b/framework/pull-agent/src/main/java/org/apache/manifoldcf/crawler/system/ManifoldCF.java
index 80f3424..e167aef 100644
--- a/framework/pull-agent/src/main/java/org/apache/manifoldcf/crawler/system/ManifoldCF.java
+++ b/framework/pull-agent/src/main/java/org/apache/manifoldcf/crawler/system/ManifoldCF.java
@@ -1706,6 +1706,8 @@
   
   protected static final String API_JOBNODE = "job";
   protected static final String API_JOBSTATUSNODE = "jobstatus";
+  protected static final String API_AUTHORIZATIONDOMAINNODE = "authorizationdomain";
+  protected static final String API_AUTHORITYGROUPNODE = "authoritygroup";
   protected static final String API_REPOSITORYCONNECTORNODE = "repositoryconnector";
   protected static final String API_OUTPUTCONNECTORNODE = "outputconnector";
   protected static final String API_AUTHORITYCONNECTORNODE = "authorityconnector";
@@ -1727,6 +1729,10 @@
   protected static final String CONNECTORNODE_DESCRIPTION = "description";
   protected static final String CONNECTORNODE_CLASSNAME = "class_name";
   
+  // Authorization domain nodes
+  protected static final String AUTHORIZATIONDOMAINNODE_DESCRIPTION = "description";
+  protected static final String AUTHORIZATIONDOMAINNODE_DOMAINNAME = "domain_name";
+  
   /** Decode path element.
   * Path elements in the API world cannot have "/" characters, or they become impossible to parse.  This method undoes
   * escaping that prevents "/" from appearing.
@@ -1979,6 +1985,7 @@
     return READRESULT_FOUND;
   }
   
+  
   /** Read an output connection's info */
   protected static int apiReadOutputConnectionInfo(IThreadContext tc, Configuration output, String connectionName, String command)
     throws ManifoldCFException
@@ -2155,6 +2162,57 @@
     return READRESULT_FOUND;
   }
   
+  /** Get authority groups */
+  protected static int apiReadAuthorityGroups(IThreadContext tc, Configuration output)
+    throws ManifoldCFException
+  {
+    try
+    {
+      IAuthorityGroupManager groupManager = AuthorityGroupManagerFactory.make(tc);
+      IAuthorityGroup[] groups = groupManager.getAllGroups();
+      int i = 0;
+      while (i < groups.length)
+      {
+        ConfigurationNode groupNode = new ConfigurationNode(API_AUTHORITYGROUPNODE);
+        formatAuthorityGroup(groupNode,groups[i++]);
+        output.addChild(output.getChildCount(),groupNode);
+      }
+    }
+    catch (ManifoldCFException e)
+    {
+      createErrorNode(output,e);
+    }
+    return READRESULT_FOUND;
+  }
+  
+  /** Read authority group */
+  protected static int apiReadAuthorityGroup(IThreadContext tc, Configuration output, String groupName)
+    throws ManifoldCFException
+  {
+    try
+    {
+      IAuthorityGroupManager groupManager = AuthorityGroupManagerFactory.make(tc);
+      IAuthorityGroup group = groupManager.load(groupName);
+      if (group != null)
+      {
+        // Fill the return object with job information
+        ConfigurationNode groupNode = new ConfigurationNode(API_AUTHORITYGROUPNODE);
+        formatAuthorityGroup(groupNode,group);
+        output.addChild(output.getChildCount(),groupNode);
+      }
+      else
+      {
+        createErrorNode(output,"Authority group '"+groupName+"' does not exist.");
+        return READRESULT_NOTFOUND;
+      }
+    }
+    catch (ManifoldCFException e)
+    {
+      createErrorNode(output,e);
+    }
+    return READRESULT_FOUND;
+  }
+
   /** Get output connections */
   protected static int apiReadOutputConnections(IThreadContext tc, Configuration output)
     throws ManifoldCFException
@@ -2470,6 +2528,44 @@
     return READRESULT_FOUND;
   }
 
+  /** List authorization domains */
+  protected static int apiReadAuthorizationDomains(IThreadContext tc, Configuration output)
+    throws ManifoldCFException
+  {
+    // List registered authorization domains
+    try
+    {
+      IAuthorizationDomainManager manager = AuthorizationDomainManagerFactory.make(tc);
+      IResultSet resultSet = manager.getDomains();
+      int j = 0;
+      while (j < resultSet.getRowCount())
+      {
+        IResultRow row = resultSet.getRow(j++);
+        ConfigurationNode child = new ConfigurationNode(API_AUTHORIZATIONDOMAINNODE);
+        String description = (String)row.getValue("description");
+        String domainName = (String)row.getValue("domainname");
+        ConfigurationNode node;
+        if (description != null)
+        {
+          node = new ConfigurationNode(AUTHORIZATIONDOMAINNODE_DESCRIPTION);
+          node.setValue(description);
+          child.addChild(child.getChildCount(),node);
+        }
+        node = new ConfigurationNode(AUTHORIZATIONDOMAINNODE_DOMAINNAME);
+        node.setValue(domainName);
+        child.addChild(child.getChildCount(),node);
+
+        output.addChild(output.getChildCount(),child);
+      }
+    }
+    catch (ManifoldCFException e)
+    {
+      createErrorNode(output,e);
+    }
+    return READRESULT_FOUND;
+
+  }
+  
   /** List repository connectors */
   protected static int apiReadRepositoryConnectors(IThreadContext tc, Configuration output)
     throws ManifoldCFException
@@ -3251,6 +3347,15 @@
       Long jobID = new Long(path.substring("jobstatusesnocounts/".length()));
       return apiReadJobStatusNoCounts(tc,output,jobID);
     }
+    else if (path.equals("authoritygroups"))
+    {
+      return apiReadAuthorityGroups(tc,output);
+    }
+    else if (path.startsWith("authoritygroups/"))
+    {
+      String groupName = decodeAPIPathElement(path.substring("authoritygroups/".length()));
+      return apiReadAuthorityGroup(tc,output,groupName);
+    }
     else if (path.equals("outputconnections"))
     {
       return apiReadOutputConnections(tc,output);
@@ -3302,7 +3407,11 @@
     else if (path.equals("repositoryconnectors"))
     {
       return apiReadRepositoryConnectors(tc,output);
-    }   
+    }
+    else if (path.equals("authorizationdomains"))
+    {
+      return apiReadAuthorizationDomains(tc,output);
+    }
     else
     {
       createErrorNode(output,"Unrecognized resource.");
@@ -3508,6 +3617,41 @@
     return WRITERESULT_FOUND;
   }
 
+  /** Write authority group.
+  */
+  protected static int apiWriteAuthorityGroup(IThreadContext tc, Configuration output, Configuration input, String groupName)
+    throws ManifoldCFException
+  {
+    ConfigurationNode groupNode = findConfigurationNode(input,API_AUTHORITYGROUPNODE);
+    if (groupNode == null)
+      throw new ManifoldCFException("Input argument must have '"+API_AUTHORITYGROUPNODE+"' field");
+      
+    // Turn the configuration node into an AuthorityGroup
+    org.apache.manifoldcf.authorities.authgroups.AuthorityGroup authorityGroup = new org.apache.manifoldcf.authorities.authgroups.AuthorityGroup();
+    processAuthorityGroup(authorityGroup,groupNode);
+      
+    if (authorityGroup.getName() == null)
+      authorityGroup.setName(groupName);
+    else
+    {
+      if (!authorityGroup.getName().equals(groupName))
+        throw new ManifoldCFException("Authority group name in path and in object must agree");
+    }
+      
+    try
+    {
+      // Save the connection.
+      IAuthorityGroupManager groupManager = AuthorityGroupManagerFactory.make(tc);
+      if (groupManager.save(authorityGroup))
+        return WRITERESULT_CREATED;
+    }
+    catch (ManifoldCFException e)
+    {
+      createErrorNode(output,e);
+    }
+    return WRITERESULT_FOUND;
+  }
+
   /** Write output connection.
   */
   protected static int apiWriteOutputConnection(IThreadContext tc, Configuration output, Configuration input, String connectionName)
@@ -3715,6 +3859,11 @@
       Long jobID = new Long(path.substring("jobs/".length()));
       return apiWriteJob(tc,output,input,jobID);
     }
+    else if (path.startsWith("authoritygroups/"))
+    {
+      String groupName = decodeAPIPathElement(path.substring("authoritygroups/".length()));
+      return apiWriteAuthorityGroup(tc,output,input,groupName);
+    }
     else if (path.startsWith("outputconnections/"))
     {
       String connectionName = decodeAPIPathElement(path.substring("outputconnections/".length()));
@@ -3786,6 +3935,23 @@
     return DELETERESULT_FOUND;
   }
   
+  /** Delete authority group.
+  */
+  protected static int apiDeleteAuthorityGroup(IThreadContext tc, Configuration output, String groupName)
+    throws ManifoldCFException
+  {
+    try
+    {
+      IAuthorityGroupManager groupManager = AuthorityGroupManagerFactory.make(tc);
+      groupManager.delete(groupName);
+    }
+    catch (ManifoldCFException e)
+    {
+      createErrorNode(output,e);
+    }
+    return DELETERESULT_FOUND;
+  }
+
   /** Delete output connection.
   */
   protected static int apiDeleteOutputConnection(IThreadContext tc, Configuration output, String connectionName)
@@ -3851,6 +4017,11 @@
       Long jobID = new Long(path.substring("jobs/".length()));
       return apiDeleteJob(tc,output,jobID);
     }
+    else if (path.startsWith("authoritygroups/"))
+    {
+      String groupName = decodeAPIPathElement(path.substring("authoritygroups/".length()));
+      return apiDeleteAuthorityGroup(tc,output,groupName);
+    }
     else if (path.startsWith("outputconnections/"))
     {
       String connectionName = decodeAPIPathElement(path.substring("outputconnections/".length()));
@@ -4557,6 +4728,73 @@
 
   // End of jobstatus API support.
   
+  // Authority group API
+  
+  protected static final String AUTHGROUPNODE_ISNEW = "isnew";
+  protected static final String AUTHGROUPNODE_NAME = "name";
+  protected static final String AUTHGROUPNODE_DESCRIPTION = "description";
+  
+  // Output connection API support.
+  
+  /** Convert input hierarchy into an AuthorityGroup object.
+  */
+  protected static void processAuthorityGroup(org.apache.manifoldcf.authorities.authgroups.AuthorityGroup group, ConfigurationNode groupNode)
+    throws ManifoldCFException
+  {
+    // Walk through the node's children
+    int i = 0;
+    while (i < groupNode.getChildCount())
+    {
+      ConfigurationNode child = groupNode.findChild(i++);
+      String childType = child.getType();
+      if (childType.equals(AUTHGROUPNODE_ISNEW))
+      {
+        if (child.getValue() == null)
+          throw new ManifoldCFException("Authority group isnew node requires a value");
+        group.setIsNew(child.getValue().equals("true"));
+      }
+      else if (childType.equals(AUTHGROUPNODE_NAME))
+      {
+        if (child.getValue() == null)
+          throw new ManifoldCFException("Authority group name node requires a value");
+        group.setName(child.getValue());
+      }
+      else if (childType.equals(AUTHGROUPNODE_DESCRIPTION))
+      {
+        if (child.getValue() == null)
+          throw new ManifoldCFException("Authority group description node requires a value");
+        group.setDescription(child.getValue());
+      }
+      else
+        throw new ManifoldCFException("Unrecognized authority group field: '"+childType+"'");
+    }
+
+  }
+  
+  /** Format an authority group.
+  */
+  protected static void formatAuthorityGroup(ConfigurationNode groupNode, IAuthorityGroup group)
+  {
+    ConfigurationNode child;
+    int j;
+
+    child = new ConfigurationNode(AUTHGROUPNODE_ISNEW);
+    child.setValue(group.getIsNew()?"true":"false");
+    groupNode.addChild(groupNode.getChildCount(),child);
+
+    child = new ConfigurationNode(AUTHGROUPNODE_NAME);
+    child.setValue(group.getName());
+    groupNode.addChild(groupNode.getChildCount(),child);
+
+    if (group.getDescription() != null)
+    {
+      child = new ConfigurationNode(AUTHGROUPNODE_DESCRIPTION);
+      child.setValue(group.getDescription());
+      groupNode.addChild(groupNode.getChildCount(),child);
+    }
+    
+  }
+
   // Connection API
   
   protected static final String CONNECTIONNODE_ISNEW = "isnew";
@@ -4572,6 +4810,7 @@
   protected static final String CONNECTIONNODE_MATCHDESCRIPTION = "match_description";
   protected static final String CONNECTIONNODE_RATE = "rate";
   protected static final String CONNECTIONNODE_AUTHDOMAIN = "authdomain";
+  protected static final String CONNECTIONNODE_AUTHGROUP = "authgroup";
   
   // Output connection API support.
   
@@ -4741,6 +4980,12 @@
           throw new ManifoldCFException("Connection authdomain node requires a value");
         connection.setAuthDomain(child.getValue());
       }
+      else if (childType.equals(CONNECTIONNODE_AUTHGROUP))
+      {
+        if (child.getValue() == null)
+          throw new ManifoldCFException("Connection authgroup node requires a value");
+        connection.setAuthGroup(child.getValue());
+      }
       else if (childType.equals(CONNECTIONNODE_DESCRIPTION))
       {
         if (child.getValue() == null)
@@ -4812,6 +5057,10 @@
       connectionNode.addChild(connectionNode.getChildCount(),child);
     }
     
+    child = new ConfigurationNode(CONNECTIONNODE_AUTHGROUP);
+    child.setValue(connection.getAuthGroup());
+    connectionNode.addChild(connectionNode.getChildCount(),child);
+
     ConfigParams cp = connection.getConfigParams();
     child = new ConfigurationNode(CONNECTIONNODE_CONFIGURATION);
     j = 0;
diff --git a/site/src/documentation/content/xdocs/en_US/concepts.xml b/site/src/documentation/content/xdocs/en_US/concepts.xml
index e5022bd..a7b8033 100644
--- a/site/src/documentation/content/xdocs/en_US/concepts.xml
+++ b/site/src/documentation/content/xdocs/en_US/concepts.xml
@@ -62,18 +62,36 @@
           would be an Active Directory SID (e.g. "S-1-23-4-1-45").  But, for example, for documents protected by Livelink a wholly different string would be used.</p>
         <p></p>
         <p>In the ManifoldCF security model, it is the job of an <em>authority</em> to provide a list of access tokens for a given searching user.  Multiple authorities cooperate
-          in that each one can add to the list of access tokens describing a given user's security.  The resulting access tokens are handed to the search engine as part of every
+          in that each one can add to the list of access tokens describing a given user's security.  A user is described in terms of a set of <em>authorization domains</em>
+          and user name tuples.  Any given authority will provide access tokens for a user name corresponding to one authorization domain.  For example,
+          an authority that understands FaceBook users would only respond to a FaceBook user name.  Access tokens from all applicable authorities are added into the final list that is handed to the search engine as part of every
           search request, so that the search engine may properly exclude documents that the user is not allowed to see.</p>
         <p></p>
         <p>When document indexing is done, therefore, it is the job of the crawler to hand access tokens to the search engine, so that it may categorize the documents properly
-          according to their accessibility.  Note that the access tokens so provided are meaningful only within the space of the governing authority.  Access tokens can be provided
-          as "grant" tokens, or as "deny" tokens.  Finally, there are multiple levels of tokens, which correspond to Active Directory's concepts of "share" security, "directory" security,
-          or "file" security.  (The latter concepts are rarely used except for documents that come from Windows or Samba systems.)</p>
+          according to their accessibility.  The access tokens the crawler attaches to a document are meaningful only within the space of the governing <em>authority group</em>.  An
+          authority group describes a set of authorities which all can cooperate to provide access tokens for a single given document.  Each authority belongs
+          to exactly one authority group.  Authority groups serve to separate access tokens into different spaces so that they cannot interfere with one another.</p>
+        <p></p>
+        <p>For example, say that you would want to crawl documents from a LiveLink repository, as well as from a Windows shared drive.
+          You will therefore have two kinds of documents that are each secured in an entirely different way.  There is a LiveLink authority connection, which provides LiveLink
+          access tokens, and there is an Active Directory authority connection, which provides Windows access tokens.  Now, you don't want there to be any chance
+          that a LiveLink access token could be confused with an Active Directory SID, so the way you do that in ManifoldCF is to create two distinct
+          authority groups, each of which provides access tokens meant for specific kinds of repository documents.  Thus, documents secured by Active
+          Directory SIDs should be indexed against an Active Directory authority group, and documents secured by LiveLink access tokens should have a LiveLink
+          authority group.  Finally, the Active Directory authority connection should then belong to the Active Directory authority group, and the LiveLink authority connection should
+          belong to the LiveLink authority group.</p>
+        <p></p>
+        <p>In addition to specifying the correct authority group, access tokens can be attached to documents as "grant" tokens, or as "deny" tokens.
+          "Grant" tokens provide access, "deny" tokens restrict it.  "Deny" tokens, if matched, always win over "grant" tokens.
+          And finally, there are multiple levels of tokens, which correspond to Active Directory's concepts
+          of "share" security, specific "directory" security, or "file" security.  (The latter concepts are rarely used except for documents that come from
+          Windows or Samba systems.)  Each level provided must agree that the document is to be visible for the document to appear in search
+          results.</p>
         <p></p>
         <p>Once all these documents and their access tokens are handed to the search engine, it is the search engine's job to enforce security by excluding inappropriate documents
           from the search results.  For Solr and for ElasticSearch, this infrastructure has been included in ManifoldCF releases as a Solr plugin (both 3.x and 4.x varieties) and an
-          ElasticSearch plugin.  Bear in mind that this plug-in is still not a complete solution, as it requires an authenticated user
-          name to be passed to it from some upstream source, possibly a JAAS authenticator within an application server framework.</p>
+          ElasticSearch plugin.  Bear in mind that this plug-in is still not a complete solution, as it requires one or more authenticated user
+          names to be passed to it from some upstream source, possibly a JAAS authenticator within an application server framework.</p>
         <p></p>
       </section>
       <section>
@@ -144,6 +162,24 @@
           <p>Jobs are allowed to share the same repository connection, and thus they can overlap in the set of documents they describe.  ManifoldCF permits this situation, although 
             when it occurs it is probably an accident.</p>
         </section>
+        <section>
+          <title>Authorization domains</title>
+          <p></p>
+          <p>ManifoldCF supports a federated concept of a user.  The same user, for instance, may have one login name for FaceBook, another for Windows,
+            and yet another for Google.  We can describe this user as having three different authorization domains: "FaceBook", "Windows", and "Google".</p>
+          <p>In ManifoldCF, each authority understands user names or ids from one specific authorization domain.  This allows ManifoldCF to be configured
+            so that access tokens generated from multiple independent sources are amalgamated, even if the incoming user names differ from source to
+            source.</p>
+        </section>
+        <section>
+          <title>Authority groups</title>
+          <p></p>
+          <p>ManifoldCF groups authority connections together in groups, so that multiple authorities can furnish security for a single document.  An authority
+            group is nothing more than a name and a description that is referenced by authority connections that are part of that group, and is referenced also
+            by repository connections that wish to be secured by that group.  For most simple repositories, there is one authority group per authority.  But
+            repositories capable of federated security (e.g. SharePoint with Claim Space support) can use multiple authorities to describe security for a single
+            document.  Authority groups allow configuration of the appropriate many-to-many relationship for this situation.</p>
+        </section>
       </section>
     </section>
   </body>
diff --git a/site/src/documentation/content/xdocs/en_US/programmatic-operation.xml b/site/src/documentation/content/xdocs/en_US/programmatic-operation.xml
index 1b33f48..a656b0b 100644
--- a/site/src/documentation/content/xdocs/en_US/programmatic-operation.xml
+++ b/site/src/documentation/content/xdocs/en_US/programmatic-operation.xml
@@ -79,10 +79,15 @@
           <p></p>
           <table>
             <tr><th>Resource</th><th>Verb</th><th>What it does</th><th>Input format/query args</th><th>Output format</th></tr>
+            <tr><td>authorizationdomains</td><td>GET</td><td>List all registered authorization domains</td><td>N/A</td><td>{"authorizationdomain":[<em>&lt;list_of_authorization_domain_objects&gt;</em>]} <strong>OR</strong> {"error":<em>&lt;error_text&gt;</em>}</td></tr>
             <tr><td>outputconnectors</td><td>GET</td><td>List all registered output connectors</td><td>N/A</td><td>{"outputconnector":[<em>&lt;list_of_output_connector_objects&gt;</em>]} <strong>OR</strong> {"error":<em>&lt;error_text&gt;</em>}</td></tr>
             <tr><td>mappingconnectors</td><td>GET</td><td>List all registered mapping connectors</td><td>N/A</td><td>{"mappingconnector":[<em>&lt;list_of_mapping_connector_objects&gt;</em>]} <strong>OR</strong> {"error":<em>&lt;error_text&gt;</em>}</td></tr>
             <tr><td>authorityconnectors</td><td>GET</td><td>List all registered authority connectors</td><td>N/A</td><td>{"authorityconnector":[<em>&lt;list_of_authority_connector_objects&gt;</em>]} <strong>OR</strong> {"error":<em>&lt;error_text&gt;</em>}</td></tr>
             <tr><td>repositoryconnectors</td><td>GET</td><td>List all registered repository connectors</td><td>N/A</td><td>{"repositoryconnector":[<em>&lt;list_of_repository_connector_objects&gt;</em>]} <strong>OR</strong> {"error":<em>&lt;error_text&gt;</em>}</td></tr>
+            <tr><td>authoritygroups</td><td>GET</td><td>List all authority groups</td><td>N/A</td><td>{"authoritygroup":[<em>&lt;list_of_authority_group_objects&gt;</em>]} <strong>OR</strong> {"error":<em>&lt;error_text&gt;</em>}</td></tr>
+            <tr><td>authoritygroups/<em>&lt;encoded_group_name&gt;</em></td><td>GET</td><td>Get a specific authority group</td><td>N/A</td><td>{"authoritygroup":<em>&lt;authority_group_object&gt;</em>} <strong>OR</strong> { } <strong>OR</strong> {"error":<em>&lt;error_text&gt;</em>}</td></tr>
+            <tr><td>authoritygroups/<em>&lt;encoded_group_name&gt;</em></td><td>PUT</td><td>Save or create an authority group</td><td>{"authoritygroup":<em>&lt;authority_group_object&gt;</em>}</td><td>{ } <strong>OR</strong> {"error":<em>&lt;error_text&gt;</em>}</td></tr>
+            <tr><td>authoritygroups/<em>&lt;encoded_group_name&gt;</em></td><td>DELETE</td><td>Delete an authority group</td><td>N/A</td><td>{ } <strong>OR</strong> {"error":<em>&lt;error_text&gt;</em>}</td></tr>
             <tr><td>outputconnections</td><td>GET</td><td>List all output connections</td><td>N/A</td><td>{"outputconnection":[<em>&lt;list_of_output_connection_objects&gt;</em>]} <strong>OR</strong> {"error":<em>&lt;error_text&gt;</em>}</td></tr>
             <tr><td>outputconnections/<em>&lt;encoded_connection_name&gt;</em></td><td>GET</td><td>Get a specific output connection</td><td>N/A</td><td>{"outputconnection":<em>&lt;output_connection_object&gt;</em>} <strong>OR</strong> { } <strong>OR</strong> {"error":<em>&lt;error_text&gt;</em>}</td></tr>
             <tr><td>outputconnections/<em>&lt;encoded_connection_name&gt;</em></td><td>PUT</td><td>Save or create an output connection</td><td>{"outputconnection":<em>&lt;output_connection_object&gt;</em>}</td><td>{ } <strong>OR</strong> {"error":<em>&lt;error_text&gt;</em>}</td></tr>
@@ -195,6 +200,18 @@
           </table>
         </section>
         <section>
+          <title>Authorization domain objects</title>
+          <p></p>
+          <p>The JSON fields an authorization domain object has are as follows:</p>
+          <p></p>
+          <table>
+            <tr><th>Field</th><th>Meaning</th></tr>
+            <tr><td>"description"</td><td>The optional description of the authorization domain</td></tr>
+            <tr><td>"domain_name"</td><td>The internal name of the authorization domain, i.e. what is sent to the Authority Service</td></tr>
+          </table>
+          <p></p>
+        </section>
+        <section>
           <title>Output connector objects</title>
           <p></p>
           <p>The JSON fields an output connector object has are as follows:</p>
@@ -243,6 +260,26 @@
           <p></p>
         </section>
         <section>
+          <title>Authority group objects</title>
+          <p></p>
+          <p>Authority group names, when they are part of a URL, should be encoded as follows:</p>
+          <p></p>
+          <ol>
+            <li>All instances of '.' should be replaced by '..'.</li>
+            <li>All instances of '/' should be replaced by '.+'.</li>
+            <li>The URL should be encoded using standard URL utf-8-based %-encoding.</li>
+          </ol>
+          <p></p>
+          <p>The JSON fields an authority group object has are as follows:</p>
+          <p></p>
+          <table>
+            <tr><th>Field</th><th>Meaning</th></tr>
+            <tr><td>"name"</td><td>The unique name of the group</td></tr>
+            <tr><td>"description"</td><td>The description of the group</td></tr>
+          </table>
+          <p></p>
+        </section>
+        <section>
           <title>Output connection objects</title>
           <p></p>
           <p>Output connection names, when they are part of a URL, should be encoded as follows:</p>
@@ -311,6 +348,7 @@
             <tr><td>"configuration"</td><td>The configuration object for the connection, which is specific to the connection class</td></tr>
             <tr><td>"prerequisite"</td><td>The mapping connection prerequisite, if any</td></tr>
             <tr><td>"authdomain"</td><td>The authorization domain for the authority connection, if any</td></tr>
+            <tr><td>"authgroup"</td><td>The required authority group for the authority connection</td></tr>
           </table>
           <p></p>
         </section>
@@ -334,7 +372,7 @@
             <tr><td>"class_name"</td><td>The java class name of the class implementing the connection</td></tr>
             <tr><td>"max_connections"</td><td>The total number of outstanding connections allowed to exist at a time</td></tr>
             <tr><td>"configuration"</td><td>The configuration object for the connection, which is specific to the connection class</td></tr>
-            <tr><td>"acl_authority"</td><td>The (optional) name of the authority that will enforce security for this connection</td></tr>
+            <tr><td>"acl_authority"</td><td>The (optional) name of the authority group that will enforce security for this connection</td></tr>
             <tr><td>"throttle"</td><td>An array of throttle objects, which control how quickly documents can be requested from this connection</td></tr>
           </table>
           <p></p>
diff --git a/site/src/documentation/content/xdocs/ja_JP/programmatic-operation.xml b/site/src/documentation/content/xdocs/ja_JP/programmatic-operation.xml
index 1b33f48..a656b0b 100644
--- a/site/src/documentation/content/xdocs/ja_JP/programmatic-operation.xml
+++ b/site/src/documentation/content/xdocs/ja_JP/programmatic-operation.xml
@@ -79,10 +79,15 @@
           <p></p>
           <table>
             <tr><th>Resource</th><th>Verb</th><th>What it does</th><th>Input format/query args</th><th>Output format</th></tr>
+            <tr><td>authorizationdomains</td><td>GET</td><td>List all registered authorization domains</td><td>N/A</td><td>{"authorizationdomain":[<em>&lt;list_of_authorization_domain_objects&gt;</em>]} <strong>OR</strong> {"error":<em>&lt;error_text&gt;</em>}</td></tr>
             <tr><td>outputconnectors</td><td>GET</td><td>List all registered output connectors</td><td>N/A</td><td>{"outputconnector":[<em>&lt;list_of_output_connector_objects&gt;</em>]} <strong>OR</strong> {"error":<em>&lt;error_text&gt;</em>}</td></tr>
             <tr><td>mappingconnectors</td><td>GET</td><td>List all registered mapping connectors</td><td>N/A</td><td>{"mappingconnector":[<em>&lt;list_of_mapping_connector_objects&gt;</em>]} <strong>OR</strong> {"error":<em>&lt;error_text&gt;</em>}</td></tr>
             <tr><td>authorityconnectors</td><td>GET</td><td>List all registered authority connectors</td><td>N/A</td><td>{"authorityconnector":[<em>&lt;list_of_authority_connector_objects&gt;</em>]} <strong>OR</strong> {"error":<em>&lt;error_text&gt;</em>}</td></tr>
             <tr><td>repositoryconnectors</td><td>GET</td><td>List all registered repository connectors</td><td>N/A</td><td>{"repositoryconnector":[<em>&lt;list_of_repository_connector_objects&gt;</em>]} <strong>OR</strong> {"error":<em>&lt;error_text&gt;</em>}</td></tr>
+            <tr><td>authoritygroups</td><td>GET</td><td>List all authority groups</td><td>N/A</td><td>{"authoritygroup":[<em>&lt;list_of_authority_group_objects&gt;</em>]} <strong>OR</strong> {"error":<em>&lt;error_text&gt;</em>}</td></tr>
+            <tr><td>authoritygroups/<em>&lt;encoded_group_name&gt;</em></td><td>GET</td><td>Get a specific authority group</td><td>N/A</td><td>{"authoritygroup":<em>&lt;authority_group_object&gt;</em>} <strong>OR</strong> { } <strong>OR</strong> {"error":<em>&lt;error_text&gt;</em>}</td></tr>
+            <tr><td>authoritygroups/<em>&lt;encoded_group_name&gt;</em></td><td>PUT</td><td>Save or create an authority group</td><td>{"authoritygroup":<em>&lt;authority_group_object&gt;</em>}</td><td>{ } <strong>OR</strong> {"error":<em>&lt;error_text&gt;</em>}</td></tr>
+            <tr><td>authoritygroups/<em>&lt;encoded_group_name&gt;</em></td><td>DELETE</td><td>Delete an authority group</td><td>N/A</td><td>{ } <strong>OR</strong> {"error":<em>&lt;error_text&gt;</em>}</td></tr>
             <tr><td>outputconnections</td><td>GET</td><td>List all output connections</td><td>N/A</td><td>{"outputconnection":[<em>&lt;list_of_output_connection_objects&gt;</em>]} <strong>OR</strong> {"error":<em>&lt;error_text&gt;</em>}</td></tr>
             <tr><td>outputconnections/<em>&lt;encoded_connection_name&gt;</em></td><td>GET</td><td>Get a specific output connection</td><td>N/A</td><td>{"outputconnection":<em>&lt;output_connection_object&gt;</em>} <strong>OR</strong> { } <strong>OR</strong> {"error":<em>&lt;error_text&gt;</em>}</td></tr>
             <tr><td>outputconnections/<em>&lt;encoded_connection_name&gt;</em></td><td>PUT</td><td>Save or create an output connection</td><td>{"outputconnection":<em>&lt;output_connection_object&gt;</em>}</td><td>{ } <strong>OR</strong> {"error":<em>&lt;error_text&gt;</em>}</td></tr>
@@ -195,6 +200,18 @@
           </table>
         </section>
         <section>
+          <title>Authorization domain objects</title>
+          <p></p>
+          <p>The JSON fields an authorization domain object has are as follows:</p>
+          <p></p>
+          <table>
+            <tr><th>Field</th><th>Meaning</th></tr>
+            <tr><td>"description"</td><td>The optional description of the authorization domain</td></tr>
+            <tr><td>"domain_name"</td><td>The internal name of the authorization domain, i.e. what is sent to the Authority Service</td></tr>
+          </table>
+          <p></p>
+        </section>
+        <section>
           <title>Output connector objects</title>
           <p></p>
           <p>The JSON fields an output connector object has are as follows:</p>
@@ -243,6 +260,26 @@
           <p></p>
         </section>
         <section>
+          <title>Authority group objects</title>
+          <p></p>
+          <p>Authority group names, when they are part of a URL, should be encoded as follows:</p>
+          <p></p>
+          <ol>
+            <li>All instances of '.' should be replaced by '..'.</li>
+            <li>All instances of '/' should be replaced by '.+'.</li>
+            <li>The URL should be encoded using standard URL utf-8-based %-encoding.</li>
+          </ol>
+          <p></p>
+          <p>The JSON fields an authority group object has are as follows:</p>
+          <p></p>
+          <table>
+            <tr><th>Field</th><th>Meaning</th></tr>
+            <tr><td>"name"</td><td>The unique name of the group</td></tr>
+            <tr><td>"description"</td><td>The description of the group</td></tr>
+          </table>
+          <p></p>
+        </section>
+        <section>
           <title>Output connection objects</title>
           <p></p>
           <p>Output connection names, when they are part of a URL, should be encoded as follows:</p>
@@ -311,6 +348,7 @@
             <tr><td>"configuration"</td><td>The configuration object for the connection, which is specific to the connection class</td></tr>
             <tr><td>"prerequisite"</td><td>The mapping connection prerequisite, if any</td></tr>
             <tr><td>"authdomain"</td><td>The authorization domain for the authority connection, if any</td></tr>
+            <tr><td>"authgroup"</td><td>The required authority group for the authority connection</td></tr>
           </table>
           <p></p>
         </section>
@@ -334,7 +372,7 @@
             <tr><td>"class_name"</td><td>The java class name of the class implementing the connection</td></tr>
             <tr><td>"max_connections"</td><td>The total number of outstanding connections allowed to exist at a time</td></tr>
             <tr><td>"configuration"</td><td>The configuration object for the connection, which is specific to the connection class</td></tr>
-            <tr><td>"acl_authority"</td><td>The (optional) name of the authority that will enforce security for this connection</td></tr>
+            <tr><td>"acl_authority"</td><td>The (optional) name of the authority group that will enforce security for this connection</td></tr>
             <tr><td>"throttle"</td><td>An array of throttle objects, which control how quickly documents can be requested from this connection</td></tr>
           </table>
           <p></p>
diff --git a/tests/activedirectory/src/test/java/org/apache/manifoldcf/activedirectory_tests/NavigationDerbyUI.java b/tests/activedirectory/src/test/java/org/apache/manifoldcf/activedirectory_tests/NavigationDerbyUI.java
index c5a3df0..4c35d0c 100644
--- a/tests/activedirectory/src/test/java/org/apache/manifoldcf/activedirectory_tests/NavigationDerbyUI.java
+++ b/tests/activedirectory/src/test/java/org/apache/manifoldcf/activedirectory_tests/NavigationDerbyUI.java
@@ -61,6 +61,19 @@
 
     // Define an authority connection via the UI
     window = testerInstance.findWindow(null);
+    link = window.findLink(testerInstance.createStringDescription("List authority groups"));
+    link.click();
+    window = testerInstance.findWindow(null);
+    link = window.findLink(testerInstance.createStringDescription("Add new authority group"));
+    link.click();
+    window = testerInstance.findWindow(null);
+    form = window.findForm(testerInstance.createStringDescription("editgroup"));
+    textarea = form.findTextarea(testerInstance.createStringDescription("groupname"));
+    textarea.setValue(testerInstance.createStringDescription("MyAuthorityConnection"));
+    button = window.findButton(testerInstance.createStringDescription("Save this authority group"));
+    button.click();
+
+    window = testerInstance.findWindow(null);
     link = window.findLink(testerInstance.createStringDescription("List authorities"));
     link.click();
     window = testerInstance.findWindow(null);
@@ -78,6 +91,8 @@
     form = window.findForm(testerInstance.createStringDescription("editconnection"));
     selectbox = form.findSelectbox(testerInstance.createStringDescription("classname"));
     selectbox.selectValue(testerInstance.createStringDescription("org.apache.manifoldcf.authorities.authorities.activedirectory.ActiveDirectoryAuthority"));
+    selectbox = form.findSelectbox(testerInstance.createStringDescription("authoritygroup"));
+    selectbox.selectValue(testerInstance.createStringDescription("MyAuthorityConnection"));
     button = window.findButton(testerInstance.createStringDescription("Continue to next page"));
     button.click();
     // Server tab
diff --git a/tests/cmis/src/test/java/org/apache/manifoldcf/cmis_tests/NavigationDerbyUI.java b/tests/cmis/src/test/java/org/apache/manifoldcf/cmis_tests/NavigationDerbyUI.java
index e5968fa..1c650ca 100644
--- a/tests/cmis/src/test/java/org/apache/manifoldcf/cmis_tests/NavigationDerbyUI.java
+++ b/tests/cmis/src/test/java/org/apache/manifoldcf/cmis_tests/NavigationDerbyUI.java
@@ -132,6 +132,19 @@
     
     // Define an authority connection via the UI
     window = testerInstance.findWindow(null);
+    link = window.findLink(testerInstance.createStringDescription("List authority groups"));
+    link.click();
+    window = testerInstance.findWindow(null);
+    link = window.findLink(testerInstance.createStringDescription("Add new authority group"));
+    link.click();
+    window = testerInstance.findWindow(null);
+    form = window.findForm(testerInstance.createStringDescription("editgroup"));
+    textarea = form.findTextarea(testerInstance.createStringDescription("groupname"));
+    textarea.setValue(testerInstance.createStringDescription("MyAuthorityConnection"));
+    button = window.findButton(testerInstance.createStringDescription("Save this authority group"));
+    button.click();
+
+    window = testerInstance.findWindow(null);
     link = window.findLink(testerInstance.createStringDescription("List authorities"));
     link.click();
     window = testerInstance.findWindow(null);
@@ -149,6 +162,8 @@
     form = window.findForm(testerInstance.createStringDescription("editconnection"));
     selectbox = form.findSelectbox(testerInstance.createStringDescription("classname"));
     selectbox.selectValue(testerInstance.createStringDescription("org.apache.manifoldcf.crawler.connectors.cmis.CmisAuthorityConnector"));
+    selectbox = form.findSelectbox(testerInstance.createStringDescription("authoritygroup"));
+    selectbox.selectValue(testerInstance.createStringDescription("MyAuthorityConnection"));
     button = window.findButton(testerInstance.createStringDescription("Continue to next page"));
     button.click();
     window = testerInstance.findWindow(null);
diff --git a/tests/ldap/src/test/java/org/apache/manifoldcf/ldap_tests/NavigationDerbyUI.java b/tests/ldap/src/test/java/org/apache/manifoldcf/ldap_tests/NavigationDerbyUI.java
index 1d2f7ff..7fdd649 100644
--- a/tests/ldap/src/test/java/org/apache/manifoldcf/ldap_tests/NavigationDerbyUI.java
+++ b/tests/ldap/src/test/java/org/apache/manifoldcf/ldap_tests/NavigationDerbyUI.java
@@ -62,6 +62,19 @@
 
     // Define an authority connection via the UI
     window = testerInstance.findWindow(null);
+    link = window.findLink(testerInstance.createStringDescription("List authority groups"));
+    link.click();
+    window = testerInstance.findWindow(null);
+    link = window.findLink(testerInstance.createStringDescription("Add new authority group"));
+    link.click();
+    window = testerInstance.findWindow(null);
+    form = window.findForm(testerInstance.createStringDescription("editgroup"));
+    textarea = form.findTextarea(testerInstance.createStringDescription("groupname"));
+    textarea.setValue(testerInstance.createStringDescription("MyAuthorityConnection"));
+    button = window.findButton(testerInstance.createStringDescription("Save this authority group"));
+    button.click();
+
+    window = testerInstance.findWindow(null);
     link = window.findLink(testerInstance.createStringDescription("List authorities"));
     link.click();
     window = testerInstance.findWindow(null);
@@ -79,6 +92,8 @@
     form = window.findForm(testerInstance.createStringDescription("editconnection"));
     selectbox = form.findSelectbox(testerInstance.createStringDescription("classname"));
     selectbox.selectValue(testerInstance.createStringDescription("org.apache.manifoldcf.authorities.authorities.ldap.LDAPAuthority"));
+    selectbox = form.findSelectbox(testerInstance.createStringDescription("authoritygroup"));
+    selectbox.selectValue(testerInstance.createStringDescription("MyAuthorityConnection"));
     button = window.findButton(testerInstance.createStringDescription("Continue to next page"));
     button.click();
     // Server tab