keycloak-aplcache

Merge pull request #3911 from almighty/oso_provider [KEYCLOAK-4528]

3/25/2017 8:25:44 PM

Details

diff --git a/services/src/main/java/org/keycloak/social/openshift/OpenshiftV3IdentityProvider.java b/services/src/main/java/org/keycloak/social/openshift/OpenshiftV3IdentityProvider.java
new file mode 100644
index 0000000..8bc4bd5
--- /dev/null
+++ b/services/src/main/java/org/keycloak/social/openshift/OpenshiftV3IdentityProvider.java
@@ -0,0 +1,66 @@
+package org.keycloak.social.openshift;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import org.keycloak.broker.oidc.AbstractOAuth2IdentityProvider;
+import org.keycloak.broker.oidc.mappers.AbstractJsonUserAttributeMapper;
+import org.keycloak.broker.oidc.util.JsonSimpleHttp;
+import org.keycloak.broker.provider.BrokeredIdentityContext;
+import org.keycloak.broker.provider.IdentityBrokerException;
+import org.keycloak.broker.provider.util.SimpleHttp;
+import org.keycloak.broker.social.SocialIdentityProvider;
+import org.keycloak.models.KeycloakSession;
+
+import java.io.IOException;
+import java.util.Optional;
+
+/**
+ * Identity provider for Openshift V3. Check <a href="https://docs.openshift.com/enterprise/3.0/architecture/additional_concepts/authentication.html">official documentation</a> for more details.
+ */
+public class OpenshiftV3IdentityProvider extends AbstractOAuth2IdentityProvider<OpenshifV3IdentityProviderConfig> implements SocialIdentityProvider<OpenshifV3IdentityProviderConfig> {
+
+    public static final String BASE_URL = "https://api.preview.openshift.com";
+    private static final String AUTH_RESOURCE = "/oauth/authorize";
+    private static final String TOKEN_RESOURCE = "/oauth/token";
+    private static final String PROFILE_RESOURCE = "/oapi/v1/users/~";
+    private static final String DEFAULT_SCOPE = "user:info";
+
+    public OpenshiftV3IdentityProvider(KeycloakSession session, OpenshifV3IdentityProviderConfig config) {
+        super(session, config);
+        final String baseUrl = Optional.ofNullable(config.getBaseUrl()).orElse(BASE_URL);
+        config.setAuthorizationUrl(baseUrl + AUTH_RESOURCE);
+        config.setTokenUrl(baseUrl + TOKEN_RESOURCE);
+        config.setUserInfoUrl(baseUrl + PROFILE_RESOURCE);
+    }
+
+    @Override
+    protected String getDefaultScopes() {
+        return DEFAULT_SCOPE;
+    }
+
+    @Override
+    protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) {
+        try {
+            final JsonNode profile = fetchProfile(accessToken);
+            final BrokeredIdentityContext user = extractUserContext(profile.get("metadata"));
+            AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias());
+            return user;
+        } catch (Exception e) {
+            throw new IdentityBrokerException("Could not obtain user profile from Openshift.", e);
+        }
+    }
+
+    private BrokeredIdentityContext extractUserContext(JsonNode metadata) {
+        final BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(metadata, "uid"));
+        user.setUsername(getJsonProperty(metadata, "name"));
+        user.setName(getJsonProperty(metadata, "fullName"));
+        user.setIdpConfig(getConfig());
+        user.setIdp(this);
+        return user;
+    }
+
+    private JsonNode fetchProfile(String accessToken) throws IOException {
+        return JsonSimpleHttp.asJson(SimpleHttp.doGet(getConfig().getUserInfoUrl(), this.session)
+                             .header("Authorization", "Bearer " + accessToken));
+    }
+
+}
diff --git a/services/src/main/java/org/keycloak/social/openshift/OpenshiftV3IdentityProviderFactory.java b/services/src/main/java/org/keycloak/social/openshift/OpenshiftV3IdentityProviderFactory.java
new file mode 100644
index 0000000..b370530
--- /dev/null
+++ b/services/src/main/java/org/keycloak/social/openshift/OpenshiftV3IdentityProviderFactory.java
@@ -0,0 +1,27 @@
+package org.keycloak.social.openshift;
+
+import org.keycloak.broker.provider.AbstractIdentityProviderFactory;
+import org.keycloak.broker.social.SocialIdentityProviderFactory;
+import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.models.KeycloakSession;
+
+public class OpenshiftV3IdentityProviderFactory extends AbstractIdentityProviderFactory<OpenshiftV3IdentityProvider> implements SocialIdentityProviderFactory<OpenshiftV3IdentityProvider> {
+
+    public static final String PROVIDER_ID = "openshift-v3";
+
+    @Override
+    public String getName() {
+        return "Openshift v3";
+    }
+
+    @Override
+    public OpenshiftV3IdentityProvider create(KeycloakSession keycloakSession, IdentityProviderModel identityProviderModel) {
+        return new OpenshiftV3IdentityProvider(keycloakSession, new OpenshifV3IdentityProviderConfig(identityProviderModel));
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+
+}
diff --git a/services/src/main/java/org/keycloak/social/openshift/OpenshifV3IdentityProviderConfig.java b/services/src/main/java/org/keycloak/social/openshift/OpenshifV3IdentityProviderConfig.java
new file mode 100644
index 0000000..cd931f8
--- /dev/null
+++ b/services/src/main/java/org/keycloak/social/openshift/OpenshifV3IdentityProviderConfig.java
@@ -0,0 +1,27 @@
+package org.keycloak.social.openshift;
+
+import org.keycloak.broker.oidc.OAuth2IdentityProviderConfig;
+import org.keycloak.models.IdentityProviderModel;
+
+public class OpenshifV3IdentityProviderConfig extends OAuth2IdentityProviderConfig {
+    private static final String BASE_URL = "baseUrl";
+
+    public OpenshifV3IdentityProviderConfig(IdentityProviderModel identityProviderModel) {
+        super(identityProviderModel);
+    }
+
+    public String getBaseUrl() {
+        return getConfig().get(BASE_URL);
+    }
+
+    public void setBaseUrl(String baseUrl) {
+        getConfig().put(BASE_URL, trimTrailingSlash(baseUrl));
+    }
+
+    private String trimTrailingSlash(String baseUrl) {
+        if (baseUrl != null && baseUrl.endsWith("/")) {
+            baseUrl = baseUrl.substring(0, baseUrl.length() - 1);
+        }
+        return baseUrl;
+    }
+}
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.broker.social.SocialIdentityProviderFactory b/services/src/main/resources/META-INF/services/org.keycloak.broker.social.SocialIdentityProviderFactory
index 1311132..00a5e51 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.broker.social.SocialIdentityProviderFactory
+++ b/services/src/main/resources/META-INF/services/org.keycloak.broker.social.SocialIdentityProviderFactory
@@ -22,3 +22,4 @@ org.keycloak.social.linkedin.LinkedInIdentityProviderFactory
 org.keycloak.social.stackoverflow.StackoverflowIdentityProviderFactory
 org.keycloak.social.twitter.TwitterIdentityProviderFactory
 org.keycloak.social.microsoft.MicrosoftIdentityProviderFactory
+org.keycloak.social.openshift.OpenshiftV3IdentityProviderFactory
diff --git a/services/src/test/java/org/keycloak/social/openshift/OpenshiftV3IdentityProviderTest.java b/services/src/test/java/org/keycloak/social/openshift/OpenshiftV3IdentityProviderTest.java
new file mode 100644
index 0000000..e39f157
--- /dev/null
+++ b/services/src/test/java/org/keycloak/social/openshift/OpenshiftV3IdentityProviderTest.java
@@ -0,0 +1,33 @@
+package org.keycloak.social.openshift;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.keycloak.models.IdentityProviderModel;
+
+public class OpenshiftV3IdentityProviderTest {
+
+    @Test
+    public void shouldConstructProviderUrls() throws Exception {
+        final OpenshifV3IdentityProviderConfig config = new OpenshifV3IdentityProviderConfig(new IdentityProviderModel());
+        config.setBaseUrl("http://openshift.io:8443");
+        final OpenshiftV3IdentityProvider openshiftV3IdentityProvider = new OpenshiftV3IdentityProvider(null, config);
+
+        assertConfiguredUrls(openshiftV3IdentityProvider);
+    }
+
+    @Test
+    public void shouldConstructProviderUrlsForBaseUrlWithTrailingSlash() throws Exception {
+        final OpenshifV3IdentityProviderConfig config = new OpenshifV3IdentityProviderConfig(new IdentityProviderModel());
+        config.setBaseUrl("http://openshift.io:8443/");
+        final OpenshiftV3IdentityProvider openshiftV3IdentityProvider = new OpenshiftV3IdentityProvider(null, config);
+
+        assertConfiguredUrls(openshiftV3IdentityProvider);
+    }
+
+    private void assertConfiguredUrls(OpenshiftV3IdentityProvider openshiftV3IdentityProvider) {
+        Assert.assertEquals("http://openshift.io:8443/oauth/authorize", openshiftV3IdentityProvider.getConfig().getAuthorizationUrl());
+        Assert.assertEquals("http://openshift.io:8443/oauth/token", openshiftV3IdentityProvider.getConfig().getTokenUrl());
+        Assert.assertEquals("http://openshift.io:8443/oapi/v1/users/~", openshiftV3IdentityProvider.getConfig().getUserInfoUrl());
+    }
+
+}
\ No newline at end of file
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java
index 09ddb39..1fb0a63 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java
@@ -37,6 +37,9 @@ import org.keycloak.social.google.GoogleIdentityProvider;
 import org.keycloak.social.google.GoogleIdentityProviderFactory;
 import org.keycloak.social.linkedin.LinkedInIdentityProvider;
 import org.keycloak.social.linkedin.LinkedInIdentityProviderFactory;
+import org.keycloak.social.openshift.OpenshifV3IdentityProviderConfig;
+import org.keycloak.social.openshift.OpenshiftV3IdentityProvider;
+import org.keycloak.social.openshift.OpenshiftV3IdentityProviderFactory;
 import org.keycloak.social.stackoverflow.StackOverflowIdentityProviderConfig;
 import org.keycloak.social.stackoverflow.StackoverflowIdentityProvider;
 import org.keycloak.social.stackoverflow.StackoverflowIdentityProviderFactory;
@@ -146,6 +149,8 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
                     assertLinkedInIdentityProviderConfig(identityProvider);
                 } else if (StackoverflowIdentityProviderFactory.PROVIDER_ID.equals(providerId)) {
                     assertStackoverflowIdentityProviderConfig(identityProvider);
+                } else if (OpenshiftV3IdentityProviderFactory.PROVIDER_ID.equals(providerId)) {
+                    assertOpenshiftIdentityProviderConfig(identityProvider);
                 } else {
                     continue;
                 }
@@ -283,6 +288,21 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
         assertEquals(StackoverflowIdentityProvider.PROFILE_URL, config.getUserInfoUrl());
     }
 
+    private void assertOpenshiftIdentityProviderConfig(IdentityProviderModel identityProvider) {
+        OpenshiftV3IdentityProvider osoIdentityProvider = new OpenshiftV3IdentityProviderFactory().create(session, identityProvider);
+        OpenshifV3IdentityProviderConfig config = osoIdentityProvider.getConfig();
+
+        assertEquals("model-openshift-v3", config.getAlias());
+        assertEquals(OpenshiftV3IdentityProviderFactory.PROVIDER_ID, config.getProviderId());
+        assertEquals(true, config.isEnabled());
+        assertEquals(false, config.isTrustEmail());
+        assertEquals(false, config.isAuthenticateByDefault());
+        assertEquals(true, config.isStoreToken());
+        assertEquals(OpenshiftV3IdentityProvider.BASE_URL, config.getBaseUrl());
+        assertEquals("clientId", config.getClientId());
+        assertEquals("clientSecret", config.getClientSecret());
+    }
+
     private void assertTwitterIdentityProviderConfig(IdentityProviderModel identityProvider) {
         TwitterIdentityProvider twitterIdentityProvider = new TwitterIdentityProviderFactory().create(session, identityProvider);
         OAuth2IdentityProviderConfig config = twitterIdentityProvider.getConfig();
diff --git a/testsuite/integration/src/test/resources/broker-test/test-realm-with-broker.json b/testsuite/integration/src/test/resources/broker-test/test-realm-with-broker.json
index 1bfc295..09fa373 100755
--- a/testsuite/integration/src/test/resources/broker-test/test-realm-with-broker.json
+++ b/testsuite/integration/src/test/resources/broker-test/test-realm-with-broker.json
@@ -92,6 +92,20 @@
             }
         },
         {
+            "alias" : "model-openshift-v3",
+            "providerId" : "openshift-v3",
+            "enabled": true,
+            "storeToken": true,
+            "config": {
+                "baseUrl": "https://api.preview.openshift.com",
+                "authorizationUrl": "authorizationUrl",
+                "tokenUrl": "tokenUrl",
+                "userInfoUrl": "userInfoUrl",
+                "clientId": "clientId",
+                "clientSecret": "clientSecret"
+            }
+        },
+        {
             "alias" : "model-saml-signed-idp",
             "providerId" : "saml",
             "displayName": "My SAML",
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SocialLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SocialLoginTest.java
index 6ac3970..46c856e 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SocialLoginTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/SocialLoginTest.java
@@ -2,15 +2,12 @@ package org.keycloak.testsuite.broker;
 
 import org.jboss.arquillian.graphene.Graphene;
 import org.jboss.arquillian.graphene.page.Page;
-import org.jboss.arquillian.graphene.wait.WebDriverWait;
-import org.junit.Assume;
 import org.junit.BeforeClass;
 import org.junit.Test;
-import org.keycloak.common.Profile;
 import org.keycloak.representations.idm.IdentityProviderRepresentation;
 import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.social.openshift.OpenshiftV3IdentityProvider;
 import org.keycloak.testsuite.AbstractKeycloakTest;
-import org.keycloak.testsuite.cli.exec.ExecutionException;
 import org.keycloak.testsuite.pages.AccountUpdateProfilePage;
 import org.keycloak.testsuite.pages.LoginPage;
 import org.keycloak.testsuite.pages.LoginUpdateProfilePage;
@@ -59,6 +56,7 @@ public class SocialLoginTest extends AbstractKeycloakTest {
         List<IdentityProviderRepresentation> idps = new LinkedList<>();
         rep.setIdentityProviders(idps);
 
+        idps.add(buildIdp("openshift-v3"));
         idps.add(buildIdp("google"));
         idps.add(buildIdp("facebook"));
         idps.add(buildIdp("github"));
@@ -71,6 +69,22 @@ public class SocialLoginTest extends AbstractKeycloakTest {
     }
 
     @Test
+    public void openshiftLogin() throws Exception {
+        account.open("social");
+        loginPage.clickSocial("openshift-v3");
+
+        Graphene.waitGui().until(ExpectedConditions.visibilityOfElementLocated(By.id("inputUsername")));
+        driver.findElement(By.id("inputUsername")).sendKeys(config.getProperty("openshift-v3.username", config.getProperty("common.username")));
+        driver.findElement(By.id("inputPassword")).sendKeys(config.getProperty("openshift-v3.password", config.getProperty("common.password")));
+        driver.findElement(By.cssSelector("button[type=submit]")).click();
+
+        Graphene.waitGui().until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector("input[name=approve]")));
+        driver.findElement(By.cssSelector("input[name=approve]")).click();
+
+        assertEquals(config.getProperty("openshift-v3.username", config.getProperty("common.profile.username")), account.getUsername());
+    }
+
+    @Test
     public void googleLogin() throws InterruptedException {
         account.open("social");
 
@@ -226,6 +240,9 @@ public class SocialLoginTest extends AbstractKeycloakTest {
         if (id.equals("stackoverflow")) {
             idp.getConfig().put("key", config.getProperty(id + ".clientKey"));
         }
+        if (id.equals("openshift-v3")) {
+            idp.getConfig().put("baseUrl", config.getProperty(id + ".baseUrl", OpenshiftV3IdentityProvider.BASE_URL));
+        }
         return idp;
     }
 
diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
index 0859504..97b2808 100644
--- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
+++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties
@@ -547,6 +547,8 @@ social.client-secret.tooltip=The client secret registered with the identity prov
 social.default-scopes.tooltip=The scopes to be sent when asking for authorization. See documentation for possible values, separator and default value'.
 key=Key
 stackoverflow.key.tooltip=The Key obtained from Stack Overflow client registration.
+openshift.base-url=Base Url
+openshift.base-url.tooltip=Base Url to Openshift Online API
 
 # User federation
 sync-ldap-roles-to-keycloak=Sync LDAP Roles To Keycloak
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-openshift-v3.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-openshift-v3.html
new file mode 100755
index 0000000..a4630ac
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-openshift-v3.html
@@ -0,0 +1 @@
+<div data-ng-include data-src="resourceUrl + '/partials/realm-identity-provider-social.html'"></div>
\ No newline at end of file
diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-openshift-v3-ext.html b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-openshift-v3-ext.html
new file mode 100644
index 0000000..b1c27de
--- /dev/null
+++ b/themes/src/main/resources/theme/base/admin/resources/partials/realm-identity-provider-openshift-v3-ext.html
@@ -0,0 +1,7 @@
+<div class="form-group clearfix">
+    <label class="col-md-2 control-label" for="baseUrl"><span class="required">*</span> {{:: 'openshift.base-url' | translate}}</label>
+    <div class="col-md-6">
+        <input class="form-control" id="baseUrl" type="text" ng-model="identityProvider.config.baseUrl" required>
+    </div>
+    <kc-tooltip>{{:: 'openshift.base-url.tooltip' | translate}}</kc-tooltip>
+</div>