From c6585675710aa800317ad35e6fdf95c50c559d68 Mon Sep 17 00:00:00 2001 From: Chris Nauroth Date: Tue, 7 Jan 2014 00:47:49 +0000 Subject: [PATCH] HDFS-5673. Implement logic for modification of ACLs. Contributed by Chris Nauroth. git-svn-id: https://svn.apache.org/repos/asf/hadoop/common/branches/HDFS-4685@1556090 13f79535-47bb-0310-9956-ffa450edef68 --- .../apache/hadoop/fs/permission/AclEntry.java | 26 +- .../hadoop/fs/permission/AclStatus.java | 5 +- .../apache/hadoop/fs/permission/TestAcl.java | 33 - .../hadoop-hdfs/CHANGES-HDFS-4685.txt | 2 + .../hadoop/hdfs/protocol/AclException.java | 39 + .../server/namenode/AclTransformation.java | 480 +++++++ .../namenode/TestAclTransformation.java | 1220 +++++++++++++++++ 7 files changed, 1745 insertions(+), 60 deletions(-) create mode 100644 hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/protocol/AclException.java create mode 100644 hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/AclTransformation.java create mode 100644 hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/TestAclTransformation.java diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/permission/AclEntry.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/permission/AclEntry.java index 2cb2250ccbc..3d57e06d471 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/permission/AclEntry.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/permission/AclEntry.java @@ -18,11 +18,8 @@ package org.apache.hadoop.fs.permission; import static org.apache.hadoop.fs.permission.AclEntryScope.*; -import static org.apache.hadoop.fs.permission.AclEntryType.*; import com.google.common.base.Objects; -import com.google.common.collect.ComparisonChain; -import com.google.common.collect.Ordering; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; @@ -31,20 +28,12 @@ import org.apache.hadoop.classification.InterfaceStability; * Defines a single entry in an ACL. An ACL entry has a type (user, group, * mask, or other), an optional name (referring to a specific user or group), a * set of permissions (any combination of read, write and execute), and a scope - * (access or default). The natural ordering for entries within an ACL is: - *
    - *
  1. owner entry (unnamed user)
  2. - *
  3. all named user entries (internal ordering undefined)
  4. - *
  5. owning group entry (unnamed group)
  6. - *
  7. all named group entries (internal ordering undefined)
  8. - *
  9. other entry
  10. - *
