From 087ea33f0f288d05f23a015cc6dcd151a8fe840b Mon Sep 17 00:00:00 2001 From: Sandor Molnar Date: Thu, 14 Sep 2023 13:43:29 +0200 Subject: [PATCH] KNOX-2961 - Knox SSO cookie Invalidation - Phase I --- .../applications/knoxauth/app/logout.jsp | 23 +++- gateway-provider-security-jwt/pom.xml | 4 + .../provider/federation/jwt/JWTMessages.java | 6 + .../jwt/filter/SSOCookieFederationFilter.java | 61 ++++++++- .../pac4j/filter/Pac4jDispatcherFilter.java | 23 +++- .../home/conf/topologies/homepage.xml | 5 + .../home/conf/topologies/knoxsso.xml | 5 + .../home/conf/topologies/manager.xml | 5 + .../token/impl/DefaultTokenStateService.java | 20 ++- .../token/impl/JDBCTokenStateService.java | 13 ++ .../token/impl/TokenStateDatabase.java | 22 +++ .../token/impl/TokenStateServiceMessages.java | 9 ++ .../impl/AliasBasedTokenStateServiceTest.java | 1 + .../token/impl/JDBCTokenStateServiceTest.java | 19 +++ .../service/knoxsso/KnoxSSOMessages.java | 3 + .../service/knoxsso/WebSSOResource.java | 30 +++++ .../service/knoxtoken/TokenResource.java | 32 ++++- .../security/token/TokenMetadata.java | 11 +- .../services/security/token/TokenUtils.java | 28 ++-- .../gateway/session/SessionInvalidator.java | 27 ++++ .../gateway/session/SessionInvalidators.java | 41 ++++++ .../knox/gateway/util/AuthFilterUtils.java | 9 ++ .../token-management/app/metadata.ts | 2 + .../app/token.management.component.html | 125 +++++++----------- .../app/token.management.component.ts | 102 ++++---------- .../app/token.management.service.ts | 8 +- .../assets/green_checkmark.svg | 2 + .../assets/red_cross_circle.svg | 92 +++++++++++++ pom.xml | 1 + 29 files changed, 535 insertions(+), 194 deletions(-) create mode 100644 gateway-spi/src/main/java/org/apache/knox/gateway/session/SessionInvalidator.java create mode 100644 gateway-spi/src/main/java/org/apache/knox/gateway/session/SessionInvalidators.java create mode 100644 knox-token-management-ui/token-management/assets/green_checkmark.svg create mode 100644 knox-token-management-ui/token-management/assets/red_cross_circle.svg diff --git a/gateway-applications/src/main/resources/applications/knoxauth/app/logout.jsp b/gateway-applications/src/main/resources/applications/knoxauth/app/logout.jsp index 1821ac7bf0..9dea1d3663 100644 --- a/gateway-applications/src/main/resources/applications/knoxauth/app/logout.jsp +++ b/gateway-applications/src/main/resources/applications/knoxauth/app/logout.jsp @@ -46,6 +46,17 @@ + + <% + final boolean autoGlobalLogout = "1".equals(request.getParameter("autoGlobalLogout")); + if (autoGlobalLogout) {%> + + <%}%> + <% String originalUrl = request.getParameter("originalUrl"); Topology topology = (Topology)request.getSession().getServletContext().getAttribute("org.apache.knox.gateway.topology"); @@ -123,8 +134,6 @@ response.setHeader("Location", globalLogoutPageURL); return; } - - %> @@ -177,18 +186,20 @@ <% if (globalLogoutPageURL != null && !globalLogoutPageURL.isEmpty()) { %> -

+

+
If you would like to logout of the Knox SSO session, you need to do so from the configured SSO provider. Subsequently, authentication will be required to access any SSO protected resources. Note that this may or may not invalidate any previously established application sessions. Application sessions are subject to their application specific session cookies and timeouts. - Global Logout -

+ + +
<% } - } + } else { %>
diff --git a/gateway-provider-security-jwt/pom.xml b/gateway-provider-security-jwt/pom.xml index e02f3ceed1..f36258f5d2 100644 --- a/gateway-provider-security-jwt/pom.xml +++ b/gateway-provider-security-jwt/pom.xml @@ -85,5 +85,9 @@ com.github.ben-manes.caffeine caffeine + + org.apache.httpcomponents + httpcore + diff --git a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/JWTMessages.java b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/JWTMessages.java index 098b559ebf..b188539f27 100644 --- a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/JWTMessages.java +++ b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/JWTMessages.java @@ -61,6 +61,9 @@ public interface JWTMessages { @Message( level = MessageLevel.DEBUG, text = "Sending redirect to: {0}" ) void sendRedirectToLoginURL(String loginURL); + @Message( level = MessageLevel.INFO, text = "Sending redirect to global logout URL: {0}" ) + void sendRedirectToLogoutURL(String logoutURL); + @Message( level = MessageLevel.WARN, text = "Configuration for authentication provider URL is missing - will derive default URL." ) void missingAuthenticationProviderUrlConfiguration(); @@ -98,4 +101,7 @@ public interface JWTMessages { @Message( level = MessageLevel.INFO, text = "Unexpected Issuer for token {0} ({1})." ) void unexpectedTokenIssuer(String tokenDisplayText, String tokenId); + + @Message( level = MessageLevel.WARN, text = "Invalid SSO cookie found! Cleaning up..." ) + void invalidSsoCookie(); } diff --git a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java index f1b86f50f2..81d6e1a97c 100644 --- a/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java +++ b/gateway-provider-security-jwt/src/main/java/org/apache/knox/gateway/provider/federation/jwt/filter/SSOCookieFederationFilter.java @@ -17,6 +17,7 @@ */ package org.apache.knox.gateway.provider.federation.jwt.filter; +import org.apache.http.HttpHeaders; import org.apache.knox.gateway.config.GatewayConfig; import org.apache.knox.gateway.i18n.messages.MessagesFactory; import org.apache.knox.gateway.provider.federation.jwt.JWTMessages; @@ -24,9 +25,11 @@ 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.apache.knox.gateway.session.SessionInvalidators; import org.apache.knox.gateway.util.AuthFilterUtils; import org.apache.knox.gateway.util.CertificateUtils; import org.apache.knox.gateway.util.CookieUtils; +import org.apache.knox.gateway.util.Urls; import org.eclipse.jetty.http.MimeTypes; import javax.security.auth.Subject; @@ -71,7 +74,7 @@ public class SSOCookieFederationFilter extends AbstractJWTFilter { private String cookieName; private String authenticationProviderUrl; private String gatewayPath; - private Set unAuthenticatedPaths = new HashSet(20); + private Set unAuthenticatedPaths = new HashSet<>(20); @Override public void init( FilterConfig filterConfig ) throws ServletException { @@ -175,7 +178,10 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha // There were no valid cookies found so redirect to login url if(res != null && !res.isCommitted()) { - sendRedirectToLoginURL(req, res); + // only if the Location header is not set already by a session invalidator + if (res.getHeader(HttpHeaders.LOCATION) == null) { + sendRedirectToLoginURL(req, res); + } } } } @@ -188,8 +194,23 @@ private void sendRedirectToLoginURL(HttpServletRequest request, HttpServletRespo } @Override - protected void handleValidationError(HttpServletRequest request, HttpServletResponse response, - int status, String error) throws IOException { + protected void handleValidationError(HttpServletRequest request, HttpServletResponse response, int status, String error) throws IOException { + if (error != null && error.startsWith("Token") && error.endsWith("disabled")) { + LOGGER.invalidSsoCookie(); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + removeAuthenticationToken(request, response); + SessionInvalidators.KNOX_SSO_INVALIDATOR.getSessionInvalidators().forEach(sessionInvalidator -> { + sessionInvalidator.onAuthenticationError(request, response); + }); + if (AuthFilterUtils.shouldDoGlobalLogout(request)) { + final String redirectTo = constructGlobalLogoutUrl(request); + LOGGER.sendRedirectToLogoutURL(redirectTo); + response.setHeader(HttpHeaders.LOCATION, redirectTo); + response.sendRedirect(redirectTo); + return; + } + } + /* We don't need redirect if this is a XHR request */ if (request.getHeader(XHR_HEADER) != null && request.getHeader(XHR_HEADER).equalsIgnoreCase(XHR_VALUE)) { @@ -206,6 +227,12 @@ protected void handleValidationError(HttpServletRequest request, HttpServletResp } } + private String constructGlobalLogoutUrl(HttpServletRequest request) { + final StringBuilder logoutUrlBuilder = new StringBuilder(deriveDefaultAuthenticationProviderUrl(request, true)); + logoutUrlBuilder.append('&').append(ORIGINAL_URL_QUERY_PARAM).append(deriveDefaultAuthenticationProviderUrl(request, false)); //orignalUrl=WebSSO login + return logoutUrlBuilder.toString(); + } + /** * Create the URL to be used for authentication of the user in the absence of * a JWT token within the incoming request. @@ -230,13 +257,17 @@ protected String constructLoginURL(HttpServletRequest request) { + request.getRequestURL().append(getOriginalQueryString(request)); } + public String deriveDefaultAuthenticationProviderUrl(HttpServletRequest request) { + return deriveDefaultAuthenticationProviderUrl(request, false); + } + /** * Derive a provider URL from the request assuming that the * KnoxSSO endpoint is local to the endpoint serving this request. * @param request origin request * @return url that is based on KnoxSSO endpoint */ - public String deriveDefaultAuthenticationProviderUrl(HttpServletRequest request) { + public String deriveDefaultAuthenticationProviderUrl(HttpServletRequest request, boolean logout) { String providerURL = null; String scheme; String host; @@ -252,7 +283,7 @@ public String deriveDefaultAuthenticationProviderUrl(HttpServletRequest request) if (!host.contains(":") && port != -1) { sb.append(':').append(port); } - sb.append('/').append(gatewayPath).append("/knoxsso/api/v1/websso"); + sb.append('/').append(gatewayPath).append(logout ? "/knoxsso/knoxauth/logout.jsp?autoGlobalLogout=1" : "/knoxsso/api/v1/websso"); providerURL = sb.toString(); } catch (MalformedURLException e) { LOGGER.failedToDeriveAuthenticationProviderUrl(e); @@ -265,4 +296,22 @@ private String getOriginalQueryString(HttpServletRequest request) { String originalQueryString = request.getQueryString(); return (originalQueryString == null) ? "" : "?" + originalQueryString; } + + private void removeAuthenticationToken(HttpServletRequest request, HttpServletResponse response) { + final Cookie c = new Cookie(cookieName, null); + c.setMaxAge(0); + c.setPath("/"); + try { + String domainName = Urls.getDomainName(request.getRequestURL().toString(), null); + if(domainName != null) { + c.setDomain(domainName); + } + } catch (MalformedURLException e) { + //log.problemWithCookieDomainUsingDefault(); + // we are probably not going to be able to + // remove the cookie due to this error but it + // isn't necessarily not going to work. + } + response.addCookie(c); + } } diff --git a/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/filter/Pac4jDispatcherFilter.java b/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/filter/Pac4jDispatcherFilter.java index b356cfe6a6..41642121e4 100644 --- a/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/filter/Pac4jDispatcherFilter.java +++ b/gateway-provider-security-pac4j/src/main/java/org/apache/knox/gateway/pac4j/filter/Pac4jDispatcherFilter.java @@ -18,6 +18,7 @@ package org.apache.knox.gateway.pac4j.filter; import org.apache.commons.lang3.StringUtils; +import org.apache.knox.gateway.config.GatewayConfig; import org.apache.knox.gateway.i18n.messages.MessagesFactory; import org.apache.knox.gateway.pac4j.Pac4jMessages; import org.apache.knox.gateway.pac4j.config.ClientConfigurationDecorator; @@ -32,6 +33,9 @@ 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.session.SessionInvalidator; +import org.apache.knox.gateway.session.SessionInvalidators; +import org.apache.knox.gateway.util.AuthFilterUtils; import org.pac4j.config.client.PropertiesConfigFactory; import org.pac4j.config.client.PropertiesConstants; import org.pac4j.core.client.Client; @@ -54,6 +58,8 @@ import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + import java.io.IOException; import java.util.Enumeration; import java.util.HashMap; @@ -73,7 +79,7 @@ * * @since 0.8.0 */ -public class Pac4jDispatcherFilter implements Filter { +public class Pac4jDispatcherFilter implements Filter, SessionInvalidator { private static final String ALIAS_PREFIX = "${ALIAS="; private static Pac4jMessages log = MessagesFactory.get(Pac4jMessages.class); @@ -234,6 +240,8 @@ public void init( FilterConfig filterConfig ) throws ServletException { config.setSessionStore(sessionStore); + SessionInvalidators.KNOX_SSO_INVALIDATOR.registerSessionInvalidator(this); + } /** @@ -322,5 +330,16 @@ public void doFilter( ServletRequest servletRequest, ServletResponse servletResp } @Override - public void destroy() { } + public void onAuthenticationError(HttpServletRequest request, HttpServletResponse response) { + final GatewayConfig gatewayConfig = (GatewayConfig) request.getServletContext().getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE); + if (gatewayConfig != null && gatewayConfig.getGlobalLogoutPageUrl() != null) { + AuthFilterUtils.markDoGlobalLogoutInRequest(request); + } + } + + @Override + public void destroy() { + SessionInvalidators.KNOX_SSO_INVALIDATOR.unregisterSessionInvalidator(this); + } + } diff --git a/gateway-release/home/conf/topologies/homepage.xml b/gateway-release/home/conf/topologies/homepage.xml index e6e8ecaa95..744824f0cb 100644 --- a/gateway-release/home/conf/topologies/homepage.xml +++ b/gateway-release/home/conf/topologies/homepage.xml @@ -55,6 +55,11 @@ federation SSOCookieProvider true + + + knox.token.exp.server-managed + false + identity-assertion diff --git a/gateway-release/home/conf/topologies/knoxsso.xml b/gateway-release/home/conf/topologies/knoxsso.xml index 75cab7bfdf..99600f8746 100644 --- a/gateway-release/home/conf/topologies/knoxsso.xml +++ b/gateway-release/home/conf/topologies/knoxsso.xml @@ -114,6 +114,11 @@ knoxsso.token.ttl 86400000 + + + knox.token.exp.server-managed + false + diff --git a/gateway-release/home/conf/topologies/manager.xml b/gateway-release/home/conf/topologies/manager.xml index 7e30841377..acade305c8 100644 --- a/gateway-release/home/conf/topologies/manager.xml +++ b/gateway-release/home/conf/topologies/manager.xml @@ -51,6 +51,11 @@ federation SSOCookieProvider true + + + knox.token.exp.server-managed + false + identity-assertion 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 cfe9a4a807..85e47c169a 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 @@ -350,11 +350,12 @@ private String getTimestampDisplay(long timestamp) { } /** - * Method that deletes expired tokens based on the token timestamp. + * Method that deletes expired tokens based on the token timestamp as well as disabled KnoxSSO cookies. */ protected void evictExpiredTokens() { if (readyForEviction()) { final Set tokensToEvict = getExpiredTokens(); + tokensToEvict.addAll(getDisabledKnoxSsoCookies()); if (!tokensToEvict.isEmpty()) { removeTokens(tokensToEvict); @@ -383,6 +384,23 @@ protected Set getExpiredTokens() { return expiredTokens; } + protected Set getDisabledKnoxSsoCookies() { + final Set disbaledKnoxSsoCookies = new HashSet<>(); + getTokenIds().forEach(tokenId -> { + TokenMetadata metadata = null; + try { + metadata = getTokenMetadata(tokenId); + } catch (UnknownTokenException e) { + // NOP + } + if (metadata != null && metadata.isKnoxSsoCookie() && !metadata.isEnabled()) { + log.evictToken(Tokens.getTokenIDDisplayText(tokenId)); + disbaledKnoxSsoCookies.add(tokenId); + } + }); + return disbaledKnoxSsoCookies; + } + /** * 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 index ab8a5b4d78..a078d55801 100644 --- 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 @@ -224,6 +224,19 @@ protected void evictExpiredTokens() { } catch (SQLException e) { log.errorRemovingTokensFromDatabase(e.getMessage(), e); } + + //removing disabled KnoxSSO cookies since they are no longer needed + try { + final Set disabledKnoxSsoCookies = tokenDatabase.getDisabledKnoxSsoCookies(); + if (!disabledKnoxSsoCookies.isEmpty()) { + log.removingDisabledKnoxSsoCookiesFromDatabase(disabledKnoxSsoCookies.size(), + String.join(", ", disabledKnoxSsoCookies.stream().map(tokenId -> Tokens.getTokenIDDisplayText(tokenId)).collect(Collectors.toSet()))); + final int numOfRemovedDisabledKnoxSsoCookies = tokenDatabase.deleteDisabledKnoxSsoCookies(); + log.removedDisabledKnoxSsoCookiesFromDatabase(numOfRemovedDisabledKnoxSsoCookies); + } + } catch (SQLException e) { + log.errorRemovingDisabledKnoxSsoCookiesFromDatabase(e.getMessage(), e); + } } @Override 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 index b5f5f0abdb..703225be0a 100644 --- 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 @@ -58,6 +58,9 @@ public class TokenStateDatabase { private static final String GET_TOKENS_CREATED_BY_USER_NAME_SQL = "SELECT kt.token_id, kt.issue_time, kt.expiration, kt.max_lifetime, ktm.md_name, ktm.md_value FROM " + TOKENS_TABLE_NAME + " kt, " + TOKEN_METADATA_TABLE_NAME + " ktm WHERE kt.token_id = ktm.token_id AND kt.token_id IN (SELECT token_id FROM " + TOKEN_METADATA_TABLE_NAME + " WHERE md_name = '" + TokenMetadata.CREATED_BY + "' AND md_value = ? )" + " ORDER BY kt.issue_time"; + private static final String GET_DISABLED_SSO_COOKIE_TOKEN_IDS = "SELECT token_id FROM " + TOKEN_METADATA_TABLE_NAME + " meta1 WHERE meta1.md_name = 'knoxSSOCookie' AND meta1.md_value = 'true' " + + "AND EXISTS (SELECT token_id FROM " + TOKEN_METADATA_TABLE_NAME + " meta2 WHERE meta1.token_id = meta2.token_id AND meta2.md_name = 'enabled' AND meta2.md_value = 'false')"; + private static final String REMOVE_DISBLED_SSO_COOKIES_SQL = "DELETE FROM " + TOKENS_TABLE_NAME + " WHERE token_id IN (" + GET_DISABLED_SSO_COOKIE_TOKEN_IDS + ")"; private final DataSource dataSource; @@ -177,6 +180,25 @@ TokenMetadata getTokenMetadata(String tokenId) throws SQLException { } } + Set getDisabledKnoxSsoCookies() throws SQLException { + final Set disabledKnoxSsoCookies = new HashSet<>(); + try (Connection connection = dataSource.getConnection(); + PreparedStatement getDisabledKnoxSsoCookiesStatement = connection.prepareStatement(GET_DISABLED_SSO_COOKIE_TOKEN_IDS); + ResultSet rs = getDisabledKnoxSsoCookiesStatement.executeQuery()) { + while (rs.next()) { + disabledKnoxSsoCookies.add(rs.getString(1)); + } + return disabledKnoxSsoCookies; + } + } + + int deleteDisabledKnoxSsoCookies() throws SQLException { + try (Connection connection = dataSource.getConnection(); + PreparedStatement deleteDisabledKnoxSsoCookiesStatement = connection.prepareStatement(REMOVE_DISBLED_SSO_COOKIES_SQL)) { + return deleteDisabledKnoxSsoCookiesStatement.executeUpdate(); + } + } + private static String decodeMetadata(String metadataName, String metadataValue) { return metadataName.equals(TokenMetadata.PASSCODE) ? new String(Base64.decodeBase64(metadataValue.getBytes(UTF_8)), UTF_8) : metadataValue; } 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 f24c1889b5..75cbb1b275 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 @@ -202,6 +202,15 @@ public interface TokenStateServiceMessages { @Message(level = MessageLevel.ERROR, text = "An error occurred while removing expired tokens from the database : {0}") void errorRemovingTokensFromDatabase(String errorMessage, @StackTrace(level = MessageLevel.DEBUG) Exception e); + @Message(level = MessageLevel.INFO, text = "Removing {0} disabled KnoxSSO cookie(s) from the database: {1}") + void removingDisabledKnoxSsoCookiesFromDatabase(int size, String disabledKnoxSsoCookieList); + + @Message(level = MessageLevel.DEBUG, text = "{0} disabled KnoxSSO cookies have been removed from the database") + void removedDisabledKnoxSsoCookiesFromDatabase(int size); + + @Message(level = MessageLevel.ERROR, text = "An error occurred while removing disabled KnoxSSO cookies from the database : {0}") + void errorRemovingDisabledKnoxSsoCookiesFromDatabase(String errorMessage, @StackTrace(level = MessageLevel.DEBUG) Exception e); + @Message(level = MessageLevel.DEBUG, text = "Fetched issue time for {0} from the database : {1}") void fetchedIssueTimeFromDatabase(String tokenId, long issueTime); 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 5c6dbcba2b..f6971f483b 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 @@ -113,6 +113,7 @@ public void testBulkTokenStateEviction() throws Exception { System.currentTimeMillis(), token.getExpiresDate().getTime(), maxTokenLifetime); + tss.addMetadata(token.getClaim(JWTToken.KNOX_ID_CLAIM), new TokenMetadata("alice")); assertTrue("Expected the token to have expired.", tss.isExpired(token)); } 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 index 024025282c..b9dd1a2267 100644 --- 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 @@ -37,6 +37,7 @@ import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.digest.HmacAlgorithms; @@ -240,6 +241,24 @@ public void testEvictExpiredTokens() throws Exception { assertEquals(0, getLongTokenAttributeFromDatabase(null, GET_TOKENS_COUNT_SQL)); } + @Test + public void testEvictDisabledKnoxSsoCookies() throws Exception { + truncateDatabase(); + final long now = System.currentTimeMillis(); + final int tokenCount = 500; + for (int i = 0; i < tokenCount; i++) { + final String tokenId = UUID.randomUUID().toString(); + jdbcTokenStateService.addToken(tokenId, now, now + TimeUnit.MINUTES.toMillis(5), TimeUnit.MINUTES.toMillis(60)); + final TokenMetadata tokenMetadata = new TokenMetadata("user"); + tokenMetadata.setKnoxSsoCookie(i % 2 == 0); // half of them are KnoxSSO cookie + tokenMetadata.setEnabled(i % 4 == 0); // quarter of them are disabled + jdbcTokenStateService.addMetadata(tokenId, tokenMetadata); + } + assertEquals(tokenCount, getLongTokenAttributeFromDatabase(null, GET_TOKENS_COUNT_SQL)); + jdbcTokenStateService.evictExpiredTokens(); //500 tokens were generated, 125 are disabled KnoxSSO cookies -> 375 should be in the DB + assertEquals(375, getLongTokenAttributeFromDatabase(null, GET_TOKENS_COUNT_SQL)); + } + private long getLongTokenAttributeFromDatabase(String tokenId, String sql) throws SQLException { try (Connection conn = getConnection(); PreparedStatement stmt = conn.prepareStatement(sql)) { if (tokenId != null) { diff --git a/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/KnoxSSOMessages.java b/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/KnoxSSOMessages.java index a1dc1bf1c4..1b47a90f88 100644 --- a/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/KnoxSSOMessages.java +++ b/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/KnoxSSOMessages.java @@ -60,4 +60,7 @@ public interface KnoxSSOMessages { @Message( level = MessageLevel.ERROR, text = "The original URL: {0} for redirecting back after authentication is " + "not valid according to the configured whitelist: {1}. See documentation for KnoxSSO Whitelisting.") void whiteListMatchFail(String original, String whitelist); + + @Message( level = MessageLevel.INFO, text = "Knox Token service ({0}) stored state for token {1} ({2})") + void storedToken(String topologyName, String tokenDisplayText, String tokenId); } diff --git a/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java b/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java index a4ed8e1b13..94e2f34820 100644 --- a/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java +++ b/gateway-service-knoxsso/src/main/java/org/apache/knox/gateway/service/knoxsso/WebSSOResource.java @@ -57,12 +57,15 @@ 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; import org.apache.knox.gateway.services.security.token.impl.JWT; import org.apache.knox.gateway.session.control.ConcurrentSessionVerifier; import org.apache.knox.gateway.util.CookieUtils; import org.apache.knox.gateway.util.RegExUtils; +import org.apache.knox.gateway.util.Tokens; import org.apache.knox.gateway.util.Urls; import org.apache.knox.gateway.util.WhitelistUtils; @@ -107,6 +110,7 @@ public class WebSSOResource { private List ssoExpectedparams = new ArrayList<>(); private String clusterName; private String tokenIssuer; + private TokenStateService tokenStateService; private String sameSiteValue; @@ -142,6 +146,13 @@ public void init() throws AliasServiceException { this.sameSiteValue = StringUtils.isBlank(context.getInitParameter(SSO_COOKIE_SAMESITE_PARAM)) ? SSO_COOKIE_SAMESITE_DEFAULT : context.getInitParameter(SSO_COOKIE_SAMESITE_PARAM); + + final GatewayServices services = (GatewayServices) context.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE); + if (services != null) { + if (TokenUtils.isServerManagedTokenStateEnabled(context)) { + tokenStateService = services.getService(ServiceType.TOKEN_STATE_SERVICE); + } + } } private void setSignatureAlogrithm() throws AliasServiceException { @@ -295,6 +306,7 @@ private Response getAuthenticationToken(int statusCode) { .setSigningKeystoreName(signingKeystoreName) .setSigningKeystoreAlias(signingKeystoreAlias) .setSigningKeystorePassphrase(signingKeystorePassphrase) + .setManaged(tokenStateService != null) .build(); JWT token = tokenAuthority.issueToken(jwtAttributes); @@ -303,6 +315,7 @@ private Response getAuthenticationToken(int statusCode) { if (!verifier.registerToken(p.getName(), token)) { throw new WebApplicationException("Too many sessions for user: " + request.getUserPrincipal().getName(), Response.Status.FORBIDDEN); } + saveToken(token); addJWTHadoopCookie(original, token); } @@ -426,4 +439,21 @@ private void removeOriginalUrlCookie(HttpServletResponse response) { c.setPath(RESOURCE_PATH); response.addCookie(c); } + + // Optional token state service persistence + private void saveToken(JWT token) { + if (tokenStateService != null) { + final String tokenId = TokenUtils.getTokenId(token); + final long issueTime = System.currentTimeMillis(); + tokenStateService.addToken(tokenId, issueTime, token.getExpiresDate().getTime(), tokenStateService.getDefaultMaxLifetimeDuration()); + final TokenMetadata tokenMetadata = new TokenMetadata(token.getSubject()); + tokenMetadata.setKnoxSsoCookie(true); + tokenStateService.addMetadata(tokenId, tokenMetadata); + LOGGER.storedToken(getTopologyName(), Tokens.getTokenDisplayText(token.toString()), Tokens.getTokenIDDisplayText(tokenId)); + } + } + + private String getTopologyName() { + return (String) context.getAttribute("org.apache.knox.gateway.gateway.cluster"); + } } 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 7b769e7925..3784e8b109 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 @@ -32,6 +32,7 @@ import java.util.Enumeration; import java.util.Map; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Optional; @@ -450,7 +451,14 @@ public Response getUserTokens(@Context UriInfo uriInfo) { final String userName = uriInfo.getQueryParameters().getFirst("userName"); final String createdBy = uriInfo.getQueryParameters().getFirst("createdBy"); - final Collection userTokens = createdBy == null ? tokenStateService.getTokens(userName) : tokenStateService.getDoAsTokens(createdBy); + final String userNameOrCreatedBy = uriInfo.getQueryParameters().getFirst("userNameOrCreatedBy"); + final Collection userTokens; + if (userNameOrCreatedBy == null) { + userTokens = createdBy == null ? tokenStateService.getTokens(userName) : tokenStateService.getDoAsTokens(createdBy); + } else { + userTokens = new HashSet<>(tokenStateService.getTokens(userNameOrCreatedBy)); + userTokens.addAll(tokenStateService.getDoAsTokens(userNameOrCreatedBy)); + } final Collection tokens = new TreeSet<>(); if (metadataMap.isEmpty()) { tokens.addAll(userTokens); @@ -568,12 +576,17 @@ public Response revoke(String token) { try { final String revoker = SubjectUtils.getCurrentEffectivePrincipalName(); final String tokenId = getTokenId(token); - if (triesToRevokeOwnToken(tokenId, revoker) || allowedRenewers.contains(revoker)) { - tokenStateService.revokeToken(tokenId); - log.revokedToken(getTopologyName(), - Tokens.getTokenDisplayText(token), - Tokens.getTokenIDDisplayText(tokenId), - revoker); + if (isKnoxSsoCookie(tokenId)) { + errorStatus = Response.Status.FORBIDDEN; + error = "SSO cookie (" + Tokens.getTokenIDDisplayText(tokenId) + ") cannot not be revoked." ; + errorCode = ErrorCode.UNAUTHORIZED; + } + if (StringUtils.isBlank(error) && (triesToRevokeOwnToken(tokenId, revoker) || allowedRenewers.contains(revoker))) { + tokenStateService.revokeToken(tokenId); + log.revokedToken(getTopologyName(), + Tokens.getTokenDisplayText(token), + Tokens.getTokenIDDisplayText(tokenId), + revoker); } else { errorStatus = Response.Status.FORBIDDEN; error = "Caller (" + revoker + ") not authorized to revoke tokens."; @@ -603,6 +616,11 @@ public Response revoke(String token) { return resp; } + private boolean isKnoxSsoCookie(String tokenId) throws UnknownTokenException { + final TokenMetadata metadata = tokenStateService.getTokenMetadata(tokenId); + return metadata == null ? false : metadata.isKnoxSsoCookie(); + } + private boolean triesToRevokeOwnToken(String tokenId, String revoker) throws UnknownTokenException { final TokenMetadata metadata = tokenStateService.getTokenMetadata(tokenId); final String tokenUserName = metadata == null ? "" : metadata.getUserName(); 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 index b90dfb4904..a3f7d29add 100644 --- 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 @@ -36,7 +36,8 @@ public class TokenMetadata { public static final String ENABLED = "enabled"; public static final String PASSCODE = "passcode"; public static final String CREATED_BY = "createdBy"; - private static final List KNOWN_MD_NAMES = Arrays.asList(USER_NAME, COMMENT, ENABLED, PASSCODE, CREATED_BY); + public static final String KNOX_SSO_COOKIE = "knoxSSOCookie"; + private static final List KNOWN_MD_NAMES = Arrays.asList(USER_NAME, COMMENT, ENABLED, PASSCODE, CREATED_BY, KNOX_SSO_COOKIE); private final Map metadataMap = new HashMap<>(); @@ -118,6 +119,14 @@ public String getCreatedBy() { return getMetadata(CREATED_BY); } + public void setKnoxSsoCookie(boolean knoxSsoCookie) { + saveMetadata(KNOX_SSO_COOKIE, String.valueOf(knoxSsoCookie)); + } + + public boolean isKnoxSsoCookie() { + return Boolean.parseBoolean(getMetadata(KNOX_SSO_COOKIE)); + } + public String toJSON() { return JsonUtils.renderAsJsonString(metadataMap); } diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenUtils.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenUtils.java index 7471150d02..4ba1defb02 100644 --- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenUtils.java +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenUtils.java @@ -57,27 +57,25 @@ public static String getTokenId(final JWT token) { * @return true, if server-managed state is enabled; Otherwise, false. */ public static boolean isServerManagedTokenStateEnabled(FilterConfig filterConfig) { - boolean isServerManaged = false; - - // First, check for explicit provider-level configuration String providerParamValue = filterConfig.getInitParameter(TokenStateService.CONFIG_SERVER_MANAGED); + return isServerManagedTokenStateEnabled(providerParamValue, filterConfig.getServletContext()); + } + + public static boolean isServerManagedTokenStateEnabled(ServletContext context) { + final String serviceParamValue = context.getInitParameter(TokenStateService.CONFIG_SERVER_MANAGED); + return isServerManagedTokenStateEnabled(serviceParamValue, context); + } - // If there is no provider-level configuration - if (providerParamValue == null || providerParamValue.isEmpty()) { + private static boolean isServerManagedTokenStateEnabled(String parameterValue, ServletContext context) { + if (parameterValue == null || parameterValue.isEmpty()) { // Fall back to the gateway-level default - ServletContext context = filterConfig.getServletContext(); - if (context != null) { - GatewayConfig config = (GatewayConfig) context.getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE); - isServerManaged = (config != null) && config.isServerManagedTokenStateEnabled(); - } + GatewayConfig config = (GatewayConfig) context.getAttribute(GatewayConfig.GATEWAY_CONFIG_ATTRIBUTE); + return (config != null) && config.isServerManagedTokenStateEnabled(); } else { - // Otherwise, apply the provider-level configuration - isServerManaged = Boolean.valueOf(providerParamValue); + // Otherwise, apply the service-level configuration + return Boolean.valueOf(parameterValue); } - - return isServerManaged; } - /** * @return configuredSignatureAlgorithm if any OR the default HMAC algorithm if {@link #useHMAC(char[], String)} is * true; the default RSA algorithm otherwise diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/session/SessionInvalidator.java b/gateway-spi/src/main/java/org/apache/knox/gateway/session/SessionInvalidator.java new file mode 100644 index 0000000000..82df7ae04f --- /dev/null +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/session/SessionInvalidator.java @@ -0,0 +1,27 @@ +/* + * 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.session; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +public interface SessionInvalidator { + + void onAuthenticationError(HttpServletRequest request, HttpServletResponse response); + +} diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/session/SessionInvalidators.java b/gateway-spi/src/main/java/org/apache/knox/gateway/session/SessionInvalidators.java new file mode 100644 index 0000000000..b2903174fd --- /dev/null +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/session/SessionInvalidators.java @@ -0,0 +1,41 @@ +/* + * 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.session; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public enum SessionInvalidators { + + KNOX_SSO_INVALIDATOR; + + private final Set sessionInvalidators = Collections.synchronizedSet(new HashSet<>()); + + public void registerSessionInvalidator(SessionInvalidator sessionInvalidator) { + sessionInvalidators.add(sessionInvalidator); + } + + public void unregisterSessionInvalidator(SessionInvalidator sessionInvalidator) { + sessionInvalidators.remove(sessionInvalidator); + } + + public Set getSessionInvalidators() { + return sessionInvalidators; + } +} diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/util/AuthFilterUtils.java b/gateway-spi/src/main/java/org/apache/knox/gateway/util/AuthFilterUtils.java index 5bd8a7e28c..676cf4b231 100644 --- a/gateway-spi/src/main/java/org/apache/knox/gateway/util/AuthFilterUtils.java +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/util/AuthFilterUtils.java @@ -46,6 +46,7 @@ public class AuthFilterUtils { public static final String PROXYUSER_PREFIX = "hadoop.proxyuser"; public static final String QUERY_PARAMETER_DOAS = "doAs"; public static final String REAL_USER_NAME_ATTRIBUTE = "real.user.name"; + public static final String DO_GLOBAL_LOGOUT_ATTRIBUTE = "do.global.logout"; private static final GatewaySpiMessages LOG = MessagesFactory.get(GatewaySpiMessages.class); private static final Map> TOPOLOGY_IMPERSONATION_PROVIDERS = new ConcurrentHashMap<>(); @@ -241,4 +242,12 @@ public static List getInitParameterNamesAsList(FilterConfig filterConfig return filterConfig.getInitParameterNames() == null ? Collections.emptyList() : Collections.list(filterConfig.getInitParameterNames()); } + public static void markDoGlobalLogoutInRequest(HttpServletRequest request) { + request.setAttribute(DO_GLOBAL_LOGOUT_ATTRIBUTE, "true"); + } + + public static boolean shouldDoGlobalLogout(HttpServletRequest request) { + return request.getAttribute(DO_GLOBAL_LOGOUT_ATTRIBUTE) == null ? false : Boolean.parseBoolean((String) request.getAttribute(DO_GLOBAL_LOGOUT_ATTRIBUTE)); + } + } diff --git a/knox-token-management-ui/token-management/app/metadata.ts b/knox-token-management-ui/token-management/app/metadata.ts index da5ff3fa94..e2ba28fa98 100644 --- a/knox-token-management-ui/token-management/app/metadata.ts +++ b/knox-token-management-ui/token-management/app/metadata.ts @@ -18,6 +18,8 @@ export class Metadata { enabled: boolean; userName: string; + createdBy: string; + knoxSsoCookie: boolean; comment: string; customMetadataMap: Map; } diff --git a/knox-token-management-ui/token-management/app/token.management.component.html b/knox-token-management-ui/token-management/app/token.management.component.html index 427e603e59..9275df06e9 100644 --- a/knox-token-management-ui/token-management/app/token.management.component.html +++ b/knox-token-management-ui/token-management/app/token.management.component.html @@ -16,120 +16,89 @@
-
-
- +
- Search by Token ID, Comment or Metadata... - + Search by Token ID, (Impersonated) User Name, Comment or Metadata... + - + - Token ID - {{knoxToken.tokenId}} + Token ID + {{knoxToken.tokenId}} - Issued - {{formatDateTime(knoxToken.issueTimeLong)}} + Issued + {{formatDateTime(knoxToken.issueTimeLong)}} - Expires - {{formatDateTime(knoxToken.expirationLong)}} + Expires + {{formatDateTime(knoxToken.expirationLong)}} - - Comment - {{knoxToken.metadata.comment}} + + User Name + {{knoxToken.metadata.userName}} - - Additional Metadata - -
    -
  • - {{metadata[0]}} = {{metadata[1]}} -
  • -
+ + Impersonated + +
+ + +

{{knoxToken.metadata.createdBy}}

+
- - Actions - - - - + + KnoxSSO + + + - - - -
- -
- - - - -
- - - - Search by Token ID, Comment, Metadata or Impersonated User Name... - - - - - - Token ID - {{knoxToken.tokenId}} - - - - Issued - {{formatDateTime(knoxToken.issueTimeLong)}} - - - - Expires - {{formatDateTime(knoxToken.expirationLong)}} - - - - Comment - {{knoxToken.metadata.comment}} + + Comment + {{knoxToken.metadata.comment}} - - Additional Metadata - + + Additional Metadata +
    -
  • - {{metadata[0]}} = {{metadata[1]}} -
  • +
  • + {{metadata[0]}} = {{metadata[1]}} +
- - Impersonated User - {{knoxToken.metadata.userName}} + + Actions + + + + +

Previously Disabled SSO Cookie!

+
- - + +
- +
diff --git a/knox-token-management-ui/token-management/app/token.management.component.ts b/knox-token-management-ui/token-management/app/token.management.component.ts index fe1a11c9dd..1f26cd1395 100644 --- a/knox-token-management-ui/token-management/app/token.management.component.ts +++ b/knox-token-management-ui/token-management/app/token.management.component.ts @@ -34,17 +34,10 @@ export class TokenManagementComponent implements OnInit { userName: string; knoxTokens: MatTableDataSource = new MatTableDataSource(); - doAsKnoxTokens: MatTableDataSource = new MatTableDataSource(); - impersonationEnabled: boolean; - displayedColumns = ['tokenId', 'issued', 'expires', 'comment', 'metadata', 'actions']; - @ViewChild('ownPaginator') paginator: MatPaginator; - @ViewChild('ownSort') sort: MatSort = new MatSort(); - - impersonationDisplayedColumns = ['impersonation.tokenId', 'impersonation.issued', 'impersonation.expires', 'impersonation.comment', - 'impersonation.metadata', 'impersonation.user.name']; - @ViewChild('impersonationPaginator') impersonationPaginator: MatPaginator; - @ViewChild('impersonationSort') impersonationSort: MatSort = new MatSort(); + displayedColumns = ['tokenId', 'issued', 'expires', 'userName', 'impersonated', 'knoxSso', 'comment', 'metadata', 'actions']; + @ViewChild('knoxTokensPaginator') paginator: MatPaginator; + @ViewChild('knoxTokensSort') sort: MatSort = new MatSort(); toggleBoolean(propertyName: string) { this[propertyName] = !this[propertyName]; @@ -55,10 +48,12 @@ export class TokenManagementComponent implements OnInit { } constructor(private tokenManagementService: TokenManagementService) { - let isMatch: (record: KnoxToken, filter: String, impersonated: boolean) => boolean = (record, filter, impersonated) => { + let isMatch: (record: KnoxToken, filter: String) => boolean = (record, filter) => { let normalizedFilter = filter.trim().toLocaleLowerCase(); let matchesTokenId = record.tokenId.toLocaleLowerCase().includes(normalizedFilter); let matchesComment = record.metadata.comment && record.metadata.comment.toLocaleLowerCase().includes(normalizedFilter); + let matchesUserName = record.metadata.userName.toLocaleLowerCase().includes(normalizedFilter); + let matchesCreatedBy = record.metadata.createdBy && record.metadata.createdBy.toLocaleLowerCase().includes(normalizedFilter); let matchesCustomMetadata = false; if (record.metadata.customMetadataMap) { for (let entry of Array.from(Object.entries(record.metadata.customMetadataMap))) { @@ -71,35 +66,19 @@ export class TokenManagementComponent implements OnInit { matchesCustomMetadata = true; // nothing to match } - let matchesImpersonatedUserName = false; // doAs username should be checked only if impersonation is enabled - if (impersonated) { - matchesImpersonatedUserName = record.metadata.userName.toLocaleLowerCase().includes(normalizedFilter); - } - - return matchesTokenId || matchesComment || matchesCustomMetadata || matchesImpersonatedUserName; + return matchesTokenId || matchesComment || matchesCustomMetadata || matchesUserName || matchesCreatedBy; }; this.knoxTokens.filterPredicate = function (record, filter) { - return isMatch(record, filter, false); - }; - - this.doAsKnoxTokens.filterPredicate = function (record, filter) { - return isMatch(record, filter, true); + return isMatch(record, filter); }; this.knoxTokens.sortingDataAccessor = (item, property) => { switch(property) { - case 'metadata.comment': return item.metadata.comment; - default: return item[property]; - } - }; - - this.doAsKnoxTokens.sortingDataAccessor = (item, property) => { - let normalizedPropertyName = property.replace('impersonation.', ''); - switch(normalizedPropertyName) { case 'metadata.comment': return item.metadata.comment; case 'metadata.username': return item.metadata.userName; - default: return item[normalizedPropertyName]; + case 'metadata.createdBy': return item.metadata.createdBy; + default: return item[property]; } }; } @@ -107,51 +86,31 @@ export class TokenManagementComponent implements OnInit { ngOnInit(): void { console.debug('TokenManagementComponent --> ngOnInit()'); this.tokenManagementService.getUserName().then(userName => this.setUserName(userName)); - this.tokenManagementService.getImpersonationEnabled() - .then(impersonationEnabled => this.impersonationEnabled = impersonationEnabled === 'true'); } setUserName(userName: string) { this.userName = userName; - this.fetchAllKnoxTokens(); - } - - fetchAllKnoxTokens(): void { - this.fetchKnoxTokens(false); - this.fetchKnoxTokens(true); - } - - fetchKnoxTokens(impersonated: boolean): void { - this.tokenManagementService.getKnoxTokens(this.userName, impersonated) - .then(tokens => this.populateTokens(impersonated, tokens)); + this.fetchKnoxTokens(); } - populateTokens(impersonated: boolean, tokens: KnoxToken[]) { - if (impersonated) { - this.doAsKnoxTokens.data = tokens; - setTimeout(() => { - this.doAsKnoxTokens.paginator = this.impersonationPaginator; - this.doAsKnoxTokens.sort = this.impersonationSort; - }); - } else { - this.knoxTokens.data = tokens; - setTimeout(() => { - this.knoxTokens.paginator = this.paginator; - this.knoxTokens.sort = this.sort; - }); - } + fetchKnoxTokens(): void { + this.tokenManagementService.getKnoxTokens(this.userName).then(tokens => this.knoxTokens.data = tokens); + setTimeout(() => { + this.knoxTokens.paginator = this.paginator; + this.knoxTokens.sort = this.sort; + }); } disableToken(tokenId: string) { - this.tokenManagementService.setEnabledDisabledFlag(false, tokenId).then((response: string) => this.fetchAllKnoxTokens()); + this.tokenManagementService.setEnabledDisabledFlag(false, tokenId).then((response: string) => this.fetchKnoxTokens()); } enableToken(tokenId: string) { - this.tokenManagementService.setEnabledDisabledFlag(true, tokenId).then((response: string) => this.fetchAllKnoxTokens()); + this.tokenManagementService.setEnabledDisabledFlag(true, tokenId).then((response: string) => this.fetchKnoxTokens()); } revokeToken(tokenId: string) { - this.tokenManagementService.revokeToken(tokenId).then((response: string) => this.fetchAllKnoxTokens()); + this.tokenManagementService.revokeToken(tokenId).then((response: string) => this.fetchKnoxTokens()); } gotoTokenGenerationPage() { @@ -170,26 +129,23 @@ export class TokenManagementComponent implements OnInit { return this.isTokenExpired(expiration) ? 'red' : 'green'; } - isImpersonationEnabled(): boolean { - return this.impersonationEnabled; - } - getCustomMetadataArray(knoxToken: KnoxToken): [string, string][] { let mdMap = new Map(); if (knoxToken.metadata.customMetadataMap) { - mdMap = knoxToken.metadata.customMetadataMap; + mdMap = new Map(Object.entries(knoxToken.metadata.customMetadataMap)); } - return Array.from(Object.entries(mdMap)); + + return Array.from(mdMap); + } + + isKnoxSSoCookie(knoxToken: KnoxToken): boolean { + return knoxToken.metadata.knoxSsoCookie; } - applyFilter(impersonated: boolean, filterValue: string) { + applyFilter(filterValue: string) { filterValue = filterValue.trim(); // Remove whitespace filterValue = filterValue.toLowerCase(); // Datasource defaults to lowercase matches - if (impersonated) { - this.doAsKnoxTokens.filter = filterValue; - } else { - this.knoxTokens.filter = filterValue; - } + this.knoxTokens.filter = filterValue; } } diff --git a/knox-token-management-ui/token-management/app/token.management.service.ts b/knox-token-management-ui/token-management/app/token.management.service.ts index c9bc842a33..f38b25472e 100644 --- a/knox-token-management-ui/token-management/app/token.management.service.ts +++ b/knox-token-management-ui/token-management/app/token.management.service.ts @@ -26,8 +26,7 @@ import {KnoxToken} from './knox.token'; export class TokenManagementService { sessionUrl = window.location.pathname.replace(new RegExp('token-management/.*'), 'session/api/v1/sessioninfo'); apiUrl = window.location.pathname.replace(new RegExp('token-management/.*'), 'knoxtoken/api/v1/token/'); - getKnoxTokensUrl = this.apiUrl + 'getUserTokens?userName='; - getDoAsKnoxTokensUrl = this.apiUrl + 'getUserTokens?createdBy='; + getKnoxTokensUrl = this.apiUrl + 'getUserTokens?userNameOrCreatedBy='; enableKnoxTokenUrl = this.apiUrl + 'enable'; disableKnoxTokenUrl = this.apiUrl + 'disable'; revokeKnoxTokenUrl = this.apiUrl + 'revoke'; @@ -35,11 +34,10 @@ export class TokenManagementService { constructor(private http: HttpClient) {} - getKnoxTokens(userName: string, impersonated: boolean): Promise { + getKnoxTokens(userName: string): Promise { let headers = new HttpHeaders(); headers = this.addJsonHeaders(headers); - let urlToUse = impersonated ? this.getDoAsKnoxTokensUrl : this.getKnoxTokensUrl; - return this.http.get(urlToUse + userName, { headers: headers}) + return this.http.get(this.getKnoxTokensUrl + userName, { headers: headers}) .toPromise() .then(response => response['tokens'] as KnoxToken[]) .catch((err: HttpErrorResponse) => { diff --git a/knox-token-management-ui/token-management/assets/green_checkmark.svg b/knox-token-management-ui/token-management/assets/green_checkmark.svg new file mode 100644 index 0000000000..19e0bd7f09 --- /dev/null +++ b/knox-token-management-ui/token-management/assets/green_checkmark.svg @@ -0,0 +1,2 @@ + diff --git a/knox-token-management-ui/token-management/assets/red_cross_circle.svg b/knox-token-management-ui/token-management/assets/red_cross_circle.svg new file mode 100644 index 0000000000..730e2080c3 --- /dev/null +++ b/knox-token-management-ui/token-management/assets/red_cross_circle.svg @@ -0,0 +1,92 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + diff --git a/pom.xml b/pom.xml index 279c580e72..07927d0ecf 100644 --- a/pom.xml +++ b/pom.xml @@ -492,6 +492,7 @@ atlassian-ide-plugin.xml **/PULL_REQUEST_TEMPLATE* **/new-service-definition-template.xml + **/token-management/**/assets/**