hadoop-hdfs-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From cnaur...@apache.org
Subject svn commit: r1556090 - in /hadoop/common/branches/HDFS-4685/hadoop-hdfs-project/hadoop-hdfs: ./ src/main/java/org/apache/hadoop/hdfs/protocol/ src/main/java/org/apache/hadoop/hdfs/server/namenode/ src/test/java/org/apache/hadoop/hdfs/server/namenode/
Date Tue, 07 Jan 2014 00:47:49 GMT
Author: cnauroth
Date: Tue Jan  7 00:47:49 2014
New Revision: 1556090

URL: http://svn.apache.org/r1556090
Log:
HDFS-5673. Implement logic for modification of ACLs. Contributed by Chris Nauroth.

Added:
    hadoop/common/branches/HDFS-4685/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/protocol/AclException.java
    hadoop/common/branches/HDFS-4685/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/AclTransformation.java
    hadoop/common/branches/HDFS-4685/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/TestAclTransformation.java
Modified:
    hadoop/common/branches/HDFS-4685/hadoop-hdfs-project/hadoop-hdfs/CHANGES-HDFS-4685.txt

Modified: hadoop/common/branches/HDFS-4685/hadoop-hdfs-project/hadoop-hdfs/CHANGES-HDFS-4685.txt
URL: http://svn.apache.org/viewvc/hadoop/common/branches/HDFS-4685/hadoop-hdfs-project/hadoop-hdfs/CHANGES-HDFS-4685.txt?rev=1556090&r1=1556089&r2=1556090&view=diff
==============================================================================
--- hadoop/common/branches/HDFS-4685/hadoop-hdfs-project/hadoop-hdfs/CHANGES-HDFS-4685.txt (original)
+++ hadoop/common/branches/HDFS-4685/hadoop-hdfs-project/hadoop-hdfs/CHANGES-HDFS-4685.txt Tue Jan  7 00:47:49 2014
@@ -26,6 +26,8 @@ HDFS-4685 (Unreleased)
 
     HADOOP-10192. FileSystem#getAclStatus has incorrect JavaDocs. (cnauroth)
 
+    HDFS-5673. Implement logic for modification of ACLs. (cnauroth)
+
   OPTIMIZATIONS
 
   BUG FIXES

Added: hadoop/common/branches/HDFS-4685/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/protocol/AclException.java
URL: http://svn.apache.org/viewvc/hadoop/common/branches/HDFS-4685/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/protocol/AclException.java?rev=1556090&view=auto
==============================================================================
--- hadoop/common/branches/HDFS-4685/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/protocol/AclException.java (added)
+++ hadoop/common/branches/HDFS-4685/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/protocol/AclException.java Tue Jan  7 00:47:49 2014
@@ -0,0 +1,39 @@
+/**
+ * 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.hadoop.hdfs.protocol;
+
+import java.io.IOException;
+
+import org.apache.hadoop.classification.InterfaceAudience;
+
+/**
+ * Indicates a failure manipulating an ACL.
+ */
+@InterfaceAudience.Private
+public class AclException extends IOException {
+  private static final long serialVersionUID = 1L;
+
+  /**
+   * Creates a new AclException.
+   *
+   * @param message String message
+   */
+  public AclException(String message) {
+    super(message);
+  }
+}

