From 4b53f31f35e30ef366262934cf96c4ef8e044177 Mon Sep 17 00:00:00 2001 From: Sergii Leshchenko Date: Wed, 4 Jul 2018 16:51:51 +0300 Subject: [PATCH] Add JWTProxy based implementation for SecureServerExposer --- .../webapp/WEB-INF/classes/che/che.properties | 2 + infrastructures/kubernetes/pom.xml | 13 ++ .../kubernetes/KubernetesInfraModule.java | 7 + .../secure/SecureServerExposerFactory.java | 8 +- .../jwtproxy/JwtProxyConfigBuilder.java | 82 +++++++ .../secure/jwtproxy/JwtProxyProvisioner.java | 210 ++++++++++++++++++ .../jwtproxy/JwtProxySecureServerExposer.java | 72 ++++++ .../JwtProxySecureServerExposerFactory.java | 19 ++ .../jwtproxy/JwtProxyConfigBuilderTest.java | 47 ++++ .../jwtproxy/JwtProxyProvisionerTest.java | 116 ++++++++++ .../JwtProxySecureServerExposerTest.java | 87 ++++++++ .../src/test/resources/jwtproxy-confg.yaml | 40 ++++ .../openshift/OpenShiftInfraModule.java | 5 + 13 files changed, 707 insertions(+), 1 deletion(-) create mode 100644 infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxyConfigBuilder.java create mode 100644 infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxyProvisioner.java create mode 100644 infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxySecureServerExposer.java create mode 100644 infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxySecureServerExposerFactory.java create mode 100644 infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxyConfigBuilderTest.java create mode 100644 infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxyProvisionerTest.java create mode 100644 infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxySecureServerExposerTest.java create mode 100644 infrastructures/kubernetes/src/test/resources/jwtproxy-confg.yaml diff --git a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties index 168288800b97..47b70d7f15b4 100644 --- a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties +++ b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties @@ -494,4 +494,6 @@ che.workspace.feature.api=NULL # Suitable values: # - 'default': no additionally authentication system will be enabled. # So servers should authenticate requests themselves. +# - 'jwtproxy': jwtproxy will authenticate requests. +# So servers will receive only authenticated ones. che.agents.auth.secure_server_exposer=default diff --git a/infrastructures/kubernetes/pom.xml b/infrastructures/kubernetes/pom.xml index 84a0eaa1341a..c6181d81505b 100644 --- a/infrastructures/kubernetes/pom.xml +++ b/infrastructures/kubernetes/pom.xml @@ -122,6 +122,10 @@ org.eclipse.che.infrastructure.docker docker-environment + + org.eclipse.che.multiuser + che-multiuser-machine-authentication + org.eclipse.persistence javax.persistence @@ -215,6 +219,15 @@ + + com.mycila + license-maven-plugin + + + src/test/resources/jwtproxy-confg.yaml + + + diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInfraModule.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInfraModule.java index 66d2e35edd43..012cf8cc80cf 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInfraModule.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInfraModule.java @@ -51,6 +51,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.server.external.ExternalServerExposerStrategyProvider; import org.eclipse.che.workspace.infrastructure.kubernetes.server.external.MultiHostIngressExternalServerExposer; import org.eclipse.che.workspace.infrastructure.kubernetes.server.external.SingleHostIngressExternalServerExposer; +import org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy.JwtProxySecureServerExposerFactory; import org.eclipse.che.workspace.infrastructure.kubernetes.wsnext.KubernetesWorkspaceNextApplier; /** @author Sergii Leshchenko */ @@ -70,6 +71,12 @@ protected void configure() { install(new FactoryModuleBuilder().build(KubernetesRuntimeFactory.class)); install(new FactoryModuleBuilder().build(KubernetesBootstrapperFactory.class)); install(new FactoryModuleBuilder().build(StartSynchronizerFactory.class)); + + install( + new FactoryModuleBuilder() + .build( + new TypeLiteral>() {})); + bind(WorkspacePVCCleaner.class).asEagerSingleton(); bind(RemoveNamespaceOnWorkspaceRemove.class).asEagerSingleton(); diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/SecureServerExposerFactory.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/SecureServerExposerFactory.java index 8cceb295c6d8..c129adbe947a 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/SecureServerExposerFactory.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/SecureServerExposerFactory.java @@ -16,6 +16,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; import org.eclipse.che.workspace.infrastructure.kubernetes.server.external.ExternalServerExposerStrategy; import org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.SecureServerExposer.DefaultSecureServerExposer; +import org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy.JwtProxySecureServerExposerFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -27,15 +28,18 @@ public class SecureServerExposerFactory { private final String serverExposer; private final ExternalServerExposerStrategy exposerStrategy; + private final JwtProxySecureServerExposerFactory jwtProxySecureServerExposerFactory; @Inject public SecureServerExposerFactory( @Named("che.agents.auth_enabled") boolean agentsAuthEnabled, @Named("che.agents.auth.secure_server_exposer") String serverExposer, - ExternalServerExposerStrategy exposerStrategy) { + ExternalServerExposerStrategy exposerStrategy, + JwtProxySecureServerExposerFactory jwtProxySecureServerExposerFactory) { this.agentsAuthEnabled = agentsAuthEnabled; this.serverExposer = serverExposer; this.exposerStrategy = exposerStrategy; + this.jwtProxySecureServerExposerFactory = jwtProxySecureServerExposerFactory; } /** @@ -49,6 +53,8 @@ public SecureServerExposer create(RuntimeIdentity identity) { } switch (serverExposer) { + case "jwtproxy": + return jwtProxySecureServerExposerFactory.create(identity); case "default": return new DefaultSecureServerExposer<>(exposerStrategy); default: diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxyConfigBuilder.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxyConfigBuilder.java new file mode 100644 index 000000000000..d580bd88de33 --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxyConfigBuilder.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy; + +import static org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy.JwtProxyProvisioner.JWT_PROXY_CONFIG_FOLDER; +import static org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy.JwtProxyProvisioner.JWT_PROXY_PUBLIC_KEY_FILE; + +import java.util.ArrayList; +import java.util.List; + +/** + * Helps to build JWTProxy config with several verifier proxies. + * + * @author Sergii Leshchenko + */ +public class JwtProxyConfigBuilder { + private final List verifierProxies = new ArrayList<>(); + private final String workspaceId; + + public JwtProxyConfigBuilder(String workspaceId) { + this.workspaceId = workspaceId; + } + + public void addVerifierProxy(Integer listenPort, String upstream) { + verifierProxies.add(new VerifierProxy(listenPort, upstream)); + } + + public String build() { + StringBuilder configBuilder = new StringBuilder(); + + configBuilder.append("jwtproxy:\n" + " verifier_proxies:\n"); + for (VerifierProxy verifierProxy : verifierProxies) { + configBuilder.append( + String.format( + " - listen_addr: :%s\n" // :4471 + + " verifier:\n" + + " upstream: %s/\n" // http://localhost:4401 + + " audience: http://%s\n" + + " max_skew: 1m\n" + + " max_ttl: 3h\n" + + " key_server:\n" + + " type: preshared\n" + + " options:\n" + + " issuer: wsmaster\n" + + " key_id: mykey\n" + + " public_key_path: " + + JWT_PROXY_CONFIG_FOLDER + + "/" + + JWT_PROXY_PUBLIC_KEY_FILE + + "\n" + + " claims_verifiers:\n" + + " - type: static\n" + + " options:\n" + + " iss: wsmaster\n" + + " nonce_storage:\n" + + " type: void\n", + verifierProxy.listenPort, + verifierProxy.upstream, + workspaceId)); + } + configBuilder.append(" signer_proxy:\n" + " enabled: false\n"); + return configBuilder.toString(); + } + + private class VerifierProxy { + private Integer listenPort; + private String upstream; + + VerifierProxy(Integer listenPort, String upstream) { + this.listenPort = listenPort; + this.upstream = upstream; + } + } +} 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 new file mode 100644 index 000000000000..acab5bd7a06d --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxyProvisioner.java @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy; + +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static org.eclipse.che.commons.lang.NameGenerator.generate; +import static org.eclipse.che.workspace.infrastructure.kubernetes.Constants.CHE_ORIGINAL_NAME_LABEL; +import static org.eclipse.che.workspace.infrastructure.kubernetes.server.KubernetesServerExposer.SERVER_PREFIX; +import static org.eclipse.che.workspace.infrastructure.kubernetes.server.KubernetesServerExposer.SERVER_UNIQUE_PART_SIZE; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableMap; +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.ConfigMapBuilder; +import io.fabric8.kubernetes.api.model.ContainerBuilder; +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.PodBuilder; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServicePort; +import io.fabric8.kubernetes.api.model.ServicePortBuilder; +import io.fabric8.kubernetes.api.model.VolumeBuilder; +import io.fabric8.kubernetes.api.model.VolumeMount; +import java.security.KeyPair; +import java.util.HashMap; +import java.util.Map; +import org.eclipse.che.api.core.model.workspace.config.MachineConfig; +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.InternalInfrastructureException; +import org.eclipse.che.api.workspace.server.spi.environment.InternalMachineConfig; +import org.eclipse.che.multiuser.machine.authentication.server.signature.SignatureKeyManager; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; +import org.eclipse.che.workspace.infrastructure.kubernetes.server.ServerServiceBuilder; + +/** @author Sergii Leshchenko */ +public class JwtProxyProvisioner { + + static final int FIRST_AVAILABLE_PORT = 4400; + + static final int JWT_PROXY_MEMORY_LIMIT_BYTES = 128 * 1024 * 1024; // 128mb + + static final String PUBLIC_KEY_HEADER = "-----BEGIN PUBLIC KEY-----\n"; + static final String PUBLIC_KEY_FOOTER = "\n-----END PUBLIC KEY-----"; + + static final String JWTPROXY_IMAGE = "ksmster/jwtproxy"; + static final String JWT_PROXY_CONFIG_FILE = "config.yaml"; + static final String JWT_PROXY_MACHINE_NAME = "jwtproxy"; + + static final String JWT_PROXY_CONFIG_FOLDER = "/config"; + static final String JWT_PROXY_PUBLIC_KEY_FILE = "mykey.pub"; + + private final SignatureKeyManager signatureKeyManager; + + private final RuntimeIdentity identity; + + private final JwtProxyConfigBuilder proxyConfigBuilder; + + private final String serviceName; + private int availablePort; + + public JwtProxyProvisioner(RuntimeIdentity identity, SignatureKeyManager signatureKeyManager) { + this.signatureKeyManager = signatureKeyManager; + + this.identity = identity; + + this.proxyConfigBuilder = new JwtProxyConfigBuilder(identity.getWorkspaceId()); + + this.serviceName = generate(SERVER_PREFIX, SERVER_UNIQUE_PART_SIZE) + "-jwtproxy"; + this.availablePort = FIRST_AVAILABLE_PORT; + } + + /** + * Modifies Kubernetes environment to expose the specified service port via JWTProxy. + * + * @param k8sEnv Kubernetes environment to modify + * @param backendServiceName service name that will be exposed + * @param backendServicePort service port that will be exposed + * @param protocol protocol that will be used for exposed port + * @return JWTProxy service port that expose the specified one + * @throws InfrastructureException if any exception occurs during port exposing + */ + public ServicePort expose( + KubernetesEnvironment k8sEnv, + String backendServiceName, + int backendServicePort, + String protocol) + throws InfrastructureException { + ensureJwtProxyInjected(k8sEnv); + + int listenPort = availablePort++; + + proxyConfigBuilder.addVerifierProxy( + listenPort, "http://" + backendServiceName + ":" + backendServicePort); + k8sEnv + .getConfigMaps() + .get(getConfigMapName()) + .getData() + .put(JWT_PROXY_CONFIG_FILE, proxyConfigBuilder.build()); + + ServicePort exposedPort = + new ServicePortBuilder() + .withName(backendServiceName + "-" + listenPort) + .withPort(listenPort) + .withProtocol(protocol) + .withNewTargetPort(listenPort) + .build(); + + k8sEnv.getServices().get(getServiceName()).getSpec().getPorts().add(exposedPort); + + return exposedPort; + } + + /** Returns service name that exposed JWTProxy Pod. */ + public String getServiceName() { + return serviceName; + } + + /** Returns config map name that will be mounted into JWTProxy Pod. */ + @VisibleForTesting + String getConfigMapName() { + return "jwtproxy-config-" + identity.getWorkspaceId(); + } + + private void ensureJwtProxyInjected(KubernetesEnvironment k8sEnv) throws InfrastructureException { + if (!k8sEnv.getMachines().containsKey(JWT_PROXY_MACHINE_NAME)) { + k8sEnv.getMachines().put(JWT_PROXY_MACHINE_NAME, createJwtProxyMachine()); + k8sEnv.getPods().put("jwtproxy", createJwtProxyPod(identity)); + + KeyPair keyPair = signatureKeyManager.getKeyPair(); + if (keyPair == null) { + throw new InternalInfrastructureException( + "Key pair for machine authentication does not exist"); + } + Map initConfigMapData = new HashMap<>(); + initConfigMapData.put( + JWT_PROXY_PUBLIC_KEY_FILE, + PUBLIC_KEY_HEADER + + java.util.Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded()) + + PUBLIC_KEY_FOOTER); + + initConfigMapData.put(JWT_PROXY_CONFIG_FILE, proxyConfigBuilder.build()); + + ConfigMap jwtProxyConfigMap = + new ConfigMapBuilder() + .withNewMetadata() + .withName(getConfigMapName()) + .endMetadata() + .withData(initConfigMapData) + .build(); + k8sEnv.getConfigMaps().put(jwtProxyConfigMap.getMetadata().getName(), jwtProxyConfigMap); + + Service jwtProxyService = + new ServerServiceBuilder() + .withName(serviceName) + .withSelectorEntry(CHE_ORIGINAL_NAME_LABEL, JWT_PROXY_MACHINE_NAME) + .withMachineName(JWT_PROXY_MACHINE_NAME) + .withPorts(emptyList()) + .build(); + k8sEnv.getServices().put(jwtProxyService.getMetadata().getName(), jwtProxyService); + } + } + + private InternalMachineConfig createJwtProxyMachine() { + return new InternalMachineConfig( + null, + emptyMap(), + emptyMap(), + ImmutableMap.of( + MachineConfig.MEMORY_LIMIT_ATTRIBUTE, Integer.toString(JWT_PROXY_MEMORY_LIMIT_BYTES)), + null); + } + + private Pod createJwtProxyPod(RuntimeIdentity identity) { + return new PodBuilder() + .withNewMetadata() + .withName("jwtproxy") + .withAnnotations( + ImmutableMap.of( + "org.eclipse.che.container.verifier.machine_name", JWT_PROXY_MACHINE_NAME)) + .endMetadata() + .withNewSpec() + .withContainers( + new ContainerBuilder() + .withName("verifier") + .withImage(JWTPROXY_IMAGE) + .withVolumeMounts( + new VolumeMount( + JWT_PROXY_CONFIG_FOLDER + "/", "jwtproxy-config-volume", false, null)) + .withArgs("-config", JWT_PROXY_CONFIG_FOLDER + "/" + JWT_PROXY_CONFIG_FILE) + .build()) + .withVolumes( + new VolumeBuilder() + .withName("jwtproxy-config-volume") + .withNewConfigMap() + .withName("jwtproxy-config-" + identity.getWorkspaceId()) + .endConfigMap() + .build()) + .endSpec() + .build(); + } +} diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxySecureServerExposer.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxySecureServerExposer.java new file mode 100644 index 000000000000..aa2fbfc50b11 --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxySecureServerExposer.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy; + +import com.google.common.annotations.VisibleForTesting; +import com.google.inject.assistedinject.Assisted; +import io.fabric8.kubernetes.api.model.ServicePort; +import java.util.Map; +import javax.inject.Inject; +import org.eclipse.che.api.core.model.workspace.config.ServerConfig; +import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.multiuser.machine.authentication.server.signature.SignatureKeyManager; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; +import org.eclipse.che.workspace.infrastructure.kubernetes.server.external.ExternalServerExposerStrategy; +import org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.SecureServerExposer; + +/** + * Expose secure servers with JWTProxy. + * + * @author Sergii Leshchenko + */ +public class JwtProxySecureServerExposer + implements SecureServerExposer { + + private final ExternalServerExposerStrategy exposerStrategy; + private final JwtProxyProvisioner proxyProvisioner; + + @VisibleForTesting + JwtProxySecureServerExposer( + JwtProxyProvisioner jwtProxyProvisioner, ExternalServerExposerStrategy exposerStrategy) { + this.exposerStrategy = exposerStrategy; + this.proxyProvisioner = jwtProxyProvisioner; + } + + @Inject + public JwtProxySecureServerExposer( + @Assisted RuntimeIdentity identity, + SignatureKeyManager signatureKeyManager, + ExternalServerExposerStrategy exposerStrategy) { + this.exposerStrategy = exposerStrategy; + + proxyProvisioner = new JwtProxyProvisioner(identity, signatureKeyManager); + } + + @Override + public void expose( + T k8sEnv, + String machineName, + String serviceName, + ServicePort servicePort, + Map secureServers) + throws InfrastructureException { + ServicePort exposedServicePort = + proxyProvisioner.expose( + k8sEnv, + serviceName, + servicePort.getTargetPort().getIntVal(), + servicePort.getProtocol()); + + exposerStrategy.expose( + k8sEnv, machineName, proxyProvisioner.getServiceName(), exposedServicePort, secureServers); + } +} diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxySecureServerExposerFactory.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxySecureServerExposerFactory.java new file mode 100644 index 000000000000..b8d17c51dc3a --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxySecureServerExposerFactory.java @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy; + +import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; + +/** @author Sergii Leshchenko */ +public interface JwtProxySecureServerExposerFactory { + JwtProxySecureServerExposer create(RuntimeIdentity identity); +} diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxyConfigBuilderTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxyConfigBuilderTest.java new file mode 100644 index 000000000000..0334798a41fe --- /dev/null +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxyConfigBuilderTest.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy; + +import static org.testng.Assert.assertEquals; + +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.testng.reporters.Files; + +/** + * Tests {@link JwtProxyConfigBuilder}. + * + * @author Sergii Leshchenko + */ +public class JwtProxyConfigBuilderTest { + + private JwtProxyConfigBuilder jwtProxyConfigBuilder; + + @BeforeMethod + public void setUp() { + jwtProxyConfigBuilder = new JwtProxyConfigBuilder("workspace123"); + } + + @Test + public void shouldBuildJwtProxyConfigInYamlFormat() throws Exception { + // given + jwtProxyConfigBuilder.addVerifierProxy(8080, "http://tomcat:8080"); + jwtProxyConfigBuilder.addVerifierProxy(4101, "ws://terminal:4101"); + + // when + String jwtProxyConfigYaml = jwtProxyConfigBuilder.build(); + + // then + assertEquals( + jwtProxyConfigYaml, + Files.readFile(getClass().getClassLoader().getResourceAsStream("jwtproxy-confg.yaml"))); + } +} 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 new file mode 100644 index 000000000000..2fa60c93b73a --- /dev/null +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxyProvisionerTest.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy; + +import static org.eclipse.che.workspace.infrastructure.kubernetes.server.KubernetesServerExposer.SERVER_PREFIX; +import static org.eclipse.che.workspace.infrastructure.kubernetes.server.KubernetesServerExposer.SERVER_UNIQUE_PART_SIZE; +import static org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy.JwtProxyProvisioner.JWT_PROXY_CONFIG_FILE; +import static org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy.JwtProxyProvisioner.JWT_PROXY_PUBLIC_KEY_FILE; +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.Mockito.when; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +import io.fabric8.kubernetes.api.model.ConfigMap; +import io.fabric8.kubernetes.api.model.Pod; +import io.fabric8.kubernetes.api.model.Service; +import java.security.KeyPair; +import java.security.PublicKey; +import java.util.Base64; +import java.util.regex.Pattern; +import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity; +import org.eclipse.che.api.workspace.server.model.impl.RuntimeIdentityImpl; +import org.eclipse.che.api.workspace.server.spi.environment.InternalMachineConfig; +import org.eclipse.che.multiuser.machine.authentication.server.signature.SignatureKeyManager; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +/** + * Tests {@link JwtProxyProvisioner}. + * + * @author Sergii Leshchenko + */ +@Listeners(MockitoTestNGListener.class) +public class JwtProxyProvisionerTest { + + private static final String WORKSPACE_ID = "workspace123"; + private static final Pattern JWTPROXY_SERVICE_NAME_PATTERN = + Pattern.compile(SERVER_PREFIX + "\\w{" + SERVER_UNIQUE_PART_SIZE + "}-jwtproxy"); + private final RuntimeIdentity runtimeId = + new RuntimeIdentityImpl(WORKSPACE_ID, "env123", "owner123"); + + @Mock private SignatureKeyManager signatureKeyManager; + private KeyPair keyPair; + @Mock private PublicKey publicKey; + + private JwtProxyProvisioner jwtProxyProvisioner; + private KubernetesEnvironment k8sEnv; + + @BeforeMethod + public void setUp() { + keyPair = new KeyPair(publicKey, null); + when(signatureKeyManager.getKeyPair()).thenReturn(keyPair); + when(publicKey.getEncoded()).thenReturn("publickey".getBytes()); + + jwtProxyProvisioner = new JwtProxyProvisioner(runtimeId, signatureKeyManager); + k8sEnv = KubernetesEnvironment.builder().build(); + } + + @Test + public void shouldReturnGeneratedJwtProxyServiceName() { + // when + String jwtProxyServiceName = jwtProxyProvisioner.getServiceName(); + + // then + assertTrue(JWTPROXY_SERVICE_NAME_PATTERN.matcher(jwtProxyServiceName).matches()); + } + + @Test + public void shouldReturnGeneratedJwtProxyConfigMapName() { + // when + String jwtProxyConfigMap = jwtProxyProvisioner.getConfigMapName(); + + // then + assertEquals(jwtProxyConfigMap, "jwtproxy-config-" + WORKSPACE_ID); + } + + @Test + public void shouldProvisionJwtProxyRelatedObjectsIntoKubernetesEnvironment() throws Exception { + // when + jwtProxyProvisioner.expose(k8sEnv, "terminal", 4401, "TCP"); + + // then + InternalMachineConfig jwtProxyMachine = + k8sEnv.getMachines().get(JwtProxyProvisioner.JWT_PROXY_MACHINE_NAME); + assertNotNull(jwtProxyMachine); + + ConfigMap configMap = k8sEnv.getConfigMaps().get(jwtProxyProvisioner.getConfigMapName()); + assertNotNull(configMap); + assertEquals( + configMap.getData().get(JWT_PROXY_PUBLIC_KEY_FILE), + PUBLIC_KEY_HEADER + + Base64.getEncoder().encodeToString("publickey".getBytes()) + + PUBLIC_KEY_FOOTER); + assertNotNull(configMap.getData().get(JWT_PROXY_CONFIG_FILE)); + + Pod jwtProxyPod = k8sEnv.getPods().get("jwtproxy"); + assertNotNull(jwtProxyPod); + + Service jwtProxyService = k8sEnv.getServices().get(jwtProxyProvisioner.getServiceName()); + assertNotNull(jwtProxyService); + } +} diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxySecureServerExposerTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxySecureServerExposerTest.java new file mode 100644 index 000000000000..ba19af057a96 --- /dev/null +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxySecureServerExposerTest.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2012-2018 Red Hat, Inc. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.collect.ImmutableMap; +import io.fabric8.kubernetes.api.model.IntOrString; +import io.fabric8.kubernetes.api.model.ServicePort; +import java.util.Map; +import org.eclipse.che.api.core.model.workspace.config.ServerConfig; +import org.eclipse.che.api.workspace.server.model.impl.ServerConfigImpl; +import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment; +import org.eclipse.che.workspace.infrastructure.kubernetes.server.external.ExternalServerExposerStrategy; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +/** + * Tests {@link JwtProxySecureServerExposer} + * + * @author Sergii Leshchenko + */ +@Listeners(MockitoTestNGListener.class) +public class JwtProxySecureServerExposerTest { + + private static final String MACHINE_SERVICE_NAME = "service123"; + private static final String MACHINE_NAME = "machine123"; + public static final String JWT_PROXY_SERVICE_NAME = "jwtProxyServiceName"; + + @Mock private KubernetesEnvironment k8sEnv; + @Mock private JwtProxyProvisioner jwtProxyProvisioner; + @Mock private ExternalServerExposerStrategy externalServerExposer; + + private JwtProxySecureServerExposer secureServerExposer; + + @BeforeMethod + public void setUp() { + secureServerExposer = + new JwtProxySecureServerExposer<>(jwtProxyProvisioner, externalServerExposer); + } + + @Test + public void shouldExposeSecureServersWithNewJwtProxyServicePort() throws Exception { + // given + ServicePort machineServicePort = new ServicePort(); + machineServicePort.setTargetPort(new IntOrString(8080)); + machineServicePort.setProtocol("TCP"); + Map servers = + ImmutableMap.of( + "server1", + new ServerConfigImpl("8080/tcp", "http", "/api", ImmutableMap.of("secure", "true")), + "server2", + new ServerConfigImpl("8080/tcp", "ws", "/connect", ImmutableMap.of("secure", "true"))); + + ServicePort jwtProxyServicePort = new ServicePort(); + doReturn(jwtProxyServicePort) + .when(jwtProxyProvisioner) + .expose(any(), anyString(), anyInt(), anyString()); + + when(jwtProxyProvisioner.getServiceName()).thenReturn(JWT_PROXY_SERVICE_NAME); + + // when + secureServerExposer.expose( + k8sEnv, MACHINE_NAME, MACHINE_SERVICE_NAME, machineServicePort, servers); + + // then + verify(jwtProxyProvisioner).expose(k8sEnv, MACHINE_SERVICE_NAME, 8080, "TCP"); + verify(externalServerExposer) + .expose(k8sEnv, MACHINE_NAME, JWT_PROXY_SERVICE_NAME, jwtProxyServicePort, servers); + } +} diff --git a/infrastructures/kubernetes/src/test/resources/jwtproxy-confg.yaml b/infrastructures/kubernetes/src/test/resources/jwtproxy-confg.yaml new file mode 100644 index 000000000000..a6f83c5c2401 --- /dev/null +++ b/infrastructures/kubernetes/src/test/resources/jwtproxy-confg.yaml @@ -0,0 +1,40 @@ +jwtproxy: + verifier_proxies: + - listen_addr: :8080 + verifier: + upstream: http://tomcat:8080/ + audience: http://workspace123 + max_skew: 1m + max_ttl: 3h + key_server: + type: preshared + options: + issuer: wsmaster + key_id: mykey + public_key_path: /config/mykey.pub + claims_verifiers: + - type: static + options: + iss: wsmaster + nonce_storage: + type: void + - listen_addr: :4101 + verifier: + upstream: ws://terminal:4101/ + audience: http://workspace123 + max_skew: 1m + max_ttl: 3h + key_server: + type: preshared + options: + issuer: wsmaster + key_id: mykey + public_key_path: /config/mykey.pub + claims_verifiers: + - type: static + options: + iss: wsmaster + nonce_storage: + type: void + signer_proxy: + enabled: false diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInfraModule.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInfraModule.java index 99bdc580c9d1..2aac70ec95c4 100644 --- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInfraModule.java +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInfraModule.java @@ -42,6 +42,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.provision.env.LogsRootEnvVariableProvider; import org.eclipse.che.workspace.infrastructure.kubernetes.provision.server.ServersConverter; import org.eclipse.che.workspace.infrastructure.kubernetes.server.external.ExternalServerExposerStrategy; +import org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy.JwtProxySecureServerExposerFactory; import org.eclipse.che.workspace.infrastructure.kubernetes.wsnext.KubernetesWorkspaceNextApplier; import org.eclipse.che.workspace.infrastructure.openshift.environment.OpenShiftEnvironment; import org.eclipse.che.workspace.infrastructure.openshift.environment.OpenShiftEnvironmentFactory; @@ -67,6 +68,10 @@ protected void configure() { install(new FactoryModuleBuilder().build(OpenShiftRuntimeFactory.class)); install(new FactoryModuleBuilder().build(StartSynchronizerFactory.class)); + install( + new FactoryModuleBuilder() + .build(new TypeLiteral>() {})); + install(new FactoryModuleBuilder().build(KubernetesBootstrapperFactory.class)); bind(WorkspacePVCCleaner.class).asEagerSingleton(); bind(RemoveProjectOnWorkspaceRemove.class).asEagerSingleton();