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

This commit is contained in:
Vlasta Ramik 2016-01-11 09:43:52 +01:00
commit 554da73398
88 changed files with 3043 additions and 376 deletions

View file

@ -7,7 +7,7 @@ Keycloak is an SSO Service for web apps and REST services. For more information
Building
--------
Ensure you have JDK 7 (or newer), Maven 3.2.1 (or newer) and Git installed
Ensure you have JDK 8 (or newer), Maven 3.2.1 (or newer) and Git installed
java -version
mvn -version

View file

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

View file

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

View file

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

View file

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

View file

@ -10,6 +10,7 @@ import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.CloseableHttpClient;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.connections.truststore.TruststoreProvider;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.common.util.EnvUtil;
@ -28,9 +29,12 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
private static final Logger logger = Logger.getLogger(DefaultHttpClientFactory.class);
private volatile CloseableHttpClient httpClient;
private Config.Scope config;
@Override
public HttpClientProvider create(KeycloakSession session) {
lazyInit(session);
return new HttpClientProvider() {
@Override
public HttpClient getHttpClient() {
@ -74,7 +78,9 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
@Override
public void close() {
try {
httpClient.close();
if (httpClient != null) {
httpClient.close();
}
} catch (IOException e) {
}
@ -87,46 +93,62 @@ public class DefaultHttpClientFactory implements HttpClientFactory {
@Override
public void init(Config.Scope config) {
long socketTimeout = config.getLong("socket-timeout-millis", -1L);
long establishConnectionTimeout = config.getLong("establish-connection-timeout-millis", -1L);
int maxPooledPerRoute = config.getInt("max-pooled-per-route", 0);
int connectionPoolSize = config.getInt("connection-pool-size", 200);
boolean disableTrustManager = config.getBoolean("disable-trust-manager", false);
boolean disableCookies = config.getBoolean("disable-cookies", true);
String hostnameVerificationPolicy = config.get("hostname-verification-policy", "WILDCARD");
HttpClientBuilder.HostnameVerificationPolicy hostnamePolicy = HttpClientBuilder.HostnameVerificationPolicy.valueOf(hostnameVerificationPolicy);
String truststore = config.get("truststore");
String truststorePassword = config.get("truststore-password");
String clientKeystore = config.get("client-keystore");
String clientKeystorePassword = config.get("client-keystore-password");
String clientPrivateKeyPassword = config.get("client-key-password");
this.config = config;
}
HttpClientBuilder builder = new HttpClientBuilder();
builder.socketTimeout(socketTimeout, TimeUnit.MILLISECONDS)
.establishConnectionTimeout(establishConnectionTimeout, TimeUnit.MILLISECONDS)
.maxPooledPerRoute(maxPooledPerRoute)
.connectionPoolSize(connectionPoolSize)
.hostnameVerification(hostnamePolicy)
.disableCookies(disableCookies);
if (disableTrustManager) builder.disableTrustManager();
if (truststore != null) {
truststore = EnvUtil.replace(truststore);
try {
builder.trustStore(KeystoreUtil.loadKeyStore(truststore, truststorePassword));
} catch (Exception e) {
throw new RuntimeException("Failed to load truststore", e);
private void lazyInit(KeycloakSession session) {
if (httpClient == null) {
synchronized(this) {
if (httpClient == null) {
long socketTimeout = config.getLong("socket-timeout-millis", -1L);
long establishConnectionTimeout = config.getLong("establish-connection-timeout-millis", -1L);
int maxPooledPerRoute = config.getInt("max-pooled-per-route", 0);
int connectionPoolSize = config.getInt("connection-pool-size", 200);
boolean disableCookies = config.getBoolean("disable-cookies", true);
String clientKeystore = config.get("client-keystore");
String clientKeystorePassword = config.get("client-keystore-password");
String clientPrivateKeyPassword = config.get("client-key-password");
TruststoreProvider truststoreProvider = session.getProvider(TruststoreProvider.class);
boolean disableTrustManager = truststoreProvider == null || truststoreProvider.getTruststore() == null;
if (disableTrustManager) {
logger.warn("Truststore is disabled");
}
HttpClientBuilder.HostnameVerificationPolicy hostnamePolicy = disableTrustManager ? null
: HttpClientBuilder.HostnameVerificationPolicy.valueOf(truststoreProvider.getPolicy().name());
HttpClientBuilder builder = new HttpClientBuilder();
builder.socketTimeout(socketTimeout, TimeUnit.MILLISECONDS)
.establishConnectionTimeout(establishConnectionTimeout, TimeUnit.MILLISECONDS)
.maxPooledPerRoute(maxPooledPerRoute)
.connectionPoolSize(connectionPoolSize)
.disableCookies(disableCookies);
if (disableTrustManager) {
// TODO: is it ok to do away with disabling trust manager?
//builder.disableTrustManager();
} else {
builder.hostnameVerification(hostnamePolicy);
try {
builder.trustStore(truststoreProvider.getTruststore());
} catch (Exception e) {
throw new RuntimeException("Failed to load truststore", e);
}
}
if (clientKeystore != null) {
clientKeystore = EnvUtil.replace(clientKeystore);
try {
KeyStore clientCertKeystore = KeystoreUtil.loadKeyStore(clientKeystore, clientKeystorePassword);
builder.keyStore(clientCertKeystore, clientPrivateKeyPassword);
} catch (Exception e) {
throw new RuntimeException("Failed to load keystore", e);
}
}
httpClient = builder.build();
}
}
}
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

View file

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

View file

@ -19,6 +19,7 @@
<module>mongo</module>
<module>mongo-update</module>
<module>http-client</module>
<module>truststore</module>
</modules>
<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>
<artifactId>keycloak-export-import-single-file</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-connections-truststore</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-connections-http-client</artifactId>

View file

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

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-mongo" services="import"/>
<module name="org.keycloak.keycloak-connections-mongo-update" services="import"/>
<module name="org.keycloak.keycloak-connections-truststore" services="import"/>
<module name="org.keycloak.keycloak-common" services="import"/>
<module name="org.keycloak.keycloak-core" services="import"/>
<module name="org.keycloak.keycloak-email-api" services="import"/>

View file

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

View file

@ -1,114 +1,134 @@
<chapter id="export-import">
<title>Export and Import</title>
<para>
Export/import is useful especially if you want to migrate your whole Keycloak database from one environment to another or migrate to different database (For example from MySQL to Oracle).
You can trigger export/import at startup of Keycloak server and it's configurable with System properties right now. The fact it's done at server startup means that no-one can access Keycloak UI or REST endpoints
and edit Keycloak database on the fly when export or import is in progress. Otherwise it could lead to inconsistent results.
</para>
<para>
You can export/import your database either to:
<itemizedlist>
<listitem>Directory on local filesystem</listitem>
<listitem>Single JSON file on your filesystem</listitem>
</itemizedlist>
<section>
<title>Startup export/import</title>
<para>
Export/import is useful especially if you want to migrate your whole Keycloak database from one environment to another or migrate to different database (For example from MySQL to Oracle).
You can trigger export/import at startup of Keycloak server and it's configurable with System properties right now. The fact it's done at server startup means that no-one can access Keycloak UI or REST endpoints
and edit Keycloak database on the fly when export or import is in progress. Otherwise it could lead to inconsistent results.
</para>
<para>
You can export/import your database either to:
<itemizedlist>
<listitem>Directory on local filesystem</listitem>
<listitem>Single JSON file on your filesystem</listitem>
</itemizedlist>
When importing using the "dir" strategy, note that the files need to follow the naming convention specified below.
If you are importing files which were previously exported, the files already follow this convention.
<itemizedlist>
<listitem>{REALM_NAME}-realm.json, such as "acme-roadrunner-affairs-realm.json" for the realm named "acme-roadrunner-affairs"</listitem>
<listitem>{REALM_NAME}-users-{INDEX}.json, such as "acme-roadrunner-affairs-users-0.json" for the first users file of the realm named "acme-roadrunner-affairs"</listitem>
</itemizedlist>
</para>
<para>
If you import to Directory, you can specify also the number of users to be stored in each JSON file. So if you have
very large amount of users in your database, you likely don't want to import them into single file as the file might be very big.
Processing of each file is done in separate transaction as exporting/importing all users at once could also lead to memory issues.
</para>
<para>
To export into unencrypted directory you can use:
<programlisting><![CDATA[
When importing using the "dir" strategy, note that the files need to follow the naming convention specified below.
If you are importing files which were previously exported, the files already follow this convention.
<itemizedlist>
<listitem>{REALM_NAME}-realm.json, such as "acme-roadrunner-affairs-realm.json" for the realm named "acme-roadrunner-affairs"</listitem>
<listitem>{REALM_NAME}-users-{INDEX}.json, such as "acme-roadrunner-affairs-users-0.json" for the first users file of the realm named "acme-roadrunner-affairs"</listitem>
</itemizedlist>
</para>
<para>
If you import to Directory, you can specify also the number of users to be stored in each JSON file. So if you have
very large amount of users in your database, you likely don't want to import them into single file as the file might be very big.
Processing of each file is done in separate transaction as exporting/importing all users at once could also lead to memory issues.
</para>
<para>
To export into unencrypted directory you can use:
<programlisting><![CDATA[
bin/standalone.sh -Dkeycloak.migration.action=export
-Dkeycloak.migration.provider=dir -Dkeycloak.migration.dir=<DIR TO EXPORT TO>
]]></programlisting>
And similarly for import just use <literal>-Dkeycloak.migration.action=import</literal> instead of <literal>export</literal> .
</para>
<para>
To export into single JSON file you can use:
<programlisting><![CDATA[
And similarly for import just use <literal>-Dkeycloak.migration.action=import</literal> instead of <literal>export</literal> .
</para>
<para>
To export into single JSON file you can use:
<programlisting><![CDATA[
bin/standalone.sh -Dkeycloak.migration.action=export
-Dkeycloak.migration.provider=singleFile -Dkeycloak.migration.file=<FILE TO EXPORT TO>
]]></programlisting>
</para>
<para>
Here's an example of importing:
<programlisting><![CDATA[
</para>
<para>
Here's an example of importing:
<programlisting><![CDATA[
bin/standalone.sh -Dkeycloak.migration.action=import
-Dkeycloak.migration.provider=singleFile -Dkeycloak.migration.file=<FILE TO IMPORT>
-Dkeycloak.migration.strategy=OVERWRITE_EXISTING
]]></programlisting>
</para>
<para>
Other available options are:
<variablelist>
<varlistentry>
<term>-Dkeycloak.migration.realmName</term>
<listitem>
<para>
can be used if you want to export just one specified realm instead of all.
If not specified, then all realms will be exported.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>-Dkeycloak.migration.usersExportStrategy</term>
<listitem>
<para>
can be used to specify for Directory providers to specify where to import users.
Possible values are:
<itemizedlist>
<listitem>DIFFERENT_FILES - Users will be exported into more different files according to maximum number of users per file. This is default value</listitem>
<listitem>SKIP - exporting of users will be skipped completely</listitem>
<listitem>REALM_FILE - All users will be exported to same file with realm (So file like "foo-realm.json" with both realm data and users)</listitem>
<listitem>SAME_FILE - All users will be exported to same file but different than realm (So file like "foo-realm.json" with realm data and "foo-users.json" with users)</listitem>
</itemizedlist>
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>-Dkeycloak.migration.usersPerFile</term>
<listitem>
<para>
can be used to specify number of users per file (and also per DB transaction).
It's 5000 by default. It's used only if usersExportStrategy is DIFFERENT_FILES
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>-Dkeycloak.migration.strategy</term>
<listitem>
<para>
is used during import. It can be used to specify how to proceed if realm with same name
already exists in the database where you are going to import data. Possible values are:
<itemizedlist>
<listitem>IGNORE_EXISTING - Ignore importing if realm of this name already exists</listitem>
<listitem>OVERWRITE_EXISTING - Remove existing realm and import it again with new data from JSON file.
If you want to fully migrate one environment to another and ensure that the new environment will contain same data
like the old one, you can specify this.
</listitem>
</itemizedlist>
</para>
</listitem>
</varlistentry>
</variablelist>
</para>
<para>
When importing realm files that weren't exported before, the option <literal>keycloak.import</literal> can be used. If more than one realm
file needs to be imported, a comma separated list of file names can be specified. This is more appropriate than the cases before, as this
will happen only after the master realm has been initialized. Examples:
<itemizedlist>
<listitem>-Dkeycloak.import=/tmp/realm1.json</listitem>
<listitem>-Dkeycloak.import=/tmp/realm1.json,/tmp/realm2.json</listitem>
</itemizedlist>
</para>
</para>
<para>
Other available options are:
<variablelist>
<varlistentry>
<term>-Dkeycloak.migration.realmName</term>
<listitem>
<para>
can be used if you want to export just one specified realm instead of all.
If not specified, then all realms will be exported.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>-Dkeycloak.migration.usersExportStrategy</term>
<listitem>
<para>
can be used to specify for Directory providers to specify where to import users.
Possible values are:
<itemizedlist>
<listitem>DIFFERENT_FILES - Users will be exported into more different files according to maximum number of users per file. This is default value</listitem>
<listitem>SKIP - exporting of users will be skipped completely</listitem>
<listitem>REALM_FILE - All users will be exported to same file with realm (So file like "foo-realm.json" with both realm data and users)</listitem>
<listitem>SAME_FILE - All users will be exported to same file but different than realm (So file like "foo-realm.json" with realm data and "foo-users.json" with users)</listitem>
</itemizedlist>
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>-Dkeycloak.migration.usersPerFile</term>
<listitem>
<para>
can be used to specify number of users per file (and also per DB transaction).
It's 5000 by default. It's used only if usersExportStrategy is DIFFERENT_FILES
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>-Dkeycloak.migration.strategy</term>
<listitem>
<para>
is used during import. It can be used to specify how to proceed if realm with same name
already exists in the database where you are going to import data. Possible values are:
<itemizedlist>
<listitem>IGNORE_EXISTING - Ignore importing if realm of this name already exists</listitem>
<listitem>OVERWRITE_EXISTING - Remove existing realm and import it again with new data from JSON file.
If you want to fully migrate one environment to another and ensure that the new environment will contain same data
like the old one, you can specify this.
</listitem>
</itemizedlist>
</para>
</listitem>
</varlistentry>
</variablelist>
</para>
<para>
When importing realm files that weren't exported before, the option <literal>keycloak.import</literal> can be used. If more than one realm
file needs to be imported, a comma separated list of file names can be specified. This is more appropriate than the cases before, as this
will happen only after the master realm has been initialized. Examples:
<itemizedlist>
<listitem>-Dkeycloak.import=/tmp/realm1.json</listitem>
<listitem>-Dkeycloak.import=/tmp/realm1.json,/tmp/realm2.json</listitem>
</itemizedlist>
</para>
</section>
<section>
<title>Admin console export/import</title>
<para>
Import of most resources can be performed from the admin console.
Exporting resources will be supported in future versions.
</para>
<para>
The files created during a "startup" export can be used to import from
the admin UI. This way, you can export from one realm and import to
another realm. Or, you can export from one server and import to another.
</para>
<warning>
<para>
The admin console import allows you to "overwrite" resources if you choose.
Use this feature with caution, especially on a production system.
</para>
</warning>
</section>
</chapter>

View file

@ -1052,7 +1052,7 @@
<literal>HTTP-POST Binding for AuthnReques</literal>
</entry>
<entry>
Allows you to specify wheter SAML authentication requests must be sent using the HTTP-POST or HTTP-Redirect protocol bindings. If enabled, it will send requests using HTTP-POST binding.
Allows you to specify whether SAML authentication requests must be sent using the HTTP-POST or HTTP-Redirect protocol bindings. If enabled, it will send requests using HTTP-POST binding.
</entry>
</row>
</tbody>
@ -1066,6 +1066,16 @@
Once you create a SAML provider, there is an <literal>EXPORT</literal> button that appears when viewing that provider.
Clicking this button will export a SAML entity descriptor which you can use to
</para>
<section>
<title>SP Descriptor</title>
<para>The SAML SP Descriptor XML file for the broker is available publically by going to this URL</para>
<programlisting>
http[s]://{host:port}/auth/realms/{realm-name}/broker/{broker-alias}/endpoint/descriptor
</programlisting>
<para>
This URL is useful if you need to import this information into an IDP that needs or is more user friendly to load from a remote URL.
</para>
</section>
</section>
<section>

View file

@ -4,7 +4,8 @@
<section>
<title>Installation</title>
<para>
Keycloak Server has three downloadable distributions.
Keycloak Server has three downloadable distributions. To run the Keycloak server you need to have Java 8 already
installed.
</para>
<para>
<itemizedlist>
@ -381,9 +382,7 @@ bin/add-user.[sh|bat] -r master -u <username> -p <password>
By default the setting is like this:
<programlisting><![CDATA[
"connectionsHttpClient": {
"default": {
"disable-trust-manager": true
}
"default": {}
},
]]></programlisting>
Possible configuration options are:
@ -420,15 +419,6 @@ bin/add-user.[sh|bat] -r master -u <username> -p <password>
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>disable-trust-manager</term>
<listitem>
<para>
If true, HTTPS server certificates are not verified. If you set this to false, you must
configure a truststore.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>disable-cookies</term>
<listitem>
@ -438,44 +428,6 @@ bin/add-user.[sh|bat] -r master -u <username> -p <password>
</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>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>
<term>client-keystore</term>
<listitem>
@ -515,13 +467,111 @@ bin/add-user.[sh|bat] -r master -u <username> -p <password>
</variablelist>
</para>
</section>
<section id="truststore">
<title>Securing Outgoing Server HTTP Requests</title>
<para>
When Keycloak connects out to remote HTTP endpoints over secure https connection, it has to validate the other
server's certificate in order to ensure it is connecting to a trusted server. That is necessary in order to
prevent man-in-the-middle attacks.
</para>
<para>
How certificates are validated is configured in the <literal>standalone/configuration/keycloak-server.json</literal>.
By default truststore provider is not configured, and any https connections fall back to standard java truststore
configuration as described in <ulink url="https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html">
Java's JSSE Reference Guide</ulink> - using <literal>javax.net.ssl.trustStore system property</literal>,
otherwise <literal>cacerts</literal> file that comes with java is used.
</para>
<para>
Truststore is used when connecting securely to identity brokers, LDAP identity providers, when sending emails,
and for backchannel communication with client applications.
Some of these facilities may - in case when no trusted certificate is found in your configured truststore -
fallback to using the JSSE provided truststore.
The default JavaMail API implementation used to send out emails behaves in this way, for example.
</para>
<para>
You can add your truststore configuration by using the following template:
<programlisting><![CDATA[
"truststore": {
"file": {
"file": "path to your .jks file containing public certificates",
"password": "password",
"hostname-verification-policy": "WILDCARD",
"disabled": false
}
}
]]></programlisting>
</para>
<para>
Possible configuration options are:
<variablelist>
<varlistentry>
<term>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">
<title>SSL/HTTPS Requirement/Modes</title>
<warning>
<para>
Keycloak is not set up by default to handle SSL/HTTPS in either the
war distribution or appliance. It is highly recommended that you either enable SSL on the Keycloak server
itself or on a reverse proxy in front of the Keycloak server.
Keycloak is not set up by default to handle SSL/HTTPS. It is highly recommended that you either enable SSL
on the Keycloak server itself or on a reverse proxy in front of the Keycloak server.
</para>
</warning>
<para>

View file

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

View file

@ -3,6 +3,8 @@
<head>
<title>Authentication Example</title>
<meta http-equiv="Content-Security-Policy" content="default-src *; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval'">
<script type="text/javascript" charset="utf-8" src="cordova.js"></script>
<script type="text/javascript" charset="utf-8" src="keycloak.js"></script>
<script type="text/javascript" charset="utf-8">

View file

@ -467,6 +467,9 @@ public class LDAPOperationManager {
if (protocol != null) {
env.put(Context.SECURITY_PROTOCOL, protocol);
if ("ssl".equals(protocol)) {
env.put("java.naming.ldap.factory.socket", "org.keycloak.connections.truststore.SSLSocketFactory");
}
}
String bindDN = this.config.getBindDN();

View file

@ -406,6 +406,16 @@ module.config([ '$routeProvider', function($routeProvider) {
},
controller : 'RealmEventsConfigCtrl'
})
.when('/realms/:realm/partial-import', {
templateUrl : resourceUrl + '/partials/partial-import.html',
resolve : {
resourceName : function() { return 'users'},
realm : function(RealmLoader) {
return RealmLoader();
}
},
controller : 'RealmImportCtrl'
})
.when('/create/user/:realm', {
templateUrl : resourceUrl + '/partials/user-detail.html',
resolve : {

View file

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

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

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.viewRealm" data-ng-class="(path[2] == 'sessions') && 'active'"><a href="#/realms/{{realm.realm}}/sessions/realm"><i class="fa fa-clock-o"></i> Sessions</a></li>
<li data-ng-show="access.viewEvents" data-ng-class="(path[2] == 'events' || path[2] == 'events-settings') && 'active'"><a href="#/realms/{{realm.realm}}/events"><i class="fa fa-calendar"></i> Events</a></li>
<li data-ng-show="access.manageRealm" ng-class="(path[2] =='partial-import') && 'active'"><a href="#/realms/{{realm.realm}}/partial-import"><span class="pficon pficon-import"></span> Import</a></li>
</ul>
</div>
</div>

View file

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

View file

@ -66,6 +66,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import org.keycloak.representations.idm.RolesRepresentation;
public class RepresentationToModel {
@ -195,47 +196,7 @@ public class RepresentationToModel {
createClients(session, rep, newRealm);
}
if (rep.getRoles() != null) {
if (rep.getRoles().getRealm() != null) { // realm roles
for (RoleRepresentation roleRep : rep.getRoles().getRealm()) {
createRole(newRealm, roleRep);
}
}
if (rep.getRoles().getClient() != null) {
for (Map.Entry<String, List<RoleRepresentation>> entry : rep.getRoles().getClient().entrySet()) {
ClientModel client = newRealm.getClientByClientId(entry.getKey());
if (client == null) {
throw new RuntimeException("App doesn't exist in role definitions: " + entry.getKey());
}
for (RoleRepresentation roleRep : entry.getValue()) {
// Application role may already exists (for example if it is defaultRole)
RoleModel role = roleRep.getId()!=null ? client.addRole(roleRep.getId(), roleRep.getName()) : client.addRole(roleRep.getName());
role.setDescription(roleRep.getDescription());
boolean scopeParamRequired = roleRep.isScopeParamRequired()==null ? false : roleRep.isScopeParamRequired();
role.setScopeParamRequired(scopeParamRequired);
}
}
}
// now that all roles are created, re-iterate and set up composites
if (rep.getRoles().getRealm() != null) { // realm roles
for (RoleRepresentation roleRep : rep.getRoles().getRealm()) {
RoleModel role = newRealm.getRole(roleRep.getName());
addComposites(role, roleRep, newRealm);
}
}
if (rep.getRoles().getClient() != null) {
for (Map.Entry<String, List<RoleRepresentation>> entry : rep.getRoles().getClient().entrySet()) {
ClientModel client = newRealm.getClientByClientId(entry.getKey());
if (client == null) {
throw new RuntimeException("App doesn't exist in role definitions: " + entry.getKey());
}
for (RoleRepresentation roleRep : entry.getValue()) {
RoleModel role = client.getRole(roleRep.getName());
addComposites(role, roleRep, newRealm);
}
}
}
}
importRoles(rep.getRoles(), newRealm);
// Setup realm default roles
if (rep.getDefaultRoles() != null) {
@ -356,6 +317,50 @@ public class RepresentationToModel {
}
}
public static void importRoles(RolesRepresentation realmRoles, RealmModel realm) {
if (realmRoles == null) return;
if (realmRoles.getRealm() != null) { // realm roles
for (RoleRepresentation roleRep : realmRoles.getRealm()) {
createRole(realm, roleRep);
}
}
if (realmRoles.getClient() != null) {
for (Map.Entry<String, List<RoleRepresentation>> entry : realmRoles.getClient().entrySet()) {
ClientModel client = realm.getClientByClientId(entry.getKey());
if (client == null) {
throw new RuntimeException("App doesn't exist in role definitions: " + entry.getKey());
}
for (RoleRepresentation roleRep : entry.getValue()) {
// Application role may already exists (for example if it is defaultRole)
RoleModel role = roleRep.getId()!=null ? client.addRole(roleRep.getId(), roleRep.getName()) : client.addRole(roleRep.getName());
role.setDescription(roleRep.getDescription());
boolean scopeParamRequired = roleRep.isScopeParamRequired()==null ? false : roleRep.isScopeParamRequired();
role.setScopeParamRequired(scopeParamRequired);
}
}
}
// now that all roles are created, re-iterate and set up composites
if (realmRoles.getRealm() != null) { // realm roles
for (RoleRepresentation roleRep : realmRoles.getRealm()) {
RoleModel role = realm.getRole(roleRep.getName());
addComposites(role, roleRep, realm);
}
}
if (realmRoles.getClient() != null) {
for (Map.Entry<String, List<RoleRepresentation>> entry : realmRoles.getClient().entrySet()) {
ClientModel client = realm.getClientByClientId(entry.getKey());
if (client == null) {
throw new RuntimeException("App doesn't exist in role definitions: " + entry.getKey());
}
for (RoleRepresentation roleRep : entry.getValue()) {
RoleModel role = client.getRole(roleRep.getName());
addComposites(role, roleRep, realm);
}
}
}
}
public static void importGroups(RealmModel realm, RealmRepresentation rep) {
List<GroupRepresentation> groups = rep.getGroups();
if (groups == null) return;
@ -639,15 +644,15 @@ public class RepresentationToModel {
if (rep.getAccountTheme() != null) realm.setAccountTheme(rep.getAccountTheme());
if (rep.getAdminTheme() != null) realm.setAdminTheme(rep.getAdminTheme());
if (rep.getEmailTheme() != null) realm.setEmailTheme(rep.getEmailTheme());
if (rep.isEventsEnabled() != null) realm.setEventsEnabled(rep.isEventsEnabled());
if (rep.getEventsExpiration() != null) realm.setEventsExpiration(rep.getEventsExpiration());
if (rep.getEventsListeners() != null) realm.setEventsListeners(new HashSet<>(rep.getEventsListeners()));
if (rep.getEnabledEventTypes() != null) realm.setEnabledEventTypes(new HashSet<>(rep.getEnabledEventTypes()));
if (rep.isAdminEventsEnabled() != null) realm.setAdminEventsEnabled(rep.isAdminEventsEnabled());
if (rep.isAdminEventsDetailsEnabled() != null) realm.setAdminEventsDetailsEnabled(rep.isAdminEventsDetailsEnabled());
if (rep.getPasswordPolicy() != null) realm.setPasswordPolicy(new PasswordPolicy(rep.getPasswordPolicy()));
if (rep.getOtpPolicyType() != null) realm.setOTPPolicy(toPolicy(rep));

View file

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

View file

@ -1059,6 +1059,7 @@ public class RealmAdapter implements RealmModel {
em.createNamedQuery("deleteGroupRoleMappingsByRole").setParameter("roleId", roleEntity.getId()).executeUpdate();
em.remove(roleEntity);
em.flush();
return true;
}
@ -1217,7 +1218,7 @@ public class RealmAdapter implements RealmModel {
realm.setEventsListeners(listeners);
em.flush();
}
@Override
public Set<String> getEnabledEventTypes() {
return realm.getEnabledEventTypes();
@ -1228,7 +1229,7 @@ public class RealmAdapter implements RealmModel {
realm.setEnabledEventTypes(enabledEventTypes);
em.flush();
}
@Override
public boolean isAdminEventsEnabled() {
return realm.isAdminEventsEnabled();
@ -1250,7 +1251,7 @@ public class RealmAdapter implements RealmModel {
realm.setAdminEventsDetailsEnabled(enabled);
em.flush();
}
@Override
public ClientModel getMasterAdminClient() {
ClientEntity masterAdminClient = realm.getMasterAdminClient();

View file

@ -640,6 +640,11 @@
<artifactId>keycloak-connections-mongo-update</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-connections-truststore</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-common</artifactId>

View file

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

View file

@ -1,6 +1,9 @@
package org.keycloak.email;
import org.jboss.logging.Logger;
import org.keycloak.connections.truststore.HostnameVerificationPolicy;
import org.keycloak.connections.truststore.JSSETruststoreConfigurator;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
@ -12,6 +15,8 @@ import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeBodyPart;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.MimeMultipart;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.Map;
import java.util.Properties;
@ -23,6 +28,12 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider {
private static final Logger log = Logger.getLogger(DefaultEmailSenderProvider.class);
private final KeycloakSession session;
public DefaultEmailSenderProvider(KeycloakSession session) {
this.session = session;
}
@Override
public void send(RealmModel realm, UserModel user, String subject, String textBody, String htmlBody) throws EmailException {
try {
@ -52,6 +63,10 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider {
props.setProperty("mail.smtp.starttls.enable", "true");
}
if (ssl || starttls) {
setupTruststore(props);
}
props.setProperty("mail.smtp.timeout", "10000");
props.setProperty("mail.smtp.connectiontimeout", "10000");
@ -94,9 +109,18 @@ public class DefaultEmailSenderProvider implements EmailSenderProvider {
}
}
private void setupTruststore(Properties props) throws NoSuchAlgorithmException, KeyManagementException {
JSSETruststoreConfigurator configurator = new JSSETruststoreConfigurator(session);
props.put("mail.smtp.ssl.socketFactory", configurator.getSSLSocketFactory());
if (configurator.getProvider().getPolicy() == HostnameVerificationPolicy.ANY) {
props.setProperty("mail.smtp.ssl.trust", "*");
}
}
@Override
public void close() {
}
}

View file

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

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");
redirectUri = null;
} else {
redirectUri = lowerCaseHostname(redirectUri);
String r = redirectUri.indexOf('?') != -1 ? redirectUri.substring(0, redirectUri.indexOf('?')) : redirectUri;
Set<String> resolveValidRedirects = resolveValidRedirects(uriInfo, rootUrl, validRedirects);
@ -96,6 +98,15 @@ public class RedirectUtils {
}
}
private static String lowerCaseHostname(String redirectUri) {
int n = redirectUri.indexOf('/', 7);
if (n == -1) {
return redirectUri.toLowerCase();
} else {
return redirectUri.substring(0, n).toLowerCase() + redirectUri.substring(n);
}
}
private static String relativeToAbsoluteURI(UriInfo uriInfo, String rootUrl, String relative) {
if (rootUrl == null) {
URI baseUri = uriInfo.getBaseUri();

View file

@ -21,7 +21,7 @@ import org.keycloak.common.util.Time;
import javax.ws.rs.core.UriInfo;
public class AdminEventBuilder {
private static final Logger log = Logger.getLogger(AdminEventBuilder.class);
private EventStoreProvider store;
@ -59,17 +59,17 @@ public class AdminEventBuilder {
authUser(auth.getUser());
authIpAddress(clientConnection.getRemoteAddr());
}
public AdminEventBuilder realm(RealmModel realm) {
adminEvent.setRealmId(realm.getId());
return this;
}
public AdminEventBuilder realm(String realmId) {
adminEvent.setRealmId(realmId);
return this;
}
public AdminEventBuilder operation(OperationType e) {
adminEvent.setOperationType(e);
return this;
@ -123,6 +123,18 @@ public class AdminEventBuilder {
return this;
}
public AdminEventBuilder resourcePath(String... pathElements) {
StringBuilder sb = new StringBuilder();
for (String element : pathElements) {
sb.append("/");
sb.append(element);
}
if (pathElements.length > 0) sb.deleteCharAt(0); // remove leading '/'
adminEvent.setResourcePath(sb.toString());
return this;
}
public AdminEventBuilder resourcePath(UriInfo uriInfo) {
String path = getResourcePath(uriInfo);
adminEvent.setResourcePath(path);
@ -155,7 +167,7 @@ public class AdminEventBuilder {
adminEvent.setError(error);
send();
}
public AdminEventBuilder representation(Object value) {
if (value == null || value.equals("")) {
return this;
@ -167,7 +179,7 @@ public class AdminEventBuilder {
}
return this;
}
public AdminEvent getEvent() {
return adminEvent;
}
@ -190,7 +202,7 @@ public class AdminEventBuilder {
log.error("Failed to save event", t);
}
}
if (listeners != null) {
for (EventListenerProvider l : listeners) {
try {

View file

@ -68,7 +68,7 @@ public class ClientResource {
private AdminEventBuilder adminEvent;
protected ClientModel client;
protected KeycloakSession session;
@Context
protected UriInfo uriInfo;
@ -107,11 +107,7 @@ public class ClientResource {
auth.requireManage();
try {
if (TRUE.equals(rep.isServiceAccountsEnabled()) && !client.isServiceAccountsEnabled()) {
new ClientManager(new RealmManager(session)).enableServiceAccount(client);;
}
RepresentationToModel.updateClient(rep, client);
updateClientFromRep(rep, client, session);
adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(rep).success();
return Response.noContent().build();
} catch (ModelDuplicateException e) {
@ -119,6 +115,13 @@ public class ClientResource {
}
}
public static void updateClientFromRep(ClientRepresentation rep, ClientModel client, KeycloakSession session) throws ModelDuplicateException {
if (TRUE.equals(rep.isServiceAccountsEnabled()) && !client.isServiceAccountsEnabled()) {
new ClientManager(new RealmManager(session)).enableServiceAccount(client);
}
RepresentationToModel.updateClient(rep, client);
}
/**
* Get representation of the client
@ -381,9 +384,9 @@ public class ClientResource {
auth.requireManage();
adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).success();
return new ResourceAdminManager(session).pushClientRevocationPolicy(uriInfo.getRequestUri(), realm, client);
}
/**
* Get application session count
*

View file

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

View file

@ -58,7 +58,7 @@ public class IdentityProviderResource {
private final KeycloakSession session;
private final IdentityProviderModel identityProviderModel;
private final AdminEventBuilder adminEvent;
@Context private UriInfo uriInfo;
public IdentityProviderResource(RealmAuth auth, RealmModel realm, KeycloakSession session, IdentityProviderModel identityProviderModel, AdminEventBuilder adminEvent) {
@ -94,9 +94,9 @@ public class IdentityProviderResource {
this.auth.requireManage();
this.realm.removeIdentityProviderByAlias(this.identityProviderModel.getAlias());
adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).success();
return Response.noContent().build();
}
@ -113,30 +113,34 @@ public class IdentityProviderResource {
try {
this.auth.requireManage();
String internalId = providerRep.getInternalId();
String newProviderId = providerRep.getAlias();
String oldProviderId = getProviderIdByInternalId(this.realm, internalId);
updateIdpFromRep(providerRep, realm, session);
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();
return Response.noContent().build();
} catch (ModelDuplicateException e) {
return ErrorResponse.exists("Identity Provider " + providerRep.getAlias() + " already exists");
}
}
public static void updateIdpFromRep(IdentityProviderRepresentation providerRep, RealmModel realm, KeycloakSession session) {
String internalId = providerRep.getInternalId();
String newProviderId = providerRep.getAlias();
String oldProviderId = getProviderIdByInternalId(realm, internalId);
realm.updateIdentityProvider(RepresentationToModel.toModel(realm, providerRep));
if (oldProviderId != null && !oldProviderId.equals(newProviderId)) {
// Admin changed the ID (alias) of identity provider. We must update all clients and users
logger.debug("Changing providerId in all clients and linked users. oldProviderId=" + oldProviderId + ", newProviderId=" + newProviderId);
updateUsersAfterProviderAliasChange(session.users().getUsers(realm, false), oldProviderId, newProviderId, realm, session);
}
}
// return ID of IdentityProvider from realm based on internalId of this provider
private String getProviderIdByInternalId(RealmModel realm, String providerInternalId) {
private static String getProviderIdByInternalId(RealmModel realm, String providerInternalId) {
List<IdentityProviderModel> providerModels = realm.getIdentityProviders();
for (IdentityProviderModel providerModel : providerModels) {
if (providerModel.getInternalId().equals(providerInternalId)) {
@ -147,17 +151,17 @@ public class IdentityProviderResource {
return null;
}
private void updateUsersAfterProviderAliasChange(List<UserModel> users, String oldProviderId, String newProviderId) {
private static void updateUsersAfterProviderAliasChange(List<UserModel> users, String oldProviderId, String newProviderId, RealmModel realm, KeycloakSession session) {
for (UserModel user : users) {
FederatedIdentityModel federatedIdentity = this.session.users().getFederatedIdentity(user, oldProviderId, this.realm);
FederatedIdentityModel federatedIdentity = session.users().getFederatedIdentity(user, oldProviderId, realm);
if (federatedIdentity != null) {
// Remove old link first
this.session.users().removeFederatedIdentity(this.realm, user, oldProviderId);
session.users().removeFederatedIdentity(realm, user, oldProviderId);
// And create new
FederatedIdentityModel newFederatedIdentity = new FederatedIdentityModel(newProviderId, federatedIdentity.getUserId(), federatedIdentity.getUserName(),
federatedIdentity.getToken());
this.session.users().addFederatedIdentity(this.realm, user, newFederatedIdentity);
session.users().addFederatedIdentity(realm, user, newFederatedIdentity);
}
}
}
@ -263,10 +267,10 @@ public class IdentityProviderResource {
auth.requireManage();
IdentityProviderMapperModel model = RepresentationToModel.toModel(mapper);
model = realm.addIdentityProviderMapper(model);
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, model.getId())
.representation(mapper).success();
return Response.created(uriInfo.getAbsolutePathBuilder().path(model.getId()).build()).build();
}

View file

@ -66,6 +66,8 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.PatternSyntaxException;
import org.keycloak.partialimport.PartialImportManager;
import org.keycloak.representations.idm.PartialImportRepresentation;
/**
* Base resource class for the admin REST api of one realm
@ -241,7 +243,7 @@ public class RealmAdminResource {
for (final UserFederationProviderModel fedProvider : federationProviders) {
usersSyncManager.refreshPeriodicSyncForProvider(session.getKeycloakSessionFactory(), session.getProvider(TimerProvider.class), fedProvider, realm.getId());
}
adminEvent.operation(OperationType.UPDATE).representation(rep).success();
return Response.noContent().build();
} catch (PatternSyntaxException e) {
@ -466,7 +468,7 @@ public class RealmAdminResource {
if (user != null) {
query.user(user);
}
if(dateFrom != null) {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
Date from = null;
@ -477,7 +479,7 @@ public class RealmAdminResource {
}
query.fromDate(from);
}
if(dateTo != null) {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
Date to = null;
@ -501,7 +503,7 @@ public class RealmAdminResource {
return query.getResultList();
}
/**
* Get admin events
*
@ -540,15 +542,15 @@ public class RealmAdminResource {
if (authClient != null) {
query.authClient(authClient);
}
if (authUser != null) {
query.authUser(authUser);
}
if (authIpAddress != null) {
query.authIpAddress(authIpAddress);
}
if (resourcePath != null) {
query.resourcePath(resourcePath);
}
@ -561,7 +563,7 @@ public class RealmAdminResource {
}
query.operation(t);
}
if(dateFrom != null) {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
Date from = null;
@ -572,7 +574,7 @@ public class RealmAdminResource {
}
query.fromTime(from);
}
if(dateTo != null) {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
Date to = null;
@ -606,7 +608,7 @@ public class RealmAdminResource {
EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
eventStore.clear(realm.getId());
}
/**
* Delete all admin events
*
@ -709,5 +711,18 @@ public class RealmAdminResource {
return ModelToRepresentation.toGroupHierarchy(found, true);
}
/**
* Partial import from a JSON file to an existing realm.
*
* @param rep
* @return
*/
@Path("partialImport")
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response partialImport(PartialImportRepresentation rep) {
auth.requireManage();
PartialImportManager partialImport = new PartialImportManager(rep, session, realm, adminEvent);
return partialImport.saveResources();
}
}

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();
if (session.getTransaction().isActive()) {
@ -189,7 +189,7 @@ public class UsersResource {
try {
UserModel user = session.users().addUser(realm, rep.getUsername());
Set<String> emptySet = Collections.emptySet();
updateUserFromRep(user, rep, emptySet);
updateUserFromRep(user, rep, emptySet, realm, session);
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, user.getId()).representation(rep).success();
@ -206,7 +206,7 @@ public class UsersResource {
}
}
private void updateUserFromRep(UserModel user, UserRepresentation rep, Set<String> attrsToRemove) {
public static void updateUserFromRep(UserModel user, UserRepresentation rep, Set<String> attrsToRemove, RealmModel realm, KeycloakSession session) {
if (realm.isEditUsernameAllowed()) {
user.setUsername(rep.getUsername());
}

View file

@ -72,9 +72,7 @@
},
"connectionsHttpClient": {
"default": {
"disable-trust-manager": true
}
"default": {}
},
@ -99,5 +97,14 @@
"databaseSchema": "${keycloak.connectionsMongo.databaseSchema:update}",
"connectionsPerHost": "${keycloak.connectionsMongo.connectionsPerHost:100}"
}
},
"truststore": {
"file": {
"file": "${keycloak.truststore.file:src/main/keystore/keycloak.truststore}",
"password": "${keycloak.truststore.password:secret}",
"hostname-verification-policy": "${keycloak.truststore.policy:WILDCARD}",
"disabled": "${keycloak.truststore.disabled:true}"
}
}
}
}

View file

@ -32,6 +32,7 @@ import org.keycloak.events.Errors;
import org.keycloak.events.EventType;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.pages.AppPage;
@ -44,6 +45,11 @@ import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule;
import org.openqa.selenium.WebDriver;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.*;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@ -96,6 +102,23 @@ public class TermsAndConditionsTest {
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectLogin().session(sessionId).assertEvent();
// assert user attribute is properly set
UserRepresentation user = keycloakRule.getUser("test", "test-user@localhost");
Map<String,List<String>> attributes = user.getAttributesAsListValues();
assertNotNull("timestamp for terms acceptance was not stored in user attributes", attributes);
List<String> termsAndConditions = attributes.get(TermsAndConditions.USER_ATTRIBUTE);
assertTrue("timestamp for terms acceptance was not stored in user attributes as "
+ TermsAndConditions.USER_ATTRIBUTE, termsAndConditions.size() == 1);
String timestamp = termsAndConditions.get(0);
assertNotNull("expected non-null timestamp for terms acceptance in user attribute "
+ TermsAndConditions.USER_ATTRIBUTE, timestamp);
try {
Integer.parseInt(timestamp);
}
catch (NumberFormatException e) {
fail("timestamp for terms acceptance is not a valid integer: '" + timestamp + "'");
}
}
@Test
@ -113,6 +136,14 @@ public class TermsAndConditionsTest {
.removeDetail(Details.CONSENT)
.assertEvent();
// assert user attribute is properly removed
UserRepresentation user = keycloakRule.getUser("test", "test-user@localhost");
Map<String,List<String>> attributes = user.getAttributesAsListValues();
if (attributes != null) {
assertNull("expected null for terms acceptance user attribute " + TermsAndConditions.USER_ATTRIBUTE,
attributes.get(TermsAndConditions.USER_ATTRIBUTE));
}
}

View file

@ -16,10 +16,7 @@ import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.WebResource;
import org.openqa.selenium.WebDriver;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.*;
import static org.junit.Assert.assertArrayEquals;
@ -41,32 +38,37 @@ public abstract class AbstractClientTest {
keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
RealmModel testRealm = manager.createRealm(REALM_NAME);
testRealm.setEnabled(true);
testRealm.setAccessCodeLifespanUserAction(600);
KeycloakModelUtils.generateRealmKeys(testRealm);
appRealm.getClientByClientId("test-app").setDirectAccessGrantsEnabled(true);
}
});
keycloak = Keycloak.getInstance("http://localhost:8081/auth", "master", "admin", "admin", Constants.ADMIN_CLI_CLIENT_ID);
RealmRepresentation rep = new RealmRepresentation();
rep.setRealm(REALM_NAME);
rep.setEnabled(true);
Map<String, String> config = new HashMap<>();
config.put("from", "auto@keycloak.org");
config.put("host", "localhost");
config.put("port", "3025");
rep.setSmtpServer(config);
keycloak.realms().create(rep);
realm = keycloak.realm(REALM_NAME);
}
@After
public void after() {
keycloak.close();
keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
RealmModel realm = manager.getRealmByName(REALM_NAME);
if (realm != null) {
manager.removeRealm(realm);
}
for (RealmRepresentation r : keycloak.realms().findAll()) {
if (r.getRealm().equals(REALM_NAME)) {
keycloak.realm(REALM_NAME).remove();
}
});
}
keycloak.close();
}
public static <T> void assertNames(List<T> actual, String... expected) {

View file

@ -4,6 +4,7 @@ import org.junit.Assert;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.IdentityProviderResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.events.Details;
@ -11,12 +12,10 @@ import org.keycloak.events.EventType;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.idm.ErrorRepresentation;
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.*;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.testsuite.Constants;
import org.keycloak.testsuite.actions.RequiredActionEmailVerificationTest;
import org.keycloak.testsuite.forms.ResetPasswordTest;
import org.keycloak.testsuite.pages.*;
@ -32,6 +31,7 @@ import javax.mail.internet.MimeMultipart;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.ClientErrorException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import java.io.IOException;
import java.util.ArrayList;
@ -63,18 +63,8 @@ public class UserTest extends AbstractClientTest {
@WebResource
protected InfoPage infoPage;
@Before
public void before() {
super.before();
keycloakRule.configure(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
RealmModel testRealm = manager.getRealm(REALM_NAME);
greenMail.configureRealm(testRealm);
}
});
}
@WebResource
protected LoginPage loginPage;
public String createUser() {
return createUser("user1", "user1@localhost");
@ -84,6 +74,7 @@ public class UserTest extends AbstractClientTest {
UserRepresentation user = new UserRepresentation();
user.setUsername(username);
user.setEmail(email);
user.setEnabled(true);
Response response = realm.users().create(user);
String createdId = ApiUtil.getCreatedId(response);
@ -600,6 +591,28 @@ public class UserTest extends AbstractClientTest {
}
}
@Test
public void resetUserPassword() {
String userId = createUser("user1", "user1@localhost");
CredentialRepresentation cred = new CredentialRepresentation();
cred.setType(CredentialRepresentation.PASSWORD);
cred.setValue("password");
cred.setTemporary(false);
realm.users().get(userId).resetPassword(cred);
String accountUrl = RealmsResource.accountUrl(UriBuilder.fromUri(Constants.AUTH_SERVER_ROOT)).build(REALM_NAME).toString();
driver.navigate().to(accountUrl);
assertEquals("Log in to admin-client-test", driver.getTitle());
loginPage.login("user1", "password");
assertEquals("Keycloak Account Management", driver.getTitle());
}
private void switchEditUsernameAllowedOn() {
RealmRepresentation rep = realm.toRepresentation();
rep.setEditUsernameAllowed(true);

View file

@ -65,8 +65,15 @@ public class OAuthRedirectUriTest {
ClientModel installedApp3 = KeycloakModelUtils.createClient(appRealm, "test-wildcard");
installedApp3.setEnabled(true);
installedApp3.addRedirectUri("http://example.com/foo/*");
installedApp3.addRedirectUri("http://with-dash.example.com/foo/*");
installedApp3.addRedirectUri("http://localhost:8081/foo/*");
installedApp3.setSecret("password");
ClientModel installedApp4 = KeycloakModelUtils.createClient(appRealm, "test-dash");
installedApp4.setEnabled(true);
installedApp4.addRedirectUri("http://with-dash.example.com");
installedApp4.addRedirectUri("http://with-dash.example.com/foo");
installedApp4.setSecret("password");
}
});
@ -216,6 +223,27 @@ public class OAuthRedirectUriTest {
checkRedirectUri("http://localhost:8081/foobar", false, true);
}
@Test
public void testDash() throws IOException {
oauth.clientId("test-dash");
checkRedirectUri("http://with-dash.example.com/foo", true);
}
@Test
public void testDifferentCaseInHostname() throws IOException {
oauth.clientId("test-dash");
checkRedirectUri("http://with-dash.example.com", true);
checkRedirectUri("http://wiTh-dAsh.example.com", true);
checkRedirectUri("http://with-dash.example.com/foo", true);
checkRedirectUri("http://wiTh-dAsh.example.com/foo", true);
checkRedirectUri("http://with-dash.eXampLe.com/foo", true);
checkRedirectUri("http://wiTh-dAsh.eXampLe.com/foo", true);
checkRedirectUri("http://wiTh-dAsh.eXampLe.com/Foo", false);
checkRedirectUri("http://wiTh-dAsh.eXampLe.com/foO", false);
}
@Test
public void testLocalhost() throws IOException {
oauth.clientId("test-installed");

View file

@ -116,6 +116,7 @@ public abstract class AbstractKeycloakRule extends ExternalResource {
try {
RealmManager manager = new RealmManager(session);
manager.setContextPath("/auth");
RealmModel adminstrationRealm = manager.getRealm(Config.getAdminRealm());
RealmModel appRealm = manager.getRealm(realmId);

View file

@ -69,6 +69,7 @@ public class KeycloakRule extends AbstractKeycloakRule {
try {
RealmManager manager = new RealmManager(session);
manager.setContextPath("/auth");
RealmModel adminstrationRealm = manager.getRealm(Config.getAdminRealm());
RealmModel appRealm = manager.getRealm("test");

View file

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

View file

@ -43,6 +43,9 @@ public class LDAPEmbeddedServer {
private static final String DEFAULT_BIND_HOST = "localhost";
private static final String DEFAULT_BIND_PORT = "10389";
private static final String DEFAULT_LDIF_FILE = "classpath:ldap/default-users.ldif";
private static final String PROPERTY_ENABLE_SSL = "enableSSL";
private static final String PROPERTY_KEYSTORE_FILE = "keystoreFile";
private static final String PROPERTY_CERTIFICATE_PASSWORD = "certificatePassword";
public static final String DSF_INMEMORY = "mem";
public static final String DSF_FILE = "file";
@ -56,6 +59,9 @@ public class LDAPEmbeddedServer {
protected String ldifFile;
protected String ldapSaslPrincipal;
protected String directoryServiceFactory;
protected boolean enableSSL = false;
protected String keystoreFile;
protected String certPassword;
protected DirectoryService directoryService;
protected LdapServer ldapServer;
@ -97,6 +103,9 @@ public class LDAPEmbeddedServer {
this.ldifFile = readProperty(PROPERTY_LDIF_FILE, DEFAULT_LDIF_FILE);
this.ldapSaslPrincipal = readProperty(PROPERTY_SASL_PRINCIPAL, null);
this.directoryServiceFactory = readProperty(PROPERTY_DSF, DEFAULT_DSF);
this.enableSSL = Boolean.valueOf(readProperty(PROPERTY_ENABLE_SSL, "false"));
this.keystoreFile = readProperty(PROPERTY_KEYSTORE_FILE, null);
this.certPassword = readProperty(PROPERTY_CERTIFICATE_PASSWORD, null);
}
protected String readProperty(String propertyName, String defaultValue) {
@ -194,6 +203,11 @@ public class LDAPEmbeddedServer {
// Read the transports
Transport ldap = new TcpTransport(this.bindHost, this.bindPort, 3, 50);
if (enableSSL) {
ldap.setEnableSSL(true);
ldapServer.setKeystoreFile(keystoreFile);
ldapServer.setCertificatePassword(certPassword);
}
ldapServer.addTransports( ldap );
// Associate the DS to this LdapServer

View file

@ -2,7 +2,7 @@
<!-- See src/resources/configuration/ReadMe.txt for how the configuration assembly works -->
<config>
<extension-module>org.jboss.as.connector</extension-module>
<subsystem xmlns="urn:jboss:domain:datasources:3.0">
<subsystem xmlns="urn:jboss:domain:datasources:4.0">
<datasources>
<datasource jndi-name="java:jboss/datasources/ExampleDS" pool-name="ExampleDS" enabled="true" use-java-context="true">
<connection-url>jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE</connection-url>

View file

@ -2,7 +2,7 @@
<!-- See src/resources/configuration/ReadMe.txt for how the configuration assembly works -->
<config default-supplement="default">
<extension-module>org.jboss.as.clustering.infinispan</extension-module>
<subsystem xmlns="urn:jboss:domain:infinispan:3.0">
<subsystem xmlns="urn:jboss:domain:infinispan:4.0">
<?CACHE-CONTAINERS?>
</subsystem>
<supplement name="default">
@ -21,16 +21,19 @@
</cache-container>
<cache-container name="web" default-cache="passivation" module="org.wildfly.clustering.web.infinispan">
<local-cache name="passivation">
<locking isolation="REPEATABLE_READ"/>
<transaction mode="BATCH"/>
<file-store passivation="true" purge="false"/>
</local-cache>
<local-cache name="persistent">
<locking isolation="REPEATABLE_READ"/>
<transaction mode="BATCH"/>
<file-store passivation="false" purge="false"/>
</local-cache>
</cache-container>
<cache-container name="ejb" aliases="sfsb" default-cache="passivation" module="org.wildfly.clustering.ejb.infinispan">
<local-cache name="passivation">
<locking isolation="REPEATABLE_READ"/>
<transaction mode="BATCH"/>
<file-store passivation="true" purge="false"/>
</local-cache>
@ -45,6 +48,11 @@
<eviction strategy="LRU" max-entries="10000"/>
<expiration max-idle="100000"/>
</local-cache>
<local-cache name="immutable-entity">
<transaction mode="NON_XA"/>
<eviction strategy="LRU" max-entries="10000"/>
<expiration max-idle="100000"/>
</local-cache>
<local-cache name="local-query">
<eviction strategy="LRU" max-entries="10000"/>
<expiration max-idle="100000"/>
@ -72,6 +80,7 @@
<cache-container name="web" default-cache="dist" module="org.wildfly.clustering.web.infinispan">
<transport lock-timeout="60000"/>
<distributed-cache name="dist" mode="ASYNC" l1-lifespan="0" owners="2">
<locking isolation="REPEATABLE_READ"/>
<transaction mode="BATCH"/>
<file-store/>
</distributed-cache>
@ -79,6 +88,7 @@
<cache-container name="ejb" aliases="sfsb" default-cache="dist" module="org.wildfly.clustering.ejb.infinispan">
<transport lock-timeout="60000"/>
<distributed-cache name="dist" mode="ASYNC" l1-lifespan="0" owners="2">
<locking isolation="REPEATABLE_READ"/>
<transaction mode="BATCH"/>
<file-store/>
</distributed-cache>