/** * 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. */ 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); // Full iteration to check for duplicates and invalid named entries. 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 + "."); } prevEntry = entry; } // Search for the required base access entries. If there is a default ACL, // then do the same check on the default entries. ScopedAclEntries scopedEntries = new ScopedAclEntries(aclBuilder); for (AclEntryType type: EnumSet.of(USER, GROUP, OTHER)) { AclEntry accessEntryKey = new AclEntry.Builder().setScope(ACCESS) .setType(type).build(); if (Collections.binarySearch(scopedEntries.getAccessEntries(), accessEntryKey, ACL_ENTRY_COMPARATOR) < 0) { throw new AclException( "Invalid ACL: the user, group and other entries are required."); } if (!scopedEntries.getDefaultEntries().isEmpty()) { AclEntry defaultEntryKey = new AclEntry.Builder().setScope(DEFAULT) .setType(type).build(); if (Collections.binarySearch(scopedEntries.getDefaultEntries(), defaultEntryKey, ACL_ENTRY_COMPARATOR) < 0) { throw new AclException( "Invalid default 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)) { // Caller explicitly removed mask entry, but it's required. throw new AclException( "Invalid ACL: mask is required and cannot be deleted."); } else if (providedMask.containsKey(scope) && (!scopeDirty.contains(scope) || maskDirty.contains(scope))) { // Caller explicitly provided new mask, or we are preserving the existing // mask in an unchanged scope. aclBuilder.add(providedMask.get(scope)); } else if (maskNeeded.contains(scope) || providedMask.containsKey(scope)) { // Otherwise, if there are maskable entries present, or the ACL // previously had a mask, then recalculate a mask automatically. 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) { Collections.sort(aclBuilder, ACL_ENTRY_COMPARATOR); ScopedAclEntries scopedEntries = new ScopedAclEntries(aclBuilder); if (!scopedEntries.getDefaultEntries().isEmpty()) { List<AclEntry> accessEntries = scopedEntries.getAccessEntries(); List<AclEntry> defaultEntries = scopedEntries.getDefaultEntries(); 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(); } } }