Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added subject propagation during WebSocket methods calls #10922

Merged
merged 4 commits into from
Aug 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,7 @@ private void configureSingleUserMode() {
}

private void configureMultiUserMode() {
// Not contains '/websocket/' and not ends with '/ws' or '/eventbus'
filterRegex("^(?!.*/websocket/)(?!.*(/ws|/eventbus)$).*").through(MachineLoginFilter.class);
filterRegex(".*").through(MachineLoginFilter.class);
install(new KeycloakServletModule());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import javax.annotation.PreDestroy;
import org.eclipse.che.api.core.jsonrpc.commons.RequestProcessor;
import org.eclipse.che.commons.lang.concurrent.LoggingUncaughtExceptionHandler;
import org.eclipse.che.commons.lang.concurrent.ThreadLocalPropagateContext;

@Singleton
public class ServerSideRequestProcessor implements RequestProcessor {
Expand Down Expand Up @@ -55,6 +56,6 @@ private void preDestroy() {

@Override
public void process(Runnable runnable) {
executorService.execute(runnable);
executorService.execute(ThreadLocalPropagateContext.wrap(runnable));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
import javax.websocket.OnOpen;
import javax.websocket.Session;
import org.eclipse.che.api.core.websocket.commons.WebSocketMessageReceiver;
import org.eclipse.che.commons.env.EnvironmentContext;
import org.eclipse.che.commons.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -35,6 +37,7 @@
* @author Dmitry Kuleshov
*/
public abstract class BasicWebSocketEndpoint {

private static final Logger LOG = LoggerFactory.getLogger(BasicWebSocketEndpoint.class);

private final WebSocketSessionRegistry registry;
Expand Down Expand Up @@ -71,14 +74,21 @@ public void onOpen(Session session) {

@OnMessage
public void onMessage(String messagePart, boolean last, Session session) {
StringBuffer buffer = sessionMessagesBuffer.get(session);
buffer.append(messagePart);
if (last) {
try {
onMessage(buffer.toString(), session);
} finally {
buffer.setLength(0);
try {
EnvironmentContext.getCurrent()
.setSubject((Subject) session.getUserProperties().get("che_subject"));

StringBuffer buffer = sessionMessagesBuffer.get(session);
buffer.append(messagePart);
if (last) {
try {
onMessage(buffer.toString(), session);
} finally {
buffer.setLength(0);
}
}
} finally {
EnvironmentContext.reset();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@

import com.google.inject.Injector;
import javax.inject.Inject;
import javax.servlet.http.HttpSession;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;

/**
Expand All @@ -26,4 +29,14 @@ public class GuiceInjectorEndpointConfigurator extends ServerEndpointConfig.Conf
public <T> T getEndpointInstance(Class<T> endpointClass) {
return injector.getInstance(endpointClass);
}

@Override
public void modifyHandshake(
ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
HttpSession httpSession = (HttpSession) request.getHttpSession();
Object sessionSubject = httpSession.getAttribute("che_subject");
if (sessionSubject != null) {
sec.getUserProperties().put("che_subject", sessionSubject);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
import javax.servlet.Filter;
import javax.servlet.FilterConfig;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
Expand All @@ -31,11 +30,7 @@ public abstract class AbstractKeycloakFilter implements Filter {
@Inject protected JwtParser jwtParser;

/** when a request came from a machine with valid token then auth is not required */
boolean shouldSkipAuthentication(HttpServletRequest request, String token) {
if (token == null) {
return request.getRequestURI() != null
&& request.getRequestURI().endsWith("api/keycloak/OIDCKeycloak.js");
}
boolean shouldSkipAuthentication(String token) {
try {
jwtParser.parse(token);
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)

Jws<Claims> jwt;
try {
if (shouldSkipAuthentication(request, token)) {
if (shouldSkipAuthentication(token)) {
chain.doFilter(req, res);
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha

final HttpServletRequest httpRequest = (HttpServletRequest) request;
final String token = tokenExtractor.getToken(httpRequest);
if (shouldSkipAuthentication(httpRequest, token)) {
if (shouldSkipAuthentication(token)) {
filterChain.doFilter(request, response);
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,25 @@
import org.eclipse.che.multiuser.keycloak.server.UnavailableResourceInMultiUserFilter;

public class KeycloakServletModule extends ServletModule {

private static final String KEYCLOAK_FILTER_PATHS =
"^"
// not equals to /keycloak/OIDCKeycloak.js
+ "(?!/keycloak/OIDCKeycloak.js)"
// not contains /docs/ (for swagger)
+ "(?!.*(/docs/))"
// not ends with '/oauth/callback/' or '/keycloak/settings/' or '/system/state'
+ "(?!.*(/keycloak/settings/?|/oauth/callback/?|/system/state/?)$)"
// all other
+ ".*";

@Override
protected void configureServlets() {
bind(KeycloakAuthenticationFilter.class).in(Singleton.class);

// Not contains /docs/ (for swagger) and not ends with '/oauth/callback/' or
// '/keycloak/settings/' or '/system/state'
filterRegex("^(?!.*(/docs/))(?!.*(/keycloak/settings/?|/oauth/callback/?|/system/state/?)$).*")
.through(KeycloakAuthenticationFilter.class);
filterRegex("^(?!.*(/docs/))(?!.*(/keycloak/settings/?|/oauth/callback/?|/system/state/?)$).*")
.through(KeycloakEnvironmentInitalizationFilter.class);
filterRegex("^(?!.*(/docs/))(?!.*(/keycloak/settings/?|/api/oauth/callback/?)$).*")
.through(IdentityIdLoggerFilter.class);
filterRegex(KEYCLOAK_FILTER_PATHS).through(KeycloakAuthenticationFilter.class);
filterRegex(KEYCLOAK_FILTER_PATHS).through(KeycloakEnvironmentInitalizationFilter.class);
filterRegex(KEYCLOAK_FILTER_PATHS).through(IdentityIdLoggerFilter.class);

// Ban change password (POST /user/password) and create a user (POST /user/) methods
// but not remove user (DELETE /user/{USER_ID}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,28 +49,22 @@ public void setup() {
when(request.getRequestURI()).thenReturn(null);
}

@Test
public void testShouldSkipAuthWhenRetrievingOIDCKeycloakJsFile() {
when(request.getRequestURI()).thenReturn("https://localhost:8080/api/keycloak/OIDCKeycloak.js");
assertTrue(abstractKeycloakFilter.shouldSkipAuthentication(request, null));
}

@Test
public void testShouldNotSkipAuthWhenNullTokenProvided() {
assertFalse(abstractKeycloakFilter.shouldSkipAuthentication(request, null));
assertFalse(abstractKeycloakFilter.shouldSkipAuthentication(null));
}

@Test
public void testShouldNotSkipAuthWhenProvidedTokenIsNotMachine() {
Jwt mock = Mockito.mock(Jwt.class);
doReturn(mock).when(jwtParser).parse(anyString());
assertFalse(abstractKeycloakFilter.shouldSkipAuthentication(request, "token"));
assertFalse(abstractKeycloakFilter.shouldSkipAuthentication("token"));
}

@Test
public void testAuthIsNotNeededWhenMachineTokenProvided() {
when(jwtParser.parse(anyString())).thenThrow(MachineTokenJwtException.class);
assertTrue(abstractKeycloakFilter.shouldSkipAuthentication(request, "token"));
assertTrue(abstractKeycloakFilter.shouldSkipAuthentication("token"));
}

static class TestLoginFilter extends AbstractKeycloakFilter {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha

// sets subject from session
final Subject sessionSubject;
if (session != null && (sessionSubject = (Subject) session.getAttribute("principal")) != null) {
if (session != null
&& (sessionSubject = (Subject) session.getAttribute("che_subject")) != null) {
try {
EnvironmentContext.getCurrent().setSubject(sessionSubject);
chain.doFilter(request, response);
Expand Down Expand Up @@ -115,7 +116,7 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
false);
EnvironmentContext.getCurrent().setSubject(subject);
final HttpSession httpSession = httpRequest.getSession(true);
httpSession.setAttribute("principal", subject);
httpSession.setAttribute("che_subject", subject);
chain.doFilter(request, response);
} finally {
EnvironmentContext.reset();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public class MachineLoginFilterTest {

private static final String SIGNATURE_ALGORITHM = "RSA";
private static final String WORKSPACE_ID = "workspace31";
private static final String PRINCIPAL = "principal";
private static final String SUBJECT = "che_subject";
private static final String USER_ID = "test_user31";
private static final String USER_NAME = "test_user";

Expand Down Expand Up @@ -105,11 +105,11 @@ public void setUp() throws Exception {

@Test
public void testProcessRequestWithSubjectFromSession() throws Exception {
when(sessionMock.getAttribute(PRINCIPAL)).thenReturn(subject);
when(sessionMock.getAttribute(SUBJECT)).thenReturn(subject);

machineLoginFilter.doFilter(getRequestMock(sessionMock, machineToken), responseMock, chainMock);

verify(sessionMock).getAttribute(PRINCIPAL);
verify(sessionMock).getAttribute(SUBJECT);
verifyZeroInteractions(tokenExtractorMock);
}

Expand All @@ -129,7 +129,7 @@ public void testProcessRequestWithValidTokenAndCreateSession() throws Exception
machineLoginFilter.doFilter(getRequestMock(null, machineToken), responseMock, chainMock);

verify(tokenExtractorMock).getToken(any(HttpServletRequest.class));
verify(sessionMock).setAttribute(PRINCIPAL, subject);
verify(sessionMock).setAttribute(SUBJECT, subject);
verifyZeroInteractions(responseMock);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.eclipse.che.api.core.NotFoundException;
import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.api.user.server.UserManager;
Expand Down Expand Up @@ -83,21 +84,24 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
}
// check token signature and verify is this token machine or not
try {
final Claims claims = jwtParser.parseClaimsJws(token).getBody();
HttpSession session = ((HttpServletRequest) request).getSession(true);
Subject sessionSubject = (Subject) session.getAttribute("che_subject");
if (sessionSubject == null || !sessionSubject.getToken().equals(token)) {
try {
sessionSubject = extractSubject(token);
session.setAttribute("che_subject", sessionSubject);
} catch (NotFoundException e) {
sendErr(
response,
SC_UNAUTHORIZED,
"Authentication with machine token failed because user for this token no longer exist.");
return;
}
}

try {
final String userId = claims.get(USER_ID_CLAIM, String.class);
// check if user with such id exists
final String userName = userManager.getById(userId).getName();
final Subject authorizedSubject =
new AuthorizedSubject(
new SubjectImpl(userName, userId, token, false), permissionChecker);
EnvironmentContext.getCurrent().setSubject(authorizedSubject);
chain.doFilter(addUserInRequest(httpRequest, authorizedSubject), response);
} catch (NotFoundException ex) {
sendErr(
response,
SC_UNAUTHORIZED,
"Authentication with machine token failed because user for this token no longer exist.");
EnvironmentContext.getCurrent().setSubject(sessionSubject);
chain.doFilter(addUserInRequest(httpRequest, sessionSubject), response);
} finally {
EnvironmentContext.reset();
}
Expand All @@ -112,6 +116,16 @@ public void doFilter(ServletRequest request, ServletResponse response, FilterCha
}
}

private Subject extractSubject(String token) throws NotFoundException, ServerException {
final Claims claims = jwtParser.parseClaimsJws(token).getBody();
final String userId = claims.get(USER_ID_CLAIM, String.class);
// check if user with such id exists
final String userName = userManager.getById(userId).getName();

return new AuthorizedSubject(
new SubjectImpl(userName, userId, token, false), permissionChecker);
}

/** Sets given error code with err message into give response. */
private static void sendErr(ServletResponse res, int errCode, String msg) throws IOException {
final HttpServletResponse response = (HttpServletResponse) res;
Expand Down