| /** |
| * 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; |
| |
| import java.io.ByteArrayInputStream; |
| import java.io.ByteArrayOutputStream; |
| import java.io.DataInputStream; |
| import java.io.DataOutputStream; |
| import java.io.IOException; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.GeneralSecurityException; |
| import java.security.SecureRandom; |
| import java.util.EnumSet; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.Map; |
| |
| import javax.crypto.KeyGenerator; |
| import javax.crypto.Mac; |
| import javax.crypto.spec.SecretKeySpec; |
| |
| import org.apache.commons.logging.Log; |
| import org.apache.commons.logging.LogFactory; |
| import org.apache.hadoop.hdfs.DFSConfigKeys; |
| import org.apache.hadoop.io.Text; |
| import org.apache.hadoop.io.WritableUtils; |
| import org.apache.hadoop.security.UserGroupInformation; |
| |
| /** |
| * AccessTokenHandler can be instantiated in 2 modes, master mode and slave |
| * mode. Master can generate new access keys and export access keys to slaves, |
| * while slaves can only import and use access keys received from master. Both |
| * master and slave can generate and verify access tokens. Typically, master |
| * mode is used by NN and slave mode is used by DN. |
| */ |
| public class AccessTokenHandler { |
| private static final Log LOG = LogFactory.getLog(AccessTokenHandler.class); |
| |
| private final boolean isMaster; |
| /* |
| * keyUpdateInterval is the interval that NN updates its access keys. It |
| * should be set long enough so that all live DN's and Balancer should have |
| * sync'ed their access keys with NN at least once during each interval. |
| */ |
| private final long keyUpdateInterval; |
| private long tokenLifetime; |
| private long serialNo = new SecureRandom().nextLong(); |
| private KeyGenerator keyGen; |
| private BlockAccessKey currentKey; |
| private BlockAccessKey nextKey; |
| private Map<Long, BlockAccessKey> allKeys; |
| |
| public static enum AccessMode { |
| READ, WRITE, COPY, REPLACE |
| }; |
| |
| /** |
| * Constructor |
| * |
| * @param isMaster |
| * @param keyUpdateInterval |
| * @param tokenLifetime |
| * @throws IOException |
| */ |
| public AccessTokenHandler(boolean isMaster, long keyUpdateInterval, |
| long tokenLifetime) throws IOException { |
| this.isMaster = isMaster; |
| this.keyUpdateInterval = keyUpdateInterval; |
| this.tokenLifetime = tokenLifetime; |
| this.allKeys = new HashMap<Long, BlockAccessKey>(); |
| if (isMaster) { |
| try { |
| generateKeys(); |
| initMac(currentKey); |
| } catch (GeneralSecurityException e) { |
| throw (IOException) new IOException( |
| "Failed to create AccessTokenHandler").initCause(e); |
| } |
| } |
| } |
| |
| /** Initialize access keys */ |
| private synchronized void generateKeys() throws NoSuchAlgorithmException { |
| keyGen = KeyGenerator.getInstance("HmacSHA1"); |
| /* |
| * 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 BlockAccessKey(serialNo, new Text(keyGen.generateKey() |
| .getEncoded()), System.currentTimeMillis() + 2 * keyUpdateInterval |
| + tokenLifetime); |
| serialNo++; |
| nextKey = new BlockAccessKey(serialNo, new Text(keyGen.generateKey() |
| .getEncoded()), System.currentTimeMillis() + 3 * keyUpdateInterval |
| + tokenLifetime); |
| allKeys.put(currentKey.getKeyID(), currentKey); |
| allKeys.put(nextKey.getKeyID(), nextKey); |
| } |
| |
| /** Initialize Mac function */ |
| private synchronized void initMac(BlockAccessKey key) throws IOException { |
| try { |
| Mac mac = Mac.getInstance("HmacSHA1"); |
| mac.init(new SecretKeySpec(key.getKey().getBytes(), "HmacSHA1")); |
| key.setMac(mac); |
| } catch (GeneralSecurityException e) { |
| throw (IOException) new IOException( |
| "Failed to initialize Mac for access key, keyID=" + key.getKeyID()) |
| .initCause(e); |
| } |
| } |
| |
| /** Export access keys, only to be used in master mode */ |
| public synchronized ExportedAccessKeys exportKeys() { |
| if (!isMaster) |
| return null; |
| if (LOG.isDebugEnabled()) |
| LOG.debug("Exporting access keys"); |
| return new ExportedAccessKeys(true, keyUpdateInterval, tokenLifetime, |
| currentKey, allKeys.values().toArray(new BlockAccessKey[0])); |
| } |
| |
| private synchronized void removeExpiredKeys() { |
| long now = System.currentTimeMillis(); |
| for (Iterator<Map.Entry<Long, BlockAccessKey>> it = allKeys.entrySet() |
| .iterator(); it.hasNext();) { |
| Map.Entry<Long, BlockAccessKey> e = it.next(); |
| if (e.getValue().getExpiryDate() < now) { |
| it.remove(); |
| } |
| } |
| } |
| |
| /** |
| * Set access keys, only to be used in slave mode |
| */ |
| public synchronized void setKeys(ExportedAccessKeys exportedKeys) |
| throws IOException { |
| if (isMaster || exportedKeys == null) |
| return; |
| LOG.info("Setting access keys"); |
| removeExpiredKeys(); |
| this.currentKey = exportedKeys.getCurrentKey(); |
| initMac(currentKey); |
| BlockAccessKey[] 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 access keys, only to be used in master mode |
| */ |
| public synchronized void updateKeys() throws IOException { |
| if (!isMaster) |
| return; |
| LOG.info("Updating access keys"); |
| removeExpiredKeys(); |
| // set final expiry date of retiring currentKey |
| allKeys.put(currentKey.getKeyID(), new BlockAccessKey(currentKey.getKeyID(), |
| currentKey.getKey(), System.currentTimeMillis() + keyUpdateInterval |
| + tokenLifetime)); |
| // update the estimated expiry date of new currentKey |
| currentKey = new BlockAccessKey(nextKey.getKeyID(), nextKey.getKey(), System |
| .currentTimeMillis() |
| + 2 * keyUpdateInterval + tokenLifetime); |
| initMac(currentKey); |
| allKeys.put(currentKey.getKeyID(), currentKey); |
| // generate a new nextKey |
| serialNo++; |
| nextKey = new BlockAccessKey(serialNo, new Text(keyGen.generateKey() |
| .getEncoded()), System.currentTimeMillis() + 3 * keyUpdateInterval |
| + tokenLifetime); |
| allKeys.put(nextKey.getKeyID(), nextKey); |
| } |
| |
| /** Check if token is well formed */ |
| private synchronized boolean verifyToken(long keyID, BlockAccessToken token) |
| throws IOException { |
| BlockAccessKey key = allKeys.get(keyID); |
| if (key == null) { |
| LOG.warn("Access key for keyID=" + keyID + " doesn't exist."); |
| return false; |
| } |
| if (key.getMac() == null) { |
| initMac(key); |
| } |
| Text tokenID = token.getTokenID(); |
| Text authenticator = new Text(key.getMac().doFinal(tokenID.getBytes())); |
| return authenticator.equals(token.getTokenAuthenticator()); |
| } |
| |
| /** Generate an access token for current user */ |
| public BlockAccessToken generateToken(long blockID, EnumSet<AccessMode> modes) |
| throws IOException { |
| UserGroupInformation ugi = UserGroupInformation.getCurrentUGI(); |
| String userID = (ugi == null ? null : ugi.getUserName()); |
| return generateToken(userID, blockID, modes); |
| } |
| |
| /** Generate an access token for a specified user */ |
| public synchronized BlockAccessToken generateToken(String userID, long blockID, |
| EnumSet<AccessMode> modes) throws IOException { |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Generating access token for user=" + userID + ", blockID=" |
| + blockID + ", access modes=" + modes + ", keyID=" |
| + currentKey.getKeyID()); |
| } |
| if (modes == null || modes.isEmpty()) |
| throw new IOException("access modes can't be null or empty"); |
| ByteArrayOutputStream buf = new ByteArrayOutputStream(4096); |
| DataOutputStream out = new DataOutputStream(buf); |
| WritableUtils.writeVLong(out, System.currentTimeMillis() + tokenLifetime); |
| WritableUtils.writeVLong(out, currentKey.getKeyID()); |
| WritableUtils.writeString(out, userID); |
| WritableUtils.writeVLong(out, blockID); |
| WritableUtils.writeVInt(out, modes.size()); |
| for (AccessMode aMode : modes) { |
| WritableUtils.writeEnum(out, aMode); |
| } |
| Text tokenID = new Text(buf.toByteArray()); |
| return new BlockAccessToken(tokenID, new Text(currentKey.getMac().doFinal( |
| tokenID.getBytes()))); |
| } |
| |
| /** Check if access should be allowed. userID is not checked if null */ |
| public boolean checkAccess(BlockAccessToken token, String userID, long blockID, |
| AccessMode mode) throws IOException { |
| long oExpiry = 0; |
| long oKeyID = 0; |
| String oUserID = null; |
| long oBlockID = 0; |
| EnumSet<AccessMode> oModes = EnumSet.noneOf(AccessMode.class); |
| |
| try { |
| ByteArrayInputStream buf = new ByteArrayInputStream(token.getTokenID() |
| .getBytes()); |
| DataInputStream in = new DataInputStream(buf); |
| oExpiry = WritableUtils.readVLong(in); |
| oKeyID = WritableUtils.readVLong(in); |
| oUserID = WritableUtils.readString(in); |
| oBlockID = WritableUtils.readVLong(in); |
| int length = WritableUtils.readVInt(in); |
| for (int i = 0; i < length; ++i) { |
| oModes.add(WritableUtils.readEnum(in, AccessMode.class)); |
| } |
| } catch (IOException e) { |
| throw (IOException) new IOException( |
| "Unable to parse access token for user=" + userID + ", blockID=" |
| + blockID + ", access mode=" + mode).initCause(e); |
| } |
| if (LOG.isDebugEnabled()) { |
| LOG.debug("Verifying access token for user=" + userID + ", blockID=" |
| + blockID + ", access mode=" + mode + ", keyID=" + oKeyID); |
| } |
| return (userID == null || userID.equals(oUserID)) && oBlockID == blockID |
| && !isExpired(oExpiry) && oModes.contains(mode) |
| && verifyToken(oKeyID, token); |
| } |
| |
| 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(BlockAccessToken token) throws IOException { |
| ByteArrayInputStream buf = new ByteArrayInputStream(token.getTokenID() |
| .getBytes()); |
| DataInputStream in = new DataInputStream(buf); |
| long expiryDate = WritableUtils.readVLong(in); |
| return isExpired(expiryDate); |
| } |
| |
| /** set token lifetime. for unit test only */ |
| synchronized void setTokenLifetime(long tokenLifetime) { |
| this.tokenLifetime = tokenLifetime; |
| } |
| } |