001 /** 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018 package org.apache.hadoop.hdfs.server.namenode; 019 020 import java.io.IOException; 021 import java.util.EnumSet; 022 import java.util.List; 023 import java.util.NavigableMap; 024 import java.util.TreeMap; 025 026 import com.google.common.base.Preconditions; 027 import com.google.common.collect.Lists; 028 import org.apache.hadoop.conf.Configuration; 029 import org.apache.hadoop.crypto.CipherSuite; 030 import org.apache.hadoop.crypto.CryptoProtocolVersion; 031 import org.apache.hadoop.fs.UnresolvedLinkException; 032 import org.apache.hadoop.fs.XAttr; 033 import org.apache.hadoop.fs.XAttrSetFlag; 034 import org.apache.hadoop.hdfs.DFSConfigKeys; 035 import org.apache.hadoop.hdfs.XAttrHelper; 036 import org.apache.hadoop.hdfs.protocol.EncryptionZone; 037 import org.apache.hadoop.hdfs.protocol.SnapshotAccessControlException; 038 import org.apache.hadoop.hdfs.protocol.proto.HdfsProtos; 039 import org.apache.hadoop.hdfs.protocolPB.PBHelper; 040 import org.slf4j.Logger; 041 import org.slf4j.LoggerFactory; 042 043 044 import static org.apache.hadoop.fs.BatchedRemoteIterator.BatchedListEntries; 045 import static org.apache.hadoop.hdfs.server.common.HdfsServerConstants 046 .CRYPTO_XATTR_ENCRYPTION_ZONE; 047 048 /** 049 * Manages the list of encryption zones in the filesystem. 050 * <p/> 051 * The EncryptionZoneManager has its own lock, but relies on the FSDirectory 052 * lock being held for many operations. The FSDirectory lock should not be 053 * taken if the manager lock is already held. 054 */ 055 public class EncryptionZoneManager { 056 057 public static Logger LOG = LoggerFactory.getLogger(EncryptionZoneManager 058 .class); 059 060 /** 061 * EncryptionZoneInt is the internal representation of an encryption zone. The 062 * external representation of an EZ is embodied in an EncryptionZone and 063 * contains the EZ's pathname. 064 */ 065 private static class EncryptionZoneInt { 066 private final long inodeId; 067 private final CipherSuite suite; 068 private final CryptoProtocolVersion version; 069 private final String keyName; 070 071 EncryptionZoneInt(long inodeId, CipherSuite suite, 072 CryptoProtocolVersion version, String keyName) { 073 Preconditions.checkArgument(suite != CipherSuite.UNKNOWN); 074 Preconditions.checkArgument(version != CryptoProtocolVersion.UNKNOWN); 075 this.inodeId = inodeId; 076 this.suite = suite; 077 this.version = version; 078 this.keyName = keyName; 079 } 080 081 long getINodeId() { 082 return inodeId; 083 } 084 085 CipherSuite getSuite() { 086 return suite; 087 } 088 089 CryptoProtocolVersion getVersion() { return version; } 090 091 String getKeyName() { 092 return keyName; 093 } 094 } 095 096 private final TreeMap<Long, EncryptionZoneInt> encryptionZones; 097 private final FSDirectory dir; 098 private final int maxListEncryptionZonesResponses; 099 100 /** 101 * Construct a new EncryptionZoneManager. 102 * 103 * @param dir Enclosing FSDirectory 104 */ 105 public EncryptionZoneManager(FSDirectory dir, Configuration conf) { 106 this.dir = dir; 107 encryptionZones = new TreeMap<Long, EncryptionZoneInt>(); 108 maxListEncryptionZonesResponses = conf.getInt( 109 DFSConfigKeys.DFS_NAMENODE_LIST_ENCRYPTION_ZONES_NUM_RESPONSES, 110 DFSConfigKeys.DFS_NAMENODE_LIST_ENCRYPTION_ZONES_NUM_RESPONSES_DEFAULT 111 ); 112 Preconditions.checkArgument(maxListEncryptionZonesResponses >= 0, 113 DFSConfigKeys.DFS_NAMENODE_LIST_ENCRYPTION_ZONES_NUM_RESPONSES + " " + 114 "must be a positive integer." 115 ); 116 } 117 118 /** 119 * Add a new encryption zone. 120 * <p/> 121 * Called while holding the FSDirectory lock. 122 * 123 * @param inodeId of the encryption zone 124 * @param keyName encryption zone key name 125 */ 126 void addEncryptionZone(Long inodeId, CipherSuite suite, 127 CryptoProtocolVersion version, String keyName) { 128 assert dir.hasWriteLock(); 129 unprotectedAddEncryptionZone(inodeId, suite, version, keyName); 130 } 131 132 /** 133 * Add a new encryption zone. 134 * <p/> 135 * Does not assume that the FSDirectory lock is held. 136 * 137 * @param inodeId of the encryption zone 138 * @param keyName encryption zone key name 139 */ 140 void unprotectedAddEncryptionZone(Long inodeId, 141 CipherSuite suite, CryptoProtocolVersion version, String keyName) { 142 final EncryptionZoneInt ez = new EncryptionZoneInt( 143 inodeId, suite, version, keyName); 144 encryptionZones.put(inodeId, ez); 145 } 146 147 /** 148 * Remove an encryption zone. 149 * <p/> 150 * Called while holding the FSDirectory lock. 151 */ 152 void removeEncryptionZone(Long inodeId) { 153 assert dir.hasWriteLock(); 154 encryptionZones.remove(inodeId); 155 } 156 157 /** 158 * Returns true if an IIP is within an encryption zone. 159 * <p/> 160 * Called while holding the FSDirectory lock. 161 */ 162 boolean isInAnEZ(INodesInPath iip) 163 throws UnresolvedLinkException, SnapshotAccessControlException { 164 assert dir.hasReadLock(); 165 return (getEncryptionZoneForPath(iip) != null); 166 } 167 168 /** 169 * Returns the path of the EncryptionZoneInt. 170 * <p/> 171 * Called while holding the FSDirectory lock. 172 */ 173 private String getFullPathName(EncryptionZoneInt ezi) { 174 assert dir.hasReadLock(); 175 return dir.getInode(ezi.getINodeId()).getFullPathName(); 176 } 177 178 /** 179 * Get the key name for an encryption zone. Returns null if <tt>iip</tt> is 180 * not within an encryption zone. 181 * <p/> 182 * Called while holding the FSDirectory lock. 183 */ 184 String getKeyName(final INodesInPath iip) { 185 assert dir.hasReadLock(); 186 EncryptionZoneInt ezi = getEncryptionZoneForPath(iip); 187 if (ezi == null) { 188 return null; 189 } 190 return ezi.getKeyName(); 191 } 192 193 /** 194 * Looks up the EncryptionZoneInt for a path within an encryption zone. 195 * Returns null if path is not within an EZ. 196 * <p/> 197 * Must be called while holding the manager lock. 198 */ 199 private EncryptionZoneInt getEncryptionZoneForPath(INodesInPath iip) { 200 assert dir.hasReadLock(); 201 Preconditions.checkNotNull(iip); 202 final INode[] inodes = iip.getINodes(); 203 for (int i = inodes.length - 1; i >= 0; i--) { 204 final INode inode = inodes[i]; 205 if (inode != null) { 206 final EncryptionZoneInt ezi = encryptionZones.get(inode.getId()); 207 if (ezi != null) { 208 return ezi; 209 } 210 } 211 } 212 return null; 213 } 214 215 /** 216 * Returns an EncryptionZone representing the ez for a given path. 217 * Returns an empty marker EncryptionZone if path is not in an ez. 218 * 219 * @param iip The INodesInPath of the path to check 220 * @return the EncryptionZone representing the ez for the path. 221 */ 222 EncryptionZone getEZINodeForPath(INodesInPath iip) { 223 final EncryptionZoneInt ezi = getEncryptionZoneForPath(iip); 224 if (ezi == null) { 225 return null; 226 } else { 227 return new EncryptionZone(ezi.getINodeId(), getFullPathName(ezi), 228 ezi.getSuite(), ezi.getVersion(), ezi.getKeyName()); 229 } 230 } 231 232 /** 233 * Throws an exception if the provided path cannot be renamed into the 234 * destination because of differing encryption zones. 235 * <p/> 236 * Called while holding the FSDirectory lock. 237 * 238 * @param srcIIP source IIP 239 * @param dstIIP destination IIP 240 * @param src source path, used for debugging 241 * @throws IOException if the src cannot be renamed to the dst 242 */ 243 void checkMoveValidity(INodesInPath srcIIP, INodesInPath dstIIP, String src) 244 throws IOException { 245 assert dir.hasReadLock(); 246 final EncryptionZoneInt srcEZI = getEncryptionZoneForPath(srcIIP); 247 final EncryptionZoneInt dstEZI = getEncryptionZoneForPath(dstIIP); 248 final boolean srcInEZ = (srcEZI != null); 249 final boolean dstInEZ = (dstEZI != null); 250 if (srcInEZ) { 251 if (!dstInEZ) { 252 throw new IOException( 253 src + " can't be moved from an encryption zone."); 254 } 255 } else { 256 if (dstInEZ) { 257 throw new IOException( 258 src + " can't be moved into an encryption zone."); 259 } 260 } 261 262 if (srcInEZ || dstInEZ) { 263 Preconditions.checkState(srcEZI != null, "couldn't find src EZ?"); 264 Preconditions.checkState(dstEZI != null, "couldn't find dst EZ?"); 265 if (srcEZI != dstEZI) { 266 final String srcEZPath = getFullPathName(srcEZI); 267 final String dstEZPath = getFullPathName(dstEZI); 268 final StringBuilder sb = new StringBuilder(src); 269 sb.append(" can't be moved from encryption zone "); 270 sb.append(srcEZPath); 271 sb.append(" to encryption zone "); 272 sb.append(dstEZPath); 273 sb.append("."); 274 throw new IOException(sb.toString()); 275 } 276 } 277 } 278 279 /** 280 * Create a new encryption zone. 281 * <p/> 282 * Called while holding the FSDirectory lock. 283 */ 284 XAttr createEncryptionZone(String src, CipherSuite suite, 285 CryptoProtocolVersion version, String keyName) 286 throws IOException { 287 assert dir.hasWriteLock(); 288 if (dir.isNonEmptyDirectory(src)) { 289 throw new IOException( 290 "Attempt to create an encryption zone for a non-empty directory."); 291 } 292 293 final INodesInPath srcIIP = dir.getINodesInPath4Write(src, false); 294 if (srcIIP != null && 295 srcIIP.getLastINode() != null && 296 !srcIIP.getLastINode().isDirectory()) { 297 throw new IOException("Attempt to create an encryption zone for a file."); 298 } 299 EncryptionZoneInt ezi = getEncryptionZoneForPath(srcIIP); 300 if (ezi != null) { 301 throw new IOException("Directory " + src + " is already in an " + 302 "encryption zone. (" + getFullPathName(ezi) + ")"); 303 } 304 305 final HdfsProtos.ZoneEncryptionInfoProto proto = 306 PBHelper.convert(suite, version, keyName); 307 final XAttr ezXAttr = XAttrHelper 308 .buildXAttr(CRYPTO_XATTR_ENCRYPTION_ZONE, proto.toByteArray()); 309 310 final List<XAttr> xattrs = Lists.newArrayListWithCapacity(1); 311 xattrs.add(ezXAttr); 312 // updating the xattr will call addEncryptionZone, 313 // done this way to handle edit log loading 314 dir.unprotectedSetXAttrs(src, xattrs, EnumSet.of(XAttrSetFlag.CREATE)); 315 return ezXAttr; 316 } 317 318 /** 319 * Cursor-based listing of encryption zones. 320 * <p/> 321 * Called while holding the FSDirectory lock. 322 */ 323 BatchedListEntries<EncryptionZone> listEncryptionZones(long prevId) 324 throws IOException { 325 assert dir.hasReadLock(); 326 NavigableMap<Long, EncryptionZoneInt> tailMap = encryptionZones.tailMap 327 (prevId, false); 328 final int numResponses = Math.min(maxListEncryptionZonesResponses, 329 tailMap.size()); 330 final List<EncryptionZone> zones = 331 Lists.newArrayListWithExpectedSize(numResponses); 332 333 int count = 0; 334 for (EncryptionZoneInt ezi : tailMap.values()) { 335 /* 336 Skip EZs that are only present in snapshots. Re-resolve the path to 337 see if the path's current inode ID matches EZ map's INode ID. 338 339 INode#getFullPathName simply calls getParent recursively, so will return 340 the INode's parents at the time it was snapshotted. It will not 341 contain a reference INode. 342 */ 343 final String pathName = getFullPathName(ezi); 344 INodesInPath iip = dir.getINodesInPath(pathName, false); 345 INode lastINode = iip.getLastINode(); 346 if (lastINode == null || lastINode.getId() != ezi.getINodeId()) { 347 continue; 348 } 349 // Add the EZ to the result list 350 zones.add(new EncryptionZone(ezi.getINodeId(), pathName, 351 ezi.getSuite(), ezi.getVersion(), ezi.getKeyName())); 352 count++; 353 if (count >= numResponses) { 354 break; 355 } 356 } 357 final boolean hasMore = (numResponses < tailMap.size()); 358 return new BatchedListEntries<EncryptionZone>(zones, hasMore); 359 } 360 }