Added: hadoop/common/branches/HDFS-4685/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/AclTransformation.java
URL: http://svn.apache.org/viewvc/hadoop/common/branches/HDFS-4685/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/AclTransformation.java?rev=1556090&view=auto
==============================================================================
--- hadoop/common/branches/HDFS-4685/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/AclTransformation.java (added)
+++ hadoop/common/branches/HDFS-4685/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/AclTransformation.java Tue Jan  7 00:47:49 2014
@@ -0,0 +1,480 @@
+/**
+ * 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.hadoop.hdfs.server.namenode;
+
+import static org.apache.hadoop.fs.permission.AclEntryScope.*;
+import static org.apache.hadoop.fs.permission.AclEntryType.*;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.EnumMap;
+import java.util.EnumSet;
+import java.util.Iterator;
+import java.util.List;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Ordering;
+
+import org.apache.hadoop.classification.InterfaceAudience;
+import org.apache.hadoop.fs.permission.AclEntry;
+import org.apache.hadoop.fs.permission.AclEntryScope;
+import org.apache.hadoop.fs.permission.AclEntryType;
+import org.apache.hadoop.fs.permission.FsAction;
+import org.apache.hadoop.fs.permission.FsPermission;
+import org.apache.hadoop.hdfs.protocol.AclException;
+
+/**
+ * AclTransformation defines the operations that can modify an ACL.  All ACL
+ * modifications take as input an existing ACL and apply logic to add new
+ * entries, modify existing entries or remove old entries.  Some operations also
+ * accept an ACL spec: a list of entries that further describes the requested
+ * change.  Different operations interpret the ACL spec differently.  In the
+ * case of adding an ACL to an inode that previously did not have one, the
+ * existing ACL can be a "minimal ACL" containing exactly 3 entries for owner,
+ * group and other, all derived from the {@link FsPermission} bits.
+ *
+ * The algorithms implemented here require sorted lists of ACL entries.  For any
+ * existing ACL, it is assumed that the entries are sorted.  This is because all
+ * ACL creation and modification is intended to go through these methods, and
+ * they all guarantee correct sort order in their outputs.  However, an ACL spec
+ * is considered untrusted user input, so all operations pre-sort the ACL spec as
+ * the first step.
+ */
+@InterfaceAudience.Private
+final class AclTransformation {
+  private static final int MAX_ENTRIES = 32;
+
+  /**
+   * Filters (discards) any existing ACL entries that have the same scope, type
+   * and name of any entry in the ACL spec.  If necessary, recalculates the mask
+   * entries.  If necessary, default entries may be inferred by copying the
+   * permissions of the corresponding access entries.  It is invalid to request
+   * removal of the mask entry from an ACL that would otherwise require a mask
+   * entry, due to existing named entries or an unnamed group entry.
+   *
+   * @param existingAcl List<AclEntry> existing ACL
+   * @param inAclSpec List<AclEntry> ACL spec describing entries to filter
+   * @return List<AclEntry> new ACL
+   * @throws AclException if validation fails
+   */
+  public static List<AclEntry> filterAclEntriesByAclSpec(
+      List<AclEntry> existingAcl, List<AclEntry> inAclSpec) throws AclException {
+    ValidatedAclSpec aclSpec = new ValidatedAclSpec(inAclSpec);
+    ArrayList<AclEntry> aclBuilder = Lists.newArrayListWithCapacity(MAX_ENTRIES);
+    EnumMap<AclEntryScope, AclEntry> providedMask =
+      Maps.newEnumMap(AclEntryScope.class);
+    EnumSet<AclEntryScope> maskDirty = EnumSet.noneOf(AclEntryScope.class);
+    EnumSet<AclEntryScope> scopeDirty = EnumSet.noneOf(AclEntryScope.class);
+    for (AclEntry existingEntry: existingAcl) {
+      if (aclSpec.containsKey(existingEntry)) {
+        scopeDirty.add(existingEntry.getScope());
+        if (existingEntry.getType() == MASK) {
+          maskDirty.add(existingEntry.getScope());
+        }
+      } else {
+        if (existingEntry.getType() == MASK) {
+          providedMask.put(existingEntry.getScope(), existingEntry);
+        } else {
+          aclBuilder.add(existingEntry);
+        }
+      }
+    }
+    copyDefaultsIfNeeded(aclBuilder);
+    calculateMasks(aclBuilder, providedMask, maskDirty, scopeDirty);
+    return buildAndValidateAcl(aclBuilder);
+  }
+
+  /**
+   * Filters (discards) any existing default ACL entries.  The new ACL retains
+   * only the access ACL entries.
+   *
+   * @param existingAcl List<AclEntry> existing ACL
+   * @return List<AclEntry> new ACL
+   * @throws AclException if validation fails
+   */
+  public static List<AclEntry> filterDefaultAclEntries(
+      List<AclEntry> existingAcl) throws AclException {
+    ArrayList<AclEntry> aclBuilder = Lists.newArrayListWithCapacity(MAX_ENTRIES);
+    for (AclEntry existingEntry: existingAcl) {
+      if (existingEntry.getScope() == DEFAULT) {
+        // Default entries sort after access entries, so we can exit early.
+        break;
+      }
+      aclBuilder.add(existingEntry);
+    }
+    return buildAndValidateAcl(aclBuilder);
+  }
+
+  /**
+   * Merges the entries of the ACL spec into the existing ACL.  If necessary,
+   * recalculates the mask entries.  If necessary, default entries may be
+   * inferred by copying the permissions of the corresponding access entries.
+   *
+   * @param existingAcl List<AclEntry> existing ACL
+   * @param inAclSpec List<AclEntry> ACL spec containing entries to merge
+   * @return List<AclEntry> new ACL
+   * @throws AclException if validation fails
+   */
+  public static List<AclEntry> mergeAclEntries(List<AclEntry> existingAcl,
+      List<AclEntry> inAclSpec) throws AclException {
+    ValidatedAclSpec aclSpec = new ValidatedAclSpec(inAclSpec);
+    ArrayList<AclEntry> aclBuilder = Lists.newArrayListWithCapacity(MAX_ENTRIES);
+    List<AclEntry> foundAclSpecEntries =
+      Lists.newArrayListWithCapacity(MAX_ENTRIES);
+    EnumMap<AclEntryScope, AclEntry> providedMask =
+      Maps.newEnumMap(AclEntryScope.class);
+    EnumSet<AclEntryScope> maskDirty = EnumSet.noneOf(AclEntryScope.class);
+    EnumSet<AclEntryScope> scopeDirty = EnumSet.noneOf(AclEntryScope.class);
+    for (AclEntry existingEntry: existingAcl) {
+      AclEntry aclSpecEntry = aclSpec.findByKey(existingEntry);
+      if (aclSpecEntry != null) {
+        foundAclSpecEntries.add(aclSpecEntry);
+        scopeDirty.add(aclSpecEntry.getScope());
+        if (aclSpecEntry.getType() == MASK) {
+          providedMask.put(aclSpecEntry.getScope(), aclSpecEntry);
+          maskDirty.add(aclSpecEntry.getScope());
+        } else {
+          aclBuilder.add(aclSpecEntry);
+        }
+      } else {
+        if (existingEntry.getType() == MASK) {
+          providedMask.put(existingEntry.getScope(), existingEntry);
+        } else {
+          aclBuilder.add(existingEntry);
+        }
+      }
+    }
+    // ACL spec entries that were not replacements are new additions.
+    for (AclEntry newEntry: aclSpec) {
+      if (Collections.binarySearch(foundAclSpecEntries, newEntry,
+          ACL_ENTRY_COMPARATOR) < 0) {
+        scopeDirty.add(newEntry.getScope());
+        if (newEntry.getType() == MASK) {
+          providedMask.put(newEntry.getScope(), newEntry);
+          maskDirty.add(newEntry.getScope());
+        } else {
+          aclBuilder.add(newEntry);
+        }
+      }
+    }
+    copyDefaultsIfNeeded(aclBuilder);
+    calculateMasks(aclBuilder, providedMask, maskDirty, scopeDirty);
+    return buildAndValidateAcl(aclBuilder);
+  }
+
+  /**
+   * Completely replaces the ACL with the entries of the ACL spec.  If
+   * necessary, recalculates the mask entries.  If necessary, default entries
+   * are inferred by copying the permissions of the corresponding access
+   * entries.  Replacement occurs separately for each of the access ACL and the
+   * default ACL.  If the ACL spec contains only access entries, then the
+   * existing default entries are retained.  If the ACL spec contains only
+   * default entries, then the existing access entries are retained.  If the ACL
+   * spec contains both access and default entries, then both are replaced.
+   *
+   * @param existingAcl List<AclEntry> existing ACL
+   * @param inAclSpec List<AclEntry> ACL spec containing replacement entries
+   * @return List<AclEntry> new ACL
+   * @throws AclException if validation fails
+   */
+  public static List<AclEntry> replaceAclEntries(List<AclEntry> existingAcl,
+      List<AclEntry> inAclSpec) throws AclException {
+    ValidatedAclSpec aclSpec = new ValidatedAclSpec(inAclSpec);
+    ArrayList<AclEntry> aclBuilder = Lists.newArrayListWithCapacity(MAX_ENTRIES);
+    // Replacement is done separately for each scope: access and default.
+    EnumMap<AclEntryScope, AclEntry> providedMask =
+      Maps.newEnumMap(AclEntryScope.class);
+    EnumSet<AclEntryScope> maskDirty = EnumSet.noneOf(AclEntryScope.class);
+    EnumSet<AclEntryScope> scopeDirty = EnumSet.noneOf(AclEntryScope.class);
+    for (AclEntry aclSpecEntry: aclSpec) {
+      scopeDirty.add(aclSpecEntry.getScope());
+      if (aclSpecEntry.getType() == MASK) {
+        providedMask.put(aclSpecEntry.getScope(), aclSpecEntry);
+        maskDirty.add(aclSpecEntry.getScope());
+      } else {
+        aclBuilder.add(aclSpecEntry);
+      }
+    }
+    // Copy existing entries if the scope was not replaced.
+    for (AclEntry existingEntry: existingAcl) {
+      if (!scopeDirty.contains(existingEntry.getScope())) {
+        if (existingEntry.getType() == MASK) {
+          providedMask.put(existingEntry.getScope(), existingEntry);
+        } else {
+          aclBuilder.add(existingEntry);
+        }
+      }
+    }
+    copyDefaultsIfNeeded(aclBuilder);
+    calculateMasks(aclBuilder, providedMask, maskDirty, scopeDirty);
+    return buildAndValidateAcl(aclBuilder);
+  }
+
+  /**
+   * There is no reason to instantiate this class.
+   */
+  private AclTransformation() {
+  }
+
+  /**
+   * Comparator that enforces required ordering for entries within an ACL:
+   * -owner entry (unnamed user)
+   * -all named user entries (internal ordering undefined)
+   * -owning group entry (unnamed group)
+   * -all named group entries (internal ordering undefined)
+   * -mask entry
+   * -other entry
+   * All access ACL entries sort ahead of all default ACL entries.
+   */
+  private static final Comparator<AclEntry> ACL_ENTRY_COMPARATOR =
+    new Comparator<AclEntry>() {
+      @Override
+      public int compare(AclEntry entry1, AclEntry entry2) {
+        return ComparisonChain.start()
+          .compare(entry1.getScope(), entry2.getScope(),
+            Ordering.explicit(ACCESS, DEFAULT))
+          .compare(entry1.getType(), entry2.getType(),
+            Ordering.explicit(USER, GROUP, MASK, OTHER))
+          .compare(entry1.getName(), entry2.getName(),
+            Ordering.natural().nullsFirst())
+          .result();
+      }
+    };
+
+  /**
+   * Builds the final list of ACL entries to return by trimming, sorting and
+   * validating the ACL entries that have been added.
+   *
+   * @param aclBuilder ArrayList<AclEntry> containing entries to build
+   * @return List<AclEntry> unmodifiable, sorted list of ACL entries
+   * @throws AclException if validation fails
+   */
+  private static List<AclEntry> buildAndValidateAcl(
+      ArrayList<AclEntry> aclBuilder) throws AclException {
+    if (aclBuilder.size() > MAX_ENTRIES) {
+      throw new AclException("Invalid ACL: ACL has " + aclBuilder.size() +
+        " entries, which exceeds maximum of " + MAX_ENTRIES + ".");
+    }
+    aclBuilder.trimToSize();
+    Collections.sort(aclBuilder, ACL_ENTRY_COMPARATOR);
+    AclEntry userEntry = null, groupEntry = null, otherEntry = null;
+    AclEntry prevEntry = null;
+    for (AclEntry entry: aclBuilder) {
+      if (prevEntry != null &&
+          ACL_ENTRY_COMPARATOR.compare(prevEntry, entry) == 0) {
+        throw new AclException(
+          "Invalid ACL: multiple entries with same scope, type and name.");
+      }
+      if (entry.getName() != null && (entry.getType() == MASK ||
+          entry.getType() == OTHER)) {
+        throw new AclException(
+          "Invalid ACL: this entry type must not have a name: " + entry + ".");
+      }
+      if (entry.getScope() == ACCESS) {
+        if (entry.getType() == USER && entry.getName() == null) {
+          userEntry = entry;
+        }
+        if (entry.getType() == GROUP && entry.getName() == null) {
+          groupEntry = entry;
+        }
+        if (entry.getType() == OTHER && entry.getName() == null) {
+          otherEntry = entry;
+        }
+      }
+      prevEntry = entry;
+    }
+    if (userEntry == null || groupEntry == null || otherEntry == null) {
+      throw new AclException(
+        "Invalid ACL: the user, group and other entries are required.");
+    }
+    return Collections.unmodifiableList(aclBuilder);
+  }
+
+  /**
+   * Calculates mask entries required for the ACL.  Mask calculation is performed
+   * separately for each scope: access and default.  This method is responsible
+   * for handling the following cases of mask calculation:
+   * 1. Throws an exception if the caller attempts to remove the mask entry of an
+   *   existing ACL that requires it.  If the ACL has any named entries, then a
+   *   mask entry is required.
+   * 2. If the caller supplied a mask in the ACL spec, use it.
+   * 3. If the caller did not supply a mask, but there are ACL entry changes in
+   *   this scope, then automatically calculate a new mask.  The permissions of
+   *   the new mask are the union of the permissions on the group entry and all
+   *   named entries.
+   *
+   * @param aclBuilder ArrayList<AclEntry> containing entries to build
+   * @param providedMask EnumMap<AclEntryScope, AclEntry> mapping each scope to
+   *   the mask entry that was provided for that scope (if provided)
+   * @param maskDirty EnumSet<AclEntryScope> which contains a scope if the mask
+   *   entry is dirty (added or deleted) in that scope
+   * @param scopeDirty EnumSet<AclEntryScope> which contains a scope if any entry
+   *   is dirty (added or deleted) in that scope
+   * @throws AclException if validation fails
+   */
+  private static void calculateMasks(List<AclEntry> aclBuilder,
+      EnumMap<AclEntryScope, AclEntry> providedMask,
+      EnumSet<AclEntryScope> maskDirty, EnumSet<AclEntryScope> scopeDirty)
+      throws AclException {
+    EnumSet<AclEntryScope> scopeFound = EnumSet.noneOf(AclEntryScope.class);
+    EnumMap<AclEntryScope, FsAction> unionPerms =
+      Maps.newEnumMap(AclEntryScope.class);
+    EnumSet<AclEntryScope> maskNeeded = EnumSet.noneOf(AclEntryScope.class);
+    // Determine which scopes are present, which scopes need a mask, and the
+    // union of group class permissions in each scope.
+    for (AclEntry entry: aclBuilder) {
+      scopeFound.add(entry.getScope());
+      if (entry.getType() == GROUP || entry.getName() != null) {
+        FsAction scopeUnionPerms = Objects.firstNonNull(
+          unionPerms.get(entry.getScope()), FsAction.NONE);
+        unionPerms.put(entry.getScope(),
+          scopeUnionPerms.or(entry.getPermission()));
+      }
+      if (entry.getName() != null) {
+        maskNeeded.add(entry.getScope());
+      }
+    }
+    // Add mask entry if needed in each scope.
+    for (AclEntryScope scope: scopeFound) {
+      if (!providedMask.containsKey(scope) && maskNeeded.contains(scope) &&
+          maskDirty.contains(scope)) {
+        throw new AclException(
+          "Invalid ACL: mask is required, but it was deleted.");
+      } else if (providedMask.containsKey(scope) &&
+          (!scopeDirty.contains(scope) || maskDirty.contains(scope))) {
+        aclBuilder.add(providedMask.get(scope));
+      } else if (maskNeeded.contains(scope)) {
+        aclBuilder.add(new AclEntry.Builder()
+          .setScope(scope)
+          .setType(MASK)
+          .setPermission(unionPerms.get(scope))
+          .build());
+      }
+    }
+  }
+
+  /**
+   * Adds unspecified default entries by copying permissions from the
+   * corresponding access entries.
+   *
+   * @param aclBuilder ArrayList<AclEntry> containing entries to build
+   */
+  private static void copyDefaultsIfNeeded(List<AclEntry> aclBuilder) {
+    int pivot = -1;
+    for (int i = 0; i < aclBuilder.size(); ++i) {
+      if (aclBuilder.get(i).getScope() == DEFAULT) {
+        pivot = i;
+        break;
+      }
+    }
+    if (pivot > -1) {
+      List<AclEntry> accessEntries = aclBuilder.subList(0, pivot);
+      List<AclEntry> defaultEntries = aclBuilder.subList(pivot,
+        aclBuilder.size());
+      List<AclEntry> copiedEntries = Lists.newArrayListWithCapacity(3);
+      for (AclEntryType type: EnumSet.of(USER, GROUP, OTHER)) {
+        AclEntry defaultEntryKey = new AclEntry.Builder().setScope(DEFAULT)
+          .setType(type).build();
+        int defaultEntryIndex = Collections.binarySearch(defaultEntries,
+          defaultEntryKey, ACL_ENTRY_COMPARATOR);
+        if (defaultEntryIndex < 0) {
+          AclEntry accessEntryKey = new AclEntry.Builder().setScope(ACCESS)
+            .setType(type).build();
+          int accessEntryIndex = Collections.binarySearch(accessEntries,
+            accessEntryKey, ACL_ENTRY_COMPARATOR);
+          if (accessEntryIndex >= 0) {
+            copiedEntries.add(new AclEntry.Builder()
+              .setScope(DEFAULT)
+              .setType(type)
+              .setPermission(accessEntries.get(accessEntryIndex).getPermission())
+              .build());
+          }
+        }
+      }
+      // Add all copied entries when done to prevent potential issues with binary
+      // search on a modified aclBulider during the main loop.
+      aclBuilder.addAll(copiedEntries);
+    }
+  }
+
+  /**
+   * An ACL spec that has been pre-validated and sorted.
+   */
+  private static final class ValidatedAclSpec implements Iterable<AclEntry> {
+    private final List<AclEntry> aclSpec;
+
+    /**
+     * Creates a ValidatedAclSpec by pre-validating and sorting the given ACL
+     * entries.  Pre-validation checks that it does not exceed the maximum
+     * entries.  This check is performed before modifying the ACL, and it's
+     * actually insufficient for enforcing the maximum number of entries.
+     * Transformation logic can create additional entries automatically,such as
+     * the mask and some of the default entries, so we also need additional
+     * checks during transformation.  The up-front check is still valuable here
+     * so that we don't run a lot of expensive transformation logic while
+     * holding the namesystem lock for an attacker who intentionally sent a huge
+     * ACL spec.
+     *
+     * @param aclSpec List<AclEntry> containing unvalidated input ACL spec
+     * @throws AclException if validation fails
+     */
+    public ValidatedAclSpec(List<AclEntry> aclSpec) throws AclException {
+      if (aclSpec.size() > MAX_ENTRIES) {
+        throw new AclException("Invalid ACL: ACL spec has " + aclSpec.size() +
+          " entries, which exceeds maximum of " + MAX_ENTRIES + ".");
+      }
+      Collections.sort(aclSpec, ACL_ENTRY_COMPARATOR);
+      this.aclSpec = aclSpec;
+    }
+
+    /**
+     * Returns true if this contains an entry matching the given key.  An ACL
+     * entry's key consists of scope, type and name (but not permission).
+     *
+     * @param key AclEntry search key
+     * @return boolean true if found
+     */
+    public boolean containsKey(AclEntry key) {
+      return Collections.binarySearch(aclSpec, key, ACL_ENTRY_COMPARATOR) >= 0;
+    }
+
+    /**
+     * Returns the entry matching the given key or null if not found.  An ACL
+     * entry's key consists of scope, type and name (but not permission).
+     *
+     * @param key AclEntry search key
+     * @return AclEntry entry matching the given key or null if not found
+     */
+    public AclEntry findByKey(AclEntry key) {
+      int index = Collections.binarySearch(aclSpec, key, ACL_ENTRY_COMPARATOR);
+      if (index >= 0) {
+        return aclSpec.get(index);
+      }
+      return null;
+    }
+
+    @Override
+    public Iterator<AclEntry> iterator() {
+      return aclSpec.iterator();
+    }
+  }
+}

