Fix b/16198970: Config option to skip reading share ACL

The adaptor attempts to preserve access control integrity when sending
Access Control Lists (ACLs) to the GSA. In general, only users that have access
to a file share have access to the files maintained on that share, so the
adaptor includes the share's ACL in those sent to the GSA.  However, in some
configurations, the adaptor may not have sufficient permissions to read the
share ACL. In those instances, the broken share ACL will prevent all files
maintained on that file share from appearing in search results. The GSA's
Index Diagnostics for those will also indicate a broken inheritance chain.

If the share ACL cannot be read by the adaptor, the administrator may
skip the attempt to read the share ACL by setting the
'filesystemadaptor.skipShareAccessControl' configuration option to 'true'.
This feeds a highly permissive share ACL to the GSA, rather than the actual
share ACL, leaving only the file system ACLs to control access.

WARNING: Bypassing the file share access control may be inconsistent with
enterprise security policies. This may allow users that do not have access
to the file share to see documents hosted by that file share in search results.

Code Review:
http://codereview.appspot.com/123780043
diff --git a/src/com/google/enterprise/adaptor/fs/FsAdaptor.java b/src/com/google/enterprise/adaptor/fs/FsAdaptor.java
index d2d58f8..bc090bd 100644
--- a/src/com/google/enterprise/adaptor/fs/FsAdaptor.java
+++ b/src/com/google/enterprise/adaptor/fs/FsAdaptor.java
@@ -116,6 +116,10 @@
   /** DocId for the share ACL named resource. */
   private static final DocId SHARE_ACL_DOCID = new DocId("shareAcl");
 
+  /** The config option that forces us to ignore the share ACL. */
+  private static final String CONFIG_SKIP_SHARE_ACL = 
+      "filesystemadaptor.skipShareAccessControl";
+
   /** The config parameter name for the prefix for BUILTIN groups. */
   private static final String CONFIG_BUILTIN_PREFIX =
       "filesystemadaptor.builtinGroupPrefix";
@@ -162,6 +166,7 @@
   private boolean isDfsUnc;
   private DocId rootPathDocId;
   private FileDelegate delegate;
+  private boolean skipShareAcl;
   private ShareAcls lastPushedShareAcls = null;
 
   /** Filter that may exclude files whose last modified time is too old. */
@@ -206,6 +211,7 @@
         + "NT AUTHORITY\\INTERACTIVE,NT AUTHORITY\\Authenticated Users");
     config.addKey(CONFIG_BUILTIN_PREFIX, "BUILTIN\\");
     config.addKey(CONFIG_NAMESPACE, Principal.DEFAULT_NAMESPACE);
+    config.addKey(CONFIG_SKIP_SHARE_ACL, "false");
     config.addKey(CONFIG_CRAWL_HIDDEN_FILES, "false");
     config.addKey(CONFIG_LAST_ACCESSED_DAYS, "");
     config.addKey(CONFIG_LAST_ACCESSED_DATE, "");
@@ -299,6 +305,11 @@
           + "property \"filesystemadaptor.crawlHiddenFiles\" to \"true\".");
     }
 
+    // The Administrator may bypass Share access control.
+    skipShareAcl = Boolean.parseBoolean(
+        context.getConfig().getValue(CONFIG_SKIP_SHARE_ACL));
+    log.log(Level.CONFIG, "skipShareAcl: {0}", skipShareAcl);
+
     // Add filters that may exclude older content.
     lastAccessTimeFilter = getFileTimeFilter(context.getConfig(),
         CONFIG_LAST_ACCESSED_DAYS, CONFIG_LAST_ACCESSED_DATE);
@@ -368,7 +379,12 @@
     Acl shareAcl;
     Acl dfsShareAcl;
 
