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 9e2b831 KNOX-2557 - Persisting token related metadata using Knox's alias service (#420) 9e2b831 is described below commit 9e2b831343974670057c24ea590cb942f2433be3 Author: Sandor Molnar <smol...@cloudera.com> AuthorDate: Mon Mar 22 22:25:14 2021 +0100 KNOX-2557 - Persisting token related metadata using Knox's alias service (#420) --- .../token/impl/AliasBasedTokenStateService.java | 77 +++++++++++++++++++- .../token/impl/DefaultTokenStateService.java | 13 ++++ .../token/impl/JournalBasedTokenStateService.java | 20 +++++- .../token/impl/ZookeeperTokenStateService.java | 3 + .../token/impl/state/FileTokenStateJournal.java | 46 +++++++++--- .../impl/state/MultiFileTokenStateJournal.java | 5 +- .../gateway/services/token/state/JournalEntry.java | 8 +++ .../services/token/state/TokenStateJournal.java | 5 +- .../impl/AliasBasedTokenStateServiceTest.java | 28 ++++++-- .../token/impl/DefaultTokenStateServiceTest.java | 62 +++++++++++----- .../impl/JournalBasedTokenStateServiceTest.java | 3 +- .../token/impl/ZookeeperTokenStateServiceTest.java | 8 +++ .../state/AbstractFileTokenStateJournalTest.java | 6 +- .../impl/state/FileTokenStateJournalTest.java | 78 +++++++++++--------- .../gateway/service/knoxtoken/TokenResource.java | 2 + .../knoxtoken/TokenServiceResourceTest.java | 10 +++ .../services/security/token/TokenMetadata.java | 82 ++++++++++++++++++++++ .../services/security/token/TokenStateService.java | 18 +++++ 18 files changed, 392 insertions(+), 82 deletions(-) diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateService.java index 545fa96..4ac128a 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateService.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateService.java @@ -42,6 +42,7 @@ import org.apache.knox.gateway.services.ServiceLifecycleException; import org.apache.knox.gateway.services.security.AliasService; import org.apache.knox.gateway.services.security.AliasServiceException; import org.apache.knox.gateway.services.security.impl.DefaultKeystoreService; +import org.apache.knox.gateway.services.security.token.TokenMetadata; import org.apache.knox.gateway.services.security.token.UnknownTokenException; import org.apache.knox.gateway.services.token.TokenStateServiceStatistics; import org.apache.knox.gateway.services.token.impl.state.TokenStateJournalFactory; @@ -55,6 +56,7 @@ import org.apache.knox.gateway.util.ExecutorServiceUtils; public class AliasBasedTokenStateService extends DefaultTokenStateService implements TokenStatePeristerMonitorListener { static final String TOKEN_MAX_LIFETIME_POSTFIX = "--max"; + static final String TOKEN_META_POSTFIX = "--meta"; protected AliasService aliasService; @@ -157,6 +159,9 @@ public class AliasBasedTokenStateService extends DefaultTokenStateService implem super.updateExpiration(tokenId, expiration); super.setMaxLifetime(tokenId, maxLifeTime); count+=2; + } else if (alias.endsWith(TOKEN_META_POSTFIX)) { + tokenId = alias.substring(0, alias.indexOf(TOKEN_META_POSTFIX)); + super.addMetadata(tokenId, TokenMetadata.fromJSON(new String(passwordAliasMapEntry.getValue()))); } // log some progress (it's very useful in case a huge amount of token related aliases in __gateway-credentials.jceks) @@ -265,7 +270,7 @@ public class AliasBasedTokenStateService extends DefaultTokenStateService implem } try { - journal.add(tokenId, issueTime, expiration, maxLifetimeDuration); + journal.add(tokenId, issueTime, expiration, maxLifetimeDuration, null); } catch (IOException e) { log.failedToAddJournalEntry(tokenId, e); } @@ -419,8 +424,45 @@ public class AliasBasedTokenStateService extends DefaultTokenStateService implem super.updateExpiration(tokenId, expiration); } + @Override + public void addMetadata(String tokenId, TokenMetadata metadata) { + addMetadataInMemory(tokenId, metadata); + try { + final JournalEntry entry = journal.get(tokenId); + if (entry != null) { + journal.add(entry.getTokenId(), Long.parseLong(entry.getIssueTime()), Long.parseLong(entry.getExpiration()), Long.parseLong(entry.getMaxLifetime()), metadata); + } + } catch (IOException e) { + log.failedToAddJournalEntry(tokenId, e); + } + + synchronized (unpersistedState) { + unpersistedState.add(new TokenMetadataState(tokenId, metadata)); + } + } + + protected void addMetadataInMemory(String tokenId, TokenMetadata metadata) { + super.addMetadata(tokenId, metadata); + } + + @Override + public TokenMetadata getTokenMetadata(String tokenId) { + TokenMetadata tokenMetadata = super.getTokenMetadata(tokenId); + if (tokenMetadata == null) { + try { + final char[] tokenMetadataAliasValue = getPasswordUsingAliasService(tokenId + TOKEN_META_POSTFIX); + if (tokenMetadataAliasValue != null) { + tokenMetadata = TokenMetadata.fromJSON(new String(tokenMetadataAliasValue)); + } + } catch (AliasServiceException e) { + log.errorAccessingTokenState(tokenId, e); + } + } + return tokenMetadata; + } + enum TokenStateType { - EXP(1), MAX(2); + EXP(1), MAX(2), META(3); private final int id; @@ -538,4 +580,35 @@ public class AliasBasedTokenStateService extends DefaultTokenStateService implem } } + private static final class TokenMetadataState implements TokenState { + + private final String tokenId; + private final TokenMetadata metadata; + + TokenMetadataState(String tokenId, TokenMetadata metadata) { + this.tokenId = tokenId; + this.metadata = metadata; + } + + @Override + public String getTokenId() { + return tokenId; + } + + @Override + public String getAlias() { + return tokenId + TOKEN_META_POSTFIX; + } + + @Override + public String getAliasValue() { + return metadata.toJSON(); + } + + @Override + public TokenStateType getType() { + return TokenStateType.META; + } + } + } 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 93d8560..f08412d 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 @@ -38,6 +38,7 @@ import javax.management.ObjectName; import org.apache.knox.gateway.config.GatewayConfig; import org.apache.knox.gateway.i18n.messages.MessagesFactory; import org.apache.knox.gateway.services.ServiceLifecycleException; +import org.apache.knox.gateway.services.security.token.TokenMetadata; import org.apache.knox.gateway.services.security.token.TokenStateService; import org.apache.knox.gateway.services.security.token.TokenUtils; import org.apache.knox.gateway.services.security.token.UnknownTokenException; @@ -62,6 +63,8 @@ public class DefaultTokenStateService implements TokenStateService { private final Map<String, Long> maxTokenLifetimes = new ConcurrentHashMap<>(); + private final Map<String, TokenMetadata> metadataMap = new ConcurrentHashMap<>(); + // Token eviction interval (in seconds) private long tokenEvictionInterval; @@ -282,6 +285,7 @@ public class DefaultTokenStateService implements TokenStateService { private void removeTokenState(final Set<String> tokenIds) { tokenExpirations.keySet().removeAll(tokenIds); maxTokenLifetimes.keySet().removeAll(tokenIds); + metadataMap.keySet().removeAll(tokenIds); log.removedTokenState(String.join(", ", tokenIds)); } @@ -375,4 +379,13 @@ public class DefaultTokenStateService implements TokenStateService { return tokenExpirations.keySet().stream().collect(Collectors.toList()); } + @Override + public void addMetadata(String tokenId, TokenMetadata metadata) { + metadataMap.put(tokenId, metadata); + } + + @Override + public TokenMetadata getTokenMetadata(String tokenId) { + return metadataMap.get(tokenId); + } } diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/JournalBasedTokenStateService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/JournalBasedTokenStateService.java index abee0e5..25597e2 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/JournalBasedTokenStateService.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/JournalBasedTokenStateService.java @@ -20,6 +20,7 @@ package org.apache.knox.gateway.services.token.impl; import org.apache.knox.gateway.config.GatewayConfig; import org.apache.knox.gateway.services.ServiceLifecycleException; +import org.apache.knox.gateway.services.security.token.TokenMetadata; import org.apache.knox.gateway.services.security.token.UnknownTokenException; import org.apache.knox.gateway.services.token.impl.state.TokenStateJournalFactory; import org.apache.knox.gateway.services.token.state.JournalEntry; @@ -68,7 +69,7 @@ public class JournalBasedTokenStateService extends DefaultTokenStateService { super.addToken(tokenId, issueTime, expiration, maxLifetimeDuration); try { - journal.add(tokenId, issueTime, expiration, maxLifetimeDuration); + journal.add(tokenId, issueTime, expiration, maxLifetimeDuration, null); } catch (IOException e) { log.failedToAddJournalEntry(tokenId, e); } @@ -151,7 +152,8 @@ public class JournalBasedTokenStateService extends DefaultTokenStateService { journal.add(entry.getTokenId(), Long.parseLong(entry.getIssueTime()), expiration, - Long.parseLong(entry.getMaxLifetime())); + Long.parseLong(entry.getMaxLifetime()), + entry.getTokenMetadata()); } } catch (IOException e) { log.errorAccessingTokenState(e); @@ -170,4 +172,18 @@ public class JournalBasedTokenStateService extends DefaultTokenStateService { return (entry == null); } + @Override + public void addMetadata(String tokenId, TokenMetadata metadata) { + super.addMetadata(tokenId, metadata); + try { + JournalEntry entry = journal.get(tokenId); + if (entry == null) { + log.journalEntryNotFound(tokenId); + } else { + journal.add(entry.getTokenId(), Long.parseLong(entry.getIssueTime()), Long.parseLong(entry.getExpiration()), Long.parseLong(entry.getMaxLifetime()), metadata); + } + } catch (IOException e) { + log.errorAccessingTokenState(e); + } + } } diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/ZookeeperTokenStateService.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/ZookeeperTokenStateService.java index 161bfc3..60d0300 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/ZookeeperTokenStateService.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/ZookeeperTokenStateService.java @@ -30,6 +30,7 @@ import org.apache.knox.gateway.services.ServiceLifecycleException; import org.apache.knox.gateway.services.factory.AliasServiceFactory; import org.apache.knox.gateway.services.security.AliasServiceException; import org.apache.knox.gateway.services.security.impl.ZookeeperRemoteAliasService; +import org.apache.knox.gateway.services.security.token.TokenMetadata; import org.apache.knox.gateway.services.token.RemoteTokenStateChangeListener; /** @@ -133,6 +134,8 @@ public class ZookeeperTokenStateService extends AliasBasedTokenStateService impl if (alias.endsWith(TOKEN_MAX_LIFETIME_POSTFIX)) { final long maxLifeTime = Long.parseLong(value); setMaxLifetime(tokenId, maxLifeTime); + } else if (alias.endsWith(TOKEN_META_POSTFIX)) { + addMetadataInMemory(tokenId, TokenMetadata.fromJSON(value)); } else { final long expiration = Long.parseLong(value); updateExpirationInMemory(tokenId, expiration); diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/state/FileTokenStateJournal.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/state/FileTokenStateJournal.java index 11dd702..966766d 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/state/FileTokenStateJournal.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/state/FileTokenStateJournal.java @@ -22,6 +22,7 @@ import org.apache.knox.gateway.config.GatewayConfig; import org.apache.knox.gateway.i18n.messages.MessagesFactory; import org.apache.knox.gateway.services.token.state.JournalEntry; import org.apache.knox.gateway.services.token.state.TokenStateJournal; +import org.apache.knox.gateway.services.security.token.TokenMetadata; import org.apache.knox.gateway.services.token.impl.TokenStateServiceMessages; import java.io.BufferedReader; @@ -49,6 +50,8 @@ abstract class FileTokenStateJournal implements TokenStateJournal { protected static final int INDEX_ISSUE_TIME = 1; protected static final int INDEX_EXPIRATION = 2; protected static final int INDEX_MAX_LIFETIME = 3; + protected static final int INDEX_USERNAME = 4; + protected static final int INDEX_COMMENT = 5; protected static final TokenStateServiceMessages log = MessagesFactory.get(TokenStateServiceMessages.class); @@ -68,7 +71,7 @@ abstract class FileTokenStateJournal implements TokenStateJournal { } @Override - public abstract void add(String tokenId, long issueTime, long expiration, long maxLifetime) throws IOException; + public abstract void add(String tokenId, long issueTime, long expiration, long maxLifetime, TokenMetadata tokenMetadata) throws IOException; @Override public void add(JournalEntry entry) throws IOException { @@ -133,24 +136,31 @@ abstract class FileTokenStateJournal implements TokenStateJournal { /** * A JournalEntry implementation for File-based TokenStateJournal implementations */ - static final class FileJournalEntry implements JournalEntry { + public static final class FileJournalEntry implements JournalEntry { private final String tokenId; private final String issueTime; private final String expiration; private final String maxLifetime; + private final TokenMetadata tokenMetadata; FileJournalEntry(final String tokenId, long issueTime, long expiration, long maxLifetime) { - this(tokenId, String.valueOf(issueTime), String.valueOf(expiration), String.valueOf(maxLifetime)); + this(tokenId, String.valueOf(issueTime), String.valueOf(expiration), String.valueOf(maxLifetime), null); + } + + FileJournalEntry(final String tokenId, long issueTime, long expiration, long maxLifetime, TokenMetadata tokenMetadata) { + this(tokenId, String.valueOf(issueTime), String.valueOf(expiration), String.valueOf(maxLifetime), tokenMetadata); } FileJournalEntry(final String tokenId, final String issueTime, final String expiration, - final String maxLifetime) { + final String maxLifetime, + final TokenMetadata tokenMetadata) { this.tokenId = tokenId; this.issueTime = issueTime; this.expiration = expiration; this.maxLifetime = maxLifetime; + this.tokenMetadata = tokenMetadata; } @Override @@ -174,8 +184,13 @@ abstract class FileTokenStateJournal implements TokenStateJournal { } @Override + public TokenMetadata getTokenMetadata() { + return tokenMetadata; + } + + @Override public String toString() { - String[] elements = new String[4]; + String[] elements = new String[6]; elements[INDEX_TOKEN_ID] = getTokenId(); @@ -188,12 +203,20 @@ abstract class FileTokenStateJournal implements TokenStateJournal { String maxLifetime = getMaxLifetime(); elements[INDEX_MAX_LIFETIME] = (maxLifetime != null) ? maxLifetime : ""; + String userName = getTokenMetadata() == null ? "" : (getTokenMetadata().getUserName() == null ? "" : getTokenMetadata().getUserName()); + elements[INDEX_USERNAME] = userName; + + String comment = getTokenMetadata() == null ? "" : (getTokenMetadata().getComment() == null ? "" : getTokenMetadata().getComment()); + elements[INDEX_COMMENT] = comment; + return String.format(Locale.ROOT, - "%s,%s,%s,%s", + "%s,%s,%s,%s,%s,%s", elements[INDEX_TOKEN_ID], elements[INDEX_ISSUE_TIME], elements[INDEX_EXPIRATION], - elements[INDEX_MAX_LIFETIME]); + elements[INDEX_MAX_LIFETIME], + elements[INDEX_USERNAME], + elements[INDEX_COMMENT]); } /** @@ -204,8 +227,8 @@ abstract class FileTokenStateJournal implements TokenStateJournal { * @return A FileJournalEntry object created from the specified entry. */ static FileJournalEntry parse(final String entry) { - String[] elements = entry.split(","); - if (elements.length < 4) { + String[] elements = entry.split(",", -1); + if (elements.length < 6) { throw new IllegalArgumentException("Invalid journal entry: " + entry); } @@ -213,11 +236,14 @@ abstract class FileTokenStateJournal implements TokenStateJournal { String issueTime = elements[INDEX_ISSUE_TIME].trim(); String expiration = elements[INDEX_EXPIRATION].trim(); String maxLifetime = elements[INDEX_MAX_LIFETIME].trim(); + String userName = elements[INDEX_USERNAME].trim(); + String comment = elements[INDEX_COMMENT].trim(); return new FileJournalEntry(tokenId.isEmpty() ? null : tokenId, issueTime.isEmpty() ? null : issueTime, expiration.isEmpty() ? null : expiration, - maxLifetime.isEmpty() ? null : maxLifetime); + maxLifetime.isEmpty() ? null : maxLifetime, + new TokenMetadata(userName.isEmpty() ? null : userName, comment.isEmpty() ? null : comment)); } } diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/state/MultiFileTokenStateJournal.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/state/MultiFileTokenStateJournal.java index dfdd1e1..6fccd60 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/state/MultiFileTokenStateJournal.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/impl/state/MultiFileTokenStateJournal.java @@ -19,6 +19,7 @@ package org.apache.knox.gateway.services.token.impl.state; import org.apache.knox.gateway.config.GatewayConfig; +import org.apache.knox.gateway.services.security.token.TokenMetadata; import org.apache.knox.gateway.services.token.state.JournalEntry; import java.io.BufferedWriter; @@ -53,8 +54,8 @@ class MultiFileTokenStateJournal extends FileTokenStateJournal { } @Override - public void add(final String tokenId, long issueTime, long expiration, long maxLifetime) throws IOException { - add(Collections.singletonList(new FileJournalEntry(tokenId, issueTime, expiration, maxLifetime))); + public void add(final String tokenId, long issueTime, long expiration, long maxLifetime, TokenMetadata tokenMetadata) throws IOException { + add(Collections.singletonList(new FileJournalEntry(tokenId, issueTime, expiration, maxLifetime, tokenMetadata))); } @Override diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/state/JournalEntry.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/state/JournalEntry.java index d520f45..ccd7833 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/state/JournalEntry.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/state/JournalEntry.java @@ -18,6 +18,8 @@ */ package org.apache.knox.gateway.services.token.state; +import org.apache.knox.gateway.services.security.token.TokenMetadata; + /** * An entry in the TokenStateJournal */ @@ -48,4 +50,10 @@ public interface JournalEntry { * @return The token's maximum allowed lifetime */ String getMaxLifetime(); + + /** + * @return The metadata belongs to this token + */ + TokenMetadata getTokenMetadata(); + } diff --git a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/state/TokenStateJournal.java b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/state/TokenStateJournal.java index a51d162..f29378c 100644 --- a/gateway-server/src/main/java/org/apache/knox/gateway/services/token/state/TokenStateJournal.java +++ b/gateway-server/src/main/java/org/apache/knox/gateway/services/token/state/TokenStateJournal.java @@ -22,6 +22,8 @@ import java.io.IOException; import java.util.Collection; import java.util.List; +import org.apache.knox.gateway.services.security.token.TokenMetadata; + /** * */ @@ -34,10 +36,11 @@ public interface TokenStateJournal { * @param issueTime The issue timestamp * @param expiration The expiration time * @param maxLifetime The maximum allowed lifetime + * @param tokenMetafata The associated token metadata * * @throws IOException exception on error */ - void add(String tokenId, long issueTime, long expiration, long maxLifetime) + void add(String tokenId, long issueTime, long expiration, long maxLifetime, TokenMetadata tokenMetadata) throws IOException; /** diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateServiceTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateServiceTest.java index 5da2f5d..fc9f316 100644 --- a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateServiceTest.java +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/AliasBasedTokenStateServiceTest.java @@ -21,6 +21,7 @@ import org.apache.knox.gateway.services.ServiceLifecycleException; import org.apache.knox.gateway.services.security.AbstractAliasService; import org.apache.knox.gateway.services.security.AliasService; import org.apache.knox.gateway.services.security.AliasServiceException; +import org.apache.knox.gateway.services.security.token.TokenMetadata; import org.apache.knox.gateway.services.security.token.TokenStateService; import org.apache.knox.gateway.services.security.token.impl.JWTToken; import org.apache.knox.gateway.services.token.state.JournalEntry; @@ -511,7 +512,8 @@ public class AliasBasedTokenStateServiceTest extends DefaultTokenStateServiceTes journal.add(token.getClaim(JWTToken.KNOX_ID_CLAIM), System.currentTimeMillis(), token.getExpiresDate().getTime(), - System.currentTimeMillis() + TimeUnit.HOURS.toMillis(24)); + System.currentTimeMillis() + TimeUnit.HOURS.toMillis(24), + null); } AliasBasedTokenStateService tss = new NoEvictionAliasBasedTokenStateService(); @@ -563,32 +565,37 @@ public class AliasBasedTokenStateServiceTest extends DefaultTokenStateServiceTes journal.add(token.getClaim(JWTToken.KNOX_ID_CLAIM), System.currentTimeMillis(), token.getExpiresDate().getTime(), - System.currentTimeMillis() + TimeUnit.HOURS.toMillis(24)); + System.currentTimeMillis() + TimeUnit.HOURS.toMillis(24), + null); } // Add an entry with an invalid token identifier journal.add(" ", System.currentTimeMillis(), System.currentTimeMillis(), - System.currentTimeMillis()); + System.currentTimeMillis(), + null); // Add an entry with an invalid issue time journal.add(new TestJournalEntry(UUID.randomUUID().toString(), "invalidLongValue", String.valueOf(System.currentTimeMillis()), - String.valueOf(System.currentTimeMillis()))); + String.valueOf(System.currentTimeMillis()), + new TokenMetadata("testUser"))); // Add an entry with an invalid expiration time journal.add(new TestJournalEntry(UUID.randomUUID().toString(), String.valueOf(System.currentTimeMillis()), "invalidLongValue", - String.valueOf(System.currentTimeMillis()))); + String.valueOf(System.currentTimeMillis()), + new TokenMetadata("testUser"))); // Add an entry with an invalid max lifetime journal.add(new TestJournalEntry(UUID.randomUUID().toString(), String.valueOf(System.currentTimeMillis()), String.valueOf(System.currentTimeMillis()), - "invalidLongValue")); + "invalidLongValue", + new TokenMetadata("testUser"))); AliasBasedTokenStateService tss = new NoEvictionAliasBasedTokenStateService(); tss.setAliasService(aliasService); @@ -844,12 +851,14 @@ public class AliasBasedTokenStateServiceTest extends DefaultTokenStateServiceTes private String issueTime; private String expiration; private String maxLifetime; + private TokenMetadata tokenMetadata; - TestJournalEntry(String tokenId, String issueTime, String expiration, String maxLifetime) { + TestJournalEntry(String tokenId, String issueTime, String expiration, String maxLifetime, TokenMetadata tokenMetadata) { this.tokenId = tokenId; this.issueTime = issueTime; this.expiration = expiration; this.maxLifetime = maxLifetime; + this.tokenMetadata = tokenMetadata; } @Override @@ -873,6 +882,11 @@ public class AliasBasedTokenStateServiceTest extends DefaultTokenStateServiceTes } @Override + public TokenMetadata getTokenMetadata() { + return tokenMetadata; + } + + @Override public String toString() { return tokenId + "," + issueTime + "," + expiration + "," + maxLifetime; } diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateServiceTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateServiceTest.java index 6eaff90..1ed01ec 100644 --- a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateServiceTest.java +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/DefaultTokenStateServiceTest.java @@ -16,20 +16,13 @@ */ package org.apache.knox.gateway.services.token.impl; -import com.nimbusds.jose.JWSSigner; -import com.nimbusds.jose.crypto.RSASSASigner; -import org.apache.knox.gateway.config.GatewayConfig; -import org.apache.knox.gateway.services.ServiceLifecycleException; -import org.apache.knox.gateway.services.security.token.TokenStateService; -import org.apache.knox.gateway.services.security.token.TokenUtils; -import org.apache.knox.gateway.services.security.token.impl.JWT; -import org.apache.knox.gateway.services.security.token.UnknownTokenException; -import org.apache.knox.gateway.services.security.token.impl.JWTToken; -import org.easymock.EasyMock; -import org.junit.BeforeClass; -import org.junit.Rule; -import org.junit.Test; -import org.junit.rules.TemporaryFolder; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import java.io.IOException; import java.nio.file.Files; @@ -42,11 +35,22 @@ import java.util.Date; import java.util.UUID; import java.util.concurrent.TimeUnit; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import org.apache.knox.gateway.config.GatewayConfig; +import org.apache.knox.gateway.services.ServiceLifecycleException; +import org.apache.knox.gateway.services.security.token.TokenMetadata; +import org.apache.knox.gateway.services.security.token.TokenStateService; +import org.apache.knox.gateway.services.security.token.TokenUtils; +import org.apache.knox.gateway.services.security.token.UnknownTokenException; +import org.apache.knox.gateway.services.security.token.impl.JWT; +import org.apache.knox.gateway.services.security.token.impl.JWTToken; +import org.easymock.EasyMock; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import com.nimbusds.jose.JWSSigner; +import com.nimbusds.jose.crypto.RSASSASigner; public class DefaultTokenStateServiceTest { @@ -262,6 +266,26 @@ public class DefaultTokenStateServiceTest { tss.getTokenExpiration(token); } + @Test + public void testAddTokenMetadata() throws Exception { + final JWT token = getJWTToken(System.currentTimeMillis()); + final String tokenId = token.getClaim(JWTToken.KNOX_ID_CLAIM); + final TokenStateService tss = new DefaultTokenStateService(); + tss.addToken((JWTToken) token, System.currentTimeMillis()); + assertNull(tss.getTokenMetadata(tokenId)); + + final String userName = "testUser"; + tss.addMetadata(token.getClaim(JWTToken.KNOX_ID_CLAIM), new TokenMetadata(userName)); + assertNotNull(tss.getTokenMetadata(tokenId)); + assertEquals(tss.getTokenMetadata(tokenId).getUserName(), userName); + assertTrue(tss.getTokenMetadata(tokenId).getComment().isEmpty()); + + final String comment = "this is my test comment"; + tss.addMetadata(token.getClaim(JWTToken.KNOX_ID_CLAIM), new TokenMetadata(userName, comment)); + assertNotNull(tss.getTokenMetadata(tokenId)); + assertEquals(tss.getTokenMetadata(tokenId).getComment(), comment); + } + protected static JWTToken createMockToken(final long expiration) { return createMockToken("abcD1234eFGHIJKLmnoPQRSTUVwXYz", expiration); } diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/JournalBasedTokenStateServiceTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/JournalBasedTokenStateServiceTest.java index bcfaec3..bb5c99f 100644 --- a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/JournalBasedTokenStateServiceTest.java +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/JournalBasedTokenStateServiceTest.java @@ -157,7 +157,8 @@ public class JournalBasedTokenStateServiceTest extends DefaultTokenStateServiceT testJournal.add(uncachedTokenId, System.currentTimeMillis(), uncachedToken.getExpiresDate().getTime(), - maxTokenLifetime); + maxTokenLifetime, + null); assertEquals("Expected the uncached journal entry", 1, testJournal.get().size()); // Create and initialize the TokenStateService diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/ZookeeperTokenStateServiceTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/ZookeeperTokenStateServiceTest.java index 443d409..a31aa47 100644 --- a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/ZookeeperTokenStateServiceTest.java +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/ZookeeperTokenStateServiceTest.java @@ -48,6 +48,7 @@ import org.apache.knox.gateway.services.security.AliasService; import org.apache.knox.gateway.services.security.KeystoreService; import org.apache.knox.gateway.services.security.KeystoreServiceException; import org.apache.knox.gateway.services.security.MasterService; +import org.apache.knox.gateway.services.security.token.TokenMetadata; import org.easymock.EasyMock; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -115,6 +116,13 @@ public class ZookeeperTokenStateServiceTest { final long expiration = zktokenStateServiceNode2.getTokenExpiration("node1Token"); Thread.sleep(LONG_TOKEN_STATE_ALIAS_PERSISTENCE_INTERVAL * 1000); assertEquals(2000L, expiration); + + final String userName = "testUser"; + final String comment = "This is my test comment"; + zktokenStateServiceNode1.addMetadata("node1Token", new TokenMetadata(userName, comment)); + Thread.sleep(LONG_TOKEN_STATE_ALIAS_PERSISTENCE_INTERVAL * 1000); + assertEquals(userName, zktokenStateServiceNode2.getTokenMetadata("node1Token").getUserName()); + assertEquals(comment, zktokenStateServiceNode2.getTokenMetadata("node1Token").getComment()); } @Test diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/state/AbstractFileTokenStateJournalTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/state/AbstractFileTokenStateJournalTest.java index 5d64f71..1c5e65f 100644 --- a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/state/AbstractFileTokenStateJournalTest.java +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/state/AbstractFileTokenStateJournalTest.java @@ -79,7 +79,7 @@ public abstract class AbstractFileTokenStateJournalTest { long issueTime = System.currentTimeMillis(); long expiration = issueTime + TimeUnit.MINUTES.toMillis(5); long maxLifetime = issueTime + (5 * TimeUnit.MINUTES.toMillis(5)); - journal.add(tokenId, issueTime, expiration, maxLifetime); + journal.add(tokenId, issueTime, expiration, maxLifetime, null); // Get the token state from the journal, and validate its contents JournalEntry entry = journal.get(tokenId); @@ -109,7 +109,7 @@ public abstract class AbstractFileTokenStateJournalTest { long issueTime = System.currentTimeMillis(); long expiration = issueTime + TimeUnit.MINUTES.toMillis(5); long maxLifetime = issueTime + (5 * TimeUnit.MINUTES.toMillis(5)); - journal.add(tokenId, issueTime, expiration, maxLifetime); + journal.add(tokenId, issueTime, expiration, maxLifetime, null); // Get the token state from the journal, and validate its contents JournalEntry entry = journal.get(tokenId); @@ -120,7 +120,7 @@ public abstract class AbstractFileTokenStateJournalTest { assertEquals(maxLifetime, Long.parseLong(entry.getMaxLifetime())); long updatedExpiration = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(5); - journal.add(tokenId, issueTime, updatedExpiration, maxLifetime); + journal.add(tokenId, issueTime, updatedExpiration, maxLifetime, null); // Get and validate the updated token state entry = journal.get(tokenId); diff --git a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/state/FileTokenStateJournalTest.java b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/state/FileTokenStateJournalTest.java index da79a4c..2fe601d 100644 --- a/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/state/FileTokenStateJournalTest.java +++ b/gateway-server/src/test/java/org/apache/knox/gateway/services/token/impl/state/FileTokenStateJournalTest.java @@ -18,15 +18,15 @@ */ package org.apache.knox.gateway.services.token.impl.state; -import org.apache.knox.gateway.services.token.state.JournalEntry; -import org.junit.Test; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import java.util.UUID; import java.util.concurrent.TimeUnit; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; +import org.apache.knox.gateway.services.token.state.JournalEntry; +import org.junit.Test; public class FileTokenStateJournalTest { @@ -40,14 +40,14 @@ public class FileTokenStateJournalTest { doTestParseJournalEntry(tokenId, issueTime, expiration, maxLifetime); } - @Test(expected = IllegalArgumentException.class) + @Test public void testParseJournalEntry_MissingMaxLifetime() { final String tokenId = UUID.randomUUID().toString(); final Long issueTime = System.currentTimeMillis(); final Long expiration = issueTime + TimeUnit.HOURS.toMillis(1); final Long maxLifetime = null; - doTestParseJournalEntry(tokenId, issueTime, expiration, maxLifetime); + doTestParseJournalEntry(tokenId, issueTime, expiration, maxLifetime, "user", null); } @Test @@ -76,57 +76,65 @@ public class FileTokenStateJournalTest { } @Test + public void tesParseTokenMetadata() throws Exception { + doTestParseJournalEntry("", "", "", "", "userName", ""); + doTestParseJournalEntry("", "", "", "", "", "comment"); + } + + @Test public void testParseJournalEntry_AllMissing() { - doTestParseJournalEntry(null, null, null, " "); + doTestParseJournalEntry(null, null, null, " ", null, null); + } + + private void doTestParseJournalEntry(final String tokenId, final Long issueTime, final Long expiration, final Long maxLifetime) { + doTestParseJournalEntry(tokenId, issueTime, expiration, maxLifetime, null, null); } private void doTestParseJournalEntry(final String tokenId, final Long issueTime, final Long expiration, - final Long maxLifetime) { + final Long maxLifetime, + final String userName, + final String comment) { doTestParseJournalEntry(tokenId, (issueTime != null ? issueTime.toString() : null), (expiration != null ? expiration.toString() : null), - (maxLifetime != null ? maxLifetime.toString() : null)); + (maxLifetime != null ? maxLifetime.toString() : null), + userName, comment); } private void doTestParseJournalEntry(final String tokenId, final String issueTime, final String expiration, - final String maxLifetime) { + final String maxLifetime, + final String userName, + final String comment) { StringBuilder entryStringBuilder = new StringBuilder(tokenId != null ? tokenId : "").append(',') .append(issueTime != null ? issueTime : "") .append(',') .append(expiration != null ? expiration : "") .append(',') - .append(maxLifetime != null ? maxLifetime : ""); + .append(maxLifetime != null ? maxLifetime : "") + .append(",").append(userName == null ? "" : userName) + .append(",").append(comment == null ? "" : comment); JournalEntry entry = FileTokenStateJournal.FileJournalEntry.parse(entryStringBuilder.toString()); assertNotNull(entry); - if (tokenId != null && !tokenId.trim().isEmpty()) { - assertEquals(tokenId, entry.getTokenId()); - } else { - assertNull(entry.getTokenId()); - } - - if (issueTime != null && !issueTime.trim().isEmpty()) { - assertEquals(issueTime, entry.getIssueTime()); - } else { - assertNull(entry.getIssueTime()); - } - - if (expiration != null && !expiration.trim().isEmpty()) { - assertEquals(expiration, entry.getExpiration()); - } else { - assertNull(entry.getExpiration()); - } - - if (maxLifetime != null && !maxLifetime.trim().isEmpty()) { - assertEquals(maxLifetime, entry.getMaxLifetime()); - } else { - assertNull(entry.getMaxLifetime()); - } + assertJournalEntryField(tokenId, entry.getTokenId()); + assertJournalEntryField(issueTime, entry.getIssueTime()); + assertJournalEntryField(expiration, entry.getExpiration()); + assertJournalEntryField(maxLifetime, entry.getMaxLifetime()); + assertJournalEntryField(userName, entry.getTokenMetadata().getUserName()); + assertJournalEntryField(comment, entry.getTokenMetadata().getComment()); + } + + private void assertJournalEntryField(String received, String parsed) { + if (received != null && !received.trim().isEmpty()) { + assertEquals(received, parsed); + } else { + assertNull(parsed); + } } diff --git a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java index d260f58..9f285ee 100644 --- a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java +++ b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java @@ -53,6 +53,7 @@ import org.apache.knox.gateway.services.security.KeystoreServiceException; import org.apache.knox.gateway.services.security.token.JWTokenAttributes; import org.apache.knox.gateway.services.security.token.JWTokenAttributesBuilder; import org.apache.knox.gateway.services.security.token.JWTokenAuthority; +import org.apache.knox.gateway.services.security.token.TokenMetadata; import org.apache.knox.gateway.services.security.token.TokenServiceException; import org.apache.knox.gateway.services.security.token.TokenStateService; import org.apache.knox.gateway.services.security.token.TokenUtils; @@ -441,6 +442,7 @@ public class TokenResource { System.currentTimeMillis(), expires, maxTokenLifetime.orElse(tokenStateService.getDefaultMaxLifetimeDuration())); + tokenStateService.addMetadata(tokenId, new TokenMetadata(p.getName())); log.storedToken(getTopologyName(), Tokens.getTokenDisplayText(accessToken), tokenId); } diff --git a/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java b/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java index 54e92a4..b9d57b0 100644 --- a/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java +++ b/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java @@ -33,6 +33,7 @@ import org.apache.knox.gateway.services.GatewayServices; import org.apache.knox.gateway.services.security.AliasService; import org.apache.knox.gateway.services.security.token.JWTokenAttributes; import org.apache.knox.gateway.services.security.token.JWTokenAuthority; +import org.apache.knox.gateway.services.security.token.TokenMetadata; import org.apache.knox.gateway.services.security.token.TokenStateService; import org.apache.knox.gateway.services.security.token.TokenUtils; import org.apache.knox.gateway.services.security.token.UnknownTokenException; @@ -1130,6 +1131,15 @@ public class TokenServiceResourceTest { } @Override + public void addMetadata(String tokenId, TokenMetadata metadata) { + } + + @Override + public TokenMetadata getTokenMetadata(String tokenId) { + return null; + } + + @Override public void init(GatewayConfig config, Map<String, String> options) throws ServiceLifecycleException { } diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadata.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadata.java new file mode 100644 index 0000000..f8a6bd3 --- /dev/null +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadata.java @@ -0,0 +1,82 @@ +/* + * 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; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; +import org.apache.knox.gateway.util.JsonUtils; + +public class TokenMetadata { + private static final String JSON_ELEMENT_USER_NAME = "userName"; + private static final String JSON_ELEMENT_COMMENT = "comment"; + private static final String EMPTY_COMMENT = ""; + + private final String userName; + private final String comment; + + public TokenMetadata(String userName) { + this(userName, EMPTY_COMMENT); + } + + public TokenMetadata(String userName, String comment) { + this.userName = userName; + this.comment = comment; + } + + public String getUserName() { + return userName; + } + + public String getComment() { + return comment; + } + + public String toJSON() { + final Map<String, String> metadataMap = new HashMap<>(); + metadataMap.put(JSON_ELEMENT_USER_NAME, getUserName()); + metadataMap.put(JSON_ELEMENT_COMMENT, getComment() == null ? EMPTY_COMMENT : getComment()); + return JsonUtils.renderAsJsonString(metadataMap); + } + + public static TokenMetadata fromJSON(String json) { + final Map<String, String> metadataMap = JsonUtils.getMapFromJsonString(json); + if (metadataMap != null) { + return new TokenMetadata(metadataMap.get(JSON_ELEMENT_USER_NAME), metadataMap.get(JSON_ELEMENT_COMMENT)); + } + throw new IllegalArgumentException("Invalid metadata JSON: " + json); + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); + } + + @Override + public boolean equals(Object obj) { + return EqualsBuilder.reflectionEquals(this, obj); + } + + @Override + public int hashCode() { + return HashCodeBuilder.reflectionHashCode(this); + } +} diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenStateService.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenStateService.java index 007c2de..736a272 100644 --- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenStateService.java +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenStateService.java @@ -166,4 +166,22 @@ public interface TokenStateService extends Service { */ long getTokenExpiration(String tokenId, boolean validate) throws UnknownTokenException; + /** + * Adds metadata to the token identified by the given ID + * + * @param tokenId + * The token's unique identifier. + * @param metadata + * The metadata to be added + */ + void addMetadata(String tokenId, TokenMetadata metadata); + + /** + * + * @param tokenId + * The token's unique identifier. + * @return The associated token metadata + */ + TokenMetadata getTokenMetadata(String tokenId); + }