Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Support single-host mode on the multi-user server #14335

Merged
merged 13 commits into from
Sep 10, 2019
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions assembly/assembly-root-war/src/main/webapp/_app/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -278,10 +278,8 @@ class Loader {
* @param {string} token
*/
asyncAuthenticate(redirectUrl, token) {
const re = new RegExp(/(https?:\/\/[^\/]+?)(?:$|\/).*/),
// \ / \ /
// scheme host:port
url = redirectUrl.replace(re, "$1" + "/jwt/auth");
redirectUrl = new URL(redirectUrl);
const url = redirectUrl.origin + redirectUrl.pathname + "/jwt/auth";
return new Promise((resolve, reject) => {
const request = new XMLHttpRequest();
request.open('GET', url);
Expand All @@ -293,7 +291,7 @@ class Loader {
return;
}
if (request.status !== 204) {
const errorMessage = 'Failed to authenticate: "' + this.getRequestErrorMessage(xhr) + '"';
const errorMessage = 'Failed to authenticate: "' + this.getRequestErrorMessage(request) + '"';
reject(new Error(errorMessage));
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ public void expose(Map<String, ? extends ServerConfig> servers) throws Infrastru
Map<String, ServerConfig> matchedExternalServers = match(externalServers, servicePort);
if (!matchedExternalServers.isEmpty()) {
externalServerExposer.expose(
k8sEnv, machineName, serviceName, servicePort, matchedExternalServers);
k8sEnv, machineName, serviceName, serviceName, servicePort, matchedExternalServers);
}

// expose service port related secure servers if exist
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,22 +51,27 @@ public ExternalServerExposer(
* @param k8sEnv Kubernetes environment
* @param machineName machine containing servers
* @param serviceName service associated with machine, mapping all machine server ports
* @param pathBase the basis for the path to be used in the ingress - usually this should be the
* service name or the name of the proxy fronting the service
* @param servicePort specific service port to be exposed externally
* @param externalServers server configs of servers to be exposed externally
*/
public void expose(
T k8sEnv,
String machineName,
String serviceName,
String pathBase,
ServicePort servicePort,
Map<String, ServerConfig> externalServers) {
Ingress ingress = generateIngress(machineName, serviceName, servicePort, externalServers);
Ingress ingress =
generateIngress(machineName, serviceName, pathBase, servicePort, externalServers);
k8sEnv.getIngresses().put(ingress.getMetadata().getName(), ingress);
}

private Ingress generateIngress(
String machineName,
String serviceName,
String pathBase,
ServicePort servicePort,
Map<String, ServerConfig> ingressesServers) {

Expand All @@ -80,7 +85,7 @@ private Ingress generateIngress(
.withPath(
String.format(
pathTransformFmt,
ensureEndsWithSlash(strategy.getIngressPath(serviceName, servicePort))))
ensureEndsWithSlash(strategy.getIngressPath(pathBase, servicePort))))
.withName(getIngressName(serviceName, servicePort))
.withMachineName(machineName)
.withServiceName(serviceName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public void expose(
String serviceName,
ServicePort servicePort,
Map<String, ServerConfig> secureServers) {
exposer.expose(k8sEnv, machineName, serviceName, servicePort, secureServers);
exposer.expose(k8sEnv, machineName, serviceName, serviceName, servicePort, secureServers);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,36 @@ public JwtProxyConfigBuilder(
this.ttl = ttl;
}

/**
* Adds a proxy before a service that will perform the JWT authentication on its behalf.
*
* @param listenPort the port to listen on
* @param upstream the URL to the backend service this proxy should be put in front of
* @param excludes the list of unsecured paths that the proxy should let pass through
* @param cookiesAuthEnabled should the JWT proxy use cookies?
* @param cookiePath the path of the cookie. This is should either be "/" or some portion of the
* URL the JWT proxy will be exposed on. It is used to enable using different proxies for
* different services, each with a different auth cookie. Super useful for having multiple
* workspaces, each authenticated with its machine token.
* @param authErrorRedirectUriPrefix the prefix used to generate the redirect to the auth page.
metlos marked this conversation as resolved.
Show resolved Hide resolved
* This should be set to the a URL path where the JWT proxy can pick up the request to perform
* the auth
*/
public void addVerifierProxy(
Integer listenPort, String upstream, Set<String> excludes, Boolean cookiesAuthEnabled) {
verifierProxies.add(new VerifierProxy(listenPort, upstream, excludes, cookiesAuthEnabled));
Integer listenPort,
String upstream,
Set<String> excludes,
Boolean cookiesAuthEnabled,
String cookiePath,
String authErrorRedirectUriPrefix) {
verifierProxies.add(
new VerifierProxy(
listenPort,
upstream,
excludes,
cookiesAuthEnabled,
cookiePath,
authErrorRedirectUriPrefix));
}

public String build() throws InternalInfrastructureException {
Expand Down Expand Up @@ -104,6 +131,7 @@ public String build() throws InternalInfrastructureException {
"public_key_path",
JWT_PROXY_CONFIG_FOLDER + '/' + JWT_PROXY_PUBLIC_KEY_FILE)))
.withCookiesEnabled(verifierProxy.cookiesAuthEnabled)
.withCookiePath(ensureStartsWithSlash(verifierProxy.cookiePath))
.withClaimsVerifier(
Collections.singleton(
new RegistrableComponentConfig()
Expand All @@ -119,6 +147,10 @@ public String build() throws InternalInfrastructureException {
verifierConfig.setAuthUrl(authPageUrl.toString());
metlos marked this conversation as resolved.
Show resolved Hide resolved
}

if (verifierProxy.authErrorRedirectUriPrefix != null) {
verifierConfig.setAuthErrorRedirectUriPrefix(verifierProxy.authErrorRedirectUriPrefix);
}

VerifierProxyConfig proxyConfig =
new VerifierProxyConfig()
.withListenAddr(":" + verifierProxy.listenPort)
Expand All @@ -135,18 +167,35 @@ public String build() throws InternalInfrastructureException {
}
}

private class VerifierProxy {
private Integer listenPort;
private String upstream;
private Set<String> excludes;
private boolean cookiesAuthEnabled;
private static final class VerifierProxy {
final Integer listenPort;
final String upstream;
final Set<String> excludes;
final boolean cookiesAuthEnabled;
final String cookiePath;
final String authErrorRedirectUriPrefix;

VerifierProxy(
Integer listenPort, String upstream, Set<String> excludes, boolean cookiesAuthEnabled) {
Integer listenPort,
String upstream,
Set<String> excludes,
boolean cookiesAuthEnabled,
String cookiePath,
String authErrorRedirectUriPrefix) {
this.listenPort = listenPort;
this.upstream = upstream;
this.excludes = excludes;
this.cookiesAuthEnabled = cookiesAuthEnabled;
this.cookiePath = cookiePath;
this.authErrorRedirectUriPrefix = authErrorRedirectUriPrefix;
}
}

private static String ensureStartsWithSlash(String val) {
if (isNullOrEmpty(val)) {
return "/";
} else {
return val.charAt(0) == '/' ? val : "/" + val;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
import org.eclipse.che.workspace.infrastructure.kubernetes.Names;
import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment;
import org.eclipse.che.workspace.infrastructure.kubernetes.server.ServerServiceBuilder;
import org.eclipse.che.workspace.infrastructure.kubernetes.server.external.IngressServiceExposureStrategy;
import org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy.factory.JwtProxyConfigBuilderFactory;

/**
Expand Down Expand Up @@ -106,12 +107,16 @@ public class JwtProxyProvisioner {
private final Map<String, String> attributes;

private final String serviceName;
private final String pathBase;
private int availablePort;

private final IngressServiceExposureStrategy ingressServiceExposureStrategy;

@Inject
public JwtProxyProvisioner(
SignatureKeyManager signatureKeyManager,
JwtProxyConfigBuilderFactory jwtProxyConfigBuilderFactory,
IngressServiceExposureStrategy ingressServiceExposureStrategy,
@Named("che.server.secure_exposer.jwtproxy.image") String jwtProxyImage,
@Named("che.server.secure_exposer.jwtproxy.memory_limit") String memoryLimitBytes,
@Assisted RuntimeIdentity identity) {
Expand All @@ -120,8 +125,13 @@ public JwtProxyProvisioner(
this.jwtProxyImage = jwtProxyImage;

this.proxyConfigBuilder = jwtProxyConfigBuilderFactory.create(identity.getWorkspaceId());

this.ingressServiceExposureStrategy = ingressServiceExposureStrategy;

this.identity = identity;
this.serviceName = generate(SERVER_PREFIX, SERVER_UNIQUE_PART_SIZE) + "-jwtproxy";
this.pathBase = generate(serverPrefix(identity), SERVER_UNIQUE_PART_SIZE) + "-jwtproxy";

this.availablePort = FIRST_AVAILABLE_PORT;
long memoryLimitLong = Size.parseSizeToMegabytes(memoryLimitBytes) * MEGABYTES_TO_BYTES_DIVIDER;
this.attributes =
Expand All @@ -148,7 +158,7 @@ public JwtProxyProvisioner(
public ServicePort expose(
KubernetesEnvironment k8sEnv,
String backendServiceName,
int backendServicePort,
ServicePort backendServicePort,
String protocol,
Map<String, ServerConfig> secureServers)
throws InfrastructureException {
Expand Down Expand Up @@ -180,17 +190,6 @@ public ServicePort expose(
}
}

proxyConfigBuilder.addVerifierProxy(
listenPort,
"http://" + backendServiceName + ":" + backendServicePort,
excludes,
cookiesAuthEnabled);
k8sEnv
.getConfigMaps()
.get(getConfigMapName())
.getData()
.put(JWT_PROXY_CONFIG_FILE, proxyConfigBuilder.build());

ServicePort exposedPort =
new ServicePortBuilder()
.withName("server-" + listenPort)
Expand All @@ -199,7 +198,20 @@ public ServicePort expose(
.withNewTargetPort(listenPort)
.build();

k8sEnv.getServices().get(getServiceName()).getSpec().getPorts().add(exposedPort);
k8sEnv.getServices().get(serviceName).getSpec().getPorts().add(exposedPort);

proxyConfigBuilder.addVerifierProxy(
listenPort,
"http://" + backendServiceName + ":" + backendServicePort.getTargetPort().getIntVal(),
excludes,
cookiesAuthEnabled,
identity.getWorkspaceId(),
metlos marked this conversation as resolved.
Show resolved Hide resolved
ingressServiceExposureStrategy.getIngressPath(pathBase, exposedPort));
k8sEnv
.getConfigMaps()
.get(getConfigMapName())
.getData()
.put(JWT_PROXY_CONFIG_FILE, proxyConfigBuilder.build());

return exposedPort;
}
Expand All @@ -209,6 +221,11 @@ public String getServiceName() {
return serviceName;
}

/** Returns the path base to be used for the ingress backed by the JWTProxy pod. */
public String getPathBase() {
return pathBase;
}

/** Returns config map name that will be mounted into JWTProxy Pod. */
@VisibleForTesting
String getConfigMapName() {
Expand Down Expand Up @@ -288,4 +305,8 @@ private Pod createJwtProxyPod() {
.endSpec()
.build();
}

private static String serverPrefix(RuntimeIdentity identity) {
return identity.getWorkspaceId() + "/" + SERVER_PREFIX;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,14 @@ public void expose(
throws InfrastructureException {
ServicePort exposedServicePort =
proxyProvisioner.expose(
k8sEnv,
serviceName,
servicePort.getTargetPort().getIntVal(),
servicePort.getProtocol(),
secureServers);
k8sEnv, serviceName, servicePort, servicePort.getProtocol(), secureServers);

exposer.expose(
k8sEnv, machineName, proxyProvisioner.getServiceName(), exposedServicePort, secureServers);
k8sEnv,
machineName,
proxyProvisioner.getServiceName(),
proxyProvisioner.getPathBase(),
exposedServicePort,
secureServers);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ public class VerifierConfig {

private Set<String> excludes;

@JsonProperty("auth_error_redirect_uri_prefix")
private String authErrorRedirectUriPrefix;

@JsonProperty("cookie_path")
private String cookiePath;

public String getAudience() {
return audience;
}
Expand Down Expand Up @@ -180,4 +186,30 @@ public VerifierConfig withCookiesEnabled(boolean cookiesEnabled) {
this.cookiesEnabled = cookiesEnabled;
return this;
}

public String getAuthErrorRedirectUriPrefix() {
return authErrorRedirectUriPrefix;
}

public void setAuthErrorRedirectUriPrefix(String authErrorRedirectUriPrefix) {
this.authErrorRedirectUriPrefix = authErrorRedirectUriPrefix;
}

public VerifierConfig withAuthErrorRedirectUriPrefix(String authErrorRedirectUriPrefix) {
this.authErrorRedirectUriPrefix = authErrorRedirectUriPrefix;
return this;
}

public String getCookiePath() {
return cookiePath;
}

public void setCookiePath(String cookiePath) {
this.cookiePath = cookiePath;
}

public VerifierConfig withCookiePath(String cookiePath) {
this.cookiePath = cookiePath;
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ private void assertThatExternalServersAreExposed(
kubernetesEnvironment,
machineName,
service.getMetadata().getName(),
service.getMetadata().getName(),
servicePort,
expectedServers);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,12 @@ public void shouldCreateIngressForServer() {

// when
externalServerExposer.expose(
kubernetesEnvironment, MACHINE_NAME, SERVICE_NAME, servicePort, serversToExpose);
kubernetesEnvironment,
MACHINE_NAME,
SERVICE_NAME,
SERVICE_NAME,
servicePort,
serversToExpose);

// then
assertThatExternalServerIsExposed(
Expand Down Expand Up @@ -114,7 +119,12 @@ public void shouldCreateIngressForServerWhenTwoServersHasTheSamePort() {

// when
externalServerExposer.expose(
kubernetesEnvironment, MACHINE_NAME, SERVICE_NAME, servicePort, serversToExpose);
kubernetesEnvironment,
MACHINE_NAME,
SERVICE_NAME,
SERVICE_NAME,
servicePort,
serversToExpose);

// then
assertEquals(kubernetesEnvironment.getIngresses().size(), 1);
Expand Down
Loading