org.apache.hadoop.security.token.delegation.AbstractDelegationTokenSecretManager.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.hadoop.security.token.delegation.AbstractDelegationTokenSecretManager.java

Source

/**
 * 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();
    }

}