Merge remote-tracking branch 'upstream/master' into eap64

This commit is contained in:
mhajas 2016-01-12 12:02:56 +01:00
commit c93db980a6
115 changed files with 3587 additions and 539 deletions

View file

@ -7,7 +7,7 @@ Keycloak is an SSO Service for web apps and REST services. For more information
Building 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 java -version
mvn -version mvn -version

View file

@ -1,5 +1,8 @@
package org.keycloak.broker.provider.util; 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.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
@ -24,6 +27,9 @@ public class SimpleHttp {
private Map<String, String> headers; private Map<String, String> headers;
private Map<String, String> params; private Map<String, String> params;
private SSLSocketFactory sslFactory;
private HostnameVerifier hostnameVerifier;
protected SimpleHttp(String url, String method) { protected SimpleHttp(String url, String method) {
this.url = url; this.url = url;
this.method = method; this.method = method;
@ -53,6 +59,15 @@ public class SimpleHttp {
return this; 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 { public String asString() throws IOException {
boolean get = method.equals("GET"); boolean get = method.equals("GET");
@ -85,6 +100,7 @@ public class SimpleHttp {
} }
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
setupTruststoreIfApplicable(connection);
OutputStream os = null; OutputStream os = null;
InputStream is = null; InputStream is = null;
@ -171,6 +187,7 @@ public class SimpleHttp {
} }
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
setupTruststoreIfApplicable(connection);
OutputStream os = null; OutputStream os = null;
InputStream is = null; InputStream is = null;
@ -235,4 +252,13 @@ public class SimpleHttp {
return writer.toString(); 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);
}
}
}
} }

View file

@ -27,6 +27,7 @@ import org.keycloak.broker.provider.AbstractIdentityProvider;
import org.keycloak.broker.provider.AuthenticationRequest; import org.keycloak.broker.provider.AuthenticationRequest;
import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.IdentityBrokerException; import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.connections.truststore.JSSETruststoreConfigurator;
import org.keycloak.events.Errors; import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder; import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType; import org.keycloak.events.EventType;
@ -247,12 +248,15 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
} }
public SimpleHttp generateTokenRequest(String authorizationCode) { public SimpleHttp generateTokenRequest(String authorizationCode) {
JSSETruststoreConfigurator configurator = new JSSETruststoreConfigurator(session);
return SimpleHttp.doPost(getConfig().getTokenUrl()) return SimpleHttp.doPost(getConfig().getTokenUrl())
.param(OAUTH2_PARAMETER_CODE, authorizationCode) .param(OAUTH2_PARAMETER_CODE, authorizationCode)
.param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId()) .param(OAUTH2_PARAMETER_CLIENT_ID, getConfig().getClientId())
.param(OAUTH2_PARAMETER_CLIENT_SECRET, getConfig().getClientSecret()) .param(OAUTH2_PARAMETER_CLIENT_SECRET, getConfig().getClientSecret())
.param(OAUTH2_PARAMETER_REDIRECT_URI, uriInfo.getAbsolutePath().toString()) .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());
} }
} }
} }

View file

