diff --git a/connections/jpa-liquibase/pom.xml b/connections/jpa-liquibase/pom.xml
index d877dea28b..d541d2a6cb 100755
--- a/connections/jpa-liquibase/pom.xml
+++ b/connections/jpa-liquibase/pom.xml
@@ -19,6 +19,11 @@
keycloak-connections-jpa
${project.version}
+
+ org.keycloak
+ keycloak-services
+ ${project.version}
+
org.liquibase
liquibase-core
@@ -33,6 +38,7 @@
org.jboss.logging
jboss-logging
+
diff --git a/connections/jpa-liquibase/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java b/connections/jpa-liquibase/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java
index 04a186c533..a611c3cd2c 100644
--- a/connections/jpa-liquibase/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java
+++ b/connections/jpa-liquibase/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java
@@ -28,6 +28,7 @@ import liquibase.resource.ClassLoaderResourceAccessor;
import liquibase.servicelocator.ServiceLocator;
import org.jboss.logging.Logger;
import org.keycloak.connections.jpa.updater.JpaUpdaterProvider;
+import org.keycloak.models.KeycloakSession;
import java.sql.Connection;
import java.sql.ResultSet;
@@ -50,9 +51,12 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
}
@Override
- public void update(Connection connection) {
+ public void update(KeycloakSession session, Connection connection) {
logger.debug("Starting database update");
+ // Need ThreadLocal as liquibase doesn't seem to have API to inject custom objects into tasks
+ ThreadLocalSessionContext.setCurrentSession(session);
+
try {
Liquibase liquibase = getLiquibase(connection);
@@ -81,7 +85,10 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
}
} catch (Exception e) {
throw new RuntimeException("Failed to update database", e);
+ } finally {
+ ThreadLocalSessionContext.removeCurrentSession();
}
+
logger.debug("Completed database update");
}
diff --git a/connections/jpa-liquibase/src/main/java/org/keycloak/connections/jpa/updater/liquibase/ThreadLocalSessionContext.java b/connections/jpa-liquibase/src/main/java/org/keycloak/connections/jpa/updater/liquibase/ThreadLocalSessionContext.java
new file mode 100644
index 0000000000..24a583f3c1
--- /dev/null
+++ b/connections/jpa-liquibase/src/main/java/org/keycloak/connections/jpa/updater/liquibase/ThreadLocalSessionContext.java
@@ -0,0 +1,23 @@
+package org.keycloak.connections.jpa.updater.liquibase;
+
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * @author Marek Posolda
+ */
+public class ThreadLocalSessionContext {
+
+ private static final ThreadLocal currentSession = new ThreadLocal();
+
+ public static KeycloakSession getCurrentSession() {
+ return currentSession.get();
+ }
+
+ public static void setCurrentSession(KeycloakSession session) {
+ currentSession.set(session);
+ }
+
+ public static void removeCurrentSession() {
+ currentSession.remove();
+ }
+}
diff --git a/connections/jpa-liquibase/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/CustomKeycloakTask.java b/connections/jpa-liquibase/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/CustomKeycloakTask.java
new file mode 100644
index 0000000000..40d71cb7b8
--- /dev/null
+++ b/connections/jpa-liquibase/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/CustomKeycloakTask.java
@@ -0,0 +1,97 @@
+package org.keycloak.connections.jpa.updater.liquibase.custom;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.util.ArrayList;
+import java.util.List;
+
+import liquibase.change.custom.CustomSqlChange;
+import liquibase.database.Database;
+import liquibase.database.jvm.JdbcConnection;
+import liquibase.exception.CustomChangeException;
+import liquibase.exception.SetupException;
+import liquibase.exception.ValidationErrors;
+import liquibase.resource.ResourceAccessor;
+import liquibase.snapshot.SnapshotGeneratorFactory;
+import liquibase.statement.SqlStatement;
+import liquibase.structure.core.Table;
+import org.keycloak.connections.jpa.updater.liquibase.ThreadLocalSessionContext;
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * @author Marek Posolda
+ */
+public abstract class CustomKeycloakTask implements CustomSqlChange {
+
+ protected KeycloakSession kcSession;
+
+ protected Database database;
+ protected JdbcConnection jdbcConnection;
+ protected Connection connection;
+ protected StringBuilder confirmationMessage = new StringBuilder();
+ protected List statements = new ArrayList();
+
+ @Override
+ public ValidationErrors validate(Database database) {
+ return null;
+ }
+
+ @Override
+ public void setFileOpener(ResourceAccessor resourceAccessor) {
+
+ }
+
+ @Override
+ public String getConfirmationMessage() {
+ return confirmationMessage.toString();
+ }
+
+ @Override
+ public void setUp() throws SetupException {
+ this.kcSession = ThreadLocalSessionContext.getCurrentSession();
+ if (this.kcSession == null) {
+ throw new SetupException("No KeycloakSession provided in ThreadLocal");
+ }
+ }
+
+ @Override
+ public SqlStatement[] generateStatements(Database database) throws CustomChangeException {
+ this.database = database;
+ jdbcConnection = (JdbcConnection) database.getConnection();
+ connection = jdbcConnection.getWrappedConnection();
+
+ if (isApplicable()) {
+ confirmationMessage.append(getTaskId() + ": ");
+ generateStatementsImpl();
+ } else {
+ confirmationMessage.append(getTaskId() + ": no update applicable for this task");
+ }
+
+ return statements.toArray(new SqlStatement[statements.size()]);
+ }
+
+ protected boolean isApplicable() throws CustomChangeException {
+ try {
+ String correctedTableName = database.correctObjectName("REALM", Table.class);
+ if (SnapshotGeneratorFactory.getInstance().has(new Table().setName(correctedTableName), database)) {
+ ResultSet resultSet = connection.createStatement().executeQuery("SELECT ID FROM REALM");
+ try {
+ return (resultSet.next());
+ } finally {
+ resultSet.close();
+ }
+ } else {
+ return false;
+ }
+ } catch (Exception e) {
+ throw new CustomChangeException("Failed to check database availability", e);
+ }
+ }
+
+ /**
+ * It's supposed to fill SQL statements to the "statements" variable and fill "confirmationMessage"
+ */
+ protected abstract void generateStatementsImpl() throws CustomChangeException;
+
+ protected abstract String getTaskId();
+}
diff --git a/connections/jpa-liquibase/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/JpaUpdate1_2_0_Beta1.java b/connections/jpa-liquibase/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/JpaUpdate1_2_0_Beta1.java
new file mode 100644
index 0000000000..b28dce5512
--- /dev/null
+++ b/connections/jpa-liquibase/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/JpaUpdate1_2_0_Beta1.java
@@ -0,0 +1,337 @@
+package org.keycloak.connections.jpa.updater.liquibase.custom;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import liquibase.exception.CustomChangeException;
+import liquibase.exception.DatabaseException;
+import liquibase.statement.core.InsertStatement;
+import liquibase.statement.core.UpdateStatement;
+import liquibase.structure.core.Table;
+import org.keycloak.Config;
+import org.keycloak.models.AdminRoles;
+import org.keycloak.models.ClaimMask;
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.services.util.MigrationUtils;
+
+/**
+ * @author Marek Posolda
+ */
+public class JpaUpdate1_2_0_Beta1 extends CustomKeycloakTask {
+
+ private String realmTableName;
+
+ @Override
+ protected String getTaskId() {
+ return "Update 1.2.0.Beta1";
+ }
+
+ @Override
+ protected void generateStatementsImpl() throws CustomChangeException {
+ realmTableName = database.correctObjectName("REALM", Table.class);
+
+ try {
+ convertSocialToIdFedRealms();
+ convertSocialToIdFedUsers();
+ addAccessCodeLoginTimeout();
+ addNewAdminRoles();
+ addDefaultProtocolMappers();
+ } catch (Exception e) {
+ throw new CustomChangeException(getTaskId() + ": Exception when updating data from previous version", e);
+ }
+ }
+
+
+ protected void convertSocialToIdFedRealms() throws SQLException, DatabaseException {
+ String identityProviderTableName = database.correctObjectName("IDENTITY_PROVIDER", Table.class);
+ String idpConfigTableName = database.correctObjectName("IDENTITY_PROVIDER_CONFIG", Table.class);
+
+ PreparedStatement statement = jdbcConnection.prepareStatement("select RSC.NAME, VALUE, REALM_ID, UPDATE_PROFILE_ON_SOC_LOGIN from REALM_SOCIAL_CONFIG RSC,REALM where RSC.REALM_ID = REALM.ID ORDER BY RSC.REALM_ID, RSC.NAME");
+ try {
+ ResultSet resultSet = statement.executeQuery();
+ try {
+ boolean providerInProgress = false;
+ String socialProviderId = null;
+ String clientId = null;
+ String clientSecret;
+ String realmId = null;
+ boolean updateProfileOnSocialLogin = false;
+ boolean first = true;
+
+ while (resultSet.next()) {
+ if (first) {
+ confirmationMessage.append("Migrating social to identity providers: ");
+ first = false;
+ }
+
+ if (!providerInProgress) {
+ String key = resultSet.getString("NAME");
+ int keyIndex = key.indexOf(".key");
+ if (keyIndex == -1) {
+ throw new IllegalStateException("Can't parse the provider from column: " + key);
+ }
+
+ socialProviderId = key.substring(0, keyIndex);
+ clientId = resultSet.getString("VALUE");
+ realmId = resultSet.getString("REALM_ID");
+ updateProfileOnSocialLogin = resultSet.getBoolean("UPDATE_PROFILE_ON_SOC_LOGIN");
+ providerInProgress = true;
+ } else {
+ clientSecret = resultSet.getString("VALUE");
+
+ String internalId = KeycloakModelUtils.generateId();
+ InsertStatement idpInsert = new InsertStatement(null, null, identityProviderTableName)
+ .addColumnValue("INTERNAL_ID", internalId)
+ .addColumnValue("ENABLED", true)
+ .addColumnValue("PROVIDER_ALIAS", socialProviderId)
+ .addColumnValue("PROVIDER_ID", socialProviderId)
+ .addColumnValue("UPDATE_PROFILE_FIRST_LOGIN", updateProfileOnSocialLogin)
+ .addColumnValue("STORE_TOKEN", false)
+ .addColumnValue("AUTHENTICATE_BY_DEFAULT", false)
+ .addColumnValue("REALM_ID", realmId);
+ InsertStatement clientIdInsert = new InsertStatement(null, null, idpConfigTableName)
+ .addColumnValue("IDENTITY_PROVIDER_ID", internalId)
+ .addColumnValue("NAME", "clientId")
+ .addColumnValue("VALUE", clientId);
+ InsertStatement clientSecretInsert = new InsertStatement(null, null, idpConfigTableName)
+ .addColumnValue("IDENTITY_PROVIDER_ID", internalId)
+ .addColumnValue("NAME", "clientSecret")
+ .addColumnValue("VALUE", clientSecret);
+
+ statements.add(idpInsert);
+ statements.add(clientIdInsert);
+ statements.add(clientSecretInsert);
+ confirmationMessage.append(socialProviderId + " in realm " + realmId + ", ");
+
+ providerInProgress = false;
+ }
+ }
+
+ // It means that some provider where processed
+ if (!first) {
+ confirmationMessage.append(". ");
+ }
+ } finally {
+ resultSet.close();
+ }
+ } finally {
+ statement.close();
+ }
+ }
+
+ protected void convertSocialToIdFedUsers() throws SQLException, DatabaseException {
+ String federatedIdentityTableName = database.correctObjectName("FEDERATED_IDENTITY", Table.class);
+ PreparedStatement statement = jdbcConnection.prepareStatement("select REALM_ID, USER_ID, SOCIAL_PROVIDER, SOCIAL_USER_ID, SOCIAL_USERNAME from USER_SOCIAL_LINK");
+ try {
+ ResultSet resultSet = statement.executeQuery();
+ try {
+ int count = 0;
+ while (resultSet.next()) {
+ InsertStatement insert = new InsertStatement(null, null, federatedIdentityTableName)
+ .addColumnValue("REALM_ID", resultSet.getString("REALM_ID"))
+ .addColumnValue("USER_ID", resultSet.getString("USER_ID"))
+ .addColumnValue("IDENTITY_PROVIDER", resultSet.getString("SOCIAL_PROVIDER"))
+ .addColumnValue("FEDERATED_USER_ID", resultSet.getString("SOCIAL_USER_ID"))
+ .addColumnValue("FEDERATED_USERNAME", resultSet.getString("SOCIAL_USERNAME"));
+ count++;
+ statements.add(insert);
+ }
+
+ confirmationMessage.append("Updating " + count + " social links to federated identities. ");
+ } finally {
+ resultSet.close();
+ }
+ } finally {
+ statement.close();
+ }
+ }
+
+ protected void addAccessCodeLoginTimeout() {
+ UpdateStatement statement = new UpdateStatement(null, null, realmTableName)
+ .addNewColumnValue("LOGIN_LIFESPAN", 1800)
+ .setWhereClause("LOGIN_LIFESPAN IS NULL");
+ statements.add(statement);
+
+ confirmationMessage.append("Updated LOGIN_LIFESPAN of all realms to 1800 seconds. ");
+ }
+
+ private void addNewAdminRoles() throws SQLException, DatabaseException{
+ addNewMasterAdminRoles();
+ addNewRealmAdminRoles();
+
+ confirmationMessage.append("Adding new admin roles. ");
+ }
+
+ protected void addNewMasterAdminRoles() throws SQLException, DatabaseException {
+ // Retrieve ID of admin role of master realm
+ String adminRoleId = getAdminRoleId();
+ String masterRealmId = Config.getAdminRealm();
+
+ PreparedStatement statement = jdbcConnection.prepareStatement("select NAME from REALM");
+ try {
+ ResultSet resultSet = statement.executeQuery();
+ try {
+ while (resultSet.next()) {
+ String realmName = resultSet.getString("NAME");
+ String masterAdminAppName = realmName + "-realm";
+
+ PreparedStatement statement2 = jdbcConnection.prepareStatement("select ID from CLIENT where REALM_ID = ? AND NAME = ?");
+ statement2.setString(1, masterRealmId);
+ statement2.setString(2, masterAdminAppName);
+
+ try {
+ ResultSet resultSet2 = statement2.executeQuery();
+ try {
+ if (resultSet2.next()) {
+ String masterAdminAppId = resultSet2.getString("ID");
+
+ addAdminRole(AdminRoles.VIEW_IDENTITY_PROVIDERS, masterRealmId, masterAdminAppId, adminRoleId);
+ addAdminRole(AdminRoles.MANAGE_IDENTITY_PROVIDERS, masterRealmId, masterAdminAppId, adminRoleId);
+ } else {
+ throw new IllegalStateException("Couldn't find ID of '" + masterAdminAppName + "' application in 'master' realm. ");
+ }
+ } finally {
+ resultSet2.close();
+ }
+ } finally {
+ statement2.close();
+ }
+ }
+ } finally {
+ resultSet.close();
+ }
+ } finally {
+ statement.close();
+ }
+ }
+
+ private String getAdminRoleId() throws SQLException, DatabaseException {
+ PreparedStatement statement = jdbcConnection.prepareStatement("select ID from KEYCLOAK_ROLE where NAME = ? AND REALM = ?");
+ statement.setString(1, AdminRoles.ADMIN);
+ statement.setString(2, Config.getAdminRealm());
+
+ try {
+ ResultSet resultSet = statement.executeQuery();
+ try {
+ if (resultSet.next()) {
+ return resultSet.getString("ID");
+ } else {
+ throw new IllegalStateException("Couldn't find ID of 'admin' role in 'master' realm");
+ }
+ } finally {
+ resultSet.close();
+ }
+ } finally {
+ statement.close();
+ }
+ }
+
+
+ protected void addNewRealmAdminRoles() throws SQLException, DatabaseException {
+ PreparedStatement statement = jdbcConnection.prepareStatement("select CLIENT.ID REALM_ADMIN_APP_ID, CLIENT.REALM_ID REALM_ID, KEYCLOAK_ROLE.ID ADMIN_ROLE_ID from CLIENT,KEYCLOAK_ROLE where KEYCLOAK_ROLE.APPLICATION = CLIENT.ID AND CLIENT.NAME = 'realm-management' AND KEYCLOAK_ROLE.NAME = ?");
+ statement.setString(1, AdminRoles.REALM_ADMIN);
+
+ try {
+ ResultSet resultSet = statement.executeQuery();
+ try {
+
+ while (resultSet.next()) {
+ String realmAdminAppId = resultSet.getString("REALM_ADMIN_APP_ID");
+ String realmId = resultSet.getString("REALM_ID");
+ String adminRoleId = resultSet.getString("ADMIN_ROLE_ID");
+
+ addAdminRole(AdminRoles.VIEW_IDENTITY_PROVIDERS, realmId, realmAdminAppId, adminRoleId);
+ addAdminRole(AdminRoles.MANAGE_IDENTITY_PROVIDERS, realmId, realmAdminAppId, adminRoleId);
+ }
+ } finally {
+ resultSet.close();
+ }
+ } finally {
+ statement.close();
+ }
+ }
+
+ private void addAdminRole(String roleName, String realmId, String applicationId, String realmAdminAppRoleId) {
+ String roleTableName = database.correctObjectName("KEYCLOAK_ROLE", Table.class);
+ String compositeRoleTableName = database.correctObjectName("COMPOSITE_ROLE", Table.class);
+ String newRoleId = KeycloakModelUtils.generateId();
+
+ InsertStatement insertRole = new InsertStatement(null, null, roleTableName)
+ .addColumnValue("ID", newRoleId)
+ .addColumnValue("APP_REALM_CONSTRAINT", applicationId)
+ .addColumnValue("APPLICATION_ROLE", true)
+ .addColumnValue("NAME", roleName)
+ .addColumnValue("REALM_ID", realmId)
+ .addColumnValue("APPLICATION", applicationId);
+
+ // Add newly created role to the composite roles of 'realm-admin' role
+ InsertStatement insertCompRole = new InsertStatement(null, null, compositeRoleTableName)
+ .addColumnValue("COMPOSITE", realmAdminAppRoleId)
+ .addColumnValue("CHILD_ROLE", newRoleId);
+
+ statements.add(insertRole);
+ statements.add(insertCompRole);
+ }
+
+ protected void addDefaultProtocolMappers() throws SQLException, DatabaseException {
+ String protocolMapperTableName = database.correctObjectName("PROTOCOL_MAPPER", Table.class);
+ String protocolMapperCfgTableName = database.correctObjectName("PROTOCOL_MAPPER_CONFIG", Table.class);
+
+ PreparedStatement statement = jdbcConnection.prepareStatement("select ID, NAME, ALLOWED_CLAIMS_MASK from CLIENT");
+
+ try {
+ ResultSet resultSet = statement.executeQuery();
+ try {
+ boolean first = true;
+ while (resultSet.next()) {
+ if (first) {
+ confirmationMessage.append("Migrating claimsMask to protocol mappers for clients: ");
+ first = false;
+ }
+
+ Object acmObj = resultSet.getObject("ALLOWED_CLAIMS_MASK");
+ long mask = (acmObj != null) ? (Long) acmObj : ClaimMask.ALL;
+
+ Collection protocolMappers = MigrationUtils.getMappersForClaimMask(this.kcSession, mask);
+ for (ProtocolMapperModel protocolMapper : protocolMappers) {
+ String mapperId = KeycloakModelUtils.generateId();
+
+ InsertStatement insert = new InsertStatement(null, null, protocolMapperTableName)
+ .addColumnValue("ID", mapperId)
+ .addColumnValue("PROTOCOL", protocolMapper.getProtocol())
+ .addColumnValue("NAME", protocolMapper.getName())
+ .addColumnValue("CONSENT_REQUIRED", protocolMapper.isConsentRequired())
+ .addColumnValue("CONSENT_TEXT", protocolMapper.getConsentText())
+ .addColumnValue("PROTOCOL_MAPPER_NAME", protocolMapper.getProtocolMapper())
+ .addColumnValue("CLIENT_ID", resultSet.getString("ID"));
+ statements.add(insert);
+
+ for (Map.Entry cfgEntry : protocolMapper.getConfig().entrySet()) {
+ InsertStatement cfgInsert = new InsertStatement(null, null, protocolMapperCfgTableName)
+ .addColumnValue("PROTOCOL_MAPPER_ID", mapperId)
+ .addColumnValue("NAME", cfgEntry.getKey())
+ .addColumnValue("VALUE", cfgEntry.getValue());
+ statements.add(cfgInsert);
+ }
+
+ }
+
+ confirmationMessage.append(resultSet.getString("NAME") + ", ");
+ }
+
+ // It means that some provider where processed
+ if (!first) {
+ confirmationMessage.append(". ");
+ }
+ } finally {
+ resultSet.close();
+ }
+ } finally {
+ statement.close();
+ }
+ }
+}
diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.Beta1.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.Beta1.xml
index a97473d77a..9459ca3d53 100755
--- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.Beta1.xml
+++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.Beta1.xml
@@ -19,9 +19,6 @@
-
-
-
@@ -153,5 +150,15 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java b/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java
index b15755c444..e66ad65970 100755
--- a/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java
+++ b/connections/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java
@@ -140,7 +140,7 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
}
if (currentVersion == null || !JpaUpdaterProvider.LAST_VERSION.equals(currentVersion)) {
- updater.update(connection);
+ updater.update(session, connection);
} else {
logger.debug("Database is up to date");
}
diff --git a/connections/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java b/connections/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java
index c5dc31f471..f3355e01d8 100755
--- a/connections/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java
+++ b/connections/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java
@@ -1,5 +1,6 @@
package org.keycloak.connections.jpa.updater;
+import org.keycloak.models.KeycloakSession;
import org.keycloak.provider.Provider;
import java.sql.Connection;
@@ -15,7 +16,7 @@ public interface JpaUpdaterProvider extends Provider {
public String getCurrentVersionSql();
- public void update(Connection connection);
+ public void update(KeycloakSession session, Connection connection);
public void validate(Connection connection);
diff --git a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-connections-jpa-liquibase/main/module.xml b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-connections-jpa-liquibase/main/module.xml
index f52bc84788..c5b7d85166 100755
--- a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-connections-jpa-liquibase/main/module.xml
+++ b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-connections-jpa-liquibase/main/module.xml
@@ -10,6 +10,7 @@
+