org.apache.accumulo.server.security.delegation.AuthenticationTokenSecretManager.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.accumulo.server.security.delegation.AuthenticationTokenSecretManager.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.accumulo.server.security.delegation;

import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;

import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

import javax.crypto.SecretKey;

import org.apache.accumulo.core.client.AccumuloException;
import org.apache.accumulo.core.client.Instance;
import org.apache.accumulo.core.client.admin.DelegationTokenConfig;
import org.apache.accumulo.core.client.impl.AuthenticationTokenIdentifier;
import org.apache.accumulo.core.client.impl.DelegationTokenImpl;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.security.token.SecretManager;
import org.apache.hadoop.security.token.Token;
import org.apache.zookeeper.KeeperException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.Maps;

/**
 * Manages an internal list of secret keys used to sign new authentication tokens as they are generated, and to validate existing tokens used for
 * authentication.
 *
 * Each TabletServer, in addition to the Master, has an instance of this {@link SecretManager} so that each can authenticate requests from clients presenting
 * delegation tokens. The Master will also run an instance of {@link AuthenticationTokenKeyManager} which handles generation of new keys and removal of old
 * keys. That class will call the methods here to ensure the in-memory cache is consistent with what is advertised in ZooKeeper.
 */
public class AuthenticationTokenSecretManager extends SecretManager<AuthenticationTokenIdentifier> {

    private static final Logger log = LoggerFactory.getLogger(AuthenticationTokenSecretManager.class);

    private final Instance instance;
    private final long tokenMaxLifetime;
    private final ConcurrentHashMap<Integer, AuthenticationKey> allKeys = new ConcurrentHashMap<Integer, AuthenticationKey>();
    private AuthenticationKey currentKey;

    /**
     * Create a new secret manager instance for generating keys.
     *
     * @param instance
     *          Accumulo instance
     * @param tokenMaxLifetime
     *          Maximum age (in milliseconds) before a token expires and is no longer valid
     */
    public AuthenticationTokenSecretManager(Instance instance, long tokenMaxLifetime) {
        requireNonNull(instance);
        checkArgument(tokenMaxLifetime > 0, "Max lifetime must be positive");
        this.instance = instance;
        this.tokenMaxLifetime = tokenMaxLifetime;
    }

    @Override
    protected byte[] createPassword(AuthenticationTokenIdentifier identifier) {
        DelegationTokenConfig cfg = identifier.getConfig();

        long now = System.currentTimeMillis();
        final AuthenticationKey secretKey;
        synchronized (this) {
            secretKey = currentKey;
        }
        identifier.setKeyId(secretKey.getKeyId());
        identifier.setIssueDate(now);
        long expiration = now + tokenMaxLifetime;
        // Catch overflow
        if (expiration < now) {
            expiration = Long.MAX_VALUE;
        }
        identifier.setExpirationDate(expiration);

        // Limit the lifetime if the user requests it
        if (null != cfg) {
            long requestedLifetime = cfg.getTokenLifetime(TimeUnit.MILLISECONDS);
            if (0 < requestedLifetime) {
                long requestedExpirationDate = identifier.getIssueDate() + requestedLifetime;
                // Catch overflow again
                if (requestedExpirationDate < identifier.getIssueDate()) {
                    requestedExpirationDate = Long.MAX_VALUE;
                }
                // Ensure that the user doesn't try to extend the expiration date -- they may only limit it
                if (requestedExpirationDate > identifier.getExpirationDate()) {
                    throw new RuntimeException("Requested token lifetime exceeds configured maximum");
                }
                log.trace("Overriding token expiration date from {} to {}", identifier.getExpirationDate(),
                        requestedExpirationDate);
                identifier.setExpirationDate(requestedExpirationDate);
            }
        }

        identifier.setInstanceId(instance.getInstanceID());
        return createPassword(identifier.getBytes(), secretKey.getKey());
    }

    @Override
    public byte[] retrievePassword(AuthenticationTokenIdentifier identifier) throws InvalidToken {
        long now = System.currentTimeMillis();
        if (identifier.getExpirationDate() < now) {
            throw new InvalidToken("Token has expired");
        }
        if (identifier.getIssueDate() > now) {
            throw new InvalidToken("Token issued in the future");
        }
        AuthenticationKey masterKey = allKeys.get(identifier.getKeyId());
        if (masterKey == null) {
            throw new InvalidToken("Unknown master key for token (id=" + identifier.getKeyId() + ")");
        }
        // regenerate the password
        return createPassword(identifier.getBytes(), masterKey.getKey());
    }

