| /** |
| * 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.security.token.block; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.DataInputStream; |
| import java.io.IOException; |
| import java.security.SecureRandom; |
| import java.util.Arrays; |
| import java.util.EnumSet; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.Map; |
| |
| import org.apache.commons.logging.Log; |
| import org.apache.commons.logging.LogFactory; |
| import org.apache.hadoop.hdfs.protocol.Block; |
| import org.apache.hadoop.io.WritableUtils; |
| import org.apache.hadoop.security.UserGroupInformation; |
| import org.apache.hadoop.security.token.SecretManager; |
| import org.apache.hadoop.security.token.Token; |
| |
| /** |
| * BlockTokenSecretManager can be instantiated in 2 modes, master mode and slave |
| * mode. Master can generate new block keys and export block keys to slaves, |
| * while slaves can only import and use block keys received from master. Both |
| * master and slave can generate and verify block tokens. Typically, master mode |
| * is used by NN and slave mode is used by DN. |
| */ |
| public class BlockTokenSecretManager extends |
| SecretManager<BlockTokenIdentifier> { |
| public static final Log LOG = LogFactory |
| .getLog(BlockTokenSecretManager.class); |
| public static final Token<BlockTokenIdentifier> DUMMY_TOKEN = new Token<BlockTokenIdentifier>(); |
| |
| private final boolean isMaster; |
| /* |
| * keyUpdateInterval is the interval that NN updates its block keys. It should |
| * be set long enough so that all live DN's and Balancer should have sync'ed |
| * their block keys with NN at least once during each interval. |
| */ |
| private final long keyUpdateInterval; |
| private volatile long tokenLifetime; |
| private int serialNo = new SecureRandom().nextInt(); |
| private BlockKey currentKey; |
| private BlockKey nextKey; |
| private Map<Integer, BlockKey> allKeys; |
| |
| public static enum AccessMode { |
| READ, WRITE, COPY, REPLACE |
| }; |
| |
| /** |
| * Constructor |
| * |
| * @param isMaster |
| * @param keyUpdateInterval |
| * @param tokenLifetime |
| * @throws IOException |
| */ |
| public BlockTokenSecretManager(boolean isMaster, long keyUpdateInterval, |
| long tokenLifetime) throws IOException { |
| this.isMaster = isMaster; |
| this.keyUpdateInterval = keyUpdateInterval; |
| this.tokenLifetime = tokenLifetime; |
| this.allKeys = new HashMap<Integer, BlockKey>(); |
| generateKeys(); |
| } |
| |
| /** Initialize block keys */ |
| private synchronized void generateKeys() { |
| if (!isMaster) |
| return; |
| /* |
| * Need to set estimated expiry dates for currentKey and nextKey so that if |
| * NN crashes, DN can still expire those keys. NN will stop using the newly |
| * generated currentKey after the first keyUpdateInterval, however it may |
| * still be used by DN and Balancer to generate new tokens before they get a |
| * chance to sync their keys with NN. Since we require keyUpdInterval to be |
| * long enough so that all live DN's and Balancer will sync their keys with |
| * NN at least once during the period, the estimated expiry date for |
| * currentKey is set to now() + 2 * keyUpdateInterval + tokenLifetime. |
| * Similarly, the estimated expiry date for nextKey is one keyUpdateInterval |
| * more. |
| */ |
| serialNo++; |
| currentKey = new BlockKey(serialNo, System.currentTimeMillis() + 2 |
| * keyUpdateInterval + tokenLifetime, generateSecret()); |
| serialNo++; |
| nextKey = new BlockKey(serialNo, System.currentTimeMillis() + 3 |
| * keyUpdateInterval + tokenLifetime, generateSecret()); |
| allKeys.put(currentKey.getKeyId(), currentKey); |
| allKeys.put(nextKey.getKeyId(), nextKey); |
| } |
| |
| /** Export block keys, only to be used in master mode */ |
| public synchronized ExportedBlockKeys exportKeys() { |
| if (!isMaster) |
| return null; |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Exporting access keys"); |
| return new ExportedBlockKeys(true, keyUpdateInterval, tokenLifetime, |
| currentKey, allKeys.values().toArray(new BlockKey[0])); |
| } |
| |
| private synchronized void removeExpiredKeys() { |
| long now = System.currentTimeMillis(); |
| for (Iterator<Map.Entry<Integer, BlockKey>> it = allKeys.entrySet() |
| .iterator(); it.hasNext();) { |
| Map.Entry<Integer, BlockKey> e = it.next(); |
| if (e.getValue().getExpiryDate() < now) { |
| it.remove(); |
| } |
| } |
| } |
| |
| /** |
| * Set block keys, only to be used in slave mode |
| */ |
| public synchronized void setKeys(ExportedBlockKeys exportedKeys) |
| throws IOException { |
| if (isMaster || exportedKeys == null) |
| return; |
| LOG.info("Setting block keys"); |
| removeExpiredKeys(); |
| this.currentKey = exportedKeys.getCurrentKey(); |
| BlockKey[] receivedKeys = exportedKeys.getAllKeys(); |
| for (int i = 0; i < receivedKeys.length; i++) { |
| if (receivedKeys[i] == null) |
| continue; |
| this.allKeys.put(receivedKeys[i].getKeyId(), receivedKeys[i]); |
| } |
| } |
| |
| /** |
| * Update block keys, only to be used in master mode |
| */ |
| public synchronized void updateKeys() throws IOException { |
| if (!isMaster) |
| return; |
| LOG.info("Updating block keys"); |
| removeExpiredKeys(); |
| // set final expiry date of retiring currentKey |
| allKeys.put(currentKey.getKeyId(), new BlockKey(currentKey.getKeyId(), |
| System.currentTimeMillis() + keyUpdateInterval + tokenLifetime, |
| currentKey.getKey())); |
| // update the estimated expiry date of new currentKey |
| currentKey = new BlockKey(nextKey.getKeyId(), System.currentTimeMillis() |
| + 2 * keyUpdateInterval + tokenLifetime, nextKey.getKey()); |
| allKeys.put(currentKey.getKeyId(), currentKey); |
| // generate a new nextKey |
| serialNo++; |
| nextKey = new BlockKey(serialNo, System.currentTimeMillis() + 3 |
| * keyUpdateInterval + tokenLifetime, generateSecret()); |
| allKeys.put(nextKey.getKeyId(), nextKey); |
| } |
| |
| /** Generate an block token for current user */ |
| public Token<BlockTokenIdentifier> generateToken(Block block, |
| EnumSet<AccessMode> modes) throws IOException { |
| return generateToken(new long [] { block.getBlockId() }, modes); |
| } |
| |
| /** Generate a block token for a specified user */ |
| public Token<BlockTokenIdentifier> generateToken(String userId, Block block, |
| EnumSet<AccessMode> modes) throws IOException { |
| return generateToken(userId, new long [] { block.getBlockId() }, modes); |
| } |
| |
| /** Generate a block token for the current user based on a collection |
| * of blockIds |
| */ |
| public Token<BlockTokenIdentifier> generateToken(long[] blockIds, |
| EnumSet<AccessMode> modes) throws IOException { |
| UserGroupInformation ugi = UserGroupInformation.getCurrentUser(); |
| String userID = (ugi == null ? null : ugi.getShortUserName()); |
| return generateToken(userID, blockIds, modes); |
| } |
| |
| /** Generate a block token based on a collection of blockIds */ |
| public Token<BlockTokenIdentifier> generateToken(String userID, |
| long[] blockIds, EnumSet<AccessMode> modes) { |
| BlockTokenIdentifier id = new BlockTokenIdentifier(userID, blockIds, modes); |
| return new Token<BlockTokenIdentifier>(id, this); |
| } |
| |
| /** |
| * Check if access should be allowed. userID is not checked if null. This |
| * method doesn't check if token password is correct. It should be used only |
| * when token password has already been verified (e.g., in the RPC layer). |
| */ |
| public void checkAccess(BlockTokenIdentifier id, String userId, Block block, |
| AccessMode mode) throws InvalidToken { |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Checking access for user=" + userId + ", block=" + block |
| + ", access mode=" + mode + " using " + id.toString()); |
| } |
| if (userId != null && !userId.equals(id.getUserId())) { |
| throw new InvalidToken("Block token with " + id.toString() |
| + " doesn't belong to user " + userId); |
| } |
| if (!id.isBlockIncluded(block.getBlockId())) { |
| throw new InvalidToken("Block token with " + id.toString() |
| + " doesn't apply to block " + block); |
| } |
| if (isExpired(id.getExpiryDate())) { |
| throw new InvalidToken("Block token with " + id.toString() |
| + " is expired."); |
| } |
| if (!id.getAccessModes().contains(mode)) { |
| throw new InvalidToken("Block token with " + id.toString() |
| + " doesn't have " + mode + " permission"); |
| } |
| } |
| |
| /** Check if access should be allowed. userID is not checked if null */ |
| public void checkAccess(Token<BlockTokenIdentifier> token, String userId, |
| Block block, AccessMode mode) throws InvalidToken { |
| BlockTokenIdentifier id = new BlockTokenIdentifier(); |
| try { |
| id.readFields(new DataInputStream(new ByteArrayInputStream(token |
| .getIdentifier()))); |
| } catch (IOException e) { |
| throw new InvalidToken( |
| "Unable to de-serialize block token identifier for user=" + userId |
| + ", block=" + block + ", access mode=" + mode); |
| } |
| checkAccess(id, userId, block, mode); |
| if (!Arrays.equals(retrievePassword(id), token.getPassword())) { |
| throw new InvalidToken("Block token with " + id.toString() |
| + " doesn't have the correct token password"); |
| } |
| } |
| |
| private static boolean isExpired(long expiryDate) { |
| return System.currentTimeMillis() > expiryDate; |
| } |
| |
| /** |
| * check if a token is expired. for unit test only. return true when token is |
| * expired, false otherwise |
| */ |
| static boolean isTokenExpired(Token<BlockTokenIdentifier> token) |
| throws IOException { |
| ByteArrayInputStream buf = new ByteArrayInputStream(token.getIdentifier()); |
| DataInputStream in = new DataInputStream(buf); |
| long expiryDate = WritableUtils.readVLong(in); |
| return isExpired(expiryDate); |
| } |
| |
| /** set token lifetime. */ |
| public void setTokenLifetime(long tokenLifetime) { |
| this.tokenLifetime = tokenLifetime; |
| } |
| |
| /** |
| * Create an empty block token identifier |
| * |
| * @return a newly created empty block token identifier |
| */ |
| @Override |
| public BlockTokenIdentifier createIdentifier() { |
| return new BlockTokenIdentifier(); |
| } |
| |
| /** |
| * Create a new password/secret for the given block token identifier. |
| * |
| * @param identifier |
| * the block token identifier |
| * @return token password/secret |
| */ |
| @Override |
| protected byte[] createPassword(BlockTokenIdentifier identifier) { |
| BlockKey key = null; |
| synchronized (this) { |
| key = currentKey; |
| } |
| if (key == null) |
| throw new IllegalStateException("currentKey hasn't been initialized."); |
| identifier.setExpiryDate(System.currentTimeMillis() + tokenLifetime); |
| identifier.setKeyId(key.getKeyId()); |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Generating block token for " + identifier.toString()); |
| } |
| return createPassword(identifier.getBytes(), key.getKey()); |
| } |
| |
| /** |
| * Look up the token password/secret for the given block token identifier. |
| * |
| * @param identifier |
| * the block token identifier to look up |
| * @return token password/secret as byte[] |
| * @throws InvalidToken |
| */ |
| @Override |
| public byte[] retrievePassword(BlockTokenIdentifier identifier) |
| throws InvalidToken { |
| if (isExpired(identifier.getExpiryDate())) { |
| throw new InvalidToken("Block token with " + identifier.toString() |
| + " is expired."); |
| } |
| BlockKey key = null; |
| synchronized (this) { |
| key = allKeys.get(identifier.getKeyId()); |
| } |
| if (key == null) { |
| throw new InvalidToken("Can't re-compute password for " |
| + identifier.toString() + ", since the required block key (keyID=" |
| + identifier.getKeyId() + ") doesn't exist."); |
| } |
| return createPassword(identifier.getBytes(), key.getKey()); |
| } |
| } |