HADOOP-18535. Implement token storage solution based on MySQL
Fixes #1240 Signed-off-by: Owen O'Malley <oomalley@linkedin.com>
This commit is contained in:
parent
11a220c6e7
commit
fb31393b65
@ -19,7 +19,9 @@
|
|||||||
package org.apache.hadoop.security.token.delegation;
|
package org.apache.hadoop.security.token.delegation;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.DataInput;
|
||||||
import java.io.DataInputStream;
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutput;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -41,6 +43,8 @@
|
|||||||
import org.apache.hadoop.fs.statistics.impl.IOStatisticsBinding;
|
import org.apache.hadoop.fs.statistics.impl.IOStatisticsBinding;
|
||||||
import org.apache.hadoop.fs.statistics.impl.IOStatisticsStore;
|
import org.apache.hadoop.fs.statistics.impl.IOStatisticsStore;
|
||||||
import org.apache.hadoop.io.Text;
|
import org.apache.hadoop.io.Text;
|
||||||
|
import org.apache.hadoop.io.Writable;
|
||||||
|
import org.apache.hadoop.io.WritableUtils;
|
||||||
import org.apache.hadoop.metrics2.annotation.Metric;
|
import org.apache.hadoop.metrics2.annotation.Metric;
|
||||||
import org.apache.hadoop.metrics2.annotation.Metrics;
|
import org.apache.hadoop.metrics2.annotation.Metrics;
|
||||||
import org.apache.hadoop.metrics2.lib.DefaultMetricsSystem;
|
import org.apache.hadoop.metrics2.lib.DefaultMetricsSystem;
|
||||||
@ -441,8 +445,9 @@ private void updateCurrentKey() throws IOException {
|
|||||||
/**
|
/**
|
||||||
* Update the current master key for generating delegation tokens
|
* Update the current master key for generating delegation tokens
|
||||||
* It should be called only by tokenRemoverThread.
|
* It should be called only by tokenRemoverThread.
|
||||||
|
* @throws IOException raised on errors performing I/O.
|
||||||
*/
|
*/
|
||||||
void rollMasterKey() throws IOException {
|
protected void rollMasterKey() throws IOException {
|
||||||
synchronized (this) {
|
synchronized (this) {
|
||||||
removeExpiredKeys();
|
removeExpiredKeys();
|
||||||
/* set final expiry date for retiring currentKey */
|
/* set final expiry date for retiring currentKey */
|
||||||
@ -677,11 +682,15 @@ public static SecretKey createSecretKey(byte[] key) {
|
|||||||
|
|
||||||
/** Class to encapsulate a token's renew date and password. */
|
/** Class to encapsulate a token's renew date and password. */
|
||||||
@InterfaceStability.Evolving
|
@InterfaceStability.Evolving
|
||||||
public static class DelegationTokenInformation {
|
public static class DelegationTokenInformation implements Writable {
|
||||||
long renewDate;
|
long renewDate;
|
||||||
byte[] password;
|
byte[] password;
|
||||||
String trackingId;
|
String trackingId;
|
||||||
|
|
||||||
|
public DelegationTokenInformation() {
|
||||||
|
this(0, null);
|
||||||
|
}
|
||||||
|
|
||||||
public DelegationTokenInformation(long renewDate, byte[] password) {
|
public DelegationTokenInformation(long renewDate, byte[] password) {
|
||||||
this(renewDate, password, null);
|
this(renewDate, password, null);
|
||||||
}
|
}
|
||||||
@ -711,6 +720,29 @@ byte[] getPassword() {
|
|||||||
public String getTrackingId() {
|
public String getTrackingId() {
|
||||||
return trackingId;
|
return trackingId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void write(DataOutput out) throws IOException {
|
||||||
|
WritableUtils.writeVLong(out, renewDate);
|
||||||
|
if (password == null) {
|
||||||
|
WritableUtils.writeVInt(out, -1);
|
||||||
|
} else {
|
||||||
|
WritableUtils.writeVInt(out, password.length);
|
||||||
|
out.write(password);
|
||||||
|
}
|
||||||
|
WritableUtils.writeString(out, trackingId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void readFields(DataInput in) throws IOException {
|
||||||
|
renewDate = WritableUtils.readVLong(in);
|
||||||
|
int len = WritableUtils.readVInt(in);
|
||||||
|
if (len > -1) {
|
||||||
|
password = new byte[len];
|
||||||
|
in.readFully(password);
|
||||||
|
}
|
||||||
|
trackingId = WritableUtils.readString(in);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Remove expired delegation tokens from cache */
|
/** Remove expired delegation tokens from cache */
|
||||||
|
@ -0,0 +1,400 @@
|
|||||||
|
/*
|
||||||
|
* 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.ByteArrayOutputStream;
|
||||||
|
import java.io.DataInputStream;
|
||||||
|
import java.io.DataOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import org.apache.hadoop.conf.Configuration;
|
||||||
|
import org.apache.hadoop.security.token.Token;
|
||||||
|
import org.apache.hadoop.security.token.delegation.web.DelegationTokenManager;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of {@link AbstractDelegationTokenSecretManager} that
|
||||||
|
* persists TokenIdentifiers and DelegationKeys in an existing SQL database.
|
||||||
|
*/
|
||||||
|
public abstract class SQLDelegationTokenSecretManager<TokenIdent
|
||||||
|
extends AbstractDelegationTokenIdentifier>
|
||||||
|
extends AbstractDelegationTokenSecretManager<TokenIdent> {
|
||||||
|
|
||||||
|
private static final Logger LOG =
|
||||||
|
LoggerFactory.getLogger(SQLDelegationTokenSecretManager.class);
|
||||||
|
|
||||||
|
public static final String SQL_DTSM_CONF_PREFIX = "sql-dt-secret-manager.";
|
||||||
|
private static final String SQL_DTSM_TOKEN_SEQNUM_BATCH_SIZE = SQL_DTSM_CONF_PREFIX
|
||||||
|
+ "token.seqnum.batch.size";
|
||||||
|
public static final int DEFAULT_SEQ_NUM_BATCH_SIZE = 10;
|
||||||
|
|
||||||
|
// Batch of sequence numbers that will be requested by the sequenceNumCounter.
|
||||||
|
// A new batch is requested once the sequenceNums available to a secret manager are
|
||||||
|
// exhausted, including during initialization.
|
||||||
|
private final int seqNumBatchSize;
|
||||||
|
|
||||||
|
// Last sequenceNum in the current batch that has been allocated to a token.
|
||||||
|
private int currentSeqNum;
|
||||||
|
|
||||||
|
// Max sequenceNum in the current batch that can be allocated to a token.
|
||||||
|
// Unused sequenceNums in the current batch cannot be reused by other routers.
|
||||||
|
private int currentMaxSeqNum;
|
||||||
|
|
||||||
|
public SQLDelegationTokenSecretManager(Configuration conf) {
|
||||||
|
super(conf.getLong(DelegationTokenManager.UPDATE_INTERVAL,
|
||||||
|
DelegationTokenManager.UPDATE_INTERVAL_DEFAULT) * 1000,
|
||||||
|
conf.getLong(DelegationTokenManager.MAX_LIFETIME,
|
||||||
|
DelegationTokenManager.MAX_LIFETIME_DEFAULT) * 1000,
|
||||||
|
conf.getLong(DelegationTokenManager.RENEW_INTERVAL,
|
||||||
|
DelegationTokenManager.RENEW_INTERVAL_DEFAULT) * 1000,
|
||||||
|
conf.getLong(DelegationTokenManager.REMOVAL_SCAN_INTERVAL,
|
||||||
|
DelegationTokenManager.REMOVAL_SCAN_INTERVAL_DEFAULT) * 1000);
|
||||||
|
|
||||||
|
this.seqNumBatchSize = conf.getInt(SQL_DTSM_TOKEN_SEQNUM_BATCH_SIZE,
|
||||||
|
DEFAULT_SEQ_NUM_BATCH_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persists a TokenIdentifier and its corresponding TokenInformation into
|
||||||
|
* the SQL database. The TokenIdentifier is expected to be unique and any
|
||||||
|
* duplicate token attempts will result in an IOException.
|
||||||
|
* @param ident TokenIdentifier to persist.
|
||||||
|
* @param tokenInfo DelegationTokenInformation associated with the TokenIdentifier.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void storeToken(TokenIdent ident,
|
||||||
|
DelegationTokenInformation tokenInfo) throws IOException {
|
||||||
|
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
DataOutputStream dos = new DataOutputStream(bos)) {
|
||||||
|
tokenInfo.write(dos);
|
||||||
|
// Add token to SQL database
|
||||||
|
insertToken(ident.getSequenceNumber(), ident.getBytes(), bos.toByteArray());
|
||||||
|
// Add token to local cache
|
||||||
|
super.storeToken(ident, tokenInfo);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new IOException("Failed to store token in SQL secret manager", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the TokenInformation of an existing TokenIdentifier in
|
||||||
|
* the SQL database.
|
||||||
|
* @param ident Existing TokenIdentifier in the SQL database.
|
||||||
|
* @param tokenInfo Updated DelegationTokenInformation associated with the TokenIdentifier.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void updateToken(TokenIdent ident,
|
||||||
|
DelegationTokenInformation tokenInfo) throws IOException {
|
||||||
|
try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
|
||||||
|
try (DataOutputStream dos = new DataOutputStream(bos)) {
|
||||||
|
tokenInfo.write(dos);
|
||||||
|
// Update token in SQL database
|
||||||
|
updateToken(ident.getSequenceNumber(), ident.getBytes(), bos.toByteArray());
|
||||||
|
// Update token in local cache
|
||||||
|
super.updateToken(ident, tokenInfo);
|
||||||
|
}
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new IOException("Failed to update token in SQL secret manager", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels a token by removing it from the SQL database. This will
|
||||||
|
* call the corresponding method in {@link AbstractDelegationTokenSecretManager}
|
||||||
|
* to perform validation and remove the token from the cache.
|
||||||
|
* @return Identifier of the canceled token
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public synchronized TokenIdent cancelToken(Token<TokenIdent> token,
|
||||||
|
String canceller) throws IOException {
|
||||||
|
try (ByteArrayInputStream bis = new ByteArrayInputStream(token.getIdentifier());
|
||||||
|
DataInputStream din = new DataInputStream(bis)) {
|
||||||
|
TokenIdent id = createIdentifier();
|
||||||
|
id.readFields(din);
|
||||||
|
|
||||||
|
// Calling getTokenInfo to load token into local cache if not present.
|
||||||
|
// super.cancelToken() requires token to be present in local cache.
|
||||||
|
getTokenInfo(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.cancelToken(token, canceller);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the existing TokenInformation from the SQL database to
|
||||||
|
* invalidate it.
|
||||||
|
* @param ident TokenInformation to remove from the SQL database.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void removeStoredToken(TokenIdent ident) throws IOException {
|
||||||
|
try {
|
||||||
|
deleteToken(ident.getSequenceNumber(), ident.getBytes());
|
||||||
|
} catch (SQLException e) {
|
||||||
|
LOG.warn("Failed to remove token in SQL secret manager", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains the DelegationTokenInformation associated with the given
|
||||||
|
* TokenIdentifier in the SQL database.
|
||||||
|
* @param ident Existing TokenIdentifier in the SQL database.
|
||||||
|
* @return DelegationTokenInformation that matches the given TokenIdentifier or
|
||||||
|
* null if it doesn't exist in the database.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected DelegationTokenInformation getTokenInfo(TokenIdent ident) {
|
||||||
|
// Look for token in local cache
|
||||||
|
DelegationTokenInformation tokenInfo = super.getTokenInfo(ident);
|
||||||
|
|
||||||
|
if (tokenInfo == null) {
|
||||||
|
try {
|
||||||
|
// Look for token in SQL database
|
||||||
|
byte[] tokenInfoBytes = selectTokenInfo(ident.getSequenceNumber(), ident.getBytes());
|
||||||
|
|
||||||
|
if (tokenInfoBytes != null) {
|
||||||
|
tokenInfo = new DelegationTokenInformation();
|
||||||
|
try (ByteArrayInputStream bis = new ByteArrayInputStream(tokenInfoBytes)) {
|
||||||
|
try (DataInputStream dis = new DataInputStream(bis)) {
|
||||||
|
tokenInfo.readFields(dis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update token in local cache
|
||||||
|
currentTokens.put(ident, tokenInfo);
|
||||||
|
}
|
||||||
|
} catch (IOException | SQLException e) {
|
||||||
|
LOG.error("Failed to get token in SQL secret manager", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains the value of the last reserved sequence number.
|
||||||
|
* @return Last reserved sequence number.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int getDelegationTokenSeqNum() {
|
||||||
|
try {
|
||||||
|
return selectSequenceNum();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Failed to get token sequence number in SQL secret manager", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the value of the last reserved sequence number.
|
||||||
|
* @param seqNum Value to update the sequence number to.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void setDelegationTokenSeqNum(int seqNum) {
|
||||||
|
try {
|
||||||
|
updateSequenceNum(seqNum);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Failed to update token sequence number in SQL secret manager", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains the next available sequence number that can be allocated to a Token.
|
||||||
|
* Sequence numbers need to be reserved using the shared sequenceNumberCounter once
|
||||||
|
* the local batch has been exhausted, which handles sequenceNumber allocation
|
||||||
|
* concurrently with other secret managers.
|
||||||
|
* This method ensures that sequence numbers are incremental in a single secret manager,
|
||||||
|
* but not across secret managers.
|
||||||
|
* @return Next available sequence number.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public synchronized int incrementDelegationTokenSeqNum() {
|
||||||
|
if (currentSeqNum >= currentMaxSeqNum) {
|
||||||
|
try {
|
||||||
|
// Request a new batch of sequence numbers and use the
|
||||||
|
// lowest one available.
|
||||||
|
currentSeqNum = incrementSequenceNum(seqNumBatchSize);
|
||||||
|
currentMaxSeqNum = currentSeqNum + seqNumBatchSize;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Failed to increment token sequence number in SQL secret manager", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ++currentSeqNum;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Persists a DelegationKey into the SQL database. The delegation keyId
|
||||||
|
* is expected to be unique and any duplicate key attempts will result
|
||||||
|
* in an IOException.
|
||||||
|
* @param key DelegationKey to persist into the SQL database.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void storeDelegationKey(DelegationKey key) throws IOException {
|
||||||
|
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
DataOutputStream dos = new DataOutputStream(bos)) {
|
||||||
|
key.write(dos);
|
||||||
|
// Add delegation key to SQL database
|
||||||
|
insertDelegationKey(key.getKeyId(), bos.toByteArray());
|
||||||
|
// Add delegation key to local cache
|
||||||
|
super.storeDelegationKey(key);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new IOException("Failed to store delegation key in SQL secret manager", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates an existing DelegationKey in the SQL database.
|
||||||
|
* @param key Updated DelegationKey.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void updateDelegationKey(DelegationKey key) throws IOException {
|
||||||
|
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
|
DataOutputStream dos = new DataOutputStream(bos)) {
|
||||||
|
key.write(dos);
|
||||||
|
// Update delegation key in SQL database
|
||||||
|
updateDelegationKey(key.getKeyId(), bos.toByteArray());
|
||||||
|
// Update delegation key in local cache
|
||||||
|
super.updateDelegationKey(key);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new IOException("Failed to update delegation key in SQL secret manager", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the existing DelegationKey from the SQL database to
|
||||||
|
* invalidate it.
|
||||||
|
* @param key DelegationKey to remove from the SQL database.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void removeStoredMasterKey(DelegationKey key) {
|
||||||
|
try {
|
||||||
|
deleteDelegationKey(key.getKeyId());
|
||||||
|
} catch (SQLException e) {
|
||||||
|
LOG.warn("Failed to remove delegation key in SQL secret manager", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains the DelegationKey from the SQL database.
|
||||||
|
* @param keyId KeyId of the DelegationKey to obtain.
|
||||||
|
* @return DelegationKey that matches the given keyId or null
|
||||||
|
* if it doesn't exist in the database.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected DelegationKey getDelegationKey(int keyId) {
|
||||||
|
// Look for delegation key in local cache
|
||||||
|
DelegationKey delegationKey = super.getDelegationKey(keyId);
|
||||||
|
|
||||||
|
if (delegationKey == null) {
|
||||||
|
try {
|
||||||
|
// Look for delegation key in SQL database
|
||||||
|
byte[] delegationKeyBytes = selectDelegationKey(keyId);
|
||||||
|
|
||||||
|
if (delegationKeyBytes != null) {
|
||||||
|
delegationKey = new DelegationKey();
|
||||||
|
try (ByteArrayInputStream bis = new ByteArrayInputStream(delegationKeyBytes)) {
|
||||||
|
try (DataInputStream dis = new DataInputStream(bis)) {
|
||||||
|
delegationKey.readFields(dis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update delegation key in local cache
|
||||||
|
allKeys.put(keyId, delegationKey);
|
||||||
|
}
|
||||||
|
} catch (IOException | SQLException e) {
|
||||||
|
LOG.error("Failed to get delegation key in SQL secret manager", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return delegationKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains the value of the last delegation key id.
|
||||||
|
* @return Last delegation key id.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int getCurrentKeyId() {
|
||||||
|
try {
|
||||||
|
return selectKeyId();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Failed to get delegation key id in SQL secret manager", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the value of the last delegation key id.
|
||||||
|
* @param keyId Value to update the delegation key id to.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void setCurrentKeyId(int keyId) {
|
||||||
|
try {
|
||||||
|
updateKeyId(keyId);
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Failed to set delegation key id in SQL secret manager", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains the next available delegation key id that can be allocated to a DelegationKey.
|
||||||
|
* Delegation key id need to be reserved using the shared delegationKeyIdCounter,
|
||||||
|
* which handles keyId allocation concurrently with other secret managers.
|
||||||
|
* @return Next available delegation key id.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int incrementCurrentKeyId() {
|
||||||
|
try {
|
||||||
|
return incrementKeyId(1) + 1;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new RuntimeException(
|
||||||
|
"Failed to increment delegation key id in SQL secret manager", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token operations in SQL database
|
||||||
|
protected abstract byte[] selectTokenInfo(int sequenceNum, byte[] tokenIdentifier)
|
||||||
|
throws SQLException;
|
||||||
|
protected abstract void insertToken(int sequenceNum, byte[] tokenIdentifier, byte[] tokenInfo)
|
||||||
|
throws SQLException;
|
||||||
|
protected abstract void updateToken(int sequenceNum, byte[] tokenIdentifier, byte[] tokenInfo)
|
||||||
|
throws SQLException;
|
||||||
|
protected abstract void deleteToken(int sequenceNum, byte[] tokenIdentifier)
|
||||||
|
throws SQLException;
|
||||||
|
// Delegation key operations in SQL database
|
||||||
|
protected abstract byte[] selectDelegationKey(int keyId) throws SQLException;
|
||||||
|
protected abstract void insertDelegationKey(int keyId, byte[] delegationKey)
|
||||||
|
throws SQLException;
|
||||||
|
protected abstract void updateDelegationKey(int keyId, byte[] delegationKey)
|
||||||
|
throws SQLException;
|
||||||
|
protected abstract void deleteDelegationKey(int keyId) throws SQLException;
|
||||||
|
// Counter operations in SQL database
|
||||||
|
protected abstract int selectSequenceNum() throws SQLException;
|
||||||
|
protected abstract void updateSequenceNum(int value) throws SQLException;
|
||||||
|
protected abstract int incrementSequenceNum(int amount) throws SQLException;
|
||||||
|
protected abstract int selectKeyId() throws SQLException;
|
||||||
|
protected abstract void updateKeyId(int value) throws SQLException;
|
||||||
|
protected abstract int incrementKeyId(int amount) throws SQLException;
|
||||||
|
}
|
@ -117,6 +117,15 @@ https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|||||||
<groupId>com.fasterxml.jackson.core</groupId>
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
<artifactId>jackson-databind</artifactId>
|
<artifactId>jackson-databind</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.zaxxer</groupId>
|
||||||
|
<artifactId>HikariCP</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>mysql</groupId>
|
||||||
|
<artifactId>mysql-connector-java</artifactId>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>junit</groupId>
|
<groupId>junit</groupId>
|
||||||
<artifactId>junit</artifactId>
|
<artifactId>junit</artifactId>
|
||||||
@ -153,6 +162,11 @@ https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|||||||
<artifactId>curator-test</artifactId>
|
<artifactId>curator-test</artifactId>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.derby</groupId>
|
||||||
|
<artifactId>derby</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.mockito</groupId>
|
<groupId>org.mockito</groupId>
|
||||||
<artifactId>mockito-core</artifactId>
|
<artifactId>mockito-core</artifactId>
|
||||||
@ -170,6 +184,14 @@ https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
|||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-surefire-plugin</artifactId>
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<systemProperties>
|
||||||
|
<property>
|
||||||
|
<name>derby.stream.error.file</name>
|
||||||
|
<value>${project.build.directory}/derby.log</value>
|
||||||
|
</property>
|
||||||
|
</systemProperties>
|
||||||
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- Script to create a new Database in MySQL for the TokenStore
|
||||||
|
|
||||||
|
CREATE DATABASE IF NOT EXISTS TokenStore;
|
@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- Script to generate all the tables for the TokenStore in MySQL
|
||||||
|
|
||||||
|
USE TokenStore
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS Tokens(
|
||||||
|
sequenceNum int NOT NULL,
|
||||||
|
tokenIdentifier varbinary(255) NOT NULL,
|
||||||
|
tokenInfo varbinary(255) NOT NULL,
|
||||||
|
modifiedTime timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY(sequenceNum, tokenIdentifier)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS DelegationKeys(
|
||||||
|
keyId int NOT NULL,
|
||||||
|
delegationKey varbinary(255) NOT NULL,
|
||||||
|
modifiedTime timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY(keyId)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS LastSequenceNum(
|
||||||
|
sequenceNum int NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Initialize the LastSequenceNum table with a single entry
|
||||||
|
INSERT INTO LastSequenceNum (sequenceNum)
|
||||||
|
SELECT 0 WHERE NOT EXISTS (SELECT * FROM LastSequenceNum);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS LastDelegationKeyId(
|
||||||
|
keyId int NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Initialize the LastDelegationKeyId table with a single entry
|
||||||
|
INSERT INTO LastDelegationKeyId (keyId)
|
||||||
|
SELECT 0 WHERE NOT EXISTS (SELECT * FROM LastDelegationKeyId);
|
@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
-- Script to create a new User in MySQL for the TokenStore
|
||||||
|
|
||||||
|
-- Update TokenStore user and password on this script
|
||||||
|
CREATE USER IF NOT EXISTS 'TokenStoreUser'@'%' IDENTIFIED BY 'TokenStorePassword';
|
||||||
|
|
||||||
|
GRANT ALL PRIVILEGES ON TokenStore.* TO 'TokenStoreUser'@'%';
|
||||||
|
|
||||||
|
FLUSH PRIVILEGES;
|
@ -0,0 +1,24 @@
|
|||||||
|
#
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
These scripts must be executed to create the TokenStore database, tables and users needed to use the
|
||||||
|
SQLDelegationTokenSecretManagerImpl as the delegation token secret manager:
|
||||||
|
1. TokenStoreDatabase.sql
|
||||||
|
2. TokenStoreTables.sql
|
||||||
|
3. TokenStoreUser.sql
|
||||||
|
|
||||||
|
Note: The TokenStoreUser.sql defines a default user/password. You are highly encouraged to set
|
||||||
|
this to a proper strong password.
|
@ -0,0 +1,138 @@
|
|||||||
|
/*
|
||||||
|
* 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.hdfs.server.federation.router.security.token;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.sql.Statement;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Distributed counter that relies on a SQL database to synchronize
|
||||||
|
* between multiple clients. This expects a table with a single int field
|
||||||
|
* to exist in the database. One record must exist on the table at all times,
|
||||||
|
* representing the last used value reserved by a client.
|
||||||
|
*/
|
||||||
|
public class DistributedSQLCounter {
|
||||||
|
private static final Logger LOG =
|
||||||
|
LoggerFactory.getLogger(DistributedSQLCounter.class);
|
||||||
|
|
||||||
|
private final String field;
|
||||||
|
private final String table;
|
||||||
|
private final SQLConnectionFactory connectionFactory;
|
||||||
|
|
||||||
|
public DistributedSQLCounter(String field, String table,
|
||||||
|
SQLConnectionFactory connectionFactory) {
|
||||||
|
this.field = field;
|
||||||
|
this.table = table;
|
||||||
|
this.connectionFactory = connectionFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtains the value of the counter.
|
||||||
|
* @return counter value.
|
||||||
|
*/
|
||||||
|
public int selectCounterValue() throws SQLException {
|
||||||
|
try (Connection connection = connectionFactory.getConnection()) {
|
||||||
|
return selectCounterValue(false, connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int selectCounterValue(boolean forUpdate, Connection connection) throws SQLException {
|
||||||
|
String query = String.format("SELECT %s FROM %s %s", field, table,
|
||||||
|
forUpdate ? "FOR UPDATE" : "");
|
||||||
|
LOG.debug("Select counter statement: " + query);
|
||||||
|
try (Statement statement = connection.createStatement();
|
||||||
|
ResultSet result = statement.executeQuery(query)) {
|
||||||
|
if (result.next()) {
|
||||||
|
return result.getInt(field);
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("Counter table not initialized: " + table);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the counter to the given value.
|
||||||
|
* @param value Value to assign to counter.
|
||||||
|
*/
|
||||||
|
public void updateCounterValue(int value) throws SQLException {
|
||||||
|
try (Connection connection = connectionFactory.getConnection(true)) {
|
||||||
|
updateCounterValue(value, connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the counter to the given value.
|
||||||
|
* @param connection Connection to database hosting the counter table.
|
||||||
|
* @param value Value to assign to counter.
|
||||||
|
*/
|
||||||
|
public void updateCounterValue(int value, Connection connection) throws SQLException {
|
||||||
|
String queryText = String.format("UPDATE %s SET %s = ?", table, field);
|
||||||
|
LOG.debug("Update counter statement: " + queryText + ". Value: " + value);
|
||||||
|
try (PreparedStatement statement = connection.prepareStatement(queryText)) {
|
||||||
|
statement.setInt(1, value);
|
||||||
|
statement.execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increments the counter by the given amount and
|
||||||
|
* returns the previous counter value.
|
||||||
|
* @param amount Amount to increase the counter.
|
||||||
|
* @return Previous counter value.
|
||||||
|
*/
|
||||||
|
public int incrementCounterValue(int amount) throws SQLException {
|
||||||
|
// Disabling auto-commit to ensure that all statements on this transaction
|
||||||
|
// are committed at once.
|
||||||
|
try (Connection connection = connectionFactory.getConnection(false)) {
|
||||||
|
// Preventing dirty reads and non-repeatable reads to ensure that the
|
||||||
|
// value read will not be updated by a different connection.
|
||||||
|
if (connection.getTransactionIsolation() < Connection.TRANSACTION_REPEATABLE_READ) {
|
||||||
|
connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Reading the counter value "FOR UPDATE" to lock the value record,
|
||||||
|
// forcing other connections to wait until this transaction is committed.
|
||||||
|
int lastValue = selectCounterValue(true, connection);
|
||||||
|
|
||||||
|
// Calculate the new counter value and handling overflow by
|
||||||
|
// resetting the counter to 0.
|
||||||
|
int newValue = lastValue + amount;
|
||||||
|
if (newValue < 0) {
|
||||||
|
lastValue = 0;
|
||||||
|
newValue = amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCounterValue(newValue, connection);
|
||||||
|
connection.commit();
|
||||||
|
return lastValue;
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Rollback transaction to release table locks
|
||||||
|
connection.rollback();
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
/*
|
||||||
|
* 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.hdfs.server.federation.router.security.token;
|
||||||
|
|
||||||
|
import com.zaxxer.hikari.HikariConfig;
|
||||||
|
import com.zaxxer.hikari.HikariDataSource;
|
||||||
|
import org.apache.hadoop.classification.VisibleForTesting;
|
||||||
|
import org.apache.hadoop.conf.Configuration;
|
||||||
|
import org.apache.hadoop.security.token.delegation.SQLDelegationTokenSecretManager;
|
||||||
|
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class that relies on a HikariDataSource to provide SQL connections.
|
||||||
|
*/
|
||||||
|
class HikariDataSourceConnectionFactory implements SQLConnectionFactory {
|
||||||
|
protected final static String HIKARI_PROPS = SQLDelegationTokenSecretManager.SQL_DTSM_CONF_PREFIX
|
||||||
|
+ "connection.hikari.";
|
||||||
|
private final HikariDataSource dataSource;
|
||||||
|
|
||||||
|
HikariDataSourceConnectionFactory(Configuration conf) {
|
||||||
|
Properties properties = new Properties();
|
||||||
|
properties.setProperty("jdbcUrl", conf.get(CONNECTION_URL));
|
||||||
|
properties.setProperty("username", conf.get(CONNECTION_USERNAME));
|
||||||
|
properties.setProperty("password", conf.get(CONNECTION_PASSWORD));
|
||||||
|
properties.setProperty("driverClassName", conf.get(CONNECTION_DRIVER));
|
||||||
|
|
||||||
|
// Include hikari connection properties
|
||||||
|
properties.putAll(conf.getPropsWithPrefix(HIKARI_PROPS));
|
||||||
|
|
||||||
|
HikariConfig hikariConfig = new HikariConfig(properties);
|
||||||
|
this.dataSource = new HikariDataSource(hikariConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Connection getConnection() throws SQLException {
|
||||||
|
return dataSource.getConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void shutdown() {
|
||||||
|
// Close database connections
|
||||||
|
dataSource.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
HikariDataSource getDataSource() {
|
||||||
|
return dataSource;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* 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.hdfs.server.federation.router.security.token;
|
||||||
|
|
||||||
|
import com.mysql.cj.jdbc.MysqlDataSource;
|
||||||
|
import com.zaxxer.hikari.HikariConfig;
|
||||||
|
import com.zaxxer.hikari.HikariDataSource;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.Properties;
|
||||||
|
import org.apache.hadoop.classification.VisibleForTesting;
|
||||||
|
import org.apache.hadoop.conf.Configuration;
|
||||||
|
import org.apache.hadoop.security.token.delegation.SQLDelegationTokenSecretManager;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface to provide SQL connections to the {@link SQLDelegationTokenSecretManagerImpl}.
|
||||||
|
*/
|
||||||
|
public interface SQLConnectionFactory {
|
||||||
|
String CONNECTION_URL = SQLDelegationTokenSecretManager.SQL_DTSM_CONF_PREFIX
|
||||||
|
+ "connection.url";
|
||||||
|
String CONNECTION_USERNAME = SQLDelegationTokenSecretManager.SQL_DTSM_CONF_PREFIX
|
||||||
|
+ "connection.username";
|
||||||
|
String CONNECTION_PASSWORD = SQLDelegationTokenSecretManager.SQL_DTSM_CONF_PREFIX
|
||||||
|
+ "connection.password";
|
||||||
|
String CONNECTION_DRIVER = SQLDelegationTokenSecretManager.SQL_DTSM_CONF_PREFIX
|
||||||
|
+ "connection.driver";
|
||||||
|
|
||||||
|
Connection getConnection() throws SQLException;
|
||||||
|
void shutdown();
|
||||||
|
|
||||||
|
default Connection getConnection(boolean autocommit) throws SQLException {
|
||||||
|
Connection connection = getConnection();
|
||||||
|
connection.setAutoCommit(autocommit);
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,242 @@
|
|||||||
|
/*
|
||||||
|
* 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.hdfs.server.federation.router.security.token;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.ResultSet;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import org.apache.hadoop.classification.VisibleForTesting;
|
||||||
|
import org.apache.hadoop.conf.Configuration;
|
||||||
|
import org.apache.hadoop.hdfs.security.token.delegation.DelegationTokenIdentifier;
|
||||||
|
import org.apache.hadoop.security.token.delegation.AbstractDelegationTokenIdentifier;
|
||||||
|
import org.apache.hadoop.security.token.delegation.SQLDelegationTokenSecretManager;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An implementation of {@link SQLDelegationTokenSecretManager} that
|
||||||
|
* persists TokenIdentifiers and DelegationKeys in a SQL database.
|
||||||
|
* This implementation relies on the Datanucleus JDO PersistenceManager, which
|
||||||
|
* can be configured with datanucleus.* configuration properties.
|
||||||
|
*/
|
||||||
|
public class SQLDelegationTokenSecretManagerImpl
|
||||||
|
extends SQLDelegationTokenSecretManager<AbstractDelegationTokenIdentifier> {
|
||||||
|
|
||||||
|
private static final Logger LOG =
|
||||||
|
LoggerFactory.getLogger(SQLDelegationTokenSecretManagerImpl.class);
|
||||||
|
private static final String SEQ_NUM_COUNTER_FIELD = "sequenceNum";
|
||||||
|
private static final String SEQ_NUM_COUNTER_TABLE = "LastSequenceNum";
|
||||||
|
private static final String KEY_ID_COUNTER_FIELD = "keyId";
|
||||||
|
private static final String KEY_ID_COUNTER_TABLE = "LastDelegationKeyId";
|
||||||
|
|
||||||
|
private final SQLConnectionFactory connectionFactory;
|
||||||
|
private final DistributedSQLCounter sequenceNumCounter;
|
||||||
|
private final DistributedSQLCounter delegationKeyIdCounter;
|
||||||
|
private final SQLSecretManagerRetriableHandler retryHandler;
|
||||||
|
|
||||||
|
public SQLDelegationTokenSecretManagerImpl(Configuration conf) {
|
||||||
|
this(conf, new HikariDataSourceConnectionFactory(conf),
|
||||||
|
SQLSecretManagerRetriableHandlerImpl.getInstance(conf));
|
||||||
|
}
|
||||||
|
|
||||||
|
public SQLDelegationTokenSecretManagerImpl(Configuration conf,
|
||||||
|
SQLConnectionFactory connectionFactory, SQLSecretManagerRetriableHandler retryHandler) {
|
||||||
|
super(conf);
|
||||||
|
|
||||||
|
this.connectionFactory = connectionFactory;
|
||||||
|
this.sequenceNumCounter = new DistributedSQLCounter(SEQ_NUM_COUNTER_FIELD,
|
||||||
|
SEQ_NUM_COUNTER_TABLE, connectionFactory);
|
||||||
|
this.delegationKeyIdCounter = new DistributedSQLCounter(KEY_ID_COUNTER_FIELD,
|
||||||
|
KEY_ID_COUNTER_TABLE, connectionFactory);
|
||||||
|
this.retryHandler = retryHandler;
|
||||||
|
|
||||||
|
try {
|
||||||
|
super.startThreads();
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("Error starting threads for MySQL secret manager", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG.info("MySQL delegation token secret manager instantiated");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DelegationTokenIdentifier createIdentifier() {
|
||||||
|
return new DelegationTokenIdentifier();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void stopThreads() {
|
||||||
|
super.stopThreads();
|
||||||
|
connectionFactory.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void insertToken(int sequenceNum, byte[] tokenIdentifier, byte[] tokenInfo)
|
||||||
|
throws SQLException {
|
||||||
|
retryHandler.execute(() -> {
|
||||||
|
try (Connection connection = connectionFactory.getConnection(true);
|
||||||
|
PreparedStatement statement = connection.prepareStatement(
|
||||||
|
"INSERT INTO Tokens (sequenceNum, tokenIdentifier, tokenInfo) VALUES (?, ?, ?)")) {
|
||||||
|
statement.setInt(1, sequenceNum);
|
||||||
|
statement.setBytes(2, tokenIdentifier);
|
||||||
|
statement.setBytes(3, tokenInfo);
|
||||||
|
statement.execute();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void updateToken(int sequenceNum, byte[] tokenIdentifier, byte[] tokenInfo)
|
||||||
|
throws SQLException {
|
||||||
|
retryHandler.execute(() -> {
|
||||||
|
try (Connection connection = connectionFactory.getConnection(true);
|
||||||
|
PreparedStatement statement = connection.prepareStatement(
|
||||||
|
"UPDATE Tokens SET tokenInfo = ? WHERE sequenceNum = ? AND tokenIdentifier = ?")) {
|
||||||
|
statement.setBytes(1, tokenInfo);
|
||||||
|
statement.setInt(2, sequenceNum);
|
||||||
|
statement.setBytes(3, tokenIdentifier);
|
||||||
|
statement.execute();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void deleteToken(int sequenceNum, byte[] tokenIdentifier) throws SQLException {
|
||||||
|
retryHandler.execute(() -> {
|
||||||
|
try (Connection connection = connectionFactory.getConnection(true);
|
||||||
|
PreparedStatement statement = connection.prepareStatement(
|
||||||
|
"DELETE FROM Tokens WHERE sequenceNum = ? AND tokenIdentifier = ?")) {
|
||||||
|
statement.setInt(1, sequenceNum);
|
||||||
|
statement.setBytes(2, tokenIdentifier);
|
||||||
|
statement.execute();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected byte[] selectTokenInfo(int sequenceNum, byte[] tokenIdentifier) throws SQLException {
|
||||||
|
return retryHandler.execute(() -> {
|
||||||
|
try (Connection connection = connectionFactory.getConnection();
|
||||||
|
PreparedStatement statement = connection.prepareStatement(
|
||||||
|
"SELECT tokenInfo FROM Tokens WHERE sequenceNum = ? AND tokenIdentifier = ?")) {
|
||||||
|
statement.setInt(1, sequenceNum);
|
||||||
|
statement.setBytes(2, tokenIdentifier);
|
||||||
|
try (ResultSet result = statement.executeQuery()) {
|
||||||
|
if (result.next()) {
|
||||||
|
return result.getBytes("tokenInfo");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void insertDelegationKey(int keyId, byte[] delegationKey) throws SQLException {
|
||||||
|
retryHandler.execute(() -> {
|
||||||
|
try (Connection connection = connectionFactory.getConnection(true);
|
||||||
|
PreparedStatement statement = connection.prepareStatement(
|
||||||
|
"INSERT INTO DelegationKeys (keyId, delegationKey) VALUES (?, ?)")) {
|
||||||
|
statement.setInt(1, keyId);
|
||||||
|
statement.setBytes(2, delegationKey);
|
||||||
|
statement.execute();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void updateDelegationKey(int keyId, byte[] delegationKey) throws SQLException {
|
||||||
|
retryHandler.execute(() -> {
|
||||||
|
try (Connection connection = connectionFactory.getConnection(true);
|
||||||
|
PreparedStatement statement = connection.prepareStatement(
|
||||||
|
"UPDATE DelegationKeys SET delegationKey = ? WHERE keyId = ?")) {
|
||||||
|
statement.setBytes(1, delegationKey);
|
||||||
|
statement.setInt(2, keyId);
|
||||||
|
statement.execute();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void deleteDelegationKey(int keyId) throws SQLException {
|
||||||
|
retryHandler.execute(() -> {
|
||||||
|
try (Connection connection = connectionFactory.getConnection(true);
|
||||||
|
PreparedStatement statement = connection.prepareStatement(
|
||||||
|
"DELETE FROM DelegationKeys WHERE keyId = ?")) {
|
||||||
|
statement.setInt(1, keyId);
|
||||||
|
statement.execute();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected byte[] selectDelegationKey(int keyId) throws SQLException {
|
||||||
|
return retryHandler.execute(() -> {
|
||||||
|
try (Connection connection = connectionFactory.getConnection();
|
||||||
|
PreparedStatement statement = connection.prepareStatement(
|
||||||
|
"SELECT delegationKey FROM DelegationKeys WHERE keyId = ?")) {
|
||||||
|
statement.setInt(1, keyId);
|
||||||
|
try (ResultSet result = statement.executeQuery()) {
|
||||||
|
if (result.next()) {
|
||||||
|
return result.getBytes("delegationKey");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int selectSequenceNum() throws SQLException {
|
||||||
|
return retryHandler.execute(() -> sequenceNumCounter.selectCounterValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void updateSequenceNum(int value) throws SQLException {
|
||||||
|
retryHandler.execute(() -> sequenceNumCounter.updateCounterValue(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int incrementSequenceNum(int amount) throws SQLException {
|
||||||
|
return retryHandler.execute(() -> sequenceNumCounter.incrementCounterValue(amount));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int selectKeyId() throws SQLException {
|
||||||
|
return retryHandler.execute(delegationKeyIdCounter::selectCounterValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void updateKeyId(int value) throws SQLException {
|
||||||
|
retryHandler.execute(() -> delegationKeyIdCounter.updateCounterValue(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int incrementKeyId(int amount) throws SQLException {
|
||||||
|
return retryHandler.execute(() -> delegationKeyIdCounter.incrementCounterValue(amount));
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
protected SQLConnectionFactory getConnectionFactory() {
|
||||||
|
return connectionFactory;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
* 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.hdfs.server.federation.router.security.token;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import org.apache.hadoop.conf.Configuration;
|
||||||
|
import org.apache.hadoop.io.retry.RetryPolicies;
|
||||||
|
import org.apache.hadoop.io.retry.RetryPolicy;
|
||||||
|
import org.apache.hadoop.io.retry.RetryProxy;
|
||||||
|
import org.apache.hadoop.security.token.delegation.SQLDelegationTokenSecretManager;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface to handle retries when {@link SQLDelegationTokenSecretManagerImpl}
|
||||||
|
* throws expected errors.
|
||||||
|
*/
|
||||||
|
public interface SQLSecretManagerRetriableHandler {
|
||||||
|
void execute(SQLCommandVoid command) throws SQLException;
|
||||||
|
<T> T execute(SQLCommand<T> command) throws SQLException;
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
interface SQLCommandVoid {
|
||||||
|
void doCall() throws SQLException;
|
||||||
|
}
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
interface SQLCommand<T> {
|
||||||
|
T doCall() throws SQLException;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Implementation of {@link SQLSecretManagerRetriableHandler} that uses a
|
||||||
|
* {@link RetryProxy} to simplify the retryable operations.
|
||||||
|
*/
|
||||||
|
class SQLSecretManagerRetriableHandlerImpl implements SQLSecretManagerRetriableHandler {
|
||||||
|
public final static String MAX_RETRIES =
|
||||||
|
SQLDelegationTokenSecretManager.SQL_DTSM_CONF_PREFIX + "max-retries";
|
||||||
|
public final static int MAX_RETRIES_DEFAULT = 0;
|
||||||
|
public final static String RETRY_SLEEP_TIME_MS =
|
||||||
|
SQLDelegationTokenSecretManager.SQL_DTSM_CONF_PREFIX + "retry-sleep-time-ms";
|
||||||
|
public final static long RETRY_SLEEP_TIME_MS_DEFAULT = 100;
|
||||||
|
|
||||||
|
private static final Logger LOG =
|
||||||
|
LoggerFactory.getLogger(SQLSecretManagerRetriableHandlerImpl.class);
|
||||||
|
|
||||||
|
static SQLSecretManagerRetriableHandler getInstance(Configuration conf) {
|
||||||
|
return getInstance(conf, new SQLSecretManagerRetriableHandlerImpl());
|
||||||
|
}
|
||||||
|
|
||||||
|
static SQLSecretManagerRetriableHandler getInstance(Configuration conf,
|
||||||
|
SQLSecretManagerRetriableHandlerImpl retryHandler) {
|
||||||
|
RetryPolicy basePolicy = RetryPolicies.exponentialBackoffRetry(
|
||||||
|
conf.getInt(MAX_RETRIES, MAX_RETRIES_DEFAULT),
|
||||||
|
conf.getLong(RETRY_SLEEP_TIME_MS, RETRY_SLEEP_TIME_MS_DEFAULT),
|
||||||
|
TimeUnit.MILLISECONDS);
|
||||||
|
|
||||||
|
// Configure SQLSecretManagerRetriableException to retry with exponential backoff
|
||||||
|
Map<Class<? extends Exception>, RetryPolicy> exceptionToPolicyMap = new HashMap<>();
|
||||||
|
exceptionToPolicyMap.put(SQLSecretManagerRetriableException.class, basePolicy);
|
||||||
|
|
||||||
|
// Configure all other exceptions to fail after one attempt
|
||||||
|
RetryPolicy retryPolicy = RetryPolicies.retryByException(
|
||||||
|
RetryPolicies.TRY_ONCE_THEN_FAIL, exceptionToPolicyMap);
|
||||||
|
|
||||||
|
return (SQLSecretManagerRetriableHandler) RetryProxy.create(
|
||||||
|
SQLSecretManagerRetriableHandler.class, retryHandler, retryPolicy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a SQL command and raises retryable errors as
|
||||||
|
* {@link SQLSecretManagerRetriableException}s so they are recognized by the
|
||||||
|
* {@link RetryProxy}.
|
||||||
|
* @param command SQL command to execute
|
||||||
|
* @throws SQLException When SQL connection errors occur
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void execute(SQLCommandVoid command) throws SQLException {
|
||||||
|
try {
|
||||||
|
command.doCall();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
LOG.warn("Failed to execute SQL command", e);
|
||||||
|
throw new SQLSecretManagerRetriableException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes a SQL command and raises retryable errors as
|
||||||
|
* {@link SQLSecretManagerRetriableException}s so they are recognized by the
|
||||||
|
* {@link RetryProxy}.
|
||||||
|
* @param command SQL command to execute
|
||||||
|
* @throws SQLException When SQL connection errors occur
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public <T> T execute(SQLCommand<T> command) throws SQLException {
|
||||||
|
try {
|
||||||
|
return command.doCall();
|
||||||
|
} catch (SQLException e) {
|
||||||
|
LOG.warn("Failed to execute SQL command", e);
|
||||||
|
throw new SQLSecretManagerRetriableException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class used to identify errors that can be retried.
|
||||||
|
*/
|
||||||
|
static class SQLSecretManagerRetriableException extends SQLException {
|
||||||
|
SQLSecretManagerRetriableException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,471 @@
|
|||||||
|
/*
|
||||||
|
* 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.hdfs.server.federation.router.security.token;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.sql.Connection;
|
||||||
|
import java.sql.DriverManager;
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
import org.apache.hadoop.conf.Configuration;
|
||||||
|
import org.apache.hadoop.security.UserGroupInformation;
|
||||||
|
import org.apache.hadoop.security.token.Token;
|
||||||
|
import org.apache.hadoop.security.token.delegation.AbstractDelegationTokenIdentifier;
|
||||||
|
import org.apache.hadoop.security.token.delegation.web.DelegationTokenManager;
|
||||||
|
import org.junit.After;
|
||||||
|
import org.junit.AfterClass;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
|
||||||
|
public class TestSQLDelegationTokenSecretManagerImpl {
|
||||||
|
private static final String CONNECTION_URL = "jdbc:derby:memory:TokenStore";
|
||||||
|
private static final int TEST_MAX_RETRIES = 3;
|
||||||
|
private static Configuration conf;
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void init() throws SQLException {
|
||||||
|
createTestDBTables();
|
||||||
|
}
|
||||||
|
|
||||||
|
@After
|
||||||
|
public void cleanup() throws SQLException {
|
||||||
|
dropTestDBTables();
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void initDatabase() throws SQLException {
|
||||||
|
DriverManager.getConnection(CONNECTION_URL + ";create=true");
|
||||||
|
|
||||||
|
conf = new Configuration();
|
||||||
|
conf.set(SQLConnectionFactory.CONNECTION_URL, CONNECTION_URL);
|
||||||
|
conf.set(SQLConnectionFactory.CONNECTION_USERNAME, "testuser");
|
||||||
|
conf.set(SQLConnectionFactory.CONNECTION_PASSWORD, "testpassword");
|
||||||
|
conf.set(SQLConnectionFactory.CONNECTION_DRIVER, "org.apache.derby.jdbc.EmbeddedDriver");
|
||||||
|
conf.setInt(SQLSecretManagerRetriableHandlerImpl.MAX_RETRIES, TEST_MAX_RETRIES);
|
||||||
|
conf.setInt(SQLSecretManagerRetriableHandlerImpl.RETRY_SLEEP_TIME_MS, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void cleanupDatabase() {
|
||||||
|
try {
|
||||||
|
DriverManager.getConnection(CONNECTION_URL + ";drop=true");
|
||||||
|
} catch (SQLException e) {
|
||||||
|
// SQLException expected when database is dropped
|
||||||
|
if (!e.getMessage().contains("dropped")) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSingleSecretManager() throws Exception {
|
||||||
|
DelegationTokenManager tokenManager = createTokenManager();
|
||||||
|
try {
|
||||||
|
Token<? extends AbstractDelegationTokenIdentifier> token =
|
||||||
|
tokenManager.createToken(UserGroupInformation.getCurrentUser(), "foo");
|
||||||
|
validateToken(tokenManager, token);
|
||||||
|
} finally {
|
||||||
|
stopTokenManager(tokenManager);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testMultipleSecretManagers() throws Exception {
|
||||||
|
DelegationTokenManager tokenManager1 = createTokenManager();
|
||||||
|
DelegationTokenManager tokenManager2 = createTokenManager();
|
||||||
|
|
||||||
|
try {
|
||||||
|
Token<? extends AbstractDelegationTokenIdentifier> token1 =
|
||||||
|
tokenManager1.createToken(UserGroupInformation.getCurrentUser(), "foo");
|
||||||
|
Token<? extends AbstractDelegationTokenIdentifier> token2 =
|
||||||
|
tokenManager2.createToken(UserGroupInformation.getCurrentUser(), "foo");
|
||||||
|
|
||||||
|
validateToken(tokenManager1, token2);
|
||||||
|
validateToken(tokenManager2, token1);
|
||||||
|
} finally {
|
||||||
|
stopTokenManager(tokenManager1);
|
||||||
|
stopTokenManager(tokenManager2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSequenceNumAllocation() throws Exception {
|
||||||
|
int tokensPerManager = SQLDelegationTokenSecretManagerImpl.DEFAULT_SEQ_NUM_BATCH_SIZE * 5;
|
||||||
|
Set<Integer> sequenceNums1 = new HashSet<>();
|
||||||
|
Set<Integer> sequenceNums2 = new HashSet<>();
|
||||||
|
Set<Integer> sequenceNums3 = new HashSet<>();
|
||||||
|
Set<Integer> sequenceNums = new HashSet<>();
|
||||||
|
DelegationTokenManager tokenManager1 = createTokenManager();
|
||||||
|
DelegationTokenManager tokenManager2 = createTokenManager();
|
||||||
|
DelegationTokenManager tokenManager3 = createTokenManager();
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (int i = 0; i < tokensPerManager; i++) {
|
||||||
|
allocateSequenceNum(tokenManager1, sequenceNums1);
|
||||||
|
allocateSequenceNum(tokenManager2, sequenceNums2);
|
||||||
|
allocateSequenceNum(tokenManager3, sequenceNums3);
|
||||||
|
sequenceNums.addAll(sequenceNums1);
|
||||||
|
sequenceNums.addAll(sequenceNums2);
|
||||||
|
sequenceNums.addAll(sequenceNums3);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.assertEquals("Verify that all tokens were created with unique sequence numbers",
|
||||||
|
tokensPerManager * 3, sequenceNums.size());
|
||||||
|
Assert.assertEquals("Verify that tokenManager1 generated unique sequence numbers",
|
||||||
|
tokensPerManager, sequenceNums1.size());
|
||||||
|
Assert.assertEquals("Verify that tokenManager2 generated unique sequence number",
|
||||||
|
tokensPerManager, sequenceNums2.size());
|
||||||
|
Assert.assertEquals("Verify that tokenManager3 generated unique sequence numbers",
|
||||||
|
tokensPerManager, sequenceNums3.size());
|
||||||
|
|
||||||
|
// Validate sequence number batches allocated in order to each token manager
|
||||||
|
int batchSize = SQLDelegationTokenSecretManagerImpl.DEFAULT_SEQ_NUM_BATCH_SIZE;
|
||||||
|
for (int seqNum = 1; seqNum < tokensPerManager;) {
|
||||||
|
// First batch allocated tokenManager1
|
||||||
|
for (int i = 0; i < batchSize; i++, seqNum++) {
|
||||||
|
Assert.assertTrue(sequenceNums1.contains(seqNum));
|
||||||
|
}
|
||||||
|
// Second batch allocated tokenManager2
|
||||||
|
for (int i = 0; i < batchSize; i++, seqNum++) {
|
||||||
|
Assert.assertTrue(sequenceNums2.contains(seqNum));
|
||||||
|
}
|
||||||
|
// Third batch allocated tokenManager3
|
||||||
|
for (int i = 0; i < batchSize; i++, seqNum++) {
|
||||||
|
Assert.assertTrue(sequenceNums3.contains(seqNum));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SQLDelegationTokenSecretManagerImpl secretManager =
|
||||||
|
(SQLDelegationTokenSecretManagerImpl) tokenManager1.getDelegationTokenSecretManager();
|
||||||
|
Assert.assertEquals("Verify that the counter is set to the highest sequence number",
|
||||||
|
tokensPerManager * 3, secretManager.getDelegationTokenSeqNum());
|
||||||
|
} finally {
|
||||||
|
stopTokenManager(tokenManager1);
|
||||||
|
stopTokenManager(tokenManager2);
|
||||||
|
stopTokenManager(tokenManager3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSequenceNumRollover() throws Exception {
|
||||||
|
int tokenBatch = SQLDelegationTokenSecretManagerImpl.DEFAULT_SEQ_NUM_BATCH_SIZE;
|
||||||
|
Set<Integer> sequenceNums = new HashSet<>();
|
||||||
|
|
||||||
|
DelegationTokenManager tokenManager = createTokenManager();
|
||||||
|
|
||||||
|
try {
|
||||||
|
SQLDelegationTokenSecretManagerImpl secretManager =
|
||||||
|
(SQLDelegationTokenSecretManagerImpl) tokenManager.getDelegationTokenSecretManager();
|
||||||
|
secretManager.setDelegationTokenSeqNum(Integer.MAX_VALUE - tokenBatch);
|
||||||
|
|
||||||
|
// Allocate sequence numbers before they are rolled over
|
||||||
|
for (int seqNum = Integer.MAX_VALUE - tokenBatch; seqNum < Integer.MAX_VALUE; seqNum++) {
|
||||||
|
allocateSequenceNum(tokenManager, sequenceNums);
|
||||||
|
Assert.assertTrue(sequenceNums.contains(seqNum + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allocate sequence numbers after they are rolled over
|
||||||
|
for (int seqNum = 0; seqNum < tokenBatch; seqNum++) {
|
||||||
|
allocateSequenceNum(tokenManager, sequenceNums);
|
||||||
|
Assert.assertTrue(sequenceNums.contains(seqNum + 1));
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
stopTokenManager(tokenManager);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDelegationKeyAllocation() throws Exception {
|
||||||
|
DelegationTokenManager tokenManager1 = createTokenManager();
|
||||||
|
|
||||||
|
try {
|
||||||
|
SQLDelegationTokenSecretManagerImpl secretManager1 =
|
||||||
|
(SQLDelegationTokenSecretManagerImpl) tokenManager1.getDelegationTokenSecretManager();
|
||||||
|
// Prevent delegation keys to roll for the rest of the test to avoid race conditions
|
||||||
|
// between the keys generated and the active keys in the database.
|
||||||
|
((TestDelegationTokenSecretManager) secretManager1).lockKeyRoll();
|
||||||
|
int keyId1 = secretManager1.getCurrentKeyId();
|
||||||
|
|
||||||
|
// Validate that latest key1 is assigned to tokenManager1 tokens
|
||||||
|
Token<? extends AbstractDelegationTokenIdentifier> token1 =
|
||||||
|
tokenManager1.createToken(UserGroupInformation.getCurrentUser(), "foo");
|
||||||
|
validateKeyId(token1, keyId1);
|
||||||
|
|
||||||
|
DelegationTokenManager tokenManager2 = createTokenManager();
|
||||||
|
try {
|
||||||
|
SQLDelegationTokenSecretManagerImpl secretManager2 =
|
||||||
|
(SQLDelegationTokenSecretManagerImpl) tokenManager2.getDelegationTokenSecretManager();
|
||||||
|
// Prevent delegation keys to roll for the rest of the test
|
||||||
|
((TestDelegationTokenSecretManager) secretManager2).lockKeyRoll();
|
||||||
|
int keyId2 = secretManager2.getCurrentKeyId();
|
||||||
|
|
||||||
|
Assert.assertNotEquals("Each secret manager has its own key", keyId1, keyId2);
|
||||||
|
|
||||||
|
// Validate that latest key2 is assigned to tokenManager2 tokens
|
||||||
|
Token<? extends AbstractDelegationTokenIdentifier> token2 =
|
||||||
|
tokenManager2.createToken(UserGroupInformation.getCurrentUser(), "foo");
|
||||||
|
validateKeyId(token2, keyId2);
|
||||||
|
|
||||||
|
// Validate that key1 is still assigned to tokenManager1 tokens
|
||||||
|
token1 = tokenManager1.createToken(UserGroupInformation.getCurrentUser(), "foo");
|
||||||
|
validateKeyId(token1, keyId1);
|
||||||
|
|
||||||
|
// Validate that key2 is still assigned to tokenManager2 tokens
|
||||||
|
token2 = tokenManager2.createToken(UserGroupInformation.getCurrentUser(), "foo");
|
||||||
|
validateKeyId(token2, keyId2);
|
||||||
|
} finally {
|
||||||
|
stopTokenManager(tokenManager2);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
stopTokenManager(tokenManager1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testHikariConfigs() {
|
||||||
|
HikariDataSourceConnectionFactory factory1 = new HikariDataSourceConnectionFactory(conf);
|
||||||
|
int defaultMaximumPoolSize = factory1.getDataSource().getMaximumPoolSize();
|
||||||
|
factory1.shutdown();
|
||||||
|
|
||||||
|
// Changing default maximumPoolSize
|
||||||
|
Configuration hikariConf = new Configuration(conf);
|
||||||
|
hikariConf.setInt(HikariDataSourceConnectionFactory.HIKARI_PROPS + "maximumPoolSize",
|
||||||
|
defaultMaximumPoolSize + 1);
|
||||||
|
|
||||||
|
// Verifying property is correctly set in datasource
|
||||||
|
HikariDataSourceConnectionFactory factory2 = new HikariDataSourceConnectionFactory(hikariConf);
|
||||||
|
Assert.assertEquals(factory2.getDataSource().getMaximumPoolSize(),
|
||||||
|
defaultMaximumPoolSize + 1);
|
||||||
|
factory2.shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testRetries() throws Exception {
|
||||||
|
DelegationTokenManager tokenManager = createTokenManager();
|
||||||
|
TestDelegationTokenSecretManager secretManager =
|
||||||
|
(TestDelegationTokenSecretManager) tokenManager.getDelegationTokenSecretManager();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Prevent delegation keys to roll for the rest of the test
|
||||||
|
secretManager.lockKeyRoll();
|
||||||
|
|
||||||
|
// Reset counter and expect a single request when inserting a token
|
||||||
|
TestRetryHandler.resetExecutionAttemptCounter();
|
||||||
|
tokenManager.createToken(UserGroupInformation.getCurrentUser(), "foo");
|
||||||
|
Assert.assertEquals(1, TestRetryHandler.getExecutionAttempts());
|
||||||
|
|
||||||
|
// Breaking database connections to cause retries
|
||||||
|
secretManager.setReadOnly(true);
|
||||||
|
|
||||||
|
// Reset counter and expect a multiple retries when failing to insert a token
|
||||||
|
TestRetryHandler.resetExecutionAttemptCounter();
|
||||||
|
tokenManager.createToken(UserGroupInformation.getCurrentUser(), "foo");
|
||||||
|
Assert.assertEquals(TEST_MAX_RETRIES + 1, TestRetryHandler.getExecutionAttempts());
|
||||||
|
} finally {
|
||||||
|
// Fix database connections
|
||||||
|
secretManager.setReadOnly(false);
|
||||||
|
stopTokenManager(tokenManager);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DelegationTokenManager createTokenManager() {
|
||||||
|
DelegationTokenManager tokenManager = new DelegationTokenManager(new Configuration(), null);
|
||||||
|
tokenManager.setExternalDelegationTokenSecretManager(new TestDelegationTokenSecretManager());
|
||||||
|
return tokenManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void allocateSequenceNum(DelegationTokenManager tokenManager, Set<Integer> sequenceNums)
|
||||||
|
throws IOException {
|
||||||
|
Token<? extends AbstractDelegationTokenIdentifier> token =
|
||||||
|
tokenManager.createToken(UserGroupInformation.getCurrentUser(), "foo");
|
||||||
|
AbstractDelegationTokenIdentifier tokenIdentifier = token.decodeIdentifier();
|
||||||
|
Assert.assertFalse("Verify sequence number is unique",
|
||||||
|
sequenceNums.contains(tokenIdentifier.getSequenceNumber()));
|
||||||
|
|
||||||
|
sequenceNums.add(tokenIdentifier.getSequenceNumber());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateToken(DelegationTokenManager tokenManager,
|
||||||
|
Token<? extends AbstractDelegationTokenIdentifier> token)
|
||||||
|
throws Exception {
|
||||||
|
SQLDelegationTokenSecretManagerImpl secretManager =
|
||||||
|
(SQLDelegationTokenSecretManagerImpl) tokenManager.getDelegationTokenSecretManager();
|
||||||
|
AbstractDelegationTokenIdentifier tokenIdentifier = token.decodeIdentifier();
|
||||||
|
|
||||||
|
// Verify token using token manager
|
||||||
|
tokenManager.verifyToken(token);
|
||||||
|
|
||||||
|
byte[] tokenInfo1 = secretManager.selectTokenInfo(tokenIdentifier.getSequenceNumber(),
|
||||||
|
tokenIdentifier.getBytes());
|
||||||
|
Assert.assertNotNull("Verify token exists in database", tokenInfo1);
|
||||||
|
|
||||||
|
// Renew token using token manager
|
||||||
|
tokenManager.renewToken(token, "foo");
|
||||||
|
|
||||||
|
byte[] tokenInfo2 = secretManager.selectTokenInfo(tokenIdentifier.getSequenceNumber(),
|
||||||
|
tokenIdentifier.getBytes());
|
||||||
|
Assert.assertNotNull("Verify token exists in database", tokenInfo2);
|
||||||
|
Assert.assertFalse("Verify token has been updated in database",
|
||||||
|
Arrays.equals(tokenInfo1, tokenInfo2));
|
||||||
|
|
||||||
|
// Cancel token using token manager
|
||||||
|
tokenManager.cancelToken(token, "foo");
|
||||||
|
byte[] tokenInfo3 = secretManager.selectTokenInfo(tokenIdentifier.getSequenceNumber(),
|
||||||
|
tokenIdentifier.getBytes());
|
||||||
|
Assert.assertNull("Verify token was removed from database", tokenInfo3);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateKeyId(Token<? extends AbstractDelegationTokenIdentifier> token,
|
||||||
|
int expectedKeyiD) throws IOException {
|
||||||
|
AbstractDelegationTokenIdentifier tokenIdentifier = token.decodeIdentifier();
|
||||||
|
Assert.assertEquals("Verify that keyId is assigned to token",
|
||||||
|
tokenIdentifier.getMasterKeyId(), expectedKeyiD);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Connection getTestDBConnection() {
|
||||||
|
try {
|
||||||
|
return DriverManager.getConnection(CONNECTION_URL);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void createTestDBTables() throws SQLException {
|
||||||
|
execute("CREATE TABLE Tokens (sequenceNum INT NOT NULL, "
|
||||||
|
+ "tokenIdentifier VARCHAR (255) FOR BIT DATA NOT NULL, "
|
||||||
|
+ "tokenInfo VARCHAR (255) FOR BIT DATA NOT NULL, "
|
||||||
|
+ "modifiedTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "
|
||||||
|
+ "PRIMARY KEY(sequenceNum))");
|
||||||
|
execute("CREATE TABLE DelegationKeys (keyId INT NOT NULL, "
|
||||||
|
+ "delegationKey VARCHAR (255) FOR BIT DATA NOT NULL, "
|
||||||
|
+ "modifiedTime TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, "
|
||||||
|
+ "PRIMARY KEY(keyId))");
|
||||||
|
execute("CREATE TABLE LastSequenceNum (sequenceNum INT NOT NULL)");
|
||||||
|
execute("INSERT INTO LastSequenceNum VALUES (0)");
|
||||||
|
execute("CREATE TABLE LastDelegationKeyId (keyId INT NOT NULL)");
|
||||||
|
execute("INSERT INTO LastDelegationKeyId VALUES (0)");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void dropTestDBTables() throws SQLException {
|
||||||
|
execute("DROP TABLE Tokens");
|
||||||
|
execute("DROP TABLE DelegationKeys");
|
||||||
|
execute("DROP TABLE LastSequenceNum");
|
||||||
|
execute("DROP TABLE LastDelegationKeyId");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void execute(String statement) throws SQLException {
|
||||||
|
try (Connection connection = getTestDBConnection()) {
|
||||||
|
connection.createStatement().execute(statement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopTokenManager(DelegationTokenManager tokenManager) {
|
||||||
|
TestDelegationTokenSecretManager secretManager =
|
||||||
|
(TestDelegationTokenSecretManager) tokenManager.getDelegationTokenSecretManager();
|
||||||
|
// Release any locks on tables
|
||||||
|
secretManager.unlockKeyRoll();
|
||||||
|
// Stop threads to close database connections
|
||||||
|
secretManager.stopThreads();
|
||||||
|
}
|
||||||
|
|
||||||
|
static class TestDelegationTokenSecretManager extends SQLDelegationTokenSecretManagerImpl {
|
||||||
|
private ReentrantLock keyRollLock;
|
||||||
|
|
||||||
|
private synchronized ReentrantLock getKeyRollLock() {
|
||||||
|
if (keyRollLock == null) {
|
||||||
|
keyRollLock = new ReentrantLock();
|
||||||
|
}
|
||||||
|
return keyRollLock;
|
||||||
|
}
|
||||||
|
|
||||||
|
TestDelegationTokenSecretManager() {
|
||||||
|
super(conf, new TestConnectionFactory(conf),
|
||||||
|
SQLSecretManagerRetriableHandlerImpl.getInstance(conf, new TestRetryHandler()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tests can call this method to prevent delegation keys from
|
||||||
|
// being rolled in the middle of a test to prevent race conditions
|
||||||
|
public void lockKeyRoll() {
|
||||||
|
getKeyRollLock().lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unlockKeyRoll() {
|
||||||
|
if (getKeyRollLock().isHeldByCurrentThread()) {
|
||||||
|
getKeyRollLock().unlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void rollMasterKey() throws IOException {
|
||||||
|
try {
|
||||||
|
lockKeyRoll();
|
||||||
|
super.rollMasterKey();
|
||||||
|
} finally {
|
||||||
|
unlockKeyRoll();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setReadOnly(boolean readOnly) {
|
||||||
|
((TestConnectionFactory) getConnectionFactory()).readOnly = readOnly;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class TestConnectionFactory extends HikariDataSourceConnectionFactory {
|
||||||
|
private boolean readOnly = false;
|
||||||
|
TestConnectionFactory(Configuration conf) {
|
||||||
|
super(conf);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Connection getConnection() throws SQLException {
|
||||||
|
Connection connection = super.getConnection();
|
||||||
|
// Change to default schema as derby driver looks for user schema
|
||||||
|
connection.setSchema("APP");
|
||||||
|
connection.setReadOnly(readOnly);
|
||||||
|
return connection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static class TestRetryHandler extends SQLSecretManagerRetriableHandlerImpl {
|
||||||
|
// Tracks the amount of times that a SQL command is executed, regardless of
|
||||||
|
// whether it completed successfully or not.
|
||||||
|
private static AtomicInteger executionAttemptCounter = new AtomicInteger();
|
||||||
|
|
||||||
|
static void resetExecutionAttemptCounter() {
|
||||||
|
executionAttemptCounter = new AtomicInteger();
|
||||||
|
}
|
||||||
|
|
||||||
|
static int getExecutionAttempts() {
|
||||||
|
return executionAttemptCounter.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void execute(SQLCommandVoid command) throws SQLException {
|
||||||
|
executionAttemptCounter.incrementAndGet();
|
||||||
|
super.execute(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -129,6 +129,8 @@
|
|||||||
<jcache.version>1.0-alpha-1</jcache.version>
|
<jcache.version>1.0-alpha-1</jcache.version>
|
||||||
<ehcache.version>3.3.1</ehcache.version>
|
<ehcache.version>3.3.1</ehcache.version>
|
||||||
<hikari.version>4.0.3</hikari.version>
|
<hikari.version>4.0.3</hikari.version>
|
||||||
|
<derby.version>10.10.2.0</derby.version>
|
||||||
|
<mysql-connector-java.version>8.0.29</mysql-connector-java.version>
|
||||||
<mssql.version>6.2.1.jre7</mssql.version>
|
<mssql.version>6.2.1.jre7</mssql.version>
|
||||||
<okhttp3.version>4.10.0</okhttp3.version>
|
<okhttp3.version>4.10.0</okhttp3.version>
|
||||||
<okio.version>3.2.0</okio.version>
|
<okio.version>3.2.0</okio.version>
|
||||||
@ -1809,6 +1811,16 @@
|
|||||||
<artifactId>HikariCP</artifactId>
|
<artifactId>HikariCP</artifactId>
|
||||||
<version>${hikari.version}</version>
|
<version>${hikari.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.derby</groupId>
|
||||||
|
<artifactId>derby</artifactId>
|
||||||
|
<version>${derby.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>mysql</groupId>
|
||||||
|
<artifactId>mysql-connector-java</artifactId>
|
||||||
|
<version>${mysql-connector-java.version}</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.microsoft.sqlserver</groupId>
|
<groupId>com.microsoft.sqlserver</groupId>
|
||||||
<artifactId>mssql-jdbc</artifactId>
|
<artifactId>mssql-jdbc</artifactId>
|
||||||
|
Loading…
Reference in New Issue
Block a user