Added: hadoop/common/branches/HDFS-4685/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/TestAclTransformation.java
URL: http://svn.apache.org/viewvc/hadoop/common/branches/HDFS-4685/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/TestAclTransformation.java?rev=1556090&view=auto
==============================================================================
--- hadoop/common/branches/HDFS-4685/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/TestAclTransformation.java (added)
+++ hadoop/common/branches/HDFS-4685/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/TestAclTransformation.java Tue Jan  7 00:47:49 2014
@@ -0,0 +1,1220 @@
+/**
+ * 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.hadoop.hdfs.server.namenode;
+
+import static org.apache.hadoop.fs.permission.AclEntryScope.*;
+import static org.apache.hadoop.fs.permission.AclEntryType.*;
+import static org.apache.hadoop.fs.permission.FsAction.*;
+import static org.apache.hadoop.hdfs.server.namenode.AclTransformation.*;
+import static org.junit.Assert.*;
+
+import java.util.List;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import org.junit.Test;
+
+import org.apache.hadoop.fs.permission.AclEntry;
+import org.apache.hadoop.fs.permission.AclEntryScope;
+import org.apache.hadoop.fs.permission.AclEntryType;
+import org.apache.hadoop.fs.permission.FsAction;
+import org.apache.hadoop.hdfs.protocol.AclException;
+import org.apache.hadoop.hdfs.server.namenode.AclTransformation;
+
+/**
+ * Tests operations that modify ACLs.  All tests in this suite have been
+ * cross-validated against Linux setfacl/getfacl to check for consistency of the
+ * HDFS implementation.
+ */
+public class TestAclTransformation {
+
+  private static final List<AclEntry> ACL_SPEC_TOO_LARGE;
+  static {
+    ACL_SPEC_TOO_LARGE = Lists.newArrayListWithCapacity(33);
+    for (int i = 0; i < 33; ++i) {
+      ACL_SPEC_TOO_LARGE.add(aclEntry(ACCESS, USER, "user" + i, ALL));
+    }
+  }
+
+  @Test
+  public void testFilterAclEntriesByAclSpec() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "bruce", READ_WRITE))
+      .add(aclEntry(ACCESS, USER, "diana", READ_EXECUTE))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, GROUP, "sales", READ_EXECUTE))
+      .add(aclEntry(ACCESS, GROUP, "execs", READ_WRITE))
+      .add(aclEntry(ACCESS, MASK, ALL))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(ACCESS, USER, "diana"),
+      aclEntry(ACCESS, GROUP, "sales"));
+    List<AclEntry> expected = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "bruce", READ_WRITE))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, GROUP, "execs", READ_WRITE))
+      .add(aclEntry(ACCESS, MASK, READ_WRITE))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .build();
+    assertEquals(expected, filterAclEntriesByAclSpec(existing, aclSpec));
+  }
+
+  @Test
+  public void testFilterAclEntriesByAclSpecUnchanged() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "bruce", ALL))
+      .add(aclEntry(ACCESS, GROUP, READ_EXECUTE))
+      .add(aclEntry(ACCESS, GROUP, "sales", ALL))
+      .add(aclEntry(ACCESS, MASK, ALL))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(ACCESS, USER, "clark"),
+      aclEntry(ACCESS, GROUP, "execs"));
+    assertEquals(existing, filterAclEntriesByAclSpec(existing, aclSpec));
+  }
+
+  @Test
+  public void testFilterAclEntriesByAclSpecAccessMaskCalculated()
+      throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "bruce", READ))
+      .add(aclEntry(ACCESS, USER, "diana", READ_WRITE))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, MASK, READ_WRITE))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(ACCESS, USER, "diana"));
+    List<AclEntry> expected = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "bruce", READ))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, MASK, READ))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .build();
+    assertEquals(expected, filterAclEntriesByAclSpec(existing, aclSpec));
+  }
+
+  @Test
+  public void testFilterAclEntriesByAclSpecDefaultMaskCalculated()
+      throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .add(aclEntry(DEFAULT, USER, ALL))
+      .add(aclEntry(DEFAULT, USER, "bruce", READ))
+      .add(aclEntry(DEFAULT, USER, "diana", READ_WRITE))
+      .add(aclEntry(DEFAULT, GROUP, READ))
+      .add(aclEntry(DEFAULT, MASK, READ_WRITE))
+      .add(aclEntry(DEFAULT, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(DEFAULT, USER, "diana"));
+    List<AclEntry> expected = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .add(aclEntry(DEFAULT, USER, ALL))
+      .add(aclEntry(DEFAULT, USER, "bruce", READ))
+      .add(aclEntry(DEFAULT, GROUP, READ))
+      .add(aclEntry(DEFAULT, MASK, READ))
+      .add(aclEntry(DEFAULT, OTHER, NONE))
+      .build();
+    assertEquals(expected, filterAclEntriesByAclSpec(existing, aclSpec));
+  }
+
+  @Test
+  public void testFilterAclEntriesByAclSpecDefaultMaskPreserved()
+      throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "bruce", READ))
+      .add(aclEntry(ACCESS, USER, "diana", READ_WRITE))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, MASK, READ_WRITE))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .add(aclEntry(DEFAULT, USER, ALL))
+      .add(aclEntry(DEFAULT, USER, "diana", ALL))
+      .add(aclEntry(DEFAULT, GROUP, READ))
+      .add(aclEntry(DEFAULT, MASK, READ))
+      .add(aclEntry(DEFAULT, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(ACCESS, USER, "diana"));
+    List<AclEntry> expected = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "bruce", READ))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, MASK, READ))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .add(aclEntry(DEFAULT, USER, ALL))
+      .add(aclEntry(DEFAULT, USER, "diana", ALL))
+      .add(aclEntry(DEFAULT, GROUP, READ))
+      .add(aclEntry(DEFAULT, MASK, READ))
+      .add(aclEntry(DEFAULT, OTHER, NONE))
+      .build();
+    assertEquals(expected, filterAclEntriesByAclSpec(existing, aclSpec));
+  }
+
+  @Test
+  public void testFilterAclEntriesByAclSpecAccessMaskPreserved()
+      throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "bruce", READ))
+      .add(aclEntry(ACCESS, USER, "diana", READ_WRITE))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, MASK, READ))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .add(aclEntry(DEFAULT, USER, ALL))
+      .add(aclEntry(DEFAULT, USER, "bruce", READ))
+      .add(aclEntry(DEFAULT, USER, "diana", READ_WRITE))
+      .add(aclEntry(DEFAULT, GROUP, READ))
+      .add(aclEntry(DEFAULT, MASK, READ_WRITE))
+      .add(aclEntry(DEFAULT, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(DEFAULT, USER, "diana"));
+    List<AclEntry> expected = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "bruce", READ))
+      .add(aclEntry(ACCESS, USER, "diana", READ_WRITE))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, MASK, READ))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .add(aclEntry(DEFAULT, USER, ALL))
+      .add(aclEntry(DEFAULT, USER, "bruce", READ))
+      .add(aclEntry(DEFAULT, GROUP, READ))
+      .add(aclEntry(DEFAULT, MASK, READ))
+      .add(aclEntry(DEFAULT, OTHER, NONE))
+      .build();
+    assertEquals(expected, filterAclEntriesByAclSpec(existing, aclSpec));
+  }
+
+  @Test
+  public void testFilterAclEntriesByAclSpecAutomaticDefaultUser()
+      throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .add(aclEntry(DEFAULT, USER, READ_WRITE))
+      .add(aclEntry(DEFAULT, USER, "bruce", READ))
+      .add(aclEntry(DEFAULT, GROUP, READ))
+      .add(aclEntry(DEFAULT, MASK, READ))
+      .add(aclEntry(DEFAULT, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(DEFAULT, USER));
+    List<AclEntry> expected = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .add(aclEntry(DEFAULT, USER, ALL))
+      .add(aclEntry(DEFAULT, USER, "bruce", READ))
+      .add(aclEntry(DEFAULT, GROUP, READ))
+      .add(aclEntry(DEFAULT, MASK, READ))
+      .add(aclEntry(DEFAULT, OTHER, NONE))
+      .build();
+    assertEquals(expected, filterAclEntriesByAclSpec(existing, aclSpec));
+  }
+
+  @Test
+  public void testFilterAclEntriesByAclSpecAutomaticDefaultGroup()
+      throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .add(aclEntry(DEFAULT, USER, READ_WRITE))
+      .add(aclEntry(DEFAULT, GROUP, READ_WRITE))
+      .add(aclEntry(DEFAULT, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(DEFAULT, GROUP));
+    List<AclEntry> expected = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .add(aclEntry(DEFAULT, USER, READ_WRITE))
+      .add(aclEntry(DEFAULT, GROUP, READ))
+      .add(aclEntry(DEFAULT, OTHER, NONE))
+      .build();
+    assertEquals(expected, filterAclEntriesByAclSpec(existing, aclSpec));
+  }
+
+  @Test
+  public void testFilterAclEntriesByAclSpecAutomaticDefaultOther()
+      throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .add(aclEntry(DEFAULT, USER, READ_WRITE))
+      .add(aclEntry(DEFAULT, GROUP, READ_WRITE))
+      .add(aclEntry(DEFAULT, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(DEFAULT, OTHER));
+    List<AclEntry> expected = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .add(aclEntry(DEFAULT, USER, READ_WRITE))
+      .add(aclEntry(DEFAULT, GROUP, READ_WRITE))
+      .add(aclEntry(DEFAULT, OTHER, READ))
+      .build();
+    assertEquals(expected, filterAclEntriesByAclSpec(existing, aclSpec));
+  }
+
+  @Test
+  public void testFilterAclEntriesByAclSpecEmptyAclSpec() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "bruce", READ_WRITE))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, MASK, ALL))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .add(aclEntry(DEFAULT, USER, ALL))
+      .add(aclEntry(DEFAULT, USER, "bruce", READ_WRITE))
+      .add(aclEntry(DEFAULT, GROUP, READ))
+      .add(aclEntry(DEFAULT, MASK, ALL))
+      .add(aclEntry(DEFAULT, OTHER, READ))
+      .build();
+    List<AclEntry> aclSpec = Lists.<AclEntry>newArrayList();
+    assertEquals(existing, filterAclEntriesByAclSpec(existing, aclSpec));
+  }
+
+  @Test(expected=AclException.class)
+  public void testFilterAclEntriesByAclSpecRemoveAccessMaskRequired()
+      throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "bruce", READ))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, MASK, ALL))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(ACCESS, MASK));
+    filterAclEntriesByAclSpec(existing, aclSpec);
+  }
+
+  @Test(expected=AclException.class)
+  public void testFilterAclEntriesByAclSpecRemoveDefaultMaskRequired()
+      throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .add(aclEntry(DEFAULT, USER, ALL))
+      .add(aclEntry(DEFAULT, USER, "bruce", READ))
+      .add(aclEntry(DEFAULT, GROUP, READ))
+      .add(aclEntry(DEFAULT, MASK, ALL))
+      .add(aclEntry(DEFAULT, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(DEFAULT, MASK));
+    filterAclEntriesByAclSpec(existing, aclSpec);
+  }
+
+  @Test(expected=AclException.class)
+  public void testFilterAclEntriesByAclSpecInputTooLarge() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .build();
+    filterAclEntriesByAclSpec(existing, ACL_SPEC_TOO_LARGE);
+  }
+
+  @Test
+  public void testFilterDefaultAclEntries() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "bruce", READ_WRITE))
+      .add(aclEntry(ACCESS, GROUP, READ_EXECUTE))
+      .add(aclEntry(ACCESS, GROUP, "sales", READ_EXECUTE))
+      .add(aclEntry(ACCESS, MASK, ALL))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .add(aclEntry(DEFAULT, USER, ALL))
+      .add(aclEntry(DEFAULT, USER, "bruce", READ_WRITE))
+      .add(aclEntry(DEFAULT, GROUP, READ))
+      .add(aclEntry(DEFAULT, GROUP, "sales", READ_EXECUTE))
+      .add(aclEntry(DEFAULT, MASK, READ_WRITE))
+      .add(aclEntry(DEFAULT, OTHER, READ_EXECUTE))
+      .build();
+    List<AclEntry> expected = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "bruce", READ_WRITE))
+      .add(aclEntry(ACCESS, GROUP, READ_EXECUTE))
+      .add(aclEntry(ACCESS, GROUP, "sales", READ_EXECUTE))
+      .add(aclEntry(ACCESS, MASK, ALL))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .build();
+    assertEquals(expected, filterDefaultAclEntries(existing));
+  }
+
+  @Test
+  public void testFilterDefaultAclEntriesUnchanged() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "bruce", ALL))
+      .add(aclEntry(ACCESS, GROUP, READ_EXECUTE))
+      .add(aclEntry(ACCESS, GROUP, "sales", ALL))
+      .add(aclEntry(ACCESS, MASK, ALL))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .build();
+    assertEquals(existing, filterDefaultAclEntries(existing));
+  }
+
+  @Test
+  public void testMergeAclEntries() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ_EXECUTE))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(ACCESS, USER, "bruce", ALL));
+    List<AclEntry> expected = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "bruce", ALL))
+      .add(aclEntry(ACCESS, GROUP, READ_EXECUTE))
+      .add(aclEntry(ACCESS, MASK, ALL))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .build();
+    assertEquals(expected, mergeAclEntries(existing, aclSpec));
+  }
+
+  @Test
+  public void testMergeAclEntriesUnchanged() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "bruce", ALL))
+      .add(aclEntry(ACCESS, GROUP, READ_EXECUTE))
+      .add(aclEntry(ACCESS, GROUP, "sales", ALL))
+      .add(aclEntry(ACCESS, MASK, ALL))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .add(aclEntry(DEFAULT, USER, ALL))
+      .add(aclEntry(DEFAULT, USER, "bruce", ALL))
+      .add(aclEntry(DEFAULT, GROUP, READ_EXECUTE))
+      .add(aclEntry(DEFAULT, GROUP, "sales", ALL))
+      .add(aclEntry(DEFAULT, MASK, ALL))
+      .add(aclEntry(DEFAULT, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(ACCESS, USER, ALL),
+      aclEntry(ACCESS, USER, "bruce", ALL),
+      aclEntry(ACCESS, GROUP, READ_EXECUTE),
+      aclEntry(ACCESS, GROUP, "sales", ALL),
+      aclEntry(ACCESS, MASK, ALL),
+      aclEntry(ACCESS, OTHER, NONE),
+      aclEntry(DEFAULT, USER, ALL),
+      aclEntry(DEFAULT, USER, "bruce", ALL),
+      aclEntry(DEFAULT, GROUP, READ_EXECUTE),
+      aclEntry(DEFAULT, GROUP, "sales", ALL),
+      aclEntry(DEFAULT, MASK, ALL),
+      aclEntry(DEFAULT, OTHER, NONE));
+    assertEquals(existing, mergeAclEntries(existing, aclSpec));
+  }
+
+  @Test
+  public void testMergeAclEntriesMultipleNewBeforeExisting()
+      throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "diana", READ))
+      .add(aclEntry(ACCESS, GROUP, READ_EXECUTE))
+      .add(aclEntry(ACCESS, MASK, READ_EXECUTE))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(ACCESS, USER, "bruce", READ_EXECUTE),
+      aclEntry(ACCESS, USER, "clark", READ_EXECUTE),
+      aclEntry(ACCESS, USER, "diana", READ_EXECUTE));
+    List<AclEntry> expected = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "bruce", READ_EXECUTE))
+      .add(aclEntry(ACCESS, USER, "clark", READ_EXECUTE))
+      .add(aclEntry(ACCESS, USER, "diana", READ_EXECUTE))
+      .add(aclEntry(ACCESS, GROUP, READ_EXECUTE))
+      .add(aclEntry(ACCESS, MASK, READ_EXECUTE))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .build();
+    assertEquals(expected, mergeAclEntries(existing, aclSpec));
+  }
+
+  @Test
+  public void testMergeAclEntriesAccessMaskCalculated() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "bruce", READ))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, MASK, READ))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(ACCESS, USER, "bruce", READ_EXECUTE),
+      aclEntry(ACCESS, USER, "diana", READ));
+    List<AclEntry> expected = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "bruce", READ_EXECUTE))
+      .add(aclEntry(ACCESS, USER, "diana", READ))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, MASK, READ_EXECUTE))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .build();
+    assertEquals(expected, mergeAclEntries(existing, aclSpec));
+  }
+
+  @Test
+  public void testMergeAclEntriesDefaultMaskCalculated() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .add(aclEntry(DEFAULT, USER, ALL))
+      .add(aclEntry(DEFAULT, USER, "bruce", READ))
+      .add(aclEntry(DEFAULT, GROUP, READ))
+      .add(aclEntry(DEFAULT, MASK, READ))
+      .add(aclEntry(DEFAULT, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(DEFAULT, USER, "bruce", READ_WRITE),
+      aclEntry(DEFAULT, USER, "diana", READ_EXECUTE));
+    List<AclEntry> expected = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .add(aclEntry(DEFAULT, USER, ALL))
+      .add(aclEntry(DEFAULT, USER, "bruce", READ_WRITE))
+      .add(aclEntry(DEFAULT, USER, "diana", READ_EXECUTE))
+      .add(aclEntry(DEFAULT, GROUP, READ))
+      .add(aclEntry(DEFAULT, MASK, ALL))
+      .add(aclEntry(DEFAULT, OTHER, NONE))
+      .build();
+    assertEquals(expected, mergeAclEntries(existing, aclSpec));
+  }
+
+  @Test
+  public void testMergeAclEntriesDefaultMaskPreserved() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .add(aclEntry(DEFAULT, USER, ALL))
+      .add(aclEntry(DEFAULT, USER, "diana", ALL))
+      .add(aclEntry(DEFAULT, GROUP, READ))
+      .add(aclEntry(DEFAULT, MASK, READ))
+      .add(aclEntry(DEFAULT, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(ACCESS, USER, "diana", FsAction.READ_EXECUTE));
+    List<AclEntry> expected = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "diana", READ_EXECUTE))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, MASK, READ_EXECUTE))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .add(aclEntry(DEFAULT, USER, ALL))
+      .add(aclEntry(DEFAULT, USER, "diana", ALL))
+      .add(aclEntry(DEFAULT, GROUP, READ))
+      .add(aclEntry(DEFAULT, MASK, READ))
+      .add(aclEntry(DEFAULT, OTHER, NONE))
+      .build();
+    assertEquals(expected, mergeAclEntries(existing, aclSpec));
+  }
+
+  @Test
+  public void testMergeAclEntriesAccessMaskPreserved() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "bruce", READ))
+      .add(aclEntry(ACCESS, USER, "diana", READ_WRITE))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, MASK, READ))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .add(aclEntry(DEFAULT, USER, ALL))
+      .add(aclEntry(DEFAULT, USER, "bruce", READ))
+      .add(aclEntry(DEFAULT, USER, "diana", READ_WRITE))
+      .add(aclEntry(DEFAULT, GROUP, READ))
+      .add(aclEntry(DEFAULT, MASK, READ_WRITE))
+      .add(aclEntry(DEFAULT, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(DEFAULT, USER, "diana", READ_EXECUTE));
+    List<AclEntry> expected = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "bruce", READ))
+      .add(aclEntry(ACCESS, USER, "diana", READ_WRITE))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, MASK, READ))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .add(aclEntry(DEFAULT, USER, ALL))
+      .add(aclEntry(DEFAULT, USER, "bruce", READ))
+      .add(aclEntry(DEFAULT, USER, "diana", READ_EXECUTE))
+      .add(aclEntry(DEFAULT, GROUP, READ))
+      .add(aclEntry(DEFAULT, MASK, READ_EXECUTE))
+      .add(aclEntry(DEFAULT, OTHER, NONE))
+      .build();
+    assertEquals(expected, mergeAclEntries(existing, aclSpec));
+  }
+
+  @Test
+  public void testMergeAclEntriesAutomaticDefaultUser() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(DEFAULT, GROUP, READ_EXECUTE),
+      aclEntry(DEFAULT, OTHER, READ));
+    List<AclEntry> expected = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .add(aclEntry(DEFAULT, USER, ALL))
+      .add(aclEntry(DEFAULT, GROUP, READ_EXECUTE))
+      .add(aclEntry(DEFAULT, OTHER, READ))
+      .build();
+    assertEquals(expected, mergeAclEntries(existing, aclSpec));
+  }
+
+  @Test
+  public void testMergeAclEntriesAutomaticDefaultGroup() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(DEFAULT, USER, READ_EXECUTE),
+      aclEntry(DEFAULT, OTHER, READ));
+    List<AclEntry> expected = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .add(aclEntry(DEFAULT, USER, READ_EXECUTE))
+      .add(aclEntry(DEFAULT, GROUP, READ))
+      .add(aclEntry(DEFAULT, OTHER, READ))
+      .build();
+    assertEquals(expected, mergeAclEntries(existing, aclSpec));
+  }
+
+  @Test
+  public void testMergeAclEntriesAutomaticDefaultOther() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(DEFAULT, USER, READ_EXECUTE),
+      aclEntry(DEFAULT, GROUP, READ_EXECUTE));
+    List<AclEntry> expected = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .add(aclEntry(DEFAULT, USER, READ_EXECUTE))
+      .add(aclEntry(DEFAULT, GROUP, READ_EXECUTE))
+      .add(aclEntry(DEFAULT, OTHER, NONE))
+      .build();
+    assertEquals(expected, mergeAclEntries(existing, aclSpec));
+  }
+
+  @Test
+  public void testMergeAclEntriesProvidedAccessMask() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(ACCESS, USER, "bruce", READ_EXECUTE),
+      aclEntry(ACCESS, MASK, ALL));
+    List<AclEntry> expected = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "bruce", READ_EXECUTE))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, MASK, ALL))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .build();
+    assertEquals(expected, mergeAclEntries(existing, aclSpec));
+  }
+
+  @Test
+  public void testMergeAclEntriesProvidedDefaultMask() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(DEFAULT, USER, ALL),
+      aclEntry(DEFAULT, GROUP, READ),
+      aclEntry(DEFAULT, MASK, ALL),
+      aclEntry(DEFAULT, OTHER, NONE));
+    List<AclEntry> expected = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .add(aclEntry(DEFAULT, USER, ALL))
+      .add(aclEntry(DEFAULT, GROUP, READ))
+      .add(aclEntry(DEFAULT, MASK, ALL))
+      .add(aclEntry(DEFAULT, OTHER, NONE))
+      .build();
+    assertEquals(expected, mergeAclEntries(existing, aclSpec));
+  }
+
+  @Test
+  public void testMergeAclEntriesEmptyAclSpec() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "bruce", READ_WRITE))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, MASK, ALL))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .add(aclEntry(DEFAULT, USER, ALL))
+      .add(aclEntry(DEFAULT, USER, "bruce", READ_WRITE))
+      .add(aclEntry(DEFAULT, GROUP, READ))
+      .add(aclEntry(DEFAULT, MASK, ALL))
+      .add(aclEntry(DEFAULT, OTHER, READ))
+      .build();
+    List<AclEntry> aclSpec = Lists.<AclEntry>newArrayList();
+    assertEquals(existing, mergeAclEntries(existing, aclSpec));
+  }
+
+  @Test(expected=AclException.class)
+  public void testMergeAclEntriesInputTooLarge() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .build();
+    mergeAclEntries(existing, ACL_SPEC_TOO_LARGE);
+  }
+
+  @Test(expected=AclException.class)
+  public void testMergeAclEntriesResultTooLarge() throws AclException {
+    ImmutableList.Builder<AclEntry> aclBuilder =
+      new ImmutableList.Builder<AclEntry>()
+        .add(aclEntry(ACCESS, USER, ALL));
+    for (int i = 1; i <= 28; ++i) {
+      aclBuilder.add(aclEntry(ACCESS, USER, "user" + i, READ));
+    }
+    aclBuilder
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, MASK, READ))
+      .add(aclEntry(ACCESS, OTHER, NONE));
+    List<AclEntry> existing = aclBuilder.build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(ACCESS, USER, "bruce", READ));
+    mergeAclEntries(existing, aclSpec);
+  }
+
+  @Test(expected=AclException.class)
+  public void testMergeAclEntriesDuplicateEntries() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(ACCESS, USER, "bruce", ALL),
+      aclEntry(ACCESS, USER, "diana", READ_WRITE),
+      aclEntry(ACCESS, USER, "clark", READ),
+      aclEntry(ACCESS, USER, "bruce", READ_EXECUTE));
+    mergeAclEntries(existing, aclSpec);
+  }
+
+  @Test(expected=AclException.class)
+  public void testMergeAclEntriesNamedMask() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(ACCESS, MASK, "bruce", READ_EXECUTE));
+    mergeAclEntries(existing, aclSpec);
+  }
+
+  @Test(expected=AclException.class)
+  public void testMergeAclEntriesNamedOther() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(ACCESS, OTHER, "bruce", READ_EXECUTE));
+    mergeAclEntries(existing, aclSpec);
+  }
+
+  @Test
+  public void testReplaceAclEntries() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "bruce", ALL))
+      .add(aclEntry(ACCESS, GROUP, READ_EXECUTE))
+      .add(aclEntry(ACCESS, MASK, ALL))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(ACCESS, USER, ALL),
+      aclEntry(ACCESS, USER, "bruce", READ_WRITE),
+      aclEntry(ACCESS, GROUP, READ_EXECUTE),
+      aclEntry(ACCESS, GROUP, "sales", ALL),
+      aclEntry(ACCESS, MASK, ALL),
+      aclEntry(ACCESS, OTHER, NONE),
+      aclEntry(DEFAULT, USER, ALL),
+      aclEntry(DEFAULT, USER, "bruce", READ_WRITE),
+      aclEntry(DEFAULT, GROUP, READ_EXECUTE),
+      aclEntry(DEFAULT, GROUP, "sales", ALL),
+      aclEntry(DEFAULT, MASK, ALL),
+      aclEntry(DEFAULT, OTHER, NONE));
+    List<AclEntry> expected = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "bruce", READ_WRITE))
+      .add(aclEntry(ACCESS, GROUP, READ_EXECUTE))
+      .add(aclEntry(ACCESS, GROUP, "sales", ALL))
+      .add(aclEntry(ACCESS, MASK, ALL))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .add(aclEntry(DEFAULT, USER, ALL))
+      .add(aclEntry(DEFAULT, USER, "bruce", READ_WRITE))
+      .add(aclEntry(DEFAULT, GROUP, READ_EXECUTE))
+      .add(aclEntry(DEFAULT, GROUP, "sales", ALL))
+      .add(aclEntry(DEFAULT, MASK, ALL))
+      .add(aclEntry(DEFAULT, OTHER, NONE))
+      .build();
+    assertEquals(expected, replaceAclEntries(existing, aclSpec));
+  }
+
+  @Test
+  public void testReplaceAclEntriesUnchanged() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "bruce", ALL))
+      .add(aclEntry(ACCESS, GROUP, READ_EXECUTE))
+      .add(aclEntry(ACCESS, GROUP, "sales", ALL))
+      .add(aclEntry(ACCESS, MASK, ALL))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .add(aclEntry(DEFAULT, USER, ALL))
+      .add(aclEntry(DEFAULT, USER, "bruce", ALL))
+      .add(aclEntry(DEFAULT, GROUP, READ_EXECUTE))
+      .add(aclEntry(DEFAULT, GROUP, "sales", ALL))
+      .add(aclEntry(DEFAULT, MASK, ALL))
+      .add(aclEntry(DEFAULT, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(ACCESS, USER, ALL),
+      aclEntry(ACCESS, USER, "bruce", ALL),
+      aclEntry(ACCESS, GROUP, READ_EXECUTE),
+      aclEntry(ACCESS, GROUP, "sales", ALL),
+      aclEntry(ACCESS, MASK, ALL),
+      aclEntry(ACCESS, OTHER, NONE),
+      aclEntry(DEFAULT, USER, ALL),
+      aclEntry(DEFAULT, USER, "bruce", ALL),
+      aclEntry(DEFAULT, GROUP, READ_EXECUTE),
+      aclEntry(DEFAULT, GROUP, "sales", ALL),
+      aclEntry(DEFAULT, MASK, ALL),
+      aclEntry(DEFAULT, OTHER, NONE));
+    assertEquals(existing, replaceAclEntries(existing, aclSpec));
+  }
+
+  @Test
+  public void testReplaceAclEntriesAccessMaskCalculated() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(ACCESS, USER, ALL),
+      aclEntry(ACCESS, USER, "bruce", READ),
+      aclEntry(ACCESS, USER, "diana", READ_WRITE),
+      aclEntry(ACCESS, GROUP, READ),
+      aclEntry(ACCESS, OTHER, READ));
+    List<AclEntry> expected = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "bruce", READ))
+      .add(aclEntry(ACCESS, USER, "diana", READ_WRITE))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, MASK, READ_WRITE))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .build();
+    assertEquals(expected, replaceAclEntries(existing, aclSpec));
+  }
+
+  @Test
+  public void testReplaceAclEntriesDefaultMaskCalculated() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(ACCESS, USER, ALL),
+      aclEntry(ACCESS, GROUP, READ),
+      aclEntry(ACCESS, OTHER, READ),
+      aclEntry(DEFAULT, USER, ALL),
+      aclEntry(DEFAULT, USER, "bruce", READ),
+      aclEntry(DEFAULT, USER, "diana", READ_WRITE),
+      aclEntry(DEFAULT, GROUP, ALL),
+      aclEntry(DEFAULT, OTHER, READ));
+    List<AclEntry> expected = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .add(aclEntry(DEFAULT, USER, ALL))
+      .add(aclEntry(DEFAULT, USER, "bruce", READ))
+      .add(aclEntry(DEFAULT, USER, "diana", READ_WRITE))
+      .add(aclEntry(DEFAULT, GROUP, ALL))
+      .add(aclEntry(DEFAULT, MASK, ALL))
+      .add(aclEntry(DEFAULT, OTHER, READ))
+      .build();
+    assertEquals(expected, replaceAclEntries(existing, aclSpec));
+  }
+
+  @Test
+  public void testReplaceAclEntriesDefaultMaskPreserved() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "bruce", READ))
+      .add(aclEntry(ACCESS, USER, "diana", READ_WRITE))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, MASK, READ_WRITE))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .add(aclEntry(DEFAULT, USER, ALL))
+      .add(aclEntry(DEFAULT, USER, "diana", ALL))
+      .add(aclEntry(DEFAULT, GROUP, READ))
+      .add(aclEntry(DEFAULT, MASK, READ))
+      .add(aclEntry(DEFAULT, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(ACCESS, USER, ALL),
+      aclEntry(ACCESS, USER, "bruce", READ),
+      aclEntry(ACCESS, USER, "diana", READ_WRITE),
+      aclEntry(ACCESS, GROUP, ALL),
+      aclEntry(ACCESS, OTHER, READ));
+    List<AclEntry> expected = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "bruce", READ))
+      .add(aclEntry(ACCESS, USER, "diana", READ_WRITE))
+      .add(aclEntry(ACCESS, GROUP, ALL))
+      .add(aclEntry(ACCESS, MASK, ALL))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .add(aclEntry(DEFAULT, USER, ALL))
+      .add(aclEntry(DEFAULT, USER, "diana", ALL))
+      .add(aclEntry(DEFAULT, GROUP, READ))
+      .add(aclEntry(DEFAULT, MASK, READ))
+      .add(aclEntry(DEFAULT, OTHER, NONE))
+      .build();
+    assertEquals(expected, replaceAclEntries(existing, aclSpec));
+  }
+
+  @Test
+  public void testReplaceAclEntriesAccessMaskPreserved() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "bruce", READ))
+      .add(aclEntry(ACCESS, USER, "diana", READ_WRITE))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, MASK, READ))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .add(aclEntry(DEFAULT, USER, ALL))
+      .add(aclEntry(DEFAULT, USER, "bruce", READ))
+      .add(aclEntry(DEFAULT, USER, "diana", READ_WRITE))
+      .add(aclEntry(DEFAULT, GROUP, READ))
+      .add(aclEntry(DEFAULT, MASK, READ_WRITE))
+      .add(aclEntry(DEFAULT, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(DEFAULT, USER, ALL),
+      aclEntry(DEFAULT, USER, "bruce", READ),
+      aclEntry(DEFAULT, GROUP, READ),
+      aclEntry(DEFAULT, OTHER, NONE));
+    List<AclEntry> expected = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, USER, "bruce", READ))
+      .add(aclEntry(ACCESS, USER, "diana", READ_WRITE))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, MASK, READ))
+      .add(aclEntry(ACCESS, OTHER, READ))
+      .add(aclEntry(DEFAULT, USER, ALL))
+      .add(aclEntry(DEFAULT, USER, "bruce", READ))
+      .add(aclEntry(DEFAULT, GROUP, READ))
+      .add(aclEntry(DEFAULT, MASK, READ))
+      .add(aclEntry(DEFAULT, OTHER, NONE))
+      .build();
+    assertEquals(expected, replaceAclEntries(existing, aclSpec));
+  }
+
+  @Test
+  public void testReplaceAclEntriesAutomaticDefaultUser() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(ACCESS, USER, ALL),
+      aclEntry(ACCESS, GROUP, READ),
+      aclEntry(ACCESS, OTHER, NONE),
+      aclEntry(DEFAULT, USER, "bruce", READ),
+      aclEntry(DEFAULT, GROUP, READ_WRITE),
+      aclEntry(DEFAULT, MASK, READ_WRITE),
+      aclEntry(DEFAULT, OTHER, READ));
+    List<AclEntry> expected = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .add(aclEntry(DEFAULT, USER, ALL))
+      .add(aclEntry(DEFAULT, USER, "bruce", READ))
+      .add(aclEntry(DEFAULT, GROUP, READ_WRITE))
+      .add(aclEntry(DEFAULT, MASK, READ_WRITE))
+      .add(aclEntry(DEFAULT, OTHER, READ))
+      .build();
+    assertEquals(expected, replaceAclEntries(existing, aclSpec));
+  }
+
+  @Test
+  public void testReplaceAclEntriesAutomaticDefaultGroup() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(ACCESS, USER, ALL),
+      aclEntry(ACCESS, GROUP, READ),
+      aclEntry(ACCESS, OTHER, NONE),
+      aclEntry(DEFAULT, USER, READ_WRITE),
+      aclEntry(DEFAULT, USER, "bruce", READ),
+      aclEntry(DEFAULT, MASK, READ),
+      aclEntry(DEFAULT, OTHER, READ));
+    List<AclEntry> expected = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .add(aclEntry(DEFAULT, USER, READ_WRITE))
+      .add(aclEntry(DEFAULT, USER, "bruce", READ))
+      .add(aclEntry(DEFAULT, GROUP, READ))
+      .add(aclEntry(DEFAULT, MASK, READ))
+      .add(aclEntry(DEFAULT, OTHER, READ))
+      .build();
+    assertEquals(expected, replaceAclEntries(existing, aclSpec));
+  }
+
+  @Test
+  public void testReplaceAclEntriesAutomaticDefaultOther() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(ACCESS, USER, ALL),
+      aclEntry(ACCESS, GROUP, READ),
+      aclEntry(ACCESS, OTHER, NONE),
+      aclEntry(DEFAULT, USER, READ_WRITE),
+      aclEntry(DEFAULT, USER, "bruce", READ),
+      aclEntry(DEFAULT, GROUP, READ_WRITE),
+      aclEntry(DEFAULT, MASK, READ_WRITE));
+    List<AclEntry> expected = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .add(aclEntry(DEFAULT, USER, READ_WRITE))
+      .add(aclEntry(DEFAULT, USER, "bruce", READ))
+      .add(aclEntry(DEFAULT, GROUP, READ_WRITE))
+      .add(aclEntry(DEFAULT, MASK, READ_WRITE))
+      .add(aclEntry(DEFAULT, OTHER, NONE))
+      .build();
+    assertEquals(expected, replaceAclEntries(existing, aclSpec));
+  }
+
+  @Test(expected=AclException.class)
+  public void testReplaceAclEntriesInputTooLarge() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .build();
+    replaceAclEntries(existing, ACL_SPEC_TOO_LARGE);
+  }
+
+  @Test(expected=AclException.class)
+  public void testReplaceAclEntriesResultTooLarge() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayListWithCapacity(32);
+    aclSpec.add(aclEntry(ACCESS, USER, ALL));
+    for (int i = 1; i <= 29; ++i) {
+      aclSpec.add(aclEntry(ACCESS, USER, "user" + i, READ));
+    }
+    aclSpec.add(aclEntry(ACCESS, GROUP, READ));
+    aclSpec.add(aclEntry(ACCESS, OTHER, NONE));
+    // The ACL spec now has 32 entries.  Automatic mask calculation will push it
+    // over the limit to 33.
+    replaceAclEntries(existing, aclSpec);
+  }
+
+  @Test(expected=AclException.class)
+  public void testReplaceAclEntriesDuplicateEntries() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(ACCESS, USER, ALL),
+      aclEntry(ACCESS, USER, "bruce", ALL),
+      aclEntry(ACCESS, USER, "diana", READ_WRITE),
+      aclEntry(ACCESS, USER, "clark", READ),
+      aclEntry(ACCESS, USER, "bruce", READ_EXECUTE),
+      aclEntry(ACCESS, GROUP, READ),
+      aclEntry(ACCESS, OTHER, NONE));
+    replaceAclEntries(existing, aclSpec);
+  }
+
+  @Test(expected=AclException.class)
+  public void testReplaceAclEntriesNamedMask() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(ACCESS, USER, ALL),
+      aclEntry(ACCESS, GROUP, READ),
+      aclEntry(ACCESS, OTHER, NONE),
+      aclEntry(ACCESS, MASK, "bruce", READ_EXECUTE));
+    replaceAclEntries(existing, aclSpec);
+  }
+
+  @Test(expected=AclException.class)
+  public void testReplaceAclEntriesNamedOther() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(ACCESS, USER, ALL),
+      aclEntry(ACCESS, GROUP, READ),
+      aclEntry(ACCESS, OTHER, NONE),
+      aclEntry(ACCESS, OTHER, "bruce", READ_EXECUTE));
+    replaceAclEntries(existing, aclSpec);
+  }
+
+  @Test(expected=AclException.class)
+  public void testReplaceAclEntriesMissingUser() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(ACCESS, USER, "bruce", READ_WRITE),
+      aclEntry(ACCESS, GROUP, READ_EXECUTE),
+      aclEntry(ACCESS, GROUP, "sales", ALL),
+      aclEntry(ACCESS, MASK, ALL),
+      aclEntry(ACCESS, OTHER, NONE));
+    replaceAclEntries(existing, aclSpec);
+  }
+
+  @Test(expected=AclException.class)
+  public void testReplaceAclEntriesMissingGroup() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(ACCESS, USER, ALL),
+      aclEntry(ACCESS, USER, "bruce", READ_WRITE),
+      aclEntry(ACCESS, GROUP, "sales", ALL),
+      aclEntry(ACCESS, MASK, ALL),
+      aclEntry(ACCESS, OTHER, NONE));
+    replaceAclEntries(existing, aclSpec);
+  }
+
+  @Test(expected=AclException.class)
+  public void testReplaceAclEntriesMissingOther() throws AclException {
+    List<AclEntry> existing = new ImmutableList.Builder<AclEntry>()
+      .add(aclEntry(ACCESS, USER, ALL))
+      .add(aclEntry(ACCESS, GROUP, READ))
+      .add(aclEntry(ACCESS, OTHER, NONE))
+      .build();
+    List<AclEntry> aclSpec = Lists.newArrayList(
+      aclEntry(ACCESS, USER, ALL),
+      aclEntry(ACCESS, USER, "bruce", READ_WRITE),
+      aclEntry(ACCESS, GROUP, READ_EXECUTE),
+      aclEntry(ACCESS, GROUP, "sales", ALL),
+      aclEntry(ACCESS, MASK, ALL));
+    replaceAclEntries(existing, aclSpec);
+  }
+
+  private static AclEntry aclEntry(AclEntryScope scope, AclEntryType type,
+      FsAction permission) {
+    return new AclEntry.Builder()
+      .setScope(scope)
+      .setType(type)
+      .setPermission(permission)
+      .build();
+  }
+
+  private static AclEntry aclEntry(AclEntryScope scope, AclEntryType type,
+      String name, FsAction permission) {
+    return new AclEntry.Builder()
+      .setScope(scope)
+      .setType(type)
+      .setName(name)
+      .setPermission(permission)
+      .build();
+  }
+
+  private static AclEntry aclEntry(AclEntryScope scope, AclEntryType type,
+      String name) {
+    return new AclEntry.Builder()
+      .setScope(scope)
+      .setType(type)
+      .setName(name)
+      .build();
+  }
+
+  private static AclEntry aclEntry(AclEntryScope scope, AclEntryType type) {
+    return new AclEntry.Builder()
+      .setScope(scope)
+      .setType(type)
+      .build();
+  }
+}



Mime
View raw message