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}