diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxyProvisioner.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxyProvisioner.java index 54ac1a3dc6..353aff8a48 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxyProvisioner.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxyProvisioner.java @@ -43,6 +43,7 @@ import java.util.Map; import java.util.Set; import javax.inject.Inject; import javax.inject.Named; +import org.eclipse.che.api.core.ServerException; import org.eclipse.che.api.core.model.workspace.config.MachineConfig; import org.eclipse.che.api.core.model.workspace.config.ServerConfig; import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity; @@ -208,10 +209,13 @@ public class JwtProxyProvisioner { k8sEnv.getMachines().put(JWT_PROXY_MACHINE_NAME, createJwtProxyMachine()); k8sEnv.getPods().put(JWT_PROXY_POD_NAME, createJwtProxyPod(identity)); - KeyPair keyPair = signatureKeyManager.getKeyPair(); - if (keyPair == null) { + KeyPair keyPair; + try { + keyPair = signatureKeyManager.getKeyPair(identity.getWorkspaceId()); + } catch (ServerException e) { throw new InternalInfrastructureException( - "Key pair for machine authentication does not exist"); + "Signature key pair for machine authentication cannot be retrieved. Reason: " + + e.getMessage()); } Map initConfigMapData = new HashMap<>(); initConfigMapData.put( diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxyProvisionerTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxyProvisionerTest.java index 10e31e3dd0..c851f95564 100644 --- a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxyProvisionerTest.java +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxyProvisionerTest.java @@ -19,6 +19,7 @@ import static org.eclipse.che.workspace.infrastructure.kubernetes.server.secure. import static org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy.JwtProxyProvisioner.PUBLIC_KEY_FOOTER; import static org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy.JwtProxyProvisioner.PUBLIC_KEY_HEADER; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -73,8 +74,8 @@ public class JwtProxyProvisionerTest { private KubernetesEnvironment k8sEnv; @BeforeMethod - public void setUp() { - when(signatureKeyManager.getKeyPair()).thenReturn(new KeyPair(publicKey, null)); + public void setUp() throws Exception { + when(signatureKeyManager.getKeyPair(anyString())).thenReturn(new KeyPair(publicKey, null)); when(publicKey.getEncoded()).thenReturn("publickey".getBytes()); when(configBuilderFactory.create(any())) diff --git a/multiuser/integration-tests/che-multiuser-cascade-removal/pom.xml b/multiuser/integration-tests/che-multiuser-cascade-removal/pom.xml index 7c4a6c3138..d44c945062 100644 --- a/multiuser/integration-tests/che-multiuser-cascade-removal/pom.xml +++ b/multiuser/integration-tests/che-multiuser-cascade-removal/pom.xml @@ -138,6 +138,16 @@ che-core-sql-schema test + + org.eclipse.che.multiuser + che-multiuser-api-authorization + test + + + org.eclipse.che.multiuser + che-multiuser-api-authorization-impl + test + org.eclipse.che.multiuser che-multiuser-api-organization @@ -158,6 +168,11 @@ che-multiuser-api-resource test + + org.eclipse.che.multiuser + che-multiuser-machine-authentication + test + org.eclipse.che.multiuser che-multiuser-permission-workspace diff --git a/multiuser/integration-tests/che-multiuser-cascade-removal/src/test/java/org/eclipse/che/multiuser/integration/jpa/cascaderemoval/JpaEntitiesCascadeRemovalTest.java b/multiuser/integration-tests/che-multiuser-cascade-removal/src/test/java/org/eclipse/che/multiuser/integration/jpa/cascaderemoval/JpaEntitiesCascadeRemovalTest.java index 4eff211f54..e0f25bfdab 100644 --- a/multiuser/integration-tests/che-multiuser-cascade-removal/src/test/java/org/eclipse/che/multiuser/integration/jpa/cascaderemoval/JpaEntitiesCascadeRemovalTest.java +++ b/multiuser/integration-tests/che-multiuser-cascade-removal/src/test/java/org/eclipse/che/multiuser/integration/jpa/cascaderemoval/JpaEntitiesCascadeRemovalTest.java @@ -20,6 +20,7 @@ import static org.eclipse.che.multiuser.integration.jpa.cascaderemoval.TestObjec import static org.eclipse.che.multiuser.integration.jpa.cascaderemoval.TestObjectsFactory.createFreeResourcesLimit; import static org.eclipse.che.multiuser.integration.jpa.cascaderemoval.TestObjectsFactory.createPreferences; import static org.eclipse.che.multiuser.integration.jpa.cascaderemoval.TestObjectsFactory.createProfile; +import static org.eclipse.che.multiuser.integration.jpa.cascaderemoval.TestObjectsFactory.createSignatureKeyPair; import static org.eclipse.che.multiuser.integration.jpa.cascaderemoval.TestObjectsFactory.createSshPair; import static org.eclipse.che.multiuser.integration.jpa.cascaderemoval.TestObjectsFactory.createStack; import static org.eclipse.che.multiuser.integration.jpa.cascaderemoval.TestObjectsFactory.createUser; @@ -44,6 +45,7 @@ import com.google.inject.multibindings.MapBinder; import com.google.inject.multibindings.Multibinder; import com.google.inject.name.Names; import com.google.inject.persist.jpa.JpaPersistModule; +import java.security.NoSuchAlgorithmException; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; @@ -95,8 +97,13 @@ import org.eclipse.che.core.db.cascade.event.CascadeEvent; import org.eclipse.che.core.db.schema.SchemaInitializer; import org.eclipse.che.core.db.schema.impl.flyway.FlywaySchemaInitializer; import org.eclipse.che.inject.lifecycle.InitModule; +import org.eclipse.che.multiuser.api.permission.server.PermissionChecker; +import org.eclipse.che.multiuser.api.permission.server.PermissionCheckerImpl; +import org.eclipse.che.multiuser.api.permission.server.PermissionsManager; import org.eclipse.che.multiuser.api.permission.server.model.impl.AbstractPermissions; import org.eclipse.che.multiuser.api.permission.server.spi.PermissionsDao; +import org.eclipse.che.multiuser.machine.authentication.server.MachineAuthModule; +import org.eclipse.che.multiuser.machine.authentication.server.signature.spi.SignatureKeyDao; import org.eclipse.che.multiuser.organization.api.OrganizationJpaModule; import org.eclipse.che.multiuser.organization.api.OrganizationManager; import org.eclipse.che.multiuser.organization.api.listener.RemoveOrganizationOnLastUserRemovedEventSubscriber; @@ -147,6 +154,7 @@ public class JpaEntitiesCascadeRemovalTest { private FactoryDao factoryDao; private StackDao stackDao; private WorkerDao workerDao; + private SignatureKeyDao signatureKeyDao; private JpaStackPermissionsDao stackPermissionsDao; private FreeResourcesLimitDao freeResourcesLimitDao; private OrganizationManager organizationManager; @@ -229,6 +237,7 @@ public class JpaEntitiesCascadeRemovalTest { install(new FactoryJpaModule()); install(new OrganizationJpaModule()); install(new MultiuserWorkspaceJpaModule()); + install(new MachineAuthModule()); bind(FreeResourcesLimitDao.class).to(JpaFreeResourcesLimitDao.class); bind(RemoveFreeResourcesLimitSubscriber.class).asEagerSingleton(); @@ -237,6 +246,8 @@ public class JpaEntitiesCascadeRemovalTest { bind(WorkspaceStatusCache.class).to(DefaultWorkspaceStatusCache.class); bind(RuntimeInfrastructure.class).toInstance(mock(RuntimeInfrastructure.class)); MapBinder.newMapBinder(binder(), String.class, InternalEnvironmentFactory.class); + bind(PermissionsManager.class); + bind(PermissionChecker.class).to(PermissionCheckerImpl.class); bind(AccountManager.class); bind(Boolean.class) .annotatedWith(Names.named("che.workspace.auto_snapshot")) @@ -293,6 +304,7 @@ public class JpaEntitiesCascadeRemovalTest { factoryDao = injector.getInstance(FactoryDao.class); stackDao = injector.getInstance(StackDao.class); workerDao = injector.getInstance(WorkerDao.class); + signatureKeyDao = injector.getInstance(SignatureKeyDao.class); freeResourcesLimitDao = injector.getInstance(FreeResourcesLimitDao.class); organizationManager = injector.getInstance(OrganizationManager.class); memberDao = injector.getInstance(MemberDao.class); @@ -361,6 +373,10 @@ public class JpaEntitiesCascadeRemovalTest { assertNull(notFoundToNull(() -> freeResourcesLimitDao.get(user.getId()))); assertNull(notFoundToNull(() -> freeResourcesLimitDao.get(user2.getId()))); + // machine token keypairs + assertNull(notFoundToNull(() -> signatureKeyDao.get(workspace1.getId()))); + assertNull(notFoundToNull(() -> signatureKeyDao.get(workspace2.getId()))); + // distributed resources is removed assertNull( notFoundToNull(() -> organizationResourcesDistributor.get(childOrganization.getId()))); @@ -395,6 +411,7 @@ public class JpaEntitiesCascadeRemovalTest { assertNotNull(notFoundToNull(() -> organizationManager.getById(organization.getId()))); assertNotNull(notFoundToNull(() -> organizationManager.getById(childOrganization.getId()))); assertNotNull(notFoundToNull(() -> organizationManager.getById(organization2.getId()))); + assertNotNull(notFoundToNull(() -> signatureKeyDao.get(workspace2.getId()))); assertFalse( organizationResourcesDistributor.getResourcesCaps(childOrganization.getId()).isEmpty()); wipeTestData(); @@ -408,7 +425,8 @@ public class JpaEntitiesCascadeRemovalTest { }; } - private void createTestData() throws NotFoundException, ConflictException, ServerException { + private void createTestData() + throws NotFoundException, ConflictException, ServerException, NoSuchAlgorithmException { userDao.create(user = createUser("bobby")); accountDao.create(account = createAccount("bobby")); // test permissions users @@ -435,6 +453,9 @@ public class JpaEntitiesCascadeRemovalTest { workerDao.store(createWorker(user2.getId(), workspace3.getId())); + signatureKeyDao.create(createSignatureKeyPair(workspace1.getId())); + signatureKeyDao.create(createSignatureKeyPair(workspace2.getId())); + stackPermissionsDao.store( new StackPermissionsImpl( user2.getId(), stack1.getId(), asList(SET_PERMISSIONS, "read", "write"))); @@ -511,6 +532,9 @@ public class JpaEntitiesCascadeRemovalTest { sshDao.remove(sshPair1.getOwner(), sshPair1.getService(), sshPair1.getName()); sshDao.remove(sshPair2.getOwner(), sshPair2.getService(), sshPair2.getName()); + signatureKeyDao.remove(workspace1.getId()); + signatureKeyDao.remove(workspace2.getId()); + workspaceDao.remove(workspace1.getId()); workspaceDao.remove(workspace2.getId()); workspaceDao.remove(workspace3.getId()); diff --git a/multiuser/integration-tests/che-multiuser-cascade-removal/src/test/java/org/eclipse/che/multiuser/integration/jpa/cascaderemoval/TestObjectsFactory.java b/multiuser/integration-tests/che-multiuser-cascade-removal/src/test/java/org/eclipse/che/multiuser/integration/jpa/cascaderemoval/TestObjectsFactory.java index 611dbc8c2c..cbbff1ff06 100644 --- a/multiuser/integration-tests/che-multiuser-cascade-removal/src/test/java/org/eclipse/che/multiuser/integration/jpa/cascaderemoval/TestObjectsFactory.java +++ b/multiuser/integration-tests/che-multiuser-cascade-removal/src/test/java/org/eclipse/che/multiuser/integration/jpa/cascaderemoval/TestObjectsFactory.java @@ -14,6 +14,9 @@ package org.eclipse.che.multiuser.integration.jpa.cascaderemoval; import static java.util.Arrays.asList; import com.google.common.collect.ImmutableMap; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -29,6 +32,7 @@ import org.eclipse.che.api.workspace.server.model.impl.WorkspaceImpl; import org.eclipse.che.api.workspace.server.model.impl.stack.StackComponentImpl; import org.eclipse.che.api.workspace.server.model.impl.stack.StackImpl; import org.eclipse.che.api.workspace.server.stack.image.StackIcon; +import org.eclipse.che.multiuser.machine.authentication.server.signature.model.impl.SignatureKeyPairImpl; import org.eclipse.che.multiuser.permission.workspace.server.model.impl.WorkerImpl; import org.eclipse.che.multiuser.resource.spi.impl.FreeResourcesLimitImpl; import org.eclipse.che.multiuser.resource.spi.impl.ResourceImpl; @@ -120,5 +124,14 @@ public final class TestObjectsFactory { Arrays.asList(new ResourceImpl("test1", 123, "mb"), new ResourceImpl("test2", 234, "h"))); } + public static SignatureKeyPairImpl createSignatureKeyPair(String workspaceId) + throws NoSuchAlgorithmException { + final KeyPairGenerator kpg; + kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(512); + final KeyPair pair = kpg.generateKeyPair(); + return new SignatureKeyPairImpl(workspaceId, pair.getPublic(), pair.getPrivate()); + } + private TestObjectsFactory() {} } diff --git a/multiuser/integration-tests/che-multiuser-cascade-removal/src/test/resources/META-INF/persistence.xml b/multiuser/integration-tests/che-multiuser-cascade-removal/src/test/resources/META-INF/persistence.xml index 05ec68f6d4..d12e938378 100644 --- a/multiuser/integration-tests/che-multiuser-cascade-removal/src/test/resources/META-INF/persistence.xml +++ b/multiuser/integration-tests/che-multiuser-cascade-removal/src/test/resources/META-INF/persistence.xml @@ -59,6 +59,9 @@ org.eclipse.che.multiuser.organization.spi.impl.MemberImpl org.eclipse.che.multiuser.organization.spi.impl.OrganizationDistributedResourcesImpl + org.eclipse.che.multiuser.machine.authentication.server.signature.model.impl.SignatureKeyImpl + org.eclipse.che.multiuser.machine.authentication.server.signature.model.impl.SignatureKeyPairImpl + org.eclipse.che.multiuser.resource.spi.impl.ResourceImpl org.eclipse.che.multiuser.resource.spi.impl.FreeResourcesLimitImpl true diff --git a/multiuser/integration-tests/che-multiuser-postgresql-tck/pom.xml b/multiuser/integration-tests/che-multiuser-postgresql-tck/pom.xml index a36ed80bad..d3bbd9a5e4 100644 --- a/multiuser/integration-tests/che-multiuser-postgresql-tck/pom.xml +++ b/multiuser/integration-tests/che-multiuser-postgresql-tck/pom.xml @@ -102,6 +102,11 @@ che-multiuser-api-permission test + + org.eclipse.che.multiuser + che-multiuser-machine-authentication + test + org.eclipse.che.multiuser che-multiuser-permission-workspace diff --git a/multiuser/integration-tests/che-multiuser-postgresql-tck/src/test/java/MultiuserPostgresqlTckModule.java b/multiuser/integration-tests/che-multiuser-postgresql-tck/src/test/java/MultiuserPostgresqlTckModule.java index 7ef1dd28d7..e10980524a 100644 --- a/multiuser/integration-tests/che-multiuser-postgresql-tck/src/test/java/MultiuserPostgresqlTckModule.java +++ b/multiuser/integration-tests/che-multiuser-postgresql-tck/src/test/java/MultiuserPostgresqlTckModule.java @@ -56,6 +56,9 @@ import org.eclipse.che.multiuser.api.permission.server.jpa.JpaSystemPermissionsD import org.eclipse.che.multiuser.api.permission.server.model.impl.SystemPermissionsImpl; import org.eclipse.che.multiuser.api.permission.server.spi.PermissionsDao; import org.eclipse.che.multiuser.api.permission.server.spi.tck.SystemPermissionsDaoTest; +import org.eclipse.che.multiuser.machine.authentication.server.signature.jpa.JpaSignatureKeyDao; +import org.eclipse.che.multiuser.machine.authentication.server.signature.model.impl.SignatureKeyPairImpl; +import org.eclipse.che.multiuser.machine.authentication.server.signature.spi.SignatureKeyDao; import org.eclipse.che.multiuser.organization.api.permissions.OrganizationDomain; import org.eclipse.che.multiuser.organization.spi.MemberDao; import org.eclipse.che.multiuser.organization.spi.OrganizationDao; @@ -163,6 +166,10 @@ public class MultiuserPostgresqlTckModule extends TckModule { bind(new TypeLiteral>() {}) .toInstance(new JpaTckRepository<>(FreeResourcesLimitImpl.class)); + // machine token keys + bind(new TypeLiteral>() {}) + .toInstance(new JpaTckRepository<>(SignatureKeyPairImpl.class)); + // dao bind(OrganizationDao.class).to(JpaOrganizationDao.class); bind(OrganizationDistributedResourcesDao.class) @@ -171,6 +178,7 @@ public class MultiuserPostgresqlTckModule extends TckModule { bind(WorkerDao.class).to(JpaWorkerDao.class); bind(MemberDao.class).to(JpaMemberDao.class); + bind(SignatureKeyDao.class).to(JpaSignatureKeyDao.class); bind(new TypeLiteral>() {}).to(JpaMemberDao.class); bind(new TypeLiteral>() {}).to(OrganizationDomain.class); diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/pom.xml b/multiuser/keycloak/che-multiuser-keycloak-server/pom.xml index 271691314d..da0e3a070a 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/pom.xml +++ b/multiuser/keycloak/che-multiuser-keycloak-server/pom.xml @@ -110,6 +110,10 @@ org.eclipse.che.core che-core-commons-auth + + org.eclipse.che.core + che-core-commons-inject + org.eclipse.che.core che-core-commons-lang diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/AbstractKeycloakFilter.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/AbstractKeycloakFilter.java index c381274b06..248f290167 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/AbstractKeycloakFilter.java +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/AbstractKeycloakFilter.java @@ -11,52 +11,38 @@ */ package org.eclipse.che.multiuser.keycloak.server; -import static org.eclipse.che.multiuser.machine.authentication.shared.Constants.MACHINE_TOKEN_KIND; - -import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.Jwt; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.MalformedJwtException; -import io.jsonwebtoken.SignatureException; -import java.security.PublicKey; +import io.jsonwebtoken.JwtParser; import javax.inject.Inject; import javax.servlet.Filter; import javax.servlet.FilterConfig; -import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; -import org.eclipse.che.multiuser.machine.authentication.server.signature.SignatureKeyManager; /** * Base abstract class for the Keycloak-related servlet filters. * - *

In particular it defines commnon use-cases when the authentication / multi-user logic should - * be skipped + *

In particular it defines common use-cases when the authentication / multi-user logic should be + * skipped */ public abstract class AbstractKeycloakFilter implements Filter { - @Inject protected SignatureKeyManager signatureKeyManager; + @Inject protected JwtParser jwtParser; /** when a request came from a machine with valid token then auth is not required */ - protected boolean shouldSkipAuthentication(HttpServletRequest request, String token) { + boolean shouldSkipAuthentication(HttpServletRequest request, String token) { if (token == null) { - if (request.getRequestURI() != null - && request.getRequestURI().endsWith("api/keycloak/OIDCKeycloak.js")) { - return true; - } - return false; + return request.getRequestURI() != null + && request.getRequestURI().endsWith("api/keycloak/OIDCKeycloak.js"); } try { - final PublicKey publicKey = signatureKeyManager.getKeyPair().getPublic(); - final Jwt jwt = Jwts.parser().setSigningKey(publicKey).parse(token); - return MACHINE_TOKEN_KIND.equals(jwt.getHeader().get("kind")); - } catch (ExpiredJwtException | MalformedJwtException | SignatureException ex) { - // given token is not signed by particular signature key so it must be checked in another way + jwtParser.parse(token); return false; + } catch (MachineTokenJwtException e) { + return true; } } @Override - public void init(FilterConfig filterConfig) throws ServletException {} + public void init(FilterConfig filterConfig) {} @Override public void destroy() {} diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakAuthenticationFilter.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakAuthenticationFilter.java index 1b2048d4fd..8ab549b345 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakAuthenticationFilter.java +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakAuthenticationFilter.java @@ -11,28 +11,12 @@ */ package org.eclipse.che.multiuser.keycloak.server; -import com.auth0.jwk.GuavaCachedJwkProvider; -import com.auth0.jwk.Jwk; -import com.auth0.jwk.JwkException; -import com.auth0.jwk.JwkProvider; -import com.auth0.jwk.UrlJwkProvider; import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Jws; -import io.jsonwebtoken.JwsHeader; import io.jsonwebtoken.JwtException; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.MalformedJwtException; -import io.jsonwebtoken.SignatureException; -import io.jsonwebtoken.SigningKeyResolverAdapter; -import io.jsonwebtoken.UnsupportedJwtException; import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URL; -import java.security.Key; -import java.security.PublicKey; import javax.inject.Inject; -import javax.inject.Named; import javax.inject.Singleton; import javax.servlet.FilterChain; import javax.servlet.ServletException; @@ -41,31 +25,19 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.che.commons.auth.token.RequestTokenExtractor; -import org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Singleton public class KeycloakAuthenticationFilter extends AbstractKeycloakFilter { + private static final Logger LOG = LoggerFactory.getLogger(KeycloakAuthenticationFilter.class); - private String jwksUrl; - private long allowedClockSkewSec; private RequestTokenExtractor tokenExtractor; - private JwkProvider jwkProvider; @Inject - public KeycloakAuthenticationFilter( - KeycloakSettings keycloakSettings, - @Named(KeycloakConstants.ALLOWED_CLOCK_SKEW_SEC) long allowedClockSkewSec, - RequestTokenExtractor tokenExtractor) - throws MalformedURLException { - this.jwksUrl = keycloakSettings.get().get(KeycloakConstants.JWKS_ENDPOINT_SETTING); - this.allowedClockSkewSec = allowedClockSkewSec; + public KeycloakAuthenticationFilter(RequestTokenExtractor tokenExtractor) { this.tokenExtractor = tokenExtractor; - if (jwksUrl != null) { - this.jwkProvider = new GuavaCachedJwkProvider(new UrlJwkProvider(new URL(jwksUrl))); - } } @Override @@ -74,80 +46,34 @@ public class KeycloakAuthenticationFilter extends AbstractKeycloakFilter { HttpServletRequest request = (HttpServletRequest) req; final String token = tokenExtractor.getToken(request); - if (shouldSkipAuthentication(request, token)) { - chain.doFilter(req, res); - return; - } - if (token == null) { - send403(res, "Authorization token is missed"); + send401(res, "Authorization token is missed"); return; } Jws jwt; try { - jwt = - Jwts.parser() - .setAllowedClockSkewSeconds(allowedClockSkewSec) - .setSigningKeyResolver( - new SigningKeyResolverAdapter() { - @Override - public Key resolveSigningKey( - @SuppressWarnings("rawtypes") JwsHeader header, Claims claims) { - try { - return getJwtPublicKey(header); - } catch (JwkException e) { - throw new JwtException( - "Error during the retrieval of the public key during JWT token validation", - e); - } - } - }) - .parseClaimsJws(token); + if (shouldSkipAuthentication(request, token)) { + chain.doFilter(req, res); + return; + } + jwt = jwtParser.parseClaimsJws(token); LOG.debug("JWT = ", jwt); // OK, we can trust this JWT - } catch (SignatureException - | IllegalArgumentException - | MalformedJwtException - | UnsupportedJwtException e) { - send403(res, "The specified token is not a valid. " + e.getMessage()); - return; } catch (ExpiredJwtException e) { - send403(res, "The specified token is expired"); + send401(res, "The specified token is expired"); + return; + } catch (JwtException e) { + send401(res, "Token validation failed: " + e.getMessage()); return; } - request.setAttribute("token", jwt); chain.doFilter(req, res); } - private synchronized PublicKey getJwtPublicKey(JwsHeader header) throws JwkException { - String kid = header.getKeyId(); - if (kid == null) { - LOG.warn( - "'kid' is missing in the JWT token header. This is not possible to validate the token with OIDC provider keys"); - return null; - } - String alg = header.getAlgorithm(); - if (alg == null) { - LOG.warn( - "'alg' is missing in the JWT token header. This is not possible to validate the token with OIDC provider keys"); - return null; - } - - if (jwkProvider == null) { - LOG.warn( - "JWK provider is not available: This is not possible to validate the token with OIDC provider keys.\n" - + "Please look into the startup logs to find out the root cause"); - return null; - } - Jwk jwk = jwkProvider.get(kid); - return jwk.getPublicKey(); - } - - private void send403(ServletResponse res, String message) throws IOException { + private void send401(ServletResponse res, String message) throws IOException { HttpServletResponse response = (HttpServletResponse) res; response.getOutputStream().write(message.getBytes()); - response.setStatus(403); + response.setStatus(401); } } diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakEnvironmentInitalizationFilter.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakEnvironmentInitalizationFilter.java index 6f41f01f01..74036af649 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakEnvironmentInitalizationFilter.java +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakEnvironmentInitalizationFilter.java @@ -44,9 +44,9 @@ import org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants; public class KeycloakEnvironmentInitalizationFilter extends AbstractKeycloakFilter { private final KeycloakUserManager userManager; - private final KeycloakSettings settings; private final RequestTokenExtractor tokenExtractor; private final PermissionChecker permissionChecker; + private final KeycloakSettings keycloakSettings; @Inject public KeycloakEnvironmentInitalizationFilter( @@ -57,7 +57,7 @@ public class KeycloakEnvironmentInitalizationFilter extends AbstractKeycloakFilt this.userManager = userManager; this.tokenExtractor = tokenExtractor; this.permissionChecker = permissionChecker; - this.settings = settings; + this.keycloakSettings = settings; } @Override @@ -82,7 +82,8 @@ public class KeycloakEnvironmentInitalizationFilter extends AbstractKeycloakFilt try { String username = - claims.get(settings.get().get(KeycloakConstants.USERNAME_CLAIM_SETTING), String.class); + claims.get( + keycloakSettings.get().get(KeycloakConstants.USERNAME_CLAIM_SETTING), String.class); if (username == null) { // fallback to unique id promised by spec // https://openid.net/specs/openid-connect-basic-1_0.html#ClaimStability username = claims.getIssuer() + ":" + claims.getSubject(); diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakJwkProvider.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakJwkProvider.java new file mode 100644 index 0000000000..71f3e2434a --- /dev/null +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakJwkProvider.java @@ -0,0 +1,42 @@ +/* + * 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.keycloak.server; + +import com.auth0.jwk.GuavaCachedJwkProvider; +import com.auth0.jwk.JwkProvider; +import com.auth0.jwk.UrlJwkProvider; +import java.net.MalformedURLException; +import java.net.URL; +import javax.inject.Inject; +import javax.inject.Provider; +import org.eclipse.che.inject.ConfigurationException; +import org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants; + +/** Constructs {@link UrlJwkProvider} based on Jwk endpoint from keycloak settings */ +public class KeycloakJwkProvider implements Provider { + + private final JwkProvider jwkProvider; + + @Inject + public KeycloakJwkProvider(KeycloakSettings keycloakSettings) throws MalformedURLException { + final String jwksUrl = keycloakSettings.get().get(KeycloakConstants.JWKS_ENDPOINT_SETTING); + if (jwksUrl == null) { + throw new ConfigurationException("Jwks endpoint url not found in keycloak settings"); + } + this.jwkProvider = new GuavaCachedJwkProvider(new UrlJwkProvider(new URL(jwksUrl))); + } + + @Override + public JwkProvider get() { + return jwkProvider; + } +} diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakJwtParserProvider.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakJwtParserProvider.java new file mode 100644 index 0000000000..5ba4737638 --- /dev/null +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakJwtParserProvider.java @@ -0,0 +1,42 @@ +/* + * 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.keycloak.server; + +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.Jwts; +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Provider; +import javax.inject.Singleton; +import org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants; + +/** Provides instance of {@link JwtParser} */ +@Singleton +public class KeycloakJwtParserProvider implements Provider { + + private final JwtParser jwtParser; + + @Inject + public KeycloakJwtParserProvider( + @Named(KeycloakConstants.ALLOWED_CLOCK_SKEW_SEC) long allowedClockSkewSec, + KeycloakSigningKeyResolver keycloakSigningKeyResolver) { + this.jwtParser = + Jwts.parser() + .setAllowedClockSkewSeconds(allowedClockSkewSec) + .setSigningKeyResolver(keycloakSigningKeyResolver); + } + + @Override + public JwtParser get() { + return jwtParser; + } +} diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakSigningKeyResolver.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakSigningKeyResolver.java new file mode 100644 index 0000000000..af02cd466e --- /dev/null +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/KeycloakSigningKeyResolver.java @@ -0,0 +1,72 @@ +/* + * 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.keycloak.server; + +import static org.eclipse.che.multiuser.machine.authentication.shared.Constants.MACHINE_TOKEN_KIND; + +import com.auth0.jwk.JwkException; +import com.auth0.jwk.JwkProvider; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwsHeader; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.SigningKeyResolverAdapter; +import java.security.Key; +import java.security.PublicKey; +import javax.inject.Inject; +import javax.inject.Singleton; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** Resolves signing key based on id from JWT header */ +@Singleton +public class KeycloakSigningKeyResolver extends SigningKeyResolverAdapter { + + private final JwkProvider jwkProvider; + + private static final Logger LOG = LoggerFactory.getLogger(KeycloakSigningKeyResolver.class); + + @Inject + KeycloakSigningKeyResolver(JwkProvider jwkProvider) { + this.jwkProvider = jwkProvider; + } + + @Override + public Key resolveSigningKey(JwsHeader header, String plaintext) { + if (MACHINE_TOKEN_KIND.equals(header.get("kind"))) { + throw new MachineTokenJwtException(); // machine token, doesn't need to verify + } + return getJwtPublicKey(header); + } + + @Override + public Key resolveSigningKey(JwsHeader header, Claims claims) { + if (MACHINE_TOKEN_KIND.equals(header.get("kind"))) { + throw new MachineTokenJwtException(); // machine token, doesn't need to verify + } + return getJwtPublicKey(header); + } + + private synchronized PublicKey getJwtPublicKey(JwsHeader header) { + String kid = header.getKeyId(); + if (header.getKeyId() == null) { + LOG.warn( + "'kid' is missing in the JWT token header. This is not possible to validate the token with OIDC provider keys"); + throw new JwtException("'kid' is missing in the JWT token header."); + } + try { + return jwkProvider.get(kid).getPublicKey(); + } catch (JwkException e) { + throw new JwtException( + "Error during the retrieval of the public key during JWT token validation", e); + } + } +} diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/MachineTokenJwtException.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/MachineTokenJwtException.java new file mode 100644 index 0000000000..9e0c63a1d4 --- /dev/null +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/MachineTokenJwtException.java @@ -0,0 +1,21 @@ +/* + * 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.keycloak.server; + +import io.jsonwebtoken.JwtException; + +public class MachineTokenJwtException extends JwtException { + + public MachineTokenJwtException() { + super("This is a machine token"); + } +} diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/deploy/KeycloakModule.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/deploy/KeycloakModule.java index ce6ea5673e..642d076523 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/deploy/KeycloakModule.java +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/main/java/org/eclipse/che/multiuser/keycloak/server/deploy/KeycloakModule.java @@ -11,12 +11,16 @@ */ package org.eclipse.che.multiuser.keycloak.server.deploy; +import com.auth0.jwk.JwkProvider; import com.google.inject.AbstractModule; +import io.jsonwebtoken.JwtParser; import org.eclipse.che.api.core.rest.HttpJsonRequestFactory; import org.eclipse.che.api.user.server.TokenValidator; import org.eclipse.che.api.user.server.spi.ProfileDao; import org.eclipse.che.multiuser.api.account.personal.PersonalAccountUserManager; import org.eclipse.che.multiuser.keycloak.server.KeycloakConfigurationService; +import org.eclipse.che.multiuser.keycloak.server.KeycloakJwkProvider; +import org.eclipse.che.multiuser.keycloak.server.KeycloakJwtParserProvider; import org.eclipse.che.multiuser.keycloak.server.KeycloakTokenValidator; import org.eclipse.che.multiuser.keycloak.server.KeycloakUserManager; import org.eclipse.che.multiuser.keycloak.server.dao.KeycloakProfileDao; @@ -32,6 +36,8 @@ public class KeycloakModule extends AbstractModule { bind(KeycloakConfigurationService.class); bind(ProfileDao.class).to(KeycloakProfileDao.class); + bind(JwkProvider.class).toProvider(KeycloakJwkProvider.class); + bind(JwtParser.class).toProvider(KeycloakJwtParserProvider.class); bind(PersonalAccountUserManager.class).to(KeycloakUserManager.class); bind(OAuthAPI.class).toProvider(OAuthAPIProvider.class); diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/AbstractKeycloakFilterTest.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/AbstractKeycloakFilterTest.java index 0c45f5c228..01dbfc5de3 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/AbstractKeycloakFilterTest.java +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/AbstractKeycloakFilterTest.java @@ -11,26 +11,21 @@ */ package org.eclipse.che.multiuser.keycloak.server; -import static io.jsonwebtoken.SignatureAlgorithm.RS256; -import static org.eclipse.che.multiuser.machine.authentication.shared.Constants.MACHINE_TOKEN_KIND; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.when; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertTrue; -import io.jsonwebtoken.Jwts; -import java.io.IOException; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.util.HashMap; -import java.util.Map; +import io.jsonwebtoken.Jwt; +import io.jsonwebtoken.JwtParser; import javax.servlet.FilterChain; -import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; -import org.eclipse.che.multiuser.machine.authentication.server.signature.SignatureKeyManager; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.testng.MockitoTestNGListener; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Listeners; @@ -45,27 +40,12 @@ import org.testng.annotations.Test; public class AbstractKeycloakFilterTest { @Mock private HttpServletRequest request; - @Mock private SignatureKeyManager signatureKeyManager; + @Mock private JwtParser jwtParser; @InjectMocks private TestLoginFilter abstractKeycloakFilter; - private String machineToken; - @BeforeMethod - public void setup() throws Exception { - final KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); - kpg.initialize(512); - final KeyPair keyPair = kpg.generateKeyPair(); - final Map header = new HashMap<>(); - header.put("kind", MACHINE_TOKEN_KIND); - machineToken = - Jwts.builder() - .setPayload("payload") - .setHeader(header) - .signWith(RS256, keyPair.getPrivate()) - .compact(); - - when(signatureKeyManager.getKeyPair()).thenReturn(keyPair); + public void setup() { when(request.getRequestURI()).thenReturn(null); } @@ -82,18 +62,23 @@ public class AbstractKeycloakFilterTest { @Test public void testShouldNotSkipAuthWhenProvidedTokenIsNotMachine() { - assertFalse(abstractKeycloakFilter.shouldSkipAuthentication(request, "testToken")); + Jwt mock = Mockito.mock(Jwt.class); + doReturn(mock).when(jwtParser).parse(anyString()); + assertFalse(abstractKeycloakFilter.shouldSkipAuthentication(request, "token")); } @Test - public void testAuthIsNotNeededWhenMachineTokenProvided() throws Exception { - assertTrue(abstractKeycloakFilter.shouldSkipAuthentication(request, machineToken)); + public void testAuthIsNotNeededWhenMachineTokenProvided() { + when(jwtParser.parse(anyString())).thenThrow(MachineTokenJwtException.class); + assertTrue(abstractKeycloakFilter.shouldSkipAuthentication(request, "token")); } static class TestLoginFilter extends AbstractKeycloakFilter { + + public TestLoginFilter() {} + @Override public void doFilter( - ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) - throws IOException, ServletException {} + ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) {} } } diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/KeycloakAuthenticationFilterTest.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/KeycloakAuthenticationFilterTest.java new file mode 100644 index 0000000000..995988c867 --- /dev/null +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/KeycloakAuthenticationFilterTest.java @@ -0,0 +1,94 @@ +/* + * 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.keycloak.server; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.JwtParser; +import java.lang.reflect.Field; +import javax.servlet.FilterChain; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.eclipse.che.commons.auth.token.RequestTokenExtractor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +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 KeycloakAuthenticationFilterTest { + + @Mock private RequestTokenExtractor tokenExtractor; + @Mock private JwtParser jwtParser; + @Mock private ServletOutputStream servletOutputStream; + @Mock private HttpServletRequest request; + @Mock private HttpServletResponse response; + @Mock private FilterChain chain; + + private KeycloakAuthenticationFilter authenticationFilter; + + @BeforeMethod + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + authenticationFilter = new KeycloakAuthenticationFilter(tokenExtractor); + Field parser = authenticationFilter.getClass().getSuperclass().getDeclaredField("jwtParser"); + parser.setAccessible(true); + parser.set(authenticationFilter, jwtParser); + when(response.getOutputStream()).thenReturn(servletOutputStream); + } + + @Test + public void shouldSend401IfNoTokenInRequest() throws Exception { + when(tokenExtractor.getToken(any(HttpServletRequest.class))).thenReturn(null); + + authenticationFilter.doFilter(request, response, chain); + + verify(response).setStatus(401); + verify(servletOutputStream).write(eq("Authorization token is missed".getBytes())); + verifyNoMoreInteractions(chain); + } + + @Test + public void shouldSend401IfTokenIsExpired() throws Exception { + when(tokenExtractor.getToken(any(HttpServletRequest.class))).thenReturn("token"); + when(jwtParser.parse(anyString())).thenThrow(ExpiredJwtException.class); + + authenticationFilter.doFilter(request, response, chain); + + verify(response).setStatus(401); + verify(servletOutputStream).write(eq("The specified token is expired".getBytes())); + verifyNoMoreInteractions(chain); + } + + @Test + public void shouldSend401IfTokenIsCheckSignatureFailed() throws Exception { + when(tokenExtractor.getToken(any(HttpServletRequest.class))).thenReturn("token"); + when(jwtParser.parse(anyString())).thenThrow(new JwtException("bad signature")); + + authenticationFilter.doFilter(request, response, chain); + + verify(response).setStatus(401); + verify(servletOutputStream).write(eq("Token validation failed: bad signature".getBytes())); + verifyNoMoreInteractions(chain); + } +} diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/KeycloakEnvironmentInitalizationFilterTest.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/KeycloakEnvironmentInitalizationFilterTest.java index 1ea1a73da7..d5a393fd5d 100644 --- a/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/KeycloakEnvironmentInitalizationFilterTest.java +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/KeycloakEnvironmentInitalizationFilterTest.java @@ -11,7 +11,6 @@ */ package org.eclipse.che.multiuser.keycloak.server; -import static io.jsonwebtoken.SignatureAlgorithm.RS512; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; @@ -23,12 +22,12 @@ import static org.mockito.Mockito.when; import static org.testng.AssertJUnit.assertEquals; import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.impl.DefaultClaims; import io.jsonwebtoken.impl.DefaultHeader; import io.jsonwebtoken.impl.DefaultJwt; +import java.lang.reflect.Field; import java.security.KeyPair; -import java.security.KeyPairGenerator; import java.security.PrivateKey; import java.security.PublicKey; import java.util.HashMap; @@ -45,7 +44,6 @@ 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; import org.eclipse.che.multiuser.machine.authentication.server.signature.SignatureKeyManager; -import org.eclipse.che.multiuser.machine.authentication.shared.Constants; import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -66,6 +64,7 @@ public class KeycloakEnvironmentInitalizationFilterTest { @Mock private HttpServletRequest request; @Mock private HttpServletResponse response; @Mock private HttpSession session; + @Mock private JwtParser jwtParser; private KeycloakEnvironmentInitalizationFilter filter; @@ -79,27 +78,17 @@ public class KeycloakEnvironmentInitalizationFilterTest { filter = new KeycloakEnvironmentInitalizationFilter( userManager, tokenExtractor, permissionChecker, keycloakSettings); - filter.signatureKeyManager = keyManager; + Field parser = filter.getClass().getSuperclass().getDeclaredField("jwtParser"); + parser.setAccessible(true); + parser.set(filter, jwtParser); final KeyPair kp = new KeyPair(mock(PublicKey.class), mock(PrivateKey.class)); - when(keyManager.getKeyPair()).thenReturn(kp); + when(keyManager.getKeyPair(anyString())).thenReturn(kp); } @Test public void shouldSkipRequestsWithMachineTokens() throws Exception { - final KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); - kpg.initialize(1024); - final KeyPair keyPair = kpg.generateKeyPair(); - when(keyManager.getKeyPair()).thenReturn(keyPair); - final Map header = new HashMap<>(); - header.put("kind", Constants.MACHINE_TOKEN_KIND); - final String token = - Jwts.builder() - .setPayload("payload") - .setHeader(header) - .signWith(RS512, keyPair.getPrivate()) - .compact(); - when(tokenExtractor.getToken(any(HttpServletRequest.class))).thenReturn(token); - + when(tokenExtractor.getToken(any(HttpServletRequest.class))).thenReturn("not_null_token"); + when(jwtParser.parse(anyString())).thenThrow(MachineTokenJwtException.class); // when filter.doFilter(request, response, chain); @@ -113,13 +102,13 @@ public class KeycloakEnvironmentInitalizationFilterTest { Subject existingSubject = new SubjectImpl("name", "id1", "token", false); UserImpl user = new UserImpl("id2", "test2@test.com", "username2"); - Subject expectedSubject = new SubjectImpl(user.getName(), user.getId(), "token2", false); ArgumentCaptor captor = ArgumentCaptor.forClass(AuthorizedSubject.class); - + DefaultJwt claims = createJwt(); + Subject expectedSubject = new SubjectImpl(user.getName(), user.getId(), "token2", false); // given when(tokenExtractor.getToken(any(HttpServletRequest.class))).thenReturn("token2"); - when(request.getAttribute("token")).thenReturn(createJwt()); + when(request.getAttribute("token")).thenReturn(claims); when(session.getAttribute(eq("che_subject"))).thenReturn(existingSubject); when(userManager.getOrCreateUser(anyString(), anyString(), anyString())).thenReturn(user); EnvironmentContext context = spy(EnvironmentContext.getCurrent()); diff --git a/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/KeycloakSigningKeyResolverTest.java b/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/KeycloakSigningKeyResolverTest.java new file mode 100644 index 0000000000..cd6eab74c3 --- /dev/null +++ b/multiuser/keycloak/che-multiuser-keycloak-server/src/test/java/org/eclipse/che/multiuser/keycloak/server/KeycloakSigningKeyResolverTest.java @@ -0,0 +1,87 @@ +/* + * 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.keycloak.server; + +import static org.eclipse.che.multiuser.machine.authentication.shared.Constants.MACHINE_TOKEN_KIND; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; + +import com.auth0.jwk.Jwk; +import com.auth0.jwk.JwkProvider; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.impl.DefaultClaims; +import io.jsonwebtoken.impl.DefaultJwsHeader; +import java.security.Key; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.util.HashMap; +import java.util.Map; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +@Listeners(MockitoTestNGListener.class) +public class KeycloakSigningKeyResolverTest { + + @Mock private JwkProvider jwkProvider; + + @InjectMocks private KeycloakSigningKeyResolver signingKeyResolver; + + @Test(expectedExceptions = MachineTokenJwtException.class) + public void shouldThrowMachineTokenExceptionOnMachineTokensWithPlainText() { + final Map param = new HashMap<>(); + param.put("kind", MACHINE_TOKEN_KIND); + DefaultJwsHeader header = new DefaultJwsHeader(param); + + signingKeyResolver.resolveSigningKey(header, "plaintext"); + verifyNoMoreInteractions(jwkProvider); + } + + @Test(expectedExceptions = MachineTokenJwtException.class) + public void shouldThrowMachineTokenExceptionOnMachineTokensWithClaims() { + final Map param = new HashMap<>(); + param.put("kind", MACHINE_TOKEN_KIND); + DefaultJwsHeader header = new DefaultJwsHeader(param); + + signingKeyResolver.resolveSigningKey(header, new DefaultClaims()); + verifyNoMoreInteractions(jwkProvider); + } + + @Test(expectedExceptions = JwtException.class) + public void shouldThrowJwtExceptionifNoKeyIdHeader() { + + signingKeyResolver.resolveSigningKey(new DefaultJwsHeader(), "plaintext"); + verifyNoMoreInteractions(jwkProvider); + } + + @Test + public void shouldReturnPublicKey() throws Exception { + final String kid = "123"; + final Jwk jwk = mock(Jwk.class); + final Map param = new HashMap<>(); + param.put("kid", kid); + final KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA"); + kpg.initialize(1024); + final KeyPair keyPair = kpg.generateKeyPair(); + + when(jwk.getPublicKey()).thenReturn(keyPair.getPublic()); + when(jwkProvider.get(eq(kid))).thenReturn(jwk); + + Key actual = signingKeyResolver.resolveSigningKey(new DefaultJwsHeader(param), "plaintext"); + assertEquals(actual, keyPair.getPublic()); + } +} diff --git a/multiuser/machine-auth/che-multiuser-machine-authentication/pom.xml b/multiuser/machine-auth/che-multiuser-machine-authentication/pom.xml index c412a386d7..ed3e81d063 100644 --- a/multiuser/machine-auth/che-multiuser-machine-authentication/pom.xml +++ b/multiuser/machine-auth/che-multiuser-machine-authentication/pom.xml @@ -62,6 +62,10 @@ javax.servlet javax.servlet-api + + org.eclipse.che.core + che-core-api-account + org.eclipse.che.core che-core-api-core diff --git a/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/MachineAuthModule.java b/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/MachineAuthModule.java index 7a8f1a13e2..8d4f67afc2 100644 --- a/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/MachineAuthModule.java +++ b/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/MachineAuthModule.java @@ -37,6 +37,8 @@ public class MachineAuthModule extends AbstractModule { bind(SignatureKeyManager.class); bind(SignatureKeyDao.class).to(JpaSignatureKeyDao.class); + bind(JpaSignatureKeyDao.RemoveKeyPairsBeforeWorkspaceRemovedEventSubscriber.class) + .asEagerSingleton(); final Multibinder envVarProviders = Multibinder.newSetBinder(binder(), EnvVarProvider.class); envVarProviders.addBinding().to(SignaturePublicKeyEnvProvider.class); diff --git a/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/MachineLoginFilter.java b/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/MachineLoginFilter.java index 00cc12b3f9..26c0d1a52e 100644 --- a/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/MachineLoginFilter.java +++ b/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/MachineLoginFilter.java @@ -14,16 +14,12 @@ package org.eclipse.che.multiuser.machine.authentication.server; 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.MACHINE_TOKEN_KIND; import static org.eclipse.che.multiuser.machine.authentication.shared.Constants.USER_ID_CLAIM; import io.jsonwebtoken.Claims; -import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.MalformedJwtException; -import io.jsonwebtoken.SignatureException; -import io.jsonwebtoken.UnsupportedJwtException; import java.io.IOException; import java.security.Principal; import javax.inject.Inject; @@ -46,7 +42,6 @@ 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; -import org.eclipse.che.multiuser.machine.authentication.server.signature.SignatureKeyManager; /** * Handles requests that comes from machines with specific machine token. @@ -59,46 +54,36 @@ public class MachineLoginFilter implements Filter { private final RequestTokenExtractor tokenExtractor; private final UserManager userManager; - private final SignatureKeyManager keyManager; private final PermissionChecker permissionChecker; + private final JwtParser jwtParser; @Inject public MachineLoginFilter( RequestTokenExtractor tokenExtractor, UserManager userManager, - SignatureKeyManager keyManager, + MachineSigningKeyResolver machineKeyResolver, PermissionChecker permissionChecker) { this.tokenExtractor = tokenExtractor; this.userManager = userManager; - this.keyManager = keyManager; this.permissionChecker = permissionChecker; + this.jwtParser = Jwts.parser().setSigningKeyResolver(machineKeyResolver); } @Override - public void init(FilterConfig filterConfig) throws ServletException {} + public void init(FilterConfig filterConfig) {} @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { final HttpServletRequest httpRequest = (HttpServletRequest) request; final String token = tokenExtractor.getToken(httpRequest); - if (isNullOrEmpty(token)) { chain.doFilter(request, response); return; } - // check token signature and verify is this token machine or not try { - final Jws jwt = - Jwts.parser().setSigningKey(keyManager.getKeyPair().getPublic()).parseClaimsJws(token); - final Claims claims = jwt.getBody(); - - if (!isMachineToken(jwt)) { - chain.doFilter(request, response); - return; - } - + final Claims claims = jwtParser.parseClaimsJws(token).getBody(); try { final String userId = claims.get(USER_ID_CLAIM, String.class); // check if user with such id exists @@ -113,28 +98,20 @@ public class MachineLoginFilter implements Filter { response, SC_UNAUTHORIZED, "Authentication with machine token failed because user for this token no longer exist."); - } catch (ServerException ex) { - sendErr( - response, - SC_UNAUTHORIZED, - format("Authentication with machine token failed cause: %s", ex.getMessage())); } finally { EnvironmentContext.reset(); } - } catch (UnsupportedJwtException - | MalformedJwtException - | SignatureException - | ExpiredJwtException ex) { - // signature check failed + } catch (NotMachineTokenJwtException ex) { + // not a machine token, bypass chain.doFilter(request, response); + } catch (ServerException | JwtException e) { + sendErr( + response, + SC_UNAUTHORIZED, + format("Authentication with machine token failed cause: %s", e.getMessage())); } } - /** Checks whether given token from a machine. */ - private boolean isMachineToken(Jws jwt) { - return MACHINE_TOKEN_KIND.equals(jwt.getHeader().get("kind")); - } - /** 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; diff --git a/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/MachineSessionInvalidator.java b/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/MachineSessionInvalidator.java index 8c1a59203e..e5ad7d395a 100644 --- a/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/MachineSessionInvalidator.java +++ b/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/MachineSessionInvalidator.java @@ -36,7 +36,7 @@ public class MachineSessionInvalidator implements EventSubscriber header = new HashMap<>(2); header.put("kind", MACHINE_TOKEN_KIND); diff --git a/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/NotMachineTokenJwtException.java b/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/NotMachineTokenJwtException.java new file mode 100644 index 0000000000..a9e46774dd --- /dev/null +++ b/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/NotMachineTokenJwtException.java @@ -0,0 +1,21 @@ +/* + * 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 io.jsonwebtoken.JwtException; + +public class NotMachineTokenJwtException extends JwtException { + + public NotMachineTokenJwtException() { + super("This is not a machine token"); + } +} diff --git a/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/signature/SignatureKeyManager.java b/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/signature/SignatureKeyManager.java index 825a8eef47..3a8b1e937a 100644 --- a/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/signature/SignatureKeyManager.java +++ b/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/signature/SignatureKeyManager.java @@ -11,10 +11,13 @@ */ package org.eclipse.che.multiuser.machine.authentication.server.signature; -import static org.eclipse.che.commons.lang.NameGenerator.generate; +import static org.eclipse.che.api.core.model.workspace.WorkspaceStatus.STOPPED; import com.google.common.annotations.Beta; import com.google.common.annotations.VisibleForTesting; +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheLoader; +import com.google.common.cache.LoadingCache; import java.security.KeyFactory; import java.security.KeyPair; import java.security.KeyPairGenerator; @@ -25,13 +28,18 @@ import java.security.spec.EncodedKeySpec; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; -import java.util.Iterator; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; import javax.annotation.PostConstruct; import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; import org.eclipse.che.api.core.ConflictException; +import org.eclipse.che.api.core.NotFoundException; import org.eclipse.che.api.core.ServerException; +import org.eclipse.che.api.core.notification.EventService; +import org.eclipse.che.api.core.notification.EventSubscriber; +import org.eclipse.che.api.workspace.shared.dto.event.WorkspaceStatusEvent; import org.eclipse.che.commons.annotation.Nullable; import org.eclipse.che.core.db.DBInitializer; import org.eclipse.che.multiuser.machine.authentication.server.signature.model.impl.SignatureKeyPairImpl; @@ -57,56 +65,95 @@ public class SignatureKeyManager { private final String algorithm; private final SignatureKeyDao signatureKeyDao; + private final EventService eventService; + private final EventSubscriber workspaceEventsSubscriber; @Inject @SuppressWarnings("unused") private DBInitializer dbInitializer; - private KeyPair cachedPair; + private LoadingCache cachedPair; @Inject public SignatureKeyManager( @Named("che.auth.signature_key_size") int keySize, @Named("che.auth.signature_key_algorithm") String algorithm, + EventService eventService, SignatureKeyDao signatureKeyDao) { this.keySize = keySize; this.algorithm = algorithm; + this.eventService = eventService; this.signatureKeyDao = signatureKeyDao; + + this.cachedPair = + CacheBuilder.newBuilder() + .maximumSize(100) + .expireAfterAccess(2, TimeUnit.HOURS) + .build( + new CacheLoader() { + @Override + public KeyPair load(String key) throws Exception { + return loadKeyPair(key); + } + }); + + this.workspaceEventsSubscriber = + new EventSubscriber() { + @Override + public void onEvent(WorkspaceStatusEvent event) { + if (event.getStatus() == STOPPED) { + removeKeyPair(event.getWorkspaceId()); + } + } + }; } /** Returns cached instance of {@link KeyPair} or null when failed to load key pair. */ @Nullable - public KeyPair getKeyPair() { - if (cachedPair == null) { - loadKeyPair(); + public KeyPair getKeyPair(String workspaceId) throws ServerException { + try { + return cachedPair.get(workspaceId); + } catch (ExecutionException e) { + throw new ServerException(e.getCause()); + } + } + + /** Removes key pair from cache and DB. */ + public void removeKeyPair(String workspaceId) { + try { + cachedPair.invalidate(workspaceId); + signatureKeyDao.remove(workspaceId); + } catch (ServerException e) { + LOG.error( + "Unable to cleanup machine token signature keypairs for ws {}. Cause: {}", + workspaceId, + e.getMessage()); } - return cachedPair; } /** Loads signature key pair if no existing keys found then stores a newly generated key pair. */ @PostConstruct @VisibleForTesting - void loadKeyPair() { + KeyPair loadKeyPair(String workspaceId) throws ServerException, ConflictException { try { - final Iterator it = signatureKeyDao.getAll(1, 0).getItems().iterator(); - if (it.hasNext()) { - cachedPair = toJavaKeyPair(it.next()); - return; + return toJavaKeyPair(signatureKeyDao.get(workspaceId)); + } catch (NotFoundException nfe) { + try { + return toJavaKeyPair(signatureKeyDao.create(generateKeyPair(workspaceId))); + } catch (ConflictException | ServerException ex) { + LOG.error( + "Failed to store signature keys for ws {}. Cause: {}", workspaceId, ex.getMessage()); + throw ex; } } catch (ServerException ex) { - LOG.error("Failed to load signature keys. Cause: {}", ex.getMessage()); - return; - } - - try { - cachedPair = toJavaKeyPair(signatureKeyDao.create(generateKeyPair())); - } catch (ConflictException | ServerException ex) { - LOG.error("Failed to store signature keys. Cause: {}", ex.getMessage()); + LOG.error( + "Failed to load signature keys for ws {}. Cause: {}", workspaceId, ex.getMessage()); + throw ex; } } @VisibleForTesting - SignatureKeyPairImpl generateKeyPair() throws ServerException { + SignatureKeyPairImpl generateKeyPair(String workspaceId) throws ServerException { final KeyPairGenerator kpg; try { kpg = KeyPairGenerator.getInstance(algorithm); @@ -116,8 +163,11 @@ public class SignatureKeyManager { kpg.initialize(keySize); final KeyPair pair = kpg.generateKeyPair(); final SignatureKeyPairImpl kp = - new SignatureKeyPairImpl(generate("signatureKey", 16), pair.getPublic(), pair.getPrivate()); - LOG.info("Generated signature key pair with id {} and algorithm {}.", kp.getId(), algorithm); + new SignatureKeyPairImpl(workspaceId, pair.getPublic(), pair.getPrivate()); + LOG.debug( + "Generated signature key pair with ws id {} and algorithm {}.", + kp.getWorkspaceId(), + algorithm); return kp; } @@ -149,4 +199,10 @@ public class SignatureKeyManager { String.format("Unsupported key spec '%s' for signature keys", key.getFormat())); } } + + @VisibleForTesting + @PostConstruct + void subscribe() { + eventService.subscribe(workspaceEventsSubscriber); + } } diff --git a/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/signature/SignatureKeyPair.java b/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/signature/SignatureKeyPair.java index ed809f951e..1f76a92c56 100644 --- a/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/signature/SignatureKeyPair.java +++ b/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/signature/SignatureKeyPair.java @@ -22,8 +22,8 @@ import com.google.common.annotations.Beta; @Beta public interface SignatureKeyPair { - /** Returns unique identifier for this sign key pair. */ - String getId(); + /** Returns workspace identifier for this sign key pair. */ + String getWorkspaceId(); /** Returns public part for this sign key pair. */ SignatureKey getPublicKey(); diff --git a/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/signature/SignaturePublicKeyEnvProvider.java b/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/signature/SignaturePublicKeyEnvProvider.java index 62957b054a..54874aa99e 100644 --- a/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/signature/SignaturePublicKeyEnvProvider.java +++ b/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/signature/SignaturePublicKeyEnvProvider.java @@ -15,7 +15,9 @@ import static org.eclipse.che.multiuser.machine.authentication.shared.Constants. import java.util.Base64; import javax.inject.Inject; +import org.eclipse.che.api.core.ServerException; import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; import org.eclipse.che.api.workspace.server.spi.provision.env.EnvVarProvider; import org.eclipse.che.commons.lang.Pair; @@ -34,9 +36,21 @@ public class SignaturePublicKeyEnvProvider implements EnvVarProvider { } @Override - public Pair get(RuntimeIdentity runtimeIdentity) { - return Pair.of( - SIGNATURE_PUBLIC_KEY_ENV, - new String(Base64.getEncoder().encode(keyManager.getKeyPair().getPublic().getEncoded()))); + public Pair get(RuntimeIdentity runtimeIdentity) throws InfrastructureException { + try { + return Pair.of( + SIGNATURE_PUBLIC_KEY_ENV, + new String( + Base64.getEncoder() + .encode( + keyManager + .getKeyPair(runtimeIdentity.getWorkspaceId()) + .getPublic() + .getEncoded()))); + } catch (ServerException e) { + throw new InfrastructureException( + "Signature key pair for machine authentication cannot be retrieved. Reason: " + + e.getMessage()); + } } } diff --git a/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/signature/jpa/JpaSignatureKeyDao.java b/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/signature/jpa/JpaSignatureKeyDao.java index 5e30e5ef28..b3d6010628 100644 --- a/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/signature/jpa/JpaSignatureKeyDao.java +++ b/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/signature/jpa/JpaSignatureKeyDao.java @@ -11,20 +11,23 @@ */ package org.eclipse.che.multiuser.machine.authentication.server.signature.jpa; -import static com.google.common.base.Preconditions.checkArgument; import static java.lang.String.format; import static java.util.Objects.requireNonNull; -import static java.util.stream.Collectors.toList; import com.google.inject.persist.Transactional; -import java.util.List; +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; import javax.inject.Inject; import javax.inject.Provider; import javax.inject.Singleton; import javax.persistence.EntityManager; +import javax.persistence.NoResultException; import org.eclipse.che.api.core.ConflictException; -import org.eclipse.che.api.core.Page; +import org.eclipse.che.api.core.NotFoundException; import org.eclipse.che.api.core.ServerException; +import org.eclipse.che.api.core.notification.EventService; +import org.eclipse.che.api.workspace.server.event.BeforeWorkspaceRemovedEvent; +import org.eclipse.che.core.db.cascade.CascadeEventSubscriber; import org.eclipse.che.core.db.jpa.DuplicateKeyException; import org.eclipse.che.multiuser.machine.authentication.server.signature.model.impl.SignatureKeyPairImpl; import org.eclipse.che.multiuser.machine.authentication.server.signature.spi.SignatureKeyDao; @@ -52,7 +55,7 @@ public class JpaSignatureKeyDao implements SignatureKeyDao { doCreate(keyPair); } catch (DuplicateKeyException dkEx) { throw new ConflictException( - format("Signature key pair with id '%s' already exists", keyPair.getId())); + format("Signature key pair for workspace '%s' already exists", keyPair.getWorkspaceId())); } catch (RuntimeException ex) { throw new ServerException(ex.getMessage(), ex); } @@ -67,20 +70,20 @@ public class JpaSignatureKeyDao implements SignatureKeyDao { } @Override - public void remove(String id) throws ServerException { - requireNonNull(id, "Required non-null key pair"); + public void remove(String workspaceId) throws ServerException { + requireNonNull(workspaceId, "Required non-null workspace Id"); try { - doRemove(id); + doRemove(workspaceId); } catch (RuntimeException ex) { throw new ServerException(ex.getMessage(), ex); } } @Transactional - protected void doRemove(String id) { - final SignatureKeyPairImpl keyPair = managerProvider.get().find(SignatureKeyPairImpl.class, id); + protected void doRemove(String workspaceId) { + final EntityManager manager = managerProvider.get(); + final SignatureKeyPairImpl keyPair = manager.find(SignatureKeyPairImpl.class, workspaceId); if (keyPair != null) { - final EntityManager manager = managerProvider.get(); manager.remove(keyPair); manager.flush(); } @@ -88,27 +91,41 @@ public class JpaSignatureKeyDao implements SignatureKeyDao { @Override @Transactional - public Page getAll(int maxItems, long skipCount) throws ServerException { - checkArgument(maxItems >= 0, "The number of items to return can't be negative."); - checkArgument( - skipCount >= 0, - "The number of items to skip can't be negative or greater than " + Integer.MAX_VALUE); + public SignatureKeyPairImpl get(String workspaceId) throws NotFoundException, ServerException { + final EntityManager manager = managerProvider.get(); try { - final EntityManager manager = managerProvider.get(); - final List list = + return new SignatureKeyPairImpl( manager .createNamedQuery("SignKeyPair.getAll", SignatureKeyPairImpl.class) - .setMaxResults(maxItems) - .setFirstResult((int) skipCount) - .getResultList() - .stream() - .map(SignatureKeyPairImpl::new) - .collect(toList()); - final long count = - manager.createNamedQuery("SignKeyPair.getAllCount", Long.class).getSingleResult(); - return new Page<>(list, skipCount, maxItems, count); + .setParameter("workspaceId", workspaceId) + .getSingleResult()); + } catch (NoResultException x) { + throw new NotFoundException( + format("Signature key pair for workspace '%s' doesn't exist", workspaceId)); } catch (RuntimeException ex) { throw new ServerException(ex.getMessage(), ex); } } + + @Singleton + public static class RemoveKeyPairsBeforeWorkspaceRemovedEventSubscriber + extends CascadeEventSubscriber { + @Inject private EventService eventService; + @Inject private SignatureKeyDao signatureKeyDao; + + @PostConstruct + public void subscribe() { + eventService.subscribe(this, BeforeWorkspaceRemovedEvent.class); + } + + @PreDestroy + public void unsubscribe() { + eventService.unsubscribe(this, BeforeWorkspaceRemovedEvent.class); + } + + @Override + public void onCascadeEvent(BeforeWorkspaceRemovedEvent event) throws Exception { + signatureKeyDao.remove(event.getWorkspace().getId()); + } + } } diff --git a/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/signature/model/impl/SignatureKeyPairImpl.java b/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/signature/model/impl/SignatureKeyPairImpl.java index 5f448e6899..3ff0861380 100644 --- a/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/signature/model/impl/SignatureKeyPairImpl.java +++ b/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/signature/model/impl/SignatureKeyPairImpl.java @@ -23,20 +23,26 @@ import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; import javax.persistence.OneToOne; import javax.persistence.Table; +import org.eclipse.che.api.workspace.server.model.impl.WorkspaceImpl; import org.eclipse.che.multiuser.machine.authentication.server.signature.SignatureKeyPair; /** @author Anton Korneta */ @Entity(name = "SignKeyPair") @Table(name = "che_sign_key_pair") @NamedQueries({ - @NamedQuery(name = "SignKeyPair.getAll", query = "SELECT kp FROM SignKeyPair kp"), - @NamedQuery(name = "SignKeyPair.getAllCount", query = "SELECT COUNT(kp) FROM SignKeyPair kp") + @NamedQuery( + name = "SignKeyPair.getAll", + query = "SELECT kp FROM SignKeyPair kp WHERE kp.workspaceId = :workspaceId"), }) public class SignatureKeyPairImpl implements SignatureKeyPair { @Id - @Column(name = "id") - private String id; + @Column(name = "workspace_id") + private String workspaceId; + + @OneToOne + @JoinColumn(name = "workspace_id", insertable = false, updatable = false) + private WorkspaceImpl workspace; @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true) @JoinColumn(name = "public_key") @@ -49,26 +55,27 @@ public class SignatureKeyPairImpl implements SignatureKeyPair { public SignatureKeyPairImpl() {} public SignatureKeyPairImpl(SignatureKeyPairImpl keyPair) { - this(keyPair.getId(), keyPair.getPublicKey(), keyPair.getPrivateKey()); + this(keyPair.getWorkspaceId(), keyPair.getPublicKey(), keyPair.getPrivateKey()); } - public SignatureKeyPairImpl(String id, PublicKey publicKey, PrivateKey privateKey) { - this(id, new SignatureKeyImpl(publicKey), new SignatureKeyImpl(privateKey)); + public SignatureKeyPairImpl(String workspaceId, PublicKey publicKey, PrivateKey privateKey) { + this(workspaceId, new SignatureKeyImpl(publicKey), new SignatureKeyImpl(privateKey)); } - public SignatureKeyPairImpl(String id, SignatureKeyImpl publicKey, SignatureKeyImpl privateKey) { - this.id = id; + public SignatureKeyPairImpl( + String workspaceId, SignatureKeyImpl publicKey, SignatureKeyImpl privateKey) { + this.workspaceId = workspaceId; this.publicKey = publicKey; this.privateKey = privateKey; } @Override - public String getId() { - return id; + public String getWorkspaceId() { + return workspaceId; } - public void setId(String id) { - this.id = id; + public void setWorkspaceId(String workspaceId) { + this.workspaceId = workspaceId; } @Override @@ -98,7 +105,7 @@ public class SignatureKeyPairImpl implements SignatureKeyPair { return false; } final SignatureKeyPairImpl that = (SignatureKeyPairImpl) obj; - return Objects.equals(id, that.id) + return Objects.equals(workspaceId, that.workspaceId) && Objects.equals(publicKey, that.publicKey) && Objects.equals(privateKey, that.privateKey); } @@ -106,7 +113,7 @@ public class SignatureKeyPairImpl implements SignatureKeyPair { @Override public int hashCode() { int hash = 7; - hash = 31 * hash + Objects.hashCode(id); + hash = 31 * hash + Objects.hashCode(workspaceId); hash = 31 * hash + Objects.hashCode(publicKey); hash = 31 * hash + Objects.hashCode(privateKey); return hash; @@ -115,8 +122,8 @@ public class SignatureKeyPairImpl implements SignatureKeyPair { @Override public String toString() { return "SignatureKeyPairImpl{" - + "id='" - + id + + "workspaceId='" + + workspaceId + '\'' + ", publicKey=" + publicKey diff --git a/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/signature/spi/SignatureKeyDao.java b/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/signature/spi/SignatureKeyDao.java index 4809e3850c..aa6094daaf 100644 --- a/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/signature/spi/SignatureKeyDao.java +++ b/multiuser/machine-auth/che-multiuser-machine-authentication/src/main/java/org/eclipse/che/multiuser/machine/authentication/server/signature/spi/SignatureKeyDao.java @@ -13,7 +13,7 @@ package org.eclipse.che.multiuser.machine.authentication.server.signature.spi; import com.google.common.annotations.Beta; import org.eclipse.che.api.core.ConflictException; -import org.eclipse.che.api.core.Page; +import org.eclipse.che.api.core.NotFoundException; import org.eclipse.che.api.core.ServerException; import org.eclipse.che.multiuser.machine.authentication.server.signature.model.impl.SignatureKeyPairImpl; @@ -36,21 +36,19 @@ public interface SignatureKeyDao { throws ConflictException, ServerException; /** - * Removes signature key pair with given id. + * Removes signature key pair with given workspace id. * - * @param id signature key identifier + * @param workspaceId workspace identifier to remove keypair from * @throws ServerException when any errors occur while removing signature key pair */ - void remove(String id) throws ServerException; + void remove(String workspaceId) throws ServerException; /** - * Returns all the signature key pairs. + * Returns signature key pair for given workspace id. * - * @param skipCount the number of signature key pairs to skip - * @param maxItems the maximum number of signature key pairs to return - * @return list of signature key pairs or an empty list when no keys were found - * @throws ServerException when any errors occur while fetching the key pairs - * @throws IllegalArgumentException when {@code maxItems} or {@code skipCount} is negative + * @param workspaceId identifier of workspace which key pair belongs to + * @return signature key pair for the given workspace + * @throws NotFoundException when any errors occur while fetching the key pairs */ - Page getAll(int maxItems, long skipCount) throws ServerException; + SignatureKeyPairImpl get(String workspaceId) throws NotFoundException, ServerException; } diff --git a/multiuser/machine-auth/che-multiuser-machine-authentication/src/test/java/org/eclipse/che/multiuser/machine/authentication/server/MachineLoginFilterTest.java b/multiuser/machine-auth/che-multiuser-machine-authentication/src/test/java/org/eclipse/che/multiuser/machine/authentication/server/MachineLoginFilterTest.java index b88be14637..81b08680e3 100644 --- a/multiuser/machine-auth/che-multiuser-machine-authentication/src/test/java/org/eclipse/che/multiuser/machine/authentication/server/MachineLoginFilterTest.java +++ b/multiuser/machine-auth/che-multiuser-machine-authentication/src/test/java/org/eclipse/che/multiuser/machine/authentication/server/MachineLoginFilterTest.java @@ -100,10 +100,13 @@ public class MachineLoginFilterTest { .compact(); machineLoginFilter = new MachineLoginFilter( - tokenExtractorMock, userManagerMock, keyManagerMock, permissionCheckerMock); + tokenExtractorMock, + userManagerMock, + new MachineSigningKeyResolver(keyManagerMock), + permissionCheckerMock); when(tokenExtractorMock.getToken(any(HttpServletRequest.class))).thenReturn(token); - when(keyManagerMock.getKeyPair()).thenReturn(keyPair); + when(keyManagerMock.getKeyPair(eq(WORKSPACE_ID))).thenReturn(keyPair); when(userMock.getName()).thenReturn(SUBJECT.getUserName()); when(userManagerMock.getById(SUBJECT.getUserId())).thenReturn(userMock); @@ -113,25 +116,27 @@ public class MachineLoginFilterTest { public void testProcessRequestWithValidToken() throws Exception { machineLoginFilter.doFilter(getRequestMock(), responseMock, chainMock); - verify(keyManagerMock).getKeyPair(); + verify(keyManagerMock).getKeyPair(eq(WORKSPACE_ID)); verify(userManagerMock).getById(anyString()); verifyZeroInteractions(responseMock); } @Test - public void testProceedRequestWhenSignatureCheckIsFailed() throws Exception { - final String tokenWithInvalidSignature = "keycloak_token"; + public void testNotProceedRequestWhenSignatureCheckIsFailed() throws Exception { final HttpServletRequest requestMock = getRequestMock(); - when(tokenExtractorMock.getToken(any(HttpServletRequest.class))) - .thenReturn(tokenWithInvalidSignature); + final KeyPairGenerator kpg = KeyPairGenerator.getInstance(SIGNATURE_ALGORITHM); + kpg.initialize(KEY_SIZE); + final KeyPair pair = kpg.generateKeyPair(); + when(keyManagerMock.getKeyPair(eq(WORKSPACE_ID))).thenReturn(pair); machineLoginFilter.doFilter(requestMock, responseMock, chainMock); verify(tokenExtractorMock).getToken(any(HttpServletRequest.class)); - verify(keyManagerMock).getKeyPair(); - verify(chainMock).doFilter(requestMock, responseMock); - verifyZeroInteractions(userManagerMock); - verifyZeroInteractions(responseMock); + verify(responseMock) + .sendError( + 401, + "Authentication with machine token failed cause: JWT signature does not match locally computed signature." + + " JWT validity cannot be asserted and should not be trusted."); } @Test @@ -154,7 +159,7 @@ public class MachineLoginFilterTest { machineLoginFilter.doFilter(getRequestMock(), responseMock, chainMock); - verify(keyManagerMock).getKeyPair(); + verify(keyManagerMock).getKeyPair(eq(WORKSPACE_ID)); verify(userManagerMock).getById(anyString()); verify(responseMock) .sendError( @@ -168,7 +173,7 @@ public class MachineLoginFilterTest { machineLoginFilter.doFilter(getRequestMock(), responseMock, chainMock); - verify(keyManagerMock).getKeyPair(); + verify(keyManagerMock).getKeyPair(eq(WORKSPACE_ID)); verify(userManagerMock).getById(anyString()); verify(responseMock) .sendError( diff --git a/multiuser/machine-auth/che-multiuser-machine-authentication/src/test/java/org/eclipse/che/multiuser/machine/authentication/server/MachineTokenRegistryTest.java b/multiuser/machine-auth/che-multiuser-machine-authentication/src/test/java/org/eclipse/che/multiuser/machine/authentication/server/MachineTokenRegistryTest.java index 485378339c..98ecd5fe80 100644 --- a/multiuser/machine-auth/che-multiuser-machine-authentication/src/test/java/org/eclipse/che/multiuser/machine/authentication/server/MachineTokenRegistryTest.java +++ b/multiuser/machine-auth/che-multiuser-machine-authentication/src/test/java/org/eclipse/che/multiuser/machine/authentication/server/MachineTokenRegistryTest.java @@ -69,7 +69,7 @@ public class MachineTokenRegistryTest { keyPair = kpg.generateKeyPair(); mockUser(USER_ID, USER_NAME); - when(signatureKeyManager.getKeyPair()).thenReturn(keyPair); + when(signatureKeyManager.getKeyPair(anyString())).thenReturn(keyPair); } @Test @@ -89,7 +89,7 @@ public class MachineTokenRegistryTest { assertEquals(subject.getUserName(), USER_NAME); assertEquals(claims.get(WORKSPACE_ID_CLAIM, String.class), WORKSPACE_ID); verify(userManager).getById(USER_ID); - verify(signatureKeyManager).getKeyPair(); + verify(signatureKeyManager).getKeyPair(anyString()); assertNotNull(generatedToken); } diff --git a/multiuser/machine-auth/che-multiuser-machine-authentication/src/test/java/org/eclipse/che/multiuser/machine/authentication/server/signature/SignatureKeyManagerTest.java b/multiuser/machine-auth/che-multiuser-machine-authentication/src/test/java/org/eclipse/che/multiuser/machine/authentication/server/signature/SignatureKeyManagerTest.java index 2a97472b10..6c58d9c0bc 100644 --- a/multiuser/machine-auth/che-multiuser-machine-authentication/src/test/java/org/eclipse/che/multiuser/machine/authentication/server/signature/SignatureKeyManagerTest.java +++ b/multiuser/machine-auth/che-multiuser-machine-authentication/src/test/java/org/eclipse/che/multiuser/machine/authentication/server/signature/SignatureKeyManagerTest.java @@ -11,26 +11,32 @@ */ package org.eclipse.che.multiuser.machine.authentication.server.signature; -import static java.util.Collections.emptyList; -import static java.util.Collections.singleton; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; -import static org.testng.Assert.assertNull; import java.security.Key; import java.security.KeyPair; import java.security.KeyPairGenerator; -import org.eclipse.che.api.core.Page; +import org.eclipse.che.api.core.NotFoundException; import org.eclipse.che.api.core.ServerException; +import org.eclipse.che.api.core.model.workspace.WorkspaceStatus; +import org.eclipse.che.api.core.notification.EventService; +import org.eclipse.che.api.core.notification.EventSubscriber; +import org.eclipse.che.api.workspace.shared.dto.event.WorkspaceStatusEvent; +import org.eclipse.che.dto.server.DtoFactory; import org.eclipse.che.multiuser.machine.authentication.server.signature.model.impl.SignatureKeyImpl; import org.eclipse.che.multiuser.machine.authentication.server.signature.model.impl.SignatureKeyPairImpl; import org.eclipse.che.multiuser.machine.authentication.server.signature.spi.SignatureKeyDao; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; import org.mockito.Mock; import org.mockito.stubbing.Answer; import org.mockito.testng.MockitoTestNGListener; @@ -50,6 +56,9 @@ public class SignatureKeyManagerTest { private static final String ALGORITHM = "RSA"; @Mock SignatureKeyDao signatureKeyDao; + @Mock EventService eventService; + + @Captor private ArgumentCaptor> captor; private KeyPairGenerator kpg; private SignatureKeyManager signatureKeyManager; @@ -58,18 +67,19 @@ public class SignatureKeyManagerTest { public void createEntities() throws Exception { kpg = KeyPairGenerator.getInstance(ALGORITHM); kpg.initialize(KEY_SIZE); - signatureKeyManager = new SignatureKeyManager(KEY_SIZE, ALGORITHM, signatureKeyDao); + signatureKeyManager = + new SignatureKeyManager(KEY_SIZE, ALGORITHM, eventService, signatureKeyDao); } @Test public void testLoadSignatureKeys() throws Exception { - final SignatureKeyPairImpl kp = newKeyPair("id_" + 1); - when(signatureKeyDao.getAll(anyInt(), anyLong())) - .thenReturn(new Page<>(singleton(kp), 0, 1, 1)); + String wsId = "WS_id_1"; + final SignatureKeyPairImpl kp = newKeyPair(wsId); + when(signatureKeyDao.get(anyString())).thenReturn(kp); - signatureKeyManager.loadKeyPair(); + signatureKeyManager.loadKeyPair(wsId); - final KeyPair cachedPair = signatureKeyManager.getKeyPair(); + final KeyPair cachedPair = signatureKeyManager.getKeyPair(wsId); assertNotNull(cachedPair); assertKeys(cachedPair.getPublic(), kp.getPublicKey()); assertKeys(cachedPair.getPrivate(), kp.getPrivateKey()); @@ -77,51 +87,67 @@ public class SignatureKeyManagerTest { @Test public void testTriesToLoadKeysOnGettingKeyPairAndNoCachedKeyPair() throws Exception { - when(signatureKeyDao.getAll(anyInt(), anyLong())) - .thenThrow(new ServerException("unexpected end of stack")); + String wsId = "WS_id_1"; + final SignatureKeyPairImpl kp = newKeyPair(wsId); + when(signatureKeyDao.create(any(SignatureKeyPairImpl.class))).thenReturn(kp); + when(signatureKeyDao.get(anyString())).thenThrow(new NotFoundException("not found")); - signatureKeyManager.getKeyPair(); + signatureKeyManager.getKeyPair("ws1"); - verify(signatureKeyDao).getAll(anyInt(), anyLong()); + verify(signatureKeyDao).get(anyString()); + verify(signatureKeyDao).create(any(SignatureKeyPairImpl.class)); } @Test public void testGeneratesNewKeyPairWhenNoExistingKeyPairFound() throws Exception { - doReturn(new Page<>(emptyList(), 0, 1, 0)).when(signatureKeyDao).getAll(anyInt(), anyLong()); + doThrow(NotFoundException.class).when(signatureKeyDao).get(anyString()); when(signatureKeyDao.create(any(SignatureKeyPairImpl.class))) .thenAnswer((Answer) invoke -> invoke.getArgument(0)); - final KeyPair cachedPair = signatureKeyManager.getKeyPair(); + final KeyPair cachedPair = signatureKeyManager.getKeyPair("ws1"); - verify(signatureKeyDao).getAll(anyInt(), anyLong()); + verify(signatureKeyDao).get(anyString()); verify(signatureKeyDao).create(any(SignatureKeyPairImpl.class)); assertNotNull(cachedPair); } - @Test - public void testReturnNullKeyPairWhenFailedToLoadAndGenerateKeys() throws Exception { - doReturn(new Page<>(emptyList(), 0, 1, 0)).when(signatureKeyDao).getAll(anyInt(), anyLong()); + @Test(expectedExceptions = ServerException.class) + public void testThrowsExceptionWhenFailedToLoadAndGenerateKeys() throws Exception { + doThrow(NotFoundException.class).when(signatureKeyDao).get(anyString()); when(signatureKeyDao.create(any(SignatureKeyPairImpl.class))) .thenThrow(new ServerException("unexpected end of stack")); - final KeyPair cachedPair = signatureKeyManager.getKeyPair(); + signatureKeyManager.getKeyPair("ws1"); - verify(signatureKeyDao).getAll(anyInt(), anyLong()); + verify(signatureKeyDao).get(anyString()); verify(signatureKeyDao).create(any(SignatureKeyPairImpl.class)); - assertNull(cachedPair); } - @Test - public void testReturnNullKeyPairWhenAlgorithmIsNotSupported() throws Exception { + @Test(expectedExceptions = ServerException.class) + public void testThrowsExceptionWhenAlgorithmIsNotSupported() throws Exception { final SignatureKeyImpl publicKey = new SignatureKeyImpl(new byte[] {}, "ECDH", "PKCS#15"); final SignatureKeyImpl privateKey = new SignatureKeyImpl(new byte[] {}, "ECDH", "PKCS#3"); final SignatureKeyPairImpl kp = new SignatureKeyPairImpl("id_" + 1, publicKey, privateKey); - doReturn(new Page<>(singleton(kp), 0, 1, 1)).when(signatureKeyDao).getAll(anyInt(), anyLong()); + doReturn(kp).when(signatureKeyDao).get(anyString()); - final KeyPair cachedPair = signatureKeyManager.getKeyPair(); + signatureKeyManager.getKeyPair("ws1"); - verify(signatureKeyDao).getAll(anyInt(), anyLong()); - assertNull(cachedPair); + verify(signatureKeyDao).get(anyString()); + } + + @Test + public void shouldRemoveKeyPairOnWorkspaceStop() throws Exception { + final String wsId = "ws123"; + signatureKeyManager.subscribe(); + verify(eventService).subscribe(captor.capture()); + final EventSubscriber subscriber = captor.getValue(); + + subscriber.onEvent( + DtoFactory.newDto(WorkspaceStatusEvent.class) + .withStatus(WorkspaceStatus.STOPPED) + .withWorkspaceId(wsId)); + + verify(signatureKeyDao, times(1)).remove(eq(wsId)); } private SignatureKeyPairImpl newKeyPair(String id) { diff --git a/multiuser/machine-auth/che-multiuser-machine-authentication/src/test/java/org/eclipse/che/multiuser/machine/authentication/server/signature/jpa/SignatureKeyTckModule.java b/multiuser/machine-auth/che-multiuser-machine-authentication/src/test/java/org/eclipse/che/multiuser/machine/authentication/server/signature/jpa/SignatureKeyTckModule.java index b8c584f4e6..28242947b3 100644 --- a/multiuser/machine-auth/che-multiuser-machine-authentication/src/test/java/org/eclipse/che/multiuser/machine/authentication/server/signature/jpa/SignatureKeyTckModule.java +++ b/multiuser/machine-auth/che-multiuser-machine-authentication/src/test/java/org/eclipse/che/multiuser/machine/authentication/server/signature/jpa/SignatureKeyTckModule.java @@ -12,6 +12,20 @@ package org.eclipse.che.multiuser.machine.authentication.server.signature.jpa; import com.google.inject.TypeLiteral; +import java.util.Collection; +import org.eclipse.che.account.spi.AccountImpl; +import org.eclipse.che.api.user.server.model.impl.UserImpl; +import org.eclipse.che.api.workspace.server.model.impl.CommandImpl; +import org.eclipse.che.api.workspace.server.model.impl.EnvironmentImpl; +import org.eclipse.che.api.workspace.server.model.impl.MachineConfigImpl; +import org.eclipse.che.api.workspace.server.model.impl.ProjectConfigImpl; +import org.eclipse.che.api.workspace.server.model.impl.RecipeImpl; +import org.eclipse.che.api.workspace.server.model.impl.ServerConfigImpl; +import org.eclipse.che.api.workspace.server.model.impl.SourceStorageImpl; +import org.eclipse.che.api.workspace.server.model.impl.VolumeImpl; +import org.eclipse.che.api.workspace.server.model.impl.WorkspaceConfigImpl; +import org.eclipse.che.api.workspace.server.model.impl.WorkspaceImpl; +import org.eclipse.che.api.workspace.server.model.impl.stack.StackImpl; import org.eclipse.che.commons.test.db.H2DBTestServer; import org.eclipse.che.commons.test.db.H2JpaCleaner; import org.eclipse.che.commons.test.db.PersistTestModuleBuilder; @@ -19,6 +33,7 @@ import org.eclipse.che.commons.test.tck.TckModule; import org.eclipse.che.commons.test.tck.TckResourcesCleaner; import org.eclipse.che.commons.test.tck.repository.JpaTckRepository; import org.eclipse.che.commons.test.tck.repository.TckRepository; +import org.eclipse.che.commons.test.tck.repository.TckRepositoryException; import org.eclipse.che.core.db.DBInitializer; import org.eclipse.che.core.db.h2.jpa.eclipselink.H2ExceptionHandler; import org.eclipse.che.core.db.schema.SchemaInitializer; @@ -37,7 +52,24 @@ public class SignatureKeyTckModule extends TckModule { new PersistTestModuleBuilder() .setDriver("org.h2.Driver") .runningOn(server) - .addEntityClasses(SignatureKeyImpl.class, SignatureKeyPairImpl.class) + .addEntityClasses( + AccountImpl.class, + UserImpl.class, + SignatureKeyImpl.class, + SignatureKeyPairImpl.class, + WorkspaceImpl.class, + WorkspaceConfigImpl.class, + ProjectConfigImpl.class, + EnvironmentImpl.class, + MachineConfigImpl.class, + SourceStorageImpl.class, + ServerConfigImpl.class, + StackImpl.class, + CommandImpl.class, + RecipeImpl.class, + VolumeImpl.class) + .addEntityClass( + "org.eclipse.che.api.workspace.server.model.impl.ProjectConfigImpl$Attribute") .setExceptionHandler(H2ExceptionHandler.class) .build()); @@ -49,5 +81,23 @@ public class SignatureKeyTckModule extends TckModule { bind(SignatureKeyDao.class).to(JpaSignatureKeyDao.class); bind(new TypeLiteral>() {}) .toInstance(new JpaTckRepository<>(SignatureKeyPairImpl.class)); + bind(new TypeLiteral>() {}) + .toInstance(new JpaTckRepository<>(AccountImpl.class)); + bind(new TypeLiteral>() {}).toInstance(new WorkspaceRepository()); + } + + private static class WorkspaceRepository extends JpaTckRepository { + public WorkspaceRepository() { + super(WorkspaceImpl.class); + } + + @Override + public void createAll(Collection entities) + throws TckRepositoryException { + for (WorkspaceImpl entity : entities) { + entity.getConfig().getProjects().forEach(ProjectConfigImpl::prePersistAttributes); + } + super.createAll(entities); + } } } diff --git a/multiuser/machine-auth/che-multiuser-machine-authentication/src/test/java/org/eclipse/che/multiuser/machine/authentication/server/signature/spi/tck/SignatureKeyDaoTest.java b/multiuser/machine-auth/che-multiuser-machine-authentication/src/test/java/org/eclipse/che/multiuser/machine/authentication/server/signature/spi/tck/SignatureKeyDaoTest.java index 047d89ceee..18f1774ff6 100644 --- a/multiuser/machine-auth/che-multiuser-machine-authentication/src/test/java/org/eclipse/che/multiuser/machine/authentication/server/signature/spi/tck/SignatureKeyDaoTest.java +++ b/multiuser/machine-auth/che-multiuser-machine-authentication/src/test/java/org/eclipse/che/multiuser/machine/authentication/server/signature/spi/tck/SignatureKeyDaoTest.java @@ -14,16 +14,22 @@ package org.eclipse.che.multiuser.machine.authentication.server.signature.spi.tc import static java.util.Arrays.asList; import static java.util.stream.Collectors.toList; import static org.testng.Assert.assertEquals; -import static org.testng.Assert.assertFalse; -import static org.testng.Assert.assertTrue; +import static org.testng.Assert.assertNotNull; import java.security.KeyPair; import java.security.KeyPairGenerator; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.stream.Stream; import javax.inject.Inject; +import org.eclipse.che.account.spi.AccountImpl; import org.eclipse.che.api.core.ConflictException; -import org.eclipse.che.api.core.Page; +import org.eclipse.che.api.core.NotFoundException; +import org.eclipse.che.api.workspace.server.model.impl.WorkspaceConfigImpl; +import org.eclipse.che.api.workspace.server.model.impl.WorkspaceImpl; import org.eclipse.che.commons.test.tck.TckListener; import org.eclipse.che.commons.test.tck.repository.TckRepository; import org.eclipse.che.commons.test.tck.repository.TckRepositoryException; @@ -47,9 +53,11 @@ public class SignatureKeyDaoTest { public static final String ALGORITHM = "RSA"; public static final int KEY_SIZE = 512; - private static final int COUNT_KEY_PAIRS = 5; + private static final int COUNT_KEY_PAIRS = 3; @Inject private TckRepository signatureKeyRepo; + @Inject private TckRepository accountRepository; + @Inject private TckRepository workspaceRepository; @Inject private SignatureKeyDao dao; private SignatureKeyPairImpl[] storedKeyPairs; @@ -58,15 +66,39 @@ public class SignatureKeyDaoTest { @AfterMethod public void removeEntities() throws TckRepositoryException { signatureKeyRepo.removeAll(); + workspaceRepository.removeAll(); + accountRepository.removeAll(); } @BeforeMethod public void createEntities() throws Exception { + + AccountImpl account = new AccountImpl("account1", "accountName", "test"); + accountRepository.createAll(Collections.singletonList(account)); + workspaceRepository.createAll( + Arrays.asList( + new WorkspaceImpl( + "ws0", + account, + new WorkspaceConfigImpl("ws-name0", "", "cfg0", null, null, null, null)), + new WorkspaceImpl( + "ws1", + account, + new WorkspaceConfigImpl("ws-name1", "", "cfg1", null, null, null, null)), + new WorkspaceImpl( + "ws2", + account, + new WorkspaceConfigImpl("ws-name2", "", "cfg2", null, null, null, null)), + new WorkspaceImpl( + "id_10", + account, + new WorkspaceConfigImpl("ws-name10", "", "cfg1", null, null, null, null)))); + storedKeyPairs = new SignatureKeyPairImpl[COUNT_KEY_PAIRS]; kpg = KeyPairGenerator.getInstance(ALGORITHM); kpg.initialize(KEY_SIZE); for (int i = 0; i < COUNT_KEY_PAIRS; i++) { - storedKeyPairs[i] = newKeyPair("id_" + i); + storedKeyPairs[i] = newKeyPair("ws" + i); } signatureKeyRepo.createAll( Stream.of(storedKeyPairs).map(SignatureKeyPairImpl::new).collect(toList())); @@ -74,54 +106,48 @@ public class SignatureKeyDaoTest { @Test public void testGetsAllKeys() throws Exception { - final Page foundKeys = dao.getAll(COUNT_KEY_PAIRS, 0); - - assertEquals(new HashSet<>(foundKeys.getItems()), new HashSet<>(asList(storedKeyPairs))); - assertEquals(foundKeys.getTotalItemsCount(), COUNT_KEY_PAIRS); - assertEquals(foundKeys.getItems().size(), COUNT_KEY_PAIRS); - } - - @Test(expectedExceptions = IllegalArgumentException.class) - public void testThrowsIllegalArgumentExceptionWhenMaxItemsIsNegative() throws Exception { - dao.getAll(-1, 0); - } - - @Test(expectedExceptions = IllegalArgumentException.class) - public void testThrowsIllegalArgumentExceptionWhenSkipCountIsNegative() throws Exception { - dao.getAll(1, -1); + List foundKeys = new ArrayList<>(); + for (SignatureKeyPairImpl expected : storedKeyPairs) { + foundKeys.add(dao.get(expected.getWorkspaceId())); + } + assertEquals(new HashSet<>(foundKeys), new HashSet<>(asList(storedKeyPairs))); + assertEquals(foundKeys.size(), COUNT_KEY_PAIRS); } @Test(expectedExceptions = ConflictException.class) public void throwsConflictExceptionWhenCreatingSignatureKeyPair() throws Exception { - final SignatureKeyPairImpl signKeyPair = newKeyPair(storedKeyPairs[0].getId()); + final SignatureKeyPairImpl signKeyPair = newKeyPair(storedKeyPairs[0].getWorkspaceId()); dao.create(signKeyPair); } + @Test(expectedExceptions = NotFoundException.class) + public void throwsNoResultExceptionWhenSearchingWrongWorkspace() throws Exception { + dao.get("unknown"); + } + @Test public void testCreatesSignatureKeyPair() throws Exception { final SignatureKeyPairImpl signKeyPair = newKeyPair("id_" + 10); dao.create(signKeyPair); - final Page keys = dao.getAll(COUNT_KEY_PAIRS + 1, 0); - assertTrue(keys.getItems().contains(signKeyPair)); - assertEquals(keys.getTotalItemsCount(), COUNT_KEY_PAIRS + 1); + final SignatureKeyPairImpl kp = dao.get(signKeyPair.getWorkspaceId()); + assertNotNull(kp); + assertEquals(kp, signKeyPair); } - @Test + @Test(expectedExceptions = NotFoundException.class) public void testRemovesSignatureKeyPair() throws Exception { final SignatureKeyPairImpl toRemove = storedKeyPairs[0]; - dao.remove(toRemove.getId()); + dao.remove(toRemove.getWorkspaceId()); - final Page keys = dao.getAll(COUNT_KEY_PAIRS, 0); - assertFalse(keys.getItems().contains(toRemove)); - assertEquals(keys.getTotalItemsCount(), COUNT_KEY_PAIRS - 1); + dao.get(toRemove.getWorkspaceId()); } - private SignatureKeyPairImpl newKeyPair(String id) { + private SignatureKeyPairImpl newKeyPair(String workspaceId) { final KeyPair pair = kpg.generateKeyPair(); - return new SignatureKeyPairImpl(id, pair.getPublic(), pair.getPrivate()); + return new SignatureKeyPairImpl(workspaceId, pair.getPublic(), pair.getPrivate()); } } diff --git a/wsmaster/che-core-sql-schema/src/main/resources/che-schema/6.10.0/2__change_signature_key_pair_id.sql b/wsmaster/che-core-sql-schema/src/main/resources/che-schema/6.10.0/2__change_signature_key_pair_id.sql new file mode 100644 index 0000000000..26328f1b2b --- /dev/null +++ b/wsmaster/che-core-sql-schema/src/main/resources/che-schema/6.10.0/2__change_signature_key_pair_id.sql @@ -0,0 +1,34 @@ +-- +-- 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 +-- + +-- Rename old table +ALTER TABLE che_sign_key_pair RENAME TO che_sign_key_pair_old; + +-- Create new key pair table +CREATE TABLE che_sign_key_pair ( + workspace_id VARCHAR(255) NOT NULL, + public_key BIGINT NOT NULL, + private_key BIGINT NOT NULL, + + PRIMARY KEY (workspace_id) +); +-- Constraint +ALTER TABLE che_sign_key_pair ADD CONSTRAINT fk_sign_workspace_id FOREIGN KEY (workspace_id) REFERENCES workspace (id); + +-- Copy data +INSERT INTO che_sign_key_pair +SELECT r.workspace_id, k.public_key, k.private_key +FROM che_k8s_runtime AS r, +(SELECT public_key, private_key FROM che_sign_key_pair_old LIMIT 1) AS k; + +-- Cleanup +DROP TABLE che_sign_key_pair_old CASCADE;