This is an automated email from the ASF dual-hosted git repository.
smolnar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/knox.git
The following commit(s) were added to refs/heads/master by this push:
new b07dbee KNOX-2554 - Implemented JDBC Token State Service (#433)
b07dbee is described below
commit b07dbee0b9bccf877d351d6e596554fb76e894ca
Author: Sandor Molnar <[email protected]>
AuthorDate: Tue Apr 20 08:37:53 2021 +0200
KNOX-2554 - Implemented JDBC Token State Service (#433)
---
gateway-server/pom.xml | 26 +++
.../gateway/config/impl/GatewayConfigImpl.java | 53 +++--
.../services/factory/TokenStateServiceFactory.java | 6 +-
.../token/impl/DefaultTokenStateService.java | 30 +--
.../services/token/impl/JDBCTokenStateService.java | 227 +++++++++++++++++++++
.../services/token/impl/TokenStateDatabase.java | 114 +++++++++++
.../token/impl/TokenStateServiceMessages.java | 57 ++++++
.../org/apache/knox/gateway/util/JDBCUtils.java | 76 +++++++
.../resources/createKnoxTokenDatabaseTable.sql | 24 +++
.../token/impl/JDBCTokenStateServiceTest.java | 207 +++++++++++++++++++
.../apache/knox/gateway/util/JDBCUtilsTest.java | 90 ++++++++
.../gateway/shell/jdbc/derby/DerbyDatabase.java | 25 ++-
.../gateway/shell/table/KnoxShellTableTest.java | 2 +-
.../apache/knox/gateway/config/GatewayConfig.java | 9 +
.../security/token/TokenStateServiceException.java | 31 +++
.../org/apache/knox/gateway/GatewayTestConfig.java | 21 ++
pom.xml | 25 ++-
17 files changed, 985 insertions(+), 38 deletions(-)
diff --git a/gateway-server/pom.xml b/gateway-server/pom.xml
index 3a8f7a6..a393cf4 100644
--- a/gateway-server/pom.xml
+++ b/gateway-server/pom.xml
@@ -397,6 +397,14 @@
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.postgresql</groupId>
+ <artifactId>postgresql</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.derby</groupId>
+ <artifactId>derbyclient</artifactId>
+ </dependency>
<!-- ********** ********** ********** ********** ********** **********
-->
<!-- ********** Test Dependencies **********
-->
@@ -415,6 +423,24 @@
</dependency>
<dependency>
+ <groupId>org.apache.knox</groupId>
+ <artifactId>gateway-shell</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.derby</groupId>
+ <artifactId>derby</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.derby</groupId>
+ <artifactId>derbynet</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ <dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity</artifactId>
<scope>test</scope>
diff --git
a/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java
b/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java
index 458026b..6ae40fe 100644
---
a/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java
+++
b/gateway-server/src/main/java/org/apache/knox/gateway/config/impl/GatewayConfigImpl.java
@@ -17,19 +17,6 @@
*/
package org.apache.knox.gateway.config.impl;
-import org.apache.commons.io.FilenameUtils;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.hadoop.conf.Configuration;
-import org.apache.hadoop.fs.Path;
-import org.apache.knox.gateway.GatewayMessages;
-import org.apache.knox.gateway.config.GatewayConfig;
-import org.apache.knox.gateway.dto.HomePageProfile;
-import org.apache.knox.gateway.i18n.messages.MessagesFactory;
-import
org.apache.knox.gateway.services.security.impl.ZookeeperRemoteAliasService;
-import org.joda.time.Period;
-import org.joda.time.format.PeriodFormatter;
-import org.joda.time.format.PeriodFormatterBuilder;
-
import static
org.apache.knox.gateway.services.security.impl.RemoteAliasService.REMOTE_ALIAS_SERVICE_TYPE;
import java.io.File;
@@ -52,6 +39,20 @@ import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
+import org.apache.commons.io.FilenameUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.fs.Path;
+import org.apache.knox.gateway.GatewayMessages;
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.dto.HomePageProfile;
+import org.apache.knox.gateway.i18n.messages.MessagesFactory;
+import
org.apache.knox.gateway.services.security.impl.ZookeeperRemoteAliasService;
+import org.joda.time.Period;
+import org.joda.time.format.PeriodFormatter;
+import org.joda.time.format.PeriodFormatterBuilder;
+
+
/**
* The configuration for the Gateway.
*
@@ -271,6 +272,12 @@ public class GatewayConfigImpl extends Configuration
implements GatewayConfig {
private static final String KNOX_HOMEPAGE_LOGOUT_ENABLED =
"knox.homepage.logout.enabled";
private static final String KNOX_INCOMING_XFORWARDED_ENABLED =
"gateway.incoming.xforwarded.enabled";
+ //Gateway Database related properties
+ private static final String GATEWAY_DATABASE_TYPE =
GATEWAY_CONFIG_FILE_PREFIX + ".database.type";
+ private static final String GATEWAY_DATABASE_HOST =
GATEWAY_CONFIG_FILE_PREFIX + ".database.host";
+ private static final String GATEWAY_DATABASE_PORT =
GATEWAY_CONFIG_FILE_PREFIX + ".database.port";
+ private static final String GATEWAY_DATABASE_NAME =
GATEWAY_CONFIG_FILE_PREFIX + ".database.name";
+
public GatewayConfigImpl() {
init();
}
@@ -1231,4 +1238,24 @@ public class GatewayConfigImpl extends Configuration
implements GatewayConfig {
profiles.put("token", HomePageProfile.getTokenProfileElements());
return profiles;
}
+
+ @Override
+ public String getDatabaseType() {
+ return get(GATEWAY_DATABASE_TYPE, "none");
+ }
+
+ @Override
+ public String getDatabaseHost() {
+ return get(GATEWAY_DATABASE_HOST);
+ }
+
+ @Override
+ public int getDatabasePort() {
+ return getInt(GATEWAY_DATABASE_PORT, 0);
+ }
+
+ @Override
+ public String getDatabaseName() {
+ return get(GATEWAY_DATABASE_NAME, "GATEWAY_DATABASE");
+ }
}
diff --git
a/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/TokenStateServiceFactory.java
b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/TokenStateServiceFactory.java
index 7d8be07..e9c141e 100644
---
a/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/TokenStateServiceFactory.java
+++
b/gateway-server/src/main/java/org/apache/knox/gateway/services/factory/TokenStateServiceFactory.java
@@ -30,6 +30,7 @@ import
org.apache.knox.gateway.services.ServiceLifecycleException;
import org.apache.knox.gateway.services.ServiceType;
import org.apache.knox.gateway.services.token.impl.AliasBasedTokenStateService;
import org.apache.knox.gateway.services.token.impl.DefaultTokenStateService;
+import org.apache.knox.gateway.services.token.impl.JDBCTokenStateService;
import
org.apache.knox.gateway.services.token.impl.JournalBasedTokenStateService;
import org.apache.knox.gateway.services.token.impl.ZookeeperTokenStateService;
@@ -49,6 +50,9 @@ public class TokenStateServiceFactory extends
AbstractServiceFactory {
service = new JournalBasedTokenStateService();
} else if (matchesImplementation(implementation,
ZookeeperTokenStateService.class)) {
service = new ZookeeperTokenStateService(gatewayServices);
+ } else if (matchesImplementation(implementation,
JDBCTokenStateService.class)) {
+ service = new JDBCTokenStateService();
+ ((JDBCTokenStateService)
service).setAliasService(getAliasService(gatewayServices));
}
logServiceUsage(isEmptyDefaultImplementation(implementation) ?
AliasBasedTokenStateService.class.getName() : implementation, serviceType);
@@ -65,6 +69,6 @@ public class TokenStateServiceFactory extends
AbstractServiceFactory {
@Override
protected Collection<String> getKnownImplementations() {
return unmodifiableList(asList(DefaultTokenStateService.class.getName(),
AliasBasedTokenStateService.class.getName(),
JournalBasedTokenStateService.class.getName(),
- ZookeeperTokenStateService.class.getName()));
+ ZookeeperTokenStateService.class.getName(),
JDBCTokenStateService.class.getName()));
}
}
diff --git
a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateService.java
b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateService.java
index 8ffc0dc..9b44dfd 100644
---
a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateService.java
+++
b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateService.java
@@ -70,7 +70,7 @@ public class DefaultTokenStateService implements
TokenStateService {
private long tokenEvictionInterval;
// Grace period (in seconds) after which an expired token should be evicted
- private long tokenEvictionGracePeriod;
+ protected long tokenEvictionGracePeriod;
// Knox token validation permissiveness
protected boolean permissiveValidationEnabled;
@@ -332,18 +332,7 @@ public class DefaultTokenStateService implements
TokenStateService {
*/
protected void evictExpiredTokens() {
if (readyForEviction()) {
- final Set<String> tokensToEvict = new HashSet<>();
-
- for (final String tokenId : getTokenIds()) {
- try {
- if (needsEviction(tokenId)) {
- log.evictToken(Tokens.getTokenIDDisplayText(tokenId));
- tokensToEvict.add(tokenId); // Add the token to the set of tokens
to evict
- }
- } catch (final Exception e) {
-
log.failedExpiredTokenEviction(Tokens.getTokenIDDisplayText(tokenId), e);
- }
- }
+ final Set<String> tokensToEvict = getExpiredTokens();
if (!tokensToEvict.isEmpty()) {
removeTokens(tokensToEvict);
@@ -357,6 +346,21 @@ public class DefaultTokenStateService implements
TokenStateService {
return true;
}
+ protected Set<String> getExpiredTokens() {
+ final Set<String> expiredTokens = new HashSet<>();
+ for (final String tokenId : getTokenIds()) {
+ try {
+ if (needsEviction(tokenId)) {
+ log.evictToken(Tokens.getTokenIDDisplayText(tokenId));
+ expiredTokens.add(tokenId); // Add the token to the set of tokens to
evict
+ }
+ } catch (final Exception e) {
+ log.failedExpiredTokenEviction(Tokens.getTokenIDDisplayText(tokenId),
e);
+ }
+ }
+ return expiredTokens;
+ }
+
/**
* Method that checks if a token's state is a candidate for eviction.
*
diff --git
a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/JDBCTokenStateService.java
b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/JDBCTokenStateService.java
new file mode 100644
index 0000000..4a3e56a
--- /dev/null
+++
b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/JDBCTokenStateService.java
@@ -0,0 +1,227 @@
+/*
+ * 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.knox.gateway.services.token.impl;
+
+import java.sql.SQLException;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.services.ServiceLifecycleException;
+import org.apache.knox.gateway.services.security.AliasService;
+import org.apache.knox.gateway.services.security.token.TokenMetadata;
+import
org.apache.knox.gateway.services.security.token.TokenStateServiceException;
+import org.apache.knox.gateway.services.security.token.UnknownTokenException;
+import org.apache.knox.gateway.util.JDBCUtils;
+import org.apache.knox.gateway.util.Tokens;
+
+public class JDBCTokenStateService extends DefaultTokenStateService {
+ private AliasService aliasService; // connection username/pw is stored here
+ private TokenStateDatabase tokenDatabase;
+
+ public void setAliasService(AliasService aliasService) {
+ this.aliasService = aliasService;
+ }
+
+ @Override
+ public void init(GatewayConfig config, Map<String, String> options) throws
ServiceLifecycleException {
+ super.init(config, options);
+ if (aliasService == null) {
+ throw new ServiceLifecycleException("The required AliasService reference
has not been set.");
+ }
+ try {
+ this.tokenDatabase = new
TokenStateDatabase(JDBCUtils.getDataSource(config, aliasService));
+ } catch (Exception e) {
+ throw new ServiceLifecycleException("Error while initiating
JDBCTokenStateService: " + e, e);
+ }
+ }
+
+ @Override
+ public void addToken(String tokenId, long issueTime, long expiration, long
maxLifetimeDuration) {
+ try {
+ final boolean added = tokenDatabase.addToken(tokenId, issueTime,
expiration, maxLifetimeDuration);
+ if (added) {
+ log.savedTokenInDatabase(Tokens.getTokenIDDisplayText(tokenId));
+
+ // add in-memory
+ super.addToken(tokenId, issueTime, expiration, maxLifetimeDuration);
+ } else {
+ log.failedToSaveTokenInDatabase(Tokens.getTokenIDDisplayText(tokenId));
+ throw new TokenStateServiceException("Failed to save token " +
Tokens.getTokenIDDisplayText(tokenId) + " in the database");
+ }
+ } catch (SQLException e) {
+ log.errorSavingTokenInDatabase(Tokens.getTokenIDDisplayText(tokenId),
e.getMessage(), e);
+ throw new TokenStateServiceException("An error occurred while saving
token " + Tokens.getTokenIDDisplayText(tokenId) + " in the database", e);
+ }
+ }
+
+ @Override
+ public long getTokenExpiration(String tokenId, boolean validate) throws
UnknownTokenException {
+ try {
+ // check the in-memory cache, then
+ return super.getTokenExpiration(tokenId, validate);
+ } catch (UnknownTokenException e) {
+ // It's not in memory
+ }
+
+ if (validate) {
+ validateToken(tokenId);
+ }
+
+ long expiration = 0;
+ try {
+ expiration = tokenDatabase.getTokenExpiration(tokenId);
+ if (expiration > 0) {
+
log.fetchedExpirationFromDatabase(Tokens.getTokenIDDisplayText(tokenId),
expiration);
+
+ // Update the in-memory cache to avoid subsequent DB look-ups for the
same state
+ super.updateExpiration(tokenId, expiration);
+ } else {
+ throw new UnknownTokenException(tokenId);
+ }
+ } catch (SQLException e) {
+
log.errorFetchingExpirationFromDatabase(Tokens.getTokenIDDisplayText(tokenId),
e.getMessage(), e);
+ }
+ return expiration;
+ }
+
+ @Override
+ protected void updateExpiration(String tokenId, long expiration) {
+ try {
+ final boolean updated = tokenDatabase.updateExpiration(tokenId,
expiration);
+ if (updated) {
+ log.updatedExpirationInDatabase(Tokens.getTokenIDDisplayText(tokenId),
expiration);
+
+ // Update in-memory
+ super.updateExpiration(tokenId, expiration);
+ } else {
+
log.failedToUpdateExpirationInDatabase(Tokens.getTokenIDDisplayText(tokenId),
expiration);
+ throw new TokenStateServiceException("Failed to updated expiration for
" + Tokens.getTokenIDDisplayText(tokenId) + " in the database");
+ }
+ } catch (SQLException e) {
+
log.errorUpdatingExpirationInDatabase(Tokens.getTokenIDDisplayText(tokenId),
e.getMessage(), e);
+ throw new TokenStateServiceException("An error occurred while updating
expiration for " + Tokens.getTokenIDDisplayText(tokenId) + " in the database",
e);
+ }
+ }
+
+ @Override
+ protected long getMaxLifetime(String tokenId) {
+ long maxLifetime = super.getMaxLifetime(tokenId);
+
+ // If there is no result from the in-memory collection, proceed to check
the Database
+ if (maxLifetime < 1L) {
+ try {
+ maxLifetime = tokenDatabase.getMaxLifetime(tokenId);
+
log.fetchedMaxLifetimeFromDatabase(Tokens.getTokenIDDisplayText(tokenId),
maxLifetime);
+ } catch (SQLException e) {
+
log.errorFetchingMaxLifetimeFromDatabase(Tokens.getTokenIDDisplayText(tokenId),
e.getMessage(), e);
+ }
+ }
+ return maxLifetime;
+ }
+
+ @Override
+ protected boolean isUnknown(String tokenId) {
+ boolean isUnknown = super.isUnknown(tokenId);
+
+ // If it's not in the cache, then check in the Database
+ if (isUnknown) {
+ try {
+ isUnknown = tokenDatabase.getMaxLifetime(tokenId) < 0;
+ } catch (SQLException e) {
+
log.errorFetchingMaxLifetimeFromDatabase(Tokens.getTokenIDDisplayText(tokenId),
e.getMessage(), e);
+ }
+ }
+ return isUnknown;
+ }
+
+ @Override
+ protected void removeToken(String tokenId) throws UnknownTokenException {
+ try {
+ final boolean removed = tokenDatabase.removeToken(tokenId);
+ if (removed) {
+ super.removeToken(tokenId);
+ log.removedTokenFromDatabase(Tokens.getTokenIDDisplayText(tokenId));
+ } else {
+ throw new UnknownTokenException(tokenId);
+ }
+ } catch (SQLException e) {
+
log.errorRemovingTokenFromDatabase(Tokens.getTokenIDDisplayText(tokenId),
e.getMessage(), e);
+ }
+ }
+
+ @Override
+ protected void evictExpiredTokens() {
+ try {
+ int numOfExpiredTokens =
tokenDatabase.deleteExpiredTokens(TimeUnit.SECONDS.toMillis(tokenEvictionGracePeriod));
+ log.removedTokensFromDatabase(numOfExpiredTokens);
+
+ // remove from in-memory collections
+ super.evictExpiredTokens();
+ } catch (SQLException e) {
+ log.errorRemovingTokensFromDatabase(e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public void addMetadata(String tokenId, TokenMetadata metadata) {
+ try {
+ final boolean added = tokenDatabase.addMetadata(tokenId, metadata);
+ if (added) {
+ log.updatedMetadataInDatabase(Tokens.getTokenIDDisplayText(tokenId));
+
+ // Update in-memory
+ super.addMetadata(tokenId, metadata);
+ } else {
+
log.failedToUpdateMetadataInDatabase(Tokens.getTokenIDDisplayText(tokenId));
+ throw new TokenStateServiceException("Failed to update metadata for "
+ Tokens.getTokenIDDisplayText(tokenId) + " in the database");
+ }
+ } catch (SQLException e) {
+
log.errorUpdatingMetadataInDatabase(Tokens.getTokenIDDisplayText(tokenId),
e.getMessage(), e);
+ throw new TokenStateServiceException("An error occurred while updating
metadata for " + Tokens.getTokenIDDisplayText(tokenId) + " in the database", e);
+ }
+ }
+
+ @Override
+ public TokenMetadata getTokenMetadata(String tokenId) throws
UnknownTokenException {
+ TokenMetadata tokenMetadata = null;
+ try {
+ tokenMetadata = super.getTokenMetadata(tokenId);
+ } catch (UnknownTokenException e) {
+ // This is expected if the metadata is not yet part of the in-memory
record. In this case, the metadata will
+ // be retrieved from the database.
+ }
+
+ if (tokenMetadata == null) {
+ try {
+ tokenMetadata = tokenDatabase.getTokenMetadata(tokenId);
+
+ if (tokenMetadata != null) {
+
log.fetchedMetadataFromDatabase(Tokens.getTokenIDDisplayText(tokenId));
+ // Update the in-memory cache to avoid subsequent DB look-ups for
the same state
+ super.addMetadata(tokenId, tokenMetadata);
+ } else {
+ throw new UnknownTokenException(tokenId);
+ }
+ } catch (SQLException e) {
+
log.errorFetchingMetadataFromDatabase(Tokens.getTokenIDDisplayText(tokenId),
e.getMessage(), e);
+ }
+ }
+ return tokenMetadata;
+ }
+}
diff --git
a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/TokenStateDatabase.java
b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/TokenStateDatabase.java
new file mode 100644
index 0000000..3bb2882
--- /dev/null
+++
b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/TokenStateDatabase.java
@@ -0,0 +1,114 @@
+/*
+ * 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.knox.gateway.services.token.impl;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import javax.sql.DataSource;
+
+import org.apache.knox.gateway.services.security.token.TokenMetadata;
+
+public class TokenStateDatabase {
+ static final String TOKENS_TABLE_NAME = "KNOX_TOKENS";
+ private static final String ADD_TOKEN_SQL = "INSERT INTO " +
TOKENS_TABLE_NAME + "(token_id, issue_time, expiration, max_lifetime) VALUES(?,
?, ?, ?)";
+ private static final String REMOVE_TOKEN_SQL = "DELETE FROM " +
TOKENS_TABLE_NAME + " WHERE token_id = ?";
+ private static final String REMOVE_EXPIRED_TOKENS_SQL = "DELETE FROM " +
TOKENS_TABLE_NAME + " WHERE expiration < ?";
+ static final String GET_TOKEN_EXPIRATION_SQL = "SELECT expiration FROM " +
TOKENS_TABLE_NAME + " WHERE token_id = ?";
+ private static final String UPDATE_TOKEN_EXPIRATION_SQL = "UPDATE " +
TOKENS_TABLE_NAME + " SET expiration = ? WHERE token_id = ?";
+ static final String GET_MAX_LIFETIME_SQL = "SELECT max_lifetime FROM " +
TOKENS_TABLE_NAME + " WHERE token_id = ?";
+ private static final String ADD_METADATA_SQL = "UPDATE " + TOKENS_TABLE_NAME
+ " SET username = ?, comment = ? WHERE token_id = ?";
+ private static final String GET_METADATA_SQL = "SELECT username, comment
FROM " + TOKENS_TABLE_NAME + " WHERE token_id = ?";
+
+ private final DataSource dataSource;
+
+ TokenStateDatabase(DataSource dataSource) throws Exception {
+ this.dataSource = dataSource;
+ }
+
+ boolean addToken(String tokenId, long issueTime, long expiration, long
maxLifetimeDuration) throws SQLException {
+ try (Connection connection = dataSource.getConnection(); PreparedStatement
addTokenStatement = connection.prepareStatement(ADD_TOKEN_SQL)) {
+ addTokenStatement.setString(1, tokenId);
+ addTokenStatement.setLong(2, issueTime);
+ addTokenStatement.setLong(3, expiration);
+ addTokenStatement.setLong(4, issueTime + maxLifetimeDuration);
+ return addTokenStatement.executeUpdate() == 1;
+ }
+ }
+
+ boolean removeToken(String tokenId) throws SQLException {
+ try (Connection connection = dataSource.getConnection(); PreparedStatement
addTokenStatement = connection.prepareStatement(REMOVE_TOKEN_SQL)) {
+ addTokenStatement.setString(1, tokenId);
+ return addTokenStatement.executeUpdate() == 1;
+ }
+ }
+
+ long getTokenExpiration(String tokenId) throws SQLException {
+ try (Connection connection = dataSource.getConnection(); PreparedStatement
getTokenExpirationStatement =
connection.prepareStatement(GET_TOKEN_EXPIRATION_SQL)) {
+ getTokenExpirationStatement.setString(1, tokenId);
+ try (ResultSet rs = getTokenExpirationStatement.executeQuery()) {
+ return rs.next() ? rs.getLong(1) : -1;
+ }
+ }
+ }
+
+ boolean updateExpiration(final String tokenId, long expiration) throws
SQLException {
+ try (Connection connection = dataSource.getConnection(); PreparedStatement
updateTokenExpirationStatement =
connection.prepareStatement(UPDATE_TOKEN_EXPIRATION_SQL)) {
+ updateTokenExpirationStatement.setLong(1, expiration);
+ updateTokenExpirationStatement.setString(2, tokenId);
+ return updateTokenExpirationStatement.executeUpdate() == 1;
+ }
+ }
+
+ long getMaxLifetime(String tokenId) throws SQLException {
+ try (Connection connection = dataSource.getConnection(); PreparedStatement
getMaxLifetimeStatement = connection.prepareStatement(GET_MAX_LIFETIME_SQL)) {
+ getMaxLifetimeStatement.setString(1, tokenId);
+ try (ResultSet rs = getMaxLifetimeStatement.executeQuery()) {
+ return rs.next() ? rs.getLong(1) : -1;
+ }
+ }
+ }
+
+ int deleteExpiredTokens(long tokenEvictionGracePeriod) throws SQLException {
+ try (Connection connection = dataSource.getConnection(); PreparedStatement
deleteExpiredTokensStatement =
connection.prepareStatement(REMOVE_EXPIRED_TOKENS_SQL)) {
+ deleteExpiredTokensStatement.setLong(1, System.currentTimeMillis() -
tokenEvictionGracePeriod);
+ return deleteExpiredTokensStatement.executeUpdate();
+ }
+ }
+
+ boolean addMetadata(String tokenId, TokenMetadata metadata) throws
SQLException {
+ try (Connection connection = dataSource.getConnection(); PreparedStatement
addMetadataStatement = connection.prepareStatement(ADD_METADATA_SQL)) {
+ addMetadataStatement.setString(1, metadata.getUserName());
+ addMetadataStatement.setString(2, metadata.getComment());
+ addMetadataStatement.setString(3, tokenId);
+ return addMetadataStatement.executeUpdate() == 1;
+ }
+ }
+
+ TokenMetadata getTokenMetadata(String tokenId) throws SQLException {
+ try (Connection connection = dataSource.getConnection(); PreparedStatement
getMaxLifetimeStatement = connection.prepareStatement(GET_METADATA_SQL)) {
+ getMaxLifetimeStatement.setString(1, tokenId);
+ try (ResultSet rs = getMaxLifetimeStatement.executeQuery()) {
+ return rs.next() ? new TokenMetadata(rs.getString(1), rs.getString(2))
: null;
+ }
+ }
+ }
+
+}
diff --git
a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/TokenStateServiceMessages.java
b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/TokenStateServiceMessages.java
index ceb11a2..a5eeece 100644
---
a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/TokenStateServiceMessages.java
+++
b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/TokenStateServiceMessages.java
@@ -177,4 +177,61 @@ public interface TokenStateServiceMessages {
@Message(level = MessageLevel.INFO, text = "Removed related token alias {0}
on receiving signal from Zookeeper ")
void onRemoteTokenStateRemoval(String alias);
+
+ @Message(level = MessageLevel.DEBUG, text = "Token {0} has been saved in the
database")
+ void savedTokenInDatabase(String tokenId);
+
+ @Message(level = MessageLevel.ERROR, text = "Failed to save token {0} in the
database")
+ void failedToSaveTokenInDatabase(String tokenId);
+
+ @Message(level = MessageLevel.ERROR, text = "An error occurred while saving
token {0} in the database : {1}")
+ void errorSavingTokenInDatabase(String tokenId, String errorMessage,
@StackTrace(level = MessageLevel.DEBUG) Exception e);
+
+ @Message(level = MessageLevel.DEBUG, text = "Token {0} has been removed from
the database")
+ void removedTokenFromDatabase(String tokenId);
+
+ @Message(level = MessageLevel.ERROR, text = "An error occurred while
removing token {0} from the database : {1}")
+ void errorRemovingTokenFromDatabase(String tokenId, String errorMessage,
@StackTrace(level = MessageLevel.DEBUG) Exception e);
+
+ @Message(level = MessageLevel.DEBUG, text = "{0} expired tokens have been
removed from the database")
+ void removedTokensFromDatabase(int size);
+
+ @Message(level = MessageLevel.ERROR, text = "An error occurred while
removing expired tokens from the database : {1}")
+ void errorRemovingTokensFromDatabase(String errorMessage, @StackTrace(level
= MessageLevel.DEBUG) Exception e);
+
+ @Message(level = MessageLevel.DEBUG, text = "Fetched expiration for {0} from
the database : {1}")
+ void fetchedExpirationFromDatabase(String tokenId, long expiration);
+
+ @Message(level = MessageLevel.ERROR, text = "An error occurred while
fetching expiration for {0} from the database : {1}")
+ void errorFetchingExpirationFromDatabase(String tokenId, String
errorMessage, @StackTrace(level = MessageLevel.DEBUG) Exception e);
+
+ @Message(level = MessageLevel.DEBUG, text = "Updated expiration for {0} in
the database to {1}")
+ void updatedExpirationInDatabase(String tokenId, long expiration);
+
+ @Message(level = MessageLevel.DEBUG, text = "Failed to updated expiration
for {0} in the database to {1}")
+ void failedToUpdateExpirationInDatabase(String tokenId, long expiration);
+
+ @Message(level = MessageLevel.ERROR, text = "An error occurred while
updating expiration for {0} in the database : {1}")
+ void errorUpdatingExpirationInDatabase(String tokenId, String errorMessage,
@StackTrace(level = MessageLevel.DEBUG) Exception e);
+
+ @Message(level = MessageLevel.DEBUG, text = "Fetched max lifetime for {0}
from the database : {1}")
+ void fetchedMaxLifetimeFromDatabase(String tokenId, long maxLifetime);
+
+ @Message(level = MessageLevel.ERROR, text = "An error occurred while
fetching max lifetime for {0} from the database : {1}")
+ void errorFetchingMaxLifetimeFromDatabase(String tokenId, String
errorMessage, @StackTrace(level = MessageLevel.DEBUG) Exception e);
+
+ @Message(level = MessageLevel.DEBUG, text = "Updated metadata for {0} in the
database")
+ void updatedMetadataInDatabase(String tokenId);
+
+ @Message(level = MessageLevel.DEBUG, text = "Failed to update metadata for
{0} in the database")
+ void failedToUpdateMetadataInDatabase(String tokenId);
+
+ @Message(level = MessageLevel.ERROR, text = "An error occurred while
updating metadata for {0} in the database : {1}")
+ void errorUpdatingMetadataInDatabase(String tokenId, String errorMessage,
@StackTrace(level = MessageLevel.DEBUG) Exception e);
+
+ @Message(level = MessageLevel.DEBUG, text = "Fetched metadata for {0} from
the database")
+ void fetchedMetadataFromDatabase(String tokenId);
+
+ @Message(level = MessageLevel.ERROR, text = "An error occurred while
fetching metadata for {0} from the database : {1}")
+ void errorFetchingMetadataFromDatabase(String tokenId, String errorMessage,
@StackTrace(level = MessageLevel.DEBUG) Exception e);
}
diff --git
a/gateway-server/src/main/java/org/apache/knox/gateway/util/JDBCUtils.java
b/gateway-server/src/main/java/org/apache/knox/gateway/util/JDBCUtils.java
new file mode 100644
index 0000000..04b6cd6
--- /dev/null
+++ b/gateway-server/src/main/java/org/apache/knox/gateway/util/JDBCUtils.java
@@ -0,0 +1,76 @@
+/*
+ * 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.knox.gateway.util;
+
+import javax.sql.DataSource;
+
+import org.apache.derby.jdbc.ClientDataSource;
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.services.security.AliasService;
+import org.apache.knox.gateway.services.security.AliasServiceException;
+import org.postgresql.ds.PGSimpleDataSource;
+
+public class JDBCUtils {
+ public static final String POSTGRES_DB_TYPE = "postgres";
+ public static final String DERBY_DB_TYPE = "derbydb";
+ public static final String DATABASE_USER_ALIAS_NAME =
"gateway_database_user";
+ public static final String DATABASE_PASSWORD_ALIAS_NAME =
"gateway_database_password";
+
+ public static DataSource getDataSource(GatewayConfig gatewayConfig,
AliasService aliasService) throws AliasServiceException {
+ if (POSTGRES_DB_TYPE.equalsIgnoreCase(gatewayConfig.getDatabaseType())) {
+ return createPostgresDataSource(gatewayConfig, aliasService);
+ } else if
(DERBY_DB_TYPE.equalsIgnoreCase(gatewayConfig.getDatabaseType())) {
+ return createDerbyDatasource(gatewayConfig, aliasService);
+ }
+ throw new IllegalArgumentException("Invalid database type: " +
gatewayConfig.getDatabaseType());
+ }
+
+ private static DataSource createPostgresDataSource(GatewayConfig
gatewayConfig, AliasService aliasService) throws AliasServiceException {
+ final PGSimpleDataSource postgresDataSource = new PGSimpleDataSource();
+ postgresDataSource.setDatabaseName(gatewayConfig.getDatabaseName());
+ postgresDataSource.setServerNames(new String[] {
gatewayConfig.getDatabaseHost() });
+ postgresDataSource.setPortNumbers(new int[] {
gatewayConfig.getDatabasePort() });
+ postgresDataSource.setUser(getDatabaseUser(aliasService));
+ postgresDataSource.setPassword(getDatabasePassword(aliasService));
+ return postgresDataSource;
+ }
+
+ private static DataSource createDerbyDatasource(GatewayConfig gatewayConfig,
AliasService aliasService) throws AliasServiceException {
+ final ClientDataSource derbyDatasource = new ClientDataSource();
+ derbyDatasource.setDatabaseName(gatewayConfig.getDatabaseName());
+ derbyDatasource.setServerName(gatewayConfig.getDatabaseHost());
+ derbyDatasource.setPortNumber(gatewayConfig.getDatabasePort());
+ derbyDatasource.setUser(getDatabaseUser(aliasService));
+ derbyDatasource.setPassword(getDatabasePassword(aliasService));
+ return derbyDatasource;
+ }
+
+ private static String getDatabaseUser(AliasService aliasService) throws
AliasServiceException {
+ return getDatabaseAlias(aliasService, DATABASE_USER_ALIAS_NAME);
+ }
+
+ private static String getDatabasePassword(AliasService aliasService) throws
AliasServiceException {
+ return getDatabaseAlias(aliasService, DATABASE_PASSWORD_ALIAS_NAME);
+ }
+
+ private static String getDatabaseAlias(AliasService aliasService, String
aliasName) throws AliasServiceException {
+ final char[] value =
aliasService.getPasswordFromAliasForGateway(aliasName);
+ return value == null ? null : new String(value);
+ }
+
+}
diff --git a/gateway-server/src/main/resources/createKnoxTokenDatabaseTable.sql
b/gateway-server/src/main/resources/createKnoxTokenDatabaseTable.sql
new file mode 100644
index 0000000..faeca79
--- /dev/null
+++ b/gateway-server/src/main/resources/createKnoxTokenDatabaseTable.sql
@@ -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.
+
+CREATE TABLE KNOX_TOKENS (
+ token_id varchar(128) NOT NULL,
+ issue_time bigint NOT NULL,
+ expiration bigint NOT NULL,
+ max_lifetime bigint NOT NULL,
+ username varchar(128),
+ comment varchar(256),
+ PRIMARY KEY (token_id)
+)
\ No newline at end of file
diff --git
a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/JDBCTokenStateServiceTest.java
b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/JDBCTokenStateServiceTest.java
new file mode 100644
index 0000000..a5c750b
--- /dev/null
+++
b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/JDBCTokenStateServiceTest.java
@@ -0,0 +1,207 @@
+/*
+ * 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.knox.gateway.services.token.impl;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.commons.io.FileUtils.readFileToString;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.UUID;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.derby.drda.NetworkServerControl;
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.services.security.AliasService;
+import org.apache.knox.gateway.services.security.token.TokenMetadata;
+import org.apache.knox.gateway.services.security.token.UnknownTokenException;
+import org.apache.knox.gateway.shell.jdbc.Database;
+import org.apache.knox.gateway.shell.jdbc.derby.DerbyDatabase;
+import org.apache.knox.gateway.util.JDBCUtils;
+import org.easymock.EasyMock;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+public class JDBCTokenStateServiceTest {
+
+ private static final String GET_TOKENS_COUNT_SQL = "SELECT count(*) FROM " +
TokenStateDatabase.TOKENS_TABLE_NAME;
+ private static final String GET_USERNAME_SQL = "SELECT username FROM " +
TokenStateDatabase.TOKENS_TABLE_NAME + " WHERE token_id = ?";
+ private static final String GET_COMMENT_SQL = "SELECT comment FROM " +
TokenStateDatabase.TOKENS_TABLE_NAME + " WHERE token_id = ?";
+ private static final String TRUNCATE_KNOX_TOKENS_SQL = "TRUNCATE TABLE " +
TokenStateDatabase.TOKENS_TABLE_NAME;
+
+ @ClassRule
+ public static final TemporaryFolder testFolder = new TemporaryFolder();
+
+ private static final String SYSTEM_PROPERTY_DERBY_STREAM_ERROR_FILE =
"derby.stream.error.file";
+ private static final String SAMPLE_DERBY_DATABASE_NAME =
"sampleDerbyDatabase";
+ private static NetworkServerControl derbyNetworkServerControl;
+ private static Database derbyDatabase;
+ private static JDBCTokenStateService jdbcTokenStateService;
+
+ @SuppressWarnings("PMD.JUnit4TestShouldUseBeforeAnnotation")
+ @BeforeClass
+ public static void setUp() throws Exception {
+ final String username = "app";
+ final String password = "P4ssW0rd!";
+ System.setProperty(SYSTEM_PROPERTY_DERBY_STREAM_ERROR_FILE, "/dev/null");
+ derbyNetworkServerControl = new NetworkServerControl(username, password);
+ derbyNetworkServerControl.start(null);
+ TimeUnit.SECONDS.sleep(1); // give a bit of time for the server to start
+ final Path derbyDatabaseFolder =
Paths.get(testFolder.newFolder().toPath().toString(),
SAMPLE_DERBY_DATABASE_NAME);
+ final GatewayConfig gatewayConfig =
EasyMock.createNiceMock(GatewayConfig.class);
+
EasyMock.expect(gatewayConfig.getDatabaseType()).andReturn(JDBCUtils.DERBY_DB_TYPE).anyTimes();
+
EasyMock.expect(gatewayConfig.getDatabaseHost()).andReturn("localhost").anyTimes();
+
EasyMock.expect(gatewayConfig.getDatabasePort()).andReturn(NetworkServerControl.DEFAULT_PORTNUMBER).anyTimes();
+
EasyMock.expect(gatewayConfig.getDatabaseName()).andReturn(derbyDatabaseFolder.toString()).anyTimes();
+ final AliasService aliasService =
EasyMock.createNiceMock(AliasService.class);
+
EasyMock.expect(aliasService.getPasswordFromAliasForGateway(JDBCUtils.DATABASE_USER_ALIAS_NAME)).andReturn(username.toCharArray()).anyTimes();
+
EasyMock.expect(aliasService.getPasswordFromAliasForGateway(JDBCUtils.DATABASE_PASSWORD_ALIAS_NAME)).andReturn(password.toCharArray()).anyTimes();
+ EasyMock.replay(gatewayConfig, aliasService);
+
+ derbyDatabase = prepareDerbyDatabase(derbyDatabaseFolder);
+ assertTrue(derbyDatabase.hasTable(TokenStateDatabase.TOKENS_TABLE_NAME));
+
+ jdbcTokenStateService = new JDBCTokenStateService();
+ jdbcTokenStateService.setAliasService(aliasService);
+ jdbcTokenStateService.init(gatewayConfig, null);
+ }
+
+ private static Database prepareDerbyDatabase(Path derbyDatabaseFolder)
throws SQLException, IOException {
+ final Database derbyDatabase = new
DerbyDatabase(derbyDatabaseFolder.toString(), true);
+ derbyDatabase.create();
+ final String createTableSql = readFileToString(new
File(JDBCTokenStateServiceTest.class.getClassLoader().getResource("createKnoxTokenDatabaseTable.sql").getFile()),
UTF_8);
+ try (Connection connection = derbyDatabase.getConnection(); Statement
createTableStatment = connection.createStatement();) {
+ createTableStatment.execute(createTableSql);
+ }
+ return derbyDatabase;
+ }
+
+ @SuppressWarnings("PMD.JUnit4TestShouldUseAfterAnnotation")
+ @AfterClass
+ public static void tearDown() throws Exception {
+ if (derbyDatabase != null) {
+ derbyDatabase.shutdown();
+ }
+ derbyNetworkServerControl.shutdown();
+ System.clearProperty(SYSTEM_PROPERTY_DERBY_STREAM_ERROR_FILE);
+ }
+
+ @Test
+ public void testAddToken() throws Exception {
+ final String tokenId = UUID.randomUUID().toString();
+ long issueTime = System.currentTimeMillis();
+ long maxLifetimeDuration = 1000;
+ long expiration = issueTime + maxLifetimeDuration;
+ jdbcTokenStateService.addToken(tokenId, issueTime, expiration,
maxLifetimeDuration);
+
+ assertEquals(expiration,
jdbcTokenStateService.getTokenExpiration(tokenId));
+ assertEquals(issueTime + maxLifetimeDuration,
jdbcTokenStateService.getMaxLifetime(tokenId));
+
+ assertEquals(expiration, getLongTokenAttributeFromDatabase(tokenId,
TokenStateDatabase.GET_TOKEN_EXPIRATION_SQL));
+ assertEquals(issueTime + maxLifetimeDuration,
getLongTokenAttributeFromDatabase(tokenId,
TokenStateDatabase.GET_MAX_LIFETIME_SQL));
+ }
+
+ @Test(expected = UnknownTokenException.class)
+ public void testRemoveToken() throws Exception {
+ truncateDatabase();
+ final String tokenId = UUID.randomUUID().toString();
+ jdbcTokenStateService.addToken(tokenId, 1, 1, 1);
+ assertEquals(1, getLongTokenAttributeFromDatabase(null,
GET_TOKENS_COUNT_SQL));
+ jdbcTokenStateService.removeToken(tokenId);
+ assertEquals(0, getLongTokenAttributeFromDatabase(null,
GET_TOKENS_COUNT_SQL));
+ jdbcTokenStateService.getTokenExpiration(tokenId);
+ }
+
+ @Test
+ public void testUpdateExpiration() throws Exception {
+ final String tokenId = UUID.randomUUID().toString();
+ jdbcTokenStateService.addToken(tokenId, 1, 1, 1);
+ jdbcTokenStateService.updateExpiration(tokenId, 2);
+
+ assertEquals(2, jdbcTokenStateService.getTokenExpiration(tokenId));
+ assertEquals(2, getLongTokenAttributeFromDatabase(tokenId,
TokenStateDatabase.GET_TOKEN_EXPIRATION_SQL));
+ }
+
+ @Test(expected = UnknownTokenException.class)
+ public void testAddMetadata() throws Exception {
+ final String tokenId = UUID.randomUUID().toString();
+ jdbcTokenStateService.addToken(tokenId, 1, 1, 1);
+ jdbcTokenStateService.addMetadata(tokenId, new TokenMetadata("sampleUser",
"my test comment"));
+
+ assertEquals("sampleUser",
jdbcTokenStateService.getTokenMetadata(tokenId).getUserName());
+ assertEquals("my test comment",
jdbcTokenStateService.getTokenMetadata(tokenId).getComment());
+
+ assertEquals("sampleUser", getStringTokenAttributeFromDatabase(tokenId,
GET_USERNAME_SQL));
+ assertEquals("my test comment",
getStringTokenAttributeFromDatabase(tokenId, GET_COMMENT_SQL));
+
+ jdbcTokenStateService.removeToken(tokenId);
+ jdbcTokenStateService.getTokenMetadata(tokenId);
+ }
+
+ @Test
+ public void testEvictExpiredTokens() throws Exception {
+ truncateDatabase();
+ final int tokenCount = 1000;
+ for (int i = 0; i < tokenCount; i++) {
+ final String tokenId = UUID.randomUUID().toString();
+ jdbcTokenStateService.addToken(tokenId, 1, 1, 1);
+ }
+ assertEquals(tokenCount, getLongTokenAttributeFromDatabase(null,
GET_TOKENS_COUNT_SQL));
+ jdbcTokenStateService.evictExpiredTokens();
+ assertEquals(0, getLongTokenAttributeFromDatabase(null,
GET_TOKENS_COUNT_SQL));
+ }
+
+ private long getLongTokenAttributeFromDatabase(String tokenId, String sql)
throws SQLException {
+ try (Connection conn = derbyDatabase.getConnection(); PreparedStatement
stmt = conn.prepareStatement(sql)) {
+ if (tokenId != null) {
+ stmt.setString(1, tokenId);
+ }
+ try (ResultSet rs = stmt.executeQuery()) {
+ return rs.next() ? rs.getLong(1) : 0;
+ }
+ }
+ }
+
+ private String getStringTokenAttributeFromDatabase(String tokenId, String
sql) throws SQLException {
+ try (Connection conn = derbyDatabase.getConnection(); PreparedStatement
stmt = conn.prepareStatement(sql)) {
+ stmt.setString(1, tokenId);
+ try (ResultSet rs = stmt.executeQuery()) {
+ return rs.next() ? rs.getString(1) : null;
+ }
+ }
+ }
+
+ private void truncateDatabase() throws SQLException {
+ try (Connection conn = derbyDatabase.getConnection(); PreparedStatement
stmt = conn.prepareStatement(TRUNCATE_KNOX_TOKENS_SQL)) {
+ stmt.executeUpdate();
+ }
+ }
+
+}
diff --git
a/gateway-server/src/test/java/org/apache/knox/gateway/util/JDBCUtilsTest.java
b/gateway-server/src/test/java/org/apache/knox/gateway/util/JDBCUtilsTest.java
new file mode 100644
index 0000000..0e37e55
--- /dev/null
+++
b/gateway-server/src/test/java/org/apache/knox/gateway/util/JDBCUtilsTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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.knox.gateway.util;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.derby.jdbc.ClientDataSource;
+import org.apache.knox.gateway.config.GatewayConfig;
+import org.apache.knox.gateway.services.security.AliasService;
+import org.apache.knox.gateway.services.security.AliasServiceException;
+import org.easymock.EasyMock;
+import org.junit.Test;
+import org.postgresql.ds.PGSimpleDataSource;
+
+public class JDBCUtilsTest {
+
+ @Test
+ public void shouldReturnPostgresDataSource() throws Exception {
+ final GatewayConfig gatewayConfig =
EasyMock.createNiceMock(GatewayConfig.class);
+
EasyMock.expect(gatewayConfig.getDatabaseType()).andReturn(JDBCUtils.POSTGRES_DB_TYPE).anyTimes();
+ final AliasService aliasService =
EasyMock.createNiceMock(AliasService.class);
+
EasyMock.expect(aliasService.getPasswordFromAliasForGateway(EasyMock.anyString())).andReturn(null).anyTimes();
+ EasyMock.replay(gatewayConfig, aliasService);
+ assertTrue(JDBCUtils.getDataSource(gatewayConfig, aliasService) instanceof
PGSimpleDataSource);
+ }
+
+ @Test
+ public void postgresDataSourceShouldHaveProperConnectionProperties() throws
AliasServiceException {
+ final GatewayConfig gatewayConfig =
EasyMock.createNiceMock(GatewayConfig.class);
+
EasyMock.expect(gatewayConfig.getDatabaseType()).andReturn(JDBCUtils.POSTGRES_DB_TYPE).anyTimes();
+
EasyMock.expect(gatewayConfig.getDatabaseHost()).andReturn("localhost").anyTimes();
+
EasyMock.expect(gatewayConfig.getDatabasePort()).andReturn(5432).anyTimes();
+
EasyMock.expect(gatewayConfig.getDatabaseName()).andReturn("sampleDatabase");
+ final AliasService aliasService =
EasyMock.createNiceMock(AliasService.class);
+
EasyMock.expect(aliasService.getPasswordFromAliasForGateway(JDBCUtils.DATABASE_USER_ALIAS_NAME)).andReturn("user".toCharArray()).anyTimes();
+
EasyMock.expect(aliasService.getPasswordFromAliasForGateway(JDBCUtils.DATABASE_PASSWORD_ALIAS_NAME)).andReturn("password".toCharArray()).anyTimes();
+ EasyMock.replay(gatewayConfig, aliasService);
+ final PGSimpleDataSource dataSource = (PGSimpleDataSource)
JDBCUtils.getDataSource(gatewayConfig, aliasService);
+ assertEquals("localhost", dataSource.getServerNames()[0]);
+ assertEquals(5432, dataSource.getPortNumbers()[0]);
+ assertEquals("sampleDatabase", dataSource.getDatabaseName());
+ assertEquals("user", dataSource.getUser());
+ assertEquals("password", dataSource.getPassword());
+ }
+
+ @Test
+ public void shouldReturnDerbyDataSource() throws Exception {
+ final GatewayConfig gatewayConfig =
EasyMock.createNiceMock(GatewayConfig.class);
+
EasyMock.expect(gatewayConfig.getDatabaseType()).andReturn(JDBCUtils.DERBY_DB_TYPE).anyTimes();
+ final AliasService aliasService =
EasyMock.createNiceMock(AliasService.class);
+
EasyMock.expect(aliasService.getPasswordFromAliasForGateway(EasyMock.anyString())).andReturn(null).anyTimes();
+ EasyMock.replay(gatewayConfig, aliasService);
+ assertTrue(JDBCUtils.getDataSource(gatewayConfig, aliasService) instanceof
ClientDataSource);
+ }
+
+ @Test
+ public void derbyDataSourceShouldHaveProperConnectionProperties() throws
AliasServiceException {
+ final GatewayConfig gatewayConfig =
EasyMock.createNiceMock(GatewayConfig.class);
+
EasyMock.expect(gatewayConfig.getDatabaseType()).andReturn(JDBCUtils.DERBY_DB_TYPE).anyTimes();
+
EasyMock.expect(gatewayConfig.getDatabaseHost()).andReturn("localhost").anyTimes();
+
EasyMock.expect(gatewayConfig.getDatabasePort()).andReturn(1527).anyTimes();
+
EasyMock.expect(gatewayConfig.getDatabaseName()).andReturn("sampleDatabase");
+ final AliasService aliasService =
EasyMock.createNiceMock(AliasService.class);
+
EasyMock.expect(aliasService.getPasswordFromAliasForGateway(JDBCUtils.DATABASE_USER_ALIAS_NAME)).andReturn("user".toCharArray()).anyTimes();
+
EasyMock.expect(aliasService.getPasswordFromAliasForGateway(JDBCUtils.DATABASE_PASSWORD_ALIAS_NAME)).andReturn("password".toCharArray()).anyTimes();
+ EasyMock.replay(gatewayConfig, aliasService);
+ final ClientDataSource dataSource = (ClientDataSource)
JDBCUtils.getDataSource(gatewayConfig, aliasService);
+ assertEquals("localhost", dataSource.getServerName());
+ assertEquals(1527, dataSource.getPortNumber());
+ assertEquals("sampleDatabase", dataSource.getDatabaseName());
+ assertEquals("user", dataSource.getUser());
+ assertEquals("password", dataSource.getPassword());
+ }
+}
diff --git
a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/jdbc/derby/DerbyDatabase.java
b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/jdbc/derby/DerbyDatabase.java
index 61edfc5..88405b6 100644
---
a/gateway-shell/src/main/java/org/apache/knox/gateway/shell/jdbc/derby/DerbyDatabase.java
+++
b/gateway-shell/src/main/java/org/apache/knox/gateway/shell/jdbc/derby/DerbyDatabase.java
@@ -27,10 +27,12 @@ import org.apache.knox.gateway.shell.jdbc.Database;
public class DerbyDatabase implements Database {
- public static final String DRIVER = "org.apache.derby.jdbc.EmbeddedDriver";
+ public static final String EMBEDDED_DRIVER =
"org.apache.derby.jdbc.EmbeddedDriver";
+ public static final String NETWORK_SERVER_DRIVER =
"org.apache.derby.jdbc.ClientDriver";
public static final String PROTOCOL = "jdbc:derby:";
private static final String CREATE_ATTRIBUTE = ";create=true";
private static final String SHUTDOWN_ATTRIBUTE = ";shutdown=true";
+ private static final String DEFAULT_SCHEMA_NAME = "APP";
private final String dbUri;
@@ -43,8 +45,12 @@ public class DerbyDatabase implements Database {
* if the database engine can not be load for some reasons
*/
public DerbyDatabase(String directory) throws DerbyDatabaseException {
- this.dbUri = PROTOCOL + directory;
- loadDriver();
+ this(directory, false);
+ }
+
+ public DerbyDatabase(String directory, boolean networkServer) throws
DerbyDatabaseException {
+ this.dbUri = PROTOCOL + (networkServer ? "//localhost:1527/" : "") +
directory;
+ loadDriver(networkServer);
}
@Override
@@ -87,7 +93,7 @@ public class DerbyDatabase implements Database {
@Override
public boolean hasTable(String tableName) throws SQLException {
- return hasTable(null, tableName);
+ return hasTable(DEFAULT_SCHEMA_NAME, tableName);
}
@Override
@@ -103,15 +109,16 @@ public class DerbyDatabase implements Database {
return result;
}
- private void loadDriver() throws DerbyDatabaseException {
+ private void loadDriver(boolean networkServer) throws DerbyDatabaseException
{
+ final String driverToLoad = networkServer ? NETWORK_SERVER_DRIVER :
EMBEDDED_DRIVER;
try {
- Class.forName(DRIVER).newInstance();
+ Class.forName(driverToLoad).newInstance();
} catch (ClassNotFoundException e) {
- throw new DerbyDatabaseException("Unable to load the JDBC driver " +
DRIVER + ". Check your CLASSPATH.", e);
+ throw new DerbyDatabaseException("Unable to load the JDBC driver " +
driverToLoad + ". Check your CLASSPATH.", e);
} catch (InstantiationException e) {
- throw new DerbyDatabaseException("Unable to instantiate the JDBC driver
" + DRIVER, e);
+ throw new DerbyDatabaseException("Unable to instantiate the JDBC driver
" + driverToLoad, e);
} catch (IllegalAccessException e) {
- throw new DerbyDatabaseException("Not allowed to access the JDBC driver
" + DRIVER, e);
+ throw new DerbyDatabaseException("Not allowed to access the JDBC driver
" + driverToLoad, e);
}
}
diff --git
a/gateway-shell/src/test/java/org/apache/knox/gateway/shell/table/KnoxShellTableTest.java
b/gateway-shell/src/test/java/org/apache/knox/gateway/shell/table/KnoxShellTableTest.java
index e44e6a1..b46bc9f 100644
---
a/gateway-shell/src/test/java/org/apache/knox/gateway/shell/table/KnoxShellTableTest.java
+++
b/gateway-shell/src/test/java/org/apache/knox/gateway/shell/table/KnoxShellTableTest.java
@@ -479,7 +479,7 @@ public class KnoxShellTableTest {
try {
derbyDatabase = prepareDerbyDatabase(derbyDatabaseFolder);
assertTrue(derbyDatabase.hasTable("BOOKS"));
- final KnoxShellTable table =
KnoxShellTable.builder().jdbc().driver(DerbyDatabase.DRIVER).connectTo(DerbyDatabase.PROTOCOL
+ derbyDatabaseFolder.toString())
+ final KnoxShellTable table =
KnoxShellTable.builder().jdbc().driver(DerbyDatabase.EMBEDDED_DRIVER).connectTo(DerbyDatabase.PROTOCOL
+ derbyDatabaseFolder.toString())
.sql("select * from books");
assertEquals(2, table.getRows().size());
assertTrue(table.values("TITLE").containsAll(Arrays.asList("Apache Knox:
The Definitive Guide", "Apache Knox: The Definitive Guide 2nd Edition")));
diff --git
a/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java
b/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java
index 020eb63..4d87061 100644
---
a/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java
+++
b/gateway-spi/src/main/java/org/apache/knox/gateway/config/GatewayConfig.java
@@ -747,4 +747,13 @@ public interface GatewayConfig {
* It's important that keys in the returned map are converted to lowercase
strings.
*/
Map<String, Collection<String>> getHomePageProfiles();
+
+ String getDatabaseType();
+
+ String getDatabaseHost();
+
+ int getDatabasePort();
+
+ String getDatabaseName();
+
}
diff --git
a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenStateServiceException.java
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenStateServiceException.java
new file mode 100644
index 0000000..6c8c6f0
--- /dev/null
+++
b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenStateServiceException.java
@@ -0,0 +1,31 @@
+/*
+ * 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.knox.gateway.services.security.token;
+
+@SuppressWarnings("serial")
+public class TokenStateServiceException extends RuntimeException {
+
+ public TokenStateServiceException(String message) {
+ super(message);
+ }
+
+ public TokenStateServiceException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
diff --git
a/gateway-test-release-utils/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
b/gateway-test-release-utils/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
index b84834d..6291e70 100644
---
a/gateway-test-release-utils/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
+++
b/gateway-test-release-utils/src/main/java/org/apache/knox/gateway/GatewayTestConfig.java
@@ -853,4 +853,25 @@ public class GatewayTestConfig extends Configuration
implements GatewayConfig {
public Map<String, Collection<String>> getHomePageProfiles() {
return null;
}
+
+ @Override
+ public String getDatabaseType() {
+ return null;
+ }
+
+ @Override
+ public String getDatabaseHost() {
+ return null;
+ }
+
+ @Override
+ public int getDatabasePort() {
+ return 0;
+ }
+
+ @Override
+ public String getDatabaseName() {
+ return null;
+ }
+
}
diff --git a/pom.xml b/pom.xml
index 4eadaa5..9e546e6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -250,6 +250,7 @@
<okhttp.version>2.7.5</okhttp.version>
<opensaml.version>3.4.5</opensaml.version>
<pac4j.version>4.3.0</pac4j.version>
+ <postgresql.version>42.2.19</postgresql.version>
<protobuf.version>3.14.0</protobuf.version>
<rest-assured.version>4.3.3</rest-assured.version>
<shiro.version>1.7.0</shiro.version>
@@ -2041,6 +2042,17 @@
<artifactId>swagger-annotations</artifactId>
<version>${swagger-annotations.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.postgresql</groupId>
+ <artifactId>postgresql</artifactId>
+ <version>${postgresql.version}</version>
+ <exclusions>
+ <exclusion>
+ <groupId>org.checkerframework</groupId>
+ <artifactId>checker-qual</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
<!-- pac4j Dependencies -->
<dependency>
@@ -2328,7 +2340,18 @@
<groupId>org.apache.derby</groupId>
<artifactId>derby</artifactId>
<version>${derby.db.version}</version>
- <scope>test</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.derby</groupId>
+ <artifactId>derbyclient</artifactId>
+ <version>${derby.db.version}</version>
+ </dependency>
+
+ <dependency>
+ <groupId>org.apache.derby</groupId>
+ <artifactId>derbynet</artifactId>
+ <version>${derby.db.version}</version>
</dependency>
<dependency>