blob: aac0952728a7303520444fc387a969be7c098efe [file] [log] [blame]
/**
* 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.classification.InterfaceAudience;
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.
*/
@InterfaceAudience.Private
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.getCurrentUser();
String userID = (ugi == null ? null : ugi.getShortUserName());
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;
}
}