blob: f7001584ee74235d09f86a249f593a68e7b05a5e [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.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.admin.DelegationTokenConfig;
import org.apache.accumulo.core.clientImpl.AuthenticationTokenIdentifier;
import org.apache.accumulo.core.clientImpl.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 Manager, has an instance of this {@link SecretManager} so
* that each can authenticate requests from clients presenting delegation tokens. The Manager 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 String instanceID;
private final long tokenMaxLifetime;
private final ConcurrentHashMap<Integer,AuthenticationKey> allKeys = new ConcurrentHashMap<>();
private AuthenticationKey currentKey;
/**
* Create a new secret manager instance for generating keys.
*
* @param instanceID
* Accumulo instance ID
* @param tokenMaxLifetime
* Maximum age (in milliseconds) before a token expires and is no longer valid
*/
public AuthenticationTokenSecretManager(String instanceID, long tokenMaxLifetime) {
requireNonNull(instanceID);
checkArgument(tokenMaxLifetime > 0, "Max lifetime must be positive");
this.instanceID = instanceID;
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 (cfg != null) {
long requestedLifetime = cfg.getTokenLifetime(TimeUnit.MILLISECONDS);
if (requestedLifetime > 0) {
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(instanceID);
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 managerKey = allKeys.get(identifier.getKeyId());
if (managerKey == null) {
throw new InvalidToken("Unknown manager key for token (id=" + identifier.getKeyId() + ")");
}
// regenerate the password
return createPassword(identifier.getBytes(), managerKey.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 (id.getInstanceId() != null) {
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<>(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 allKeys.remove(keyId) != null;
}
/**
* 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 currentKey != null;
}
/**
* 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);
}
}