org.apache.accumulo.server.security.delegation.AuthenticationTokenSecretManager Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of accumulo-server-base Show documentation
Show all versions of accumulo-server-base Show documentation
A common base library for Apache Accumulo servers.
/*
* 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 {
private static final Logger log = LoggerFactory.getLogger(AuthenticationTokenSecretManager.class);
private final Instance instance;
private final long tokenMaxLifetime;
private final ConcurrentHashMap allKeys = new ConcurrentHashMap();
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,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 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 null != allKeys.remove(keyId);
}
/**
* The current {@link AuthenticationKey}, may be null.
*
* @return The current key, or null.
*/
@VisibleForTesting
AuthenticationKey getCurrentKey() {
return currentKey;
}
@VisibleForTesting
Map 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> iter = allKeys.entrySet().iterator();
while (iter.hasNext()) {
Entry 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);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy