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
This commit is contained in:
parent
29dac1d629
commit
c658567571
|
@ -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:
|
||||
* <ol>
|
||||
* <li>owner entry (unnamed user)</li>
|
||||
* <li>all named user entries (internal ordering undefined)</li>
|
||||
* <li>owning group entry (unnamed group)</li>
|
||||
* <li>all named group entries (internal ordering undefined)</li>
|
||||
* <li>other entry</li>
|
||||
* </ol>
|
||||
* 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<AclEntry> {
|
||||
public class AclEntry {
|
||||
private final AclEntryType type;
|
||||
private final String name;
|
||||
private final FsAction permission;
|
||||
|
@ -106,15 +95,6 @@ public class AclEntry implements Comparable<AclEntry> {
|
|||
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();
|
||||
|
|
|
@ -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<AclEntry> entriesCopy = Lists.newArrayList(entries);
|
||||
Collections.sort(entriesCopy);
|
||||
this.entries = entriesCopy;
|
||||
this.entries = Lists.newArrayList(entries);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<AclEntry> 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());
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue