forked from openanalytics/shinyproxy
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request 'Fix #26403: Gracefully hide app when user logs ou…
…t in another tab' (openanalytics#21) from feature/26403 into develop
- Loading branch information
Showing
7 changed files
with
145 additions
and
35 deletions.
There are no files selected for viewing
117 changes: 117 additions & 0 deletions
117
src/main/java/eu/openanalytics/shinyproxy/AuthenticationRequiredFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
/** | ||
* ShinyProxy | ||
* | ||
* Copyright (C) 2016-2021 Open Analytics | ||
* | ||
* =========================================================================== | ||
* | ||
* This program is free software: you can redistribute it and/or modify | ||
* it under the terms of the Apache License as published by | ||
* The Apache Software Foundation, either version 2 of the License, or | ||
* (at your option) any later version. | ||
* | ||
* This program is distributed in the hope that it will be useful, | ||
* but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
* Apache License for more details. | ||
* | ||
* You should have received a copy of the Apache License | ||
* along with this program. If not, see <http://www.apache.org/licenses/> | ||
*/ | ||
package eu.openanalytics.shinyproxy; | ||
|
||
import eu.openanalytics.shinyproxy.controllers.AppController; | ||
import org.springframework.security.access.AccessDeniedException; | ||
import org.springframework.security.core.AuthenticationException; | ||
import org.springframework.security.core.context.SecurityContextHolder; | ||
import org.springframework.security.web.AuthenticationEntryPoint; | ||
import org.springframework.security.web.access.ExceptionTranslationFilter; | ||
import org.springframework.security.web.util.ThrowableAnalyzer; | ||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; | ||
import org.springframework.security.web.util.matcher.OrRequestMatcher; | ||
import org.springframework.security.web.util.matcher.RequestMatcher; | ||
import org.springframework.web.filter.GenericFilterBean; | ||
|
||
import javax.servlet.FilterChain; | ||
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; | ||
|
||
/** | ||
* A filter that blocks the default {@link AuthenticationEntryPoint} when requests are made to certain endpoints. | ||
* These endpoints are: | ||
* - /app_direct_i/* /* /** (without spaces), i.e. any subpath on the app_direct endpoint (thus not the page that loads the app) | ||
* - /heartbeat/* , i.e. heartbeat requests | ||
* | ||
* When the filter detects that a user is not authenticated when requesting one of these endpoints, it returns the response: | ||
* {"status":"error", "message":"shinyproxy_authentication_required"} with status code 401. | ||
* This response is specific unique enough such that it can be handled by the frontend. | ||
* | ||
* See {@link AppController#appDirect} where a similar approach is used for apps that have been stopped. | ||
* | ||
* Note: this cannot be easily implemented as a {@link AuthenticationEntryPoint} since these entrypoints are sometimes, | ||
* but not always overridden by the authentication backend. | ||
*/ | ||
public class AuthenticationRequiredFilter extends GenericFilterBean { | ||
|
||
private final ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer(); | ||
|
||
private static final RequestMatcher REQUEST_MATCHER = new OrRequestMatcher( | ||
new AntPathRequestMatcher("/app_direct_i/*/*/**"), | ||
new AntPathRequestMatcher("/heartbeat/*")); | ||
|
||
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { | ||
HttpServletRequest request = (HttpServletRequest) req; | ||
HttpServletResponse response = (HttpServletResponse) res; | ||
|
||
try { | ||
chain.doFilter(request, response); | ||
} catch (IOException ex) { | ||
throw ex; | ||
} catch (Exception ex) { | ||
if (REQUEST_MATCHER.matches(request) && isAuthException(ex)) { | ||
if (response.isCommitted()) { | ||
throw new ServletException("Unable to handle the Spring Security Exception because the response is already committed.", ex); | ||
} | ||
SecurityContextHolder.getContext().setAuthentication(null); | ||
response.setStatus(401); | ||
response.getWriter().write("{\"status\":\"error\", \"message\":\"shinyproxy_authentication_required\"}"); | ||
return; | ||
} | ||
throw ex; | ||
} | ||
} | ||
|
||
/** | ||
* @param ex the exception to check | ||
* @return whether this exception indicates that the user is not authenticated | ||
*/ | ||
private boolean isAuthException(Exception ex) { | ||
Throwable[] causeChain = throwableAnalyzer.determineCauseChain(ex); | ||
RuntimeException ase = (AuthenticationException) throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain); | ||
if (ase != null) { | ||
return true; | ||
} | ||
ase = (AccessDeniedException) throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain); | ||
return ase != null; | ||
} | ||
|
||
/** | ||
* Based on {@link ExceptionTranslationFilter.DefaultThrowableAnalyzer} | ||
*/ | ||
private static final class DefaultThrowableAnalyzer extends ThrowableAnalyzer { | ||
protected void initExtractorMap() { | ||
super.initExtractorMap(); | ||
|
||
registerExtractor(ServletException.class, throwable -> { | ||
ThrowableAnalyzer.verifyThrowableHierarchy(throwable, | ||
ServletException.class); | ||
return ((ServletException) throwable).getRootCause(); | ||
}); | ||
} | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -120,7 +120,6 @@ body > div#navbar { padding-top: 0px; } | |
} | ||
|
||
.refreshButton { | ||
width: 2000px; | ||
font-size: 18px; | ||
} | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters