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 c49302a0f KNOX-2961 - Knox SSO cookie Invalidation - Phase I (#797)
c49302a0f is described below
commit c49302a0f7ac27f92811d4d65cdc76da7077f5d2
Author: Sandor Molnar <[email protected]>
AuthorDate: Fri Oct 6 09:23:26 2023 +0200
KNOX-2961 - Knox SSO cookie Invalidation - Phase I (#797)
---
.../resources/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 +++-
gateway-release/home/conf/topologies/homepage.xml | 5 +
gateway-release/home/conf/topologies/knoxsso.xml | 5 +
gateway-release/home/conf/topologies/manager.xml | 5 +
.../token/impl/DefaultTokenStateService.java | 2 +-
.../token/impl/TokenStateServiceMessages.java | 9 ++
.../impl/AliasBasedTokenStateServiceTest.java | 1 +
.../gateway/service/knoxsso/KnoxSSOMessages.java | 3 +
.../gateway/service/knoxsso/WebSSOResource.java | 30 +++++
.../gateway/service/knoxtoken/TokenResource.java | 32 ++++--
.../services/security/token/TokenMetadata.java | 11 +-
.../services/security/token/TokenUtils.java | 28 +++--
.../knox/gateway/session/SessionInvalidator.java | 52 +++++++++
.../knox/gateway/session/SessionInvalidators.java | 51 +++++++++
.../apache/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 +-
.../token-management/assets/green_checkmark.svg | 2 +
.../token-management/assets/red_cross_circle.svg | 92 +++++++++++++++
pom.xml | 1 +
26 files changed, 498 insertions(+), 194 deletions(-)
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 1821ac7bf..9dea1d366 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 @@
<script src="libs/bower/jquery/js/jquery-3.5.1.min.js" ></script>
<script type="text/javascript" src="js/knoxauth.js"></script>
+
+ <%
+ final boolean autoGlobalLogout =
"1".equals(request.getParameter("autoGlobalLogout"));
+ if (autoGlobalLogout) {%>
+ <script type="text/javascript">
+ window.onload=function() {
+
window.setTimeout(document.getElementById("globalLogoutForm").submit(), 10);
+ };
+ </script>
+ <%}%>
+
<%
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;
}
-
-
%>
<!-- Helper function to delete cookie -->
@@ -177,18 +186,20 @@
<%
if (globalLogoutPageURL != null && !globalLogoutPageURL.isEmpty())
{
%>
- <p style="color: white;display: block">
+ <form method="POST" action="#" id="globalLogoutForm">
+ <div>
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.
- <a href="<%= request.getRequestURI() %>?globalLogout=1"
>Global Logout</a>
- </p>
+ <input type="hidden" name="globalLogout" value="1"
id="globalLogoutUrl"/>
+ <button type="submit" style="background: none!important;
border: none; padding: 0!important; color: #06A; text-decoration: none; cursor:
pointer;">Global Logout</button>
+ </form>
</div>
<%
}
- }
+ }
else {
%>
<div style="background: gray;text-color: white;text-align:center;">
diff --git a/gateway-provider-security-jwt/pom.xml
b/gateway-provider-security-jwt/pom.xml
index e02f3ceed..f36258f5d 100644
--- a/gateway-provider-security-jwt/pom.xml
+++ b/gateway-provider-security-jwt/pom.xml
@@ -85,5 +85,9 @@
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpcore</artifactId>
+ </dependency>
</dependencies>
</project>
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 098b559eb..b188539f2 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 f1b86f50f..81d6e1a97 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.security.PrimaryPrincipal;
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<String> unAuthenticatedPaths = new HashSet(20);
+ private Set<String> unAuthenticatedPaths = new HashSet<>(20);
@Override
public void init( FilterConfig filterConfig ) throws ServletException {
@@ -175,7 +178,10 @@ public class SSOCookieFederationFilter extends
AbstractJWTFilter {
// 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 @@ public class SSOCookieFederationFilter extends
AbstractJWTFilter {
}
@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 @@ public class SSOCookieFederationFilter extends
AbstractJWTFilter {
}
}
+ 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 @@ public class SSOCookieFederationFilter extends
AbstractJWTFilter {
+ 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 class SSOCookieFederationFilter extends
AbstractJWTFilter {
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 @@ public class SSOCookieFederationFilter extends
AbstractJWTFilter {
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 b356cfe6a..41642121e 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.CryptoService;
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.ServletException;
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 @@ import java.util.Map;
*
* @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 class Pac4jDispatcherFilter implements Filter {
config.setSessionStore(sessionStore);
+ SessionInvalidators.KNOX_SSO_INVALIDATOR.registerSessionInvalidator(this);
+
}
/**
@@ -322,5 +330,16 @@ public class Pac4jDispatcherFilter implements Filter {
}
@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 e6e8ecaa9..744824f0c 100644
--- a/gateway-release/home/conf/topologies/homepage.xml
+++ b/gateway-release/home/conf/topologies/homepage.xml
@@ -55,6 +55,11 @@
<role>federation</role>
<name>SSOCookieProvider</name>
<enabled>true</enabled>
+ <param>
+ <!-- since 2.1.0: KnoxSSO cookie validation -->
+ <name>knox.token.exp.server-managed</name>
+ <value>false</value>
+ </param>
</provider>
<provider>
<role>identity-assertion</role>
diff --git a/gateway-release/home/conf/topologies/knoxsso.xml
b/gateway-release/home/conf/topologies/knoxsso.xml
index 75cab7bfd..99600f874 100644
--- a/gateway-release/home/conf/topologies/knoxsso.xml
+++ b/gateway-release/home/conf/topologies/knoxsso.xml
@@ -114,6 +114,11 @@
<name>knoxsso.token.ttl</name>
<value>86400000</value>
</param>
+ <param>
+ <!-- since 2.1.0: KnoxSSO cookie validation -->
+ <name>knox.token.exp.server-managed</name>
+ <value>false</value>
+ </param>
</service>
</topology>
diff --git a/gateway-release/home/conf/topologies/manager.xml
b/gateway-release/home/conf/topologies/manager.xml
index 7e3084137..acade305c 100644
--- a/gateway-release/home/conf/topologies/manager.xml
+++ b/gateway-release/home/conf/topologies/manager.xml
@@ -51,6 +51,11 @@
<role>federation</role>
<name>SSOCookieProvider</name>
<enabled>true</enabled>
+ <param>
+ <!-- since 2.1.0: KnoxSSO cookie validation -->
+ <name>knox.token.exp.server-managed</name>
+ <value>false</value>
+ </param>
</provider>
<provider>
<role>identity-assertion</role>
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 cfe9a4a80..9eb327e8f 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,7 +350,7 @@ public class DefaultTokenStateService implements
TokenStateService {
}
/**
- * 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()) {
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 f24c1889b..75cbb1b27 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 5c6dbcba2..f6971f483 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 class AliasBasedTokenStateServiceTest extends
DefaultTokenStateServiceTes
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-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 a1dc1bf1c..1b47a90f8 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 a4ed8e1b1..94e2f3482 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.AliasServiceException;
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<String> ssoExpectedparams = new ArrayList<>();
private String clusterName;
private String tokenIssuer;
+ private TokenStateService tokenStateService;
private String sameSiteValue;
@@ -142,6 +146,13 @@ public class WebSSOResource {
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 @@ public class WebSSOResource {
.setSigningKeystoreName(signingKeystoreName)
.setSigningKeystoreAlias(signingKeystoreAlias)
.setSigningKeystorePassphrase(signingKeystorePassphrase)
+ .setManaged(tokenStateService != null)
.build();
JWT token = tokenAuthority.issueToken(jwtAttributes);
@@ -303,6 +315,7 @@ public class WebSSOResource {
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 @@ public class WebSSOResource {
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 7b769e792..3784e8b10 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.Collections;
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 class TokenResource {
final String userName =
uriInfo.getQueryParameters().getFirst("userName");
final String createdBy =
uriInfo.getQueryParameters().getFirst("createdBy");
- final Collection<KnoxToken> userTokens = createdBy == null ?
tokenStateService.getTokens(userName) :
tokenStateService.getDoAsTokens(createdBy);
+ final String userNameOrCreatedBy =
uriInfo.getQueryParameters().getFirst("userNameOrCreatedBy");
+ final Collection<KnoxToken> 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<KnoxToken> tokens = new TreeSet<>();
if (metadataMap.isEmpty()) {
tokens.addAll(userTokens);
@@ -568,12 +576,17 @@ public class TokenResource {
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 class TokenResource {
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 b90dfb490..a3f7d29ad 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<String> KNOWN_MD_NAMES = Arrays.asList(USER_NAME,
COMMENT, ENABLED, PASSCODE, CREATED_BY);
+ public static final String KNOX_SSO_COOKIE = "knoxSSOCookie";
+ private static final List<String> KNOWN_MD_NAMES = Arrays.asList(USER_NAME,
COMMENT, ENABLED, PASSCODE, CREATED_BY, KNOX_SSO_COOKIE);
private final Map<String, String> metadataMap = new HashMap<>();
@@ -118,6 +119,14 @@ public class TokenMetadata {
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 7471150d0..4ba1defb0 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 class TokenUtils {
* @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 <code>configuredSignatureAlgorithm</code> if any OR the default
HMAC algorithm if {@link #useHMAC(char[], String)} is
* <code>true</code>; 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 000000000..9ce78cea0
--- /dev/null
+++
b/gateway-spi/src/main/java/org/apache/knox/gateway/session/SessionInvalidator.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.knox.gateway.session;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+
+/**
+ * <p>
+ * This interface helps processing KnoxSSO cookie invalidation flows in a way
+ * such as implementations set the appropriate parameters in the given
+ * request/response objects which the {@link SSOCookieFederationFilter} can
+ * handle.
+ * </p>
+ * <p>
+ * All implementations must be registered/unregistered in the
+ * {@link SessionInvalidators.KNOX_SSO_INVALIDATOR} container, otherwise they
+ * will not be notified about authentication errors by
+ * {@link SSOCookieFederationFilter}
+ * </p>
+ * The idea is to implement the observer pattern so that different layers can
be
+ * decoupled but still be notified about authentication issues.
+ * </p>
+ *
+ * @see <a href="https://issues.apache.org/jira/browse/KNOX-2961">KNOX-2961</a>
+ * for more information on KnoxSSO cookie validation.
+ */
+public interface SessionInvalidator {
+
+ /**
+ * Triggers a notification that an authentication error occurred that is
+ * relevant from KnoxSSO cookie invalidation perspective
+ */
+ 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 000000000..971977fab
--- /dev/null
+++
b/gateway-spi/src/main/java/org/apache/knox/gateway/session/SessionInvalidators.java
@@ -0,0 +1,51 @@
+/*
+ * 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;
+
+/**
+ * A singleton container for {@link SessionInvalidator} instances that must be
used
+ * within the JVM when dealing with KnoxSSO cookie invalidation signals.
+ *
+ * All {@link SessionInvalidator} implementations should register/unregister
themselves
+ * here upon their own initialization/destroy phases.
+ */
+public enum SessionInvalidators {
+
+ /**
+ * The only session invalidatiors container instance.
+ */
+ KNOX_SSO_INVALIDATOR;
+
+ private final Set<SessionInvalidator> sessionInvalidators =
Collections.synchronizedSet(new HashSet<>());
+
+ public void registerSessionInvalidator(SessionInvalidator
sessionInvalidator) {
+ sessionInvalidators.add(sessionInvalidator);
+ }
+
+ public void unregisterSessionInvalidator(SessionInvalidator
sessionInvalidator) {
+ sessionInvalidators.remove(sessionInvalidator);
+ }
+
+ public Set<SessionInvalidator> 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 5bd8a7e28..676cf4b23 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<String, Map<String, ImpersonationProvider>>
TOPOLOGY_IMPERSONATION_PROVIDERS = new ConcurrentHashMap<>();
@@ -241,4 +242,12 @@ public class AuthFilterUtils {
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 da5ff3fa9..e2ba28fa9 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<string, string>;
}
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 427e603e5..9275df06e 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 @@
<div>
<button (click)="gotoTokenGenerationPage();">Generate New
Token</button>
- <button type="button" title="Refresh Knox Tokens"
(click)="fetchAllKnoxTokens();">
+ <button type="button" title="Refresh Knox Tokens"
(click)="fetchKnoxTokens();">
<span class="glyphicon glyphicon-refresh"></span>
</button>
</div>
- <div class="table-responsive" style="width:100%; overflow: auto;
overflow-y: scroll; padding: 10px 0px 0px 0px;">
- <label>My Knox Tokens</label>
+ <div class="table-responsive" style="width:100%; overflow: auto;
overflow-y: auto; padding: 10px 0px 0px 0px;">
<mat-form-field [floatLabel]="always" appearance="fill" #search>
- <mat-label>Search by Token ID, Comment or Metadata...</mat-label>
- <input matInput (keyup)="applyFilter(false, $event.target.value)">
+ <mat-label>Search by Token ID, (Impersonated) User Name, Comment
or Metadata...</mat-label>
+ <input matInput (keyup)="applyFilter($event.target.value)">
</mat-form-field>
- <mat-table [dataSource]="knoxTokens" matSort #ownSort="matSort">
+ <mat-table [dataSource]="knoxTokens" matSort #knoxTokensSort="matSort">
<ng-container matColumnDef="tokenId">
- <mat-header-cell *matHeaderCellDef
mat-sort-header="tokenId">Token ID</mat-header-cell>
- <mat-cell *matCellDef="let
knoxToken">{{knoxToken.tokenId}}</mat-cell>
+ <mat-header-cell *matHeaderCellDef mat-sort-header="tokenId"
style="text-align: center; justify-content: center;">Token ID</mat-header-cell>
+ <mat-cell *matCellDef="let knoxToken" style="text-align:
center; justify-content: center;">{{knoxToken.tokenId}}</mat-cell>
</ng-container>
<ng-container matColumnDef="issued">
- <mat-header-cell *matHeaderCellDef
mat-sort-header="issueTime">Issued</mat-header-cell>
- <mat-cell *matCellDef="let
knoxToken">{{formatDateTime(knoxToken.issueTimeLong)}}</mat-cell>
+ <mat-header-cell *matHeaderCellDef mat-sort-header="issueTime"
style="text-align: center; justify-content: center;">Issued</mat-header-cell>
+ <mat-cell *matCellDef="let knoxToken" style="text-align:
center; justify-content:
center;">{{formatDateTime(knoxToken.issueTimeLong)}}</mat-cell>
</ng-container>
<ng-container matColumnDef="expires">
- <mat-header-cell *matHeaderCellDef
mat-sort-header="expiration">Expires</mat-header-cell>
- <mat-cell *matCellDef="let knoxToken"
[style.color]="getExpirationColor(knoxToken.expirationLong)">{{formatDateTime(knoxToken.expirationLong)}}</mat-cell>
+ <mat-header-cell *matHeaderCellDef
mat-sort-header="expiration" style="text-align: center; justify-content:
center;">Expires</mat-header-cell>
+ <mat-cell *matCellDef="let knoxToken"
[style.color]="getExpirationColor(knoxToken.expirationLong)" style="text-align:
center; justify-content:
center;">{{formatDateTime(knoxToken.expirationLong)}}</mat-cell>
</ng-container>
- <ng-container matColumnDef="comment">
- <mat-header-cell *matHeaderCellDef
mat-sort-header="metadata.comment">Comment</mat-header-cell>
- <mat-cell *matCellDef="let
knoxToken">{{knoxToken.metadata.comment}}</mat-cell>
+ <ng-container matColumnDef="userName">
+ <mat-header-cell *matHeaderCellDef mat-sort-header="userName"
style="text-align: center; justify-content: center;">User Name</mat-header-cell>
+ <mat-cell *matCellDef="let knoxToken" style="text-align:
center; justify-content: center;">{{knoxToken.metadata.userName}}</mat-cell>
</ng-container>
- <ng-container matColumnDef="metadata">
- <mat-header-cell *matHeaderCellDef>Additional
Metadata</mat-header-cell>
- <mat-cell *matCellDef="let knoxToken">
- <ul>
- <li *ngFor="let metadata of
getCustomMetadataArray(knoxToken)">
- {{metadata[0]}} = {{metadata[1]}}
- </li>
- </ul>
+ <ng-container matColumnDef="impersonated">
+ <mat-header-cell *matHeaderCellDef
mat-sort-header="impersonated" style="text-align: center; justify-content:
center;">Impersonated</mat-header-cell>
+ <mat-cell *matCellDef="let knoxToken" style="text-align:
center; justify-content: center;">
+ <div>
+ <img *ngIf="!knoxToken.metadata.createdBy"
src="assets/red_cross_circle.svg" style="height:20px; width:auto" />
+ <img *ngIf="knoxToken.metadata.createdBy"
src="assets/green_checkmark.svg" style="height:20px; width:auto" />
+ <p
*ngIf="knoxToken.metadata.createdBy">{{knoxToken.metadata.createdBy}}</p>
+ </div>
</mat-cell>
</ng-container>
- <ng-container matColumnDef="actions">
- <mat-header-cell *matHeaderCellDef>Actions</mat-header-cell>
- <mat-cell *matCellDef="let knoxToken">
- <button *ngIf="knoxToken.metadata.enabled &&
!isTokenExpired(knoxToken.expirationLong)"
(click)="disableToken(knoxToken.tokenId);">Disable</button>
- <button *ngIf="!knoxToken.metadata.enabled &&
!isTokenExpired(knoxToken.expirationLong)"
(click)="enableToken(knoxToken.tokenId);">Enable</button>
- <button
(click)="revokeToken(knoxToken.tokenId);">Revoke</button>
+ <ng-container matColumnDef="knoxSso">
+ <mat-header-cell *matHeaderCellDef mat-sort-header="knoxSso"
style="text-align: center; justify-content: center;">KnoxSSO</mat-header-cell>
+ <mat-cell *matCellDef="let knoxToken" style="text-align:
center; justify-content: center;">
+ <img *ngIf="isKnoxSSoCookie(knoxToken)"
src="assets/green_checkmark.svg" style="height:20px; width:auto" />
+ <img *ngIf="!isKnoxSSoCookie(knoxToken)"
src="assets/red_cross_circle.svg" style="height:20px; width:auto" />
</mat-cell>
</ng-container>
- <mat-header-row
*matHeaderRowDef="displayedColumns"></mat-header-row>
- <mat-row *matRowDef="let row; columns:
displayedColumns;"></mat-row>
-
- </mat-table>
- <mat-paginator #ownPaginator [pageSizeOptions]="[5, 10, 25, 100]"
[showFirstLastButtons]="true"></mat-paginator>
- </div>
-
-
- <!-- 'doAs' Knox Tokens (tokens created by the current user on behalf on
another user -->
-
- <div class="table-responsive" style="width:100%; overflow: auto;
overflow-y: scroll; padding: 10px 0px 0px 0px;">
- <label>Impersonated Knox Tokens</label>
-
- <mat-form-field [floatLabel]="always" appearance="fill"
#impersonationSearch>
- <mat-label>Search by Token ID, Comment, Metadata or Impersonated
User Name...</mat-label>
- <input matInput (keyup)="applyFilter(true, $event.target.value)">
- </mat-form-field>
-
- <mat-table [dataSource]="doAsKnoxTokens" matSort
#impersonationSort="matSort">
- <ng-container matColumnDef="impersonation.tokenId">
- <mat-header-cell *matHeaderCellDef
mat-sort-header="impersonation.tokenId">Token ID</mat-header-cell>
- <mat-cell *matCellDef="let
knoxToken">{{knoxToken.tokenId}}</mat-cell>
- </ng-container>
-
- <ng-container matColumnDef="impersonation.issued">
- <mat-header-cell *matHeaderCellDef
mat-sort-header="impersonation.issueTime">Issued</mat-header-cell>
- <mat-cell *matCellDef="let
knoxToken">{{formatDateTime(knoxToken.issueTimeLong)}}</mat-cell>
- </ng-container>
-
- <ng-container matColumnDef="impersonation.expires">
- <mat-header-cell *matHeaderCellDef
mat-sort-header="impersonation.expiration">Expires</mat-header-cell>
- <mat-cell *matCellDef="let knoxToken"
[style.color]="getExpirationColor(knoxToken.expirationLong)">{{formatDateTime(knoxToken.expirationLong)}}</mat-cell>
- </ng-container>
-
- <ng-container matColumnDef="impersonation.comment">
- <mat-header-cell *matHeaderCellDef
mat-sort-header="impersonation.metadata.comment">Comment</mat-header-cell>
- <mat-cell *matCellDef="let
knoxToken">{{knoxToken.metadata.comment}}</mat-cell>
+ <ng-container matColumnDef="comment">
+ <mat-header-cell *matHeaderCellDef
mat-sort-header="metadata.comment" style="text-align: center; justify-content:
center;">Comment</mat-header-cell>
+ <mat-cell *matCellDef="let knoxToken" style="text-align:
center; justify-content: center;">{{knoxToken.metadata.comment}}</mat-cell>
</ng-container>
- <ng-container matColumnDef="impersonation.metadata">
- <mat-header-cell *matHeaderCellDef>Additional
Metadata</mat-header-cell>
- <mat-cell *matCellDef="let knoxToken">
+ <ng-container matColumnDef="metadata">
+ <mat-header-cell *matHeaderCellDef style="text-align: center;
justify-content: center;">Additional Metadata</mat-header-cell>
+ <mat-cell *matCellDef="let knoxToken" style="text-align:
center; justify-content: center;">
<ul>
- <li *ngFor="let metadata of
getCustomMetadataArray(knoxToken)">
- {{metadata[0]}} = {{metadata[1]}}
- </li>
+ <li *ngFor="let metadata of
getCustomMetadataArray(knoxToken)">
+ {{metadata[0]}} = {{metadata[1]}}
+ </li>
</ul>
</mat-cell>
</ng-container>
- <ng-container matColumnDef="impersonation.user.name">
- <mat-header-cell *matHeaderCellDef
mat-sort-header="impersonation.metadata.username">Impersonated
User</mat-header-cell>
- <mat-cell *matCellDef="let
knoxToken">{{knoxToken.metadata.userName}}</mat-cell>
+ <ng-container matColumnDef="actions" style="text-align: center;
justify-content: center;">
+ <mat-header-cell *matHeaderCellDef style="text-align: center;
justify-content: center;">Actions</mat-header-cell>
+ <mat-cell *matCellDef="let knoxToken">
+ <button *ngIf="knoxToken.metadata.enabled &&
!isTokenExpired(knoxToken.expirationLong)"
(click)="disableToken(knoxToken.tokenId);">Disable</button>
+ <button *ngIf="!isKnoxSSoCookie(knoxToken) &&
!knoxToken.metadata.enabled && !isTokenExpired(knoxToken.expirationLong)"
(click)="enableToken(knoxToken.tokenId);">Enable</button>
+ <button *ngIf="!isKnoxSSoCookie(knoxToken)"
(click)="revokeToken(knoxToken.tokenId);">Revoke</button>
+ <p *ngIf="isKnoxSSoCookie(knoxToken) &&
!knoxToken.metadata.enabled" style="color:orange">Previously Disabled SSO
Cookie!</p>
+ </mat-cell>
</ng-container>
- <mat-header-row
*matHeaderRowDef="impersonationDisplayedColumns"></mat-header-row>
- <mat-row *matRowDef="let row; columns:
impersonationDisplayedColumns"></mat-row>
+ <mat-header-row
*matHeaderRowDef="displayedColumns"></mat-header-row>
+ <mat-row *matRowDef="let row; columns:
displayedColumns;"></mat-row>
</mat-table>
- <mat-paginator #impersonationPaginator [pageSizeOptions]="[5, 10, 25,
100]" [showFirstLastButtons]="true"></mat-paginator>
+ <mat-paginator #knoxTokensPaginator [pageSizeOptions]="[5, 10, 25,
100]" [pageSize]="25" [showFirstLastButtons]="true"></mat-paginator>
</div>
</div>
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 fe1a11c9d..1f26cd139 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<KnoxToken> = new MatTableDataSource();
- doAsKnoxTokens: MatTableDataSource<KnoxToken> = 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 c9bc842a3..f38b25472 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<KnoxToken[]> {
+ getKnoxTokens(userName: string): Promise<KnoxToken[]> {
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 000000000..19e0bd7f0
--- /dev/null
+++ b/knox-token-management-ui/token-management/assets/green_checkmark.svg
@@ -0,0 +1,2 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"
enable-background="new 0 0 64 64"><path
d="M32,2C15.431,2,2,15.432,2,32c0,16.568,13.432,30,30,30c16.568,0,30-13.432,30-30C62,15.432,48.568,2,32,2z
M25.025,50
+
l-0.02-0.02L24.988,50L11,35.6l7.029-7.164l6.977,7.184l21-21.619L53,21.199L25.025,50z"
fill="#43a047"/></svg>
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 000000000..730e2080c
--- /dev/null
+++ b/knox-token-management-ui/token-management/assets/red_cross_circle.svg
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ viewBox="0 0 48 48"
+ version="1.1"
+ id="svg15"
+ sodipodi:docname="cross red circle.svg"
+ inkscape:version="0.92.3 (2405546, 2018-03-11)">
+ <metadata
+ id="metadata19">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1920"
+ inkscape:window-height="1027"
+ id="namedview17"
+ showgrid="false"
+ inkscape:zoom="4.9166667"
+ inkscape:cx="-11.694915"
+ inkscape:cy="40.271186"
+ inkscape:window-x="-8"
+ inkscape:window-y="-8"
+ inkscape:window-maximized="1"
+ inkscape:current-layer="g13" />
+ <defs
+ id="defs7">
+ <linearGradient
+ id="linearGradient828"
+ osb:paint="solid">
+ <stop
+ style="stop-color:#ff0000;stop-opacity:1;"
+ offset="0"
+ id="stop826" />
+ </linearGradient>
+ <linearGradient
+ id="0"
+ gradientUnits="userSpaceOnUse"
+ y1="47.37"
+ x2="0"
+ y2="-1.429">
+ <stop
+ stop-color="#c52828"
+ id="stop2" />
+ <stop
+ offset="1"
+ stop-color="#ff5454"
+ id="stop4" />
+ </linearGradient>
+ </defs>
+ <g
+ transform="matrix(.99999 0 0 .99999-58.37.882)"
+ enable-background="new"
+ id="g13"
+ style="fill-opacity:1">
+ <circle
+ cx="82.37"
+ cy="23.12"
+ r="24"
+ fill="url(#0)"
+ id="circle9"
+ style="fill-opacity:1;fill:#dd3333" />
+ <path
+ d="m87.77 23.725l5.939-5.939c.377-.372.566-.835.566-1.373
0-.54-.189-.997-.566-1.374l-2.747-2.747c-.377-.372-.835-.564-1.373-.564-.539
0-.997.186-1.374.564l-5.939
5.939-5.939-5.939c-.377-.372-.835-.564-1.374-.564-.539
0-.997.186-1.374.564l-2.748 2.747c-.377.378-.566.835-.566 1.374 0
.54.188.997.566 1.373l5.939 5.939-5.939 5.94c-.377.372-.566.835-.566 1.373 0
.54.188.997.566 1.373l2.748 2.747c.377.378.835.564 1.374.564.539 0 .997-.186
1.374-.564l5.939-5.939 5.94 5.939c.377.378.835. [...]
+ fill="#fff"
+ fill-opacity=".842"
+ id="path11"
+ style="fill-opacity:1;fill:#ffffff" />
+ </g>
+</svg>
diff --git a/pom.xml b/pom.xml
index 279c580e7..07927d0ec 100644
--- a/pom.xml
+++ b/pom.xml
@@ -492,6 +492,7 @@
<exclude>atlassian-ide-plugin.xml</exclude>
<exclude>**/PULL_REQUEST_TEMPLATE*</exclude>
<exclude>**/new-service-definition-template.xml</exclude>
+ <exclude>**/token-management/**/assets/**</exclude>
</excludes>
</configuration>
</plugin>