-    if (isDfsUnc) {
+    if (skipShareAcl) {
+      // Ignore the Share ACL, but create a benign placeholder.
+      dfsShareAcl = null;
+      shareAcl = new Acl.Builder().setEverythingCaseInsensitive()
+          .setInheritanceType(InheritanceType.CHILD_OVERRIDES).build();
+    } else if (isDfsUnc) {
       // For a DFS UNC we have a DFS Acl that must be sent. Also, the share Acl
       // must be the Acl for the target storage UNC.
       // TODO(mifern): This assumes that rootPath is a DFS link since it calls
@@ -716,8 +732,8 @@
 
     public ShareAcls(Acl shareAcl, Acl dfsShareAcl) {
       Preconditions.checkNotNull(shareAcl, "the share Acl may not be null");
-      Preconditions.checkArgument(!isDfsUnc || (dfsShareAcl != null),
-          "the DFS share Acl may not be null");
+      Preconditions.checkArgument(skipShareAcl || !isDfsUnc 
+          || (dfsShareAcl != null), "the DFS share Acl may not be null");
       this.shareAcl = shareAcl;
       this.dfsShareAcl = dfsShareAcl;
     }
diff --git a/src/overview.html b/src/overview.html
index a84f486..d3e6c7c 100644
--- a/src/overview.html
+++ b/src/overview.html
@@ -23,8 +23,10 @@
     <li>Read the content of documents</li> 
     <li>Read attributes of files and folders</li>
     <li>Read permissions (ACLs) for both files and folders</li>
-    <li>Write basic attributes permissions. See below: <em>Advanced Topics > Not changing 'last
-    access' of the documents on the share</em></li>
+    <li>Write basic attributes permissions. See below: <em>Advanced Topics > 
+    Not changing 'last access' of the documents on the share</em></li>
+    <li>Read permissions (ACLs) for the file share. See below:
+    <em>Advanced Topics > Skipping Share Access Control</em></li>
   </ul>
 
   <p>Membership in one of these groups grants a Windows account the
@@ -175,6 +177,19 @@
   </pre>
   </dd>
   <dt>
+  <code>filesystemadaptor.skipShareAccessControl</code>
+  </dt>
+  <dd>
+  This boolean configuration property enables or disables sending
+  the Access Control List (ACL) for the file share to the GSA.
+  See below: <em>Advanced Topics > Skipping Share Access Control</em>.
+  <p/>
+  Normally, the share ACLs are sent to the GSA, so the default value is:
+  <pre>
+  false
+  </pre>
+  </dd>
+  <dt>
   <code>filesystemadaptor.lastAccessedDate</code>
   </dt>
   <dd>
@@ -294,8 +309,29 @@
 document content during a crawl.
 
 <br>
-<br>
+<h4>Skipping File Share Access Control</h4>
+<p>The adaptor attempts to preserve access control integrity when sending
+Access Control Lists (ACLs) to the GSA. In general, only users that have access
+to a file share have access to the files maintained on that share, so the 
+adaptor includes the share's ACL in those sent to the GSA.  However, in some
+configurations, the adaptor may not have sufficient permissions to read the
+share ACL. In those instances, the broken share ACL will prevent all files
+maintained on that file share from appearing in search results. The GSA's
+Index Diagnostics for those will also indicate a broken inheritance chain.</p>
 
+<p>If the share ACL cannot be read by the adaptor, the administrator may
+skip the attempt to read the share ACL by setting the 
+<code>filesystemadaptor.skipShareAccessControl</code> configuration option
+to <code>true</code>.  This feeds a highly permissive share ACL to the
+GSA, rather than the actual share ACL.</p>
+
+<p><b>WARNING:</b> Bypassing the file share access control may be 
+inconsistent with your enterprise security policies. This may allow users
+that do not have access to the file share to see documents hosted by that
+file share in search results.</p>
+
+<br>
+<br>
 
 <h3> Developer Topics </h3>
 
diff --git a/test/com/google/enterprise/adaptor/fs/FsAdaptorTest.java b/test/com/google/enterprise/adaptor/fs/FsAdaptorTest.java
index 85e31c7..32b2b91 100644
--- a/test/com/google/enterprise/adaptor/fs/FsAdaptorTest.java
+++ b/test/com/google/enterprise/adaptor/fs/FsAdaptorTest.java
@@ -300,6 +300,49 @@
   }
 
   @Test
+  public void testGetDocIdsSkipShareAcl() throws Exception {
+    config.overrideKey("filesystemadaptor.skipShareAccessControl", "true");
+    adaptor.init(context);
+    adaptor.getDocIds(pusher);
+
+    // We should just push the root docid.
+    List<Record> records = pusher.getRecords();
+    assertEquals(1, records.size());
+    assertEquals(delegate.newDocId(rootPath), records.get(0).getDocId());
+
+    // We should have pushed an empty, child-overrides ACL for the share.
+    List<Map<DocId, Acl>> namedResources = pusher.getNamedResources();
+    Acl expected = new Acl.Builder().setEverythingCaseInsensitive()
+        .setInheritanceType(InheritanceType.CHILD_OVERRIDES).build();
+    assertEquals(1, namedResources.size());
+    assertEquals(1, namedResources.get(0).size());
+    assertEquals(expected, namedResources.get(0).get(shareAclDocId));
+  }
+
+  @Test
+  public void testGetDocIdsDfsSkipShareAcls() throws Exception {
+    Path uncPath = Paths.get("\\\\dfshost\\share");
+    root.setDfsUncActiveStorageUnc(uncPath);
+    root.setDfsShareAclView(MockFile.FULL_ACCESS_ACLVIEW);
+    config.overrideKey("filesystemadaptor.skipShareAccessControl", "true");
+    adaptor.init(context);
+    adaptor.getDocIds(pusher);
+
+    // We should just push the root docid.
+    List<Record> records = pusher.getRecords();
+    assertEquals(1, records.size());
+    assertEquals(delegate.newDocId(rootPath), records.get(0).getDocId());
+
+    // We should have pushed an empty, child-overrides ACL for the share.
+    List<Map<DocId, Acl>> namedResources = pusher.getNamedResources();
+    Acl expected = new Acl.Builder().setEverythingCaseInsensitive()
+        .setInheritanceType(InheritanceType.CHILD_OVERRIDES).build();
+    assertEquals(1, namedResources.size());
+    assertEquals(1, namedResources.get(0).size());
+    assertEquals(expected, namedResources.get(0).get(shareAclDocId));
+  }
+
+  @Test
   public void testGetDocIdsBrokenDfs() throws Exception {
     Path uncPath = Paths.get("\\\\dfshost\\share");
     root.setDfsUncActiveStorageUnc(uncPath);