Java tutorial
/** * 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.security.token.delegation; import java.io.ByteArrayInputStream; import java.io.DataInputStream; import java.io.IOException; import java.security.MessageDigest; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import java.util.Set; import javax.crypto.SecretKey; import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceStability; import org.apache.hadoop.io.Text; import org.apache.hadoop.security.AccessControlException; import org.apache.hadoop.security.HadoopKerberosName; import org.apache.hadoop.security.token.SecretManager; import org.apache.hadoop.security.token.Token; import org.apache.hadoop.util.Daemon; import org.apache.hadoop.util.Time; import com.google.common.base.Preconditions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @InterfaceAudience.Public @InterfaceStability.Evolving public abstract class AbstractDelegationTokenSecretManager<TokenIdent extends AbstractDelegationTokenIdentifier> extends SecretManager<TokenIdent> { private static final Logger LOG = LoggerFactory.getLogger(AbstractDelegationTokenSecretManager.class); private String formatTokenId(TokenIdent id) { return "(" + id + ")"; } /** * Cache of currently valid tokens, mapping from DelegationTokenIdentifier * to DelegationTokenInformation. Protected by this object lock. */ protected final Map<TokenIdent, DelegationTokenInformation> currentTokens = new HashMap<TokenIdent, DelegationTokenInformation>(); /** * Sequence number to create DelegationTokenIdentifier. * Protected by this object lock. */ protected int delegationTokenSequenceNumber = 0; /** * Access to allKeys is protected by this object lock */ protected final Map<Integer, DelegationKey> allKeys = new HashMap<Integer, DelegationKey>(); /** * Access to currentId is protected by this object lock. */ protected int currentId = 0; /** * Access to currentKey is protected by this object lock */ private DelegationKey currentKey; private long keyUpdateInterval; private long tokenMaxLifetime; private long tokenRemoverScanInterval; private long tokenRenewInterval; /** * Whether to store a token's tracking ID in its TokenInformation. * Can be overridden by a subclass. */ protected boolean storeTokenTrackingId; private Thread tokenRemoverThread; protected volatile boolean running; /** * If the delegation token update thread holds this lock, it will * not get interrupted. */ protected Object noInterruptsLock = new Object(); /** * Create a secret manager * @param delegationKeyUpdateInterval the number of milliseconds for rolling * new secret keys. * @param delegationTokenMaxLifetime the maximum lifetime of the delegation * tokens in milliseconds * @param delegationTokenRenewInterval how often the tokens must be renewed * in milliseconds * @param delegationTokenRemoverScanInterval how often the tokens are scanned * for expired tokens in milliseconds */ public AbstractDelegationTokenSecretManager(long delegationKeyUpdateInterval, long delegationTokenMaxLifetime, long delegationTokenRenewInterval, long delegationTokenRemoverScanInterval) { this.keyUpdateInterval = delegationKeyUpdateInterval; this.tokenMaxLifetime = delegationTokenMaxLifetime; this.tokenRenewInterval = delegationTokenRenewInterval; this.tokenRemoverScanInterval = delegationTokenRemoverScanInterval; this.storeTokenTrackingId = false; } /** should be called before this object is used */ public void startThreads() throws IOException { Preconditions.checkState(!running); updateCurrentKey(); synchronized (this) { running = true; tokenRemoverThread = new Daemon(new ExpiredTokenRemover()); tokenRemoverThread.start(); } } /** * Reset all data structures and mutable state. */ public synchronized void reset() { setCurrentKeyId(0); allKeys.clear(); setDelegationTokenSeqNum(0); currentTokens.clear(); } /** * Add a previously used master key to cache (when NN restarts), * should be called before activate(). * */ public synchronized void addKey(DelegationKey key) throws IOException { if (running) // a safety check throw new IOException("Can't add delegation key to a running SecretManager."); if (key.getKeyId() > getCurrentKeyId()) { setCurrentKeyId(key.getKeyId()); } allKeys.put(key.getKeyId(), key); } public synchronized DelegationKey[] getAllKeys() { return allKeys.values().toArray(new DelegationKey[0]); } // HDFS protected void logUpdateMasterKey(DelegationKey key) throws IOException { return; } // HDFS protected void logExpireToken(TokenIdent ident) throws IOException { return; } // RM protected void storeNewMasterKey(DelegationKey key) throws IOException { return; } // RM protected void removeStoredMasterKey(DelegationKey key) { return; } // RM protected void storeNewToken(TokenIdent ident, long renewDate) throws IOException { return; } // RM protected void removeStoredToken(TokenIdent ident) throws IOException { } // RM protected void updateStoredToken(TokenIdent ident, long renewDate) throws IOException { return; } /** * For subclasses externalizing the storage, for example Zookeeper * based implementations */ protected synchronized int getCurrentKeyId() { return currentId; } /** * For subclasses externalizing the storage, for example Zookeeper * based implementations */ protected synchronized int incrementCurrentKeyId() { return ++currentId; } /** * For subclasses externalizing the storage, for example Zookeeper * based implementations */ protected synchronized void setCurrentKeyId(int keyId) { currentId = keyId; } /** * For subclasses externalizing the storage, for example Zookeeper * based implementations */ protected synchronized int getDelegationTokenSeqNum() { return delegationTokenSequenceNumber; } /** * For subclasses externalizing the storage, for example Zookeeper * based implementations */ protected synchronized int incrementDelegationTokenSeqNum() { return ++delegationTokenSequenceNumber; } /** * For subclasses externalizing the storage, for example Zookeeper * based implementations */ protected synchronized void setDelegationTokenSeqNum(int seqNum) { delegationTokenSequenceNumber = seqNum; } /** * For subclasses externalizing the storage, for example Zookeeper * based implementations */ protected DelegationKey getDelegationKey(int keyId) { return allKeys.get(keyId); } /** * For subclasses externalizing the storage, for example Zookeeper * based implementations */ protected void storeDelegationKey(DelegationKey key) throws IOException { allKeys.put(key.getKeyId(), key); storeNewMasterKey(key); } /** * For subclasses externalizing the storage, for example Zookeeper * based implementations */ protected void updateDelegationKey(DelegationKey key) throws IOException { allKeys.put(key.getKeyId(), key); } /** * For subclasses externalizing the storage, for example Zookeeper * based implementations */ protected DelegationTokenInformation getTokenInfo(TokenIdent ident) { return currentTokens.get(ident); } /** * For subclasses externalizing the storage, for example Zookeeper * based implementations */ protected void storeToken(TokenIdent ident, DelegationTokenInformation tokenInfo) throws IOException { currentTokens.put(ident, tokenInfo); storeNewToken(ident, tokenInfo.getRenewDate()); } /** * For subclasses externalizing the storage, for example Zookeeper * based implementations */ protected void updateToken(TokenIdent ident, DelegationTokenInformation tokenInfo) throws IOException { currentTokens.put(ident, tokenInfo); updateStoredToken(ident, tokenInfo.getRenewDate()); } /** * This method is intended to be used for recovering persisted delegation * tokens * This method must be called before this secret manager is activated (before * startThreads() is called) * @param identifier identifier read from persistent storage * @param renewDate token renew time * @throws IOException */ public synchronized void addPersistedDelegationToken(TokenIdent identifier, long renewDate) throws IOException { if (running) { // a safety check throw new IOException("Can't add persisted delegation token to a running SecretManager."); } int keyId = identifier.getMasterKeyId(); DelegationKey dKey = allKeys.get(keyId); if (dKey == null) { LOG.warn("No KEY found for persisted identifier " + formatTokenId(identifier)); return; } byte[] password = createPassword(identifier.getBytes(), dKey.getKey()); if (identifier.getSequenceNumber() > getDelegationTokenSeqNum()) { setDelegationTokenSeqNum(identifier.getSequenceNumber()); } if (getTokenInfo(identifier) == null) { currentTokens.put(identifier, new DelegationTokenInformation(renewDate, password, getTrackingIdIfEnabled(identifier))); } else { throw new IOException("Same delegation token being added twice: " + formatTokenId(identifier)); } } /** * Update the current master key * This is called once by startThreads before tokenRemoverThread is created, * and only by tokenRemoverThread afterwards. */ private void updateCurrentKey() throws IOException { LOG.info("Updating the current master key for generating delegation tokens"); /* Create a new currentKey with an estimated expiry date. */ int newCurrentId; synchronized (this) { newCurrentId = incrementCurrentKeyId(); } DelegationKey newKey = new DelegationKey(newCurrentId, System.currentTimeMillis() + keyUpdateInterval + tokenMaxLifetime, generateSecret()); //Log must be invoked outside the lock on 'this' logUpdateMasterKey(newKey); synchronized (this) { currentKey = newKey; storeDelegationKey(currentKey); } } /** * Update the current master key for generating delegation tokens * It should be called only by tokenRemoverThread. */ void rollMasterKey() throws IOException { synchronized (this) { removeExpiredKeys(); /* set final expiry date for retiring currentKey */ currentKey.setExpiryDate(Time.now() + tokenMaxLifetime); /* * currentKey might have been removed by removeExpiredKeys(), if * updateMasterKey() isn't called at expected interval. Add it back to * allKeys just in case. */ updateDelegationKey(currentKey); } updateCurrentKey(); } private synchronized void removeExpiredKeys() { long now = Time.now(); for (Iterator<Map.Entry<Integer, DelegationKey>> it = allKeys.entrySet().iterator(); it.hasNext();) { Map.Entry<Integer, DelegationKey> e = it.next(); if (e.getValue().getExpiryDate() < now) { it.remove(); // ensure the tokens generated by this current key can be recovered // with this current key after this current key is rolled if (!e.getValue().equals(currentKey)) removeStoredMasterKey(e.getValue()); } } } @Override protected synchronized byte[] createPassword(TokenIdent identifier) { int sequenceNum; long now = Time.now(); sequenceNum = incrementDelegationTokenSeqNum(); identifier.setIssueDate(now); identifier.setMaxDate(now + tokenMaxLifetime); identifier.setMasterKeyId(currentKey.getKeyId()); identifier.setSequenceNumber(sequenceNum); LOG.info("Creating password for identifier: " + formatTokenId(identifier) + ", currentKey: " + currentKey.getKeyId()); byte[] password = createPassword(identifier.getBytes(), currentKey.getKey()); DelegationTokenInformation tokenInfo = new DelegationTokenInformation(now + tokenRenewInterval, password, getTrackingIdIfEnabled(identifier)); try { storeToken(identifier, tokenInfo); } catch (IOException ioe) { LOG.error("Could not store token " + formatTokenId(identifier) + "!!", ioe); } return password; } /** * Find the DelegationTokenInformation for the given token id, and verify that * if the token is expired. Note that this method should be called with * acquiring the secret manager's monitor. */ protected DelegationTokenInformation checkToken(TokenIdent identifier) throws InvalidToken { assert Thread.holdsLock(this); DelegationTokenInformation info = getTokenInfo(identifier); if (info == null) { throw new InvalidToken("token " + formatTokenId(identifier) + " can't be found in cache"); } long now = Time.now(); if (info.getRenewDate() < now) { throw new InvalidToken("token " + formatTokenId(identifier) + " is " + "expired, current time: " + Time.formatTime(now) + " expected renewal time: " + Time.formatTime(info.getRenewDate())); } return info; } @Override public synchronized byte[] retrievePassword(TokenIdent identifier) throws InvalidToken { return checkToken(identifier).getPassword(); } protected String getTrackingIdIfEnabled(TokenIdent ident) { if (storeTokenTrackingId) { return ident.getTrackingId(); } return null; } public synchronized String getTokenTrackingId(TokenIdent identifier) { DelegationTokenInformation info = getTokenInfo(identifier); if (info == null) { return null; } return info.getTrackingId(); } /** * Verifies that the given identifier and password are valid and match. * @param identifier Token identifier. * @param password Password in the token. * @throws InvalidToken */ public synchronized void verifyToken(TokenIdent identifier, byte[] password) throws InvalidToken { byte[] storedPassword = retrievePassword(identifier); if (!MessageDigest.isEqual(password, storedPassword)) { throw new InvalidToken("token " + formatTokenId(identifier) + " is invalid, password doesn't match"); } } /** * Renew a delegation token. * @param token the token to renew * @param renewer the full principal name of the user doing the renewal * @return the new expiration time * @throws InvalidToken if the token is invalid * @throws AccessControlException if the user can't renew token */ public synchronized long renewToken(Token<TokenIdent> token, String renewer) throws InvalidToken, IOException { ByteArrayInputStream buf = new ByteArrayInputStream(token.getIdentifier()); DataInputStream in = new DataInputStream(buf); TokenIdent id = createIdentifier(); id.readFields(in); LOG.info("Token renewal for identifier: " + formatTokenId(id) + "; total currentTokens " + currentTokens.size()); long now = Time.now(); if (id.getMaxDate() < now) { throw new InvalidToken( renewer + " tried to renew an expired token " + formatTokenId(id) + " max expiration date: " + Time.formatTime(id.getMaxDate()) + " currentTime: " + Time.formatTime(now)); } if ((id.getRenewer() == null) || (id.getRenewer().toString().isEmpty())) { throw new AccessControlException( renewer + " tried to renew a token " + formatTokenId(id) + " without a renewer"); } if (!id.getRenewer().toString().equals(renewer)) { throw new AccessControlException(renewer + " tries to renew a token " + formatTokenId(id) + " with non-matching renewer " + id.getRenewer()); } DelegationKey key = getDelegationKey(id.getMasterKeyId()); if (key == null) { throw new InvalidToken("Unable to find master key for keyId=" + id.getMasterKeyId() + " from cache. Failed to renew an unexpired token " + formatTokenId(id) + " with sequenceNumber=" + id.getSequenceNumber()); } byte[] password = createPassword(token.getIdentifier(), key.getKey()); if (!MessageDigest.isEqual(password, token.getPassword())) { throw new AccessControlException( renewer + " is trying to renew a token " + formatTokenId(id) + " with wrong password"); } long renewTime = Math.min(id.getMaxDate(), now + tokenRenewInterval); String trackingId = getTrackingIdIfEnabled(id); DelegationTokenInformation info = new DelegationTokenInformation(renewTime, password, trackingId); if (getTokenInfo(id) == null) { throw new InvalidToken("Renewal request for unknown token " + formatTokenId(id)); } updateToken(id, info); return renewTime; } /** * Cancel a token by removing it from cache. * @return Identifier of the canceled token * @throws InvalidToken for invalid token * @throws AccessControlException if the user isn't allowed to cancel */ public synchronized TokenIdent cancelToken(Token<TokenIdent> token, String canceller) throws IOException { ByteArrayInputStream buf = new ByteArrayInputStream(token.getIdentifier()); DataInputStream in = new DataInputStream(buf); TokenIdent id = createIdentifier(); id.readFields(in); LOG.info("Token cancellation requested for identifier: " + formatTokenId(id)); if (id.getUser() == null) { throw new InvalidToken("Token with no owner " + formatTokenId(id)); } String owner = id.getUser().getUserName(); Text renewer = id.getRenewer(); HadoopKerberosName cancelerKrbName = new HadoopKerberosName(canceller); String cancelerShortName = cancelerKrbName.getShortName(); if (!canceller.equals(owner) && (renewer == null || renewer.toString().isEmpty() || !cancelerShortName.equals(renewer.toString()))) { throw new AccessControlException( canceller + " is not authorized to cancel the token " + formatTokenId(id)); } DelegationTokenInformation info = currentTokens.remove(id); if (info == null) { throw new InvalidToken("Token not found " + formatTokenId(id)); } removeStoredToken(id); return id; } /** * Convert the byte[] to a secret key * @param key the byte[] to create the secret key from * @return the secret key */ public static SecretKey createSecretKey(byte[] key) { return SecretManager.createSecretKey(key); } /** Class to encapsulate a token's renew date and password. */ @InterfaceStability.Evolving public static class DelegationTokenInformation { long renewDate; byte[] password; String trackingId; public DelegationTokenInformation(long renewDate, byte[] password) { this(renewDate, password, null); } public DelegationTokenInformation(long renewDate, byte[] password, String trackingId) { this.renewDate = renewDate; this.password = password; this.trackingId = trackingId; } /** returns renew date */ public long getRenewDate() { return renewDate; } /** returns password */ byte[] getPassword() { return password; } /** returns tracking id */ public String getTrackingId() { return trackingId; } } /** Remove expired delegation tokens from cache */ private void removeExpiredToken() throws IOException { long now = Time.now(); Set<TokenIdent> expiredTokens = new HashSet<TokenIdent>(); synchronized (this) { Iterator<Map.Entry<TokenIdent, DelegationTokenInformation>> i = currentTokens.entrySet().iterator(); while (i.hasNext()) { Map.Entry<TokenIdent, DelegationTokenInformation> entry = i.next(); long renewDate = entry.getValue().getRenewDate(); if (renewDate < now) { expiredTokens.add(entry.getKey()); i.remove(); } } } // don't hold lock on 'this' to avoid edit log updates blocking token ops logExpireTokens(expiredTokens); } protected void logExpireTokens(Collection<TokenIdent> expiredTokens) throws IOException { for (TokenIdent ident : expiredTokens) { logExpireToken(ident); LOG.info("Removing expired token " + formatTokenId(ident)); removeStoredToken(ident); } } public void stopThreads() { if (LOG.isDebugEnabled()) LOG.debug("Stopping expired delegation token remover thread"); running = false; if (tokenRemoverThread != null) { synchronized (noInterruptsLock) { tokenRemoverThread.interrupt(); } try { tokenRemoverThread.join(); } catch (InterruptedException e) { throw new RuntimeException("Unable to join on token removal thread", e); } } } /** * is secretMgr running * @return true if secret mgr is running */ public synchronized boolean isRunning() { return running; } private class ExpiredTokenRemover extends Thread { private long lastMasterKeyUpdate; private long lastTokenCacheCleanup; @Override public void run() { LOG.info("Starting expired delegation token remover thread, " + "tokenRemoverScanInterval=" + tokenRemoverScanInterval / (60 * 1000) + " min(s)"); try { while (running) { long now = Time.now(); if (lastMasterKeyUpdate + keyUpdateInterval < now) { try { rollMasterKey(); lastMasterKeyUpdate = now; } catch (IOException e) { LOG.error("Master key updating failed: ", e); } } if (lastTokenCacheCleanup + tokenRemoverScanInterval < now) { removeExpiredToken(); lastTokenCacheCleanup = now; } try { Thread.sleep(Math.min(5000, keyUpdateInterval)); // 5 seconds } catch (InterruptedException ie) { LOG.error("ExpiredTokenRemover received " + ie); } } } catch (Throwable t) { LOG.error("ExpiredTokenRemover thread received unexpected exception", t); Runtime.getRuntime().exit(-1); } } } /** * Decode the token identifier. The subclass can customize the way to decode * the token identifier. * * @param token the token where to extract the identifier * @return the delegation token identifier * @throws IOException */ public TokenIdent decodeTokenIdentifier(Token<TokenIdent> token) throws IOException { return token.decodeIdentifier(); } }