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
parent
a2473aee75
commit
e0890235fa
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 \
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue