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
019package org.apache.hadoop.hdfs.security.token.block;
020
021import java.io.ByteArrayInputStream;
022import java.io.DataInputStream;
023import java.io.IOException;
024import java.security.SecureRandom;
025import java.util.Arrays;
026import java.util.EnumSet;
027import java.util.HashMap;
028import java.util.Iterator;
029import java.util.Map;
030
031import org.apache.commons.logging.Log;
032import org.apache.commons.logging.LogFactory;
033import org.apache.hadoop.classification.InterfaceAudience;
034import org.apache.hadoop.hdfs.protocol.ExtendedBlock;
035import org.apache.hadoop.io.WritableUtils;
036import org.apache.hadoop.security.UserGroupInformation;
037import org.apache.hadoop.security.token.SecretManager;
038import org.apache.hadoop.security.token.Token;
039
040/**
041 * BlockTokenSecretManager can be instantiated in 2 modes, master mode and slave
042 * mode. Master can generate new block keys and export block keys to slaves,
043 * while slaves can only import and use block keys received from master. Both
044 * master and slave can generate and verify block tokens. Typically, master mode
045 * is used by NN and slave mode is used by DN.
046 */
047@InterfaceAudience.Private
048public class BlockTokenSecretManager extends
049    SecretManager<BlockTokenIdentifier> {
050  public static final Log LOG = LogFactory
051      .getLog(BlockTokenSecretManager.class);
052  public static final Token<BlockTokenIdentifier> DUMMY_TOKEN = new Token<BlockTokenIdentifier>();
053
054  private final boolean isMaster;
055  /**
056   * keyUpdateInterval is the interval that NN updates its block keys. It should
057   * be set long enough so that all live DN's and Balancer should have sync'ed
058   * their block keys with NN at least once during each interval.
059   */
060  private final long keyUpdateInterval;
061  private volatile long tokenLifetime;
062  private int serialNo = new SecureRandom().nextInt();
063  private BlockKey currentKey;
064  private BlockKey nextKey;
065  private Map<Integer, BlockKey> allKeys;
066
067  public static enum AccessMode {
068    READ, WRITE, COPY, REPLACE
069  };
070
071  /**
072   * Constructor
073   * 
074   * @param isMaster
075   * @param keyUpdateInterval
076   * @param tokenLifetime
077   * @throws IOException
078   */
079  public BlockTokenSecretManager(boolean isMaster, long keyUpdateInterval,
080      long tokenLifetime) throws IOException {
081    this.isMaster = isMaster;
082    this.keyUpdateInterval = keyUpdateInterval;
083    this.tokenLifetime = tokenLifetime;
084    this.allKeys = new HashMap<Integer, BlockKey>();
085    generateKeys();
086  }
087
088  /** Initialize block keys */
089  private synchronized void generateKeys() {
090    if (!isMaster)
091      return;
092    /*
093     * Need to set estimated expiry dates for currentKey and nextKey so that if
094     * NN crashes, DN can still expire those keys. NN will stop using the newly
095     * generated currentKey after the first keyUpdateInterval, however it may
096     * still be used by DN and Balancer to generate new tokens before they get a
097     * chance to sync their keys with NN. Since we require keyUpdInterval to be
098     * long enough so that all live DN's and Balancer will sync their keys with
099     * NN at least once during the period, the estimated expiry date for
100     * currentKey is set to now() + 2 * keyUpdateInterval + tokenLifetime.
101     * Similarly, the estimated expiry date for nextKey is one keyUpdateInterval
102     * more.
103     */
104    serialNo++;
105    currentKey = new BlockKey(serialNo, System.currentTimeMillis() + 2
106        * keyUpdateInterval + tokenLifetime, generateSecret());
107    serialNo++;
108    nextKey = new BlockKey(serialNo, System.currentTimeMillis() + 3
109        * keyUpdateInterval + tokenLifetime, generateSecret());
110    allKeys.put(currentKey.getKeyId(), currentKey);
111    allKeys.put(nextKey.getKeyId(), nextKey);
112  }
113
114  /** Export block keys, only to be used in master mode */
115  public synchronized ExportedBlockKeys exportKeys() {
116    if (!isMaster)
117      return null;
118    if (LOG.isDebugEnabled())
119      LOG.debug("Exporting access keys");
120    return new ExportedBlockKeys(true, keyUpdateInterval, tokenLifetime,
121        currentKey, allKeys.values().toArray(new BlockKey[0]));
122  }
123
124  private synchronized void removeExpiredKeys() {
125    long now = System.currentTimeMillis();
126    for (Iterator<Map.Entry<Integer, BlockKey>> it = allKeys.entrySet()
127        .iterator(); it.hasNext();) {
128      Map.Entry<Integer, BlockKey> e = it.next();
129      if (e.getValue().getExpiryDate() < now) {
130        it.remove();
131      }
132    }
133  }
134
135  /**
136   * Set block keys, only to be used in slave mode
137   */
138  public synchronized void setKeys(ExportedBlockKeys exportedKeys)
139      throws IOException {
140    if (isMaster || exportedKeys == null)
141      return;
142    LOG.info("Setting block keys");
143    removeExpiredKeys();
144    this.currentKey = exportedKeys.getCurrentKey();
145    BlockKey[] receivedKeys = exportedKeys.getAllKeys();
146    for (int i = 0; i < receivedKeys.length; i++) {
147      if (receivedKeys[i] == null)
148        continue;
149      this.allKeys.put(receivedKeys[i].getKeyId(), receivedKeys[i]);
150    }
151  }
152
153  /**
154   * Update block keys if update time > update interval.
155   * @return true if the keys are updated.
156   */
157  public boolean updateKeys(final long updateTime) throws IOException {
158    if (updateTime > keyUpdateInterval) {
159      return updateKeys();
160    }
161    return false;
162  }
163
164  /**
165   * Update block keys, only to be used in master mode
166   */
167  synchronized boolean updateKeys() throws IOException {
168    if (!isMaster)
169      return false;
170
171    LOG.info("Updating block keys");
172    removeExpiredKeys();
173    // set final expiry date of retiring currentKey
174    allKeys.put(currentKey.getKeyId(), new BlockKey(currentKey.getKeyId(),
175        System.currentTimeMillis() + keyUpdateInterval + tokenLifetime,
176        currentKey.getKey()));
177    // update the estimated expiry date of new currentKey
178    currentKey = new BlockKey(nextKey.getKeyId(), System.currentTimeMillis()
179        + 2 * keyUpdateInterval + tokenLifetime, nextKey.getKey());
180    allKeys.put(currentKey.getKeyId(), currentKey);
181    // generate a new nextKey
182    serialNo++;
183    nextKey = new BlockKey(serialNo, System.currentTimeMillis() + 3
184        * keyUpdateInterval + tokenLifetime, generateSecret());
185    allKeys.put(nextKey.getKeyId(), nextKey);
186    return true;
187  }
188
189  /** Generate an block token for current user */
190  public Token<BlockTokenIdentifier> generateToken(ExtendedBlock block,
191      EnumSet<AccessMode> modes) throws IOException {
192    UserGroupInformation ugi = UserGroupInformation.getCurrentUser();
193    String userID = (ugi == null ? null : ugi.getShortUserName());
194    return generateToken(userID, block, modes);
195  }
196
197  /** Generate a block token for a specified user */
198  public Token<BlockTokenIdentifier> generateToken(String userId,
199      ExtendedBlock block, EnumSet<AccessMode> modes) throws IOException {
200    BlockTokenIdentifier id = new BlockTokenIdentifier(userId, block
201        .getBlockPoolId(), block.getBlockId(), modes);
202    return new Token<BlockTokenIdentifier>(id, this);
203  }
204
205  /**
206   * Check if access should be allowed. userID is not checked if null. This
207   * method doesn't check if token password is correct. It should be used only
208   * when token password has already been verified (e.g., in the RPC layer).
209   */
210  public void checkAccess(BlockTokenIdentifier id, String userId,
211      ExtendedBlock block, AccessMode mode) throws InvalidToken {
212    if (LOG.isDebugEnabled()) {
213      LOG.debug("Checking access for user=" + userId + ", block=" + block
214          + ", access mode=" + mode + " using " + id.toString());
215    }
216    if (userId != null && !userId.equals(id.getUserId())) {
217      throw new InvalidToken("Block token with " + id.toString()
218          + " doesn't belong to user " + userId);
219    }
220    if (!id.getBlockPoolId().equals(block.getBlockPoolId())) {
221      throw new InvalidToken("Block token with " + id.toString()
222          + " doesn't apply to block " + block);
223    }
224    if (id.getBlockId() != block.getBlockId()) {
225      throw new InvalidToken("Block token with " + id.toString()
226          + " doesn't apply to block " + block);
227    }
228    if (isExpired(id.getExpiryDate())) {
229      throw new InvalidToken("Block token with " + id.toString()
230          + " is expired.");
231    }
232    if (!id.getAccessModes().contains(mode)) {
233      throw new InvalidToken("Block token with " + id.toString()
234          + " doesn't have " + mode + " permission");
235    }
236  }
237
238  /** Check if access should be allowed. userID is not checked if null */
239  public void checkAccess(Token<BlockTokenIdentifier> token, String userId,
240      ExtendedBlock block, AccessMode mode) throws InvalidToken {
241    BlockTokenIdentifier id = new BlockTokenIdentifier();
242    try {
243      id.readFields(new DataInputStream(new ByteArrayInputStream(token
244          .getIdentifier())));
245    } catch (IOException e) {
246      throw new InvalidToken(
247          "Unable to de-serialize block token identifier for user=" + userId
248              + ", block=" + block + ", access mode=" + mode);
249    }
250    checkAccess(id, userId, block, mode);
251    if (!Arrays.equals(retrievePassword(id), token.getPassword())) {
252      throw new InvalidToken("Block token with " + id.toString()
253          + " doesn't have the correct token password");
254    }
255  }
256
257  private static boolean isExpired(long expiryDate) {
258    return System.currentTimeMillis() > expiryDate;
259  }
260
261  /**
262   * check if a token is expired. for unit test only. return true when token is
263   * expired, false otherwise
264   */
265  static boolean isTokenExpired(Token<BlockTokenIdentifier> token)
266      throws IOException {
267    ByteArrayInputStream buf = new ByteArrayInputStream(token.getIdentifier());
268    DataInputStream in = new DataInputStream(buf);
269    long expiryDate = WritableUtils.readVLong(in);
270    return isExpired(expiryDate);
271  }
272
273  /** set token lifetime. */
274  public void setTokenLifetime(long tokenLifetime) {
275    this.tokenLifetime = tokenLifetime;
276  }
277
278  /**
279   * Create an empty block token identifier
280   * 
281   * @return a newly created empty block token identifier
282   */
283  @Override
284  public BlockTokenIdentifier createIdentifier() {
285    return new BlockTokenIdentifier();
286  }
287
288  /**
289   * Create a new password/secret for the given block token identifier.
290   * 
291   * @param identifier
292   *          the block token identifier
293   * @return token password/secret
294   */
295  @Override
296  protected byte[] createPassword(BlockTokenIdentifier identifier) {
297    BlockKey key = null;
298    synchronized (this) {
299      key = currentKey;
300    }
301    if (key == null)
302      throw new IllegalStateException("currentKey hasn't been initialized.");
303    identifier.setExpiryDate(System.currentTimeMillis() + tokenLifetime);
304    identifier.setKeyId(key.getKeyId());
305    if (LOG.isDebugEnabled()) {
306      LOG.debug("Generating block token for " + identifier.toString());
307    }
308    return createPassword(identifier.getBytes(), key.getKey());
309  }
310
311  /**
312   * Look up the token password/secret for the given block token identifier.
313   * 
314   * @param identifier
315   *          the block token identifier to look up
316   * @return token password/secret as byte[]
317   * @throws InvalidToken
318   */
319  @Override
320  public byte[] retrievePassword(BlockTokenIdentifier identifier)
321      throws InvalidToken {
322    if (isExpired(identifier.getExpiryDate())) {
323      throw new InvalidToken("Block token with " + identifier.toString()
324          + " is expired.");
325    }
326    BlockKey key = null;
327    synchronized (this) {
328      key = allKeys.get(identifier.getKeyId());
329    }
330    if (key == null) {
331      throw new InvalidToken("Can't re-compute password for "
332          + identifier.toString() + ", since the required block key (keyID="
333          + identifier.getKeyId() + ") doesn't exist.");
334    }
335    return createPassword(identifier.getBytes(), key.getKey());
336  }
337}