diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-jpa/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-jpa/main/module.xml index 5373067447..94c80a2a96 100755 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-jpa/main/module.xml +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-model-jpa/main/module.xml @@ -25,6 +25,7 @@ + diff --git a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-server-spi/main/module.xml b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-server-spi/main/module.xml index 95f34b6943..8b5632e30a 100755 --- a/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-server-spi/main/module.xml +++ b/distribution/feature-packs/server-feature-pack/src/main/resources/modules/system/layers/keycloak/org/keycloak/keycloak-server-spi/main/module.xml @@ -33,5 +33,6 @@ + diff --git a/federation/ldap/pom.xml b/federation/ldap/pom.xml index f6a8c4bae1..257c617af0 100755 --- a/federation/ldap/pom.xml +++ b/federation/ldap/pom.xml @@ -74,6 +74,11 @@ junit test + + org.jboss.spec.javax.transaction + jboss-transaction-api_1.2_spec + provided + diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProvider.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProvider.java index db85a825c9..e35e5b0734 100644 --- a/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProvider.java +++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProvider.java @@ -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(); } diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java index 3c74264f8d..d029e16767 100755 --- a/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java +++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java @@ -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 Stian Thorgersen @@ -60,16 +71,29 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide private volatile EntityManagerFactory emf; private Config.Scope config; - - private Map operationalInfo; + + private Map 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 properties = new HashMap(); + Map properties = new HashMap(); - 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 getOperationalInfo() { - return operationalInfo; - } + public Map getOperationalInfo() { + return operationalInfo; + } private MigrationStrategy getMigrationStrategy() { String migrationStrategy = config.get("migrationStrategy"); diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/lock/LiquibaseDBLockProvider.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/lock/LiquibaseDBLockProvider.java index 7c48499798..d080542032 100644 --- a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/lock/LiquibaseDBLockProvider.java +++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/lock/LiquibaseDBLockProvider.java @@ -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 Marek Posolda @@ -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() { diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/util/JpaUtils.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/util/JpaUtils.java index 5ac7d2f6a9..0385f6aa70 100644 --- a/model/jpa/src/main/java/org/keycloak/connections/jpa/util/JpaUtils.java +++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/util/JpaUtils.java @@ -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 properties, ClassLoader classLoader) { - PersistenceXmlParser parser = new PersistenceXmlParser(new ClassLoaderServiceImpl(classLoader), PersistenceUnitTransactionType.RESOURCE_LOCAL); + public static EntityManagerFactory createEntityManagerFactory(KeycloakSession session, String unitName, Map properties, ClassLoader classLoader, boolean jta) { + PersistenceUnitTransactionType txType = jta ? PersistenceUnitTransactionType.JTA : PersistenceUnitTransactionType.RESOURCE_LOCAL; + PersistenceXmlParser parser = new PersistenceXmlParser(new ClassLoaderServiceImpl(classLoader), txType); List 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(); } diff --git a/server-spi/src/main/java/org/keycloak/models/KeycloakTransactionManager.java b/server-spi/src/main/java/org/keycloak/models/KeycloakTransactionManager.java index 0e2dcbea97..a18d8cfd96 100755 --- a/server-spi/src/main/java/org/keycloak/models/KeycloakTransactionManager.java +++ b/server-spi/src/main/java/org/keycloak/models/KeycloakTransactionManager.java @@ -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); diff --git a/server-spi/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java b/server-spi/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java index bc20d49042..b8adc6271d 100755 --- a/server-spi/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java +++ b/server-spi/src/main/java/org/keycloak/models/utils/KeycloakModelUtils.java @@ -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); + } + } + + } + + } } diff --git a/services/src/main/java/org/keycloak/transaction/JtaTransactionManagerLookup.java b/server-spi/src/main/java/org/keycloak/transaction/JtaTransactionManagerLookup.java similarity index 100% rename from services/src/main/java/org/keycloak/transaction/JtaTransactionManagerLookup.java rename to server-spi/src/main/java/org/keycloak/transaction/JtaTransactionManagerLookup.java diff --git a/services/src/main/java/org/keycloak/transaction/TransactionManagerLookupSpi.java b/server-spi/src/main/java/org/keycloak/transaction/TransactionManagerLookupSpi.java similarity index 100% rename from services/src/main/java/org/keycloak/transaction/TransactionManagerLookupSpi.java rename to server-spi/src/main/java/org/keycloak/transaction/TransactionManagerLookupSpi.java diff --git a/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi index d2431d84b0..5ab0346aa3 100755 --- a/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/server-spi/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -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 \ No newline at end of file +org.keycloak.policy.PasswordPolicyManagerSpi +org.keycloak.transaction.TransactionManagerLookupSpi diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java index 539bafbb5a..b1ae4ddac1 100644 --- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java +++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSession.java @@ -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); } diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java index 45bef3c3ea..d5474dcb69 100755 --- a/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java +++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakSessionFactory.java @@ -49,7 +49,6 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr private Map, String> provider = new HashMap<>(); private volatile Map, Map> factoriesMap = new HashMap<>(); protected CopyOnWriteArrayList 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, Map> getFactoriesCopy() { Map, Map> 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; } diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakTransactionManager.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakTransactionManager.java index 53f92f4fb8..81379a1cdf 100755 --- a/services/src/main/java/org/keycloak/services/DefaultKeycloakTransactionManager.java +++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakTransactionManager.java @@ -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 afterCompletion = new LinkedList(); 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(); } diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java index 6cdf52eab2..68e080663b 100644 --- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java +++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java @@ -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); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java index 3986988f87..26daf84019 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java @@ -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!"); } } diff --git a/services/src/main/java/org/keycloak/transaction/JtaTransactionWrapper.java b/services/src/main/java/org/keycloak/transaction/JtaTransactionWrapper.java index ecc3071a0d..e387ff4599 100644 --- a/services/src/main/java/org/keycloak/transaction/JtaTransactionWrapper.java +++ b/services/src/main/java/org/keycloak/transaction/JtaTransactionWrapper.java @@ -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); diff --git a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi index 77cba5e1ef..55b31a09f9 100755 --- a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -18,5 +18,4 @@ org.keycloak.exportimport.ClientDescriptionConverterSpi org.keycloak.wellknown.WellKnownSpi org.keycloak.services.clientregistration.ClientRegistrationSpi -org.keycloak.transaction.TransactionManagerLookupSpi diff --git a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-datasources.xml b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-datasources.xml index 326fa7f26f..c823e4f3a3 100755 --- a/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-datasources.xml +++ b/wildfly/server-subsystem/src/main/resources/subsystem-templates/keycloak-datasources.xml @@ -29,7 +29,7 @@ sa - + h2