Allow creating OpenShift objects under the current user account on OCP (#9577)

* Support identity provider token retrieval in both JSON or URL formats.

That's required because some identity providers (such a `openshift-v3`)
correctly return the token information in JSON, as expected. So
switching to the url-based syntax should only used when the returned
json is invalid.

Signed-off-by: David Festal <dfestal@redhat.com>

* Introduce an `OpenShiftClientConfigFactory` to allow customizing the OpenShift config returned according to the current context (workspace ID, current user)

Signed-off-by: David Festal <dfestal@redhat.com>

* Openshift Infra + Multi-user => allow using OpenShift identity provider to connect to openshift with the OS oauth token of the current Che user.

This introduces a new property:

`che.infra.openshift.oauth_identity_provider`

Signed-off-by: David Festal <dfestal@redhat.com>

* Notify the user when a workspace cannot be started from the nav bar.

Signed-off-by: David Festal <dfestal@redhat.com>

* Add the ability to install the Openshift certificate into Keycloak

Signed-off-by: David Festal <dfestal@redhat.com>

* Add a yaml file to provide the openshift certificate as a secret,

in case it has to be installed into the dedicated Keycloak server.

Then the commands to install Che multiuser on Minishift with this
certificate are:

```
oc new-project che

oc process -f multi/openshift-certificate-secret.yaml -p
CERTIFICATE="$(minishift ssh docker exec origin /bin/cat
./openshift.local.config/master/ca.crt)" | oc apply -f -; \
oc new-app -f multi/postgres-template.yaml; \
oc new-app -f multi/keycloak-template.yaml -p ROUTING_SUFFIX=$(minishift
ip).nip.io; \
oc apply -f pvc/che-server-pvc.yaml; \
oc new-app -f che-server-template.yaml -p ROUTING_SUFFIX=$(minishift
ip).nip.io -p CHE_MULTIUSER=true -p
CHE_INFRA_OPENSHIFT_OAUTH__IDENTITY__PROVIDER=openshift-v3; \
oc set volume dc/che --add -m /data --name=che-data-volume
--claim-name=che-data-volume
```

Of course it's still needed to register the `openshift-v3` identity
provider in the Keycloak server, as well as, add the corresponding
`OAuthClient` object in Minihshift.

Signed-off-by: David Festal <dfestal@redhat.com>
6.19.x
David Festal 2018-05-28 11:39:44 +02:00 committed by GitHub
parent a2473aee75
commit e0890235fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 590 additions and 12 deletions

View File

@ -90,8 +90,10 @@ import org.eclipse.che.workspace.infrastructure.docker.DockerInfraModule;
import org.eclipse.che.workspace.infrastructure.docker.local.LocalDockerModule;
import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesInfraModule;
import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesInfrastructure;
import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftClientConfigFactory;
import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftInfraModule;
import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftInfrastructure;
import org.eclipse.che.workspace.infrastructure.openshift.multiuser.oauth.IdentityProviderConfigFactory;
import org.eclipse.persistence.config.PersistenceUnitProperties;
import org.flywaydb.core.internal.util.PlaceholderReplacer;
@ -301,6 +303,11 @@ public class WsMasterModule extends AbstractModule {
bind(WorkspaceStatusCache.class)
.to(org.eclipse.che.api.workspace.server.DefaultWorkspaceStatusCache.class);
}
if (OpenShiftInfrastructure.NAME.equals(infrastructure)) {
bind(OpenShiftClientConfigFactory.class).to(IdentityProviderConfigFactory.class);
}
persistenceProperties.put(
PersistenceUnitProperties.EXCEPTION_HANDLER_CLASS,
"org.eclipse.che.core.db.postgresql.jpa.eclipselink.PostgreSqlExceptionHandler");

View File

@ -96,6 +96,20 @@ che.organization.email.org_removed_template=st-html-templates/organization_delet
che.organization.email.org_renamed_subject=Che Organization renamed
che.organization.email.org_renamed_template=st-html-templates/organization_renamed
##### MULTI-USER-SPECIFIC OPENSHIFT INFRASTRUCTURE CONFIGURATION #####
# Alias of the Openshift identity provider registered in Keycloak,
# that should be used to create workspace OpenShift resources in
# Openshift namespaces owned by the current Che user.
#
# Should be set to NULL if `che.infra.openshift.project`
# is set to a non-empty value.
#
# For more information see the following documentation:
# https://www.keycloak.org/docs/3.3/server_admin/topics/identity-broker/social/openshift.html
che.infra.openshift.oauth_identity_provider=NULL
##### KEYCLOACK CONFIGURATION #####
# Url to keycloak identity provider server

View File

@ -13,6 +13,8 @@ import {CheWorkspace} from '../../../components/api/workspace/che-workspace.fact
import IdeSvc from '../../../app/ide/ide.service';
import {CheBranding} from '../../../components/branding/che-branding.factory';
import {WorkspacesService} from '../../workspaces/workspaces.service';
import {CheNotification} from '../../../components/notification/che-notification.factory';
const MAX_RECENT_WORKSPACES_ITEMS: number = 5;
@ -24,7 +26,7 @@ const MAX_RECENT_WORKSPACES_ITEMS: number = 5;
*/
export class NavbarRecentWorkspacesController {
static $inject = ['ideSvc', 'cheWorkspace', 'cheBranding', '$window', '$log', '$scope', '$rootScope', 'workspacesService'];
static $inject = ['ideSvc', 'cheWorkspace', 'cheBranding', '$window', '$log', '$scope', '$rootScope', 'workspacesService', 'cheNotification'];
cheWorkspace: CheWorkspace;
dropdownItemTempl: Array<any>;
@ -40,6 +42,8 @@ export class NavbarRecentWorkspacesController {
dropdownItems: Object;
workspaceCreationLink: string;
workspacesService: WorkspacesService;
cheNotification: CheNotification;
/**
* Default constructor
@ -51,7 +55,8 @@ export class NavbarRecentWorkspacesController {
$log: ng.ILogService,
$scope: ng.IScope,
$rootScope: ng.IRootScopeService,
workspacesService: WorkspacesService) {
workspacesService: WorkspacesService,
cheNotification: CheNotification) {
this.ideSvc = ideSvc;
this.cheWorkspace = cheWorkspace;
this.$log = $log;
@ -59,6 +64,7 @@ export class NavbarRecentWorkspacesController {
this.$rootScope = $rootScope;
this.workspaceCreationLink = cheBranding.getWorkspace().creationLink;
this.workspacesService = workspacesService;
this.cheNotification = cheNotification;
// workspace updated time map by id
this.workspaceUpdated = new Map();
@ -312,6 +318,7 @@ export class NavbarRecentWorkspacesController {
angular.noop();
}, (error: any) => {
this.$log.error(error);
this.cheNotification.showError('Stop workspace error.', error);
});
}
@ -325,6 +332,7 @@ export class NavbarRecentWorkspacesController {
this.updateRecentWorkspace(workspaceId);
this.cheWorkspace.startWorkspace(workspace.id, workspace.config.defaultEnv).catch((error: any) => {
this.$log.error(error);
this.cheNotification.showError('Run workspace error.', error);
});
}

View File

@ -238,3 +238,24 @@ IMPORTANT! Self-signed certificates aren't acceptable.
Default credentials are `admin:admin`.
Go to Clients, `che-public` client and edit **Valid Redirect URIs** and **Web Origins** URLs so that they use **https** protocol.
You do not need to do that if you initially deploy Che with https support.
## Creating workspace resources in personal OpenShift accounts
To allow creating workspace OpenShift resources in personal OpenShift accounts, you should:
- configure the Openshift identity provider in Keycloak as described in the Admin Guide
- install the Openshift console certificate in the Keycloak server (if it's self-signed) by:
- retrieve the OpenShift console certificate into the `~/openshift.crt` file with this command:
`minishift ssh docker exec origin /bin/cat ./openshift.local.config/master/ca.crt > ~/openshift.crt`
- running the following command before all other commands:
```bash
oc process -f multi/openshift-certificate-secret.yaml -p CERTIFICATE="$(cat ~/openshift.crt)" | oc apply -f -
```
- add the following parameters to the `oc new-app -f che-server-template.yaml` command:
```
-p CHE_INFRA_OPENSHIFT_PROJECT=NULL -p CHE_INFRA_OPENSHIFT_OAUTH__IDENTITY__PROVIDER=openshift-v3
```

View File

@ -115,6 +115,8 @@ objects:
value: "INFO"
- name: CHE_KEYCLOAK_AUTH__SERVER__URL
value: "${CHE_KEYCLOAK_AUTH__SERVER__URL}"
- name: CHE_INFRA_OPENSHIFT_OAUTH__IDENTITY__PROVIDER
value: "${CHE_INFRA_OPENSHIFT_OAUTH__IDENTITY__PROVIDER}"
- name: CHE_OAUTH_GITHUB_CLIENTID
value: "${CHE_OAUTH_GITHUB_CLIENTID}"
- name: CHE_OAUTH_GITHUB_CLIENTSECRET
@ -233,6 +235,10 @@ parameters:
displayName: Identity provider URL
description: URL of a remote identity provider. Defaults to Keycloak bundled with Che multi user
value: '${PROTOCOL}://keycloak-${NAMESPACE}.${ROUTING_SUFFIX}/auth'
- name: CHE_INFRA_OPENSHIFT_OAUTH__IDENTITY__PROVIDER
displayName: Alias of the Openshift identity provider in Keycloak
description: Alias of the Openshift identity provider registered in Keycloak, that should be used to create workspace OpenShift resources in Openshift namespaces owned by the current Che user.
value: 'NULL'
- name: STRATEGY
displayName: Update Strategy
description: Che server update strategy. Defaults to Recreate. Use Rolling only if Che deployment does not use PVC

View File

@ -53,6 +53,12 @@ objects:
value: "${ROUTING_SUFFIX}"
- name: CHE_KEYCLOAK_ADMIN_REQUIRE_UPDATE_PASSWORD
value: "${CHE_KEYCLOAK_ADMIN_REQUIRE_UPDATE_PASSWORD}"
- name: OPENSHIFT_IDENTITY_PROVIDER_CERTIFICATE
valueFrom:
secretKeyRef:
key: ca.crt
name: openshift-identity-provider
optional: true
image: '${IMAGE_KEYCLOAK}:${CHE_VERSION}'
command: ["/scripts/kc_realm_user.sh"]
imagePullPolicy: Always

View File

@ -0,0 +1,29 @@
# 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
#
---
kind: Template
apiVersion: v1
metadata:
name: openshift-identity-provider-certificate
annotations:
description: Che
objects:
- apiVersion: v1
stringData:
ca.crt: >-
${CERTIFICATE}
kind: Secret
metadata:
name: openshift-identity-provider
namespace: che
type: Opaque
parameters:
- name: CERTIFICATE
displayName: Openshift console certificate
labels:
app: keycloak
template: openshift-identity-provider-certificate

View File

@ -8,6 +8,8 @@
FROM jboss/keycloak-openshift:3.3.0.CR2-3
ADD che /opt/jboss/keycloak/themes/che
ADD . /scripts/
ADD cli /scripts/cli
USER root
RUN chgrp -R 0 /scripts && \
chmod -R g+rwX /scripts

View File

@ -0,0 +1,4 @@
embed-server --server-config=standalone.xml --std-out=echo
/subsystem=keycloak-server/spi=truststore/:add
/subsystem=keycloak-server/spi=truststore/provider=file/:add(properties={file => "/scripts/openshift.jks", password => "openshift", disabled => "false" },enabled=true)
stop-embedded-server

View File

@ -27,6 +27,13 @@ if [ $KEYCLOAK_USER ] && [ $KEYCLOAK_PASSWORD ]; then
/opt/jboss/keycloak/bin/add-user-keycloak.sh --user $KEYCLOAK_USER --password $KEYCLOAK_PASSWORD
fi
if [ "${OPENSHIFT_IDENTITY_PROVIDER_CERTIFICATE}" != "" ]; then
echo "${OPENSHIFT_IDENTITY_PROVIDER_CERTIFICATE}" > /scripts/openshift.cer
keytool -importcert -alias HOSTDOMAIN -keystore /scripts/openshift.jks -file /scripts/openshift.cer -storepass openshift -noprompt
keytool -importkeystore -srckeystore $JAVA_HOME/jre/lib/security/cacerts -destkeystore /scripts/openshift.jks -srcstorepass changeit -deststorepass openshift
/opt/jboss/keycloak/bin/jboss-cli.sh --file=/scripts/cli/add_openshift_certificate.cli && rm -rf /opt/jboss/keycloak/standalone/configuration/standalone_xml_history
fi
echo "Starting Keycloak server..."
/opt/jboss/keycloak/bin/standalone.sh -Dkeycloak.migration.action=import \

View File

@ -98,6 +98,14 @@
<groupId>org.eclipse.che.infrastructure.docker</groupId>
<artifactId>docker-environment</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.multiuser</groupId>
<artifactId>che-multiuser-keycloak-server</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.che.multiuser</groupId>
<artifactId>che-multiuser-keycloak-shared</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>

View File

@ -0,0 +1,35 @@
/*
* 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.openshift;
import io.fabric8.kubernetes.client.Config;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
import org.eclipse.che.commons.annotation.Nullable;
import org.eclipse.che.workspace.infrastructure.openshift.multiuser.oauth.IdentityProviderConfigFactory;
/**
* This class allows customizing the OpenShift {@link Config} returned by the {@link
* OpenShiftClientFactory} according to the current context (workspace ID, current user).
*
* @author David Festal
* @see IdentityProviderConfigFactory
*/
public class OpenShiftClientConfigFactory {
/**
* Builds the Openshift {@link Config} object based on a default {@link Config} object and an
* optional workspace Id.
*/
public Config buildConfig(Config defaultConfig, @Nullable String workspaceId)
throws InfrastructureException {
return defaultConfig;
}
}

View File

@ -56,8 +56,11 @@ public class OpenShiftClientFactory extends KubernetesClientFactory {
private static final Logger LOG = LoggerFactory.getLogger(OpenShiftClientFactory.class);
private final OpenShiftClientConfigFactory configBuilder;
@Inject
public OpenShiftClientFactory(
OpenShiftClientConfigFactory configBuilder,
@Nullable @Named("che.infra.kubernetes.master_url") String masterUrl,
@Nullable @Named("che.infra.kubernetes.username") String username,
@Nullable @Named("che.infra.kubernetes.password") String password,
@ -79,6 +82,7 @@ public class OpenShiftClientFactory extends KubernetesClientFactory {
maxConcurrentRequestsPerHost,
maxIdleConnections,
connectionPoolKeepAlive);
this.configBuilder = configBuilder;
}
protected Config buildDefaultConfig(
@ -108,6 +112,19 @@ public class OpenShiftClientFactory extends KubernetesClientFactory {
return theConfig;
}
/**
* Builds the Openshift {@link Config} object based on a default {@link Config} object and an
* optional workspace Id.
*
* <p>This method overrides the one in the Kubernetes infrastructure to introduce an additional
* extension level by delegating to an {@link OpenShiftClientConfigFactory}
*/
@Override
protected Config buildConfig(Config defaultConfig, @Nullable String workspaceId)
throws InfrastructureException {
return configBuilder.buildConfig(defaultConfig, workspaceId);
}
protected Interceptor buildKubernetesInterceptor(Config config) {
final String oauthToken;
if (Utils.isNotNullOrEmpty(config.getUsername())

View File

@ -0,0 +1,171 @@
/*
* 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.openshift.multiuser.oauth;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.AUTH_SERVER_URL_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.CLIENT_ID_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.REALM_SETTING;
import com.google.inject.Provider;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.openshift.client.OpenShiftConfig;
import io.fabric8.openshift.client.OpenShiftConfigBuilder;
import java.util.Optional;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.eclipse.che.api.core.BadRequestException;
import org.eclipse.che.api.core.UnauthorizedException;
import org.eclipse.che.api.workspace.server.WorkspaceRuntimes;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
import org.eclipse.che.api.workspace.server.spi.RuntimeContext;
import org.eclipse.che.commons.annotation.Nullable;
import org.eclipse.che.commons.env.EnvironmentContext;
import org.eclipse.che.commons.subject.Subject;
import org.eclipse.che.multiuser.keycloak.server.KeycloakServiceClient;
import org.eclipse.che.multiuser.keycloak.server.KeycloakSettings;
import org.eclipse.che.multiuser.keycloak.shared.dto.KeycloakTokenResponse;
import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftClientConfigFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* This class retrieves the OpenShift OAuth token of the current Che user, and injects it the
* OpenShift {@link Config} object, so that workspace OpenShift resources will be created under the
* OpenShift account of the current Che user.
*
* <p>The OpenShift OAuth token is retrieved using the OpenShift identity provider configured in the
* Keycloak server.
*
* <p>If the current user is not the user that started the current workspace (for operations such as
* idling, che server shutdown, etc ...), then global OpenShift infrastructure credentials defined
* in the Che properties will be used.
*
* @author David Festal
*/
@Singleton
public class IdentityProviderConfigFactory extends OpenShiftClientConfigFactory {
private static final Logger LOG = LoggerFactory.getLogger(IdentityProviderConfigFactory.class);
private final String oauthIdentityProvider;
private final KeycloakServiceClient keycloakServiceClient;
private final KeycloakSettings keycloakSettings;
private final Provider<WorkspaceRuntimes> workspaceRuntimeProvider;
private final String messageToLinkAccount;
private String rootUrl;
@Inject
public IdentityProviderConfigFactory(
KeycloakServiceClient keycloakServiceClient,
KeycloakSettings keycloakSettings,
Provider<WorkspaceRuntimes> workspaceRuntimeProvider,
@Nullable @Named("che.infra.openshift.oauth_identity_provider") String oauthIdentityProvider,
@Named("che.api") String apiEndpoint) {
super();
this.keycloakServiceClient = keycloakServiceClient;
this.keycloakSettings = keycloakSettings;
this.workspaceRuntimeProvider = workspaceRuntimeProvider;
this.oauthIdentityProvider = oauthIdentityProvider;
rootUrl = apiEndpoint;
if (rootUrl.endsWith("/")) {
rootUrl = rootUrl.substring(0, rootUrl.length() - 1);
}
if (rootUrl.endsWith("/api")) {
rootUrl = rootUrl.substring(0, rootUrl.length() - 4);
}
String referrer_uri =
rootUrl.replace("http://", "http%3A%2F%2F").replace("https://", "https%3A%2F%2F")
+ "%2Fdashboard%2F?redirect_fragment%3D%2Fworkspaces";
messageToLinkAccount =
"You should link your account with the <strong>"
+ oauthIdentityProvider
+ "</strong> \n"
+ "identity provider by visiting the "
+ "<a href='"
+ keycloakSettings.get().get(AUTH_SERVER_URL_SETTING)
+ "/realms/"
+ keycloakSettings.get().get(REALM_SETTING)
+ "/account/identity?referrer="
+ keycloakSettings.get().get(CLIENT_ID_SETTING)
+ "&referrer_uri="
+ referrer_uri
+ "' target='_blank' rel='noopener noreferrer'><strong>Federated Identities</strong></a> page of your Che account";
}
/**
* Builds the Openshift {@link Config} object based on a default {@link Config} object and an
* optional workspace Id.
*/
public Config buildConfig(Config defaultConfig, @Nullable String workspaceId)
throws InfrastructureException {
Subject subject = EnvironmentContext.getCurrent().getSubject();
String workspaceOwnerId = null;
if (workspaceId != null) {
@SuppressWarnings("rawtypes")
Optional<RuntimeContext> context =
workspaceRuntimeProvider.get().getRuntimeContext(workspaceId);
workspaceOwnerId = context.map(c -> c.getIdentity().getOwnerId()).orElse(null);
}
if (oauthIdentityProvider != null
&& subject != Subject.ANONYMOUS
&& (workspaceOwnerId == null || subject.getUserId().equals(workspaceOwnerId))) {
try {
KeycloakTokenResponse keycloakTokenInfos =
keycloakServiceClient.getIdentityProviderToken(oauthIdentityProvider);
if ("user:full".equals(keycloakTokenInfos.getScope())) {
return new OpenShiftConfigBuilder(OpenShiftConfig.wrap(defaultConfig))
.withOauthToken(keycloakTokenInfos.getAccessToken())
.build();
} else {
throw new InfrastructureException(
"Cannot retrieve user Openshift token: '"
+ oauthIdentityProvider
+ "' identity provider is not granted full rights: "
+ oauthIdentityProvider);
}
} catch (UnauthorizedException e) {
LOG.error("cannot retrieve User Openshift token from the identity provider", e);
throw new InfrastructureException(messageToLinkAccount);
} catch (BadRequestException e) {
LOG.error(
"cannot retrieve User Openshift token from the '"
+ oauthIdentityProvider
+ "' identity provider",
e);
if (e.getMessage().endsWith("Invalid token.")) {
throw new InfrastructureException(
"Your session has expired. \nPlease "
+ "<a href='javascript:location.reload();' target='_top'>"
+ "login"
+ "</a> to Che again to get access to your Openshift account");
}
throw new InfrastructureException(e.getMessage(), e);
} catch (Exception e) {
LOG.error(
"cannot retrieve User Openshift token from the '"
+ oauthIdentityProvider
+ "' identity provider",
e);
throw new InfrastructureException(e.getMessage(), e);
}
}
return defaultConfig;
}
}

View File

@ -0,0 +1,230 @@
/*
* 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.openshift.multiuser.oauth;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.AUTH_SERVER_URL_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.CLIENT_ID_SETTING;
import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.REALM_SETTING;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
import com.google.inject.Provider;
import io.fabric8.kubernetes.client.Config;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.eclipse.che.api.core.BadRequestException;
import org.eclipse.che.api.core.UnauthorizedException;
import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity;
import org.eclipse.che.api.core.server.dto.DtoServerImpls.ServiceErrorImpl;
import org.eclipse.che.api.workspace.server.WorkspaceRuntimes;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
import org.eclipse.che.api.workspace.server.spi.RuntimeContext;
import org.eclipse.che.commons.env.EnvironmentContext;
import org.eclipse.che.commons.subject.Subject;
import org.eclipse.che.multiuser.keycloak.server.KeycloakServiceClient;
import org.eclipse.che.multiuser.keycloak.server.KeycloakSettings;
import org.eclipse.che.multiuser.keycloak.shared.dto.KeycloakTokenResponse;
import org.mockito.Mock;
import org.mockito.testng.MockitoTestNGListener;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
/** @author David Festal */
@Listeners(MockitoTestNGListener.class)
public class IdentityProviderConfigBuilderTest {
private static final String PROVIDER = "openshift-v3";
private static final String THE_USER_ID = "a_user_id";
private static final String ANOTHER_USER_ID = "another_user_id";
private static final String A_WORKSPACE_ID = "workspace_id";
private static final String FULL_SCOPE = "user:full";
private static final String ACCESS_TOKEN = "accessToken";
private static final String AUTH_SERVER_URL = "http://keycloak.url/auth";
private static final String REALM = "realm";
private static final String CLIENT_ID = "clientId";
private static final String API_ENDPOINT = "http://che-host/api";
private static final String SHOULD_LINK_ERROR_MESSAGE =
"You should link your account with the <strong>"
+ PROVIDER
+ "</strong> \n"
+ "identity provider by visiting the "
+ "<a href='"
+ AUTH_SERVER_URL
+ "/realms/"
+ REALM
+ "/account/identity?referrer="
+ CLIENT_ID
+ "&referrer_uri="
+ "http%3A%2F%2Fche-host%2Fdashboard%2F?redirect_fragment%3D%2Fworkspaces"
+ "' target='_blank' rel='noopener noreferrer'><strong>Federated Identities</strong></a> page of your Che account";
private static final String SESSION_EXPIRED_MESSAGE =
"Your session has expired. \nPlease "
+ "<a href='javascript:location.reload();' target='_top'>"
+ "login"
+ "</a> to Che again to get access to your Openshift account";
private static final Map<String, String> keycloakSettingsMap = new HashMap<String, String>();
@Mock private KeycloakServiceClient keycloakServiceClient;
@Mock private KeycloakSettings keycloakSettings;
@Mock private Provider<WorkspaceRuntimes> workspaceRuntimeProvider;
@Mock private WorkspaceRuntimes workspaceRuntimes;
@Mock private Subject subject;
@Mock private RuntimeIdentity runtimeIdentity;
@SuppressWarnings("rawtypes")
@Mock
private RuntimeContext runtimeContext;
@Mock private KeycloakTokenResponse tokenResponse;
private EnvironmentContext context;
private IdentityProviderConfigFactory configBuilder;
private Config defaultConfig;
static {
keycloakSettingsMap.put(AUTH_SERVER_URL_SETTING, AUTH_SERVER_URL);
keycloakSettingsMap.put(REALM_SETTING, REALM);
keycloakSettingsMap.put(CLIENT_ID_SETTING, CLIENT_ID);
}
@BeforeMethod
public void setUp() throws Exception {
when(keycloakSettings.get()).thenReturn(keycloakSettingsMap);
context = spy(EnvironmentContext.getCurrent());
EnvironmentContext.setCurrent(context);
doReturn(subject).when(context).getSubject();
when(workspaceRuntimeProvider.get()).thenReturn(workspaceRuntimes);
when(workspaceRuntimes.getRuntimeContext(anyString()))
.thenReturn(Optional.<RuntimeContext>ofNullable(runtimeContext));
when(runtimeContext.getIdentity()).thenReturn(runtimeIdentity);
when(runtimeIdentity.getOwnerId()).thenReturn(THE_USER_ID);
when(subject.getUserId()).thenReturn(THE_USER_ID);
when(tokenResponse.getScope()).thenReturn(FULL_SCOPE);
when(tokenResponse.getAccessToken()).thenReturn(ACCESS_TOKEN);
configBuilder =
new IdentityProviderConfigFactory(
keycloakServiceClient,
keycloakSettings,
workspaceRuntimeProvider,
PROVIDER,
API_ENDPOINT);
defaultConfig = new io.fabric8.kubernetes.client.ConfigBuilder().build();
}
@Test
public void testFallbackToDefaultConfigWhenProvideIsNull() throws Exception {
when(keycloakServiceClient.getIdentityProviderToken(anyString())).thenReturn(tokenResponse);
configBuilder =
new IdentityProviderConfigFactory(
keycloakServiceClient, keycloakSettings, workspaceRuntimeProvider, null, API_ENDPOINT);
assertTrue(defaultConfig == configBuilder.buildConfig(defaultConfig, A_WORKSPACE_ID));
}
@Test
public void testFallbackToDefaultConfigWhenSubjectIsAnonymous() throws Exception {
when(keycloakServiceClient.getIdentityProviderToken(anyString())).thenReturn(tokenResponse);
doReturn(Subject.ANONYMOUS).when(context).getSubject();
assertTrue(defaultConfig == configBuilder.buildConfig(defaultConfig, A_WORKSPACE_ID));
}
@Test
public void testFallbackToDefaultConfigWhenCurrentUserIsDifferentFromWorkspaceOwner()
throws Exception {
when(keycloakServiceClient.getIdentityProviderToken(anyString())).thenReturn(tokenResponse);
when(runtimeIdentity.getOwnerId()).thenReturn(ANOTHER_USER_ID);
assertTrue(defaultConfig == configBuilder.buildConfig(defaultConfig, A_WORKSPACE_ID));
}
@SuppressWarnings("rawtypes")
@Test
public void testCreateUserConfigWhenNoRuntimeContext() throws Exception {
when(keycloakServiceClient.getIdentityProviderToken(anyString())).thenReturn(tokenResponse);
when(workspaceRuntimes.getRuntimeContext(anyString()))
.thenReturn(Optional.<RuntimeContext>empty());
Config resultConfig = configBuilder.buildConfig(defaultConfig, A_WORKSPACE_ID);
assertEquals(resultConfig.getOauthToken(), ACCESS_TOKEN);
}
@Test
public void testCreateUserConfigWhenWorkspaceIdIsNull() throws Exception {
when(keycloakServiceClient.getIdentityProviderToken(anyString())).thenReturn(tokenResponse);
Config resultConfig = configBuilder.buildConfig(defaultConfig, null);
assertEquals(resultConfig.getOauthToken(), ACCESS_TOKEN);
}
@Test
public void testCreateUserConfig() throws Exception {
when(keycloakServiceClient.getIdentityProviderToken(anyString())).thenReturn(tokenResponse);
Config resultConfig = configBuilder.buildConfig(defaultConfig, A_WORKSPACE_ID);
assertEquals(resultConfig.getOauthToken(), ACCESS_TOKEN);
}
@Test(expectedExceptions = {InfrastructureException.class})
public void testThrowOnBadScope() throws Exception {
when(keycloakServiceClient.getIdentityProviderToken(anyString())).thenReturn(tokenResponse);
when(tokenResponse.getScope()).thenReturn("bad:scope");
Config resultConfig = configBuilder.buildConfig(defaultConfig, A_WORKSPACE_ID);
assertEquals(resultConfig.getOauthToken(), ACCESS_TOKEN);
}
public void testRethrowOnUnauthorizedException() throws Exception {
doThrow(new UnauthorizedException(ServiceErrorImpl.make().withMessage("Any other message")))
.when(keycloakServiceClient)
.getIdentityProviderToken(anyString());
try {
configBuilder.buildConfig(defaultConfig, A_WORKSPACE_ID);
} catch (InfrastructureException e) {
assertEquals(e.getMessage(), SHOULD_LINK_ERROR_MESSAGE, "The exception message is wrong");
return;
}
fail(
"Should have thrown an exception with the following message: " + SHOULD_LINK_ERROR_MESSAGE);
}
@Test(expectedExceptions = {InfrastructureException.class})
public void testRethrowOnBadRequestException() throws Exception {
doThrow(new BadRequestException(ServiceErrorImpl.make().withMessage("Any other message")))
.when(keycloakServiceClient)
.getIdentityProviderToken(anyString());
configBuilder.buildConfig(defaultConfig, A_WORKSPACE_ID);
}
public void testRethrowOnInvalidTokenBadRequestException() throws Exception {
doThrow(new BadRequestException(ServiceErrorImpl.make().withMessage("Invalid token.")))
.when(keycloakServiceClient)
.getIdentityProviderToken(anyString());
try {
configBuilder.buildConfig(defaultConfig, A_WORKSPACE_ID);
} catch (InfrastructureException e) {
assertEquals(e.getMessage(), SESSION_EXPIRED_MESSAGE, "The exception message is wrong");
return;
}
fail("Should have thrown an exception with the following message: " + SESSION_EXPIRED_MESSAGE);
}
@Test(expectedExceptions = {InfrastructureException.class})
public void testRethrowOnAnyException() throws Exception {
when(keycloakServiceClient.getIdentityProviderToken(anyString())).thenThrow(Exception.class);
configBuilder.buildConfig(defaultConfig, A_WORKSPACE_ID);
}
}

View File

@ -15,6 +15,7 @@ import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.REALM_
import com.google.common.io.CharStreams;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.impl.DefaultClaims;
import java.io.IOException;
@ -79,7 +80,8 @@ public class KeycloakServiceClient {
* @param redirectAfterLogin URL to return after login
* @return URL to redirect client to perform account linking
*/
public String getAccountLinkingURL(Jwt token, String oauthProvider, String redirectAfterLogin) {
public String getAccountLinkingURL(
@SuppressWarnings("rawtypes") Jwt token, String oauthProvider, String redirectAfterLogin) {
DefaultClaims claims = (DefaultClaims) token.getBody();
final String clientId = claims.getAudience();
@ -208,15 +210,26 @@ public class KeycloakServiceClient {
}
}
/** Converts key=value&foo=bar string into json */
/** Converts key=value&foo=bar string into json if necessary */
private static String toJson(String source) {
Map<String, String> queryPairs = new HashMap<>();
Arrays.stream(source.split("&"))
.forEach(
p -> {
int delimiterIndex = p.indexOf("=");
queryPairs.put(p.substring(0, delimiterIndex), p.substring(delimiterIndex + 1));
});
return gson.toJson(queryPairs);
if (source == null) {
return null;
}
try {
// Check that the source is valid Json Object (can be returned as a Map)
gson.<Map<String, String>>fromJson(source, Map.class);
return source;
} catch (JsonSyntaxException notJsonException) {
// The source is not valid Json: let's see if
// it is in 'key=value&foo=bar' format
Map<String, String> queryPairs = new HashMap<>();
Arrays.stream(source.split("&"))
.forEach(
p -> {
int delimiterIndex = p.indexOf("=");
queryPairs.put(p.substring(0, delimiterIndex), p.substring(delimiterIndex + 1));
});
return gson.toJson(queryPairs);
}
}
}