Merge remote-tracking branch 'upstream/master' into prod
This commit is contained in:
commit
554da73398
88 changed files with 3043 additions and 376 deletions
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -145,7 +145,7 @@ public class HttpClientBuilder {
|
|||
* Disable cookie management.
|
||||
*/
|
||||
public HttpClientBuilder disableCookies(boolean disable) {
|
||||
this.disableTrustManager = disable;
|
||||
this.disableCookies = disable;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
35
connections/truststore/pom.xml
Executable 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>
|
|
@ -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() {
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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> {
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
org.keycloak.connections.truststore.FileTruststoreProviderFactory
|
|
@ -0,0 +1 @@
|
|||
org.keycloak.connections.truststore.TruststoreSpi
|
|
@ -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;
|
||||
}
|
||||
}
|
5
dependencies/server-min/pom.xml
vendored
5
dependencies/server-min/pom.xml
vendored
|
@ -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>
|
||||
|
|
|
@ -45,9 +45,7 @@
|
|||
},
|
||||
|
||||
"connectionsHttpClient": {
|
||||
"default": {
|
||||
"disable-trust-manager": true
|
||||
}
|
||||
"default": {}
|
||||
},
|
||||
|
||||
"connectionsJpa": {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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"/>
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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>
|
|
@ -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"/>
|
||||
|
|
|
@ -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"/>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 : {
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
});
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
5
pom.xml
5
pom.xml
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ public class DefaultEmailSenderProviderFactory implements EmailSenderProviderFac
|
|||
|
||||
@Override
|
||||
public EmailSenderProvider create(KeycloakSession session) {
|
||||
return new DefaultEmailSenderProvider();
|
||||
return new DefaultEmailSenderProvider(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -51,9 +51,7 @@
|
|||
},
|
||||
|
||||
"connectionsHttpClient": {
|
||||
"default": {
|
||||
"disable-trust-manager": true
|
||||
}
|
||||
"default": {}
|
||||
},
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue