keycloak-aplcache
Changes
connections/http-client/pom.xml 4(+4 -0)
connections/http-client/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java 100(+61 -39)
connections/http-client/src/main/java/org/keycloak/connections/httpclient/HttpClientBuilder.java 2(+1 -1)
connections/pom.xml 1(+1 -0)
connections/truststore/pom.xml 35(+35 -0)
connections/truststore/src/main/java/org/keycloak/connections/truststore/FileTruststoreProvider.java 31(+31 -0)
connections/truststore/src/main/java/org/keycloak/connections/truststore/FileTruststoreProviderFactory.java 102(+102 -0)
connections/truststore/src/main/java/org/keycloak/connections/truststore/HostnameVerificationPolicy.java 19(+19 -0)
connections/truststore/src/main/java/org/keycloak/connections/truststore/JSSETruststoreConfigurator.java 106(+106 -0)
connections/truststore/src/main/java/org/keycloak/connections/truststore/SSLSocketFactory.java 87(+87 -0)
connections/truststore/src/main/java/org/keycloak/connections/truststore/TruststoreProvider.java 15(+15 -0)
connections/truststore/src/main/java/org/keycloak/connections/truststore/TruststoreProviderFactory.java 9(+9 -0)
connections/truststore/src/main/java/org/keycloak/connections/truststore/TruststoreProviderSingleton.java 17(+17 -0)
connections/truststore/src/main/java/org/keycloak/connections/truststore/TruststoreSpi.java 31(+31 -0)
connections/truststore/src/main/resources/META-INF/services/org.keycloak.connections.truststore.TruststoreProviderFactory 1(+1 -0)
dependencies/server-min/pom.xml 5(+4 -1)
distribution/feature-packs/server-feature-pack/src/main/resources/content/standalone/configuration/keycloak-server.json 4(+1 -3)
distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-broker-core/main/module.xml 1(+1 -0)
distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-broker-oidc/main/module.xml 1(+1 -0)
distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-connections-http-client/main/module.xml 1(+1 -0)
distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-connections-truststore/main/module.xml 17(+17 -0)
distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml 1(+1 -0)
distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-services/main/module.xml 1(+1 -0)
distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-broker-core/main/module.xml 1(+1 -0)
distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-broker-oidc/main/module.xml 1(+1 -0)
distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-connections-http-client/main/module.xml 1(+1 -0)
distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-connections-truststore/main/module.xml 17(+17 -0)
distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-eap6-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml 1(+1 -0)
distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml 1(+1 -0)
examples/cordova/www/config.xml 5(+5 -0)
examples/cordova/www/index.html 2(+2 -0)
federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPOperationManager.java 3(+3 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js 14(+14 -0)
forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js 212(+204 -8)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-client.html 4(+3 -1)
forms/common-themes/src/main/resources/theme/base/admin/resources/partials/partial-import.html 120(+120 -0)
pom.xml 5(+5 -0)
README.md 2(+1 -1)
services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java 14(+11 -3)
services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java 52(+28 -24)
testsuite/integration/src/test/java/org/keycloak/testsuite/actions/TermsAndConditionsTest.java 31(+31 -0)
testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AbstractClientTest.java 40(+21 -19)
testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java 28(+28 -0)
Details
diff --git a/broker/core/src/main/java/org/keycloak/broker/provider/util/SimpleHttp.java b/broker/core/src/main/java/org/keycloak/broker/provider/util/SimpleHttp.java
index 5fa23b0..b10c46e 100755
--- a/broker/core/src/main/java/org/keycloak/broker/provider/util/SimpleHttp.java
+++ b/broker/core/src/main/java/org/keycloak/broker/provider/util/SimpleHttp.java
@@ -1,5 +1,8 @@
package org.keycloak.broker.provider.util;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLSocketFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
@@ -24,6 +27,9 @@ public class SimpleHttp {
private Map<String, String> headers;
private Map<String, String> params;
+ private SSLSocketFactory sslFactory;
+ private HostnameVerifier hostnameVerifier;
+
protected SimpleHttp(String url, String method) {
this.url = url;
this.method = method;
@@ -53,6 +59,15 @@ public class SimpleHttp {
return this;
}
+ public SimpleHttp sslFactory(SSLSocketFactory factory) {
+ sslFactory = factory;
+ return this;
+ }
+
+ public SimpleHttp hostnameVerifier(HostnameVerifier verifier) {
+ hostnameVerifier = verifier;
+ return this;
+ }
public String asString() throws IOException {
boolean get = method.equals("GET");
@@ -85,6 +100,7 @@ public class SimpleHttp {
}
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
+ setupTruststoreIfApplicable(connection);
OutputStream os = null;
InputStream is = null;
@@ -171,6 +187,7 @@ public class SimpleHttp {
}
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
+ setupTruststoreIfApplicable(connection);
OutputStream os = null;
InputStream is = null;
@@ -235,4 +252,13 @@ public class SimpleHttp {
return writer.toString();
}
+ private void setupTruststoreIfApplicable(HttpURLConnection connection) {
+ if (connection instanceof HttpsURLConnection && sslFactory != null) {
+ HttpsURLConnection con = (HttpsURLConnection) connection;
+ con.setSSLSocketFactory(sslFactory);
+ if (hostnameVerifier != null) {
+ con.setHostnameVerifier(hostnameVerifier);
+ }
+ }
+ }
}
diff --git a/broker/oidc/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java b/broker/oidc/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
index 0c8dc3c..6e2543b 100755
--- a/broker/oidc/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
+++ b/broker/oidc/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java
@@ -27,6 +27,7 @@ import org.keycloak.broker.provider.AbstractIdentityProvider;
import org.keycloak.broker.provider.AuthenticationRequest;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException;
+import org.keycloak.connections.truststore.JSSETruststoreConfigurator;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
@@ -247,12 +248,15 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
}
public SimpleHttp generateTokenRequest(String authorizationCode) {
+ JSSETruststoreConfigurator configurator = new JSSETruststoreConfigurator(session);
return SimpleHttp.doPost(getConfig().getTokenUrl())
.param(OAUTH2_PARAMETER_CODE, authorizationCode)
.param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
.param(OAUTH2_PARAMETER_CLIENT_SECRET, getConfig().getClientSecret())
.param(OAUTH2_PARAMETER_REDIRECT_URI, uriInfo.getAbsolutePath().toString())
- .param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_AUTHORIZATION_CODE);
+ .param(OAUTH2_PARAMETER_GRANT_TYPE, OAUTH2_GRANT_TYPE_AUTHORIZATION_CODE)
+ .sslFactory(configurator.getSSLSocketFactory())
+ .hostnameVerifier(configurator.getHostnameVerifier());
}
}
}
diff --git a/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
index c237b8d..c754258 100755
--- a/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
+++ b/broker/saml/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java
@@ -1,6 +1,7 @@
package org.keycloak.broker.saml;
import org.jboss.logging.Logger;
+import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.common.ClientConnection;
import org.keycloak.common.VerificationException;
import org.keycloak.broker.provider.BrokeredIdentityContext;
@@ -45,6 +46,7 @@ import javax.ws.rs.Consumes;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
+import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
@@ -96,6 +98,13 @@ public class SAMLEndpoint {
}
@GET
+ @NoCache
+ @Path("descriptor")
+ public Response getSPDescriptor() {
+ return provider.export(uriInfo, realm, null);
+ }
+
+ @GET
public Response redirectBinding(@QueryParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest,
@QueryParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse,
@QueryParam(GeneralConstants.RELAY_STATE) String relayState) {
connections/http-client/pom.xml 4(+4 -0)
diff --git a/connections/http-client/pom.xml b/connections/http-client/pom.xml
index f88ee65..07e7f6f 100755
--- a/connections/http-client/pom.xml
+++ b/connections/http-client/pom.xml
@@ -23,6 +23,10 @@
<artifactId>keycloak-model-api</artifactId>
</dependency>
<dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-connections-truststore</artifactId>
+ </dependency>
+ <dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<scope>provided</scope>
diff --git a/connections/http-client/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java b/connections/http-client/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java
index a06b296..67f7cbd 100755
--- a/connections/http-client/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java
+++ b/connections/http-client/src/main/java/org/keycloak/connections/httpclient/DefaultHttpClientFactory.java
@@ -10,6 +10,7 @@ import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.CloseableHttpClient;
import org.jboss.logging.Logger;
import org.keycloak.Config;
+import org.keycloak.connections.truststore.TruststoreProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.common.util.EnvUtil;
@@ -28,9 +29,12 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
private static final Logger logger = Logger.getLogger(DefaultHttpClientFactory.class);
private volatile CloseableHttpClient httpClient;
+ private Config.Scope config;
@Override
public HttpClientProvider create(KeycloakSession session) {
+ lazyInit(session);
+
return new HttpClientProvider() {
@Override
public HttpClient getHttpClient() {
@@ -74,7 +78,9 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
@Override
public void close() {
try {
- httpClient.close();
+ if (httpClient != null) {
+ httpClient.close();
+ }
} catch (IOException e) {
}
@@ -87,46 +93,62 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
@Override
public void init(Config.Scope config) {
- long socketTimeout = config.getLong("socket-timeout-millis", -1L);
- long establishConnectionTimeout = config.getLong("establish-connection-timeout-millis", -1L);
- int maxPooledPerRoute = config.getInt("max-pooled-per-route", 0);
- int connectionPoolSize = config.getInt("connection-pool-size", 200);
- boolean disableTrustManager = config.getBoolean("disable-trust-manager", false);
- boolean disableCookies = config.getBoolean("disable-cookies", true);
- String hostnameVerificationPolicy = config.get("hostname-verification-policy", "WILDCARD");
- HttpClientBuilder.HostnameVerificationPolicy hostnamePolicy = HttpClientBuilder.HostnameVerificationPolicy.valueOf(hostnameVerificationPolicy);
- String truststore = config.get("truststore");
- String truststorePassword = config.get("truststore-password");
- String clientKeystore = config.get("client-keystore");
- String clientKeystorePassword = config.get("client-keystore-password");
- String clientPrivateKeyPassword = config.get("client-key-password");
-
- HttpClientBuilder builder = new HttpClientBuilder();
- builder.socketTimeout(socketTimeout, TimeUnit.MILLISECONDS)
- .establishConnectionTimeout(establishConnectionTimeout, TimeUnit.MILLISECONDS)
- .maxPooledPerRoute(maxPooledPerRoute)
- .connectionPoolSize(connectionPoolSize)
- .hostnameVerification(hostnamePolicy)
- .disableCookies(disableCookies);
- if (disableTrustManager) builder.disableTrustManager();
- if (truststore != null) {
- truststore = EnvUtil.replace(truststore);
- try {
- builder.trustStore(KeystoreUtil.loadKeyStore(truststore, truststorePassword));
- } catch (Exception e) {
- throw new RuntimeException("Failed to load truststore", e);
- }
- }
- if (clientKeystore != null) {
- clientKeystore = EnvUtil.replace(clientKeystore);
- try {
- KeyStore clientCertKeystore = KeystoreUtil.loadKeyStore(clientKeystore, clientKeystorePassword);
- builder.keyStore(clientCertKeystore, clientPrivateKeyPassword);
- } catch (Exception e) {
- throw new RuntimeException("Failed to load keystore", e);
+ this.config = config;
+ }
+
+ private void lazyInit(KeycloakSession session) {
+ if (httpClient == null) {
+ synchronized(this) {
+ if (httpClient == null) {
+ long socketTimeout = config.getLong("socket-timeout-millis", -1L);
+ long establishConnectionTimeout = config.getLong("establish-connection-timeout-millis", -1L);
+ int maxPooledPerRoute = config.getInt("max-pooled-per-route", 0);
+ int connectionPoolSize = config.getInt("connection-pool-size", 200);
+ boolean disableCookies = config.getBoolean("disable-cookies", true);
+ String clientKeystore = config.get("client-keystore");
+ String clientKeystorePassword = config.get("client-keystore-password");
+ String clientPrivateKeyPassword = config.get("client-key-password");
+
+ TruststoreProvider truststoreProvider = session.getProvider(TruststoreProvider.class);
+ boolean disableTrustManager = truststoreProvider == null || truststoreProvider.getTruststore() == null;
+ if (disableTrustManager) {
+ logger.warn("Truststore is disabled");
+ }
+ HttpClientBuilder.HostnameVerificationPolicy hostnamePolicy = disableTrustManager ? null
+ : HttpClientBuilder.HostnameVerificationPolicy.valueOf(truststoreProvider.getPolicy().name());
+
+ HttpClientBuilder builder = new HttpClientBuilder();
+ builder.socketTimeout(socketTimeout, TimeUnit.MILLISECONDS)
+ .establishConnectionTimeout(establishConnectionTimeout, TimeUnit.MILLISECONDS)
+ .maxPooledPerRoute(maxPooledPerRoute)
+ .connectionPoolSize(connectionPoolSize)
+ .disableCookies(disableCookies);
+
+ if (disableTrustManager) {
+ // TODO: is it ok to do away with disabling trust manager?
+ //builder.disableTrustManager();
+ } else {
+ builder.hostnameVerification(hostnamePolicy);
+ try {
+ builder.trustStore(truststoreProvider.getTruststore());
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to load truststore", e);
+ }
+ }
+
+ if (clientKeystore != null) {
+ clientKeystore = EnvUtil.replace(clientKeystore);
+ try {
+ KeyStore clientCertKeystore = KeystoreUtil.loadKeyStore(clientKeystore, clientKeystorePassword);
+ builder.keyStore(clientCertKeystore, clientPrivateKeyPassword);
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to load keystore", e);
+ }
+ }
+ httpClient = builder.build();
+ }
}
}
- httpClient = builder.build();
}
@Override
diff --git a/connections/http-client/src/main/java/org/keycloak/connections/httpclient/HttpClientBuilder.java b/connections/http-client/src/main/java/org/keycloak/connections/httpclient/HttpClientBuilder.java
index 0668382..a16c12a 100755
--- a/connections/http-client/src/main/java/org/keycloak/connections/httpclient/HttpClientBuilder.java
+++ b/connections/http-client/src/main/java/org/keycloak/connections/httpclient/HttpClientBuilder.java
@@ -145,7 +145,7 @@ public class HttpClientBuilder {
* Disable cookie management.
*/
public HttpClientBuilder disableCookies(boolean disable) {
- this.disableTrustManager = disable;
+ this.disableCookies = disable;
return this;
}
connections/pom.xml 1(+1 -0)
diff --git a/connections/pom.xml b/connections/pom.xml
index b931994..17cca7f 100755
--- a/connections/pom.xml
+++ b/connections/pom.xml
@@ -19,6 +19,7 @@
<module>mongo</module>
<module>mongo-update</module>
<module>http-client</module>
+ <module>truststore</module>
</modules>
<build>
connections/truststore/pom.xml 35(+35 -0)
diff --git a/connections/truststore/pom.xml b/connections/truststore/pom.xml
new file mode 100755
index 0000000..d72b6d8
--- /dev/null
+++ b/connections/truststore/pom.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <parent>
+ <artifactId>keycloak-parent</artifactId>
+ <groupId>org.keycloak</groupId>
+ <version>1.8.0.CR1-SNAPSHOT</version>
+ <relativePath>../../pom.xml</relativePath>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>keycloak-connections-truststore</artifactId>
+ <name>Keycloak Truststore</name>
+ <description/>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.httpcomponents</groupId>
+ <artifactId>httpclient</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-model-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.jboss.logging</groupId>
+ <artifactId>jboss-logging</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+</project>
diff --git a/connections/truststore/src/main/java/org/keycloak/connections/truststore/FileTruststoreProvider.java b/connections/truststore/src/main/java/org/keycloak/connections/truststore/FileTruststoreProvider.java
new file mode 100644
index 0000000..d49ddb1
--- /dev/null
+++ b/connections/truststore/src/main/java/org/keycloak/connections/truststore/FileTruststoreProvider.java
@@ -0,0 +1,31 @@
+package org.keycloak.connections.truststore;
+
+import java.security.KeyStore;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class FileTruststoreProvider implements TruststoreProvider {
+
+ private final HostnameVerificationPolicy policy;
+ private final KeyStore truststore;
+
+ FileTruststoreProvider(KeyStore truststore, HostnameVerificationPolicy policy) {
+ this.policy = policy;
+ this.truststore = truststore;
+ }
+
+ @Override
+ public HostnameVerificationPolicy getPolicy() {
+ return policy;
+ }
+
+ @Override
+ public KeyStore getTruststore() {
+ return truststore;
+ }
+
+ @Override
+ public void close() {
+ }
+}
diff --git a/connections/truststore/src/main/java/org/keycloak/connections/truststore/FileTruststoreProviderFactory.java b/connections/truststore/src/main/java/org/keycloak/connections/truststore/FileTruststoreProviderFactory.java
new file mode 100644
index 0000000..0e2a5d3
--- /dev/null
+++ b/connections/truststore/src/main/java/org/keycloak/connections/truststore/FileTruststoreProviderFactory.java
@@ -0,0 +1,102 @@
+package org.keycloak.connections.truststore;
+
+import org.jboss.logging.Logger;
+import org.keycloak.Config;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.KeyStore;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class FileTruststoreProviderFactory implements TruststoreProviderFactory {
+
+ private static final Logger log = Logger.getLogger(FileTruststoreProviderFactory.class);
+
+ private TruststoreProvider provider;
+
+ @Override
+ public TruststoreProvider create(KeycloakSession session) {
+ return provider;
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+
+ String storepath = config.get("file");
+ String pass = config.get("password");
+ String policy = config.get("hostname-verification-policy");
+ Boolean disabled = config.getBoolean("disabled", null);
+
+ // if "truststore" . "file" is not configured then it is disabled
+ if (storepath == null && pass == null && policy == null && disabled == null) {
+ return;
+ }
+
+ // if explicitly disabled
+ if (disabled != null && disabled) {
+ return;
+ }
+
+ HostnameVerificationPolicy verificationPolicy = null;
+ KeyStore truststore = null;
+
+ if (storepath == null) {
+ throw new RuntimeException("Attribute 'file' missing in 'truststore':'file' configuration");
+ }
+ if (pass == null) {
+ throw new RuntimeException("Attribute 'password' missing in 'truststore':'file' configuration");
+ }
+
+ try {
+ truststore = loadStore(storepath, pass == null ? null :pass.toCharArray());
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to initialize TruststoreProviderFactory: " + new File(storepath).getAbsolutePath(), e);
+ }
+ if (policy == null) {
+ verificationPolicy = HostnameVerificationPolicy.WILDCARD;
+ } else {
+ try {
+ verificationPolicy = HostnameVerificationPolicy.valueOf(policy);
+ } catch (Exception e) {
+ throw new RuntimeException("Invalid value for 'hostname-verification-policy': " + policy + " (must be one of: ANY, WILDCARD, STRICT)");
+ }
+ }
+
+ provider = new FileTruststoreProvider(truststore, verificationPolicy);
+ TruststoreProviderSingleton.set(provider);
+ log.debug("File trustore provider initialized: " + new File(storepath).getAbsolutePath());
+ }
+
+ private KeyStore loadStore(String path, char[] password) throws Exception {
+ KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
+ InputStream is = new FileInputStream(path);
+ try {
+ ks.load(is, password);
+ return ks;
+ } finally {
+ try {
+ is.close();
+ } catch (IOException ignored) {
+ }
+ }
+ }
+
+ @Override
+ public void postInit(KeycloakSessionFactory factory) {
+ }
+
+ @Override
+ public void close() {
+ }
+
+ @Override
+ public String getId() {
+ return "file";
+ }
+}
diff --git a/connections/truststore/src/main/java/org/keycloak/connections/truststore/HostnameVerificationPolicy.java b/connections/truststore/src/main/java/org/keycloak/connections/truststore/HostnameVerificationPolicy.java
new file mode 100644
index 0000000..0b6c53b
--- /dev/null
+++ b/connections/truststore/src/main/java/org/keycloak/connections/truststore/HostnameVerificationPolicy.java
@@ -0,0 +1,19 @@
+package org.keycloak.connections.truststore;
+
+public enum HostnameVerificationPolicy {
+
+ /**
+ * Hostname verification is not done on the server's certificate
+ */
+ ANY,
+
+ /**
+ * Allows wildcards in subdomain names i.e. *.foo.com
+ */
+ WILDCARD,
+
+ /**
+ * CN must match hostname connecting to
+ */
+ STRICT
+}
\ No newline at end of file
diff --git a/connections/truststore/src/main/java/org/keycloak/connections/truststore/JSSETruststoreConfigurator.java b/connections/truststore/src/main/java/org/keycloak/connections/truststore/JSSETruststoreConfigurator.java
new file mode 100644
index 0000000..e323f7a
--- /dev/null
+++ b/connections/truststore/src/main/java/org/keycloak/connections/truststore/JSSETruststoreConfigurator.java
@@ -0,0 +1,106 @@
+package org.keycloak.connections.truststore;
+
+import org.apache.http.conn.ssl.BrowserCompatHostnameVerifier;
+import org.apache.http.conn.ssl.StrictHostnameVerifier;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class JSSETruststoreConfigurator {
+
+ private TruststoreProvider provider;
+ private volatile javax.net.ssl.SSLSocketFactory sslFactory;
+ private volatile TrustManager[] tm;
+
+ public JSSETruststoreConfigurator(KeycloakSession session) {
+ KeycloakSessionFactory factory = session.getKeycloakSessionFactory();
+ TruststoreProviderFactory truststoreFactory = (TruststoreProviderFactory) factory.getProviderFactory(TruststoreProvider.class, "file");
+
+ provider = truststoreFactory.create(session);
+ if (provider != null && provider.getTruststore() == null) {
+ provider = null;
+ }
+ }
+
+ public JSSETruststoreConfigurator(TruststoreProvider provider) {
+ this.provider = provider;
+ }
+
+ public javax.net.ssl.SSLSocketFactory getSSLSocketFactory() {
+ if (provider == null) {
+ return null;
+ }
+
+ if (sslFactory == null) {
+ synchronized(this) {
+ if (sslFactory == null) {
+ try {
+ SSLContext sslctx = SSLContext.getInstance("TLS");
+ sslctx.init(null, getTrustManagers(), null);
+ sslFactory = sslctx.getSocketFactory();
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to initialize SSLContext: ", e);
+ }
+ }
+ }
+ }
+ return sslFactory;
+ }
+
+ public TrustManager[] getTrustManagers() {
+ if (provider == null) {
+ return null;
+ }
+
+ if (tm == null) {
+ synchronized (this) {
+ if (tm == null) {
+ TrustManagerFactory tmf = null;
+ try {
+ tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ tmf.init(provider.getTruststore());
+ tm = tmf.getTrustManagers();
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to initialize TrustManager: ", e);
+ }
+ }
+ }
+ }
+ return tm;
+ }
+
+ public HostnameVerifier getHostnameVerifier() {
+ if (provider == null) {
+ return null;
+ }
+
+ HostnameVerificationPolicy policy = provider.getPolicy();
+ switch (policy) {
+ case ANY:
+ return new HostnameVerifier() {
+ @Override
+ public boolean verify(String s, SSLSession sslSession) {
+ return true;
+ }
+ };
+ case WILDCARD:
+ return new BrowserCompatHostnameVerifier();
+ case STRICT:
+ return new StrictHostnameVerifier();
+ default:
+ throw new IllegalStateException("Unknown policy: " + policy.name());
+ }
+ }
+
+ public TruststoreProvider getProvider() {
+ return provider;
+ }
+}
diff --git a/connections/truststore/src/main/java/org/keycloak/connections/truststore/SSLSocketFactory.java b/connections/truststore/src/main/java/org/keycloak/connections/truststore/SSLSocketFactory.java
new file mode 100644
index 0000000..86f5667
--- /dev/null
+++ b/connections/truststore/src/main/java/org/keycloak/connections/truststore/SSLSocketFactory.java
@@ -0,0 +1,87 @@
+package org.keycloak.connections.truststore;
+
+import org.jboss.logging.Logger;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+
+
+/**
+ * Using this class is ugly, but it is the only way to push our truststore to the default LDAP client implementation.
+ * <p>
+ * This SSLSocketFactory can only use truststore configured by TruststoreProvider after the ProviderFactory was
+ * initialized using standard Spi load / init mechanism. That will only happen if "truststore" provider is configured
+ * in keycloak-server.json.
+ * <p>
+ * If TruststoreProvider is not available this SSLSocketFactory will delegate all operations to javax.net.ssl.SSLSocketFactory.getDefault().
+ *
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+
+public class SSLSocketFactory extends javax.net.ssl.SSLSocketFactory {
+
+ private static final Logger log = Logger.getLogger(SSLSocketFactory.class);
+
+ private static SSLSocketFactory instance;
+
+ private final javax.net.ssl.SSLSocketFactory sslsf;
+
+ private SSLSocketFactory() {
+
+ TruststoreProvider provider = TruststoreProviderSingleton.get();
+ javax.net.ssl.SSLSocketFactory sf = null;
+ if (provider != null) {
+ sf = new JSSETruststoreConfigurator(provider).getSSLSocketFactory();
+ }
+
+ if (sf == null) {
+ log.info("No truststore provider found - using default SSLSocketFactory");
+ sf = (javax.net.ssl.SSLSocketFactory) javax.net.ssl.SSLSocketFactory.getDefault();
+ }
+
+ sslsf = sf;
+ }
+
+ public static synchronized SSLSocketFactory getDefault() {
+ if (instance == null) {
+ instance = new SSLSocketFactory();
+ }
+ return instance;
+ }
+
+ @Override
+ public String[] getDefaultCipherSuites() {
+ return sslsf.getDefaultCipherSuites();
+ }
+
+ @Override
+ public String[] getSupportedCipherSuites() {
+ return sslsf.getSupportedCipherSuites();
+ }
+
+ @Override
+ public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException {
+ return sslsf.createSocket(socket, host, port, autoClose);
+ }
+
+ @Override
+ public Socket createSocket(String host, int port) throws IOException {
+ return sslsf.createSocket(host, port);
+ }
+
+ @Override
+ public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
+ return sslsf.createSocket(host, port, localHost, localPort);
+ }
+
+ @Override
+ public Socket createSocket(InetAddress host, int port) throws IOException {
+ return sslsf.createSocket(host, port);
+ }
+
+ @Override
+ public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
+ return sslsf.createSocket(address, port, localAddress, localPort);
+ }
+}
diff --git a/connections/truststore/src/main/java/org/keycloak/connections/truststore/TruststoreProvider.java b/connections/truststore/src/main/java/org/keycloak/connections/truststore/TruststoreProvider.java
new file mode 100644
index 0000000..54cc6c3
--- /dev/null
+++ b/connections/truststore/src/main/java/org/keycloak/connections/truststore/TruststoreProvider.java
@@ -0,0 +1,15 @@
+package org.keycloak.connections.truststore;
+
+import org.keycloak.provider.Provider;
+
+import java.security.KeyStore;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public interface TruststoreProvider extends Provider {
+
+ HostnameVerificationPolicy getPolicy();
+
+ KeyStore getTruststore();
+}
diff --git a/connections/truststore/src/main/java/org/keycloak/connections/truststore/TruststoreProviderFactory.java b/connections/truststore/src/main/java/org/keycloak/connections/truststore/TruststoreProviderFactory.java
new file mode 100644
index 0000000..10ed867
--- /dev/null
+++ b/connections/truststore/src/main/java/org/keycloak/connections/truststore/TruststoreProviderFactory.java
@@ -0,0 +1,9 @@
+package org.keycloak.connections.truststore;
+
+import org.keycloak.provider.ProviderFactory;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public interface TruststoreProviderFactory extends ProviderFactory<TruststoreProvider> {
+}
diff --git a/connections/truststore/src/main/java/org/keycloak/connections/truststore/TruststoreProviderSingleton.java b/connections/truststore/src/main/java/org/keycloak/connections/truststore/TruststoreProviderSingleton.java
new file mode 100644
index 0000000..520e781
--- /dev/null
+++ b/connections/truststore/src/main/java/org/keycloak/connections/truststore/TruststoreProviderSingleton.java
@@ -0,0 +1,17 @@
+package org.keycloak.connections.truststore;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+class TruststoreProviderSingleton {
+
+ static private TruststoreProvider provider;
+
+ static void set(TruststoreProvider tp) {
+ provider = tp;
+ }
+
+ static TruststoreProvider get() {
+ return provider;
+ }
+}
diff --git a/connections/truststore/src/main/java/org/keycloak/connections/truststore/TruststoreSpi.java b/connections/truststore/src/main/java/org/keycloak/connections/truststore/TruststoreSpi.java
new file mode 100644
index 0000000..385346e
--- /dev/null
+++ b/connections/truststore/src/main/java/org/keycloak/connections/truststore/TruststoreSpi.java
@@ -0,0 +1,31 @@
+package org.keycloak.connections.truststore;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+import org.keycloak.provider.Spi;
+
+/**
+ * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a>
+ */
+public class TruststoreSpi implements Spi {
+
+ @Override
+ public boolean isInternal() {
+ return true;
+ }
+
+ @Override
+ public String getName() {
+ return "truststore";
+ }
+
+ @Override
+ public Class<? extends Provider> getProviderClass() {
+ return TruststoreProvider.class;
+ }
+
+ @Override
+ public Class<? extends ProviderFactory> getProviderFactoryClass() {
+ return TruststoreProviderFactory.class;
+ }
+}
diff --git a/connections/truststore/src/main/resources/META-INF/services/org.keycloak.connections.truststore.TruststoreProviderFactory b/connections/truststore/src/main/resources/META-INF/services/org.keycloak.connections.truststore.TruststoreProviderFactory
new file mode 100644
index 0000000..5b38b43
--- /dev/null
+++ b/connections/truststore/src/main/resources/META-INF/services/org.keycloak.connections.truststore.TruststoreProviderFactory
@@ -0,0 +1 @@
+org.keycloak.connections.truststore.FileTruststoreProviderFactory
\ No newline at end of file
diff --git a/connections/truststore/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/connections/truststore/src/main/resources/META-INF/services/org.keycloak.provider.Spi
new file mode 100644
index 0000000..3be9970
--- /dev/null
+++ b/connections/truststore/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -0,0 +1 @@
+org.keycloak.connections.truststore.TruststoreSpi
\ No newline at end of file
diff --git a/core/src/main/java/org/keycloak/representations/idm/PartialImportRepresentation.java b/core/src/main/java/org/keycloak/representations/idm/PartialImportRepresentation.java
new file mode 100644
index 0000000..9b8c2ae
--- /dev/null
+++ b/core/src/main/java/org/keycloak/representations/idm/PartialImportRepresentation.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.keycloak.representations.idm;
+
+import java.util.List;
+import org.codehaus.jackson.annotate.JsonIgnoreProperties;
+
+/**
+ * Used for partial import of users, clients, roles, and identity providers.
+ *
+ * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
+ */
+@JsonIgnoreProperties(ignoreUnknown=true)
+public class PartialImportRepresentation {
+ public enum Policy { SKIP, OVERWRITE, FAIL };
+
+ protected Policy policy = Policy.FAIL;
+ protected String ifResourceExists = "";
+ protected List<UserRepresentation> users;
+ protected List<ClientRepresentation> clients;
+ protected List<IdentityProviderRepresentation> identityProviders;
+ protected RolesRepresentation roles;
+
+ public boolean hasUsers() {
+ return (users != null) && !users.isEmpty();
+ }
+
+ public boolean hasClients() {
+ return (clients != null) && !clients.isEmpty();
+ }
+
+ public boolean hasIdps() {
+ return (identityProviders != null) && !identityProviders.isEmpty();
+ }
+
+ public boolean hasRealmRoles() {
+ return (roles != null) && (roles.getRealm() != null) && (!roles.getRealm().isEmpty());
+ }
+
+ public boolean hasClientRoles() {
+ return (roles != null) && (roles.getClient() != null) && (!roles.getClient().isEmpty());
+ }
+
+ public String getIfResourceExists() {
+ return ifResourceExists;
+ }
+
+ public void setIfResourceExists(String ifResourceExists) {
+ this.ifResourceExists = ifResourceExists;
+ this.policy = Policy.valueOf(ifResourceExists);
+ }
+
+ public Policy getPolicy() {
+ return this.policy;
+ }
+
+ public List<UserRepresentation> getUsers() {
+ return users;
+ }
+
+ public void setUsers(List<UserRepresentation> users) {
+ this.users = users;
+ }
+
+ public List<ClientRepresentation> getClients() {
+ return clients;
+ }
+
+ public void setClients(List<ClientRepresentation> clients) {
+ this.clients = clients;
+ }
+
+ public List<IdentityProviderRepresentation> getIdentityProviders() {
+ return identityProviders;
+ }
+
+ public void setIdentityProviders(List<IdentityProviderRepresentation> identityProviders) {
+ this.identityProviders = identityProviders;
+ }
+
+ public RolesRepresentation getRoles() {
+ return roles;
+ }
+
+ public void setRoles(RolesRepresentation roles) {
+ this.roles = roles;
+ }
+}
dependencies/server-min/pom.xml 5(+4 -1)
diff --git a/dependencies/server-min/pom.xml b/dependencies/server-min/pom.xml
index 019e73c..422cc28 100755
--- a/dependencies/server-min/pom.xml
+++ b/dependencies/server-min/pom.xml
@@ -131,7 +131,10 @@
<groupId>org.keycloak</groupId>
<artifactId>keycloak-export-import-single-file</artifactId>
</dependency>
-
+ <dependency>
+ <groupId>org.keycloak</groupId>
+ <artifactId>keycloak-connections-truststore</artifactId>
+ </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-connections-http-client</artifactId>
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/content/standalone/configuration/keycloak-server.json b/distribution/feature-packs/server-feature-pack/src/main/resources/content/standalone/configuration/keycloak-server.json
index 3e4315c..a5b4d1b 100644
--- a/distribution/feature-packs/server-feature-pack/src/main/resources/content/standalone/configuration/keycloak-server.json
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/content/standalone/configuration/keycloak-server.json
@@ -45,9 +45,7 @@
},
"connectionsHttpClient": {
- "default": {
- "disable-trust-manager": true
- }
+ "default": {}
},
"connectionsJpa": {
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-broker-core/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-broker-core/main/module.xml
index b7ed82e..e7e18c2 100755
--- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-broker-core/main/module.xml
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-broker-core/main/module.xml
@@ -13,6 +13,7 @@
<module name="org.keycloak.keycloak-events-api"/>
<module name="javax.ws.rs.api"/>
<module name="org.jboss.logging"/>
+ <module name="javax.api"/>
</dependencies>
</module>
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-broker-oidc/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-broker-oidc/main/module.xml
index 2f4a97d..d070fa9 100755
--- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-broker-oidc/main/module.xml
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-broker-oidc/main/module.xml
@@ -13,6 +13,7 @@
<module name="org.keycloak.keycloak-events-api"/>
<module name="org.keycloak.keycloak-broker-core"/>
<module name="org.keycloak.keycloak-services"/>
+ <module name="org.keycloak.keycloak-connections-truststore"/>
<module name="org.codehaus.jackson.jackson-core-asl"/>
<module name="org.codehaus.jackson.jackson-mapper-asl"/>
<module name="org.codehaus.jackson.jackson-xc"/>
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-connections-http-client/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-connections-http-client/main/module.xml
index 27295dd..5611eca 100755
--- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-connections-http-client/main/module.xml
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-connections-http-client/main/module.xml
@@ -16,6 +16,7 @@
<module name="org.jboss.logging"/>
<module name="javax.api"/>
<module name="org.apache.httpcomponents"/>
+ <module name="org.keycloak.keycloak-connections-truststore"/>
</dependencies>
</module>
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-connections-truststore/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-connections-truststore/main/module.xml
new file mode 100755
index 0000000..8599425
--- /dev/null
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-connections-truststore/main/module.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+
+
+<module xmlns="urn:jboss:module:1.3" name="org.keycloak.keycloak-connections-truststore">
+ <resources>
+ <artifact name="${org.keycloak:keycloak-connections-truststore}"/>
+ </resources>
+ <dependencies>
+ <module name="org.keycloak.keycloak-core"/>
+ <module name="org.keycloak.keycloak-model-api"/>
+ <module name="org.jboss.logging"/>
+ <module name="javax.api"/>
+ <module name="org.apache.httpcomponents"/>
+ </dependencies>
+
+</module>
\ No newline at end of file
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml
index 7e61fb4..0a67753 100755
--- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml
@@ -8,6 +8,7 @@
<module name="org.keycloak.keycloak-connections-jpa-liquibase" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo-update" services="import"/>
+ <module name="org.keycloak.keycloak-connections-truststore" services="import"/>
<module name="org.keycloak.keycloak-common" services="import"/>
<module name="org.keycloak.keycloak-core" services="import"/>
<module name="org.keycloak.keycloak-email-api" services="import"/>
diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-services/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-services/main/module.xml
index 927b03f..12d9ebb 100755
--- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-services/main/module.xml
+++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/base/org/keycloak/keycloak-services/main/module.xml
@@ -18,6 +18,7 @@
<module name="org.keycloak.keycloak-connections-jpa-liquibase" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo-update" services="import"/>
+ <module name="org.keycloak.keycloak-connections-truststore" services="import"/>
<module name="org.keycloak.keycloak-common" services="import"/>
<module name="org.keycloak.keycloak-core" services="import"/>
<module name="org.keycloak.keycloak-email-api" services="import"/>
diff --git a/distribution/server-overlay/eap6/eap6-server-modules/build.xml b/distribution/server-overlay/eap6/eap6-server-modules/build.xml
index 09be9a4..97cbb6f 100755
--- a/distribution/server-overlay/eap6/eap6-server-modules/build.xml
+++ b/distribution/server-overlay/eap6/eap6-server-modules/build.xml
@@ -181,6 +181,10 @@
<maven-resource group="org.keycloak" artifact="keycloak-connections-infinispan"/>
</module-def>
+ <module-def name="org.keycloak.keycloak-connections-truststore">
+ <maven-resource group="org.keycloak" artifact="keycloak-connections-truststore"/>
+ </module-def>
+
<module-def name="org.keycloak.keycloak-model-jpa">
<maven-resource group="org.keycloak" artifact="keycloak-model-jpa"/>
</module-def>
diff --git a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-broker-core/main/module.xml b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-broker-core/main/module.xml
index fac17da..6cb957f 100755
--- a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-broker-core/main/module.xml
+++ b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-broker-core/main/module.xml
@@ -13,6 +13,7 @@
<module name="org.keycloak.keycloak-events-api"/>
<module name="javax.ws.rs.api"/>
<module name="org.jboss.logging"/>
+ <module name="javax.api"/>
</dependencies>
</module>
diff --git a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-broker-oidc/main/module.xml b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-broker-oidc/main/module.xml
index f2155cb..b872468 100755
--- a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-broker-oidc/main/module.xml
+++ b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-broker-oidc/main/module.xml
@@ -13,6 +13,7 @@
<module name="org.keycloak.keycloak-events-api"/>
<module name="org.keycloak.keycloak-broker-core"/>
<module name="org.keycloak.keycloak-services"/>
+ <module name="org.keycloak.keycloak-connections-truststore"/>
<module name="org.codehaus.jackson.jackson-core-asl"/>
<module name="org.codehaus.jackson.jackson-mapper-asl"/>
<module name="org.codehaus.jackson.jackson-xc"/>
diff --git a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-connections-http-client/main/module.xml b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-connections-http-client/main/module.xml
index 60489b2..a10b80f 100755
--- a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-connections-http-client/main/module.xml
+++ b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-connections-http-client/main/module.xml
@@ -13,6 +13,7 @@
<module name="org.keycloak.keycloak-common"/>
<module name="org.keycloak.keycloak-core"/>
<module name="org.keycloak.keycloak-model-api"/>
+ <module name="org.keycloak.keycloak-connections-truststore"/>
<module name="org.jboss.logging"/>
<module name="javax.api"/>
<module name="org.apache.httpcomponents"/>
diff --git a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-connections-truststore/main/module.xml b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-connections-truststore/main/module.xml
new file mode 100755
index 0000000..5c44976
--- /dev/null
+++ b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-connections-truststore/main/module.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+
+
+<module xmlns="urn:jboss:module:1.1" name="org.keycloak.keycloak-connections-truststore">
+ <resources>
+ <!-- Insert resources here -->
+ </resources>
+ <dependencies>
+ <module name="org.keycloak.keycloak-core"/>
+ <module name="org.keycloak.keycloak-model-api"/>
+ <module name="org.jboss.logging"/>
+ <module name="javax.api"/>
+ <module name="org.apache.httpcomponents"/>
+ </dependencies>
+
+</module>
\ No newline at end of file
diff --git a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-eap6-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-eap6-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml
index 7e61fb4..0a67753 100755
--- a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-eap6-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml
+++ b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-eap6-server-subsystem/main/server-war/WEB-INF/jboss-deployment-structure.xml
@@ -8,6 +8,7 @@
<module name="org.keycloak.keycloak-connections-jpa-liquibase" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo-update" services="import"/>
+ <module name="org.keycloak.keycloak-connections-truststore" services="import"/>
<module name="org.keycloak.keycloak-common" services="import"/>
<module name="org.keycloak.keycloak-core" services="import"/>
<module name="org.keycloak.keycloak-email-api" services="import"/>
diff --git a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml
index 56f2537..893c5e2 100755
--- a/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml
+++ b/distribution/server-overlay/eap6/eap6-server-modules/src/main/resources/modules/org/keycloak/keycloak-services/main/module.xml
@@ -18,6 +18,7 @@
<module name="org.keycloak.keycloak-connections-jpa-liquibase" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo-update" services="import"/>
+ <module name="org.keycloak.keycloak-connections-truststore" services="import"/>
<module name="org.keycloak.keycloak-common" services="import"/>
<module name="org.keycloak.keycloak-core" services="import"/>
<module name="org.keycloak.keycloak-email-api" services="import"/>
diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/export-import.xml b/docbook/auth-server-docs/reference/en/en-US/modules/export-import.xml
index fbd6016..6ec8660 100755
--- a/docbook/auth-server-docs/reference/en/en-US/modules/export-import.xml
+++ b/docbook/auth-server-docs/reference/en/en-US/modules/export-import.xml
@@ -1,114 +1,134 @@
<chapter id="export-import">
<title>Export and Import</title>
- <para>
- Export/import is useful especially if you want to migrate your whole Keycloak database from one environment to another or migrate to different database (For example from MySQL to Oracle).
- You can trigger export/import at startup of Keycloak server and it's configurable with System properties right now. The fact it's done at server startup means that no-one can access Keycloak UI or REST endpoints
- and edit Keycloak database on the fly when export or import is in progress. Otherwise it could lead to inconsistent results.
- </para>
- <para>
- You can export/import your database either to:
- <itemizedlist>
- <listitem>Directory on local filesystem</listitem>
- <listitem>Single JSON file on your filesystem</listitem>
- </itemizedlist>
+ <section>
+ <title>Startup export/import</title>
+ <para>
+ Export/import is useful especially if you want to migrate your whole Keycloak database from one environment to another or migrate to different database (For example from MySQL to Oracle).
+ You can trigger export/import at startup of Keycloak server and it's configurable with System properties right now. The fact it's done at server startup means that no-one can access Keycloak UI or REST endpoints
+ and edit Keycloak database on the fly when export or import is in progress. Otherwise it could lead to inconsistent results.
+ </para>
+ <para>
+ You can export/import your database either to:
+ <itemizedlist>
+ <listitem>Directory on local filesystem</listitem>
+ <listitem>Single JSON file on your filesystem</listitem>
+ </itemizedlist>
- When importing using the "dir" strategy, note that the files need to follow the naming convention specified below.
- If you are importing files which were previously exported, the files already follow this convention.
- <itemizedlist>
- <listitem>{REALM_NAME}-realm.json, such as "acme-roadrunner-affairs-realm.json" for the realm named "acme-roadrunner-affairs"</listitem>
- <listitem>{REALM_NAME}-users-{INDEX}.json, such as "acme-roadrunner-affairs-users-0.json" for the first users file of the realm named "acme-roadrunner-affairs"</listitem>
- </itemizedlist>
- </para>
- <para>
- If you import to Directory, you can specify also the number of users to be stored in each JSON file. So if you have
- very large amount of users in your database, you likely don't want to import them into single file as the file might be very big.
- Processing of each file is done in separate transaction as exporting/importing all users at once could also lead to memory issues.
- </para>
- <para>
- To export into unencrypted directory you can use:
- <programlisting><![CDATA[
+ When importing using the "dir" strategy, note that the files need to follow the naming convention specified below.
+ If you are importing files which were previously exported, the files already follow this convention.
+ <itemizedlist>
+ <listitem>{REALM_NAME}-realm.json, such as "acme-roadrunner-affairs-realm.json" for the realm named "acme-roadrunner-affairs"</listitem>
+ <listitem>{REALM_NAME}-users-{INDEX}.json, such as "acme-roadrunner-affairs-users-0.json" for the first users file of the realm named "acme-roadrunner-affairs"</listitem>
+ </itemizedlist>
+ </para>
+ <para>
+ If you import to Directory, you can specify also the number of users to be stored in each JSON file. So if you have
+ very large amount of users in your database, you likely don't want to import them into single file as the file might be very big.
+ Processing of each file is done in separate transaction as exporting/importing all users at once could also lead to memory issues.
+ </para>
+ <para>
+ To export into unencrypted directory you can use:
+ <programlisting><![CDATA[
bin/standalone.sh -Dkeycloak.migration.action=export
-Dkeycloak.migration.provider=dir -Dkeycloak.migration.dir=<DIR TO EXPORT TO>
]]></programlisting>
- And similarly for import just use <literal>-Dkeycloak.migration.action=import</literal> instead of <literal>export</literal> .
- </para>
- <para>
- To export into single JSON file you can use:
- <programlisting><![CDATA[
+ And similarly for import just use <literal>-Dkeycloak.migration.action=import</literal> instead of <literal>export</literal> .
+ </para>
+ <para>
+ To export into single JSON file you can use:
+ <programlisting><![CDATA[
bin/standalone.sh -Dkeycloak.migration.action=export
-Dkeycloak.migration.provider=singleFile -Dkeycloak.migration.file=<FILE TO EXPORT TO>
]]></programlisting>
- </para>
- <para>
- Here's an example of importing:
- <programlisting><![CDATA[
+ </para>
+ <para>
+ Here's an example of importing:
+ <programlisting><![CDATA[
bin/standalone.sh -Dkeycloak.migration.action=import
-Dkeycloak.migration.provider=singleFile -Dkeycloak.migration.file=<FILE TO IMPORT>
-Dkeycloak.migration.strategy=OVERWRITE_EXISTING
]]></programlisting>
- </para>
- <para>
- Other available options are:
- <variablelist>
- <varlistentry>
- <term>-Dkeycloak.migration.realmName</term>
- <listitem>
- <para>
- can be used if you want to export just one specified realm instead of all.
- If not specified, then all realms will be exported.
- </para>
- </listitem>
- </varlistentry>
- <varlistentry>
- <term>-Dkeycloak.migration.usersExportStrategy</term>
- <listitem>
- <para>
- can be used to specify for Directory providers to specify where to import users.
- Possible values are:
- <itemizedlist>
- <listitem>DIFFERENT_FILES - Users will be exported into more different files according to maximum number of users per file. This is default value</listitem>
- <listitem>SKIP - exporting of users will be skipped completely</listitem>
- <listitem>REALM_FILE - All users will be exported to same file with realm (So file like "foo-realm.json" with both realm data and users)</listitem>
- <listitem>SAME_FILE - All users will be exported to same file but different than realm (So file like "foo-realm.json" with realm data and "foo-users.json" with users)</listitem>
- </itemizedlist>
- </para>
- </listitem>
- </varlistentry>
- <varlistentry>
- <term>-Dkeycloak.migration.usersPerFile</term>
- <listitem>
- <para>
- can be used to specify number of users per file (and also per DB transaction).
- It's 5000 by default. It's used only if usersExportStrategy is DIFFERENT_FILES
- </para>
- </listitem>
- </varlistentry>
- <varlistentry>
- <term>-Dkeycloak.migration.strategy</term>
- <listitem>
- <para>
- is used during import. It can be used to specify how to proceed if realm with same name
- already exists in the database where you are going to import data. Possible values are:
- <itemizedlist>
- <listitem>IGNORE_EXISTING - Ignore importing if realm of this name already exists</listitem>
- <listitem>OVERWRITE_EXISTING - Remove existing realm and import it again with new data from JSON file.
- If you want to fully migrate one environment to another and ensure that the new environment will contain same data
- like the old one, you can specify this.
- </listitem>
- </itemizedlist>
- </para>
- </listitem>
- </varlistentry>
- </variablelist>
- </para>
- <para>
- When importing realm files that weren't exported before, the option <literal>keycloak.import</literal> can be used. If more than one realm
- file needs to be imported, a comma separated list of file names can be specified. This is more appropriate than the cases before, as this
- will happen only after the master realm has been initialized. Examples:
- <itemizedlist>
- <listitem>-Dkeycloak.import=/tmp/realm1.json</listitem>
- <listitem>-Dkeycloak.import=/tmp/realm1.json,/tmp/realm2.json</listitem>
- </itemizedlist>
- </para>
-
+ </para>
+ <para>
+ Other available options are:
+ <variablelist>
+ <varlistentry>
+ <term>-Dkeycloak.migration.realmName</term>
+ <listitem>
+ <para>
+ can be used if you want to export just one specified realm instead of all.
+ If not specified, then all realms will be exported.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>-Dkeycloak.migration.usersExportStrategy</term>
+ <listitem>
+ <para>
+ can be used to specify for Directory providers to specify where to import users.
+ Possible values are:
+ <itemizedlist>
+ <listitem>DIFFERENT_FILES - Users will be exported into more different files according to maximum number of users per file. This is default value</listitem>
+ <listitem>SKIP - exporting of users will be skipped completely</listitem>
+ <listitem>REALM_FILE - All users will be exported to same file with realm (So file like "foo-realm.json" with both realm data and users)</listitem>
+ <listitem>SAME_FILE - All users will be exported to same file but different than realm (So file like "foo-realm.json" with realm data and "foo-users.json" with users)</listitem>
+ </itemizedlist>
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>-Dkeycloak.migration.usersPerFile</term>
+ <listitem>
+ <para>
+ can be used to specify number of users per file (and also per DB transaction).
+ It's 5000 by default. It's used only if usersExportStrategy is DIFFERENT_FILES
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>-Dkeycloak.migration.strategy</term>
+ <listitem>
+ <para>
+ is used during import. It can be used to specify how to proceed if realm with same name
+ already exists in the database where you are going to import data. Possible values are:
+ <itemizedlist>
+ <listitem>IGNORE_EXISTING - Ignore importing if realm of this name already exists</listitem>
+ <listitem>OVERWRITE_EXISTING - Remove existing realm and import it again with new data from JSON file.
+ If you want to fully migrate one environment to another and ensure that the new environment will contain same data
+ like the old one, you can specify this.
+ </listitem>
+ </itemizedlist>
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </para>
+ <para>
+ When importing realm files that weren't exported before, the option <literal>keycloak.import</literal> can be used. If more than one realm
+ file needs to be imported, a comma separated list of file names can be specified. This is more appropriate than the cases before, as this
+ will happen only after the master realm has been initialized. Examples:
+ <itemizedlist>
+ <listitem>-Dkeycloak.import=/tmp/realm1.json</listitem>
+ <listitem>-Dkeycloak.import=/tmp/realm1.json,/tmp/realm2.json</listitem>
+ </itemizedlist>
+ </para>
+ </section>
+ <section>
+ <title>Admin console export/import</title>
+ <para>
+ Import of most resources can be performed from the admin console.
+ Exporting resources will be supported in future versions.
+ </para>
+ <para>
+ The files created during a "startup" export can be used to import from
+ the admin UI. This way, you can export from one realm and import to
+ another realm. Or, you can export from one server and import to another.
+ </para>
+ <warning>
+ <para>
+ The admin console import allows you to "overwrite" resources if you choose.
+ Use this feature with caution, especially on a production system.
+ </para>
+ </warning>
+ </section>
</chapter>
\ No newline at end of file
diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/identity-broker.xml b/docbook/auth-server-docs/reference/en/en-US/modules/identity-broker.xml
index cebf3af..c0183ba 100755
--- a/docbook/auth-server-docs/reference/en/en-US/modules/identity-broker.xml
+++ b/docbook/auth-server-docs/reference/en/en-US/modules/identity-broker.xml
@@ -1052,7 +1052,7 @@
<literal>HTTP-POST Binding for AuthnReques</literal>
</entry>
<entry>
- Allows you to specify wheter SAML authentication requests must be sent using the HTTP-POST or HTTP-Redirect protocol bindings. If enabled, it will send requests using HTTP-POST binding.
+ Allows you to specify whether SAML authentication requests must be sent using the HTTP-POST or HTTP-Redirect protocol bindings. If enabled, it will send requests using HTTP-POST binding.
</entry>
</row>
</tbody>
@@ -1066,6 +1066,16 @@
Once you create a SAML provider, there is an <literal>EXPORT</literal> button that appears when viewing that provider.
Clicking this button will export a SAML entity descriptor which you can use to
</para>
+ <section>
+ <title>SP Descriptor</title>
+ <para>The SAML SP Descriptor XML file for the broker is available publically by going to this URL</para>
+ <programlisting>
+ http[s]://{host:port}/auth/realms/{realm-name}/broker/{broker-alias}/endpoint/descriptor
+ </programlisting>
+ <para>
+ This URL is useful if you need to import this information into an IDP that needs or is more user friendly to load from a remote URL.
+ </para>
+ </section>
</section>
<section>
diff --git a/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml b/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml
index 38ca3f8..369d5f1 100755
--- a/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml
+++ b/docbook/auth-server-docs/reference/en/en-US/modules/server-installation.xml
@@ -4,7 +4,8 @@
<section>
<title>Installation</title>
<para>
- Keycloak Server has three downloadable distributions.
+ Keycloak Server has three downloadable distributions. To run the Keycloak server you need to have Java 8 already
+ installed.
</para>
<para>
<itemizedlist>
@@ -381,9 +382,7 @@ bin/add-user.[sh|bat] -r master -u <username> -p <password>
By default the setting is like this:
<programlisting><![CDATA[
"connectionsHttpClient": {
- "default": {
- "disable-trust-manager": true
- }
+ "default": {}
},
]]></programlisting>
Possible configuration options are:
@@ -421,15 +420,6 @@ bin/add-user.[sh|bat] -r master -u <username> -p <password>
</listitem>
</varlistentry>
<varlistentry>
- <term>disable-trust-manager</term>
- <listitem>
- <para>
- If true, HTTPS server certificates are not verified. If you set this to false, you must
- configure a truststore.
- </para>
- </listitem>
- </varlistentry>
- <varlistentry>
<term>disable-cookies</term>
<listitem>
<para>
@@ -439,89 +429,149 @@ bin/add-user.[sh|bat] -r master -u <username> -p <password>
</listitem>
</varlistentry>
<varlistentry>
- <term>hostname-verification-policy</term>
+ <term>client-keystore</term>
<listitem>
<para>
- <literal>WILDCARD</literal> by default. For HTTPS requests, this verifies the hostname
- of the server's certificate. <literal>ANY</literal> means that the hostname is not verified.
- <literal>WILDCARD</literal> Allows wildcards in subdomain names i.e. *.foo.com.
- <literal>STRICT</literal> CN must match hostname exactly.
+ This is the file path to a Java keystore file.
+ This keystore contains client certificate for two-way SSL.
</para>
</listitem>
</varlistentry>
<varlistentry>
- <term>truststore</term>
+ <term>client-keystore-password</term>
<listitem>
<para>
- The value is the file path to a Java keystore file. If
- you prefix the path with <literal>classpath:</literal>, then the truststore will be obtained
- from the deployment's classpath instead.
- HTTPS
- requests need a way to verify the host of the server they are talking to. This is
- what the trustore does. The keystore contains one or more trusted
- host certificates or certificate authorities.
+ Password for the client keystore.
+ This is
+ <emphasis>REQUIRED</emphasis>
+ if
+ <literal>client-keystore</literal>
+ is set.
</para>
</listitem>
</varlistentry>
<varlistentry>
- <term>truststore-password</term>
+ <term>client-key-password</term>
<listitem>
<para>
- Password for the truststore keystore.
+ <emphasis>Not supported yet, but we will support in future versions.</emphasis>
+ Password for the client's key.
This is
<emphasis>REQUIRED</emphasis>
if
- <literal>truststore</literal>
+ <literal>client-keystore</literal>
is set.
</para>
</listitem>
</varlistentry>
+ </variablelist>
+ </para>
+ </section>
+ <section id="truststore">
+ <title>Securing Outgoing Server HTTP Requests</title>
+ <para>
+ When Keycloak connects out to remote HTTP endpoints over secure https connection, it has to validate the other
+ server's certificate in order to ensure it is connecting to a trusted server. That is necessary in order to
+ prevent man-in-the-middle attacks.
+ </para>
+ <para>
+ How certificates are validated is configured in the <literal>standalone/configuration/keycloak-server.json</literal>.
+ By default truststore provider is not configured, and any https connections fall back to standard java truststore
+ configuration as described in <ulink url="https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html">
+ Java's JSSE Reference Guide</ulink> - using <literal>javax.net.ssl.trustStore system property</literal>,
+ otherwise <literal>cacerts</literal> file that comes with java is used.
+ </para>
+ <para>
+ Truststore is used when connecting securely to identity brokers, LDAP identity providers, when sending emails,
+ and for backchannel communication with client applications.
+
+ Some of these facilities may - in case when no trusted certificate is found in your configured truststore -
+ fallback to using the JSSE provided truststore.
+
+ The default JavaMail API implementation used to send out emails behaves in this way, for example.
+ </para>
+ <para>
+ You can add your truststore configuration by using the following template:
+
+ <programlisting><![CDATA[
+"truststore": {
+ "file": {
+ "file": "path to your .jks file containing public certificates",
+ "password": "password",
+ "hostname-verification-policy": "WILDCARD",
+ "disabled": false
+ }
+}
+]]></programlisting>
+
+ </para>
+ <para>
+ Possible configuration options are:
+
+ <variablelist>
<varlistentry>
- <term>client-keystore</term>
+ <term>file</term>
<listitem>
<para>
- This is the file path to a Java keystore file.
- This keystore contains client certificate for two-way SSL.
+ The value is the file path to a Java keystore file.
+ HTTPS requests need a way to verify the host of the server they are talking to. This is
+ what the trustore does. The keystore contains one or more trusted host certificates or
+ certificate authorities. Truststore file should only contain public certificates of your secured hosts.
+
+ This is
+ <emphasis>REQUIRED</emphasis>
+ if <literal>disabled</literal> is not true.
</para>
</listitem>
</varlistentry>
<varlistentry>
- <term>client-keystore-password</term>
+ <term>password</term>
<listitem>
<para>
- Password for the client keystore.
+ Password for the truststore.
This is
<emphasis>REQUIRED</emphasis>
- if
- <literal>client-keystore</literal>
- is set.
+ if <literal>disabled</literal> is not true.
</para>
</listitem>
</varlistentry>
<varlistentry>
- <term>client-key-password</term>
+ <term>hostname-verification-policy</term>
<listitem>
<para>
- <emphasis>Not supported yet, but we will support in future versions.</emphasis>
- Password for the client's key.
- This is
- <emphasis>REQUIRED</emphasis>
- if
- <literal>client-keystore</literal>
- is set.
+ <literal>WILDCARD</literal> by default. For HTTPS requests, this verifies the hostname
+ of the server's certificate. <literal>ANY</literal> means that the hostname is not verified.
+ <literal>WILDCARD</literal> Allows wildcards in subdomain names i.e. *.foo.com.
+ <literal>STRICT</literal> CN must match hostname exactly.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>disabled</term>
+ <listitem>
+ <para>
+ If true (default value), truststore configuration will be ignored, and certificate checking will
+ fall back to JSSE configuration as described. If set to false, you must
+ configure <literal>file</literal>, and <literal>password</literal> for the truststore.
</para>
</listitem>
</varlistentry>
</variablelist>
</para>
+ <para>
+ You can use <emphasis>keytool</emphasis> to create a new truststore file and add trusted host certificates to it:
+
+ <programlisting>
+$ keytool -import -alias HOSTDOMAIN -keystore truststore.jks -file host-certificate.cer
+ </programlisting>
+ </para>
</section>
<section id="ssl_modes">
<title>SSL/HTTPS Requirement/Modes</title>
<warning>
<para>
- Keycloak is not set up by default to handle SSL/HTTPS in either the
- war distribution or appliance. It is highly recommended that you either enable SSL on the Keycloak server
- itself or on a reverse proxy in front of the Keycloak server.
+ Keycloak is not set up by default to handle SSL/HTTPS. It is highly recommended that you either enable SSL
+ on the Keycloak server itself or on a reverse proxy in front of the Keycloak server.
</para>
</warning>
<para>
examples/cordova/www/config.xml 5(+5 -0)
diff --git a/examples/cordova/www/config.xml b/examples/cordova/www/config.xml
index 111b45b..f457a56 100644
--- a/examples/cordova/www/config.xml
+++ b/examples/cordova/www/config.xml
@@ -12,4 +12,9 @@
<gap:plugin name="cordova-plugin-whitelist" version="1.0.0" source="npm" />
<access origin="*"/>
+
+ <allow-navigation href="*" />
+
+ <allow-intent href="http://*/*" />
+ <allow-intent href="https://*/*" />
</widget>
examples/cordova/www/index.html 2(+2 -0)
diff --git a/examples/cordova/www/index.html b/examples/cordova/www/index.html
index a11ab24..c943afc 100644
--- a/examples/cordova/www/index.html
+++ b/examples/cordova/www/index.html
@@ -3,6 +3,8 @@
<head>
<title>Authentication Example</title>
+ <meta http-equiv="Content-Security-Policy" content="default-src *; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval'">
+
<script type="text/javascript" charset="utf-8" src="cordova.js"></script>
<script type="text/javascript" charset="utf-8" src="keycloak.js"></script>
<script type="text/javascript" charset="utf-8">
diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPOperationManager.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPOperationManager.java
index 3d0d22b..612fd73 100644
--- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPOperationManager.java
+++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPOperationManager.java
@@ -467,6 +467,9 @@ public class LDAPOperationManager {
if (protocol != null) {
env.put(Context.SECURITY_PROTOCOL, protocol);
+ if ("ssl".equals(protocol)) {
+ env.put("java.naming.ldap.factory.socket", "org.keycloak.connections.truststore.SSLSocketFactory");
+ }
}
String bindDN = this.config.getBindDN();
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
index be30adf..69df83e 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js
@@ -406,6 +406,16 @@ module.config([ '$routeProvider', function($routeProvider) {
},
controller : 'RealmEventsConfigCtrl'
})
+ .when('/realms/:realm/partial-import', {
+ templateUrl : resourceUrl + '/partials/partial-import.html',
+ resolve : {
+ resourceName : function() { return 'users'},
+ realm : function(RealmLoader) {
+ return RealmLoader();
+ }
+ },
+ controller : 'RealmImportCtrl'
+ })
.when('/create/user/:realm', {
templateUrl : resourceUrl + '/partials/user-detail.html',
resolve : {
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
index eb58ca0..b0853cb 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/clients.js
@@ -1055,8 +1055,10 @@ module.controller('CreateClientCtrl', function($scope, realm, client, templates,
'saml'];//Object.keys(serverInfo.providers['login-protocol'].providers).sort();
$scope.create = true;
$scope.templates = [ {name:'NONE'}];
+ var templateNameMap = new Object();
for (var i = 0; i < templates.length; i++) {
var template = templates[i];
+ templateNameMap[template.name] = template;
$scope.templates.push(template);
}
@@ -1096,6 +1098,18 @@ module.controller('CreateClientCtrl', function($scope, realm, client, templates,
$scope.changed = true;
}
+ $scope.changeTemplate = function() {
+ if ($scope.client.clientTemplate == 'NONE') {
+ $scope.protocol = 'openid-connect';
+ $scope.client.protocol = 'openid-connect';
+ $scope.client.clientTemplate = null;
+
+ } else {
+ var template = templateNameMap[$scope.client.clientTemplate];
+ $scope.protocol = template.protocol;
+ $scope.client.protocol = template.protocol;
+ }
+ }
$scope.changeProtocol = function() {
if ($scope.protocol == "openid-connect") {
$scope.client.protocol = "openid-connect";
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
index 445b8d0..a3c99e8 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js
@@ -2062,14 +2062,210 @@ module.controller('ClientInitialAccessCreateCtrl', function($scope, realm, Clien
};
});
+module.controller('RealmImportCtrl', function($scope, realm, $route,
+ Notifications, $modal, $resource) {
+ $scope.rawContent = {};
+ $scope.fileContent = {
+ enabled: true
+ };
+ $scope.changed = false;
+ $scope.files = [];
+ $scope.realm = realm;
+ $scope.overwrite = false;
+ $scope.skip = false;
+ $scope.importUsers = false;
+ $scope.importClients = false;
+ $scope.importIdentityProviders = false;
+ $scope.importRealmRoles = false;
+ $scope.importClientRoles = false;
+ $scope.ifResourceExists='FAIL';
+ $scope.isMultiRealm = false;
+ $scope.results = {};
+ $scope.currentPage = 0;
+ var pageSize = 15;
+
+ var oldCopy = angular.copy($scope.fileContent);
+ $scope.importFile = function($fileContent){
+ var parsed;
+ try {
+ parsed = JSON.parse($fileContent);
+ } catch (e) {
+ Notifications.error('Unable to parse JSON file.');
+ return;
+ }
+
+ $scope.rawContent = angular.copy(parsed);
+ if (($scope.rawContent instanceof Array) && ($scope.rawContent.length > 0)) {
+ if ($scope.rawContent.length > 1) $scope.isMultiRealm = true;
+ $scope.fileContent = $scope.rawContent[0];
+ } else {
+ $scope.fileContent = $scope.rawContent;
+ }
+
+ $scope.importing = true;
+ $scope.importUsers = $scope.hasArray('users');
+ $scope.importClients = $scope.hasArray('clients');
+ $scope.importIdentityProviders = $scope.hasArray('identityProviders');
+ $scope.importRealmRoles = $scope.hasRealmRoles();
+ $scope.importClientRoles = $scope.hasClientRoles();
+ $scope.results = {};
+ if (!$scope.hasResources()) {
+ $scope.nothingToImport();
+ }
+ };
+ $scope.hasResults = function() {
+ return (Object.keys($scope.results).length > 0) &&
+ ($scope.results.results !== undefined) &&
+ ($scope.results.results.length > 0);
+ }
+
+ $scope.resultsPage = function() {
+ if (!$scope.hasResults()) return {};
+ return $scope.results.results.slice(startIndex(), endIndex());
+ }
+
+ function startIndex() {
+ return pageSize * $scope.currentPage;
+ }
+
+ function endIndex() {
+ var length = $scope.results.results.length;
+ var endIndex = startIndex() + pageSize;
+ if (endIndex > length) endIndex = length;
+ return endIndex;
+ }
+
+ $scope.setFirstPage = function() {
+ $scope.currentPage = 0;
+ }
+
+ $scope.setNextPage = function() {
+ $scope.currentPage++;
+ }
+
+ $scope.setPreviousPage = function() {
+ $scope.currentPage--;
+ }
+
+ $scope.hasNext = function() {
+ if (!$scope.hasResults()) return false;
+ var length = $scope.results.results.length;
+ //console.log('length=' + length);
+ var endIndex = startIndex() + pageSize;
+ //console.log('endIndex=' + endIndex);
+ return length > endIndex;
+ }
+
+ $scope.hasPrevious = function() {
+ if (!$scope.hasResults()) return false;
+ return $scope.currentPage > 0;
+ }
+
+ $scope.viewImportDetails = function() {
+ $modal.open({
+ templateUrl: resourceUrl + '/partials/modal/view-object.html',
+ controller: 'ObjectModalCtrl',
+ resolve: {
+ object: function () {
+ return $scope.fileContent;
+ }
+ }
+ })
+ };
+
+ $scope.hasArray = function(section) {
+ return ($scope.fileContent !== 'undefined') &&
+ ($scope.fileContent.hasOwnProperty(section)) &&
+ ($scope.fileContent[section] instanceof Array) &&
+ ($scope.fileContent[section].length > 0);
+ }
+
+ $scope.hasRealmRoles = function() {
+ return $scope.hasRoles() &&
+ ($scope.fileContent.roles.hasOwnProperty('realm')) &&
+ ($scope.fileContent.roles.realm instanceof Array) &&
+ ($scope.fileContent.roles.realm.length > 0);
+ }
+
+ $scope.hasRoles = function() {
+ return ($scope.fileContent !== 'undefined') &&
+ ($scope.fileContent.hasOwnProperty('roles')) &&
+ ($scope.fileContent.roles !== 'undefined');
+ }
+
+ $scope.hasClientRoles = function() {
+ return $scope.hasRoles() &&
+ ($scope.fileContent.roles.hasOwnProperty('client')) &&
+ (Object.keys($scope.fileContent.roles.client).length > 0);
+ }
+
+ $scope.itemCount = function(section) {
+ if (!$scope.importing) return 0;
+ if ($scope.hasRealmRoles() && (section === 'roles.realm')) return $scope.fileContent.roles.realm.length;
+ if ($scope.hasClientRoles() && (section === 'roles.client')) return Object.keys($scope.fileContent.roles.client).length;
+
+ if (!$scope.fileContent.hasOwnProperty(section)) return 0;
+
+ return $scope.fileContent[section].length;
+ }
+
+ $scope.hasResources = function() {
+ return ($scope.importUsers && $scope.hasArray('users')) ||
+ ($scope.importClients && $scope.hasArray('clients')) ||
+ ($scope.importIdentityProviders && $scope.hasArray('identityProviders')) ||
+ ($scope.importRealmRoles && $scope.hasRealmRoles()) ||
+ ($scope.importClientRoles && $scope.hasClientRoles());
+ }
+
+ $scope.nothingToImport = function() {
+ Notifications.error('No resouces specified to import.');
+ }
+
+ $scope.$watch('fileContent', function() {
+ if (!angular.equals($scope.fileContent, oldCopy)) {
+ $scope.changed = true;
+ }
+ }, true);
+
+ $scope.successMessage = function() {
+ var message = $scope.results.added + ' records added. ';
+ if ($scope.ifResourceExists === 'SKIP') {
+ message += $scope.results.skipped + ' records skipped.'
+ }
+ if ($scope.ifResourceExists === 'OVERWRITE') {
+ message += $scope.results.overwritten + ' records overwritten.';
+ }
+ return message;
+ }
+
+ $scope.save = function() {
+ var json = angular.copy($scope.fileContent);
+ json.ifResourceExists = $scope.ifResourceExists;
+ if (!$scope.importUsers) delete json.users;
+ if (!$scope.importIdentityProviders) delete json.identityProviders;
+ if (!$scope.importClients) delete json.clients;
+
+ if (json.hasOwnProperty('roles')) {
+ if (!$scope.importRealmRoles) delete json.roles.realm;
+ if (!$scope.importClientRoles) delete json.roles.client;
+ }
+
+ var importFile = $resource(authUrl + '/admin/realms/' + realm.realm + '/partialImport');
+ $scope.results = importFile.save(json, function() {
+ Notifications.success($scope.successMessage());
+ }, function(error) {
+ if (error.data.errorMessage) {
+ Notifications.error(error.data.errorMessage);
+ } else {
+ Notifications.error('Unexpected error during import');
+ }
+ });
+ };
+
+ $scope.reset = function() {
+ $route.reload();
+ }
-
-
-
-
-
-
-
-
+});
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-client.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-client.html
index dd2a962..e1a56de 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-client.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/create-client.html
@@ -37,7 +37,8 @@
<select class="form-control" id="protocol"
ng-change="changeProtocol()"
ng-model="protocol"
- ng-options="aProtocol for aProtocol in protocols">
+ ng-options="aProtocol for aProtocol in protocols"
+ ng-disabled="client.clientTemplate">
</select>
</div>
</div>
@@ -48,6 +49,7 @@
<div class="col-sm-6">
<div>
<select class="form-control" id="template"
+ ng-change="changeTemplate()"
ng-model="client.clientTemplate"
ng-options="template.name as template.name for template in templates">
</select>
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/partial-import.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/partial-import.html
new file mode 100644
index 0000000..d15cd71
--- /dev/null
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/partial-import.html
@@ -0,0 +1,120 @@
+<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
+
+ <h1>Partial Import</h1>
+
+ <form class="form-horizontal" name="partialImportForm" novalidate>
+ <fieldset class="border-top">
+ <div class="form-group">
+ <label for="name" class="col-sm-2 control-label">File</label>
+
+ <div class="col-md-6" data-ng-hide="importing">
+ <label for="import-file" class="btn btn-default">{{:: 'select-file'| translate}} <i class="pficon pficon-import"></i></label>
+ <input id="import-file" type="file" class="hidden" kc-on-read-file="importFile($fileContent)"/>
+ </div>
+
+ <div class="col-md-6" data-ng-show="importing">
+ <button class="btn btn-default" data-ng-click="viewImportDetails()">{{:: 'view-details'| translate}}</button>
+ <button class="btn btn-default" data-ng-click="reset()">{{:: 'clear-import'| translate}}</button>
+ </div>
+ </div>
+
+ <div class="form-group" data-ng-show="importing && isMultiRealm && !hasResults()">
+ <label for="fromRealm" class="col-md-2 control-label">Import from realm</label>
+ <div class="col-md-2">
+ <div>
+ <select id="fromRealm" ng-model="fileContent" class="form-control"
+ ng-options="item as item.realm for item in rawContent">
+ </select>
+ </div>
+ </div>
+ </div>
+
+ <div class="form-group" data-ng-show="importing && hasArray('users') && !hasResults()">
+ <label class="col-md-2 control-label" for="importUsers">Import Users ({{itemCount('users')}})</label>
+ <div class="col-sm-6">
+ <input ng-model="importUsers" name="importUsers" id="importUsers" onoffswitch on-text="{{:: 'onText'| translate}}" off-text="{{:: 'offText'| translate}}"/>
+ </div>
+ </div>
+
+ <div class="form-group" data-ng-show="importing && hasArray('clients') && !hasResults()">
+ <label class="col-md-2 control-label" for="importClients">Import Clients ({{itemCount('clients')}})</label>
+ <div class="col-sm-6">
+ <input ng-model="importClients" name="importClients" id="importClients" onoffswitch on-text="{{:: 'onText'| translate}}" off-text="{{:: 'offText'| translate}}"/>
+ </div>
+ </div>
+
+ <div class="form-group" data-ng-show="importing && hasArray('identityProviders') && !hasResults()">
+ <label class="col-md-2 control-label" for="importIdentityProviders">Import Identity Providers ({{itemCount('identityProviders')}})</label>
+ <div class="col-sm-6">
+ <input ng-model="importIdentityProviders" name="importIdentityProviders" id="importIdentityProviders" onoffswitch on-text="{{:: 'onText'| translate}}" off-text="{{:: 'offText'| translate}}"/>
+ </div>
+ </div>
+
+ <div class="form-group" data-ng-show="importing && hasRealmRoles() && !hasResults()">
+ <label class="col-md-2 control-label" for="importRealmRoles">Import Realm Roles ({{itemCount('roles.realm')}})</label>
+ <div class="col-sm-6">
+ <input ng-model="importRealmRoles" name="importRealmRoles" id="importRealmRoles" onoffswitch on-text="{{:: 'onText'| translate}}" off-text="{{:: 'offText'| translate}}"/>
+ </div>
+ </div>
+
+ <div class="form-group" data-ng-show="importing && hasClientRoles() && !hasResults()">
+ <label class="col-md-2 control-label" for="importClientRoles">Import Client Roles ({{itemCount('roles.client')}})</label>
+ <div class="col-sm-6">
+ <input ng-model="importClientRoles" name="importClientRoles" id="importClientRoles" onoffswitch on-text="{{:: 'onText'| translate}}" off-text="{{:: 'offText'| translate}}"/>
+ </div>
+ </div>
+
+ <div class="form-group" data-ng-show="importing && hasResources() && !hasResults()">
+ <label for="ifResourceExists" class="col-md-2 control-label">If a resource exists</label>
+ <div class="col-md-2">
+ <div>
+ <select id="ifResourceExists" ng-model="ifResourceExists" class="form-control">
+ <option value="FAIL">Fail</option>
+ <option value="SKIP">Skip</option>
+ <option value="OVERWRITE">Overwrite</option>
+ </select>
+ </div>
+ </div>
+ <kc-tooltip>Specify what should be done if you try to import a resource that already exists.</kc-tooltip>
+ </div>
+ </fieldset>
+
+ <div class="form-group" data-ng-show="importing && hasResources() && !hasResults()">
+ <div class="col-md-10 col-md-offset-2">
+ <button kc-save data-ng-disabled="!changed">{{:: 'import'| translate}}</button>
+ </div>
+ </div>
+
+ <div class="form-group" data-ng-show="hasResults()">
+ {{successMessage()}}
+ <table class="table table-striped table-bordered">
+ <thead>
+ <tr>
+ <th>Action</th>
+ <th>Type</th>
+ <th>Name</th>
+ <th>Id</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="result in resultsPage()" >
+ <td ng-show="result.action == 'OVERWRITTEN'"><span class="label label-danger">{{result.action}}</span></td>
+ <td ng-show="result.action == 'SKIPPED'"><span class="label label-warning">{{result.action}}</span></td>
+ <td ng-show="result.action == 'ADDED'"><span class="label label-success">{{result.action}}</span></td>
+ <td>{{result.resourceType}}</td>
+ <td>{{result.resourceName}}</td>
+ <td>{{result.id}}</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <div class="table-nav">
+ <button data-ng-click="setFirstPage()" class="first" ng-disabled="">First page</button>
+ <button data-ng-click="setPreviousPage()" class="prev" ng-disabled="!hasPrevious()">Previous page</button>
+ <button data-ng-click="setNextPage()" class="next" ng-disabled="!hasNext()">Next page</button>
+ </div>
+ </div>
+ </form>
+</div>
+
+<kc-menu></kc-menu>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html
index 5904fd7..dabb36c 100755
--- a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html
+++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html
@@ -45,6 +45,7 @@
<li data-ng-show="access.viewUsers" data-ng-class="(path[2] == 'users' || path[1] == 'user') && 'active'"><a href="#/realms/{{realm.realm}}/users"><span class="pficon pficon-user"></span> Users</a></li>
<li data-ng-show="access.viewRealm" data-ng-class="(path[2] == 'sessions') && 'active'"><a href="#/realms/{{realm.realm}}/sessions/realm"><i class="fa fa-clock-o"></i> Sessions</a></li>
<li data-ng-show="access.viewEvents" data-ng-class="(path[2] == 'events' || path[2] == 'events-settings') && 'active'"><a href="#/realms/{{realm.realm}}/events"><i class="fa fa-calendar"></i> Events</a></li>
+ <li data-ng-show="access.manageRealm" ng-class="(path[2] =='partial-import') && 'active'"><a href="#/realms/{{realm.realm}}/partial-import"><span class="pficon pficon-import"></span> Import</a></li>
</ul>
</div>
</div>
\ No newline at end of file
diff --git a/forms/common-themes/src/main/resources/theme/keycloak/login/resources/css/login.css b/forms/common-themes/src/main/resources/theme/keycloak/login/resources/css/login.css
index f951c1d..0c05a4b 100644
--- a/forms/common-themes/src/main/resources/theme/keycloak/login/resources/css/login.css
+++ b/forms/common-themes/src/main/resources/theme/keycloak/login/resources/css/login.css
@@ -75,9 +75,6 @@
background-image: url("../img/keycloak-logo.png");
background-repeat: no-repeat;
- position: absolute;
- top: 50px;
- right: 50px;
height: 37px;
width: 154px;
}
@@ -96,12 +93,6 @@
margin-bottom: 15px;
}
-#kc-container-wrapper {
- bottom: 13%;
- position: absolute;
- width: 100%;
-}
-
#kc-content {
position: relative;
}
@@ -280,16 +271,33 @@ ol#kc-totp-settings li:first-of-type {
background-image: linear-gradient(rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%) !important;
}
+@media (min-width: 768px) {
+ #kc-container-wrapper {
+ bottom: 13%;
+ position: absolute;
+ width: 100%;
+ }
+
+ #kc-logo-wrapper {
+ position: absolute;
+ top: 50px;
+ right: 50px;
+ }
+}
+
@media (max-width: 767px) {
+
#kc-logo-wrapper {
- top: 15px;
- right: 15px;
+ background-position: center;
+ width: 100%;
+ margin: 20px 0;
}
#kc-header {
padding-left: 15px;
padding-right: 15px;
float: none;
+ text-align: center;
}
#kc-feedback {
@@ -323,7 +331,5 @@ ol#kc-totp-settings li:first-of-type {
@media (max-height: 500px) {
#kc-container-wrapper {
- position: inherit;
- float: none;
}
}
diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
index f40f962..87394a1 100755
--- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
+++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java
@@ -66,6 +66,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
+import org.keycloak.representations.idm.RolesRepresentation;
public class RepresentationToModel {
@@ -195,47 +196,7 @@ public class RepresentationToModel {
createClients(session, rep, newRealm);
}
- if (rep.getRoles() != null) {
- if (rep.getRoles().getRealm() != null) { // realm roles
- for (RoleRepresentation roleRep : rep.getRoles().getRealm()) {
- createRole(newRealm, roleRep);
- }
- }
- if (rep.getRoles().getClient() != null) {
- for (Map.Entry<String, List<RoleRepresentation>> entry : rep.getRoles().getClient().entrySet()) {
- ClientModel client = newRealm.getClientByClientId(entry.getKey());
- if (client == null) {
- throw new RuntimeException("App doesn't exist in role definitions: " + entry.getKey());
- }
- for (RoleRepresentation roleRep : entry.getValue()) {
- // Application role may already exists (for example if it is defaultRole)
- RoleModel role = roleRep.getId()!=null ? client.addRole(roleRep.getId(), roleRep.getName()) : client.addRole(roleRep.getName());
- role.setDescription(roleRep.getDescription());
- boolean scopeParamRequired = roleRep.isScopeParamRequired()==null ? false : roleRep.isScopeParamRequired();
- role.setScopeParamRequired(scopeParamRequired);
- }
- }
- }
- // now that all roles are created, re-iterate and set up composites
- if (rep.getRoles().getRealm() != null) { // realm roles
- for (RoleRepresentation roleRep : rep.getRoles().getRealm()) {
- RoleModel role = newRealm.getRole(roleRep.getName());
- addComposites(role, roleRep, newRealm);
- }
- }
- if (rep.getRoles().getClient() != null) {
- for (Map.Entry<String, List<RoleRepresentation>> entry : rep.getRoles().getClient().entrySet()) {
- ClientModel client = newRealm.getClientByClientId(entry.getKey());
- if (client == null) {
- throw new RuntimeException("App doesn't exist in role definitions: " + entry.getKey());
- }
- for (RoleRepresentation roleRep : entry.getValue()) {
- RoleModel role = client.getRole(roleRep.getName());
- addComposites(role, roleRep, newRealm);
- }
- }
- }
- }
+ importRoles(rep.getRoles(), newRealm);
// Setup realm default roles
if (rep.getDefaultRoles() != null) {
@@ -356,6 +317,50 @@ public class RepresentationToModel {
}
}
+ public static void importRoles(RolesRepresentation realmRoles, RealmModel realm) {
+ if (realmRoles == null) return;
+
+ if (realmRoles.getRealm() != null) { // realm roles
+ for (RoleRepresentation roleRep : realmRoles.getRealm()) {
+ createRole(realm, roleRep);
+ }
+ }
+ if (realmRoles.getClient() != null) {
+ for (Map.Entry<String, List<RoleRepresentation>> entry : realmRoles.getClient().entrySet()) {
+ ClientModel client = realm.getClientByClientId(entry.getKey());
+ if (client == null) {
+ throw new RuntimeException("App doesn't exist in role definitions: " + entry.getKey());
+ }
+ for (RoleRepresentation roleRep : entry.getValue()) {
+ // Application role may already exists (for example if it is defaultRole)
+ RoleModel role = roleRep.getId()!=null ? client.addRole(roleRep.getId(), roleRep.getName()) : client.addRole(roleRep.getName());
+ role.setDescription(roleRep.getDescription());
+ boolean scopeParamRequired = roleRep.isScopeParamRequired()==null ? false : roleRep.isScopeParamRequired();
+ role.setScopeParamRequired(scopeParamRequired);
+ }
+ }
+ }
+ // now that all roles are created, re-iterate and set up composites
+ if (realmRoles.getRealm() != null) { // realm roles
+ for (RoleRepresentation roleRep : realmRoles.getRealm()) {
+ RoleModel role = realm.getRole(roleRep.getName());
+ addComposites(role, roleRep, realm);
+ }
+ }
+ if (realmRoles.getClient() != null) {
+ for (Map.Entry<String, List<RoleRepresentation>> entry : realmRoles.getClient().entrySet()) {
+ ClientModel client = realm.getClientByClientId(entry.getKey());
+ if (client == null) {
+ throw new RuntimeException("App doesn't exist in role definitions: " + entry.getKey());
+ }
+ for (RoleRepresentation roleRep : entry.getValue()) {
+ RoleModel role = client.getRole(roleRep.getName());
+ addComposites(role, roleRep, realm);
+ }
+ }
+ }
+ }
+
public static void importGroups(RealmModel realm, RealmRepresentation rep) {
List<GroupRepresentation> groups = rep.getGroups();
if (groups == null) return;
@@ -639,15 +644,15 @@ public class RepresentationToModel {
if (rep.getAccountTheme() != null) realm.setAccountTheme(rep.getAccountTheme());
if (rep.getAdminTheme() != null) realm.setAdminTheme(rep.getAdminTheme());
if (rep.getEmailTheme() != null) realm.setEmailTheme(rep.getEmailTheme());
-
+
if (rep.isEventsEnabled() != null) realm.setEventsEnabled(rep.isEventsEnabled());
if (rep.getEventsExpiration() != null) realm.setEventsExpiration(rep.getEventsExpiration());
if (rep.getEventsListeners() != null) realm.setEventsListeners(new HashSet<>(rep.getEventsListeners()));
if (rep.getEnabledEventTypes() != null) realm.setEnabledEventTypes(new HashSet<>(rep.getEnabledEventTypes()));
-
+
if (rep.isAdminEventsEnabled() != null) realm.setAdminEventsEnabled(rep.isAdminEventsEnabled());
if (rep.isAdminEventsDetailsEnabled() != null) realm.setAdminEventsDetailsEnabled(rep.isAdminEventsDetailsEnabled());
-
+
if (rep.getPasswordPolicy() != null) realm.setPasswordPolicy(new PasswordPolicy(rep.getPasswordPolicy()));
if (rep.getOtpPolicyType() != null) realm.setOTPPolicy(toPolicy(rep));
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
index 36fd1fd..f44abe8 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java
@@ -76,9 +76,12 @@ public class JpaUserProvider implements UserProvider {
userModel.joinGroup(g);
}
}
- for (RequiredActionProviderModel r : realm.getRequiredActionProviders()) {
- if (r.isEnabled() && r.isDefaultAction()) {
- userModel.addRequiredAction(r.getAlias());
+
+ if (addDefaultRequiredActions){
+ for (RequiredActionProviderModel r : realm.getRequiredActionProviders()) {
+ if (r.isEnabled() && r.isDefaultAction()) {
+ userModel.addRequiredAction(r.getAlias());
+ }
}
}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
index ce72bba..d5be946 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
@@ -1059,6 +1059,7 @@ public class RealmAdapter implements RealmModel {
em.createNamedQuery("deleteGroupRoleMappingsByRole").setParameter("roleId", roleEntity.getId()).executeUpdate();
em.remove(roleEntity);
+ em.flush();
return true;
}
@@ -1217,7 +1218,7 @@ public class RealmAdapter implements RealmModel {
realm.setEventsListeners(listeners);
em.flush();
}
-
+
@Override
public Set<String> getEnabledEventTypes() {
return realm.getEnabledEventTypes();
@@ -1228,7 +1229,7 @@ public class RealmAdapter implements RealmModel {
realm.setEnabledEventTypes(enabledEventTypes);
em.flush();
}
-
+
@Override
public boolean isAdminEventsEnabled() {
return realm.isAdminEventsEnabled();
@@ -1250,7 +1251,7 @@ public class RealmAdapter implements RealmModel {
realm.setAdminEventsDetailsEnabled(enabled);
em.flush();
}
-
+
@Override
public ClientModel getMasterAdminClient() {
ClientEntity masterAdminClient = realm.getMasterAdminClient();
pom.xml 5(+5 -0)
diff --git a/pom.xml b/pom.xml
index 03e1416..83ac183 100755
--- a/pom.xml
+++ b/pom.xml
@@ -642,6 +642,11 @@
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
+ <artifactId>keycloak-connections-truststore</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.keycloak</groupId>
<artifactId>keycloak-common</artifactId>
<version>${project.version}</version>
</dependency>
README.md 2(+1 -1)
diff --git a/README.md b/README.md
index 68c2ff0..c2e67fc 100755
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@ Keycloak is an SSO Service for web apps and REST services. For more information
Building
--------
-Ensure you have JDK 7 (or newer), Maven 3.2.1 (or newer) and Git installed
+Ensure you have JDK 8 (or newer), Maven 3.2.1 (or newer) and Git installed
java -version
mvn -version
diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java b/services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java
index cb5ecd2..b34f040 100755
--- a/services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java
+++ b/services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java
@@ -1,9 +1,14 @@
package org.keycloak.authentication.requiredactions;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Date;
+
import org.keycloak.Config;
import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider;
+import org.keycloak.common.util.Time;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
@@ -14,8 +19,8 @@ import javax.ws.rs.core.Response;
* @version $Revision: 1 $
*/
public class TermsAndConditions implements RequiredActionProvider, RequiredActionFactory {
-
public static final String PROVIDER_ID = "terms_and_conditions";
+ public static final String USER_ATTRIBUTE = PROVIDER_ID;
@Override
public RequiredActionProvider create(KeycloakSession session) {
@@ -46,18 +51,21 @@ public class TermsAndConditions implements RequiredActionProvider, RequiredActio
@Override
public void requiredActionChallenge(RequiredActionContext context) {
- Response challenge = context.form().createForm("terms.ftl");
+ Response challenge = context.form().createForm("terms.ftl");
context.challenge(challenge);
}
@Override
public void processAction(RequiredActionContext context) {
if (context.getHttpRequest().getDecodedFormParameters().containsKey("cancel")) {
+ context.getUser().removeAttribute(USER_ATTRIBUTE);
context.failure();
return;
}
- context.success();
+ context.getUser().setAttribute(USER_ATTRIBUTE, Arrays.asList(Integer.toString(Time.currentTime())));
+
+ context.success();
}
@Override
diff --git a/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java b/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java
index 1a06877..5f7f68b 100644
--- a/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java
+++ b/services/src/main/java/org/keycloak/email/DefaultEmailSenderProvider.java
@@ -1,6 +1,9 @@
package org.keycloak.email;
import org.jboss.logging.Logger;
+import org.keycloak.connections.truststore.HostnameVerificationPolicy;
+import org.keycloak.connections.truststore.JSSETruststoreConfigurator;
+import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
@@ -12,6 +15,8 @@ import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.Map;
import java.util.Properties;
@@ -23,6 +28,12 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider {
private static final Logger log = Logger.getLogger(DefaultEmailSenderProvider.class);
+ private final KeycloakSession session;
+
+ public DefaultEmailSenderProvider(KeycloakSession session) {
+ this.session = session;
+ }
+
@Override
public void send(RealmModel realm, UserModel user, String subject, String textBody, String htmlBody) throws EmailException {
try {
@@ -52,6 +63,10 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider {
props.setProperty("mail.smtp.starttls.enable", "true");
}
+ if (ssl || starttls) {
+ setupTruststore(props);
+ }
+
props.setProperty("mail.smtp.timeout", "10000");
props.setProperty("mail.smtp.connectiontimeout", "10000");
@@ -94,9 +109,18 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider {
}
}
+ private void setupTruststore(Properties props) throws NoSuchAlgorithmException, KeyManagementException {
+
+ JSSETruststoreConfigurator configurator = new JSSETruststoreConfigurator(session);
+
+ props.put("mail.smtp.ssl.socketFactory", configurator.getSSLSocketFactory());
+ if (configurator.getProvider().getPolicy() == HostnameVerificationPolicy.ANY) {
+ props.setProperty("mail.smtp.ssl.trust", "*");
+ }
+ }
+
@Override
public void close() {
}
-
}
diff --git a/services/src/main/java/org/keycloak/email/DefaultEmailSenderProviderFactory.java b/services/src/main/java/org/keycloak/email/DefaultEmailSenderProviderFactory.java
index 9677000..de8dfd7 100644
--- a/services/src/main/java/org/keycloak/email/DefaultEmailSenderProviderFactory.java
+++ b/services/src/main/java/org/keycloak/email/DefaultEmailSenderProviderFactory.java
@@ -11,7 +11,7 @@ public class DefaultEmailSenderProviderFactory implements EmailSenderProviderFac
@Override
public EmailSenderProvider create(KeycloakSession session) {
- return new DefaultEmailSenderProvider();
+ return new DefaultEmailSenderProvider(session);
}
@Override
diff --git a/services/src/main/java/org/keycloak/partialimport/AbstractPartialImport.java b/services/src/main/java/org/keycloak/partialimport/AbstractPartialImport.java
new file mode 100644
index 0000000..33f9c12
--- /dev/null
+++ b/services/src/main/java/org/keycloak/partialimport/AbstractPartialImport.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.keycloak.partialimport;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import javax.ws.rs.core.Response;
+import org.jboss.logging.Logger;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.representations.idm.PartialImportRepresentation;
+import org.keycloak.services.ErrorResponse;
+
+/**
+ * Base PartialImport for most resource types.
+ *
+ * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
+ */
+public abstract class AbstractPartialImport<T> implements PartialImport<T> {
+ protected static Logger logger = Logger.getLogger(AbstractPartialImport.class);
+
+ protected final Set<T> toOverwrite = new HashSet<>();
+ protected final Set<T> toSkip = new HashSet<>();
+
+ public abstract List<T> getRepList(PartialImportRepresentation partialImportRep);
+ public abstract String getName(T resourceRep);
+ public abstract String getModelId(RealmModel realm, KeycloakSession session, T resourceRep);
+ public abstract boolean exists(RealmModel realm, KeycloakSession session, T resourceRep);
+ public abstract String existsMessage(T resourceRep);
+ public abstract ResourceType getResourceType();
+ public abstract void remove(RealmModel realm, KeycloakSession session, T resourceRep);
+ public abstract void create(RealmModel realm, KeycloakSession session, T resourceRep);
+
+ @Override
+ public void prepare(PartialImportRepresentation partialImportRep,
+ RealmModel realm,
+ KeycloakSession session) throws ErrorResponseException {
+ List<T> repList = getRepList(partialImportRep);
+ if ((repList == null) || repList.isEmpty()) return;
+
+ for (T resourceRep : getRepList(partialImportRep)) {
+ if (exists(realm, session, resourceRep)) {
+ switch (partialImportRep.getPolicy()) {
+ case SKIP: toSkip.add(resourceRep); break;
+ case OVERWRITE: toOverwrite.add(resourceRep); break;
+ default: throw existsError(existsMessage(resourceRep));
+ }
+ }
+ }
+ }
+
+ protected ErrorResponseException existsError(String message) {
+ Response error = ErrorResponse.exists(message);
+ return new ErrorResponseException(error);
+ }
+
+ protected PartialImportResult overwritten(String modelId, T resourceRep){
+ return PartialImportResult.overwritten(getResourceType(), getName(resourceRep), modelId, resourceRep);
+ }
+
+ protected PartialImportResult skipped(String modelId, T resourceRep) {
+ return PartialImportResult.skipped(getResourceType(), getName(resourceRep), modelId, resourceRep);
+ }
+
+ protected PartialImportResult added(String modelId, T resourceRep) {
+ return PartialImportResult.added(getResourceType(), getName(resourceRep), modelId, resourceRep);
+ }
+
+ @Override
+ public void removeOverwrites(RealmModel realm, KeycloakSession session) {
+ for (T resourceRep : toOverwrite) {
+ remove(realm, session, resourceRep);
+ }
+ }
+
+ @Override
+ public PartialImportResults doImport(PartialImportRepresentation partialImportRep, RealmModel realm, KeycloakSession session) throws ErrorResponseException {
+ PartialImportResults results = new PartialImportResults();
+ List<T> repList = getRepList(partialImportRep);
+ if ((repList == null) || repList.isEmpty()) return results;
+
+ for (T resourceRep : toOverwrite) {
+ try {
+ create(realm, session, resourceRep);
+ } catch (Exception e) {
+ logger.error("Error overwriting " + getName(resourceRep), e);
+ throw new ErrorResponseException(ErrorResponse.error(e.getMessage(), Response.Status.INTERNAL_SERVER_ERROR));
+ }
+
+ String modelId = getModelId(realm, session, resourceRep);
+ results.addResult(overwritten(modelId, resourceRep));
+ }
+
+ for (T resourceRep : toSkip) {
+ String modelId = getModelId(realm, session, resourceRep);
+ results.addResult(skipped(modelId, resourceRep));
+ }
+
+ for (T resourceRep : repList) {
+ if (toOverwrite.contains(resourceRep)) continue;
+ if (toSkip.contains(resourceRep)) continue;
+
+ try {
+ create(realm, session, resourceRep);
+ String modelId = getModelId(realm, session, resourceRep);
+ results.addResult(added(modelId, resourceRep));
+ } catch (Exception e) {
+ logger.error("Error creating " + getName(resourceRep), e);
+ throw new ErrorResponseException(ErrorResponse.error(e.getMessage(), Response.Status.INTERNAL_SERVER_ERROR));
+ }
+ }
+
+ return results;
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/partialimport/Action.java b/services/src/main/java/org/keycloak/partialimport/Action.java
new file mode 100644
index 0000000..86ea54f
--- /dev/null
+++ b/services/src/main/java/org/keycloak/partialimport/Action.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.keycloak.partialimport;
+
+/**
+ * Enum for actions taken by PartialImport.
+ *
+ * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
+ */
+public enum Action {
+ ADDED, SKIPPED, OVERWRITTEN
+}
diff --git a/services/src/main/java/org/keycloak/partialimport/ClientRolesPartialImport.java b/services/src/main/java/org/keycloak/partialimport/ClientRolesPartialImport.java
new file mode 100644
index 0000000..d309b51
--- /dev/null
+++ b/services/src/main/java/org/keycloak/partialimport/ClientRolesPartialImport.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.keycloak.partialimport;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.ws.rs.core.Response;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.PartialImportRepresentation;
+import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.services.ErrorResponse;
+
+/**
+ * Partial Import handler for Client Roles.
+ *
+ * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
+ */
+public class ClientRolesPartialImport {
+ private final Map<String, Set<RoleRepresentation>> toOverwrite = new HashMap<>();
+ private final Map<String, Set<RoleRepresentation>> toSkip = new HashMap<>();
+
+ public Map<String, Set<RoleRepresentation>> getToOverwrite() {
+ return this.toOverwrite;
+ }
+
+ public Map<String, Set<RoleRepresentation>> getToSkip() {
+ return this.toSkip;
+ }
+
+ public Map<String, List<RoleRepresentation>> getRepList(PartialImportRepresentation partialImportRep) {
+ if (partialImportRep.getRoles() == null) return null;
+ return partialImportRep.getRoles().getClient();
+ }
+
+ public String getName(RoleRepresentation roleRep) {
+ if (roleRep.getName() == null)
+ throw new IllegalStateException("Client role to import does not have a name");
+ return roleRep.getName();
+ }
+
+ public String getCombinedName(String clientId, RoleRepresentation roleRep) {
+ return clientId + "-->" + getName(roleRep);
+ }
+
+ public boolean exists(RealmModel realm, KeycloakSession session, String clientId, RoleRepresentation roleRep) {
+ ClientModel client = realm.getClientByClientId(clientId);
+ if (client == null) return false;
+
+ for (RoleModel role : client.getRoles()) {
+ if (getName(roleRep).equals(role.getName())) return true;
+ }
+
+ return false;
+ }
+
+ // check if client currently exists or will exists as a result of this partial import
+ private boolean clientExists(PartialImportRepresentation partialImportRep, RealmModel realm, String clientId) {
+ if (realm.getClientByClientId(clientId) != null) return true;
+
+ if (partialImportRep.getClients() == null) return false;
+
+ for (ClientRepresentation client : partialImportRep.getClients()) {
+ if (clientId.equals(client.getClientId())) return true;
+ }
+
+ return false;
+ }
+
+ public String existsMessage(String clientId, RoleRepresentation roleRep) {
+ return "Client role '" + getName(roleRep) + "' for client '" + clientId + "' already exists.";
+ }
+
+ public ResourceType getResourceType() {
+ return ResourceType.CLIENT_ROLE;
+ }
+
+ public void deleteRole(RealmModel realm, String clientId, RoleRepresentation roleRep) {
+ ClientModel client = realm.getClientByClientId(clientId);
+ if (client == null) {
+ // client might have been removed as part of this partial import
+ return;
+ }
+ RoleModel role = client.getRole(getName(roleRep));
+ client.removeRole(role);
+ }
+
+ public void prepare(PartialImportRepresentation partialImportRep, RealmModel realm, KeycloakSession session) throws ErrorResponseException {
+ Map<String, List<RoleRepresentation>> repList = getRepList(partialImportRep);
+ if (repList == null || repList.isEmpty()) return;
+
+ for (String clientId : repList.keySet()) {
+ if (!clientExists(partialImportRep, realm, clientId)) {
+ throw noClientFound(clientId);
+ }
+
+ toOverwrite.put(clientId, new HashSet<RoleRepresentation>());
+ toSkip.put(clientId, new HashSet<RoleRepresentation>());
+ for (RoleRepresentation roleRep : repList.get(clientId)) {
+ if (exists(realm, session, clientId, roleRep)) {
+ switch (partialImportRep.getPolicy()) {
+ case SKIP:
+ toSkip.get(clientId).add(roleRep);
+ break;
+ case OVERWRITE:
+ toOverwrite.get(clientId).add(roleRep);
+ break;
+ default:
+ throw exists(existsMessage(clientId, roleRep));
+ }
+ }
+ }
+ }
+ }
+
+ protected ErrorResponseException exists(String message) {
+ Response error = ErrorResponse.exists(message);
+ return new ErrorResponseException(error);
+ }
+
+ protected ErrorResponseException noClientFound(String clientId) {
+ String message = "Can not import client roles for nonexistent client named " + clientId;
+ Response error = ErrorResponse.error(message, Response.Status.PRECONDITION_FAILED);
+ return new ErrorResponseException(error);
+ }
+
+ public PartialImportResult overwritten(String clientId, String modelId, RoleRepresentation roleRep) {
+ return PartialImportResult.overwritten(getResourceType(), getCombinedName(clientId, roleRep), modelId, roleRep);
+ }
+
+ public PartialImportResult skipped(String clientId, String modelId, RoleRepresentation roleRep) {
+ return PartialImportResult.skipped(getResourceType(), getCombinedName(clientId, roleRep), modelId, roleRep);
+ }
+
+ public PartialImportResult added(String clientId, String modelId, RoleRepresentation roleRep) {
+ return PartialImportResult.added(getResourceType(), getCombinedName(clientId, roleRep), modelId, roleRep);
+ }
+
+ public String getModelId(RealmModel realm, String clientId) {
+ return realm.getClientByClientId(clientId).getId();
+ }
+}
diff --git a/services/src/main/java/org/keycloak/partialimport/ClientsPartialImport.java b/services/src/main/java/org/keycloak/partialimport/ClientsPartialImport.java
new file mode 100644
index 0000000..c04ab46
--- /dev/null
+++ b/services/src/main/java/org/keycloak/partialimport/ClientsPartialImport.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.keycloak.partialimport;
+
+import java.util.List;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.models.utils.RepresentationToModel;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.PartialImportRepresentation;
+import org.keycloak.representations.idm.ProtocolMapperRepresentation;
+import org.keycloak.services.managers.ClientManager;
+import org.keycloak.services.managers.RealmManager;
+
+/**
+ * PartialImport handler for Clients.
+ *
+ * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
+ */
+public class ClientsPartialImport extends AbstractPartialImport<ClientRepresentation> {
+
+ @Override
+ public List<ClientRepresentation> getRepList(PartialImportRepresentation partialImportRep) {
+ return partialImportRep.getClients();
+ }
+
+ @Override
+ public String getName(ClientRepresentation clientRep) {
+ return clientRep.getClientId();
+ }
+
+ @Override
+ public String getModelId(RealmModel realm, KeycloakSession session, ClientRepresentation clientRep) {
+ return realm.getClientByClientId(getName(clientRep)).getId();
+ }
+
+ @Override
+ public boolean exists(RealmModel realm, KeycloakSession session, ClientRepresentation clientRep) {
+ return realm.getClientByClientId(getName(clientRep)) != null;
+ }
+
+ @Override
+ public String existsMessage(ClientRepresentation clientRep) {
+ return "Client id '" + getName(clientRep) + "' already exists";
+ }
+
+ @Override
+ public ResourceType getResourceType() {
+ return ResourceType.CLIENT;
+ }
+
+ @Override
+ public void remove(RealmModel realm, KeycloakSession session, ClientRepresentation clientRep) {
+ ClientModel clientModel = realm.getClientByClientId(getName(clientRep));
+ new ClientManager(new RealmManager(session)).removeClient(realm, clientModel);
+ }
+
+ @Override
+ public void create(RealmModel realm, KeycloakSession session, ClientRepresentation clientRep) {
+ clientRep.setId(KeycloakModelUtils.generateId());
+
+ List<ProtocolMapperRepresentation> mappers = clientRep.getProtocolMappers();
+ if (mappers != null) {
+ for (ProtocolMapperRepresentation mapper : mappers) {
+ mapper.setId(KeycloakModelUtils.generateId());
+ }
+ }
+
+ RepresentationToModel.createClient(session, realm, clientRep, true);
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/partialimport/ErrorResponseException.java b/services/src/main/java/org/keycloak/partialimport/ErrorResponseException.java
new file mode 100644
index 0000000..b273f56
--- /dev/null
+++ b/services/src/main/java/org/keycloak/partialimport/ErrorResponseException.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.keycloak.partialimport;
+
+import javax.ws.rs.core.Response;
+
+
+/**
+ * An exception that can hold a Response object.
+ *
+ * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
+ */
+public class ErrorResponseException extends Exception {
+ private final Response response;
+
+ public ErrorResponseException(Response response) {
+ this.response = response;
+ }
+
+ public Response getResponse() {
+ return response;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/partialimport/IdentityProvidersPartialImport.java b/services/src/main/java/org/keycloak/partialimport/IdentityProvidersPartialImport.java
new file mode 100644
index 0000000..59e1153
--- /dev/null
+++ b/services/src/main/java/org/keycloak/partialimport/IdentityProvidersPartialImport.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.keycloak.partialimport;
+
+import java.util.List;
+import org.keycloak.models.IdentityProviderModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.models.utils.RepresentationToModel;
+import org.keycloak.representations.idm.IdentityProviderRepresentation;
+import org.keycloak.representations.idm.PartialImportRepresentation;
+
+/**
+ * PartialImport handler for Identitiy Providers.
+ *
+ * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
+ */
+public class IdentityProvidersPartialImport extends AbstractPartialImport<IdentityProviderRepresentation> {
+
+ @Override
+ public List<IdentityProviderRepresentation> getRepList(PartialImportRepresentation partialImportRep) {
+ return partialImportRep.getIdentityProviders();
+ }
+
+ @Override
+ public String getName(IdentityProviderRepresentation idpRep) {
+ return idpRep.getAlias();
+ }
+
+ @Override
+ public String getModelId(RealmModel realm, KeycloakSession session, IdentityProviderRepresentation idpRep) {
+ return realm.getIdentityProviderByAlias(getName(idpRep)).getInternalId();
+ }
+
+ @Override
+ public boolean exists(RealmModel realm, KeycloakSession session, IdentityProviderRepresentation idpRep) {
+ return realm.getIdentityProviderByAlias(getName(idpRep)) != null;
+ }
+
+ @Override
+ public String existsMessage(IdentityProviderRepresentation idpRep) {
+ return "Identity Provider '" + getName(idpRep) + "' already exists.";
+ }
+
+ @Override
+ public ResourceType getResourceType() {
+ return ResourceType.IDP;
+ }
+
+ @Override
+ public void remove(RealmModel realm, KeycloakSession session, IdentityProviderRepresentation idpRep) {
+ realm.removeIdentityProviderByAlias(getName(idpRep));
+ }
+
+ @Override
+ public void create(RealmModel realm, KeycloakSession session, IdentityProviderRepresentation idpRep) {
+ idpRep.setInternalId(KeycloakModelUtils.generateId());
+ IdentityProviderModel identityProvider = RepresentationToModel.toModel(realm, idpRep);
+ realm.addIdentityProvider(identityProvider);
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/partialimport/PartialImport.java b/services/src/main/java/org/keycloak/partialimport/PartialImport.java
new file mode 100644
index 0000000..eaae106
--- /dev/null
+++ b/services/src/main/java/org/keycloak/partialimport/PartialImport.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.keycloak.partialimport;
+
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.representations.idm.PartialImportRepresentation;
+
+/**
+ * Main interface for PartialImport handlers.
+ *
+ * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
+ */
+public interface PartialImport<T> {
+
+ /**
+ * Find which resources will need to be skipped or overwritten. Also,
+ * do a preliminary check for errors.
+ *
+ * @param rep Everything in the PartialImport request.
+ * @param realm Realm to be imported into.
+ * @param session The KeycloakSession.
+ * @throws ErrorResponseException If the PartialImport can not be performed,
+ * throw this exception.
+ */
+ public void prepare(PartialImportRepresentation rep,
+ RealmModel realm,
+ KeycloakSession session) throws ErrorResponseException;
+
+ /**
+ * Delete resources that will be overwritten. This is done separately so
+ * that it can be called for all resource types before calling all the doImports.
+ *
+ * It was found that doing delete/add per resource causes errors because of
+ * cascading deletes.
+ *
+ * @param realm Realm to be imported into.
+ * @param session The KeycloakSession
+ */
+ public void removeOverwrites(RealmModel realm, KeycloakSession session);
+
+ /**
+ * Create (or re-create) all the imported resources.
+ *
+ * @param rep Everything in the PartialImport request.
+ * @param realm Realm to be imported into.
+ * @param session The KeycloakSession.
+ * @return The final results of the PartialImport request.
+ * @throws ErrorResponseException if an error was detected trying to doImport a resource.
+ */
+ public PartialImportResults doImport(PartialImportRepresentation rep,
+ RealmModel realm,
+ KeycloakSession session) throws ErrorResponseException;
+}
diff --git a/services/src/main/java/org/keycloak/partialimport/PartialImportManager.java b/services/src/main/java/org/keycloak/partialimport/PartialImportManager.java
new file mode 100644
index 0000000..1bc391f
--- /dev/null
+++ b/services/src/main/java/org/keycloak/partialimport/PartialImportManager.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.keycloak.partialimport;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.ws.rs.core.Response;
+import org.keycloak.events.admin.OperationType;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.representations.idm.PartialImportRepresentation;
+import org.keycloak.services.resources.admin.AdminEventBuilder;
+
+/**
+ * This class manages the PartialImport handlers.
+ *
+ * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
+ */
+public class PartialImportManager {
+ private final List<PartialImport> partialImports = new ArrayList<>();
+
+ private final PartialImportRepresentation rep;
+ private final KeycloakSession session;
+ private final RealmModel realm;
+ private final AdminEventBuilder adminEvent;
+
+ public PartialImportManager(PartialImportRepresentation rep, KeycloakSession session,
+ RealmModel realm, AdminEventBuilder adminEvent) {
+ this.rep = rep;
+ this.session = session;
+ this.realm = realm;
+ this.adminEvent = adminEvent;
+
+ // Do not change the order of these!!!
+ partialImports.add(new ClientsPartialImport());
+ partialImports.add(new RolesPartialImport());
+ partialImports.add(new IdentityProvidersPartialImport());
+ partialImports.add(new UsersPartialImport());
+ }
+
+ public Response saveResources() {
+
+ PartialImportResults results = new PartialImportResults();
+
+ for (PartialImport partialImport : partialImports) {
+ try {
+ partialImport.prepare(rep, realm, session);
+ } catch (ErrorResponseException error) {
+ if (session.getTransaction().isActive()) session.getTransaction().setRollbackOnly();
+ return error.getResponse();
+ }
+ }
+
+ for (PartialImport partialImport : partialImports) {
+ try {
+ partialImport.removeOverwrites(realm, session);
+ results.addAllResults(partialImport.doImport(rep, realm, session));
+ } catch (ErrorResponseException error) {
+ if (session.getTransaction().isActive()) session.getTransaction().setRollbackOnly();
+ return error.getResponse();
+ }
+ }
+
+ for (PartialImportResult result : results.getResults()) {
+ switch (result.getAction()) {
+ case ADDED : addedEvent(result); break;
+ case OVERWRITTEN: overwrittenEvent(result); break;
+ }
+ }
+
+ if (session.getTransaction().isActive()) {
+ session.getTransaction().commit();
+ }
+
+ return Response.ok(results).build();
+ }
+
+ private void addedEvent(PartialImportResult result) {
+ adminEvent.operation(OperationType.CREATE)
+ .resourcePath(result.getResourceType().getPath(), result.getId())
+ .representation(result.getRepresentation())
+ .success();
+ };
+
+ private void overwrittenEvent(PartialImportResult result) {
+ adminEvent.operation(OperationType.UPDATE)
+ .resourcePath(result.getResourceType().getPath(), result.getId())
+ .representation(result.getRepresentation())
+ .success();
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/partialimport/PartialImportResult.java b/services/src/main/java/org/keycloak/partialimport/PartialImportResult.java
new file mode 100644
index 0000000..6a732e8
--- /dev/null
+++ b/services/src/main/java/org/keycloak/partialimport/PartialImportResult.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.keycloak.partialimport;
+
+import org.codehaus.jackson.annotate.JsonIgnore;
+
+/**
+ * This class represents a single result for a resource imported.
+ *
+ * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
+ */
+public class PartialImportResult {
+
+ private final Action action;
+ private final ResourceType resourceType;
+ private final String resourceName;
+ private final String id;
+ private final Object representation;
+
+ private PartialImportResult(Action action, ResourceType resourceType, String resourceName, String id, Object representation) {
+ this.action = action;
+ this.resourceType = resourceType;
+ this.resourceName = resourceName;
+ this.id = id;
+ this.representation = representation;
+ };
+
+ public static PartialImportResult skipped(ResourceType resourceType, String resourceName, String id, Object representation) {
+ return new PartialImportResult(Action.SKIPPED, resourceType, resourceName, id, representation);
+ }
+
+ public static PartialImportResult added(ResourceType resourceType, String resourceName, String id, Object representation) {
+ return new PartialImportResult(Action.ADDED, resourceType, resourceName, id, representation);
+ }
+
+ public static PartialImportResult overwritten(ResourceType resourceType, String resourceName, String id, Object representation) {
+ return new PartialImportResult(Action.OVERWRITTEN, resourceType, resourceName, id, representation);
+ }
+
+ public Action getAction() {
+ return action;
+ }
+
+ public ResourceType getResourceType() {
+ return resourceType;
+ }
+
+ public String getResourceName() {
+ return resourceName;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ @JsonIgnore
+ public Object getRepresentation() {
+ return representation;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/partialimport/PartialImportResults.java b/services/src/main/java/org/keycloak/partialimport/PartialImportResults.java
new file mode 100644
index 0000000..cc372f4
--- /dev/null
+++ b/services/src/main/java/org/keycloak/partialimport/PartialImportResults.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.keycloak.partialimport;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Aggregates all the PartialImportResult objects.
+ * These results are used in the admin UI and for creating admin events.
+ *
+ * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
+ */
+public class PartialImportResults {
+
+ private final Set<PartialImportResult> importResults = new HashSet<>();
+
+ public void addResult(PartialImportResult result) {
+ importResults.add(result);
+ }
+
+ public void addAllResults(PartialImportResults results) {
+ importResults.addAll(results.getResults());
+ }
+
+ public int getAdded() {
+ int added = 0;
+ for (PartialImportResult result : importResults) {
+ if (result.getAction() == Action.ADDED) added++;
+ }
+
+ return added;
+ }
+
+ public int getOverwritten() {
+ int overwritten = 0;
+ for (PartialImportResult result : importResults) {
+ if (result.getAction() == Action.OVERWRITTEN) overwritten++;
+ }
+
+ return overwritten;
+ }
+
+ public int getSkipped() {
+ int skipped = 0;
+ for (PartialImportResult result : importResults) {
+ if (result.getAction() == Action.SKIPPED) skipped++;
+ }
+
+ return skipped;
+ }
+
+ public Set<PartialImportResult> getResults() {
+ return importResults;
+ }
+}
diff --git a/services/src/main/java/org/keycloak/partialimport/RealmRolesPartialImport.java b/services/src/main/java/org/keycloak/partialimport/RealmRolesPartialImport.java
new file mode 100644
index 0000000..f7276a1
--- /dev/null
+++ b/services/src/main/java/org/keycloak/partialimport/RealmRolesPartialImport.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.keycloak.partialimport;
+
+import java.util.List;
+import java.util.Set;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.representations.idm.PartialImportRepresentation;
+import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.services.resources.admin.RoleResource;
+
+/**
+ * PartialImport handler for Realm Roles.
+ *
+ * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
+ */
+public class RealmRolesPartialImport extends AbstractPartialImport<RoleRepresentation> {
+
+ public Set<RoleRepresentation> getToOverwrite() {
+ return this.toOverwrite;
+ }
+
+ public Set<RoleRepresentation> getToSkip() {
+ return this.toSkip;
+ }
+
+ @Override
+ public List<RoleRepresentation> getRepList(PartialImportRepresentation partialImportRep) {
+ if (partialImportRep.getRoles() == null) return null;
+ return partialImportRep.getRoles().getRealm();
+ }
+
+ @Override
+ public String getName(RoleRepresentation roleRep) {
+ if (roleRep.getName() == null)
+ throw new IllegalStateException("Realm role to import does not have a name");
+ return roleRep.getName();
+ }
+
+ @Override
+ public String getModelId(RealmModel realm, KeycloakSession session, RoleRepresentation roleRep) {
+ for (RoleModel role : realm.getRoles()) {
+ if (getName(roleRep).equals(role.getName())) return role.getId();
+ }
+
+ return null;
+ }
+
+ @Override
+ public boolean exists(RealmModel realm, KeycloakSession session, RoleRepresentation roleRep) {
+ for (RoleModel role : realm.getRoles()) {
+ if (getName(roleRep).equals(role.getName())) return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public String existsMessage(RoleRepresentation roleRep) {
+ return "Realm role '" + getName(roleRep) + "' already exists.";
+ }
+
+ @Override
+ public ResourceType getResourceType() {
+ return ResourceType.REALM_ROLE;
+ }
+
+ @Override
+ public void remove(RealmModel realm, KeycloakSession session, RoleRepresentation roleRep) {
+ RoleModel role = realm.getRole(getName(roleRep));
+ RoleHelper helper = new RoleHelper(realm);
+ helper.deleteRole(role);
+ }
+
+ @Override
+ public void create(RealmModel realm, KeycloakSession session, RoleRepresentation roleRep) {
+ realm.addRole(getName(roleRep));
+ }
+
+ public static class RoleHelper extends RoleResource {
+ public RoleHelper(RealmModel realm) {
+ super(realm);
+ }
+
+ @Override
+ protected void deleteRole(RoleModel role) {
+ super.deleteRole(role);
+ }
+ }
+}
diff --git a/services/src/main/java/org/keycloak/partialimport/ResourceType.java b/services/src/main/java/org/keycloak/partialimport/ResourceType.java
new file mode 100644
index 0000000..25a09d2
--- /dev/null
+++ b/services/src/main/java/org/keycloak/partialimport/ResourceType.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.keycloak.partialimport;
+
+/**
+ * Enum for each resource type that can be partially imported.
+ *
+ * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
+ */
+public enum ResourceType {
+ USER, CLIENT, IDP, REALM_ROLE, CLIENT_ROLE;
+
+ /**
+ * Used to create the admin path in events.
+ *
+ * @return The resource portion of the path.
+ */
+ public String getPath() {
+ switch(this) {
+ case USER: return "users";
+ case CLIENT: return "clients";
+ case IDP: return "identity-provider-settings";
+ case REALM_ROLE: return "realms";
+ case CLIENT_ROLE: return "clients";
+ default: return "";
+ }
+ }
+
+ @Override
+ public String toString() {
+ switch(this) {
+ case USER: return "User";
+ case CLIENT: return "Client";
+ case IDP: return "Identity Provider";
+ case REALM_ROLE: return "Realm Role";
+ case CLIENT_ROLE: return "Client Role";
+ default: return super.toString();
+ }
+ }
+}
diff --git a/services/src/main/java/org/keycloak/partialimport/RolesPartialImport.java b/services/src/main/java/org/keycloak/partialimport/RolesPartialImport.java
new file mode 100644
index 0000000..6bf145e
--- /dev/null
+++ b/services/src/main/java/org/keycloak/partialimport/RolesPartialImport.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package org.keycloak.partialimport;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.ws.rs.core.Response;
+import org.jboss.logging.Logger;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.models.utils.RepresentationToModel;
+import org.keycloak.representations.idm.PartialImportRepresentation;
+import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.idm.RolesRepresentation;
+import org.keycloak.services.ErrorResponse;
+
+/**
+ * This class handles both realm roles and client roles. It delegates to
+ * RealmRolesPartialImport and ClientRolesPartialImport, which are no longer used
+ * directly by the PartialImportManager.
+ *
+ * The strategy is to utilize RepresentationToModel.importRoles(). That way,
+ * the complex code for bulk creation of roles is kept in one place. To do this, the
+ * logic for skip needs to remove the roles that are going to be skipped so that
+ * importRoles() doesn't know about them. The logic for overwrite needs to delete
+ * the overwritten roles before importRoles() is called.
+ *
+ * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
+ */
+public class RolesPartialImport implements PartialImport<RolesRepresentation> {
+ protected static Logger logger = Logger.getLogger(RolesPartialImport.class);
+
+ private Set<RoleRepresentation> realmRolesToOverwrite;
+ private Set<RoleRepresentation> realmRolesToSkip;
+
+ private Map<String, Set<RoleRepresentation>> clientRolesToOverwrite;
+ private Map<String, Set<RoleRepresentation>> clientRolesToSkip;
+
+ private final RealmRolesPartialImport realmRolesPI = new RealmRolesPartialImport();
+ private final ClientRolesPartialImport clientRolesPI = new ClientRolesPartialImport();
+
+ @Override
+ public void prepare(PartialImportRepresentation rep, RealmModel realm, KeycloakSession session) throws ErrorResponseException {
+ prepareRealmRoles(rep, realm, session);
+ prepareClientRoles(rep, realm, session);
+ }
+
+ private void prepareRealmRoles(PartialImportRepresentation rep, RealmModel realm, KeycloakSession session) throws ErrorResponseException {
+ if (!rep.hasRealmRoles()) return;
+
+ realmRolesPI.prepare(rep, realm, session);
+ this.realmRolesToOverwrite = realmRolesPI.getToOverwrite();
+ this.realmRolesToSkip = realmRolesPI.getToSkip();
+ }
+
+ private void prepareClientRoles(PartialImportRepresentation rep, RealmModel realm, KeycloakSession session) throws ErrorResponseException {
+ if (!rep.hasClientRoles()) return;
+
+ clientRolesPI.prepare(rep, realm, session);
+ this.clientRolesToOverwrite = clientRolesPI.getToOverwrite();
+ this.clientRolesToSkip = clientRolesPI.getToSkip();
+ }
+
+ @Override
+ public void removeOverwrites(RealmModel realm, KeycloakSession session) {
+ deleteClientRoleOverwrites(realm);
+ deleteRealmRoleOverwrites(realm, session);
+ }
+
+ @Override
+ public PartialImportResults doImport(PartialImportRepresentation rep, RealmModel realm, KeycloakSession session) throws ErrorResponseException {
+ PartialImportResults results = new PartialImportResults();
+ if (!rep.hasRealmRoles() && !rep.hasClientRoles()) return results;
+
+ // finalize preparation and add results for skips
+ removeRealmRoleSkips(results, rep, realm, session);
+ removeClientRoleSkips(results, rep, realm);
+ if (rep.hasRealmRoles()) setUniqueIds(rep.getRoles().getRealm());
+ if (rep.hasClientRoles()) setUniqueIds(rep.getRoles().getClient());
+
+ try {
+ RepresentationToModel.importRoles(rep.getRoles(), realm);
+ } catch (Exception e) {
+ logger.error("Error importing roles", e);
+ throw new ErrorResponseException(ErrorResponse.error(e.getMessage(), Response.Status.INTERNAL_SERVER_ERROR));
+ }
+
+ // add "add" results for new roles created
+ realmRoleAdds(results, rep, realm, session);
+ clientRoleAdds(results, rep, realm);
+
+ // add "overwritten" results for roles overwritten
+ addResultsForOverwrittenRealmRoles(results, realm, session);
+ addResultsForOverwrittenClientRoles(results, realm);
+
+ return results;
+ }
+
+ private void setUniqueIds(List<RoleRepresentation> realmRoles) {
+ for (RoleRepresentation realmRole : realmRoles) {
+ realmRole.setId(KeycloakModelUtils.generateId());
+ }
+ }
+
+ private void setUniqueIds(Map<String, List<RoleRepresentation>> clientRoles) {
+ for (String clientId : clientRoles.keySet()) {
+ for (RoleRepresentation clientRole : clientRoles.get(clientId)) {
+ clientRole.setId(KeycloakModelUtils.generateId());
+ }
+ }
+ }
+
+ private void removeRealmRoleSkips(PartialImportResults results,
+ PartialImportRepresentation rep,
+ RealmModel realm,
+ KeycloakSession session) {
+ if (isEmpty(realmRolesToSkip)) return;
+
+ for (RoleRepresentation roleRep : realmRolesToSkip) {
+ rep.getRoles().getRealm().remove(roleRep);
+ String modelId = realmRolesPI.getModelId(realm, session, roleRep);
+ results.addResult(realmRolesPI.skipped(modelId, roleRep));
+ }
+ }
+
+ private void removeClientRoleSkips(PartialImportResults results,
+ PartialImportRepresentation rep,
+ RealmModel realm) {
+ if (isEmpty(clientRolesToSkip)) return;
+
+ for (String clientId : clientRolesToSkip.keySet()) {
+ for (RoleRepresentation roleRep : clientRolesToSkip.get(clientId)) {
+ rep.getRoles().getClient().get(clientId).remove(roleRep);
+ String modelId = clientRolesPI.getModelId(realm, clientId);
+ results.addResult(clientRolesPI.skipped(clientId, modelId, roleRep));
+ }
+ }
+ }
+
+ private void deleteRealmRoleOverwrites(RealmModel realm, KeycloakSession session) {
+ if (isEmpty(realmRolesToOverwrite)) return;
+
+ for (RoleRepresentation roleRep : realmRolesToOverwrite) {
+ realmRolesPI.remove(realm, session, roleRep);
+ }
+ }
+
+ private void addResultsForOverwrittenRealmRoles(PartialImportResults results, RealmModel realm, KeycloakSession session) {
+ if (isEmpty(realmRolesToOverwrite)) return;
+
+ for (RoleRepresentation roleRep : realmRolesToOverwrite) {
+ String modelId = realmRolesPI.getModelId(realm, session, roleRep);
+ results.addResult(realmRolesPI.overwritten(modelId, roleRep));
+ }
+ }
+
+ private void deleteClientRoleOverwrites(RealmModel realm) {
+ if (isEmpty(clientRolesToOverwrite)) return;
+
+ for (String clientId : clientRolesToOverwrite.keySet()) {
+ for (RoleRepresentation roleRep : clientRolesToOverwrite.get(clientId)) {
+ clientRolesPI.deleteRole(realm, clientId, roleRep);
+ }
+ }
+ }
+
+ private void addResultsForOverwrittenClientRoles(PartialImportResults results, RealmModel realm) {
+ if (isEmpty(clientRolesToOverwrite)) return;
+
+ for (String clientId : clientRolesToOverwrite.keySet()) {
+ for (RoleRepresentation roleRep : clientRolesToOverwrite.get(clientId)) {
+ String modelId = clientRolesPI.getModelId(realm, clientId);
+ results.addResult(clientRolesPI.overwritten(clientId, modelId, roleRep));
+ }
+ }
+ }
+
+ private boolean isEmpty(Set set) {
+ return (set == null) || (set.isEmpty());
+ }
+
+ private boolean isEmpty(Map map) {
+ return (map == null) || (map.isEmpty());
+ }
+
+ private void realmRoleAdds(PartialImportResults results,
+ PartialImportRepresentation rep,
+ RealmModel realm,
+ KeycloakSession session) {
+ if (!rep.hasRealmRoles()) return;
+
+ for (RoleRepresentation roleRep : rep.getRoles().getRealm()) {
+ if (realmRolesToOverwrite.contains(roleRep)) continue;
+ if (realmRolesToSkip.contains(roleRep)) continue;
+
+ String modelId = realmRolesPI.getModelId(realm, session, roleRep);
+ results.addResult(realmRolesPI.added(modelId, roleRep));
+ }
+ }
+
+ private void clientRoleAdds(PartialImportResults results,
+ PartialImportRepresentation rep,
+ RealmModel realm) {
+ if (!rep.hasClientRoles()) return;
+
+ Map<String, List<RoleRepresentation>> repList = clientRolesPI.getRepList(rep);
+ for (String clientId : repList.keySet()) {
+ for (RoleRepresentation roleRep : repList.get(clientId)) {
+ if (clientRolesToOverwrite.get(clientId).contains(roleRep)) continue;
+ if (clientRolesToSkip.get(clientId).contains(roleRep)) continue;
+
+ String modelId = clientRolesPI.getModelId(realm, clientId);
+ results.addResult(clientRolesPI.added(clientId, modelId, roleRep));
+ }
+ }
+ }
+}
diff --git a/services/src/main/java/org/keycloak/partialimport/UsersPartialImport.java b/services/src/main/java/org/keycloak/partialimport/UsersPartialImport.java
new file mode 100644
index 0000000..2ae3fc3
--- /dev/null
+++ b/services/src/main/java/org/keycloak/partialimport/UsersPartialImport.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2016 Red Hat Inc. and/or its affiliates and other contributors
+ * as indicated by the @author tags. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package org.keycloak.partialimport;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.models.utils.RepresentationToModel;
+import org.keycloak.representations.idm.PartialImportRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.services.managers.UserManager;
+
+/**
+ * PartialImport handler for users.
+ *
+ * @author Stan Silvert ssilvert@redhat.com (C) 2016 Red Hat Inc.
+ */
+public class UsersPartialImport extends AbstractPartialImport<UserRepresentation> {
+
+ // Sometimes session.users().getUserByUsername() doesn't work right after create,
+ // so we cache the created id here.
+ private final Map<String, String> createdIds = new HashMap<>();
+
+ @Override
+ public List<UserRepresentation> getRepList(PartialImportRepresentation partialImportRep) {
+ return partialImportRep.getUsers();
+ }
+
+ @Override
+ public String getName(UserRepresentation user) {
+ if (user.getUsername() != null) return user.getUsername();
+
+ return user.getEmail();
+ }
+
+ @Override
+ public String getModelId(RealmModel realm, KeycloakSession session, UserRepresentation user) {
+ if (createdIds.containsKey(getName(user))) return createdIds.get(getName(user));
+
+ String userName = user.getUsername();
+ if (userName != null) {
+ return session.users().getUserByUsername(userName, realm).getId();
+ } else {
+ String email = user.getEmail();
+ return session.users().getUserByEmail(email, realm).getId();
+ }
+ }
+
+ @Override
+ public boolean exists(RealmModel realm, KeycloakSession session, UserRepresentation user) {
+ return userNameExists(realm, session, user) || userEmailExists(realm, session, user);
+ }
+
+ private boolean userNameExists(RealmModel realm, KeycloakSession session, UserRepresentation user) {
+ return session.users().getUserByUsername(user.getUsername(), realm) != null;
+ }
+
+ private boolean userEmailExists(RealmModel realm, KeycloakSession session, UserRepresentation user) {
+ return (user.getEmail() != null) &&
+ (session.users().getUserByEmail(user.getEmail(), realm) != null);
+ }
+
+ @Override
+ public String existsMessage(UserRepresentation user) {
+ if (user.getEmail() == null) {
+ return "User with user name " + getName(user) + " already exists.";
+ }
+
+ return "User with user name " + getName(user) + " or with email " + user.getEmail() + " already exists.";
+ }
+
+ @Override
+ public ResourceType getResourceType() {
+ return ResourceType.USER;
+ }
+
+ @Override
+ public void remove(RealmModel realm, KeycloakSession session, UserRepresentation user) {
+ UserModel userModel = session.users().getUserByUsername(user.getUsername(), realm);
+ if (userModel == null) {
+ userModel = session.users().getUserByEmail(user.getEmail(), realm);
+ }
+
+ boolean success = new UserManager(session).removeUser(realm, userModel);
+ if (!success) throw new RuntimeException("Unable to overwrite user " + getName(user));
+ }
+
+ @Override
+ public void create(RealmModel realm, KeycloakSession session, UserRepresentation user) {
+ Map<String, ClientModel> apps = realm.getClientNameMap();
+ user.setId(KeycloakModelUtils.generateId());
+ UserModel userModel = RepresentationToModel.createUser(session, realm, user, apps);
+ if (userModel == null) throw new RuntimeException("Unable to create user " + getName(user));
+ createdIds.put(getName(user), userModel.getId());
+ }
+
+}
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java
index ff4601f..cf8354d 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/RedirectUtils.java
@@ -63,6 +63,8 @@ public class RedirectUtils {
logger.debug("No Redirect URIs supplied");
redirectUri = null;
} else {
+ redirectUri = lowerCaseHostname(redirectUri);
+
String r = redirectUri.indexOf('?') != -1 ? redirectUri.substring(0, redirectUri.indexOf('?')) : redirectUri;
Set<String> resolveValidRedirects = resolveValidRedirects(uriInfo, rootUrl, validRedirects);
@@ -96,6 +98,15 @@ public class RedirectUtils {
}
}
+ private static String lowerCaseHostname(String redirectUri) {
+ int n = redirectUri.indexOf('/', 7);
+ if (n == -1) {
+ return redirectUri.toLowerCase();
+ } else {
+ return redirectUri.substring(0, n).toLowerCase() + redirectUri.substring(n);
+ }
+ }
+
private static String relativeToAbsoluteURI(UriInfo uriInfo, String rootUrl, String relative) {
if (rootUrl == null) {
URI baseUri = uriInfo.getBaseUri();
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminEventBuilder.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminEventBuilder.java
index 637218e..ea88a7d 100644
--- a/services/src/main/java/org/keycloak/services/resources/admin/AdminEventBuilder.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminEventBuilder.java
@@ -21,7 +21,7 @@ import org.keycloak.common.util.Time;
import javax.ws.rs.core.UriInfo;
public class AdminEventBuilder {
-
+
private static final Logger log = Logger.getLogger(AdminEventBuilder.class);
private EventStoreProvider store;
@@ -59,17 +59,17 @@ public class AdminEventBuilder {
authUser(auth.getUser());
authIpAddress(clientConnection.getRemoteAddr());
}
-
+
public AdminEventBuilder realm(RealmModel realm) {
adminEvent.setRealmId(realm.getId());
return this;
}
-
+
public AdminEventBuilder realm(String realmId) {
adminEvent.setRealmId(realmId);
return this;
}
-
+
public AdminEventBuilder operation(OperationType e) {
adminEvent.setOperationType(e);
return this;
@@ -123,6 +123,18 @@ public class AdminEventBuilder {
return this;
}
+ public AdminEventBuilder resourcePath(String... pathElements) {
+ StringBuilder sb = new StringBuilder();
+ for (String element : pathElements) {
+ sb.append("/");
+ sb.append(element);
+ }
+ if (pathElements.length > 0) sb.deleteCharAt(0); // remove leading '/'
+
+ adminEvent.setResourcePath(sb.toString());
+ return this;
+ }
+
public AdminEventBuilder resourcePath(UriInfo uriInfo) {
String path = getResourcePath(uriInfo);
adminEvent.setResourcePath(path);
@@ -155,7 +167,7 @@ public class AdminEventBuilder {
adminEvent.setError(error);
send();
}
-
+
public AdminEventBuilder representation(Object value) {
if (value == null || value.equals("")) {
return this;
@@ -167,7 +179,7 @@ public class AdminEventBuilder {
}
return this;
}
-
+
public AdminEvent getEvent() {
return adminEvent;
}
@@ -190,7 +202,7 @@ public class AdminEventBuilder {
log.error("Failed to save event", t);
}
}
-
+
if (listeners != null) {
for (EventListenerProvider l : listeners) {
try {
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
index 7c19f61..6a26b85 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientResource.java
@@ -68,7 +68,7 @@ public class ClientResource {
private AdminEventBuilder adminEvent;
protected ClientModel client;
protected KeycloakSession session;
-
+
@Context
protected UriInfo uriInfo;
@@ -107,11 +107,7 @@ public class ClientResource {
auth.requireManage();
try {
- if (TRUE.equals(rep.isServiceAccountsEnabled()) && !client.isServiceAccountsEnabled()) {
- new ClientManager(new RealmManager(session)).enableServiceAccount(client);;
- }
-
- RepresentationToModel.updateClient(rep, client);
+ updateClientFromRep(rep, client, session);
adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(rep).success();
return Response.noContent().build();
} catch (ModelDuplicateException e) {
@@ -119,6 +115,13 @@ public class ClientResource {
}
}
+ public static void updateClientFromRep(ClientRepresentation rep, ClientModel client, KeycloakSession session) throws ModelDuplicateException {
+ if (TRUE.equals(rep.isServiceAccountsEnabled()) && !client.isServiceAccountsEnabled()) {
+ new ClientManager(new RealmManager(session)).enableServiceAccount(client);
+ }
+
+ RepresentationToModel.updateClient(rep, client);
+ }
/**
* Get representation of the client
@@ -381,9 +384,9 @@ public class ClientResource {
auth.requireManage();
adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).success();
return new ResourceAdminManager(session).pushClientRevocationPolicy(uriInfo.getRequestUri(), realm, client);
-
+
}
-
+
/**
* Get application session count
*
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/ClientTemplatesResource.java b/services/src/main/java/org/keycloak/services/resources/admin/ClientTemplatesResource.java
index 6a65875..c52fb1d 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/ClientTemplatesResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/ClientTemplatesResource.java
@@ -75,6 +75,7 @@ public class ClientTemplatesResource {
client.setId(clientModel.getId());
client.setName(clientModel.getName());
client.setDescription(clientModel.getDescription());
+ client.setProtocol(clientModel.getProtocol());
rep.add(client);
}
}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java
index b49cf91..f824a0e 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/IdentityProviderResource.java
@@ -58,7 +58,7 @@ public class IdentityProviderResource {
private final KeycloakSession session;
private final IdentityProviderModel identityProviderModel;
private final AdminEventBuilder adminEvent;
-
+
@Context private UriInfo uriInfo;
public IdentityProviderResource(RealmAuth auth, RealmModel realm, KeycloakSession session, IdentityProviderModel identityProviderModel, AdminEventBuilder adminEvent) {
@@ -94,9 +94,9 @@ public class IdentityProviderResource {
this.auth.requireManage();
this.realm.removeIdentityProviderByAlias(this.identityProviderModel.getAlias());
-
+
adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).success();
-
+
return Response.noContent().build();
}
@@ -113,30 +113,34 @@ public class IdentityProviderResource {
try {
this.auth.requireManage();
- String internalId = providerRep.getInternalId();
- String newProviderId = providerRep.getAlias();
- String oldProviderId = getProviderIdByInternalId(this.realm, internalId);
-
- this.realm.updateIdentityProvider(RepresentationToModel.toModel(realm, providerRep));
-
- if (oldProviderId != null && !oldProviderId.equals(newProviderId)) {
+ updateIdpFromRep(providerRep, realm, session);
- // Admin changed the ID (alias) of identity provider. We must update all clients and users
- logger.debug("Changing providerId in all clients and linked users. oldProviderId=" + oldProviderId + ", newProviderId=" + newProviderId);
-
- updateUsersAfterProviderAliasChange(this.session.users().getUsers(this.realm, false), oldProviderId, newProviderId);
- }
-
adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(providerRep).success();
-
+
return Response.noContent().build();
} catch (ModelDuplicateException e) {
return ErrorResponse.exists("Identity Provider " + providerRep.getAlias() + " already exists");
}
}
+ public static void updateIdpFromRep(IdentityProviderRepresentation providerRep, RealmModel realm, KeycloakSession session) {
+ String internalId = providerRep.getInternalId();
+ String newProviderId = providerRep.getAlias();
+ String oldProviderId = getProviderIdByInternalId(realm, internalId);
+
+ realm.updateIdentityProvider(RepresentationToModel.toModel(realm, providerRep));
+
+ if (oldProviderId != null && !oldProviderId.equals(newProviderId)) {
+
+ // Admin changed the ID (alias) of identity provider. We must update all clients and users
+ logger.debug("Changing providerId in all clients and linked users. oldProviderId=" + oldProviderId + ", newProviderId=" + newProviderId);
+
+ updateUsersAfterProviderAliasChange(session.users().getUsers(realm, false), oldProviderId, newProviderId, realm, session);
+ }
+ }
+
// return ID of IdentityProvider from realm based on internalId of this provider
- private String getProviderIdByInternalId(RealmModel realm, String providerInternalId) {
+ private static String getProviderIdByInternalId(RealmModel realm, String providerInternalId) {
List<IdentityProviderModel> providerModels = realm.getIdentityProviders();
for (IdentityProviderModel providerModel : providerModels) {
if (providerModel.getInternalId().equals(providerInternalId)) {
@@ -147,17 +151,17 @@ public class IdentityProviderResource {
return null;
}
- private void updateUsersAfterProviderAliasChange(List<UserModel> users, String oldProviderId, String newProviderId) {
+ private static void updateUsersAfterProviderAliasChange(List<UserModel> users, String oldProviderId, String newProviderId, RealmModel realm, KeycloakSession session) {
for (UserModel user : users) {
- FederatedIdentityModel federatedIdentity = this.session.users().getFederatedIdentity(user, oldProviderId, this.realm);
+ FederatedIdentityModel federatedIdentity = session.users().getFederatedIdentity(user, oldProviderId, realm);
if (federatedIdentity != null) {
// Remove old link first
- this.session.users().removeFederatedIdentity(this.realm, user, oldProviderId);
+ session.users().removeFederatedIdentity(realm, user, oldProviderId);
// And create new
FederatedIdentityModel newFederatedIdentity = new FederatedIdentityModel(newProviderId, federatedIdentity.getUserId(), federatedIdentity.getUserName(),
federatedIdentity.getToken());
- this.session.users().addFederatedIdentity(this.realm, user, newFederatedIdentity);
+ session.users().addFederatedIdentity(realm, user, newFederatedIdentity);
}
}
}
@@ -263,10 +267,10 @@ public class IdentityProviderResource {
auth.requireManage();
IdentityProviderMapperModel model = RepresentationToModel.toModel(mapper);
model = realm.addIdentityProviderMapper(model);
-
+
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, model.getId())
.representation(mapper).success();
-
+
return Response.created(uriInfo.getAbsolutePathBuilder().path(model.getId()).build()).build();
}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
index cb97855..1cece17 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java
@@ -66,6 +66,8 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.PatternSyntaxException;
+import org.keycloak.partialimport.PartialImportManager;
+import org.keycloak.representations.idm.PartialImportRepresentation;
/**
* Base resource class for the admin REST api of one realm
@@ -241,7 +243,7 @@ public class RealmAdminResource {
for (final UserFederationProviderModel fedProvider : federationProviders) {
usersSyncManager.refreshPeriodicSyncForProvider(session.getKeycloakSessionFactory(), session.getProvider(TimerProvider.class), fedProvider, realm.getId());
}
-
+
adminEvent.operation(OperationType.UPDATE).representation(rep).success();
return Response.noContent().build();
} catch (PatternSyntaxException e) {
@@ -466,7 +468,7 @@ public class RealmAdminResource {
if (user != null) {
query.user(user);
}
-
+
if(dateFrom != null) {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
Date from = null;
@@ -477,7 +479,7 @@ public class RealmAdminResource {
}
query.fromDate(from);
}
-
+
if(dateTo != null) {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
Date to = null;
@@ -501,7 +503,7 @@ public class RealmAdminResource {
return query.getResultList();
}
-
+
/**
* Get admin events
*
@@ -540,15 +542,15 @@ public class RealmAdminResource {
if (authClient != null) {
query.authClient(authClient);
}
-
+
if (authUser != null) {
query.authUser(authUser);
}
-
+
if (authIpAddress != null) {
query.authIpAddress(authIpAddress);
}
-
+
if (resourcePath != null) {
query.resourcePath(resourcePath);
}
@@ -561,7 +563,7 @@ public class RealmAdminResource {
}
query.operation(t);
}
-
+
if(dateFrom != null) {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
Date from = null;
@@ -572,7 +574,7 @@ public class RealmAdminResource {
}
query.fromTime(from);
}
-
+
if(dateTo != null) {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
Date to = null;
@@ -606,7 +608,7 @@ public class RealmAdminResource {
EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
eventStore.clear(realm.getId());
}
-
+
/**
* Delete all admin events
*
@@ -709,5 +711,18 @@ public class RealmAdminResource {
return ModelToRepresentation.toGroupHierarchy(found, true);
}
-
+ /**
+ * Partial import from a JSON file to an existing realm.
+ *
+ * @param rep
+ * @return
+ */
+ @Path("partialImport")
+ @POST
+ @Consumes(MediaType.APPLICATION_JSON)
+ public Response partialImport(PartialImportRepresentation rep) {
+ auth.requireManage();
+ PartialImportManager partialImport = new PartialImportManager(rep, session, realm, adminEvent);
+ return partialImport.saveResources();
+ }
}
diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
index 67bd67e..f78f33f 100755
--- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
+++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java
@@ -150,7 +150,7 @@ public class UsersResource {
}
}
- updateUserFromRep(user, rep, attrsToRemove);
+ updateUserFromRep(user, rep, attrsToRemove, realm, session);
adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(rep).success();
if (session.getTransaction().isActive()) {
@@ -189,7 +189,7 @@ public class UsersResource {
try {
UserModel user = session.users().addUser(realm, rep.getUsername());
Set<String> emptySet = Collections.emptySet();
- updateUserFromRep(user, rep, emptySet);
+ updateUserFromRep(user, rep, emptySet, realm, session);
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, user.getId()).representation(rep).success();
@@ -206,7 +206,7 @@ public class UsersResource {
}
}
- private void updateUserFromRep(UserModel user, UserRepresentation rep, Set<String> attrsToRemove) {
+ public static void updateUserFromRep(UserModel user, UserRepresentation rep, Set<String> attrsToRemove, RealmModel realm, KeycloakSession session) {
if (realm.isEditUsernameAllowed()) {
user.setUsername(rep.getUsername());
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/TermsAndConditionsTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/TermsAndConditionsTest.java
index f758900..b1cf981 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/TermsAndConditionsTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/TermsAndConditionsTest.java
@@ -32,6 +32,7 @@ import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
+import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.pages.AppPage;
@@ -44,6 +45,11 @@ import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule;
import org.openqa.selenium.WebDriver;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.Assert.*;
+
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@@ -96,6 +102,23 @@ public class TermsAndConditionsTest {
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectLogin().session(sessionId).assertEvent();
+
+ // assert user attribute is properly set
+ UserRepresentation user = keycloakRule.getUser("test", "test-user@localhost");
+ Map<String,List<String>> attributes = user.getAttributesAsListValues();
+ assertNotNull("timestamp for terms acceptance was not stored in user attributes", attributes);
+ List<String> termsAndConditions = attributes.get(TermsAndConditions.USER_ATTRIBUTE);
+ assertTrue("timestamp for terms acceptance was not stored in user attributes as "
+ + TermsAndConditions.USER_ATTRIBUTE, termsAndConditions.size() == 1);
+ String timestamp = termsAndConditions.get(0);
+ assertNotNull("expected non-null timestamp for terms acceptance in user attribute "
+ + TermsAndConditions.USER_ATTRIBUTE, timestamp);
+ try {
+ Integer.parseInt(timestamp);
+ }
+ catch (NumberFormatException e) {
+ fail("timestamp for terms acceptance is not a valid integer: '" + timestamp + "'");
+ }
}
@Test
@@ -113,6 +136,14 @@ public class TermsAndConditionsTest {
.removeDetail(Details.CONSENT)
.assertEvent();
+
+ // assert user attribute is properly removed
+ UserRepresentation user = keycloakRule.getUser("test", "test-user@localhost");
+ Map<String,List<String>> attributes = user.getAttributesAsListValues();
+ if (attributes != null) {
+ assertNull("expected null for terms acceptance user attribute " + TermsAndConditions.USER_ATTRIBUTE,
+ attributes.get(TermsAndConditions.USER_ATTRIBUTE));
+ }
}
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AbstractClientTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AbstractClientTest.java
index 99edd91..52d0bf1 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AbstractClientTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/AbstractClientTest.java
@@ -16,10 +16,7 @@ import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.WebResource;
import org.openqa.selenium.WebDriver;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
+import java.util.*;
import static org.junit.Assert.assertArrayEquals;
@@ -41,32 +38,37 @@ public abstract class AbstractClientTest {
keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
- RealmModel testRealm = manager.createRealm(REALM_NAME);
- testRealm.setEnabled(true);
- testRealm.setAccessCodeLifespanUserAction(600);
- KeycloakModelUtils.generateRealmKeys(testRealm);
-
appRealm.getClientByClientId("test-app").setDirectAccessGrantsEnabled(true);
}
});
keycloak = Keycloak.getInstance("http://localhost:8081/auth", "master", "admin", "admin", Constants.ADMIN_CLI_CLIENT_ID);
+
+ RealmRepresentation rep = new RealmRepresentation();
+ rep.setRealm(REALM_NAME);
+ rep.setEnabled(true);
+
+ Map<String, String> config = new HashMap<>();
+ config.put("from", "auto@keycloak.org");
+ config.put("host", "localhost");
+ config.put("port", "3025");
+
+ rep.setSmtpServer(config);
+
+ keycloak.realms().create(rep);
+
realm = keycloak.realm(REALM_NAME);
}
@After
public void after() {
- keycloak.close();
-
- keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
- @Override
- public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
- RealmModel realm = manager.getRealmByName(REALM_NAME);
- if (realm != null) {
- manager.removeRealm(realm);
- }
+ for (RealmRepresentation r : keycloak.realms().findAll()) {
+ if (r.getRealm().equals(REALM_NAME)) {
+ keycloak.realm(REALM_NAME).remove();
}
- });
+ }
+
+ keycloak.close();
}
public static <T> void assertNames(List<T> actual, String... expected) {
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/UserTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/UserTest.java
index 0acd7a0..cae756e 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/UserTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/admin/UserTest.java
@@ -4,6 +4,7 @@ import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
+import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.IdentityProviderResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.events.Details;
@@ -11,12 +12,10 @@ import org.keycloak.events.EventType;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
-import org.keycloak.representations.idm.ErrorRepresentation;
-import org.keycloak.representations.idm.FederatedIdentityRepresentation;
-import org.keycloak.representations.idm.IdentityProviderRepresentation;
-import org.keycloak.representations.idm.RealmRepresentation;
-import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.representations.idm.*;
import org.keycloak.services.managers.RealmManager;
+import org.keycloak.services.resources.RealmsResource;
+import org.keycloak.testsuite.Constants;
import org.keycloak.testsuite.actions.RequiredActionEmailVerificationTest;
import org.keycloak.testsuite.forms.ResetPasswordTest;
import org.keycloak.testsuite.pages.*;
@@ -32,6 +31,7 @@ import javax.mail.internet.MimeMultipart;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.ClientErrorException;
import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
import java.io.IOException;
import java.util.ArrayList;
@@ -63,18 +63,8 @@ public class UserTest extends AbstractClientTest {
@WebResource
protected InfoPage infoPage;
- @Before
- public void before() {
- super.before();
-
- keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
- @Override
- public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
- RealmModel testRealm = manager.getRealm(REALM_NAME);
- greenMail.configureRealm(testRealm);
- }
- });
- }
+ @WebResource
+ protected LoginPage loginPage;
public String createUser() {
return createUser("user1", "user1@localhost");
@@ -84,6 +74,7 @@ public class UserTest extends AbstractClientTest {
UserRepresentation user = new UserRepresentation();
user.setUsername(username);
user.setEmail(email);
+ user.setEnabled(true);
Response response = realm.users().create(user);
String createdId = ApiUtil.getCreatedId(response);
@@ -600,6 +591,28 @@ public class UserTest extends AbstractClientTest {
}
}
+ @Test
+ public void resetUserPassword() {
+ String userId = createUser("user1", "user1@localhost");
+
+ CredentialRepresentation cred = new CredentialRepresentation();
+ cred.setType(CredentialRepresentation.PASSWORD);
+ cred.setValue("password");
+ cred.setTemporary(false);
+
+ realm.users().get(userId).resetPassword(cred);
+
+ String accountUrl = RealmsResource.accountUrl(UriBuilder.fromUri(Constants.AUTH_SERVER_ROOT)).build(REALM_NAME).toString();
+
+ driver.navigate().to(accountUrl);
+
+ assertEquals("Log in to admin-client-test", driver.getTitle());
+
+ loginPage.login("user1", "password");
+
+ assertEquals("Keycloak Account Management", driver.getTitle());
+ }
+
private void switchEditUsernameAllowedOn() {
RealmRepresentation rep = realm.toRepresentation();
rep.setEditUsernameAllowed(true);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java
index 527e279..d74812a 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/OAuthRedirectUriTest.java
@@ -65,8 +65,15 @@ public class OAuthRedirectUriTest {
ClientModel installedApp3 = KeycloakModelUtils.createClient(appRealm, "test-wildcard");
installedApp3.setEnabled(true);
installedApp3.addRedirectUri("http://example.com/foo/*");
+ installedApp3.addRedirectUri("http://with-dash.example.com/foo/*");
installedApp3.addRedirectUri("http://localhost:8081/foo/*");
installedApp3.setSecret("password");
+
+ ClientModel installedApp4 = KeycloakModelUtils.createClient(appRealm, "test-dash");
+ installedApp4.setEnabled(true);
+ installedApp4.addRedirectUri("http://with-dash.example.com");
+ installedApp4.addRedirectUri("http://with-dash.example.com/foo");
+ installedApp4.setSecret("password");
}
});
@@ -217,6 +224,27 @@ public class OAuthRedirectUriTest {
}
@Test
+ public void testDash() throws IOException {
+ oauth.clientId("test-dash");
+
+ checkRedirectUri("http://with-dash.example.com/foo", true);
+ }
+
+ @Test
+ public void testDifferentCaseInHostname() throws IOException {
+ oauth.clientId("test-dash");
+
+ checkRedirectUri("http://with-dash.example.com", true);
+ checkRedirectUri("http://wiTh-dAsh.example.com", true);
+ checkRedirectUri("http://with-dash.example.com/foo", true);
+ checkRedirectUri("http://wiTh-dAsh.example.com/foo", true);
+ checkRedirectUri("http://with-dash.eXampLe.com/foo", true);
+ checkRedirectUri("http://wiTh-dAsh.eXampLe.com/foo", true);
+ checkRedirectUri("http://wiTh-dAsh.eXampLe.com/Foo", false);
+ checkRedirectUri("http://wiTh-dAsh.eXampLe.com/foO", false);
+ }
+
+ @Test
public void testLocalhost() throws IOException {
oauth.clientId("test-installed");
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java
index f4ef632..1999dca 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/AbstractKeycloakRule.java
@@ -116,6 +116,7 @@ public abstract class AbstractKeycloakRule extends ExternalResource {
try {
RealmManager manager = new RealmManager(session);
+ manager.setContextPath("/auth");
RealmModel adminstrationRealm = manager.getRealm(Config.getAdminRealm());
RealmModel appRealm = manager.getRealm(realmId);
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java
index 8fc4cd6..ab99b0c 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/rule/KeycloakRule.java
@@ -69,6 +69,7 @@ public class KeycloakRule extends AbstractKeycloakRule {
try {
RealmManager manager = new RealmManager(session);
+ manager.setContextPath("/auth");
RealmModel adminstrationRealm = manager.getRealm(Config.getAdminRealm());
RealmModel appRealm = manager.getRealm("test");
diff --git a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json
index 26fec93..41c2cf5 100755
--- a/testsuite/integration/src/test/resources/META-INF/keycloak-server.json
+++ b/testsuite/integration/src/test/resources/META-INF/keycloak-server.json
@@ -51,9 +51,7 @@
},
"connectionsHttpClient": {
- "default": {
- "disable-trust-manager": true
- }
+ "default": {}
},
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
index 4d4c2f6..0978231 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
@@ -72,9 +72,7 @@
},
"connectionsHttpClient": {
- "default": {
- "disable-trust-manager": true
- }
+ "default": {}
},
@@ -99,5 +97,14 @@
"databaseSchema": "${keycloak.connectionsMongo.databaseSchema:update}",
"connectionsPerHost": "${keycloak.connectionsMongo.connectionsPerHost:100}"
}
+ },
+
+ "truststore": {
+ "file": {
+ "file": "${keycloak.truststore.file:src/main/keystore/keycloak.truststore}",
+ "password": "${keycloak.truststore.password:secret}",
+ "hostname-verification-policy": "${keycloak.truststore.policy:WILDCARD}",
+ "disabled": "${keycloak.truststore.disabled:true}"
+ }
}
-}
\ No newline at end of file
+}
diff --git a/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/LDAPEmbeddedServer.java b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/LDAPEmbeddedServer.java
index 4abe94d..cdc919e 100644
--- a/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/LDAPEmbeddedServer.java
+++ b/util/embedded-ldap/src/main/java/org/keycloak/util/ldap/LDAPEmbeddedServer.java
@@ -43,6 +43,9 @@ public class LDAPEmbeddedServer {
private static final String DEFAULT_BIND_HOST = "localhost";
private static final String DEFAULT_BIND_PORT = "10389";
private static final String DEFAULT_LDIF_FILE = "classpath:ldap/default-users.ldif";
+ private static final String PROPERTY_ENABLE_SSL = "enableSSL";
+ private static final String PROPERTY_KEYSTORE_FILE = "keystoreFile";
+ private static final String PROPERTY_CERTIFICATE_PASSWORD = "certificatePassword";
public static final String DSF_INMEMORY = "mem";
public static final String DSF_FILE = "file";
@@ -56,6 +59,9 @@ public class LDAPEmbeddedServer {
protected String ldifFile;
protected String ldapSaslPrincipal;
protected String directoryServiceFactory;
+ protected boolean enableSSL = false;
+ protected String keystoreFile;
+ protected String certPassword;
protected DirectoryService directoryService;
protected LdapServer ldapServer;
@@ -97,6 +103,9 @@ public class LDAPEmbeddedServer {
this.ldifFile = readProperty(PROPERTY_LDIF_FILE, DEFAULT_LDIF_FILE);
this.ldapSaslPrincipal = readProperty(PROPERTY_SASL_PRINCIPAL, null);
this.directoryServiceFactory = readProperty(PROPERTY_DSF, DEFAULT_DSF);
+ this.enableSSL = Boolean.valueOf(readProperty(PROPERTY_ENABLE_SSL, "false"));
+ this.keystoreFile = readProperty(PROPERTY_KEYSTORE_FILE, null);
+ this.certPassword = readProperty(PROPERTY_CERTIFICATE_PASSWORD, null);
}
protected String readProperty(String propertyName, String defaultValue) {
@@ -194,6 +203,11 @@ public class LDAPEmbeddedServer {
// Read the transports
Transport ldap = new TcpTransport(this.bindHost, this.bindPort, 3, 50);
+ if (enableSSL) {
+ ldap.setEnableSSL(true);
+ ldapServer.setKeystoreFile(keystoreFile);
+ ldapServer.setCertificatePassword(certPassword);
+ }
ldapServer.addTransports( ldap );
// Associate the DS to this LdapServer
diff --git a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-datasources.xml b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-datasources.xml
index 9f05130..ae34909 100644
--- a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-datasources.xml
+++ b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-datasources.xml
@@ -2,7 +2,7 @@
<!-- See src/resources/configuration/ReadMe.txt for how the configuration assembly works -->
<config>
<extension-module>org.jboss.as.connector</extension-module>
- <subsystem xmlns="urn:jboss:domain:datasources:3.0">
+ <subsystem xmlns="urn:jboss:domain:datasources:4.0">
<datasources>
<datasource jndi-name="java:jboss/datasources/ExampleDS" pool-name="ExampleDS" enabled="true" use-java-context="true">
<connection-url>jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE</connection-url>
diff --git a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml
index 5da36aa..ae08dd3 100644
--- a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml
+++ b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-infinispan.xml
@@ -2,7 +2,7 @@
<!-- See src/resources/configuration/ReadMe.txt for how the configuration assembly works -->
<config default-supplement="default">
<extension-module>org.jboss.as.clustering.infinispan</extension-module>
- <subsystem xmlns="urn:jboss:domain:infinispan:3.0">
+ <subsystem xmlns="urn:jboss:domain:infinispan:4.0">
<?CACHE-CONTAINERS?>
</subsystem>
<supplement name="default">
@@ -21,16 +21,19 @@
</cache-container>
<cache-container name="web" default-cache="passivation" module="org.wildfly.clustering.web.infinispan">
<local-cache name="passivation">
+ <locking isolation="REPEATABLE_READ"/>
<transaction mode="BATCH"/>
<file-store passivation="true" purge="false"/>
</local-cache>
<local-cache name="persistent">
+ <locking isolation="REPEATABLE_READ"/>
<transaction mode="BATCH"/>
<file-store passivation="false" purge="false"/>
</local-cache>
</cache-container>
<cache-container name="ejb" aliases="sfsb" default-cache="passivation" module="org.wildfly.clustering.ejb.infinispan">
<local-cache name="passivation">
+ <locking isolation="REPEATABLE_READ"/>
<transaction mode="BATCH"/>
<file-store passivation="true" purge="false"/>
</local-cache>
@@ -45,6 +48,11 @@
<eviction strategy="LRU" max-entries="10000"/>
<expiration max-idle="100000"/>
</local-cache>
+ <local-cache name="immutable-entity">
+ <transaction mode="NON_XA"/>
+ <eviction strategy="LRU" max-entries="10000"/>
+ <expiration max-idle="100000"/>
+ </local-cache>
<local-cache name="local-query">
<eviction strategy="LRU" max-entries="10000"/>
<expiration max-idle="100000"/>
@@ -72,6 +80,7 @@
<cache-container name="web" default-cache="dist" module="org.wildfly.clustering.web.infinispan">
<transport lock-timeout="60000"/>
<distributed-cache name="dist" mode="ASYNC" l1-lifespan="0" owners="2">
+ <locking isolation="REPEATABLE_READ"/>
<transaction mode="BATCH"/>
<file-store/>
</distributed-cache>
@@ -79,6 +88,7 @@
<cache-container name="ejb" aliases="sfsb" default-cache="dist" module="org.wildfly.clustering.ejb.infinispan">
<transport lock-timeout="60000"/>
<distributed-cache name="dist" mode="ASYNC" l1-lifespan="0" owners="2">
+ <locking isolation="REPEATABLE_READ"/>
<transaction mode="BATCH"/>
<file-store/>
</distributed-cache>