Limit scope of the machine token signed requests

6.19.x
Max Shaposhnik 2018-09-18 17:23:59 +03:00 committed by GitHub
parent d7bf4f8d94
commit 01d9fc73da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 276 additions and 4 deletions

View File

@ -62,6 +62,10 @@
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
<dependency>
<groupId>javax.ws.rs</groupId>
<artifactId>javax.ws.rs-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.core</groupId>
<artifactId>che-core-api-core</artifactId>
@ -122,6 +126,10 @@
<groupId>org.eclipse.persistence</groupId>
<artifactId>javax.persistence</artifactId>
</dependency>
<dependency>
<groupId>org.everrest</groupId>
<artifactId>everrest-core</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>

View File

@ -35,6 +35,8 @@ public class MachineAuthModule extends AbstractModule {
bind(MachineTokenProvider.class).to(MachineTokenProviderImpl.class);
bind(MachineTokenAccessFilter.class);
bind(SignatureKeyManager.class);
bind(SignatureKeyDao.class).to(JpaSignatureKeyDao.class);
bind(JpaSignatureKeyDao.RemoveKeyPairsBeforeWorkspaceRemovedEventSubscriber.class)

View File

@ -15,6 +15,7 @@ import static com.google.common.base.Strings.isNullOrEmpty;
import static java.lang.String.format;
import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
import static org.eclipse.che.multiuser.machine.authentication.shared.Constants.USER_ID_CLAIM;
import static org.eclipse.che.multiuser.machine.authentication.shared.Constants.WORKSPACE_ID_CLAIM;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
@ -41,7 +42,6 @@ import org.eclipse.che.commons.auth.token.RequestTokenExtractor;
import org.eclipse.che.commons.env.EnvironmentContext;
import org.eclipse.che.commons.subject.Subject;
import org.eclipse.che.commons.subject.SubjectImpl;
import org.eclipse.che.multiuser.api.permission.server.AuthorizedSubject;
import org.eclipse.che.multiuser.api.permission.server.PermissionChecker;
/**
@ -121,9 +121,9 @@ public class MachineLoginFilter implements Filter {
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);
final String workspaceId = claims.get(WORKSPACE_ID_CLAIM, String.class);
return new MachineTokenAuthorizedSubject(
new SubjectImpl(userName, userId, token, false), permissionChecker, workspaceId);
}
/** Sets given error code with err message into give response. */

View File

@ -0,0 +1,59 @@
/*
* Copyright (c) 2012-2018 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.multiuser.machine.authentication.server;
import static java.util.Arrays.asList;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.SetMultimap;
import javax.ws.rs.Path;
import org.eclipse.che.api.core.ForbiddenException;
import org.eclipse.che.commons.env.EnvironmentContext;
import org.eclipse.che.everrest.CheMethodInvokerFilter;
import org.everrest.core.Filter;
import org.everrest.core.resource.GenericResourceMethod;
/**
* Limits set of methods which can be invoked using machine token signed requests.
*
* @author Max Shaposhnik (mshaposh@redhat.com)
*/
@Filter
@Path("/{path:.*}")
public class MachineTokenAccessFilter extends CheMethodInvokerFilter {
private final SetMultimap<String, String> allowedMethodsByPath = HashMultimap.create();
public MachineTokenAccessFilter() {
allowedMethodsByPath.putAll(
"/workspace", asList("getByKey", "addProject", "updateProject", "deleteProject"));
allowedMethodsByPath.putAll("/ssh", asList("getPair", "generatePair"));
allowedMethodsByPath.putAll(
"/factory",
asList("getFactoryJson", "getFactory", "getFactoryByAttribute", "resolveFactory"));
allowedMethodsByPath.put("/preferences", "find");
allowedMethodsByPath.put("/activity", "active");
}
@Override
protected void filter(GenericResourceMethod genericMethodResource, Object[] arguments)
throws ForbiddenException {
if (!(EnvironmentContext.getCurrent().getSubject() instanceof MachineTokenAuthorizedSubject)) {
return;
}
if (!allowedMethodsByPath
.get(genericMethodResource.getParentResource().getPathValue().getPath())
.contains(genericMethodResource.getMethod().getName())) {
throw new ForbiddenException("This operation cannot be performed using machine token.");
}
}
}

View File

@ -0,0 +1,43 @@
/*
* Copyright (c) 2012-2018 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.multiuser.machine.authentication.server;
import org.eclipse.che.commons.subject.Subject;
import org.eclipse.che.multiuser.api.permission.server.AuthorizedSubject;
import org.eclipse.che.multiuser.api.permission.server.PermissionChecker;
import org.eclipse.che.multiuser.permission.workspace.server.WorkspaceDomain;
/**
* An implementation of {@link Subject} which should be used when request was signed by machine
* token. This implementation limits all workspace related permissions to the single workspace for
* which the machine token was issued.
*
* @author Max Shaposhnik (mshaposh@redhat.com)
*/
public class MachineTokenAuthorizedSubject extends AuthorizedSubject {
private final String claimsWorkspaceId;
public MachineTokenAuthorizedSubject(
Subject baseSubject, PermissionChecker permissionChecker, String claimsWorkspaceId) {
super(baseSubject, permissionChecker);
this.claimsWorkspaceId = claimsWorkspaceId;
}
@Override
public boolean hasPermission(String domain, String instance, String action) {
if (domain.equals(WorkspaceDomain.DOMAIN_ID) && !instance.equals(claimsWorkspaceId)) {
return false;
}
return super.hasPermission(domain, instance, action);
}
}

View File

@ -24,6 +24,7 @@ import static org.mockito.Mockito.when;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.impl.DefaultClaims;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.util.HashMap;
@ -139,6 +140,32 @@ public class MachineLoginFilterTest {
+ " JWT validity cannot be asserted and should not be trusted.");
}
@Test
public void testNotProceedRequestWhenNoWorkspaceIdClaim() throws Exception {
final HttpServletRequest requestMock = getRequestMock();
final KeyPairGenerator kpg = KeyPairGenerator.getInstance(SIGNATURE_ALGORITHM);
kpg.initialize(KEY_SIZE);
final KeyPair pair = kpg.generateKeyPair();
final Claims badClaims = new DefaultClaims();
badClaims.put(Constants.USER_ID_CLAIM, SUBJECT.getUserId());
badClaims.put(Claims.ID, "84123-132-fn31");
final String token =
Jwts.builder()
.setClaims(badClaims)
.setHeader(HEADER)
.signWith(RS512, pair.getPrivate())
.compact();
when(tokenExtractorMock.getToken(any(HttpServletRequest.class))).thenReturn(token);
machineLoginFilter.doFilter(requestMock, responseMock, chainMock);
verify(tokenExtractorMock).getToken(any(HttpServletRequest.class));
verify(responseMock)
.sendError(
401,
"Authentication with machine token failed cause: Unable to fetch signature key pair: no workspace id present in token");
}
@Test
public void testProceedRequestWhenEmptyTokenProvided() throws Exception {
final HttpServletRequest requestMock = getRequestMock();

View File

@ -0,0 +1,73 @@
/*
* Copyright (c) 2012-2018 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.multiuser.machine.authentication.server;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verifyZeroInteractions;
import static org.mockito.Mockito.when;
import java.lang.reflect.Method;
import org.eclipse.che.api.core.ForbiddenException;
import org.eclipse.che.api.workspace.server.WorkspaceService;
import org.eclipse.che.commons.env.EnvironmentContext;
import org.eclipse.che.multiuser.api.permission.server.AuthorizedSubject;
import org.everrest.core.impl.resource.PathValue;
import org.everrest.core.resource.GenericResourceMethod;
import org.everrest.core.resource.ResourceDescriptor;
import org.mockito.Mock;
import org.mockito.testng.MockitoTestNGListener;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
@Listeners(MockitoTestNGListener.class)
public class MachineTokenAccessFilterTest {
@Mock EnvironmentContext environmentContext;
@Mock GenericResourceMethod genericMethodResource;
@Mock MachineTokenAuthorizedSubject machineTokenAuthorizedSubject;
@Mock AuthorizedSubject authorizedSubject;
MachineTokenAccessFilter filter;
@BeforeMethod
private void setUp() {
filter = new MachineTokenAccessFilter();
}
@Test
public void shouldNotLimitAccessIfSubjectIsNotMachineAuthorized() throws Exception {
when(environmentContext.getSubject()).thenReturn(authorizedSubject);
EnvironmentContext.setCurrent(environmentContext);
filter.filter(genericMethodResource, new Object[] {});
verifyZeroInteractions(genericMethodResource);
}
@Test(expectedExceptions = ForbiddenException.class)
public void shouldLimitAccessIfMethodIsNotAllowed() throws Exception {
when(environmentContext.getSubject()).thenReturn(machineTokenAuthorizedSubject);
EnvironmentContext.setCurrent(environmentContext);
Method method = WorkspaceService.class.getMethod("getServiceDescriptor");
ResourceDescriptor descriptor = mock(ResourceDescriptor.class);
PathValue pathValue = mock(PathValue.class);
when(genericMethodResource.getMethod()).thenReturn(method);
when(descriptor.getPathValue()).thenReturn(pathValue);
when(genericMethodResource.getParentResource()).thenReturn(descriptor);
when(pathValue.getPath()).thenReturn("/workspace");
filter.filter(genericMethodResource, new Object[] {});
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright (c) 2012-2018 Red Hat, Inc.
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
package org.eclipse.che.multiuser.machine.authentication.server;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertFalse;
import org.eclipse.che.commons.subject.Subject;
import org.eclipse.che.multiuser.api.permission.server.PermissionChecker;
import org.eclipse.che.multiuser.api.permission.server.SystemDomain;
import org.eclipse.che.multiuser.permission.workspace.server.WorkspaceDomain;
import org.mockito.Mock;
import org.mockito.testng.MockitoTestNGListener;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
@Listeners(MockitoTestNGListener.class)
public class MachineTokenAuthorizedSubjectTest {
private static final String WS_ID = "ws123";
private static final String USER_ID = "user123";
@Mock private PermissionChecker permissionChecker;
@Mock Subject baseSubject;
private MachineTokenAuthorizedSubject subject;
@BeforeMethod
private void setUp() {
when(baseSubject.getUserId()).thenReturn(USER_ID);
subject = new MachineTokenAuthorizedSubject(baseSubject, permissionChecker, WS_ID);
}
@Test
public void shouldRejectPermissionsFromAnotherWorkspace() {
assertFalse(
subject.hasPermission(WorkspaceDomain.DOMAIN_ID, "another_ws", WorkspaceDomain.READ));
}
@Test
public void shouldRequestPermissionsFromBaseSubjectForNonWorkspaceDomains() throws Exception {
subject.hasPermission(SystemDomain.DOMAIN_ID, "", SystemDomain.MANAGE_SYSTEM_ACTION);
verify(permissionChecker, atLeastOnce())
.hasPermission(
eq(USER_ID), eq(SystemDomain.DOMAIN_ID), eq(""), eq(SystemDomain.MANAGE_SYSTEM_ACTION));
}
}