@ -1,6 +1,7 @@
package org.keycloak.broker.saml; package org.keycloak.broker.saml;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.keycloak.common.ClientConnection; import org.keycloak.common.ClientConnection;
import org.keycloak.common.VerificationException; import org.keycloak.common.VerificationException;
import org.keycloak.broker.provider.BrokeredIdentityContext; import org.keycloak.broker.provider.BrokeredIdentityContext;
@ -45,6 +46,7 @@ import javax.ws.rs.Consumes;
import javax.ws.rs.FormParam; import javax.ws.rs.FormParam;
import javax.ws.rs.GET; import javax.ws.rs.GET;
import javax.ws.rs.POST; import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam; import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context; import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.HttpHeaders;
@ -95,6 +97,13 @@ public class SAMLEndpoint {
this.provider = provider; this.provider = provider;
} }
@GET
@NoCache
@Path("descriptor")
public Response getSPDescriptor() {
return provider.export(uriInfo, realm, null);
}
@GET @GET
public Response redirectBinding(@QueryParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest, public Response redirectBinding(@QueryParam(GeneralConstants.SAML_REQUEST_KEY) String samlRequest,
@QueryParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse, @QueryParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse,

View file

@ -22,6 +22,10 @@
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-model-api</artifactId> <artifactId>keycloak-model-api</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-connections-truststore</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.apache.httpcomponents</groupId> <groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId> <artifactId>httpclient</artifactId>

View file

@ -10,6 +10,7 @@ import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.CloseableHttpClient;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.connections.truststore.TruststoreProvider;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.common.util.EnvUtil; 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 static final Logger logger = Logger.getLogger(DefaultHttpClientFactory.class);
private volatile CloseableHttpClient httpClient; private volatile CloseableHttpClient httpClient;
private Config.Scope config;
@Override @Override
public HttpClientProvider create(KeycloakSession session) { public HttpClientProvider create(KeycloakSession session) {
lazyInit(session);
return new HttpClientProvider() { return new HttpClientProvider() {
@Override @Override
public HttpClient getHttpClient() { public HttpClient getHttpClient() {
@ -74,7 +78,9 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
@Override @Override
public void close() { public void close() {
try { try {
httpClient.close(); if (httpClient != null) {
httpClient.close();
}
} catch (IOException e) { } catch (IOException e) {
} }
@ -87,46 +93,62 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
@Override @Override
public void init(Config.Scope config) { public void init(Config.Scope config) {
long socketTimeout = config.getLong("socket-timeout-millis", -1L); this.config = config;
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(); private void lazyInit(KeycloakSession session) {
builder.socketTimeout(socketTimeout, TimeUnit.MILLISECONDS) if (httpClient == null) {
.establishConnectionTimeout(establishConnectionTimeout, TimeUnit.MILLISECONDS) synchronized(this) {
.maxPooledPerRoute(maxPooledPerRoute) if (httpClient == null) {
.connectionPoolSize(connectionPoolSize) long socketTimeout = config.getLong("socket-timeout-millis", -1L);
.hostnameVerification(hostnamePolicy) long establishConnectionTimeout = config.getLong("establish-connection-timeout-millis", -1L);
.disableCookies(disableCookies); int maxPooledPerRoute = config.getInt("max-pooled-per-route", 0);
if (disableTrustManager) builder.disableTrustManager(); int connectionPoolSize = config.getInt("connection-pool-size", 200);
if (truststore != null) { boolean disableCookies = config.getBoolean("disable-cookies", true);
truststore = EnvUtil.replace(truststore); String clientKeystore = config.get("client-keystore");
try { String clientKeystorePassword = config.get("client-keystore-password");
builder.trustStore(KeystoreUtil.loadKeyStore(truststore, truststorePassword)); String clientPrivateKeyPassword = config.get("client-key-password");
} catch (Exception e) {
throw new RuntimeException("Failed to load truststore", e); 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();
}
} }
} }
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();
} }
@Override @Override

View file

@ -145,7 +145,7 @@ public class HttpClientBuilder {
* Disable cookie management. * Disable cookie management.
*/ */
public HttpClientBuilder disableCookies(boolean disable) { public HttpClientBuilder disableCookies(boolean disable) {
this.disableTrustManager = disable; this.disableCookies = disable;
return this; return this;
} }

View file

@ -44,7 +44,7 @@ public abstract class Update {
o.append(f, 1); o.append(f, 1);
} }
col.ensureIndex(o, new BasicDBObject("unique", unique).append("sparse", sparse)); col.createIndex(o, new BasicDBObject("unique", unique).append("sparse", sparse));
log.debugv("Created index {0}, fields={1}, unique={2}, sparse={3}", name, Arrays.toString(fields), unique, sparse); log.debugv("Created index {0}, fields={1}, unique={2}, sparse={3}", name, Arrays.toString(fields), unique, sparse);
} }

View file

@ -18,7 +18,7 @@ public class Update1_0_0_Final extends Update {
@Override @Override
public void update(KeycloakSession session) throws ClassNotFoundException { public void update(KeycloakSession session) throws ClassNotFoundException {
DBCollection realmsCollection = db.getCollection("realms"); DBCollection realmsCollection = db.getCollection("realms");
realmsCollection.ensureIndex(new BasicDBObject("name", 1), new BasicDBObject("unique", true)); realmsCollection.createIndex(new BasicDBObject("name", 1), new BasicDBObject("unique", true));
DefaultMongoUpdaterProvider.log.debugv("Created collection {0}", "realms"); DefaultMongoUpdaterProvider.log.debugv("Created collection {0}", "realms");

View file

@ -174,7 +174,6 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
operationalInfo.put("mongoHosts", hosts); operationalInfo.put("mongoHosts", hosts);
operationalInfo.put("mongoDatabaseName", dbName); operationalInfo.put("mongoDatabaseName", dbName);
operationalInfo.put("mongoUser", uri.getUsername()); operationalInfo.put("mongoUser", uri.getUsername());
operationalInfo.put("mongoDriverVersion", client.getVersion());
logger.debugv("Initialized mongo model. host(s): %s, db: %s", uri.getHosts(), dbName); logger.debugv("Initialized mongo model. host(s): %s, db: %s", uri.getHosts(), dbName);
return client; return client;
@ -198,7 +197,6 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
operationalInfo.put("mongoServerAddress", client.getAddress().toString()); operationalInfo.put("mongoServerAddress", client.getAddress().toString());
operationalInfo.put("mongoDatabaseName", dbName); operationalInfo.put("mongoDatabaseName", dbName);
operationalInfo.put("mongoUser", user); operationalInfo.put("mongoUser", user);
operationalInfo.put("mongoDriverVersion", client.getVersion());
logger.debugv("Initialized mongo model. host: %s, port: %d, db: %s", host, port, dbName); logger.debugv("Initialized mongo model. host: %s, port: %d, db: %s", host, port, dbName);
return client; return client;
@ -214,9 +212,6 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
checkIntOption("socketTimeout", builder); checkIntOption("socketTimeout", builder);
checkBooleanOption("socketKeepAlive", builder); checkBooleanOption("socketKeepAlive", builder);
checkBooleanOption("autoConnectRetry", builder); checkBooleanOption("autoConnectRetry", builder);
if (config.getLong("maxAutoConnectRetryTime") != null) {
builder.maxAutoConnectRetryTime(config.getLong("maxAutoConnectRetryTime"));
}
if(config.getBoolean("ssl", false)) { if(config.getBoolean("ssl", false)) {
builder.socketFactory(SSLSocketFactory.getDefault()); builder.socketFactory(SSLSocketFactory.getDefault());
} }

View file

@ -1,13 +1,6 @@
package org.keycloak.connections.mongo.impl; package org.keycloak.connections.mongo.impl;
import com.mongodb.BasicDBList; import com.mongodb.*;
import com.mongodb.BasicDBObject;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;
import com.mongodb.MongoException;
import com.mongodb.WriteResult;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.connections.mongo.api.MongoCollection; import org.keycloak.connections.mongo.api.MongoCollection;
import org.keycloak.connections.mongo.api.MongoEntity; import org.keycloak.connections.mongo.api.MongoEntity;
@ -133,7 +126,7 @@ public class MongoStoreImpl implements MongoStore {
} }
public static ModelException convertException(MongoException e) { public static ModelException convertException(MongoException e) {
if (e instanceof MongoException.DuplicateKey) { if (e instanceof DuplicateKeyException) {
return new ModelDuplicateException(e); return new ModelDuplicateException(e);
} else { } else {
return new ModelException(e); return new ModelException(e);

View file

@ -19,6 +19,7 @@
<module>mongo</module> <module>mongo</module>
<module>mongo-update</module> <module>mongo-update</module>
<module>http-client</module> <module>http-client</module>
<module>truststore</module>
</modules> </modules>
<build> <build>

35
connections/truststore/pom.xml Executable file
View file

@ -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>

View file

@ -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() {
}
}

View file

@ -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";
}
}

View file

@ -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
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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();
}

View file

@ -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> {
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -0,0 +1 @@
org.keycloak.connections.truststore.FileTruststoreProviderFactory

View file

@ -0,0 +1 @@
org.keycloak.connections.truststore.TruststoreSpi

View file

@ -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;
}
}

View file

@ -131,7 +131,10 @@
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-export-import-single-file</artifactId> <artifactId>keycloak-export-import-single-file</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-connections-truststore</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-connections-http-client</artifactId> <artifactId>keycloak-connections-http-client</artifactId>

View file

@ -45,9 +45,7 @@
}, },
"connectionsHttpClient": { "connectionsHttpClient": {
"default": { "default": {}
"disable-trust-manager": true
}
}, },
"connectionsJpa": { "connectionsJpa": {

View file

@ -13,6 +13,7 @@
<module name="org.keycloak.keycloak-events-api"/> <module name="org.keycloak.keycloak-events-api"/>
<module name="javax.ws.rs.api"/> <module name="javax.ws.rs.api"/>
<module name="org.jboss.logging"/> <module name="org.jboss.logging"/>
<module name="javax.api"/>
</dependencies> </dependencies>
</module> </module>

View file

@ -13,6 +13,7 @@
<module name="org.keycloak.keycloak-events-api"/> <module name="org.keycloak.keycloak-events-api"/>
<module name="org.keycloak.keycloak-broker-core"/> <module name="org.keycloak.keycloak-broker-core"/>
<module name="org.keycloak.keycloak-services"/> <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-core-asl"/>
<module name="org.codehaus.jackson.jackson-mapper-asl"/> <module name="org.codehaus.jackson.jackson-mapper-asl"/>
<module name="org.codehaus.jackson.jackson-xc"/> <module name="org.codehaus.jackson.jackson-xc"/>

View file

@ -16,6 +16,7 @@
<module name="org.jboss.logging"/> <module name="org.jboss.logging"/>
<module name="javax.api"/> <module name="javax.api"/>
<module name="org.apache.httpcomponents"/> <module name="org.apache.httpcomponents"/>
<module name="org.keycloak.keycloak-connections-truststore"/>
</dependencies> </dependencies>
</module> </module>

View file

@ -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>

View file

@ -15,6 +15,7 @@
<module name="org.keycloak.keycloak-core"/> <module name="org.keycloak.keycloak-core"/>
<module name="org.keycloak.keycloak-services"/> <module name="org.keycloak.keycloak-services"/>
<module name="org.keycloak.keycloak-social-core"/> <module name="org.keycloak.keycloak-social-core"/>
<module name="org.keycloak.keycloak-broker-core"/>
<module name="javax.ws.rs.api"/> <module name="javax.ws.rs.api"/>
<module name="org.jboss.logging"/> <module name="org.jboss.logging"/>
<module name="org.freemarker"/> <module name="org.freemarker"/>

View file

@ -8,6 +8,7 @@
<module name="org.keycloak.keycloak-connections-jpa-liquibase" services="import"/> <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" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo-update" 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-common" services="import"/>
<module name="org.keycloak.keycloak-core" services="import"/> <module name="org.keycloak.keycloak-core" services="import"/>
<module name="org.keycloak.keycloak-email-api" services="import"/> <module name="org.keycloak.keycloak-email-api" services="import"/>

View file

@ -18,6 +18,7 @@
<module name="org.keycloak.keycloak-connections-jpa-liquibase" services="import"/> <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" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo-update" 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-common" services="import"/>
<module name="org.keycloak.keycloak-core" services="import"/> <module name="org.keycloak.keycloak-core" services="import"/>
<module name="org.keycloak.keycloak-email-api" services="import"/> <module name="org.keycloak.keycloak-email-api" services="import"/>

View file

@ -181,6 +181,10 @@
<maven-resource group="org.keycloak" artifact="keycloak-connections-infinispan"/> <maven-resource group="org.keycloak" artifact="keycloak-connections-infinispan"/>
</module-def> </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"> <module-def name="org.keycloak.keycloak-model-jpa">
<maven-resource group="org.keycloak" artifact="keycloak-model-jpa"/> <maven-resource group="org.keycloak" artifact="keycloak-model-jpa"/>
</module-def> </module-def>

View file

@ -13,6 +13,7 @@
<module name="org.keycloak.keycloak-events-api"/> <module name="org.keycloak.keycloak-events-api"/>
<module name="javax.ws.rs.api"/> <module name="javax.ws.rs.api"/>
<module name="org.jboss.logging"/> <module name="org.jboss.logging"/>
<module name="javax.api"/>
</dependencies> </dependencies>
</module> </module>

View file

@ -13,6 +13,7 @@
<module name="org.keycloak.keycloak-events-api"/> <module name="org.keycloak.keycloak-events-api"/>
<module name="org.keycloak.keycloak-broker-core"/> <module name="org.keycloak.keycloak-broker-core"/>
<module name="org.keycloak.keycloak-services"/> <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-core-asl"/>
<module name="org.codehaus.jackson.jackson-mapper-asl"/> <module name="org.codehaus.jackson.jackson-mapper-asl"/>
<module name="org.codehaus.jackson.jackson-xc"/> <module name="org.codehaus.jackson.jackson-xc"/>

View file

@ -13,6 +13,7 @@
<module name="org.keycloak.keycloak-common"/> <module name="org.keycloak.keycloak-common"/>
<module name="org.keycloak.keycloak-core"/> <module name="org.keycloak.keycloak-core"/>
<module name="org.keycloak.keycloak-model-api"/> <module name="org.keycloak.keycloak-model-api"/>
<module name="org.keycloak.keycloak-connections-truststore"/>
<module name="org.jboss.logging"/> <module name="org.jboss.logging"/>
<module name="javax.api"/> <module name="javax.api"/>
<module name="org.apache.httpcomponents"/> <module name="org.apache.httpcomponents"/>

View file

@ -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>

View file

@ -8,6 +8,7 @@
<module name="org.keycloak.keycloak-connections-jpa-liquibase" services="import"/> <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" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo-update" 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-common" services="import"/>
<module name="org.keycloak.keycloak-core" services="import"/> <module name="org.keycloak.keycloak-core" services="import"/>
<module name="org.keycloak.keycloak-email-api" services="import"/> <module name="org.keycloak.keycloak-email-api" services="import"/>

View file

@ -15,6 +15,7 @@
<module name="org.keycloak.keycloak-core"/> <module name="org.keycloak.keycloak-core"/>
<module name="org.keycloak.keycloak-services"/> <module name="org.keycloak.keycloak-services"/>
<module name="org.keycloak.keycloak-social-core"/> <module name="org.keycloak.keycloak-social-core"/>
<module name="org.keycloak.keycloak-broker-core"/>
<module name="javax.ws.rs.api"/> <module name="javax.ws.rs.api"/>
<module name="org.jboss.logging"/> <module name="org.jboss.logging"/>
<module name="org.freemarker"/> <module name="org.freemarker"/>

View file

@ -16,6 +16,7 @@
<module name="org.keycloak.keycloak-core"/> <module name="org.keycloak.keycloak-core"/>
<module name="org.keycloak.keycloak-services"/> <module name="org.keycloak.keycloak-services"/>
<module name="org.keycloak.keycloak-social-core"/> <module name="org.keycloak.keycloak-social-core"/>
<module name="org.keycloak.keycloak-broker-core"/>
<module name="javax.ws.rs.api"/> <module name="javax.ws.rs.api"/>
<module name="org.jboss.logging"/> <module name="org.jboss.logging"/>
<module name="org.freemarker"/> <module name="org.freemarker"/>

View file

@ -18,6 +18,7 @@
<module name="org.keycloak.keycloak-connections-jpa-liquibase" services="import"/> <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" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo-update" 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-common" services="import"/>
<module name="org.keycloak.keycloak-core" services="import"/> <module name="org.keycloak.keycloak-core" services="import"/>
<module name="org.keycloak.keycloak-email-api" services="import"/> <module name="org.keycloak.keycloak-email-api" services="import"/>

View file

@ -1,114 +1,134 @@
<chapter id="export-import"> <chapter id="export-import">
<title>Export and Import</title> <title>Export and Import</title>
<para> <section>
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). <title>Startup export/import</title>
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 <para>
and edit Keycloak database on the fly when export or import is in progress. Otherwise it could lead to inconsistent results. 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).
</para> 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
<para> and edit Keycloak database on the fly when export or import is in progress. Otherwise it could lead to inconsistent results.
You can export/import your database either to: </para>
<itemizedlist> <para>
<listitem>Directory on local filesystem</listitem> You can export/import your database either to:
<listitem>Single JSON file on your filesystem</listitem> <itemizedlist>
</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. 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. If you are importing files which were previously exported, the files already follow this convention.
<itemizedlist> <itemizedlist>
<listitem>{REALM_NAME}-realm.json, such as "acme-roadrunner-affairs-realm.json" for the realm named "acme-roadrunner-affairs"</listitem> <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> <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> </itemizedlist>
</para> </para>
<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 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. 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. Processing of each file is done in separate transaction as exporting/importing all users at once could also lead to memory issues.
</para> </para>
<para> <para>
To export into unencrypted directory you can use: To export into unencrypted directory you can use:
<programlisting><![CDATA[ <programlisting><![CDATA[
bin/standalone.sh -Dkeycloak.migration.action=export bin/standalone.sh -Dkeycloak.migration.action=export
-Dkeycloak.migration.provider=dir -Dkeycloak.migration.dir=<DIR TO EXPORT TO> -Dkeycloak.migration.provider=dir -Dkeycloak.migration.dir=<DIR TO EXPORT TO>
]]></programlisting> ]]></programlisting>
And similarly for import just use <literal>-Dkeycloak.migration.action=import</literal> instead of <literal>export</literal> . And similarly for import just use <literal>-Dkeycloak.migration.action=import</literal> instead of <literal>export</literal> .
</para> </para>
<para> <para>
To export into single JSON file you can use: To export into single JSON file you can use:
<programlisting><![CDATA[ <programlisting><![CDATA[
bin/standalone.sh -Dkeycloak.migration.action=export bin/standalone.sh -Dkeycloak.migration.action=export
-Dkeycloak.migration.provider=singleFile -Dkeycloak.migration.file=<FILE TO EXPORT TO> -Dkeycloak.migration.provider=singleFile -Dkeycloak.migration.file=<FILE TO EXPORT TO>
]]></programlisting> ]]></programlisting>
</para> </para>
<para> <para>
Here's an example of importing: Here's an example of importing:
<programlisting><![CDATA[ <programlisting><![CDATA[
bin/standalone.sh -Dkeycloak.migration.action=import bin/standalone.sh -Dkeycloak.migration.action=import
-Dkeycloak.migration.provider=singleFile -Dkeycloak.migration.file=<FILE TO IMPORT> -Dkeycloak.migration.provider=singleFile -Dkeycloak.migration.file=<FILE TO IMPORT>
-Dkeycloak.migration.strategy=OVERWRITE_EXISTING -Dkeycloak.migration.strategy=OVERWRITE_EXISTING
]]></programlisting> ]]></programlisting>
</para> </para>
<para> <para>
Other available options are: Other available options are:
<variablelist> <variablelist>
<varlistentry> <varlistentry>
<term>-Dkeycloak.migration.realmName</term> <term>-Dkeycloak.migration.realmName</term>
<listitem> <listitem>
<para> <para>
can be used if you want to export just one specified realm instead of all. can be used if you want to export just one specified realm instead of all.
If not specified, then all realms will be exported. If not specified, then all realms will be exported.
</para> </para>
</listitem> </listitem>
</varlistentry> </varlistentry>
<varlistentry> <varlistentry>
<term>-Dkeycloak.migration.usersExportStrategy</term> <term>-Dkeycloak.migration.usersExportStrategy</term>
<listitem> <listitem>
<para> <para>
can be used to specify for Directory providers to specify where to import users. can be used to specify for Directory providers to specify where to import users.
Possible values are: Possible values are:
<itemizedlist> <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>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>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>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> <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> </itemizedlist>
</para> </para>
</listitem> </listitem>
</varlistentry> </varlistentry>
<varlistentry> <varlistentry>
<term>-Dkeycloak.migration.usersPerFile</term> <term>-Dkeycloak.migration.usersPerFile</term>
<listitem> <listitem>
<para> <para>
can be used to specify number of users per file (and also per DB transaction). 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 It's 5000 by default. It's used only if usersExportStrategy is DIFFERENT_FILES
</para> </para>
</listitem> </listitem>
</varlistentry> </varlistentry>
<varlistentry> <varlistentry>
<term>-Dkeycloak.migration.strategy</term> <term>-Dkeycloak.migration.strategy</term>
<listitem> <listitem>
<para> <para>
is used during import. It can be used to specify how to proceed if realm with same name 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: already exists in the database where you are going to import data. Possible values are:
<itemizedlist> <itemizedlist>
<listitem>IGNORE_EXISTING - Ignore importing if realm of this name already exists</listitem> <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. <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 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. like the old one, you can specify this.
</listitem> </listitem>
</itemizedlist> </itemizedlist>
</para> </para>
</listitem> </listitem>
</varlistentry> </varlistentry>
</variablelist> </variablelist>
</para> </para>
<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 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 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: will happen only after the master realm has been initialized. Examples:
<itemizedlist> <itemizedlist>
<listitem>-Dkeycloak.import=/tmp/realm1.json</listitem> <listitem>-Dkeycloak.import=/tmp/realm1.json</listitem>
<listitem>-Dkeycloak.import=/tmp/realm1.json,/tmp/realm2.json</listitem> <listitem>-Dkeycloak.import=/tmp/realm1.json,/tmp/realm2.json</listitem>
</itemizedlist> </itemizedlist>
</para> </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> </chapter>

View file

@ -1052,7 +1052,7 @@
<literal>HTTP-POST Binding for AuthnReques</literal> <literal>HTTP-POST Binding for AuthnReques</literal>
</entry> </entry>
<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> </entry>
</row> </row>
</tbody> </tbody>
@ -1066,6 +1066,16 @@
Once you create a SAML provider, there is an <literal>EXPORT</literal> button that appears when viewing that provider. 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 Clicking this button will export a SAML entity descriptor which you can use to
</para> </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>
<section> <section>

View file

@ -4,7 +4,8 @@
<section> <section>
<title>Installation</title> <title>Installation</title>
<para> <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>
<para> <para>
<itemizedlist> <itemizedlist>
@ -381,9 +382,7 @@ bin/add-user.[sh|bat] -r master -u <username> -p <password>
By default the setting is like this: By default the setting is like this:
<programlisting><![CDATA[ <programlisting><![CDATA[
"connectionsHttpClient": { "connectionsHttpClient": {
"default": { "default": {}
"disable-trust-manager": true
}
}, },
]]></programlisting> ]]></programlisting>
Possible configuration options are: Possible configuration options are:
@ -420,15 +419,6 @@ bin/add-user.[sh|bat] -r master -u <username> -p <password>
</para> </para>
</listitem> </listitem>
</varlistentry> </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> <varlistentry>
<term>disable-cookies</term> <term>disable-cookies</term>
<listitem> <listitem>
@ -438,44 +428,6 @@ bin/add-user.[sh|bat] -r master -u <username> -p <password>
</para> </para>
</listitem> </listitem>
</varlistentry> </varlistentry>
<varlistentry>
<term>hostname-verification-policy</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.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>truststore</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.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>truststore-password</term>
<listitem>
<para>
Password for the truststore keystore.
This is
<emphasis>REQUIRED</emphasis>
if
<literal>truststore</literal>
is set.
</para>
</listitem>
</varlistentry>
<varlistentry> <varlistentry>
<term>client-keystore</term> <term>client-keystore</term>
<listitem> <listitem>
@ -515,13 +467,111 @@ bin/add-user.[sh|bat] -r master -u <username> -p <password>
</variablelist> </variablelist>
</para> </para>
</section> </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>file</term>
<listitem>
<para>
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>password</term>
<listitem>
<para>
Password for the truststore.
This is
<emphasis>REQUIRED</emphasis>
if <literal>disabled</literal> is not true.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>hostname-verification-policy</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.
</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"> <section id="ssl_modes">
<title>SSL/HTTPS Requirement/Modes</title> <title>SSL/HTTPS Requirement/Modes</title>
<warning> <warning>
<para> <para>
Keycloak is not set up by default to handle SSL/HTTPS in either the Keycloak is not set up by default to handle SSL/HTTPS. It is highly recommended that you either enable SSL
war distribution or appliance. It is highly recommended that you either enable SSL on the Keycloak server on the Keycloak server itself or on a reverse proxy in front of the Keycloak server.
itself or on a reverse proxy in front of the Keycloak server.
</para> </para>
</warning> </warning>
<para> <para>

View file

@ -12,4 +12,9 @@
<gap:plugin name="cordova-plugin-whitelist" version="1.0.0" source="npm" /> <gap:plugin name="cordova-plugin-whitelist" version="1.0.0" source="npm" />
<access origin="*"/> <access origin="*"/>
<allow-navigation href="*" />
<allow-intent href="http://*/*" />
<allow-intent href="https://*/*" />
</widget> </widget>

View file

@ -3,6 +3,8 @@
<head> <head>
<title>Authentication Example</title> <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="cordova.js"></script>
<script type="text/javascript" charset="utf-8" src="keycloak.js"></script> <script type="text/javascript" charset="utf-8" src="keycloak.js"></script>
<script type="text/javascript" charset="utf-8"> <script type="text/javascript" charset="utf-8">

View file

@ -467,6 +467,9 @@ public class LDAPOperationManager {
if (protocol != null) { if (protocol != null) {
env.put(Context.SECURITY_PROTOCOL, protocol); 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(); String bindDN = this.config.getBindDN();

View file

@ -406,6 +406,16 @@ module.config([ '$routeProvider', function($routeProvider) {
}, },
controller : 'RealmEventsConfigCtrl' 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', { .when('/create/user/:realm', {
templateUrl : resourceUrl + '/partials/user-detail.html', templateUrl : resourceUrl + '/partials/user-detail.html',
resolve : { resolve : {

View file

@ -1055,8 +1055,10 @@ module.controller('CreateClientCtrl', function($scope, realm, client, templates,
'saml'];//Object.keys(serverInfo.providers['login-protocol'].providers).sort(); 'saml'];//Object.keys(serverInfo.providers['login-protocol'].providers).sort();
$scope.create = true; $scope.create = true;
$scope.templates = [ {name:'NONE'}]; $scope.templates = [ {name:'NONE'}];
var templateNameMap = new Object();
for (var i = 0; i < templates.length; i++) { for (var i = 0; i < templates.length; i++) {
var template = templates[i]; var template = templates[i];
templateNameMap[template.name] = template;
$scope.templates.push(template); $scope.templates.push(template);
} }
@ -1096,6 +1098,18 @@ module.controller('CreateClientCtrl', function($scope, realm, client, templates,
$scope.changed = true; $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() { $scope.changeProtocol = function() {
if ($scope.protocol == "openid-connect") { if ($scope.protocol == "openid-connect") {
$scope.client.protocol = "openid-connect"; $scope.client.protocol = "openid-connect";

View file

@ -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();
}
});

View file

@ -37,7 +37,8 @@
<select class="form-control" id="protocol" <select class="form-control" id="protocol"
ng-change="changeProtocol()" ng-change="changeProtocol()"
ng-model="protocol" ng-model="protocol"
ng-options="aProtocol for aProtocol in protocols"> ng-options="aProtocol for aProtocol in protocols"
ng-disabled="client.clientTemplate">
</select> </select>
</div> </div>
</div> </div>
@ -48,6 +49,7 @@
<div class="col-sm-6"> <div class="col-sm-6">
<div> <div>
<select class="form-control" id="template" <select class="form-control" id="template"
ng-change="changeTemplate()"
ng-model="client.clientTemplate" ng-model="client.clientTemplate"
ng-options="template.name as template.name for template in templates"> ng-options="template.name as template.name for template in templates">
</select> </select>

View file

@ -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>

View file

@ -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.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.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.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> </ul>
</div> </div>
</div> </div>

View file

@ -75,9 +75,6 @@
background-image: url("../img/keycloak-logo.png"); background-image: url("../img/keycloak-logo.png");
background-repeat: no-repeat; background-repeat: no-repeat;
position: absolute;
top: 50px;
right: 50px;
height: 37px; height: 37px;
width: 154px; width: 154px;
} }
@ -96,12 +93,6 @@
margin-bottom: 15px; margin-bottom: 15px;
} }
#kc-container-wrapper {
bottom: 13%;
position: absolute;
width: 100%;
}
#kc-content { #kc-content {
position: relative; 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; background-image: linear-gradient(rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%) !important;
} }
@media (max-width: 767px) { @media (min-width: 768px) {
#kc-container-wrapper {
bottom: 13%;
position: absolute;
width: 100%;
}
#kc-logo-wrapper { #kc-logo-wrapper {
top: 15px; position: absolute;
right: 15px; top: 50px;
right: 50px;
}
}
@media (max-width: 767px) {
#kc-logo-wrapper {
background-position: center;
width: 100%;
margin: 20px 0;
} }
#kc-header { #kc-header {
padding-left: 15px; padding-left: 15px;
padding-right: 15px; padding-right: 15px;
float: none; float: none;
text-align: center;
} }
#kc-feedback { #kc-feedback {
@ -323,7 +331,5 @@ ol#kc-totp-settings li:first-of-type {
@media (max-height: 500px) { @media (max-height: 500px) {
#kc-container-wrapper { #kc-container-wrapper {
position: inherit;
float: none;
} }
} }

View file

@ -8,7 +8,10 @@ import org.keycloak.models.*;
public class PasswordHashManager { public class PasswordHashManager {
public static UserCredentialValueModel encode(KeycloakSession session, RealmModel realm, String rawPassword) { public static UserCredentialValueModel encode(KeycloakSession session, RealmModel realm, String rawPassword) {
PasswordPolicy passwordPolicy = realm.getPasswordPolicy(); return encode(session, realm.getPasswordPolicy(), rawPassword);
}
public static UserCredentialValueModel encode(KeycloakSession session, PasswordPolicy passwordPolicy, String rawPassword) {
String algorithm = passwordPolicy.getHashAlgorithm(); String algorithm = passwordPolicy.getHashAlgorithm();
int iterations = passwordPolicy.getHashIterations(); int iterations = passwordPolicy.getHashIterations();
if (iterations < 1) { if (iterations < 1) {
@ -22,7 +25,11 @@ public class PasswordHashManager {
} }
public static boolean verify(KeycloakSession session, RealmModel realm, String password, UserCredentialValueModel credential) { public static boolean verify(KeycloakSession session, RealmModel realm, String password, UserCredentialValueModel credential) {
String algorithm = credential.getAlgorithm() != null ? credential.getAlgorithm() : realm.getPasswordPolicy().getHashAlgorithm(); return verify(session, realm.getPasswordPolicy(), password, credential);
}
public static boolean verify(KeycloakSession session, PasswordPolicy passwordPolicy, String password, UserCredentialValueModel credential) {
String algorithm = credential.getAlgorithm() != null ? credential.getAlgorithm() : passwordPolicy.getHashAlgorithm();
PasswordHashProvider provider = session.getProvider(PasswordHashProvider.class, algorithm); PasswordHashProvider provider = session.getProvider(PasswordHashProvider.class, algorithm);
return provider.verify(password, credential); return provider.verify(password, credential);
} }

View file

@ -30,60 +30,52 @@ public class PasswordPolicy implements Serializable {
private String policyString; private String policyString;
public PasswordPolicy(String policyString) { public PasswordPolicy(String policyString) {
if (policyString == null || policyString.length() == 0) { this.policyString = policyString;
this.policyString = null; this.policies = new LinkedList<>();
policies = Collections.emptyList();
} else {
this.policyString = policyString;
policies = parse(policyString);
}
}
private static List<Policy> parse(String policyString) { if (policyString != null && !policyString.isEmpty()) {
List<Policy> list = new LinkedList<Policy>(); for (String policy : policyString.split(" and ")) {
String[] policies = policyString.split(" and "); policy = policy.trim();
for (String policy : policies) {
policy = policy.trim();
String name; String name;
String arg = null; String arg = null;
int i = policy.indexOf('('); int i = policy.indexOf('(');
if (i == -1) { if (i == -1) {
name = policy.trim(); name = policy.trim();
} else { } else {
name = policy.substring(0, i).trim(); name = policy.substring(0, i).trim();
arg = policy.substring(i + 1, policy.length() - 1); arg = policy.substring(i + 1, policy.length() - 1);
} }
if (name.equals(Length.NAME)) { if (name.equals(Length.NAME)) {
list.add(new Length(arg)); policies.add(new Length(arg));
} else if (name.equals(Digits.NAME)) { } else if (name.equals(Digits.NAME)) {
list.add(new Digits(arg)); policies.add(new Digits(arg));
} else if (name.equals(LowerCase.NAME)) { } else if (name.equals(LowerCase.NAME)) {
list.add(new LowerCase(arg)); policies.add(new LowerCase(arg));
} else if (name.equals(UpperCase.NAME)) { } else if (name.equals(UpperCase.NAME)) {
list.add(new UpperCase(arg)); policies.add(new UpperCase(arg));
} else if (name.equals(SpecialChars.NAME)) { } else if (name.equals(SpecialChars.NAME)) {
list.add(new SpecialChars(arg)); policies.add(new SpecialChars(arg));
} else if (name.equals(NotUsername.NAME)) { } else if (name.equals(NotUsername.NAME)) {
list.add(new NotUsername(arg)); policies.add(new NotUsername(arg));
} else if (name.equals(HashAlgorithm.NAME)) { } else if (name.equals(HashAlgorithm.NAME)) {
list.add(new HashAlgorithm(arg)); policies.add(new HashAlgorithm(arg));
} else if (name.equals(HashIterations.NAME)) { } else if (name.equals(HashIterations.NAME)) {
list.add(new HashIterations(arg)); policies.add(new HashIterations(arg));
} else if (name.equals(RegexPatterns.NAME)) { } else if (name.equals(RegexPatterns.NAME)) {
Pattern.compile(arg); Pattern.compile(arg);
list.add(new RegexPatterns(arg)); policies.add(new RegexPatterns(arg));
} else if (name.equals(PasswordHistory.NAME)) { } else if (name.equals(PasswordHistory.NAME)) {
list.add(new PasswordHistory(arg)); policies.add(new PasswordHistory(arg, this));
} else if (name.equals(ForceExpiredPasswordChange.NAME)) { } else if (name.equals(ForceExpiredPasswordChange.NAME)) {
list.add(new ForceExpiredPasswordChange(arg)); policies.add(new ForceExpiredPasswordChange(arg));
} else { } else {
throw new IllegalArgumentException("Unsupported policy"); throw new IllegalArgumentException("Unsupported policy");
}
} }
} }
return list;
} }
public String getHashAlgorithm() { public String getHashAlgorithm() {
@ -396,10 +388,12 @@ public class PasswordPolicy implements Serializable {
private static class PasswordHistory implements Policy { private static class PasswordHistory implements Policy {
private static final String NAME = "passwordHistory"; private static final String NAME = "passwordHistory";
private final PasswordPolicy passwordPolicy;
private int passwordHistoryPolicyValue; private int passwordHistoryPolicyValue;
public PasswordHistory(String arg) public PasswordHistory(String arg, PasswordPolicy passwordPolicy)
{ {
this.passwordPolicy = passwordPolicy;
passwordHistoryPolicyValue = intArg(NAME, 3, arg); passwordHistoryPolicyValue = intArg(NAME, 3, arg);
} }
@ -410,13 +404,10 @@ public class PasswordPolicy implements Serializable {
@Override @Override
public Error validate(KeycloakSession session, UserModel user, String password) { public Error validate(KeycloakSession session, UserModel user, String password) {
if (passwordHistoryPolicyValue != -1) { if (passwordHistoryPolicyValue != -1) {
UserCredentialValueModel cred = getCredentialValueModel(user, UserCredentialModel.PASSWORD); UserCredentialValueModel cred = getCredentialValueModel(user, UserCredentialModel.PASSWORD);
if (cred != null) { if (cred != null) {
PasswordHashProvider hashProvider = session.getProvider(PasswordHashProvider.class, cred.getAlgorithm()); if(PasswordHashManager.verify(session, passwordPolicy, password, cred)) {
if(hashProvider.verify(password, cred)) {
return new Error(INVALID_PASSWORD_HISTORY, passwordHistoryPolicyValue); return new Error(INVALID_PASSWORD_HISTORY, passwordHistoryPolicyValue);
} }
} }
@ -424,8 +415,7 @@ public class PasswordPolicy implements Serializable {
List<UserCredentialValueModel> passwordExpiredCredentials = getCredentialValueModels(user, passwordHistoryPolicyValue - 1, List<UserCredentialValueModel> passwordExpiredCredentials = getCredentialValueModels(user, passwordHistoryPolicyValue - 1,
UserCredentialModel.PASSWORD_HISTORY); UserCredentialModel.PASSWORD_HISTORY);
for (UserCredentialValueModel credential : passwordExpiredCredentials) { for (UserCredentialValueModel credential : passwordExpiredCredentials) {
PasswordHashProvider hashProvider = session.getProvider(PasswordHashProvider.class, cred.getAlgorithm()); if (PasswordHashManager.verify(session, passwordPolicy, password, credential)) {
if (hashProvider.verify(password, credential)) {
return new Error(INVALID_PASSWORD_HISTORY, passwordHistoryPolicyValue); return new Error(INVALID_PASSWORD_HISTORY, passwordHistoryPolicyValue);
} }
} }

View file

@ -66,6 +66,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.TreeSet; import java.util.TreeSet;
import org.keycloak.representations.idm.RolesRepresentation;
public class RepresentationToModel { public class RepresentationToModel {
@ -195,47 +196,7 @@ public class RepresentationToModel {
createClients(session, rep, newRealm); createClients(session, rep, newRealm);
} }
if (rep.getRoles() != null) { importRoles(rep.getRoles(), newRealm);
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);
}
}
}
}
// Setup realm default roles // Setup realm default roles
if (rep.getDefaultRoles() != null) { 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) { public static void importGroups(RealmModel realm, RealmRepresentation rep) {
List<GroupRepresentation> groups = rep.getGroups(); List<GroupRepresentation> groups = rep.getGroups();
if (groups == null) return; if (groups == null) return;

View file

@ -81,11 +81,8 @@ public class InfinispanUserCache implements UserCache {
@Override @Override
public void invalidateRealmUsers(String realmId) { public void invalidateRealmUsers(String realmId) {
logger.tracev("Invalidating users for realm {0}", realmId); logger.tracev("Invalidating users for realm {0}", realmId);
for (Map.Entry<String, CachedUser> u : cache.entrySet()) {
if (u.getValue().getRealm().equals(realmId)) { cache.clear();
cache.remove(u.getKey());
}
}
} }
@Override @Override

View file

@ -76,9 +76,12 @@ public class JpaUserProvider implements UserProvider {
userModel.joinGroup(g); userModel.joinGroup(g);
} }
} }
for (RequiredActionProviderModel r : realm.getRequiredActionProviders()) {
if (r.isEnabled() && r.isDefaultAction()) { if (addDefaultRequiredActions){
userModel.addRequiredAction(r.getAlias()); for (RequiredActionProviderModel r : realm.getRequiredActionProviders()) {
if (r.isEnabled() && r.isDefaultAction()) {
userModel.addRequiredAction(r.getAlias());
}
} }
} }

View file

@ -1059,6 +1059,7 @@ public class RealmAdapter implements RealmModel {
em.createNamedQuery("deleteGroupRoleMappingsByRole").setParameter("roleId", roleEntity.getId()).executeUpdate(); em.createNamedQuery("deleteGroupRoleMappingsByRole").setParameter("roleId", roleEntity.getId()).executeUpdate();
em.remove(roleEntity); em.remove(roleEntity);
em.flush();
return true; return true;
} }

View file

@ -433,25 +433,23 @@ public class UserAdapter implements UserModel {
@Override @Override
public List<UserCredentialValueModel> getCredentialsDirectly() { public List<UserCredentialValueModel> getCredentialsDirectly() {
List<CredentialEntity> credentials = new ArrayList<CredentialEntity>(user.getCredentials()); List<CredentialEntity> credentials = new ArrayList<>(user.getCredentials());
List<UserCredentialValueModel> result = new ArrayList<UserCredentialValueModel>(); List<UserCredentialValueModel> result = new ArrayList<>();
if (credentials != null) { for (CredentialEntity credEntity : credentials) {
for (CredentialEntity credEntity : credentials) { UserCredentialValueModel credModel = new UserCredentialValueModel();
UserCredentialValueModel credModel = new UserCredentialValueModel(); credModel.setType(credEntity.getType());
credModel.setType(credEntity.getType()); credModel.setDevice(credEntity.getDevice());
credModel.setDevice(credEntity.getDevice()); credModel.setValue(credEntity.getValue());
credModel.setValue(credEntity.getValue()); credModel.setCreatedDate(credEntity.getCreatedDate());
credModel.setCreatedDate(credEntity.getCreatedDate()); credModel.setSalt(credEntity.getSalt());
credModel.setSalt(credEntity.getSalt()); credModel.setHashIterations(credEntity.getHashIterations());
credModel.setHashIterations(credEntity.getHashIterations()); credModel.setCounter(credEntity.getCounter());
credModel.setCounter(credEntity.getCounter()); credModel.setAlgorithm(credEntity.getAlgorithm());
credModel.setAlgorithm(credEntity.getAlgorithm()); credModel.setDigits(credEntity.getDigits());
credModel.setDigits(credEntity.getDigits()); credModel.setPeriod(credEntity.getPeriod());
credModel.setPeriod(credEntity.getPeriod());
result.add(credModel); result.add(credModel);
}
} }
return result; return result;

View file

@ -710,7 +710,12 @@ public class ClientAdapter extends AbstractMongoAdapter<MongoClientEntity> imple
@Override @Override
public void setClientTemplate(ClientTemplateModel template) { public void setClientTemplate(ClientTemplateModel template) {
getMongoEntity().setClientTemplate(template.getId()); if (template == null) {
getMongoEntity().setClientTemplate(null);
} else {
getMongoEntity().setClientTemplate(template.getId());
}
updateMongoEntity(); updateMongoEntity();
} }

View file

@ -382,6 +382,8 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
credModel.setValue(credEntity.getValue()); credModel.setValue(credEntity.getValue());
credModel.setSalt(credEntity.getSalt()); credModel.setSalt(credEntity.getSalt());
credModel.setHashIterations(credEntity.getHashIterations()); credModel.setHashIterations(credEntity.getHashIterations());
credModel.setAlgorithm(credEntity.getAlgorithm());
if (UserCredentialModel.isOtp(credEntity.getType())) { if (UserCredentialModel.isOtp(credEntity.getType())) {
credModel.setCounter(credEntity.getCounter()); credModel.setCounter(credEntity.getCounter());
if (credEntity.getAlgorithm() == null) { if (credEntity.getAlgorithm() == null) {

View file

@ -1,25 +1,15 @@
package org.keycloak.models.mongo.keycloak.entities; package org.keycloak.models.mongo.keycloak.entities;
import com.mongodb.DBObject;
import com.mongodb.QueryBuilder;
import org.jboss.logging.Logger;
import org.keycloak.connections.mongo.api.MongoCollection; import org.keycloak.connections.mongo.api.MongoCollection;
import org.keycloak.connections.mongo.api.MongoField;
import org.keycloak.connections.mongo.api.MongoIdentifiableEntity; import org.keycloak.connections.mongo.api.MongoIdentifiableEntity;
import org.keycloak.connections.mongo.api.MongoStore;
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext; import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.models.entities.GroupEntity; import org.keycloak.models.entities.GroupEntity;
import org.keycloak.models.entities.RoleEntity;
import java.util.List;
/** /**
*/ */
@MongoCollection(collectionName = "groups") @MongoCollection(collectionName = "groups")
public class MongoGroupEntity extends GroupEntity implements MongoIdentifiableEntity { public class MongoGroupEntity extends GroupEntity implements MongoIdentifiableEntity {
private static final Logger logger = Logger.getLogger(MongoGroupEntity.class);
@Override @Override
public void afterRemove(MongoStoreInvocationContext invContext) { public void afterRemove(MongoStoreInvocationContext invContext) {
} }

View file

@ -26,6 +26,12 @@ public class MongoRealmEntity extends RealmEntity implements MongoIdentifiableEn
// Remove all roles of this realm // Remove all roles of this realm
context.getMongoStore().removeEntities(MongoRoleEntity.class, query, true, context); context.getMongoStore().removeEntities(MongoRoleEntity.class, query, true, context);
// Remove all client templates of this realm
context.getMongoStore().removeEntities(MongoClientTemplateEntity.class, query, true, context);
// Remove all client templates of this realm
context.getMongoStore().removeEntities(MongoGroupEntity.class, query, true, context);
// Remove all clients of this realm // Remove all clients of this realm
context.getMongoStore().removeEntities(MongoClientEntity.class, query, true, context); context.getMongoStore().removeEntities(MongoClientEntity.class, query, true, context);
} }

View file

@ -35,7 +35,7 @@
<keycloak.apache.httpcomponents.version>4.2.1</keycloak.apache.httpcomponents.version> <keycloak.apache.httpcomponents.version>4.2.1</keycloak.apache.httpcomponents.version>
<undertow.version>1.1.1.Final</undertow.version> <undertow.version>1.1.1.Final</undertow.version>
<picketlink.version>2.7.0.Final</picketlink.version> <picketlink.version>2.7.0.Final</picketlink.version>
<mongo.driver.version>2.11.3</mongo.driver.version> <mongo.driver.version>3.2.0</mongo.driver.version>
<jboss.logging.version>3.1.4.GA</jboss.logging.version> <jboss.logging.version>3.1.4.GA</jboss.logging.version>
<syslog4j.version>0.9.30</syslog4j.version> <syslog4j.version>0.9.30</syslog4j.version>
<jboss-logging-tools.version>1.2.0.Beta1</jboss-logging-tools.version> <jboss-logging-tools.version>1.2.0.Beta1</jboss-logging-tools.version>
@ -640,6 +640,11 @@
<artifactId>keycloak-connections-mongo-update</artifactId> <artifactId>keycloak-connections-mongo-update</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-connections-truststore</artifactId>
<version>${project.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.keycloak</groupId> <groupId>org.keycloak</groupId>
<artifactId>keycloak-common</artifactId> <artifactId>keycloak-common</artifactId>

View file

@ -0,0 +1,234 @@
package org.keycloak.authentication.authenticators.browser;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import javax.ws.rs.core.MultivaluedMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
import static org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticator.OtpDecision.*;
import static org.keycloak.models.utils.KeycloakModelUtils.getRoleFromString;
import static org.keycloak.models.utils.KeycloakModelUtils.hasRole;
/**
* An {@link OTPFormAuthenticator} that can conditionally require OTP authentication.
* <p>
* <p>
* The decision for whether or not to require OTP authentication can be made based on multiple conditions
* which are evaluated in the following order. The first matching condition determines the outcome.
* </p>
* <ol>
* <li>User Attribute</li>
* <li>Role</li>
* <li>Request Header</li>
* <li>Configured Default</li>
* </ol>
* <p>
* If no condition matches, the {@link ConditionalOtpFormAuthenticator} fallback is to require OTP authentication.
* </p>
* <p>
* <h2>User Attribute</h2>
* A User Attribute like <code>otp_auth</code> can be used to control OTP authentication on individual user level.
* The supported values are <i>skip</i> and <i>force</i>. If the value is set to <i>skip</i> then the OTP auth is skipped for the user,
* otherwise if the value is <i>force</i> then the OTP auth is enforced. The setting is ignored for any other value.
* </p>
* <p>
* <h2>Role</h2>
* A role can be used to control the OTP authentication. If the user has the specified role the OTP authentication is forced.
* Otherwise if no role is selected the setting is ignored.
* <p>
* </p>
* <p>
* <h2>Request Header</h2>
* <p>
* Request Headers are matched via regex {@link Pattern}s and can be specified as a whitelist and blacklist.
* <i>No OTP for Header</i> specifies the pattern for which OTP authentication <b>is not</b> required.
* This can be used to specify trusted networks, e.g. via: <code>X-Forwarded-Host: (1.2.3.4|1.2.3.5)</code> where
* The IPs 1.2.3.4, 1.2.3.5 denote trusted machines.
* <i>Force OTP for Header</i> specifies the pattern for which OTP authentication <b>is</b> required. Whitelist entries take
* precedence before blacklist entries.
* </p>
* <p>
* <h2>Configured Default</h2>
* A default fall-though behaviour can be specified to handle cases where all previous conditions did not lead to a conclusion.
* An OTP authentication is required in case no default is configured.
* </p>
*
* @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
*/
public class ConditionalOtpFormAuthenticator extends OTPFormAuthenticator {
public static final String SKIP = "skip";
public static final String FORCE = "force";
public static final String OTP_CONTROL_USER_ATTRIBUTE = "otpControlAttribute";
public static final String FORCE_OTP_ROLE = "forceOtpRole";
public static final String NO_OTP_REQUIRED_FOR_HTTP_HEADER = "noOtpRequiredForHeaderPattern";
public static final String FORCE_OTP_FOR_HTTP_HEADER = "forceOtpForHeaderPattern";
public static final String DEFAULT_OTP_OUTCOME = "defaultOtpOutcome";
enum OtpDecision {
SKIP_OTP, SHOW_OTP, ABSTAIN
}
@Override
public void authenticate(AuthenticationFlowContext context) {
Map<String, String> config = context.getAuthenticatorConfig().getConfig();
if (tryConcludeBasedOn(voteForUserOtpControlAttribute(context, config), context)) {
return;
}
if (tryConcludeBasedOn(voteForUserForceOtpRole(context, config), context)) {
return;
}
if (tryConcludeBasedOn(voteForHttpHeaderMatchesPattern(context, config), context)) {
return;
}
if (tryConcludeBasedOn(voteForDefaultFallback(context, config), context)) {
return;
}
showOtpForm(context);
}
private OtpDecision voteForDefaultFallback(AuthenticationFlowContext context, Map<String, String> config) {
if (!config.containsKey(DEFAULT_OTP_OUTCOME)) {
return ABSTAIN;
}
switch (config.get(DEFAULT_OTP_OUTCOME)) {
case SKIP:
return SKIP_OTP;
case FORCE:
return SHOW_OTP;
default:
return ABSTAIN;
}
}
private boolean tryConcludeBasedOn(OtpDecision state, AuthenticationFlowContext context) {
switch (state) {
case SHOW_OTP:
showOtpForm(context);
return true;
case SKIP_OTP:
context.success();
return true;
default:
return false;
}
}
private void showOtpForm(AuthenticationFlowContext context) {
super.authenticate(context);
}
private OtpDecision voteForUserOtpControlAttribute(AuthenticationFlowContext context, Map<String, String> config) {
if (!config.containsKey(OTP_CONTROL_USER_ATTRIBUTE)) {
return ABSTAIN;
}
String attributeName = config.get(OTP_CONTROL_USER_ATTRIBUTE);
if (attributeName == null) {
return ABSTAIN;
}
List<String> values = context.getUser().getAttribute(attributeName);
if (values.isEmpty()) {
return ABSTAIN;
}
String value = values.get(0).trim();
switch (value) {
case SKIP:
return SKIP_OTP;
case FORCE:
return SHOW_OTP;
default:
return ABSTAIN;
}
}
private OtpDecision voteForHttpHeaderMatchesPattern(AuthenticationFlowContext context, Map<String, String> config) {
if (!config.containsKey(FORCE_OTP_FOR_HTTP_HEADER) && !config.containsKey(NO_OTP_REQUIRED_FOR_HTTP_HEADER)) {
return ABSTAIN;
}
MultivaluedMap<String, String> requestHeaders = context.getHttpRequest().getHttpHeaders().getRequestHeaders();
//Inverted to allow white-lists, e.g. for specifying trusted remote hosts: X-Forwarded-Host: (1.2.3.4|1.2.3.5)
if (containsMatchingRequestHeader(requestHeaders, config.get(NO_OTP_REQUIRED_FOR_HTTP_HEADER))) {
return SKIP_OTP;
}
if (containsMatchingRequestHeader(requestHeaders, config.get(FORCE_OTP_FOR_HTTP_HEADER))) {
return SHOW_OTP;
}
return ABSTAIN;
}
private boolean containsMatchingRequestHeader(MultivaluedMap<String, String> requestHeaders, String headerPattern) {
if (headerPattern == null) {
return false;
}
//TODO cache RequestHeader Patterns
//TODO how to deal with pattern syntax exceptions?
Pattern pattern = Pattern.compile(headerPattern, Pattern.DOTALL);
for (Map.Entry<String, List<String>> entry : requestHeaders.entrySet()) {
String key = entry.getKey();
for (String value : entry.getValue()) {
String headerEntry = key.trim() + ": " + value.trim();
if (pattern.matcher(headerEntry).matches()) {
return true;
}
}
}
return false;
}
private OtpDecision voteForUserForceOtpRole(AuthenticationFlowContext context, Map<String, String> config) {
if (!config.containsKey(FORCE_OTP_ROLE)) {
return ABSTAIN;
}
RoleModel forceOtpRole = getRoleFromString(context.getRealm(), config.get(FORCE_OTP_ROLE));
UserModel user = context.getUser();
if (hasRole(user.getRoleMappings(), forceOtpRole)) {
return SHOW_OTP;
}
return ABSTAIN;
}
}

View file

@ -0,0 +1,132 @@
package org.keycloak.authentication.authenticators.browser;
import org.keycloak.Config;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.provider.ProviderConfigProperty;
import java.util.List;
import static java.util.Arrays.asList;
import static org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticator.*;
import static org.keycloak.provider.ProviderConfigProperty.*;
/**
* An {@link AuthenticatorFactory} for {@link ConditionalOtpFormAuthenticator}s.
*
* @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
*/
public class ConditionalOtpFormAuthenticatorFactory implements AuthenticatorFactory {
public static final String PROVIDER_ID = "auth-conditional-otp-form";
public static final ConditionalOtpFormAuthenticator SINGLETON = new ConditionalOtpFormAuthenticator();
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.REQUIRED,
AuthenticationExecutionModel.Requirement.OPTIONAL,
AuthenticationExecutionModel.Requirement.DISABLED};
@Override
public Authenticator create(KeycloakSession session) {
return SINGLETON;
}
@Override
public void init(Config.Scope config) {
//NOOP
}
@Override
public void postInit(KeycloakSessionFactory factory) {
//NOOP
}
@Override
public void close() {
//NOOP
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public String getReferenceCategory() {
return UserCredentialModel.TOTP;
}
@Override
public boolean isConfigurable() {
return true;
}
@Override
public boolean isUserSetupAllowed() {
return true;
}
@Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
return REQUIREMENT_CHOICES;
}
@Override
public String getDisplayType() {
return "Conditional OTP Form";
}
@Override
public String getHelpText() {
return "Validates a OTP on a separate OTP form. Only shown if required based on the configured conditions.";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
ProviderConfigProperty forceOtpUserAttribute = new ProviderConfigProperty();
forceOtpUserAttribute.setType(STRING_TYPE);
forceOtpUserAttribute.setName(OTP_CONTROL_USER_ATTRIBUTE);
forceOtpUserAttribute.setLabel("OTP control User Attribute");
forceOtpUserAttribute.setHelpText("The name of the user attribute to explicitly control OTP auth. " +
"If attribute value is 'force' then OTP is always required. " +
"If value is 'skip' the OTP auth is skipped. Otherwise this check is ignored.");
ProviderConfigProperty forceOtpRole = new ProviderConfigProperty();
forceOtpRole.setType(ROLE_TYPE);
forceOtpRole.setName(FORCE_OTP_ROLE);
forceOtpRole.setLabel("Force OTP for Role");
forceOtpRole.setHelpText("OTP is always required if user has the given Role.");
ProviderConfigProperty noOtpRequiredForHttpHeader = new ProviderConfigProperty();
noOtpRequiredForHttpHeader.setType(STRING_TYPE);
noOtpRequiredForHttpHeader.setName(NO_OTP_REQUIRED_FOR_HTTP_HEADER);
noOtpRequiredForHttpHeader.setLabel("No OTP for Header");
noOtpRequiredForHttpHeader.setHelpText("OTP required if a HTTP request header does not match the given pattern." +
"Can be used to specify trusted networks via: X-Forwarded-Host: (1.2.3.4|1.2.3.5)." +
"In this case requests from 1.2.3.4 and 1.2.3.5 come from a trusted source.");
noOtpRequiredForHttpHeader.setDefaultValue("");
ProviderConfigProperty forceOtpForHttpHeader = new ProviderConfigProperty();
forceOtpForHttpHeader.setType(STRING_TYPE);
forceOtpForHttpHeader.setName(FORCE_OTP_FOR_HTTP_HEADER);
forceOtpForHttpHeader.setLabel("Force OTP for Header");
forceOtpForHttpHeader.setHelpText("OTP required if a HTTP request header matches the given pattern.");
forceOtpForHttpHeader.setDefaultValue("");
ProviderConfigProperty defaultOutcome = new ProviderConfigProperty();
defaultOutcome.setType(LIST_TYPE);
defaultOutcome.setName(DEFAULT_OTP_OUTCOME);
defaultOutcome.setLabel("Fallback OTP handling");
defaultOutcome.setDefaultValue(asList(SKIP, FORCE));
defaultOutcome.setHelpText("What to do in case of every check abstains. Defaults to force OTP authentication.");
return asList(forceOtpUserAttribute, forceOtpRole, noOtpRequiredForHttpHeader, forceOtpForHttpHeader, defaultOutcome);
}
}

View file

@ -1,9 +1,14 @@
package org.keycloak.authentication.requiredactions; package org.keycloak.authentication.requiredactions;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import org.keycloak.Config; import org.keycloak.Config;
import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionContext;
import org.keycloak.authentication.RequiredActionFactory; import org.keycloak.authentication.RequiredActionFactory;
import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.common.util.Time;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakSessionFactory;
@ -14,8 +19,8 @@ import javax.ws.rs.core.Response;
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class TermsAndConditions implements RequiredActionProvider, RequiredActionFactory { public class TermsAndConditions implements RequiredActionProvider, RequiredActionFactory {
public static final String PROVIDER_ID = "terms_and_conditions"; public static final String PROVIDER_ID = "terms_and_conditions";
public static final String USER_ATTRIBUTE = PROVIDER_ID;
@Override @Override
public RequiredActionProvider create(KeycloakSession session) { public RequiredActionProvider create(KeycloakSession session) {
@ -46,18 +51,21 @@ public class TermsAndConditions implements RequiredActionProvider, RequiredActio
@Override @Override
public void requiredActionChallenge(RequiredActionContext context) { public void requiredActionChallenge(RequiredActionContext context) {
Response challenge = context.form().createForm("terms.ftl"); Response challenge = context.form().createForm("terms.ftl");
context.challenge(challenge); context.challenge(challenge);
} }
@Override @Override
public void processAction(RequiredActionContext context) { public void processAction(RequiredActionContext context) {
if (context.getHttpRequest().getDecodedFormParameters().containsKey("cancel")) { if (context.getHttpRequest().getDecodedFormParameters().containsKey("cancel")) {
context.getUser().removeAttribute(USER_ATTRIBUTE);
context.failure(); context.failure();
return; return;
} }
context.success();
context.getUser().setAttribute(USER_ATTRIBUTE, Arrays.asList(Integer.toString(Time.currentTime())));
context.success();
} }
@Override @Override

View file

@ -1,6 +1,9 @@
package org.keycloak.email; package org.keycloak.email;
import org.jboss.logging.Logger; 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.RealmModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
@ -12,6 +15,8 @@ import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart; import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage; import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart; import javax.mail.internet.MimeMultipart;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.Date; import java.util.Date;
import java.util.Map; import java.util.Map;
import java.util.Properties; import java.util.Properties;
@ -23,6 +28,12 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider {
private static final Logger log = Logger.getLogger(DefaultEmailSenderProvider.class); private static final Logger log = Logger.getLogger(DefaultEmailSenderProvider.class);
private final KeycloakSession session;
public DefaultEmailSenderProvider(KeycloakSession session) {
this.session = session;
}
@Override @Override
public void send(RealmModel realm, UserModel user, String subject, String textBody, String htmlBody) throws EmailException { public void send(RealmModel realm, UserModel user, String subject, String textBody, String htmlBody) throws EmailException {
try { try {
@ -52,6 +63,10 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider {
props.setProperty("mail.smtp.starttls.enable", "true"); props.setProperty("mail.smtp.starttls.enable", "true");
} }
if (ssl || starttls) {
setupTruststore(props);
}
props.setProperty("mail.smtp.timeout", "10000"); props.setProperty("mail.smtp.timeout", "10000");
props.setProperty("mail.smtp.connectiontimeout", "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 @Override
public void close() { public void close() {
} }
} }

View file

@ -11,7 +11,7 @@ public class DefaultEmailSenderProviderFactory implements EmailSenderProviderFac
@Override @Override
public EmailSenderProvider create(KeycloakSession session) { public EmailSenderProvider create(KeycloakSession session) {
return new DefaultEmailSenderProvider(); return new DefaultEmailSenderProvider(session);
} }
@Override @Override

View file

@ -3,6 +3,7 @@ package org.keycloak.exportimport;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import java.io.IOException; import java.io.IOException;
@ -13,7 +14,7 @@ public class ExportImportManager {
private static final Logger logger = Logger.getLogger(ExportImportManager.class); private static final Logger logger = Logger.getLogger(ExportImportManager.class);
private KeycloakSession session; private KeycloakSessionFactory sessionFactory;
private final String realmName; private final String realmName;
@ -21,7 +22,7 @@ public class ExportImportManager {
private ImportProvider importProvider; private ImportProvider importProvider;
public ExportImportManager(KeycloakSession session) { public ExportImportManager(KeycloakSession session) {
this.session = session; this.sessionFactory = session.getKeycloakSessionFactory();
realmName = ExportImportConfig.getRealmName(); realmName = ExportImportConfig.getRealmName();
@ -65,10 +66,10 @@ public class ExportImportManager {
Strategy strategy = ExportImportConfig.getStrategy(); Strategy strategy = ExportImportConfig.getStrategy();
if (realmName == null) { if (realmName == null) {
logger.infof("Full model import requested. Strategy: %s", strategy.toString()); logger.infof("Full model import requested. Strategy: %s", strategy.toString());
importProvider.importModel(session.getKeycloakSessionFactory(), strategy); importProvider.importModel(sessionFactory, strategy);
} else { } else {
logger.infof("Import of realm '%s' requested. Strategy: %s", realmName, strategy.toString()); logger.infof("Import of realm '%s' requested. Strategy: %s", realmName, strategy.toString());
importProvider.importRealm(session.getKeycloakSessionFactory(), realmName, strategy); importProvider.importRealm(sessionFactory, realmName, strategy);
} }
logger.info("Import finished successfully"); logger.info("Import finished successfully");
} catch (IOException e) { } catch (IOException e) {
@ -80,10 +81,10 @@ public class ExportImportManager {
try { try {
if (realmName == null) { if (realmName == null) {
logger.info("Full model export requested"); logger.info("Full model export requested");
exportProvider.exportModel(session.getKeycloakSessionFactory()); exportProvider.exportModel(sessionFactory);
} else { } else {
logger.infof("Export of realm '%s' requested", realmName); logger.infof("Export of realm '%s' requested", realmName);
exportProvider.exportRealm(session.getKeycloakSessionFactory(), realmName); exportProvider.exportRealm(sessionFactory, realmName);
} }
logger.info("Export finished successfully"); logger.info("Export finished successfully");
} catch (IOException e) { } catch (IOException e) {

View file

@ -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;
}
}

View file

@ -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
}

View file

@ -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();
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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();
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}
}

View file

@ -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();
}
}
}

View file

@ -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));
}
}
}
}

View file

@ -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());
}
}

View file

@ -63,6 +63,8 @@ public class RedirectUtils {
logger.debug("No Redirect URIs supplied"); logger.debug("No Redirect URIs supplied");
redirectUri = null; redirectUri = null;
} else { } else {
redirectUri = lowerCaseHostname(redirectUri);
String r = redirectUri.indexOf('?') != -1 ? redirectUri.substring(0, redirectUri.indexOf('?')) : redirectUri; String r = redirectUri.indexOf('?') != -1 ? redirectUri.substring(0, redirectUri.indexOf('?')) : redirectUri;
Set<String> resolveValidRedirects = resolveValidRedirects(uriInfo, rootUrl, validRedirects); 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) { private static String relativeToAbsoluteURI(UriInfo uriInfo, String rootUrl, String relative) {
if (rootUrl == null) { if (rootUrl == null) {
URI baseUri = uriInfo.getBaseUri(); URI baseUri = uriInfo.getBaseUri();

View file

@ -39,42 +39,34 @@ public class ApplianceBootstrap {
throw new IllegalStateException("Can't create default realm as realms already exists"); throw new IllegalStateException("Can't create default realm as realms already exists");
} }
KeycloakSession session = this.session.getKeycloakSessionFactory().create(); String adminRealmName = Config.getAdminRealm();
try { logger.info("Initializing " + adminRealmName + " realm");
session.getTransaction().begin();
String adminRealmName = Config.getAdminRealm();
logger.info("Initializing " + adminRealmName + " realm");
RealmManager manager = new RealmManager(session); RealmManager manager = new RealmManager(session);
manager.setContextPath(contextPath); manager.setContextPath(contextPath);
RealmModel realm = manager.createRealm(adminRealmName, adminRealmName); RealmModel realm = manager.createRealm(adminRealmName, adminRealmName);
realm.setName(adminRealmName); realm.setName(adminRealmName);
realm.setDisplayName(Version.NAME); realm.setDisplayName(Version.NAME);
realm.setDisplayNameHtml(Version.NAME_HTML); realm.setDisplayNameHtml(Version.NAME_HTML);
realm.setEnabled(true); realm.setEnabled(true);
realm.addRequiredCredential(CredentialRepresentation.PASSWORD); realm.addRequiredCredential(CredentialRepresentation.PASSWORD);
realm.setSsoSessionIdleTimeout(1800); realm.setSsoSessionIdleTimeout(1800);
realm.setAccessTokenLifespan(60); realm.setAccessTokenLifespan(60);
realm.setAccessTokenLifespanForImplicitFlow(Constants.DEFAULT_ACCESS_TOKEN_LIFESPAN_FOR_IMPLICIT_FLOW_TIMEOUT); realm.setAccessTokenLifespanForImplicitFlow(Constants.DEFAULT_ACCESS_TOKEN_LIFESPAN_FOR_IMPLICIT_FLOW_TIMEOUT);
realm.setSsoSessionMaxLifespan(36000); realm.setSsoSessionMaxLifespan(36000);
realm.setOfflineSessionIdleTimeout(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT); realm.setOfflineSessionIdleTimeout(Constants.DEFAULT_OFFLINE_SESSION_IDLE_TIMEOUT);
realm.setAccessCodeLifespan(60); realm.setAccessCodeLifespan(60);
realm.setAccessCodeLifespanUserAction(300); realm.setAccessCodeLifespanUserAction(300);
realm.setAccessCodeLifespanLogin(1800); realm.setAccessCodeLifespanLogin(1800);
realm.setSslRequired(SslRequired.EXTERNAL); realm.setSslRequired(SslRequired.EXTERNAL);
realm.setRegistrationAllowed(false); realm.setRegistrationAllowed(false);
realm.setRegistrationEmailAsUsername(false); realm.setRegistrationEmailAsUsername(false);
KeycloakModelUtils.generateRealmKeys(realm); KeycloakModelUtils.generateRealmKeys(realm);
session.getTransaction().commit();
} finally {
session.close();
}
return true; return true;
} }
public void createMasterRealmUser(KeycloakSession session, String username, String password) { public void createMasterRealmUser(String username, String password) {
RealmModel realm = session.realms().getRealm(Config.getAdminRealm()); RealmModel realm = session.realms().getRealm(Config.getAdminRealm());
if (session.users().getUsersCount(realm) > 0) { if (session.users().getUsersCount(realm) > 0) {
throw new IllegalStateException("Can't create initial user as users already exists"); throw new IllegalStateException("Can't create initial user as users already exists");

View file

@ -83,11 +83,12 @@ public class KeycloakApplication extends Application {
boolean bootstrapAdminUser = false; boolean bootstrapAdminUser = false;
KeycloakSession session = sessionFactory.create(); KeycloakSession session = sessionFactory.create();
ExportImportManager exportImportManager;
try { try {
session.getTransaction().begin(); session.getTransaction().begin();
ApplianceBootstrap applianceBootstrap = new ApplianceBootstrap(session); ApplianceBootstrap applianceBootstrap = new ApplianceBootstrap(session);
ExportImportManager exportImportManager = new ExportImportManager(session); exportImportManager = new ExportImportManager(session);
boolean createMasterRealm = applianceBootstrap.isNewInstall(); boolean createMasterRealm = applianceBootstrap.isNewInstall();
if (exportImportManager.isRunImport() && exportImportManager.isImportMasterIncluded()) { if (exportImportManager.isRunImport() && exportImportManager.isImportMasterIncluded()) {
@ -97,20 +98,27 @@ public class KeycloakApplication extends Application {
if (createMasterRealm) { if (createMasterRealm) {
applianceBootstrap.createMasterRealm(contextPath); applianceBootstrap.createMasterRealm(contextPath);
} }
session.getTransaction().commit();
} finally {
session.close();
}
if (exportImportManager.isRunImport()) { if (exportImportManager.isRunImport()) {
exportImportManager.runImport(); exportImportManager.runImport();
} else { } else {
importRealms(); importRealms();
} }
importAddUser(); importAddUser();
if (exportImportManager.isRunExport()) { if (exportImportManager.isRunExport()) {
exportImportManager.runExport(); exportImportManager.runExport();
} }
bootstrapAdminUser = applianceBootstrap.isNoMasterUser(); session = sessionFactory.create();
try {
session.getTransaction().begin();
bootstrapAdminUser = new ApplianceBootstrap(session).isNoMasterUser();
session.getTransaction().commit(); session.getTransaction().commit();
} finally { } finally {
@ -192,10 +200,15 @@ public class KeycloakApplication extends Application {
public static void setupScheduledTasks(final KeycloakSessionFactory sessionFactory) { public static void setupScheduledTasks(final KeycloakSessionFactory sessionFactory) {
long interval = Config.scope("scheduled").getLong("interval", 60L) * 1000; long interval = Config.scope("scheduled").getLong("interval", 60L) * 1000;
TimerProvider timer = sessionFactory.create().getProvider(TimerProvider.class); KeycloakSession session = sessionFactory.create();
timer.schedule(new ScheduledTaskRunner(sessionFactory, new ClearExpiredEvents()), interval, "ClearExpiredEvents"); try {
timer.schedule(new ScheduledTaskRunner(sessionFactory, new ClearExpiredUserSessions()), interval, "ClearExpiredUserSessions"); TimerProvider timer = session.getProvider(TimerProvider.class);
new UsersSyncManager().bootstrapPeriodic(sessionFactory, timer); timer.schedule(new ScheduledTaskRunner(sessionFactory, new ClearExpiredEvents()), interval, "ClearExpiredEvents");
timer.schedule(new ScheduledTaskRunner(sessionFactory, new ClearExpiredUserSessions()), interval, "ClearExpiredUserSessions");
new UsersSyncManager().bootstrapPeriodic(sessionFactory, timer);
} finally {
session.close();
}
} }
public KeycloakSessionFactory getSessionFactory() { public KeycloakSessionFactory getSessionFactory() {

View file

@ -92,7 +92,7 @@ public class WelcomeResource {
ApplianceBootstrap applianceBootstrap = new ApplianceBootstrap(session); ApplianceBootstrap applianceBootstrap = new ApplianceBootstrap(session);
if (applianceBootstrap.isNoMasterUser()) { if (applianceBootstrap.isNoMasterUser()) {
bootstrap = false; bootstrap = false;
applianceBootstrap.createMasterRealmUser(session, username, password); applianceBootstrap.createMasterRealmUser(username, password);
logger.infov("Created initial admin user with username {0}", username); logger.infov("Created initial admin user with username {0}", username);
return createWelcomePage("User created", null); return createWelcomePage("User created", null);

View file

@ -123,6 +123,18 @@ public class AdminEventBuilder {
return this; 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) { public AdminEventBuilder resourcePath(UriInfo uriInfo) {
String path = getResourcePath(uriInfo); String path = getResourcePath(uriInfo);
adminEvent.setResourcePath(path); adminEvent.setResourcePath(path);

View file

@ -107,11 +107,7 @@ public class ClientResource {
auth.requireManage(); auth.requireManage();
try { try {
if (TRUE.equals(rep.isServiceAccountsEnabled()) && !client.isServiceAccountsEnabled()) { updateClientFromRep(rep, client, session);
new ClientManager(new RealmManager(session)).enableServiceAccount(client);;
}
RepresentationToModel.updateClient(rep, client);
adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(rep).success(); adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(rep).success();
return Response.noContent().build(); return Response.noContent().build();
} catch (ModelDuplicateException e) { } 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 * Get representation of the client

View file

@ -75,6 +75,7 @@ public class ClientTemplatesResource {
client.setId(clientModel.getId()); client.setId(clientModel.getId());
client.setName(clientModel.getName()); client.setName(clientModel.getName());
client.setDescription(clientModel.getDescription()); client.setDescription(clientModel.getDescription());
client.setProtocol(clientModel.getProtocol());
rep.add(client); rep.add(client);
} }
} }

View file

@ -113,19 +113,7 @@ public class IdentityProviderResource {
try { try {
this.auth.requireManage(); this.auth.requireManage();
String internalId = providerRep.getInternalId(); updateIdpFromRep(providerRep, realm, session);
String newProviderId = providerRep.getAlias();
String oldProviderId = getProviderIdByInternalId(this.realm, internalId);
this.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(this.session.users().getUsers(this.realm, false), oldProviderId, newProviderId);
}
adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(providerRep).success(); adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(providerRep).success();
@ -135,8 +123,24 @@ public class IdentityProviderResource {
} }
} }
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 // 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(); List<IdentityProviderModel> providerModels = realm.getIdentityProviders();
for (IdentityProviderModel providerModel : providerModels) { for (IdentityProviderModel providerModel : providerModels) {
if (providerModel.getInternalId().equals(providerInternalId)) { if (providerModel.getInternalId().equals(providerInternalId)) {
@ -147,17 +151,17 @@ public class IdentityProviderResource {
return null; 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) { 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) { if (federatedIdentity != null) {
// Remove old link first // Remove old link first
this.session.users().removeFederatedIdentity(this.realm, user, oldProviderId); session.users().removeFederatedIdentity(realm, user, oldProviderId);
// And create new // And create new
FederatedIdentityModel newFederatedIdentity = new FederatedIdentityModel(newProviderId, federatedIdentity.getUserId(), federatedIdentity.getUserName(), FederatedIdentityModel newFederatedIdentity = new FederatedIdentityModel(newProviderId, federatedIdentity.getUserId(), federatedIdentity.getUserName(),
federatedIdentity.getToken()); federatedIdentity.getToken());
this.session.users().addFederatedIdentity(this.realm, user, newFederatedIdentity); session.users().addFederatedIdentity(realm, user, newFederatedIdentity);
} }
} }
} }

View file

@ -66,6 +66,8 @@ import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.regex.PatternSyntaxException; 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 * Base resource class for the admin REST api of one realm
@ -709,5 +711,18 @@ public class RealmAdminResource {
return ModelToRepresentation.toGroupHierarchy(found, true); 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();
}
} }

View file

@ -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(); adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(rep).success();
if (session.getTransaction().isActive()) { if (session.getTransaction().isActive()) {
@ -189,7 +189,7 @@ public class UsersResource {
try { try {
UserModel user = session.users().addUser(realm, rep.getUsername()); UserModel user = session.users().addUser(realm, rep.getUsername());
Set<String> emptySet = Collections.emptySet(); 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(); 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()) { if (realm.isEditUsernameAllowed()) {
user.setUsername(rep.getUsername()); user.setUsername(rep.getUsername());
} }

View file

@ -14,3 +14,4 @@ org.keycloak.authentication.authenticators.broker.IdpCreateUserIfUniqueAuthentic
org.keycloak.authentication.authenticators.broker.IdpConfirmLinkAuthenticatorFactory org.keycloak.authentication.authenticators.broker.IdpConfirmLinkAuthenticatorFactory
org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticatorFactory org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticatorFactory
org.keycloak.authentication.authenticators.broker.IdpUsernamePasswordFormFactory org.keycloak.authentication.authenticators.broker.IdpUsernamePasswordFormFactory
org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticatorFactory

View file

@ -16,8 +16,8 @@ import org.w3c.dom.Document;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.util.List; import java.util.List;
import static com.mongodb.util.MyAsserts.assertFalse;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.keycloak.testsuite.auth.page.AuthRealm.SAMLSERVLETDEMO; import static org.keycloak.testsuite.auth.page.AuthRealm.SAMLSERVLETDEMO;
import static org.keycloak.testsuite.util.IOUtil.*; import static org.keycloak.testsuite.util.IOUtil.*;

Some files were not shown because too many files have changed in this diff Show more