KEYCLOAK-3440
This commit is contained in:
parent
15d31a202f
commit
da135389c7
20 changed files with 330 additions and 130 deletions
|
@ -25,6 +25,7 @@
|
|||
</resources>
|
||||
|
||||
<dependencies>
|
||||
<module name="javax.transaction.api"/>
|
||||
<module name="org.keycloak.keycloak-common"/>
|
||||
<module name="org.keycloak.keycloak-core"/>
|
||||
<module name="org.keycloak.keycloak-server-spi"/>
|
||||
|
|
|
@ -33,5 +33,6 @@
|
|||
<module name="javax.ws.rs.api"/>
|
||||
<module name="org.apache.httpcomponents"/>
|
||||
<module name="org.jboss.resteasy.resteasy-jaxrs"/>
|
||||
<module name="javax.transaction.api"/>
|
||||
</dependencies>
|
||||
</module>
|
||||
|
|
|
@ -74,6 +74,11 @@
|
|||
<artifactId>junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.spec.javax.transaction</groupId>
|
||||
<artifactId>jboss-transaction-api_1.2_spec</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
|
|
@ -17,6 +17,8 @@
|
|||
|
||||
package org.keycloak.connections.jpa;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import javax.persistence.EntityManager;
|
||||
|
||||
/**
|
||||
|
@ -24,6 +26,7 @@ import javax.persistence.EntityManager;
|
|||
*/
|
||||
public class DefaultJpaConnectionProvider implements JpaConnectionProvider {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(DefaultJpaConnectionProvider.class);
|
||||
private final EntityManager em;
|
||||
|
||||
public DefaultJpaConnectionProvider(EntityManager em) {
|
||||
|
@ -37,6 +40,7 @@ public class DefaultJpaConnectionProvider implements JpaConnectionProvider {
|
|||
|
||||
@Override
|
||||
public void close() {
|
||||
logger.trace("DefaultJpaConnectionProvider close()");
|
||||
em.close();
|
||||
}
|
||||
|
||||
|
|
|
@ -29,12 +29,22 @@ import java.util.Map;
|
|||
import javax.naming.InitialContext;
|
||||
import javax.persistence.EntityManager;
|
||||
import javax.persistence.EntityManagerFactory;
|
||||
import javax.persistence.SynchronizationType;
|
||||
import javax.sql.DataSource;
|
||||
import javax.transaction.InvalidTransactionException;
|
||||
import javax.transaction.Synchronization;
|
||||
import javax.transaction.SystemException;
|
||||
import javax.transaction.Transaction;
|
||||
import javax.transaction.TransactionManager;
|
||||
import javax.transaction.UserTransaction;
|
||||
|
||||
import org.hibernate.ejb.AvailableSettings;
|
||||
import org.hibernate.engine.transaction.jta.platform.internal.AbstractJtaPlatform;
|
||||
import org.hibernate.engine.transaction.jta.platform.spi.JtaPlatform;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.connections.jpa.updater.JpaUpdaterProvider;
|
||||
import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProvider;
|
||||
import org.keycloak.connections.jpa.util.JpaUtils;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
|
@ -45,6 +55,7 @@ import org.keycloak.provider.ServerInfoAwareProviderFactory;
|
|||
import org.keycloak.models.dblock.DBLockManager;
|
||||
import org.keycloak.ServerStartupError;
|
||||
import org.keycloak.timer.TimerProvider;
|
||||
import org.keycloak.transaction.JtaTransactionManagerLookup;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
|
||||
|
@ -60,16 +71,29 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
|
|||
private volatile EntityManagerFactory emf;
|
||||
|
||||
private Config.Scope config;
|
||||
|
||||
private Map<String,String> operationalInfo;
|
||||
|
||||
private Map<String, String> operationalInfo;
|
||||
|
||||
private boolean jtaEnabled;
|
||||
private JtaTransactionManagerLookup jtaLookup;
|
||||
|
||||
private KeycloakSessionFactory factory;
|
||||
|
||||
@Override
|
||||
public JpaConnectionProvider create(KeycloakSession session) {
|
||||
logger.trace("Create JpaConnectionProvider");
|
||||
lazyInit(session);
|
||||
|
||||
EntityManager em = emf.createEntityManager();
|
||||
EntityManager em = null;
|
||||
if (!jtaEnabled) {
|
||||
logger.trace("enlisting EntityManager in JpaKeycloakTransaction");
|
||||
em = emf.createEntityManager();
|
||||
} else {
|
||||
|
||||
em = emf.createEntityManager(SynchronizationType.SYNCHRONIZED);
|
||||
}
|
||||
em = PersistenceExceptionConverter.create(em);
|
||||
session.getTransactionManager().enlist(new JpaKeycloakTransaction(em));
|
||||
if (!jtaEnabled) session.getTransactionManager().enlist(new JpaKeycloakTransaction(em));
|
||||
return new DefaultJpaConnectionProvider(em);
|
||||
}
|
||||
|
||||
|
@ -92,85 +116,112 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
|
|||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
this.factory = factory;
|
||||
checkJtaEnabled(factory);
|
||||
|
||||
}
|
||||
|
||||
protected void checkJtaEnabled(KeycloakSessionFactory factory) {
|
||||
jtaLookup = (JtaTransactionManagerLookup) factory.getProviderFactory(JtaTransactionManagerLookup.class);
|
||||
if (jtaLookup != null) {
|
||||
if (jtaLookup.getTransactionManager() != null) {
|
||||
jtaEnabled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void lazyInit(KeycloakSession session) {
|
||||
if (emf == null) {
|
||||
synchronized (this) {
|
||||
if (emf == null) {
|
||||
logger.debug("Initializing JPA connections");
|
||||
KeycloakModelUtils.suspendJtaTransaction(session.getKeycloakSessionFactory(), () -> {
|
||||
logger.debug("Initializing JPA connections");
|
||||
|
||||
Map<String, Object> properties = new HashMap<String, Object>();
|
||||
Map<String, Object> properties = new HashMap<String, Object>();
|
||||
|
||||
String unitName = "keycloak-default";
|
||||
String unitName = "keycloak-default";
|
||||
|
||||
String dataSource = config.get("dataSource");
|
||||
if (dataSource != null) {
|
||||
if (config.getBoolean("jta", false)) {
|
||||
properties.put(AvailableSettings.JTA_DATASOURCE, dataSource);
|
||||
String dataSource = config.get("dataSource");
|
||||
if (dataSource != null) {
|
||||
if (config.getBoolean("jta", jtaEnabled)) {
|
||||
properties.put(AvailableSettings.JTA_DATASOURCE, dataSource);
|
||||
} else {
|
||||
properties.put(AvailableSettings.NON_JTA_DATASOURCE, dataSource);
|
||||
}
|
||||
} else {
|
||||
properties.put(AvailableSettings.NON_JTA_DATASOURCE, dataSource);
|
||||
}
|
||||
} else {
|
||||
properties.put(AvailableSettings.JDBC_URL, config.get("url"));
|
||||
properties.put(AvailableSettings.JDBC_DRIVER, config.get("driver"));
|
||||
properties.put(AvailableSettings.JDBC_URL, config.get("url"));
|
||||
properties.put(AvailableSettings.JDBC_DRIVER, config.get("driver"));
|
||||
|
||||
String user = config.get("user");
|
||||
if (user != null) {
|
||||
properties.put(AvailableSettings.JDBC_USER, user);
|
||||
}
|
||||
String password = config.get("password");
|
||||
if (password != null) {
|
||||
properties.put(AvailableSettings.JDBC_PASSWORD, password);
|
||||
}
|
||||
}
|
||||
|
||||
String schema = getSchema();
|
||||
if (schema != null) {
|
||||
properties.put(JpaUtils.HIBERNATE_DEFAULT_SCHEMA, schema);
|
||||
}
|
||||
|
||||
MigrationStrategy migrationStrategy = getMigrationStrategy();
|
||||
boolean initializeEmpty = config.getBoolean("initializeEmpty", true);
|
||||
File databaseUpdateFile = getDatabaseUpdateFile();
|
||||
|
||||
properties.put("hibernate.show_sql", config.getBoolean("showSql", false));
|
||||
properties.put("hibernate.format_sql", config.getBoolean("formatSql", true));
|
||||
|
||||
Connection connection = getConnection();
|
||||
try{
|
||||
prepareOperationalInfo(connection);
|
||||
|
||||
String driverDialect = detectDialect(connection);
|
||||
if (driverDialect != null) {
|
||||
properties.put("hibernate.dialect", driverDialect);
|
||||
String user = config.get("user");
|
||||
if (user != null) {
|
||||
properties.put(AvailableSettings.JDBC_USER, user);
|
||||
}
|
||||
String password = config.get("password");
|
||||
if (password != null) {
|
||||
properties.put(AvailableSettings.JDBC_PASSWORD, password);
|
||||
}
|
||||
}
|
||||
|
||||
migration(migrationStrategy, initializeEmpty, schema, databaseUpdateFile, connection, session);
|
||||
|
||||
int globalStatsInterval = config.getInt("globalStatsInterval", -1);
|
||||
if (globalStatsInterval != -1) {
|
||||
properties.put("hibernate.generate_statistics", true);
|
||||
String schema = getSchema();
|
||||
if (schema != null) {
|
||||
properties.put(JpaUtils.HIBERNATE_DEFAULT_SCHEMA, schema);
|
||||
}
|
||||
|
||||
logger.trace("Creating EntityManagerFactory");
|
||||
emf = JpaUtils.createEntityManagerFactory(session, unitName, properties, getClass().getClassLoader());
|
||||
logger.trace("EntityManagerFactory created");
|
||||
MigrationStrategy migrationStrategy = getMigrationStrategy();
|
||||
boolean initializeEmpty = config.getBoolean("initializeEmpty", true);
|
||||
File databaseUpdateFile = getDatabaseUpdateFile();
|
||||
|
||||
if (globalStatsInterval != -1) {
|
||||
startGlobalStats(session, globalStatsInterval);
|
||||
properties.put("hibernate.show_sql", config.getBoolean("showSql", false));
|
||||
properties.put("hibernate.format_sql", config.getBoolean("formatSql", true));
|
||||
|
||||
Connection connection = getConnection();
|
||||
try {
|
||||
prepareOperationalInfo(connection);
|
||||
|
||||
String driverDialect = detectDialect(connection);
|
||||
if (driverDialect != null) {
|
||||
properties.put("hibernate.dialect", driverDialect);
|
||||
}
|
||||
|
||||
migration(migrationStrategy, initializeEmpty, schema, databaseUpdateFile, connection, session);
|
||||
|
||||
int globalStatsInterval = config.getInt("globalStatsInterval", -1);
|
||||
if (globalStatsInterval != -1) {
|
||||
properties.put("hibernate.generate_statistics", true);
|
||||
}
|
||||
|
||||
logger.trace("Creating EntityManagerFactory");
|
||||
logger.tracev("***** create EMF jtaEnabled {0} ", jtaEnabled);
|
||||
if (jtaEnabled) {
|
||||
properties.put(org.hibernate.cfg.AvailableSettings.JTA_PLATFORM, new AbstractJtaPlatform() {
|
||||
@Override
|
||||
protected TransactionManager locateTransactionManager() {
|
||||
return jtaLookup.getTransactionManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected UserTransaction locateUserTransaction() {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
emf = JpaUtils.createEntityManagerFactory(session, unitName, properties, getClass().getClassLoader(), jtaEnabled);
|
||||
logger.trace("EntityManagerFactory created");
|
||||
|
||||
if (globalStatsInterval != -1) {
|
||||
startGlobalStats(session, globalStatsInterval);
|
||||
}
|
||||
} finally {
|
||||
// Close after creating EntityManagerFactory to prevent in-mem databases from closing
|
||||
if (connection != null) {
|
||||
try {
|
||||
connection.close();
|
||||
} catch (SQLException e) {
|
||||
logger.warn("Can't close connection", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
// Close after creating EntityManagerFactory to prevent in-mem databases from closing
|
||||
if (connection != null) {
|
||||
try {
|
||||
connection.close();
|
||||
} catch (SQLException e) {
|
||||
logger.warn("Can't close connection", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -182,19 +233,19 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
|
|||
}
|
||||
|
||||
protected void prepareOperationalInfo(Connection connection) {
|
||||
try {
|
||||
operationalInfo = new LinkedHashMap<>();
|
||||
DatabaseMetaData md = connection.getMetaData();
|
||||
operationalInfo.put("databaseUrl",md.getURL());
|
||||
operationalInfo.put("databaseUser", md.getUserName());
|
||||
operationalInfo.put("databaseProduct", md.getDatabaseProductName() + " " + md.getDatabaseProductVersion());
|
||||
operationalInfo.put("databaseDriver", md.getDriverName() + " " + md.getDriverVersion());
|
||||
try {
|
||||
operationalInfo = new LinkedHashMap<>();
|
||||
DatabaseMetaData md = connection.getMetaData();
|
||||
operationalInfo.put("databaseUrl", md.getURL());
|
||||
operationalInfo.put("databaseUser", md.getUserName());
|
||||
operationalInfo.put("databaseProduct", md.getDatabaseProductName() + " " + md.getDatabaseProductVersion());
|
||||
operationalInfo.put("databaseDriver", md.getDriverName() + " " + md.getDriverVersion());
|
||||
|
||||
logger.debugf("Database info: %s", operationalInfo.toString());
|
||||
} catch (SQLException e) {
|
||||
logger.warn("Unable to prepare operational info due database exception: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
logger.warn("Unable to prepare operational info due database exception: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected String detectDialect(Connection connection) {
|
||||
|
@ -334,11 +385,11 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
|
|||
public String getSchema() {
|
||||
return config.get("schema");
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Map<String,String> getOperationalInfo() {
|
||||
return operationalInfo;
|
||||
}
|
||||
public Map<String, String> getOperationalInfo() {
|
||||
return operationalInfo;
|
||||
}
|
||||
|
||||
private MigrationStrategy getMigrationStrategy() {
|
||||
String migrationStrategy = config.get("migrationStrategy");
|
||||
|
|
|
@ -23,14 +23,13 @@ import java.sql.SQLException;
|
|||
import liquibase.Liquibase;
|
||||
import liquibase.exception.DatabaseException;
|
||||
import liquibase.exception.LiquibaseException;
|
||||
import liquibase.exception.LockException;
|
||||
import liquibase.lockservice.LockService;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.connections.jpa.JpaConnectionProvider;
|
||||
import org.keycloak.connections.jpa.JpaConnectionProviderFactory;
|
||||
import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProvider;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.dblock.DBLockProvider;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
|
@ -57,6 +56,7 @@ public class LiquibaseDBLockProvider implements DBLockProvider {
|
|||
this.session = session;
|
||||
}
|
||||
|
||||
|
||||
private void lazyInit() {
|
||||
if (!initialized) {
|
||||
LiquibaseConnectionProvider liquibaseProvider = session.getProvider(LiquibaseConnectionProvider.class);
|
||||
|
@ -92,35 +92,41 @@ public class LiquibaseDBLockProvider implements DBLockProvider {
|
|||
|
||||
@Override
|
||||
public void waitForLock() {
|
||||
lazyInit();
|
||||
KeycloakModelUtils.suspendJtaTransaction(session.getKeycloakSessionFactory(), () -> {
|
||||
|
||||
while (maxAttempts > 0) {
|
||||
try {
|
||||
lockService.waitForLock();
|
||||
factory.setHasLock(true);
|
||||
this.maxAttempts = DEFAULT_MAX_ATTEMPTS;
|
||||
return;
|
||||
} catch (LockRetryException le) {
|
||||
// Indicates we should try to acquire lock again in different transaction
|
||||
safeRollbackConnection();
|
||||
restart();
|
||||
maxAttempts--;
|
||||
} catch (RuntimeException re) {
|
||||
safeRollbackConnection();
|
||||
safeCloseConnection();
|
||||
throw re;
|
||||
lazyInit();
|
||||
|
||||
while (maxAttempts > 0) {
|
||||
try {
|
||||
lockService.waitForLock();
|
||||
factory.setHasLock(true);
|
||||
this.maxAttempts = DEFAULT_MAX_ATTEMPTS;
|
||||
return;
|
||||
} catch (LockRetryException le) {
|
||||
// Indicates we should try to acquire lock again in different transaction
|
||||
safeRollbackConnection();
|
||||
restart();
|
||||
maxAttempts--;
|
||||
} catch (RuntimeException re) {
|
||||
safeRollbackConnection();
|
||||
safeCloseConnection();
|
||||
throw re;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void releaseLock() {
|
||||
lazyInit();
|
||||
KeycloakModelUtils.suspendJtaTransaction(session.getKeycloakSessionFactory(), () -> {
|
||||
lazyInit();
|
||||
|
||||
lockService.releaseLock();
|
||||
lockService.reset();
|
||||
factory.setHasLock(false);
|
||||
lockService.releaseLock();
|
||||
lockService.reset();
|
||||
factory.setHasLock(false);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -136,21 +142,25 @@ public class LiquibaseDBLockProvider implements DBLockProvider {
|
|||
|
||||
@Override
|
||||
public void destroyLockInfo() {
|
||||
lazyInit();
|
||||
KeycloakModelUtils.suspendJtaTransaction(session.getKeycloakSessionFactory(), () -> {
|
||||
lazyInit();
|
||||
|
||||
try {
|
||||
this.lockService.destroy();
|
||||
dbConnection.commit();
|
||||
logger.debug("Destroyed lock table");
|
||||
} catch (DatabaseException | SQLException de) {
|
||||
logger.error("Failed to destroy lock table");
|
||||
safeRollbackConnection();
|
||||
}
|
||||
try {
|
||||
this.lockService.destroy();
|
||||
dbConnection.commit();
|
||||
logger.debug("Destroyed lock table");
|
||||
} catch (DatabaseException | SQLException de) {
|
||||
logger.error("Failed to destroy lock table");
|
||||
safeRollbackConnection();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
safeCloseConnection();
|
||||
KeycloakModelUtils.suspendJtaTransaction(session.getKeycloakSessionFactory(), () -> {
|
||||
safeCloseConnection();
|
||||
});
|
||||
}
|
||||
|
||||
private void safeRollbackConnection() {
|
||||
|
|
|
@ -21,6 +21,8 @@ import org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl;
|
|||
import org.hibernate.jpa.boot.internal.ParsedPersistenceXmlDescriptor;
|
||||
import org.hibernate.jpa.boot.internal.PersistenceXmlParser;
|
||||
import org.hibernate.jpa.boot.spi.Bootstrap;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory;
|
||||
import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider;
|
||||
import org.keycloak.connections.jpa.entityprovider.ProxyClassLoader;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
|
@ -46,8 +48,9 @@ public class JpaUtils {
|
|||
return (schema==null) ? tableName : schema + "." + tableName;
|
||||
}
|
||||
|
||||
public static EntityManagerFactory createEntityManagerFactory(KeycloakSession session, String unitName, Map<String, Object> properties, ClassLoader classLoader) {
|
||||
PersistenceXmlParser parser = new PersistenceXmlParser(new ClassLoaderServiceImpl(classLoader), PersistenceUnitTransactionType.RESOURCE_LOCAL);
|
||||
public static EntityManagerFactory createEntityManagerFactory(KeycloakSession session, String unitName, Map<String, Object> properties, ClassLoader classLoader, boolean jta) {
|
||||
PersistenceUnitTransactionType txType = jta ? PersistenceUnitTransactionType.JTA : PersistenceUnitTransactionType.RESOURCE_LOCAL;
|
||||
PersistenceXmlParser parser = new PersistenceXmlParser(new ClassLoaderServiceImpl(classLoader), txType);
|
||||
List<ParsedPersistenceXmlDescriptor> persistenceUnits = parser.doResolve(properties);
|
||||
for (ParsedPersistenceXmlDescriptor persistenceUnit : persistenceUnits) {
|
||||
if (persistenceUnit.getName().equals(unitName)) {
|
||||
|
@ -58,6 +61,7 @@ public class JpaUtils {
|
|||
}
|
||||
// Now build the entity manager factory, supplying a proxy classloader, so Hibernate will be able
|
||||
// to find and load the extra provided entities. Set the provided classloader as parent classloader.
|
||||
persistenceUnit.setTransactionType(txType);
|
||||
return Bootstrap.getEntityManagerFactoryBuilder(persistenceUnit, properties,
|
||||
new ProxyClassLoader(providedEntities, classLoader)).build();
|
||||
}
|
||||
|
|
|
@ -23,6 +23,21 @@ package org.keycloak.models;
|
|||
*/
|
||||
public interface KeycloakTransactionManager extends KeycloakTransaction {
|
||||
|
||||
enum JTAPolicy {
|
||||
/**
|
||||
* Do not interact with JTA at all
|
||||
*
|
||||
*/
|
||||
NOT_SUPPORTED,
|
||||
/**
|
||||
* A new JTA Transaction will be created when Keycloak TM begin() is called. If an existing JTA transaction
|
||||
* exists, it is suspended and resumed after the Keycloak transaction finishes.
|
||||
*/
|
||||
REQUIRES_NEW,
|
||||
}
|
||||
|
||||
JTAPolicy getJTAPolicy();
|
||||
void setJTAPolicy(JTAPolicy policy);
|
||||
void enlist(KeycloakTransaction transaction);
|
||||
void enlistAfterCompletion(KeycloakTransaction transaction);
|
||||
|
||||
|
|
|
@ -44,8 +44,14 @@ import org.keycloak.models.UserModel;
|
|||
import org.keycloak.representations.idm.CertificateRepresentation;
|
||||
import org.keycloak.common.util.CertificateUtils;
|
||||
import org.keycloak.common.util.PemUtils;
|
||||
import org.keycloak.transaction.JtaTransactionManagerLookup;
|
||||
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import javax.naming.InitialContext;
|
||||
import javax.sql.DataSource;
|
||||
import javax.transaction.InvalidTransactionException;
|
||||
import javax.transaction.SystemException;
|
||||
import javax.transaction.Transaction;
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
import java.security.Key;
|
||||
|
@ -56,6 +62,7 @@ import java.security.PrivateKey;
|
|||
import java.security.PublicKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.sql.DriverManager;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
|
@ -63,6 +70,7 @@ import java.util.List;
|
|||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* Set of helper methods, which are useful in various model implementations.
|
||||
|
@ -303,6 +311,7 @@ public final class KeycloakModelUtils {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
public static String getMasterRealmAdminApplicationClientId(String realmName) {
|
||||
return realmName + "-realm";
|
||||
}
|
||||
|
@ -651,4 +660,33 @@ public final class KeycloakModelUtils {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void suspendJtaTransaction(KeycloakSessionFactory factory, Runnable runnable) {
|
||||
JtaTransactionManagerLookup lookup = (JtaTransactionManagerLookup)factory.getProviderFactory(JtaTransactionManagerLookup.class);
|
||||
Transaction suspended = null;
|
||||
try {
|
||||
if (lookup != null) {
|
||||
if (lookup.getTransactionManager() != null) {
|
||||
try {
|
||||
suspended = lookup.getTransactionManager().suspend();
|
||||
} catch (SystemException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
runnable.run();
|
||||
} finally {
|
||||
if (suspended != null) {
|
||||
try {
|
||||
lookup.getTransactionManager().resume(suspended);
|
||||
} catch (InvalidTransactionException e) {
|
||||
throw new RuntimeException(e);
|
||||
} catch (SystemException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,4 +61,5 @@ org.keycloak.authorization.AuthorizationSpi
|
|||
org.keycloak.models.cache.authorization.CachedStoreFactorySpi
|
||||
org.keycloak.protocol.oidc.TokenIntrospectionSpi
|
||||
org.keycloak.policy.PasswordPolicySpi
|
||||
org.keycloak.policy.PasswordPolicyManagerSpi
|
||||
org.keycloak.policy.PasswordPolicyManagerSpi
|
||||
org.keycloak.transaction.TransactionManagerLookupSpi
|
||||
|
|
|
@ -27,6 +27,7 @@ import org.keycloak.scripting.ScriptingProvider;
|
|||
import org.keycloak.storage.UserStorageManager;
|
||||
import org.keycloak.storage.federated.UserFederatedStorageProvider;
|
||||
|
||||
import javax.transaction.TransactionManager;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
|
@ -51,7 +52,7 @@ public class DefaultKeycloakSession implements KeycloakSession {
|
|||
|
||||
public DefaultKeycloakSession(DefaultKeycloakSessionFactory factory) {
|
||||
this.factory = factory;
|
||||
this.transactionManager = new DefaultKeycloakTransactionManager();
|
||||
this.transactionManager = new DefaultKeycloakTransactionManager(this);
|
||||
federationManager = new UserFederationManager(this);
|
||||
context = new DefaultKeycloakContext(this);
|
||||
}
|
||||
|
|
|
@ -49,7 +49,6 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
|
|||
private Map<Class<? extends Provider>, String> provider = new HashMap<>();
|
||||
private volatile Map<Class<? extends Provider>, Map<String, ProviderFactory>> factoriesMap = new HashMap<>();
|
||||
protected CopyOnWriteArrayList<ProviderEventListener> listeners = new CopyOnWriteArrayList<>();
|
||||
private TransactionManager tm;
|
||||
|
||||
// TODO: Likely should be changed to int and use Time.currentTime() to be compatible with all our "time" reps
|
||||
protected long serverStartupTimestamp;
|
||||
|
@ -97,8 +96,6 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
|
|||
// make the session factory ready for hot deployment
|
||||
ProviderManagerRegistry.SINGLETON.setDeployer(this);
|
||||
|
||||
JtaTransactionManagerLookup lookup = (JtaTransactionManagerLookup)getProviderFactory(JtaTransactionManagerLookup.class);
|
||||
if (lookup != null) tm = lookup.getTransactionManager();
|
||||
}
|
||||
protected Map<Class<? extends Provider>, Map<String, ProviderFactory>> getFactoriesCopy() {
|
||||
Map<Class<? extends Provider>, Map<String, ProviderFactory>> copy = new HashMap<>();
|
||||
|
@ -282,9 +279,6 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
|
|||
|
||||
public KeycloakSession create() {
|
||||
KeycloakSession session = new DefaultKeycloakSession(this);
|
||||
if (tm != null) {
|
||||
session.getTransactionManager().enlist(new JtaTransactionWrapper(tm));
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
|
|
|
@ -16,9 +16,13 @@
|
|||
*/
|
||||
package org.keycloak.services;
|
||||
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakTransaction;
|
||||
import org.keycloak.models.KeycloakTransactionManager;
|
||||
import org.keycloak.transaction.JtaTransactionManagerLookup;
|
||||
import org.keycloak.transaction.JtaTransactionWrapper;
|
||||
|
||||
import javax.transaction.TransactionManager;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
|
@ -34,6 +38,12 @@ public class DefaultKeycloakTransactionManager implements KeycloakTransactionMan
|
|||
private List<KeycloakTransaction> afterCompletion = new LinkedList<KeycloakTransaction>();
|
||||
private boolean active;
|
||||
private boolean rollback;
|
||||
private KeycloakSession session;
|
||||
private JTAPolicy jtaPolicy = JTAPolicy.REQUIRES_NEW;
|
||||
|
||||
public DefaultKeycloakTransactionManager(KeycloakSession session) {
|
||||
this.session = session;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void enlist(KeycloakTransaction transaction) {
|
||||
|
@ -62,12 +72,31 @@ public class DefaultKeycloakTransactionManager implements KeycloakTransactionMan
|
|||
prepare.add(transaction);
|
||||
}
|
||||
|
||||
@Override
|
||||
public JTAPolicy getJTAPolicy() {
|
||||
return jtaPolicy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setJTAPolicy(JTAPolicy policy) {
|
||||
jtaPolicy = policy;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void begin() {
|
||||
if (active) {
|
||||
throw new IllegalStateException("Transaction already active");
|
||||
}
|
||||
|
||||
if (jtaPolicy == JTAPolicy.REQUIRES_NEW) {
|
||||
JtaTransactionManagerLookup jtaLookup = session.getProvider(JtaTransactionManagerLookup.class);
|
||||
TransactionManager tm = jtaLookup.getTransactionManager();
|
||||
if (tm != null) {
|
||||
enlist(new JtaTransactionWrapper(tm));
|
||||
}
|
||||
}
|
||||
|
||||
for (KeycloakTransaction tx : transactions) {
|
||||
tx.begin();
|
||||
}
|
||||
|
|
|
@ -45,10 +45,13 @@ import org.keycloak.services.scheduled.ClusterAwareScheduledTaskRunner;
|
|||
import org.keycloak.services.util.JsonConfigProvider;
|
||||
import org.keycloak.services.util.ObjectMapperResolver;
|
||||
import org.keycloak.timer.TimerProvider;
|
||||
import org.keycloak.transaction.JtaTransactionManagerLookup;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
import org.keycloak.common.util.SystemEnvProperties;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.transaction.SystemException;
|
||||
import javax.transaction.Transaction;
|
||||
import javax.ws.rs.core.Application;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
@ -155,11 +158,28 @@ public class KeycloakApplication extends Application {
|
|||
// Migrate model, bootstrap master realm, import realms and create admin user. This is done with acquired dbLock
|
||||
protected ExportImportManager migrateAndBootstrap() {
|
||||
ExportImportManager exportImportManager;
|
||||
logger.debug("Calling migrateModel");
|
||||
migrateModel();
|
||||
|
||||
logger.debug("bootstrap");
|
||||
KeycloakSession session = sessionFactory.create();
|
||||
try {
|
||||
session.getTransactionManager().begin();
|
||||
JtaTransactionManagerLookup lookup = (JtaTransactionManagerLookup) sessionFactory.getProviderFactory(JtaTransactionManagerLookup.class);
|
||||
if (lookup != null) {
|
||||
if (lookup.getTransactionManager() != null) {
|
||||
try {
|
||||
Transaction transaction = lookup.getTransactionManager().getTransaction();
|
||||
logger.debugv("bootstrap current transaction? {0}", transaction != null);
|
||||
if (transaction != null) {
|
||||
logger.debugv("bootstrap current transaction status? {0}", transaction.getStatus());
|
||||
}
|
||||
} catch (SystemException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ApplianceBootstrap applianceBootstrap = new ApplianceBootstrap(session);
|
||||
exportImportManager = new ExportImportManager(session);
|
||||
|
|
|
@ -184,6 +184,8 @@ public class UsersResource {
|
|||
return ErrorResponse.exists("User is read only!");
|
||||
} catch (ModelException me) {
|
||||
return ErrorResponse.exists("Could not update user!");
|
||||
} catch (Exception me) { // JPA may be committed by JTA which can't
|
||||
return ErrorResponse.exists("Could not update user!");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,9 @@
|
|||
*/
|
||||
package org.keycloak.transaction;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.KeycloakTransaction;
|
||||
import org.keycloak.storage.UserStorageManager;
|
||||
|
||||
import javax.transaction.InvalidTransactionException;
|
||||
import javax.transaction.NotSupportedException;
|
||||
|
@ -31,16 +33,22 @@ import javax.transaction.UserTransaction;
|
|||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class JtaTransactionWrapper implements KeycloakTransaction {
|
||||
private static final Logger logger = Logger.getLogger(JtaTransactionWrapper.class);
|
||||
protected TransactionManager tm;
|
||||
protected Transaction ut;
|
||||
protected Transaction suspended;
|
||||
protected Exception ended;
|
||||
|
||||
public JtaTransactionWrapper(TransactionManager tm) {
|
||||
this.tm = tm;
|
||||
try {
|
||||
|
||||
suspended = tm.suspend();
|
||||
logger.debug("new JtaTransactionWrapper");
|
||||
logger.debugv("was existing? {0}", suspended != null);
|
||||
tm.begin();
|
||||
ut = tm.getTransaction();
|
||||
//ended = new Exception();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
@ -53,16 +61,20 @@ public class JtaTransactionWrapper implements KeycloakTransaction {
|
|||
@Override
|
||||
public void commit() {
|
||||
try {
|
||||
ut.commit();
|
||||
logger.debug("JtaTransactionWrapper commit");
|
||||
tm.commit();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
end();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rollback() {
|
||||
try {
|
||||
ut.rollback();
|
||||
logger.debug("JtaTransactionWrapper rollback");
|
||||
tm.rollback();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
|
@ -74,7 +86,7 @@ public class JtaTransactionWrapper implements KeycloakTransaction {
|
|||
@Override
|
||||
public void setRollbackOnly() {
|
||||
try {
|
||||
ut.setRollbackOnly();
|
||||
tm.setRollbackOnly();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
@ -83,7 +95,7 @@ public class JtaTransactionWrapper implements KeycloakTransaction {
|
|||
@Override
|
||||
public boolean getRollbackOnly() {
|
||||
try {
|
||||
return ut.getStatus() == Status.STATUS_MARKED_ROLLBACK;
|
||||
return tm.getStatus() == Status.STATUS_MARKED_ROLLBACK;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
@ -92,15 +104,28 @@ public class JtaTransactionWrapper implements KeycloakTransaction {
|
|||
@Override
|
||||
public boolean isActive() {
|
||||
try {
|
||||
return ut.getStatus() == Status.STATUS_ACTIVE;
|
||||
return tm.getStatus() == Status.STATUS_ACTIVE;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
/*
|
||||
|
||||
@Override
|
||||
protected void finalize() throws Throwable {
|
||||
if (ended != null) {
|
||||
logger.error("TX didn't close at position", ended);
|
||||
}
|
||||
|
||||
}
|
||||
*/
|
||||
|
||||
protected void end() {
|
||||
ended = null;
|
||||
logger.debug("JtaTransactionWrapper end");
|
||||
if (suspended != null) {
|
||||
try {
|
||||
logger.debug("JtaTransactionWrapper resuming suspended");
|
||||
tm.resume(suspended);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
|
|
|
@ -18,5 +18,4 @@
|
|||
org.keycloak.exportimport.ClientDescriptionConverterSpi
|
||||
org.keycloak.wellknown.WellKnownSpi
|
||||
org.keycloak.services.clientregistration.ClientRegistrationSpi
|
||||
org.keycloak.transaction.TransactionManagerLookupSpi
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
<password>sa</password>
|
||||
</security>
|
||||
</datasource>
|
||||
<datasource jndi-name="java:jboss/datasources/KeycloakDS" jta="false" pool-name="KeycloakDS" enabled="true" use-java-context="true">
|
||||
<datasource jndi-name="java:jboss/datasources/KeycloakDS" pool-name="KeycloakDS" enabled="true" use-java-context="true">
|
||||
<?KEYCLOAK_DS_CONNECTION_URL?>
|
||||
<driver>h2</driver>
|
||||
<security>
|
||||
|
|
Loading…
Reference in a new issue