    @Override
    public AuthenticationTokenIdentifier createIdentifier() {
        // Return our TokenIdentifier implementation
        return new AuthenticationTokenIdentifier();
    }

    /**
     * Generates a delegation token for the user with the provided {@code username}.
     *
     * @param username
     *          The client to generate the delegation token for.
     * @param cfg
     *          A configuration object for obtaining the delegation token
     * @return A delegation token for {@code username} created using the {@link #currentKey}.
     */
    public Entry<Token<AuthenticationTokenIdentifier>, AuthenticationTokenIdentifier> generateToken(String username,
            DelegationTokenConfig cfg) throws AccumuloException {
        requireNonNull(username);
        requireNonNull(cfg);

        final AuthenticationTokenIdentifier id = new AuthenticationTokenIdentifier(username, cfg);

        final StringBuilder svcName = new StringBuilder(DelegationTokenImpl.SERVICE_NAME);
        if (null != id.getInstanceId()) {
            svcName.append("-").append(id.getInstanceId());
        }
        // Create password will update the state on the identifier given currentKey. Need to call this before serializing the identifier
        byte[] password;
        try {
            password = createPassword(id);
        } catch (RuntimeException e) {
            throw new AccumuloException(e.getMessage());
        }
        // The use of the ServiceLoader inside Token doesn't work to automatically get the Identifier
        // Explicitly returning the identifier also saves an extra deserialization
        Token<AuthenticationTokenIdentifier> token = new Token<AuthenticationTokenIdentifier>(id.getBytes(),
                password, id.getKind(), new Text(svcName.toString()));
        return Maps.immutableEntry(token, id);
    }

    /**
     * Add the provided {@code key} to the in-memory copy of all {@link AuthenticationKey}s.
     *
     * @param key
     *          The key to add.
     */
    public synchronized void addKey(AuthenticationKey key) {
        requireNonNull(key);

        log.debug("Adding AuthenticationKey with keyId {}", key.getKeyId());

        allKeys.put(key.getKeyId(), key);
        if (currentKey == null || key.getKeyId() > currentKey.getKeyId()) {
            currentKey = key;
        }
    }

    /**
     * Removes the {@link AuthenticationKey} from the local cache of keys using the provided {@code keyId}.
     *
     * @param keyId
     *          The unique ID for the {@link AuthenticationKey} to remove.
     * @return True if the key was removed, otherwise false.
     */
    synchronized boolean removeKey(Integer keyId) {
        requireNonNull(keyId);

        log.debug("Removing AuthenticatioKey with keyId {}", keyId);

        return null != allKeys.remove(keyId);
    }

    /**
     * The current {@link AuthenticationKey}, may be null.
     *
     * @return The current key, or null.
     */
    @VisibleForTesting
    AuthenticationKey getCurrentKey() {
        return currentKey;
    }

    @VisibleForTesting
    Map<Integer, AuthenticationKey> getKeys() {
        return allKeys;
    }

    /**
     * Inspect each key cached in {@link #allKeys} and remove it if the expiration date has passed. For each removed local {@link AuthenticationKey}, the key is
     * also removed from ZooKeeper using the provided {@code keyDistributor} instance.
     *
     * @param keyDistributor
     *          ZooKeeper key distribution class
     */
    synchronized int removeExpiredKeys(ZooAuthenticationKeyDistributor keyDistributor) {
        long now = System.currentTimeMillis();
        int keysRemoved = 0;
        Iterator<Entry<Integer, AuthenticationKey>> iter = allKeys.entrySet().iterator();
        while (iter.hasNext()) {
            Entry<Integer, AuthenticationKey> entry = iter.next();
            AuthenticationKey key = entry.getValue();
            if (key.getExpirationDate() < now) {
                log.debug("Removing expired delegation token key {}", key.getKeyId());
                iter.remove();
                keysRemoved++;
                try {
                    keyDistributor.remove(key);
                } catch (KeeperException | InterruptedException e) {
                    log.error("Failed to remove AuthenticationKey from ZooKeeper. Exiting", e);
                    throw new RuntimeException(e);
                }
            }
        }
        return keysRemoved;
    }

    synchronized boolean isCurrentKeySet() {
        return null != currentKey;
    }

    /**
     * Atomic operation to remove all AuthenticationKeys
     */
    public synchronized void removeAllKeys() {
        allKeys.clear();
        currentKey = null;
    }

    @Override
    protected SecretKey generateSecret() {
        // Method in the parent is a different package, provide the explicit override so we can use it directly in our package.
        return super.generateSecret();
    }

    public static SecretKey createSecretKey(byte[] raw) {
        return SecretManager.createSecretKey(raw);
    }
}