- * All access ACL entries sort ahead of all default ACL entries. AclEntry - * instances are immutable. Use a {@link Builder} to create a new instance. + * (access or default). AclEntry instances are immutable. Use a {@link Builder} + * to create a new instance. */ @InterfaceAudience.Public @InterfaceStability.Evolving -public class AclEntry implements Comparable { +public class AclEntry { private final AclEntryType type; private final String name; private final FsAction permission; @@ -106,15 +95,6 @@ public class AclEntry implements Comparable { return Objects.hashCode(type, name, permission, scope); } - @Override - public int compareTo(AclEntry other) { - return ComparisonChain.start() - .compare(scope, other.scope, Ordering.explicit(ACCESS, DEFAULT)) - .compare(type, other.type, Ordering.explicit(USER, GROUP, MASK, OTHER)) - .compare(name, other.name, Ordering.natural().nullsFirst()) - .result(); - } - @Override public String toString() { StringBuilder sb = new StringBuilder(); diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/permission/AclStatus.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/permission/AclStatus.java index c881b0faab8..4a7258f0a27 100644 --- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/permission/AclStatus.java +++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/fs/permission/AclStatus.java @@ -17,7 +17,6 @@ */ package org.apache.hadoop.fs.permission; -import java.util.Collections; import java.util.List; import org.apache.hadoop.classification.InterfaceAudience; @@ -197,8 +196,6 @@ public class AclStatus { this.owner = owner; this.group = group; this.stickyBit = stickyBit; - List entriesCopy = Lists.newArrayList(entries); - Collections.sort(entriesCopy); - this.entries = entriesCopy; + this.entries = Lists.newArrayList(entries); } } diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/permission/TestAcl.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/permission/TestAcl.java index 94db270f984..f33da8aa8be 100644 --- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/permission/TestAcl.java +++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/fs/permission/TestAcl.java @@ -19,13 +19,9 @@ package org.apache.hadoop.fs.permission; import static org.junit.Assert.*; -import java.util.List; - import org.junit.BeforeClass; import org.junit.Test; -import com.google.common.collect.Lists; - /** * Tests covering basic functionality of the ACL objects. */ @@ -165,35 +161,6 @@ public class TestAcl { assertFalse(ENTRY3.hashCode() == ENTRY4.hashCode()); } - @Test - public void testEntryNaturalOrdering() { - AclEntry expected[] = new AclEntry[] { - ENTRY5, // owner - ENTRY1, // named user - ENTRY11, // group - ENTRY3, // named group - ENTRY9, // mask - ENTRY7, // other - ENTRY13, // default owner - ENTRY8, // default named user - ENTRY12, // default group - ENTRY6, // default named group - ENTRY10, // default mask - ENTRY4 // default other - }; - List actual = Lists.newArrayList(STATUS4.getEntries()); - assertNotNull(actual); - assertEquals(expected.length, actual.size()); - for (int i = 0; i < expected.length; ++i) { - AclEntry expectedEntry = expected[i]; - AclEntry actualEntry = actual.get(i); - assertEquals( - String.format("At position %d, expected = %s, actual = %s", i, - expectedEntry, actualEntry), - expectedEntry, actualEntry); - } - } - @Test public void testEntryScopeIsAccessIfUnspecified() { assertEquals(AclEntryScope.ACCESS, ENTRY1.getScope()); diff --git a/hadoop-hdfs-project/hadoop-hdfs/CHANGES-HDFS-4685.txt b/hadoop-hdfs-project/hadoop-hdfs/CHANGES-HDFS-4685.txt index 7b07ed58e6a..77d4c387167 100644 --- a/hadoop-hdfs-project/hadoop-hdfs/CHANGES-HDFS-4685.txt +++ b/hadoop-hdfs-project/hadoop-hdfs/CHANGES-HDFS-4685.txt @@ -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 diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/protocol/AclException.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/protocol/AclException.java new file mode 100644 index 00000000000..12109999d8d --- /dev/null +++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/protocol/AclException.java @@ -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); + } +} diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/AclTransformation.java b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/AclTransformation.java new file mode 100644 index 00000000000..06e95ff2beb --- /dev/null +++ b/hadoop-hdfs-project/hadoop-hdfs/src/main/java/org/apache/hadoop/hdfs/server/namenode/AclTransformation.java @@ -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 existing ACL + * @param inAclSpec List ACL spec describing entries to filter + * @return List new ACL + * @throws AclException if validation fails + */ + public static List filterAclEntriesByAclSpec( + List existingAcl, List inAclSpec) throws AclException { + ValidatedAclSpec aclSpec = new ValidatedAclSpec(inAclSpec); + ArrayList aclBuilder = Lists.newArrayListWithCapacity(MAX_ENTRIES); + EnumMap providedMask = + Maps.newEnumMap(AclEntryScope.class); + EnumSet maskDirty = EnumSet.noneOf(AclEntryScope.class); + EnumSet 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 existing ACL + * @return List new ACL + * @throws AclException if validation fails + */ + public static List filterDefaultAclEntries( + List existingAcl) throws AclException { + ArrayList 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 existing ACL + * @param inAclSpec List ACL spec containing entries to merge + * @return List new ACL + * @throws AclException if validation fails + */ + public static List mergeAclEntries(List existingAcl, + List inAclSpec) throws AclException { + ValidatedAclSpec aclSpec = new ValidatedAclSpec(inAclSpec); + ArrayList aclBuilder = Lists.newArrayListWithCapacity(MAX_ENTRIES); + List foundAclSpecEntries = + Lists.newArrayListWithCapacity(MAX_ENTRIES); + EnumMap providedMask = + Maps.newEnumMap(AclEntryScope.class); + EnumSet maskDirty = EnumSet.noneOf(AclEntryScope.class); + EnumSet 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 existing ACL + * @param inAclSpec List ACL spec containing replacement entries + * @return List new ACL + * @throws AclException if validation fails + */ + public static List replaceAclEntries(List existingAcl, + List inAclSpec) throws AclException { + ValidatedAclSpec aclSpec = new ValidatedAclSpec(inAclSpec); + ArrayList aclBuilder = Lists.newArrayListWithCapacity(MAX_ENTRIES); + // Replacement is done separately for each scope: access and default. + EnumMap providedMask = + Maps.newEnumMap(AclEntryScope.class); + EnumSet maskDirty = EnumSet.noneOf(AclEntryScope.class); + EnumSet 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 ACL_ENTRY_COMPARATOR = + new Comparator() { + @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 containing entries to build + * @return List unmodifiable, sorted list of ACL entries + * @throws AclException if validation fails + */ + private static List buildAndValidateAcl( + ArrayList 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 containing entries to build + * @param providedMask EnumMap mapping each scope to + * the mask entry that was provided for that scope (if provided) + * @param maskDirty EnumSet which contains a scope if the mask + * entry is dirty (added or deleted) in that scope + * @param scopeDirty EnumSet 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 aclBuilder, + EnumMap providedMask, + EnumSet maskDirty, EnumSet scopeDirty) + throws AclException { + EnumSet scopeFound = EnumSet.noneOf(AclEntryScope.class); + EnumMap unionPerms = + Maps.newEnumMap(AclEntryScope.class); + EnumSet 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 containing entries to build + */ + private static void copyDefaultsIfNeeded(List 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 accessEntries = aclBuilder.subList(0, pivot); + List defaultEntries = aclBuilder.subList(pivot, + aclBuilder.size()); + List 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 { + private final List 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 containing unvalidated input ACL spec + * @throws AclException if validation fails + */ + public ValidatedAclSpec(List 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 iterator() { + return aclSpec.iterator(); + } + } +} diff --git a/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/TestAclTransformation.java b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/TestAclTransformation.java new file mode 100644 index 00000000000..26ce1236da0 --- /dev/null +++ b/hadoop-hdfs-project/hadoop-hdfs/src/test/java/org/apache/hadoop/hdfs/server/namenode/TestAclTransformation.java @@ -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 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 existing = new ImmutableList.Builder() + .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 aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, "diana"), + aclEntry(ACCESS, GROUP, "sales")); + List expected = new ImmutableList.Builder() + .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 existing = new ImmutableList.Builder() + .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 aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, "clark"), + aclEntry(ACCESS, GROUP, "execs")); + assertEquals(existing, filterAclEntriesByAclSpec(existing, aclSpec)); + } + + @Test + public void testFilterAclEntriesByAclSpecAccessMaskCalculated() + throws AclException { + List existing = new ImmutableList.Builder() + .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 aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, "diana")); + List expected = new ImmutableList.Builder() + .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 existing = new ImmutableList.Builder() + .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 aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, USER, "diana")); + List expected = new ImmutableList.Builder() + .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 existing = new ImmutableList.Builder() + .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 aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, "diana")); + List expected = new ImmutableList.Builder() + .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 existing = new ImmutableList.Builder() + .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 aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, USER, "diana")); + List expected = new ImmutableList.Builder() + .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 existing = new ImmutableList.Builder() + .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 aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, USER)); + List expected = new ImmutableList.Builder() + .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 existing = new ImmutableList.Builder() + .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 aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, GROUP)); + List expected = new ImmutableList.Builder() + .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 existing = new ImmutableList.Builder() + .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 aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, OTHER)); + List expected = new ImmutableList.Builder() + .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 existing = new ImmutableList.Builder() + .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 aclSpec = Lists.newArrayList(); + assertEquals(existing, filterAclEntriesByAclSpec(existing, aclSpec)); + } + + @Test(expected=AclException.class) + public void testFilterAclEntriesByAclSpecRemoveAccessMaskRequired() + throws AclException { + List existing = new ImmutableList.Builder() + .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 aclSpec = Lists.newArrayList( + aclEntry(ACCESS, MASK)); + filterAclEntriesByAclSpec(existing, aclSpec); + } + + @Test(expected=AclException.class) + public void testFilterAclEntriesByAclSpecRemoveDefaultMaskRequired() + throws AclException { + List existing = new ImmutableList.Builder() + .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 aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, MASK)); + filterAclEntriesByAclSpec(existing, aclSpec); + } + + @Test(expected=AclException.class) + public void testFilterAclEntriesByAclSpecInputTooLarge() throws AclException { + List existing = new ImmutableList.Builder() + .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 existing = new ImmutableList.Builder() + .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 expected = new ImmutableList.Builder() + .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 existing = new ImmutableList.Builder() + .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 existing = new ImmutableList.Builder() + .add(aclEntry(ACCESS, USER, ALL)) + .add(aclEntry(ACCESS, GROUP, READ_EXECUTE)) + .add(aclEntry(ACCESS, OTHER, NONE)) + .build(); + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, "bruce", ALL)); + List expected = new ImmutableList.Builder() + .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 existing = new ImmutableList.Builder() + .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 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 existing = new ImmutableList.Builder() + .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 aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, "bruce", READ_EXECUTE), + aclEntry(ACCESS, USER, "clark", READ_EXECUTE), + aclEntry(ACCESS, USER, "diana", READ_EXECUTE)); + List expected = new ImmutableList.Builder() + .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 existing = new ImmutableList.Builder() + .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 aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, "bruce", READ_EXECUTE), + aclEntry(ACCESS, USER, "diana", READ)); + List expected = new ImmutableList.Builder() + .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 existing = new ImmutableList.Builder() + .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 aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, USER, "bruce", READ_WRITE), + aclEntry(DEFAULT, USER, "diana", READ_EXECUTE)); + List expected = new ImmutableList.Builder() + .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 existing = new ImmutableList.Builder() + .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 aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, "diana", FsAction.READ_EXECUTE)); + List expected = new ImmutableList.Builder() + .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 existing = new ImmutableList.Builder() + .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 aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, USER, "diana", READ_EXECUTE)); + List expected = new ImmutableList.Builder() + .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 existing = new ImmutableList.Builder() + .add(aclEntry(ACCESS, USER, ALL)) + .add(aclEntry(ACCESS, GROUP, READ)) + .add(aclEntry(ACCESS, OTHER, READ)) + .build(); + List aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, GROUP, READ_EXECUTE), + aclEntry(DEFAULT, OTHER, READ)); + List expected = new ImmutableList.Builder() + .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 existing = new ImmutableList.Builder() + .add(aclEntry(ACCESS, USER, ALL)) + .add(aclEntry(ACCESS, GROUP, READ)) + .add(aclEntry(ACCESS, OTHER, READ)) + .build(); + List aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, USER, READ_EXECUTE), + aclEntry(DEFAULT, OTHER, READ)); + List expected = new ImmutableList.Builder() + .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 existing = new ImmutableList.Builder() + .add(aclEntry(ACCESS, USER, ALL)) + .add(aclEntry(ACCESS, GROUP, READ)) + .add(aclEntry(ACCESS, OTHER, NONE)) + .build(); + List aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, USER, READ_EXECUTE), + aclEntry(DEFAULT, GROUP, READ_EXECUTE)); + List expected = new ImmutableList.Builder() + .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 existing = new ImmutableList.Builder() + .add(aclEntry(ACCESS, USER, ALL)) + .add(aclEntry(ACCESS, GROUP, READ)) + .add(aclEntry(ACCESS, OTHER, NONE)) + .build(); + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, "bruce", READ_EXECUTE), + aclEntry(ACCESS, MASK, ALL)); + List expected = new ImmutableList.Builder() + .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 existing = new ImmutableList.Builder() + .add(aclEntry(ACCESS, USER, ALL)) + .add(aclEntry(ACCESS, GROUP, READ)) + .add(aclEntry(ACCESS, OTHER, NONE)) + .build(); + List aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, USER, ALL), + aclEntry(DEFAULT, GROUP, READ), + aclEntry(DEFAULT, MASK, ALL), + aclEntry(DEFAULT, OTHER, NONE)); + List expected = new ImmutableList.Builder() + .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 existing = new ImmutableList.Builder() + .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 aclSpec = Lists.newArrayList(); + assertEquals(existing, mergeAclEntries(existing, aclSpec)); + } + + @Test(expected=AclException.class) + public void testMergeAclEntriesInputTooLarge() throws AclException { + List existing = new ImmutableList.Builder() + .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 aclBuilder = + new ImmutableList.Builder() + .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 existing = aclBuilder.build(); + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, USER, "bruce", READ)); + mergeAclEntries(existing, aclSpec); + } + + @Test(expected=AclException.class) + public void testMergeAclEntriesDuplicateEntries() throws AclException { + List existing = new ImmutableList.Builder() + .add(aclEntry(ACCESS, USER, ALL)) + .add(aclEntry(ACCESS, GROUP, READ)) + .add(aclEntry(ACCESS, OTHER, NONE)) + .build(); + List 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 existing = new ImmutableList.Builder() + .add(aclEntry(ACCESS, USER, ALL)) + .add(aclEntry(ACCESS, GROUP, READ)) + .add(aclEntry(ACCESS, OTHER, NONE)) + .build(); + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, MASK, "bruce", READ_EXECUTE)); + mergeAclEntries(existing, aclSpec); + } + + @Test(expected=AclException.class) + public void testMergeAclEntriesNamedOther() throws AclException { + List existing = new ImmutableList.Builder() + .add(aclEntry(ACCESS, USER, ALL)) + .add(aclEntry(ACCESS, GROUP, READ)) + .add(aclEntry(ACCESS, OTHER, NONE)) + .build(); + List aclSpec = Lists.newArrayList( + aclEntry(ACCESS, OTHER, "bruce", READ_EXECUTE)); + mergeAclEntries(existing, aclSpec); + } + + @Test + public void testReplaceAclEntries() throws AclException { + List existing = new ImmutableList.Builder() + .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 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 expected = new ImmutableList.Builder() + .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 existing = new ImmutableList.Builder() + .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 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 existing = new ImmutableList.Builder() + .add(aclEntry(ACCESS, USER, ALL)) + .add(aclEntry(ACCESS, GROUP, READ)) + .add(aclEntry(ACCESS, OTHER, READ)) + .build(); + List 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 expected = new ImmutableList.Builder() + .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 existing = new ImmutableList.Builder() + .add(aclEntry(ACCESS, USER, ALL)) + .add(aclEntry(ACCESS, GROUP, READ)) + .add(aclEntry(ACCESS, OTHER, READ)) + .build(); + List 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 expected = new ImmutableList.Builder() + .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 existing = new ImmutableList.Builder() + .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 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 expected = new ImmutableList.Builder() + .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 existing = new ImmutableList.Builder() + .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 aclSpec = Lists.newArrayList( + aclEntry(DEFAULT, USER, ALL), + aclEntry(DEFAULT, USER, "bruce", READ), + aclEntry(DEFAULT, GROUP, READ), + aclEntry(DEFAULT, OTHER, NONE)); + List expected = new ImmutableList.Builder() + .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 existing = new ImmutableList.Builder() + .add(aclEntry(ACCESS, USER, ALL)) + .add(aclEntry(ACCESS, GROUP, READ)) + .add(aclEntry(ACCESS, OTHER, NONE)) + .build(); + List 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 expected = new ImmutableList.Builder() + .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 existing = new ImmutableList.Builder() + .add(aclEntry(ACCESS, USER, ALL)) + .add(aclEntry(ACCESS, GROUP, READ)) + .add(aclEntry(ACCESS, OTHER, NONE)) + .build(); + List 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 expected = new ImmutableList.Builder() + .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 existing = new ImmutableList.Builder() + .add(aclEntry(ACCESS, USER, ALL)) + .add(aclEntry(ACCESS, GROUP, READ)) + .add(aclEntry(ACCESS, OTHER, NONE)) + .build(); + List 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 expected = new ImmutableList.Builder() + .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 existing = new ImmutableList.Builder() + .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 existing = new ImmutableList.Builder() + .add(aclEntry(ACCESS, USER, ALL)) + .add(aclEntry(ACCESS, GROUP, READ)) + .add(aclEntry(ACCESS, OTHER, NONE)) + .build(); + List 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 existing = new ImmutableList.Builder() + .add(aclEntry(ACCESS, USER, ALL)) + .add(aclEntry(ACCESS, GROUP, READ)) + .add(aclEntry(ACCESS, OTHER, NONE)) + .build(); + List 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 existing = new ImmutableList.Builder() + .add(aclEntry(ACCESS, USER, ALL)) + .add(aclEntry(ACCESS, GROUP, READ)) + .add(aclEntry(ACCESS, OTHER, NONE)) + .build(); + List 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 existing = new ImmutableList.Builder() + .add(aclEntry(ACCESS, USER, ALL)) + .add(aclEntry(ACCESS, GROUP, READ)) + .add(aclEntry(ACCESS, OTHER, NONE)) + .build(); + List 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 existing = new ImmutableList.Builder() + .add(aclEntry(ACCESS, USER, ALL)) + .add(aclEntry(ACCESS, GROUP, READ)) + .add(aclEntry(ACCESS, OTHER, NONE)) + .build(); + List 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 existing = new ImmutableList.Builder() + .add(aclEntry(ACCESS, USER, ALL)) + .add(aclEntry(ACCESS, GROUP, READ)) + .add(aclEntry(ACCESS, OTHER, NONE)) + .build(); + List 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 existing = new ImmutableList.Builder() + .add(aclEntry(ACCESS, USER, ALL)) + .add(aclEntry(ACCESS, GROUP, READ)) + .add(aclEntry(ACCESS, OTHER, NONE)) + .build(); + List 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(); + } +}