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 */
018package org.apache.hadoop.hdfs.server.namenode;
019
020import java.io.IOException;
021import java.util.EnumSet;
022import java.util.List;
023import java.util.NavigableMap;
024import java.util.TreeMap;
025
026import com.google.common.base.Preconditions;
027import com.google.common.collect.Lists;
028import org.apache.hadoop.conf.Configuration;
029import org.apache.hadoop.crypto.CipherSuite;
030import org.apache.hadoop.crypto.CryptoProtocolVersion;
031import org.apache.hadoop.fs.UnresolvedLinkException;
032import org.apache.hadoop.fs.XAttr;
033import org.apache.hadoop.fs.XAttrSetFlag;
034import org.apache.hadoop.hdfs.DFSConfigKeys;
035import org.apache.hadoop.hdfs.XAttrHelper;
036import org.apache.hadoop.hdfs.protocol.EncryptionZone;
037import org.apache.hadoop.hdfs.protocol.SnapshotAccessControlException;
038import org.apache.hadoop.hdfs.protocol.proto.HdfsProtos;
039import org.apache.hadoop.hdfs.protocolPB.PBHelper;
040import org.slf4j.Logger;
041import org.slf4j.LoggerFactory;
042
043
044import static org.apache.hadoop.fs.BatchedRemoteIterator.BatchedListEntries;
045import 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 */
055public 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    List<INode> inodes = iip.getReadOnlyINodes();
203    for (int i = inodes.size() - 1; i >= 0; i--) {
204      final INode inode = inodes.get(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        if (srcEZI.getINodeId() == srcIIP.getLastINode().getId()) {
253          // src is ez root and dest is not in an ez. Allow the rename.
254          return;
255        }
256        throw new IOException(
257            src + " can't be moved from an encryption zone.");
258      }
259    } else {
260      if (dstInEZ) {
261        throw new IOException(
262            src + " can't be moved into an encryption zone.");
263      }
264    }
265
266    if (srcInEZ) {
267      if (srcEZI != dstEZI) {
268        final String srcEZPath = getFullPathName(srcEZI);
269        final String dstEZPath = getFullPathName(dstEZI);
270        final StringBuilder sb = new StringBuilder(src);
271        sb.append(" can't be moved from encryption zone ");
272        sb.append(srcEZPath);
273        sb.append(" to encryption zone ");
274        sb.append(dstEZPath);
275        sb.append(".");
276        throw new IOException(sb.toString());
277      }
278    }
279  }
280
281  /**
282   * Create a new encryption zone.
283   * <p/>
284   * Called while holding the FSDirectory lock.
285   */
286  XAttr createEncryptionZone(String src, CipherSuite suite,
287      CryptoProtocolVersion version, String keyName)
288      throws IOException {
289    assert dir.hasWriteLock();
290    final INodesInPath srcIIP = dir.getINodesInPath4Write(src, false);
291    if (dir.isNonEmptyDirectory(srcIIP)) {
292      throw new IOException(
293          "Attempt to create an encryption zone for a non-empty directory.");
294    }
295
296    if (srcIIP != null &&
297        srcIIP.getLastINode() != null &&
298        !srcIIP.getLastINode().isDirectory()) {
299      throw new IOException("Attempt to create an encryption zone for a file.");
300    }
301    EncryptionZoneInt ezi = getEncryptionZoneForPath(srcIIP);
302    if (ezi != null) {
303      throw new IOException("Directory " + src + " is already in an " +
304          "encryption zone. (" + getFullPathName(ezi) + ")");
305    }
306
307    final HdfsProtos.ZoneEncryptionInfoProto proto =
308        PBHelper.convert(suite, version, keyName);
309    final XAttr ezXAttr = XAttrHelper
310        .buildXAttr(CRYPTO_XATTR_ENCRYPTION_ZONE, proto.toByteArray());
311
312    final List<XAttr> xattrs = Lists.newArrayListWithCapacity(1);
313    xattrs.add(ezXAttr);
314    // updating the xattr will call addEncryptionZone,
315    // done this way to handle edit log loading
316    FSDirXAttrOp.unprotectedSetXAttrs(dir, src, xattrs,
317                                      EnumSet.of(XAttrSetFlag.CREATE));
318    return ezXAttr;
319  }
320
321  /**
322   * Cursor-based listing of encryption zones.
323   * <p/>
324   * Called while holding the FSDirectory lock.
325   */
326  BatchedListEntries<EncryptionZone> listEncryptionZones(long prevId)
327      throws IOException {
328    assert dir.hasReadLock();
329    NavigableMap<Long, EncryptionZoneInt> tailMap = encryptionZones.tailMap
330        (prevId, false);
331    final int numResponses = Math.min(maxListEncryptionZonesResponses,
332        tailMap.size());
333    final List<EncryptionZone> zones =
334        Lists.newArrayListWithExpectedSize(numResponses);
335
336    int count = 0;
337    for (EncryptionZoneInt ezi : tailMap.values()) {
338      /*
339       Skip EZs that are only present in snapshots. Re-resolve the path to 
340       see if the path's current inode ID matches EZ map's INode ID.
341       
342       INode#getFullPathName simply calls getParent recursively, so will return
343       the INode's parents at the time it was snapshotted. It will not 
344       contain a reference INode.
345      */
346      final String pathName = getFullPathName(ezi);
347      INodesInPath iip = dir.getINodesInPath(pathName, false);
348      INode lastINode = iip.getLastINode();
349      if (lastINode == null || lastINode.getId() != ezi.getINodeId()) {
350        continue;
351      }
352      // Add the EZ to the result list
353      zones.add(new EncryptionZone(ezi.getINodeId(), pathName,
354          ezi.getSuite(), ezi.getVersion(), ezi.getKeyName()));
355      count++;
356      if (count >= numResponses) {
357        break;
358      }
359    }
360    final boolean hasMore = (numResponses < tailMap.size());
361    return new BatchedListEntries<EncryptionZone>(zones, hasMore);
362  }
363}