Merge pull request #760 from stianst/liquibase

KEYCLOAK-736 Database migration support"
This commit is contained in:
Stian Thorgersen 2014-10-14 10:41:48 +02:00
commit d86547620a
42 changed files with 1371 additions and 193 deletions

View file

@ -0,0 +1,74 @@
<?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.1.0-Alpha1-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-connections-jpa-liquibase</artifactId>
<name>Keycloak Connections JPA Liquibase Updater</name>
<description/>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-connections-jpa</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
<exclusions>
<exclusion>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-maven-plugin</artifactId>
<!-- set to ${liquibase.version} once Liquibase 3.2.3 is released (https://liquibase.jira.com/browse/CORE-1987) -->
<version>3.1.1</version>
<configuration>
<changeLogFile>META-INF/jpa-changelog-master.xml</changeLogFile>
<url>${url}</url>
<driver>${driver}</driver>
<username>${username}</username>
<password>${password}</password>
<referenceUrl>${referenceUrl}</referenceUrl>
<referenceDriver>${driver}</referenceDriver>
<referenceUsername>${username}</referenceUsername>
<referencePassword>${password}</referencePassword>
</configuration>
<dependencies>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>${h2.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
<properties>
<username>sa</username>
<password></password>
<driver>org.h2.Driver</driver>
</properties>
</project>

View file

@ -0,0 +1,208 @@
package org.keycloak.connections.jpa.updater.liquibase;
import liquibase.Contexts;
import liquibase.Liquibase;
import liquibase.changelog.ChangeSet;
import liquibase.changelog.DatabaseChangeLog;
import liquibase.changelog.RanChangeSet;
import liquibase.database.Database;
import liquibase.database.DatabaseFactory;
import liquibase.database.core.DB2Database;
import liquibase.database.core.DerbyDatabase;
import liquibase.database.core.FirebirdDatabase;
import liquibase.database.core.H2Database;
import liquibase.database.core.HsqlDatabase;
import liquibase.database.core.InformixDatabase;
import liquibase.database.core.MSSQLDatabase;
import liquibase.database.core.MySQLDatabase;
import liquibase.database.core.OracleDatabase;
import liquibase.database.core.PostgresDatabase;
import liquibase.database.core.SQLiteDatabase;
import liquibase.database.core.SybaseASADatabase;
import liquibase.database.core.SybaseDatabase;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.LiquibaseException;
import liquibase.logging.LogFactory;
import liquibase.logging.LogLevel;
import liquibase.resource.ClassLoaderResourceAccessor;
import liquibase.servicelocator.ServiceLocator;
import org.jboss.logging.Logger;
import org.keycloak.connections.jpa.updater.JpaUpdaterProvider;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
private static final Logger logger = Logger.getLogger(LiquibaseJpaUpdaterProvider.class);
private static final String CHANGELOG = "META-INF/jpa-changelog-master.xml";
@Override
public String getCurrentVersionSql() {
return "SELECT ID from DATABASECHANGELOG ORDER BY DATEEXECUTED DESC LIMIT 1";
}
@Override
public void update(Connection connection) {
logger.debug("Starting database update");
try {
Liquibase liquibase = getLiquibase(connection);
List<ChangeSet> changeSets = liquibase.listUnrunChangeSets((Contexts) null);
if (!changeSets.isEmpty()) {
if (changeSets.get(0).getId().equals(FIRST_VERSION)) {
Statement statement = connection.createStatement();
try {
statement.executeQuery("SELECT id FROM REALM");
logger.infov("Updating database from {0} to {1}", FIRST_VERSION, changeSets.get(changeSets.size() - 1).getId());
liquibase.markNextChangeSetRan((Contexts) null);
} catch (SQLException e) {
logger.info("Initializing database schema");
}
} else {
if (logger.isDebugEnabled()) {
List<RanChangeSet> ranChangeSets = liquibase.getDatabase().getRanChangeSetList();
logger.debugv("Updating database from {0} to {1}", ranChangeSets.get(ranChangeSets.size() - 1).getId(), changeSets.get(changeSets.size() - 1).getId());
} else {
logger.infov("Updating database");
}
}
liquibase.update((Contexts) null);
}
} catch (Exception e) {
throw new RuntimeException("Failed to update database", e);
}
logger.debug("Completed database update");
}
@Override
public void validate(Connection connection) {
try {
Liquibase liquibase = getLiquibase(connection);
liquibase.validate();
} catch (Exception e) {
throw new RuntimeException("Failed to validate database", e);
}
}
private Liquibase getLiquibase(Connection connection) throws Exception {
if (!System.getProperties().containsKey("liquibase.scan.packages")) {
System.setProperty("liquibase.scan.packages", "liquibase.change,liquibase.changelog,liquibase.database,liquibase.parser,liquibase.precondition,liquibase.datatype,liquibase.serializer,liquibase.sqlgenerator,liquibase.executor,liquibase.snapshot,liquibase.logging,liquibase.diff,liquibase.structure,liquibase.structurecompare,liquibase.lockservice");
}
LogFactory.setInstance(new LogWrapper());
Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(connection));
return new Liquibase(CHANGELOG, new ClassLoaderResourceAccessor(getClass().getClassLoader()), database);
}
@Override
public void close() {
}
private static class LogWrapper extends LogFactory {
private liquibase.logging.Logger logger = new liquibase.logging.Logger() {
@Override
public void setName(String name) {
}
@Override
public void setLogLevel(String level) {
}
@Override
public void setLogLevel(LogLevel level) {
}
@Override
public void setLogLevel(String logLevel, String logFile) {
}
@Override
public void severe(String message) {
LiquibaseJpaUpdaterProvider.logger.error(message);
}
@Override
public void severe(String message, Throwable e) {
LiquibaseJpaUpdaterProvider.logger.error(message, e);
}
@Override
public void warning(String message) {
LiquibaseJpaUpdaterProvider.logger.warn(message);
}
@Override
public void warning(String message, Throwable e) {
LiquibaseJpaUpdaterProvider.logger.warn(message, e);
}
@Override
public void info(String message) {
LiquibaseJpaUpdaterProvider.logger.debug(message);
}
@Override
public void info(String message, Throwable e) {
LiquibaseJpaUpdaterProvider.logger.debug(message, e);
}
@Override
public void debug(String message) {
LiquibaseJpaUpdaterProvider.logger.trace(message);
}
@Override
public LogLevel getLogLevel() {
if (LiquibaseJpaUpdaterProvider.logger.isTraceEnabled()) {
return LogLevel.DEBUG;
} else if (LiquibaseJpaUpdaterProvider.logger.isDebugEnabled()) {
return LogLevel.INFO;
} else {
return LogLevel.WARNING;
}
}
@Override
public void debug(String message, Throwable e) {
LiquibaseJpaUpdaterProvider.logger.trace(message, e);
}
@Override
public void setChangeLog(DatabaseChangeLog databaseChangeLog) {
}
@Override
public void setChangeSet(ChangeSet changeSet) {
}
@Override
public int getPriority() {
return 0;
}
};
@Override
public liquibase.logging.Logger getLog(String name) {
return logger;
}
@Override
public liquibase.logging.Logger getLog() {
return logger;
}
}
}

View file

@ -0,0 +1,31 @@
package org.keycloak.connections.jpa.updater.liquibase;
import org.keycloak.Config;
import org.keycloak.connections.jpa.updater.JpaUpdaterProvider;
import org.keycloak.connections.jpa.updater.JpaUpdaterProviderFactory;
import org.keycloak.models.KeycloakSession;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class LiquibaseJpaUpdaterProviderFactory implements JpaUpdaterProviderFactory {
@Override
public JpaUpdaterProvider create(KeycloakSession session) {
return new LiquibaseJpaUpdaterProvider();
}
@Override
public void init(Config.Scope config) {
}
@Override
public void close() {
}
@Override
public String getId() {
return "liquibase";
}
}

View file

@ -0,0 +1,376 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.2.xsd">
<changeSet author="sthorger@redhat.com" id="1.0.0.Final">
<createTable tableName="APPLICATION_DEFAULT_ROLES">
<column name="APPLICATION_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="ROLE_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
</createTable>
<createTable tableName="CLIENT">
<column name="DTYPE" type="VARCHAR(31)">
<constraints nullable="false"/>
</column>
<column name="ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="ALLOWED_CLAIMS_MASK" type="BIGINT(19)"/>
<column name="ENABLED" type="BOOLEAN(1)"/>
<column name="FULL_SCOPE_ALLOWED" type="BOOLEAN(1)"/>
<column name="NAME" type="VARCHAR(255)"/>
<column name="NOT_BEFORE" type="INT(10)"/>
<column name="PUBLIC_CLIENT" type="BOOLEAN(1)"/>
<column name="SECRET" type="VARCHAR(255)"/>
<column name="BASE_URL" type="VARCHAR(255)"/>
<column name="BEARER_ONLY" type="BOOLEAN(1)"/>
<column name="MANAGEMENT_URL" type="VARCHAR(255)"/>
<column name="SURROGATE_AUTH_REQUIRED" type="BOOLEAN(1)"/>
<column name="DIRECT_GRANTS_ONLY" type="BOOLEAN(1)"/>
<column name="REALM_ID" type="VARCHAR(36)"/>
</createTable>
<createTable tableName="CLIENT_SESSION">
<column name="ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="ACTION" type="INT(10)"/>
<column name="CLIENT_ID" type="VARCHAR(36)"/>
<column name="REDIRECT_URI" type="VARCHAR(255)"/>
<column name="STATE" type="VARCHAR(255)"/>
<column name="TIMESTAMP" type="INT(10)"/>
<column name="SESSION_ID" type="VARCHAR(36)"/>
</createTable>
<createTable tableName="CLIENT_SESSION_ROLE">
<column name="ROLE_ID" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="CLIENT_SESSION" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
</createTable>
<createTable tableName="COMPOSITE_ROLE">
<column name="COMPOSITE" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="CHILD_ROLE" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
</createTable>
<createTable tableName="CREDENTIAL">
<column name="ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="DEVICE" type="VARCHAR(255)"/>
<column name="HASH_ITERATIONS" type="INT(10)"/>
<column name="SALT" type="BINARY(255)"/>
<column name="TYPE" type="VARCHAR(255)"/>
<column name="VALUE" type="VARCHAR(255)"/>
<column name="USER_ID" type="VARCHAR(36)"/>
</createTable>
<createTable tableName="EVENT_ENTITY">
<column name="ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="CLIENT_ID" type="VARCHAR(255)"/>
<column name="DETAILS_JSON" type="VARCHAR(2550)"/>
<column name="ERROR" type="VARCHAR(255)"/>
<column name="IP_ADDRESS" type="VARCHAR(255)"/>
<column name="REALM_ID" type="VARCHAR(255)"/>
<column name="SESSION_ID" type="VARCHAR(255)"/>
<column name="TIME" type="BIGINT(19)"/>
<column name="TYPE" type="VARCHAR(255)"/>
<column name="USER_ID" type="VARCHAR(255)"/>
</createTable>
<createTable tableName="FED_PROVIDERS">
<column name="REALM_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="USERFEDERATIONPROVIDERS_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
</createTable>
<createTable tableName="KEYCLOAK_ROLE">
<column name="ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="APP_REALM_CONSTRAINT" type="VARCHAR(36)"/>
<column name="APPLICATION_ROLE" type="BOOLEAN(1)"/>
<column name="DESCRIPTION" type="VARCHAR(255)"/>
<column name="NAME" type="VARCHAR(255)"/>
<column name="REALM_ID" type="VARCHAR(255)"/>
<column name="APPLICATION" type="VARCHAR(36)"/>
<column name="REALM" type="VARCHAR(36)"/>
</createTable>
<createTable tableName="REALM">
<column name="ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="ACCESS_CODE_LIFESPAN" type="INT(10)"/>
<column name="USER_ACTION_LIFESPAN" type="INT(10)"/>
<column name="ACCESS_TOKEN_LIFESPAN" type="INT(10)"/>
<column name="ACCOUNT_THEME" type="VARCHAR(255)"/>
<column name="ADMIN_THEME" type="VARCHAR(255)"/>
<column name="EMAIL_THEME" type="VARCHAR(255)"/>
<column name="ENABLED" type="BOOLEAN(1)"/>
<column name="EVENTS_ENABLED" type="BOOLEAN(1)"/>
<column name="EVENTS_EXPIRATION" type="BIGINT(19)"/>
<column name="LOGIN_THEME" type="VARCHAR(255)"/>
<column name="NAME" type="VARCHAR(255)"/>
<column name="NOT_BEFORE" type="INT(10)"/>
<column name="PASSWORD_CRED_GRANT_ALLOWED" type="BOOLEAN(1)"/>
<column name="PASSWORD_POLICY" type="VARCHAR(255)"/>
<column name="PRIVATE_KEY" type="VARCHAR(2048)"/>
<column name="PUBLIC_KEY" type="VARCHAR(2048)"/>
<column name="REGISTRATION_ALLOWED" type="BOOLEAN(1)"/>
<column name="REMEMBER_ME" type="BOOLEAN(1)"/>
<column name="RESET_PASSWORD_ALLOWED" type="BOOLEAN(1)"/>
<column name="SOCIAL" type="BOOLEAN(1)"/>
<column name="SSL_REQUIRED" type="VARCHAR(255)"/>
<column name="SSO_IDLE_TIMEOUT" type="INT(10)"/>
<column name="SSO_MAX_LIFESPAN" type="INT(10)"/>
<column name="UPDATE_PROFILE_ON_SOC_LOGIN" type="BOOLEAN(1)"/>
<column name="VERIFY_EMAIL" type="BOOLEAN(1)"/>
<column name="MASTER_ADMIN_APP" type="VARCHAR(36)"/>
</createTable>
<createTable tableName="REALM_APPLICATION">
<column name="APPLICATION_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="REALM_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
</createTable>
<createTable tableName="REALM_ATTRIBUTE">
<column name="NAME" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="VALUE" type="VARCHAR(255)"/>
<column name="REALM_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
</createTable>
<createTable tableName="REALM_DEFAULT_ROLES">
<column name="REALM_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="ROLE_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
</createTable>
<createTable tableName="REALM_EVENTS_LISTENERS">
<column name="REALM_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="VALUE" type="VARCHAR(255)"/>
</createTable>
<createTable tableName="REALM_REQUIRED_CREDENTIAL">
<column name="TYPE" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="FORM_LABEL" type="VARCHAR(255)"/>
<column name="INPUT" type="BOOLEAN(1)"/>
<column name="SECRET" type="BOOLEAN(1)"/>
<column name="REALM_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
</createTable>
<createTable tableName="REALM_SMTP_CONFIG">
<column name="REALM_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="VALUE" type="VARCHAR(255)"/>
<column name="NAME" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
</createTable>
<createTable tableName="REALM_SOCIAL_CONFIG">
<column name="REALM_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="VALUE" type="VARCHAR(255)"/>
<column name="NAME" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
</createTable>
<createTable tableName="REDIRECT_URIS">
<column name="CLIENT_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="VALUE" type="VARCHAR(255)"/>
</createTable>
<createTable tableName="SCOPE_MAPPING">
<column name="CLIENT_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="ROLE_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
</createTable>
<createTable tableName="USERNAME_LOGIN_FAILURE">
<column name="REALM_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="USERNAME" type="VARCHAR(200)">
<constraints nullable="false"/>
</column>
<column name="FAILED_LOGIN_NOT_BEFORE" type="INT(10)"/>
<column name="LAST_FAILURE" type="BIGINT(19)"/>
<column name="LAST_IP_FAILURE" type="VARCHAR(255)"/>
<column name="NUM_FAILURES" type="INT(10)"/>
</createTable>
<createTable tableName="USER_ATTRIBUTE">
<column name="NAME" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="VALUE" type="VARCHAR(255)"/>
<column name="USER_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
</createTable>
<createTable tableName="USER_ENTITY">
<column name="ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="EMAIL" type="VARCHAR(255)"/>
<column name="EMAIL_CONSTRAINT" type="VARCHAR(255)"/>
<column name="EMAIL_VERIFIED" type="BOOLEAN(1)"/>
<column name="ENABLED" type="BOOLEAN(1)"/>
<column name="FEDERATION_LINK" type="VARCHAR(255)"/>
<column name="FIRST_NAME" type="VARCHAR(255)"/>
<column name="LAST_NAME" type="VARCHAR(255)"/>
<column name="REALM_ID" type="VARCHAR(255)"/>
<column name="TOTP" type="BOOLEAN(1)"/>
<column name="USERNAME" type="VARCHAR(255)"/>
</createTable>
<createTable tableName="USER_FEDERATION_CONFIG">
<column name="USER_FEDERATION_PROVIDER_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="VALUE" type="VARCHAR(255)"/>
<column name="NAME" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
</createTable>
<createTable tableName="USER_FEDERATION_PROVIDER">
<column name="ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="CHANGED_SYNC_PERIOD" type="INT(10)"/>
<column name="DISPLAY_NAME" type="VARCHAR(255)"/>
<column name="FULL_SYNC_PERIOD" type="INT(10)"/>
<column name="LAST_SYNC" type="INT(10)"/>
<column name="PRIORITY" type="INT(10)"/>
<column name="PROVIDER_NAME" type="VARCHAR(255)"/>
<column name="REALM_ID" type="VARCHAR(36)"/>
</createTable>
<createTable tableName="USER_REQUIRED_ACTION">
<column name="ACTION" type="INT(10)">
<constraints nullable="false"/>
</column>
<column name="USER_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
</createTable>
<createTable tableName="USER_ROLE_MAPPING">
<column name="ROLE_ID" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="USER_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
</createTable>
<createTable tableName="USER_SESSION">
<column name="ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="AUTH_METHOD" type="VARCHAR(255)"/>
<column name="IP_ADDRESS" type="VARCHAR(255)"/>
<column name="LAST_SESSION_REFRESH" type="INT(10)"/>
<column name="LOGIN_USERNAME" type="VARCHAR(255)"/>
<column name="REALM_ID" type="VARCHAR(255)"/>
<column name="REMEMBER_ME" type="BOOLEAN(1)"/>
<column name="STARTED" type="INT(10)"/>
<column name="USER_ID" type="VARCHAR(255)"/>
</createTable>
<createTable tableName="USER_SOCIAL_LINK">
<column name="SOCIAL_PROVIDER" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="REALM_ID" type="VARCHAR(255)"/>
<column name="SOCIAL_USER_ID" type="VARCHAR(255)"/>
<column name="SOCIAL_USERNAME" type="VARCHAR(255)"/>
<column name="USER_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
</createTable>
<createTable tableName="WEB_ORIGINS">
<column name="CLIENT_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="VALUE" type="VARCHAR(255)"/>
</createTable>
<addPrimaryKey columnNames="REALM_ID, NAME" constraintName="CONSTRAINT_1" tableName="REALM_SOCIAL_CONFIG"/>
<addPrimaryKey columnNames="REALM_ID, USERNAME" constraintName="CONSTRAINT_17" tableName="USERNAME_LOGIN_FAILURE"/>
<addPrimaryKey columnNames="ACTION, USER_ID" constraintName="CONSTRAINT_2" tableName="USER_REQUIRED_ACTION"/>
<addPrimaryKey columnNames="SOCIAL_PROVIDER, USER_ID" constraintName="CONSTRAINT_3" tableName="USER_SOCIAL_LINK"/>
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_4" tableName="EVENT_ENTITY"/>
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_4A" tableName="REALM"/>
<addPrimaryKey columnNames="CLIENT_SESSION, ROLE_ID" constraintName="CONSTRAINT_5" tableName="CLIENT_SESSION_ROLE"/>
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_57" tableName="USER_SESSION"/>
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_5C" tableName="USER_FEDERATION_PROVIDER"/>
<addPrimaryKey columnNames="NAME, USER_ID" constraintName="CONSTRAINT_6" tableName="USER_ATTRIBUTE"/>
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_7" tableName="CLIENT"/>
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_8" tableName="CLIENT_SESSION"/>
<addPrimaryKey columnNames="CLIENT_ID, ROLE_ID" constraintName="CONSTRAINT_81" tableName="SCOPE_MAPPING"/>
<addPrimaryKey columnNames="NAME, REALM_ID" constraintName="CONSTRAINT_9" tableName="REALM_ATTRIBUTE"/>
<addPrimaryKey columnNames="REALM_ID, TYPE" constraintName="CONSTRAINT_92" tableName="REALM_REQUIRED_CREDENTIAL"/>
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_A" tableName="KEYCLOAK_ROLE"/>
<addPrimaryKey columnNames="ROLE_ID, USER_ID" constraintName="CONSTRAINT_C" tableName="USER_ROLE_MAPPING"/>
<addPrimaryKey columnNames="REALM_ID, NAME" constraintName="CONSTRAINT_E" tableName="REALM_SMTP_CONFIG"/>
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_F" tableName="CREDENTIAL"/>
<addPrimaryKey columnNames="USER_FEDERATION_PROVIDER_ID, NAME" constraintName="CONSTRAINT_F9" tableName="USER_FEDERATION_CONFIG"/>
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_FB" tableName="USER_ENTITY"/>
<addUniqueConstraint columnNames="ROLE_ID" constraintName="UK_8AELWNIBJI49AVXSRTUF6XJOW" deferrable="false" disabled="false" initiallyDeferred="false" tableName="APPLICATION_DEFAULT_ROLES"/>
<addUniqueConstraint columnNames="REALM_ID,NAME" constraintName="UK_B71CJLBENV945RB6GCON438AT" deferrable="false" disabled="false" initiallyDeferred="false" tableName="CLIENT"/>
<addUniqueConstraint columnNames="USERFEDERATIONPROVIDERS_ID" constraintName="UK_DCCIRJLIPU1478VQC89DID88C" deferrable="false" disabled="false" initiallyDeferred="false" tableName="FED_PROVIDERS"/>
<addUniqueConstraint columnNames="REALM_ID,EMAIL_CONSTRAINT" constraintName="UK_DYKN684SL8UP1CRFEI6ECKHD7" deferrable="false" disabled="false" initiallyDeferred="false" tableName="USER_ENTITY"/>
<addUniqueConstraint columnNames="ROLE_ID" constraintName="UK_H4WPD7W4HSOOLNI3H0SW7BTJE" deferrable="false" disabled="false" initiallyDeferred="false" tableName="REALM_DEFAULT_ROLES"/>
<addUniqueConstraint columnNames="NAME,APP_REALM_CONSTRAINT" constraintName="UK_J3RWUVD56ONTGSUHOGM184WW2" deferrable="false" disabled="false" initiallyDeferred="false" tableName="KEYCLOAK_ROLE"/>
<addUniqueConstraint columnNames="REALM_ID" constraintName="UK_L5QGA3RFME47335JY8JXYXH3I" deferrable="false" disabled="false" initiallyDeferred="false" tableName="REALM_APPLICATION"/>
<addUniqueConstraint columnNames="NAME" constraintName="UK_ORVSDMLA56612EAEFIQ6WL5OI" deferrable="false" disabled="false" initiallyDeferred="false" tableName="REALM"/>
<addUniqueConstraint columnNames="REALM_ID,USERNAME" constraintName="UK_RU8TT6T700S9V50BU18WS5HA6" deferrable="false" disabled="false" initiallyDeferred="false" tableName="USER_ENTITY"/>
<addForeignKeyConstraint baseColumnNames="CLIENT_SESSION" baseTableName="CLIENT_SESSION_ROLE" constraintName="FK_11B7SGQW18I532811V7O2DV76" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="CLIENT_SESSION"/>
<addForeignKeyConstraint baseColumnNames="CLIENT_ID" baseTableName="REDIRECT_URIS" constraintName="FK_1BURS8PB4OUJ97H5WUPPAHV9F" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="CLIENT"/>
<addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="USER_FEDERATION_PROVIDER" constraintName="FK_1FJ32F6PTOLW2QY60CD8N01E8" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="REALM"/>
<addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="FED_PROVIDERS" constraintName="FK_213LYQ09FKXQ8K8NY8DY3737T" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="REALM"/>
<addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="REALM_REQUIRED_CREDENTIAL" constraintName="FK_5HG65LYBEVAVKQFKI3KPONH9V" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="REALM"/>
<addForeignKeyConstraint baseColumnNames="USER_ID" baseTableName="USER_ATTRIBUTE" constraintName="FK_5HRM2VLF9QL5FU043KQEPOVBR" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="USER_ENTITY"/>
<addForeignKeyConstraint baseColumnNames="USER_ID" baseTableName="USER_SOCIAL_LINK" constraintName="FK_68CJYS5UWM55UY823Y75XG4OM" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="USER_ENTITY"/>
<addForeignKeyConstraint baseColumnNames="USER_ID" baseTableName="USER_REQUIRED_ACTION" constraintName="FK_6QJ3W1JW9CVAFHE19BWSIUVMD" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="USER_ENTITY"/>
<addForeignKeyConstraint baseColumnNames="REALM" baseTableName="KEYCLOAK_ROLE" constraintName="FK_6VYQFE4CN4WLQ8R6KT5VDSJ5C" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="REALM"/>
<addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="REALM_SMTP_CONFIG" constraintName="FK_70EJ8XDXGXD0B9HH6180IRR0O" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="REALM"/>
<addForeignKeyConstraint baseColumnNames="APPLICATION_ID" baseTableName="REALM_APPLICATION" constraintName="FK_71S3P0DIUXAWWQQSA528UBY2Q" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="REALM"/>
<addForeignKeyConstraint baseColumnNames="ROLE_ID" baseTableName="APPLICATION_DEFAULT_ROLES" constraintName="FK_8AELWNIBJI49AVXSRTUF6XJOW" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="KEYCLOAK_ROLE"/>
<addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="REALM_ATTRIBUTE" constraintName="FK_8SHXD6L3E9ATQUKACXGPFFPTW" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="REALM"/>
<addForeignKeyConstraint baseColumnNames="COMPOSITE" baseTableName="COMPOSITE_ROLE" constraintName="FK_A63WVEKFTU8JO1PNJ81E7MCE2" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="KEYCLOAK_ROLE"/>
<addForeignKeyConstraint baseColumnNames="SESSION_ID" baseTableName="CLIENT_SESSION" constraintName="FK_B4AO2VCVAT6UKAU74WBWTFQO1" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="USER_SESSION"/>
<addForeignKeyConstraint baseColumnNames="USER_ID" baseTableName="USER_ROLE_MAPPING" constraintName="FK_C4FQV34P1MBYLLOXANG7B1Q3L" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="USER_ENTITY"/>
<addForeignKeyConstraint baseColumnNames="USERFEDERATIONPROVIDERS_ID" baseTableName="FED_PROVIDERS" constraintName="FK_DCCIRJLIPU1478VQC89DID88C" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="USER_FEDERATION_PROVIDER"/>
<addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="REALM_DEFAULT_ROLES" constraintName="FK_EVUDB1PPW84OXFAX2DRS03ICC" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="REALM"/>
<addForeignKeyConstraint baseColumnNames="CHILD_ROLE" baseTableName="COMPOSITE_ROLE" constraintName="FK_GR7THLLB9LU8Q4VQA4524JJY8" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="KEYCLOAK_ROLE"/>
<addForeignKeyConstraint baseColumnNames="ROLE_ID" baseTableName="REALM_DEFAULT_ROLES" constraintName="FK_H4WPD7W4HSOOLNI3H0SW7BTJE" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="KEYCLOAK_ROLE"/>
<addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="REALM_EVENTS_LISTENERS" constraintName="FK_H846O4H0W8EPX5NXEV9F5Y69J" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="REALM"/>
<addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="REALM_APPLICATION" constraintName="FK_L5QGA3RFME47335JY8JXYXH3I" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="CLIENT"/>
<addForeignKeyConstraint baseColumnNames="CLIENT_ID" baseTableName="WEB_ORIGINS" constraintName="FK_LOJPHO213XCX4WNKOG82SSRFY" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="CLIENT"/>
<addForeignKeyConstraint baseColumnNames="APPLICATION_ID" baseTableName="APPLICATION_DEFAULT_ROLES" constraintName="FK_MAYLTS7KLWQW2H8M2B5JOYTKY" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="CLIENT"/>
<addForeignKeyConstraint baseColumnNames="CLIENT_ID" baseTableName="SCOPE_MAPPING" constraintName="FK_OUSE064PLMLR732LXJCN1Q5F1" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="CLIENT"/>
<addForeignKeyConstraint baseColumnNames="ROLE_ID" baseTableName="SCOPE_MAPPING" constraintName="FK_P3RH9GRKU11KQFRS4FLTT7RNQ" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="KEYCLOAK_ROLE"/>
<addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="CLIENT" constraintName="FK_P56CTINXXB9GSK57FO49F9TAC" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="REALM"/>
<addForeignKeyConstraint baseColumnNames="USER_ID" baseTableName="CREDENTIAL" constraintName="FK_PFYR0GLASQYL0DEI3KL69R6V0" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="USER_ENTITY"/>
<addForeignKeyConstraint baseColumnNames="APPLICATION" baseTableName="KEYCLOAK_ROLE" constraintName="FK_PIMO5LE2C0RAL09FL8CM9WFW9" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="CLIENT"/>
<addForeignKeyConstraint baseColumnNames="MASTER_ADMIN_APP" baseTableName="REALM" constraintName="FK_RSAF444KK6QRKMS7N56AIWQ5Y" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="CLIENT"/>
<addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="REALM_SOCIAL_CONFIG" constraintName="FK_SV5I3C2TI7G0G922FGE683SOV" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="REALM"/>
<addForeignKeyConstraint baseColumnNames="USER_FEDERATION_PROVIDER_ID" baseTableName="USER_FEDERATION_CONFIG" constraintName="FK_T13HPU1J94R2EBPEKR39X5EU5" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="USER_FEDERATION_PROVIDER"/>
</changeSet>
</databaseChangeLog>

View file

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.2.xsd">
<changeSet author="sthorger@redhat.com" id="1.1.0.Beta1">
<delete tableName="CLIENT_SESSION_ROLE"/>
<delete tableName="CLIENT_SESSION"/>
<delete tableName="USER_SESSION"/>
<createTable tableName="CLIENT_ATTRIBUTES">
<column name="CLIENT_ID" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
<column name="VALUE" type="VARCHAR(2048)"/>
<column name="NAME" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
</createTable>
<createTable tableName="CLIENT_SESSION_NOTE">
<column name="NAME" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="VALUE" type="VARCHAR(255)"/>
<column name="CLIENT_SESSION" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
</createTable>
<addColumn tableName="CLIENT_SESSION">
<column name="AUTH_METHOD" type="VARCHAR(255)"/>
</addColumn>
<addColumn tableName="CLIENT">
<column name="PROTOCOL" type="VARCHAR(255)"/>
</addColumn>
<addColumn tableName="CLIENT_SESSION">
<column name="REALM_ID" type="VARCHAR(255)"/>
</addColumn>
<addPrimaryKey columnNames="CLIENT_ID, NAME" constraintName="CONSTRAINT_3C" tableName="CLIENT_ATTRIBUTES"/>
<addPrimaryKey columnNames="CLIENT_SESSION, NAME" constraintName="CONSTRAINT_5E" tableName="CLIENT_SESSION_NOTE"/>
<addForeignKeyConstraint baseColumnNames="CLIENT_ID" baseTableName="CLIENT_ATTRIBUTES" constraintName="FK3C47C64BEACCA966" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="CLIENT"/>
<addForeignKeyConstraint baseColumnNames="CLIENT_SESSION" baseTableName="CLIENT_SESSION_NOTE" constraintName="FK5EDFB00FF51C2736" deferrable="false" initiallyDeferred="false" onDelete="RESTRICT" onUpdate="RESTRICT" referencedColumnNames="ID" referencedTableName="CLIENT_SESSION"/>
</changeSet>
</databaseChangeLog>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.2.xsd">
<include file="META-INF/jpa-changelog-1.0.0.Final.xml"/>
<include file="META-INF/jpa-changelog-1.1.0.Beta1.xml"/>
</databaseChangeLog>

View file

@ -0,0 +1 @@
org.keycloak.connections.jpa.updater.liquibase.LiquibaseJpaUpdaterProviderFactory

View file

@ -1,12 +1,20 @@
package org.keycloak.connections.jpa;
import org.hibernate.ejb.AvailableSettings;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.connections.jpa.updater.JpaUpdaterProvider;
import org.keycloak.models.KeycloakSession;
import javax.naming.InitialContext;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
@ -15,13 +23,15 @@ import java.util.Map;
*/
public class DefaultJpaConnectionProviderFactory implements JpaConnectionProviderFactory {
private static final Logger logger = Logger.getLogger(DefaultJpaConnectionProviderFactory.class);
private volatile EntityManagerFactory emf;
private Config.Scope config;
@Override
public JpaConnectionProvider create(KeycloakSession session) {
lazyInit();
lazyInit(session);
EntityManager em = emf.createEntityManager();
em = PersistenceExceptionConverter.create(em);
@ -46,11 +56,17 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
this.config = config;
}
private void lazyInit() {
private void lazyInit(KeycloakSession session) {
if (emf == null) {
synchronized (this) {
if (emf == null) {
logger.debug("Initializing JPA connections");
Connection connection = null;
String unitName = config.get("unitName");
String databaseSchema = config.get("databaseSchema");
Map<String, Object> properties = new HashMap<String, Object>();
// Only load config from keycloak-server.json if unitName is not specified
@ -83,19 +99,80 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
properties.put("hibernate.dialect", driverDialect);
}
String databaseSchema = config.get("databaseSchema", "validate");
if (databaseSchema != null) {
properties.put("hibernate.hbm2ddl.auto", databaseSchema);
if (databaseSchema.equals("development-update")) {
properties.put("hibernate.hbm2ddl.auto", "update");
databaseSchema = null;
} else if (databaseSchema.equals("development-validate")) {
properties.put("hibernate.hbm2ddl.auto", "validate");
databaseSchema = null;
}
}
properties.put("hibernate.show_sql", config.getBoolean("showSql", false));
properties.put("hibernate.format_sql", config.getBoolean("formatSql", true));
}
if (databaseSchema != null) {
logger.trace("Updating database");
JpaUpdaterProvider updater = session.getProvider(JpaUpdaterProvider.class);
connection = getConnection();
if (databaseSchema.equals("update")) {
String currentVersion = null;
try {
ResultSet resultSet = connection.createStatement().executeQuery(updater.getCurrentVersionSql());
if (resultSet.next()) {
currentVersion = resultSet.getString(1);
}
} catch (SQLException e) {
}
if (currentVersion == null || !JpaUpdaterProvider.LAST_VERSION.equals(currentVersion)) {
updater.update(connection);
} else {
logger.debug("Database is up to date");
}
} else if (databaseSchema.equals("validate")) {
updater.validate(connection);
} else {
throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema);
}
logger.trace("Database update completed");
}
logger.trace("Creating EntityManagerFactory");
emf = Persistence.createEntityManagerFactory(unitName, properties);
logger.trace("EntityManagerFactory created");
// Close after creating EntityManagerFactory to prevent in-mem databases from closing
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
logger.warn(e);
}
}
}
}
}
}
private Connection getConnection() {
try {
String dataSourceLookup = config.get("dataSource");
if (dataSourceLookup != null) {
DataSource dataSource = (DataSource) new InitialContext().lookup(dataSourceLookup);
return dataSource.getConnection();
} else {
Class.forName(config.get("driver"));
return DriverManager.getConnection(config.get("url"), config.get("user"), config.get("password"));
}
} catch (Exception e) {
throw new RuntimeException("Failed to connect to database", e);
}
}
}

View file

@ -0,0 +1,22 @@
package org.keycloak.connections.jpa.updater;
import org.keycloak.provider.Provider;
import java.sql.Connection;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface JpaUpdaterProvider extends Provider {
public String FIRST_VERSION = "1.0.0.Final";
public String LAST_VERSION = "1.1.0.Beta1";
public String getCurrentVersionSql();
public void update(Connection connection);
public void validate(Connection connection);
}

View file

@ -0,0 +1,9 @@
package org.keycloak.connections.jpa.updater;
import org.keycloak.provider.ProviderFactory;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface JpaUpdaterProviderFactory extends ProviderFactory<JpaUpdaterProvider> {
}

View file

@ -0,0 +1,27 @@
package org.keycloak.connections.jpa.updater;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class JpaUpdaterSpi implements Spi {
@Override
public String getName() {
return "connectionsJpaUpdater";
}
@Override
public Class<? extends Provider> getProviderClass() {
return JpaUpdaterProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return JpaUpdaterProviderFactory.class;
}
}

View file

@ -1 +1,2 @@
org.keycloak.connections.jpa.JpaConnectionSpi
org.keycloak.connections.jpa.JpaConnectionSpi
org.keycloak.connections.jpa.updater.JpaUpdaterSpi

View file

@ -9,6 +9,7 @@ import org.keycloak.Config;
import org.keycloak.connections.mongo.api.MongoStore;
import org.keycloak.connections.mongo.impl.MongoStoreImpl;
import org.keycloak.connections.mongo.impl.context.TransactionMongoStoreInvocationContext;
import org.keycloak.connections.mongo.updater.DefaultMongoUpdaterProvider;
import org.keycloak.models.KeycloakSession;
import java.util.Collections;
@ -64,7 +65,6 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
String host = config.get("host", ServerAddress.defaultHost());
int port = config.getInt("port", ServerAddress.defaultPort());
String dbName = config.get("db", "keycloak");
boolean clearOnStartup = config.getBoolean("clearOnStartup", false);
String user = config.get("user");
String password = config.get("password");
@ -77,9 +77,18 @@ public class DefaultMongoConnectionFactoryProvider implements MongoConnectionPro
this.db = client.getDB(dbName);
this.mongoStore = new MongoStoreImpl(db, clearOnStartup, getManagedEntities());
String databaseSchema = config.get("databaseSchema");
if (databaseSchema != null) {
if (databaseSchema.equals("update")) {
new DefaultMongoUpdaterProvider().update(db);
} else {
throw new RuntimeException("Invalid value for databaseSchema: " + databaseSchema);
}
}
logger.infof("Initialized mongo model. host: %s, port: %d, db: %s, clearOnStartup: %b", host, port, dbName, clearOnStartup);
this.mongoStore = new MongoStoreImpl(db, getManagedEntities());
logger.debugv("Initialized mongo model. host: %s, port: %d, db: %s", host, port, dbName);
} catch (Exception e) {
throw new RuntimeException(e);
}

View file

@ -1,26 +0,0 @@
package org.keycloak.connections.mongo.api;
import java.lang.annotation.Documented;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@Target({TYPE})
@Documented
@Retention(RUNTIME)
@Inherited
public @interface MongoIndex {
String[] fields();
boolean unique() default false;
boolean sparse() default false;
}

View file

@ -1,22 +0,0 @@
package org.keycloak.connections.mongo.api;
import java.lang.annotation.Documented;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
@Target({TYPE})
@Documented
@Retention(RUNTIME)
@Inherited
public @interface MongoIndexes {
MongoIndex[] value();
}

View file

@ -11,8 +11,6 @@ import org.jboss.logging.Logger;
import org.keycloak.connections.mongo.api.MongoCollection;
import org.keycloak.connections.mongo.api.MongoEntity;
import org.keycloak.connections.mongo.api.MongoIdentifiableEntity;
import org.keycloak.connections.mongo.api.MongoIndex;
import org.keycloak.connections.mongo.api.MongoIndexes;
import org.keycloak.connections.mongo.api.MongoStore;
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.connections.mongo.api.context.MongoTask;
@ -57,7 +55,7 @@ public class MongoStoreImpl implements MongoStore {
new ConcurrentHashMap<Class<?>, EntityInfo>();
public MongoStoreImpl(DB database, boolean clearCollectionsOnStartup, Class<?>[] managedEntityTypes) {
public MongoStoreImpl(DB database, Class<?>[] managedEntityTypes) {
this.database = database;
mapperRegistry = new MapperRegistry();
@ -86,13 +84,6 @@ public class MongoStoreImpl implements MongoStore {
mapperRegistry.addAppObjectMapper(new MongoEntityMapper(this, mapperRegistry, type));
mapperRegistry.addDBObjectMapper(new BasicDBObjectMapper(this, mapperRegistry, type));
}
if (clearCollectionsOnStartup) {
// dropDatabase();
clearManagedCollections(managedEntityTypes);
}
initManagedCollections(managedEntityTypes);
}
protected void dropDatabase() {
@ -100,63 +91,6 @@ public class MongoStoreImpl implements MongoStore {
logger.info("Database " + this.database.getName() + " dropped in MongoDB");
}
// Don't drop database, but just clear all data in managed collections (useful for export/import or during development)
protected void clearManagedCollections(Class<?>[] managedEntityTypes) {
for (Class<?> clazz : managedEntityTypes) {
DBCollection dbCollection = getDBCollectionForType(clazz);
if (dbCollection != null) {
dbCollection.remove(new BasicDBObject());
logger.debug("Collection " + dbCollection.getName() + " cleared from " + this.database.getName());
}
}
}
protected void initManagedCollections(Class<?>[] managedEntityTypes) {
for (Class<?> clazz : managedEntityTypes) {
EntityInfo entityInfo = getEntityInfo(clazz);
String dbCollectionName = entityInfo.getDbCollectionName();
if (dbCollectionName != null && !database.collectionExists(dbCollectionName)) {
DBCollection dbCollection = database.getCollection(dbCollectionName);
logger.debug("Created collection " + dbCollection.getName() + " in " + this.database.getName());
MongoIndex index = clazz.getAnnotation(MongoIndex.class);
if (index != null) {
createIndex(dbCollection, index);
}
MongoIndexes indexes = clazz.getAnnotation(MongoIndexes.class);
if (indexes != null) {
for (MongoIndex i : indexes.value()) {
createIndex(dbCollection, i);
}
}
}
}
}
protected void createIndex(DBCollection dbCollection, MongoIndex index) {
BasicDBObject fields = new BasicDBObject();
for (String f : index.fields()) {
fields.put(f, 1);
}
boolean unique = index.unique();
boolean sparse = index.sparse();
BasicDBObject options = new BasicDBObject();
if (unique) {
options.put("unique", unique);
}
if (sparse) {
options.put("sparse", sparse);
}
dbCollection.ensureIndex(fields, options);
logger.debug("Created index " + fields + "(options: " + options + ") on " + dbCollection.getName() + " in " + this.database.getName());
}
@Override
public void insertEntity(MongoIdentifiableEntity entity, MongoStoreInvocationContext context) {
Class<? extends MongoEntity> clazz = entity.getClass();

View file

@ -0,0 +1,103 @@
package org.keycloak.connections.mongo.updater;
import com.mongodb.BasicDBObject;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;
import org.jboss.logging.Logger;
import org.keycloak.connections.mongo.updater.updates.Update;
import org.keycloak.connections.mongo.updater.updates.Update1_0_0_Final;
import org.keycloak.connections.mongo.updater.updates.Update1_1_0_Beta1;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class DefaultMongoUpdaterProvider implements MongoUpdaterProvider {
public static final Logger log = Logger.getLogger(DefaultMongoUpdaterProvider.class);
public static final String CHANGE_LOG_COLLECTION = "databaseChangeLog";
private Class<? extends Update>[] updates = new Class[]{
Update1_0_0_Final.class,
Update1_1_0_Beta1.class
};
@Override
public void update(DB db) {
log.debug("Starting database update");
try {
boolean changeLogExists = db.collectionExists(CHANGE_LOG_COLLECTION);
boolean realmExists = db.collectionExists("realms");
DBCollection changeLog = db.getCollection(CHANGE_LOG_COLLECTION);
List<String> executed = new LinkedList<String>();
if (!changeLogExists && realmExists) {
Update1_0_0_Final u = new Update1_0_0_Final();
executed.add(u.getId());
createLog(changeLog, u, 1);
} else if (changeLogExists) {
DBCursor cursor = changeLog.find().sort(new BasicDBObject("orderExecuted", 1));
while (cursor.hasNext()) {
executed.add((String) cursor.next().get("_id"));
}
}
List<Update> updatesToRun = new LinkedList<Update>();
for (Class<? extends Update> updateClass : updates) {
Update u = updateClass.newInstance();
if (!executed.contains(u.getId())) {
updatesToRun.add(u);
}
}
if (!updatesToRun.isEmpty()) {
if (executed.isEmpty()) {
log.info("Initializing database schema");
} else {
if (log.isDebugEnabled()) {
log.infov("Updating database from {0} to {1}", executed.get(executed.size() - 1), updatesToRun.get(updatesToRun.size() - 1).getId());
} else {
log.debugv("Updating database");
}
}
int order = executed.size();
for (Update u : updatesToRun) {
log.debugv("Executing updates for {0}", u.getId());
u.setLog(log);
u.setDb(db);
u.update();
createLog(changeLog, u, ++order);
log.debugv("Completed updates for {0}", u.getId());
}
}
} catch (Exception e) {
throw new RuntimeException("Failed to update database", e);
}
log.debug("Completed database update");
}
private void createLog(DBCollection changeLog, Update update, int orderExecuted) {
changeLog.insert(new BasicDBObject("_id", update.getId()).append("dateExecuted", new Date()).append("orderExecuted", orderExecuted));
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,29 @@
package org.keycloak.connections.mongo.updater;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class DefaultMongoUpdaterProviderFactory implements MongoUpdaterProviderFactory {
@Override
public MongoUpdaterProvider create(KeycloakSession session) {
return new DefaultMongoUpdaterProvider();
}
@Override
public void init(Config.Scope config) {
}
@Override
public void close() {
}
@Override
public String getId() {
return "default";
}
}

View file

@ -0,0 +1,13 @@
package org.keycloak.connections.mongo.updater;
import com.mongodb.DB;
import org.keycloak.provider.Provider;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface MongoUpdaterProvider extends Provider {
public void update(DB db);
}

View file

@ -0,0 +1,9 @@
package org.keycloak.connections.mongo.updater;
import org.keycloak.provider.ProviderFactory;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public interface MongoUpdaterProviderFactory extends ProviderFactory<MongoUpdaterProvider> {
}

View file

@ -0,0 +1,27 @@
package org.keycloak.connections.mongo.updater;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class MongoUpdaterSpi implements Spi {
@Override
public String getName() {
return "connectionsMongoUpdater";
}
@Override
public Class<? extends Provider> getProviderClass() {
return MongoUpdaterProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return MongoUpdaterProviderFactory.class;
}
}

View file

@ -0,0 +1,62 @@
package org.keycloak.connections.mongo.updater.updates;
import com.mongodb.BasicDBObject;
import com.mongodb.DB;
import com.mongodb.DBCollection;
import org.jboss.logging.Logger;
import java.util.Arrays;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public abstract class Update {
protected DB db;
protected Logger log;
public abstract String getId();
public abstract void update() throws ClassNotFoundException;
protected DBCollection createCollection(String name) {
if (db.collectionExists(name)) {
throw new RuntimeException("Failed to create collection {0}: collection already exists");
}
DBCollection col = db.getCollection(name);
log.debugv("Created collection {0}", name);
return col;
}
protected void ensureIndex(String name, String field, boolean unique, boolean sparse) {
ensureIndex(name, new String[]{field}, unique, sparse);
}
protected void ensureIndex(String name, String[] fields, boolean unique, boolean sparse) {
DBCollection col = db.getCollection(name);
BasicDBObject o = new BasicDBObject();
for (String f : fields) {
o.append(f, 1);
}
col.ensureIndex(o, new BasicDBObject("unique", unique).append("sparse", sparse));
log.debugv("Created index {0}, fields={1}, unique={2}, sparse={3}", name, Arrays.toString(fields), unique, sparse);
}
protected void deleteEntries(String collection) {
db.getCollection(collection).remove(new BasicDBObject());
log.debugv("Deleted entries from {0}", collection);
}
public void setLog(Logger log) {
this.log = log;
}
public void setDb(DB db) {
this.db = db;
}
}

View file

@ -0,0 +1,44 @@
package org.keycloak.connections.mongo.updater.updates;
import com.mongodb.BasicDBObject;
import com.mongodb.DBCollection;
import org.keycloak.connections.mongo.updater.DefaultMongoUpdaterProvider;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class Update1_0_0_Final extends Update {
@Override
public String getId() {
return "1.0.0.Final";
}
@Override
public void update() throws ClassNotFoundException {
DBCollection realmsCollection = db.getCollection("realms");
realmsCollection.ensureIndex(new BasicDBObject("name", 1), new BasicDBObject("unique", true));
DefaultMongoUpdaterProvider.log.debugv("Created collection {0}", "realms");
createCollection("users");
ensureIndex("users", new String[] { "realmId", "username"}, true, false);
ensureIndex("users", "emailIndex", true, true);
createCollection("roles");
ensureIndex("roles", "nameIndex", true, false);
createCollection("applications");
ensureIndex("applications", new String[]{"realmId", "name"}, true, false);
createCollection("oauthClients");
ensureIndex("oauthClients", new String[] { "realmId", "name"}, true, false);
createCollection("userFailures");
createCollection("sessions");
createCollection("clientSessions");
}
}

View file

@ -0,0 +1,19 @@
package org.keycloak.connections.mongo.updater.updates;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class Update1_1_0_Beta1 extends Update {
@Override
public String getId() {
return "1.1.0.Beta1";
}
@Override
public void update() {
deleteEntries("clientSessions");
deleteEntries("sessions");
}
}

View file

@ -14,6 +14,7 @@
<modules>
<module>jpa</module>
<module>jpa-liquibase</module>
<module>infinispan</module>
<module>mongo</module>
</modules>

View file

@ -26,6 +26,11 @@
<artifactId>keycloak-connections-jpa</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-connections-jpa-liquibase</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-connections-infinispan</artifactId>
@ -180,6 +185,17 @@
<groupId>de.idyl</groupId>
<artifactId>winzipaes</artifactId>
</dependency>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
<exclusions>
<exclusion>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
</project>

View file

@ -0,0 +1,79 @@
Updating Database Schema
========================
Keycloak supports automatically migrating the database to a new version. This is done by applying one or more change-sets
to the existing database. This means if you need to do any changes to database schemas for JPA or Mongo you need to create
a change-set that can transform the schema as well as any existing data.
This includes changes to:
* Realm entities
* User entities
* User session entities
* Event entities
Creating a JPA change-set
-------------------------
We use Liquibase to support updating the database. The change-sets are located in
`connections/jpa-liquibase/src/main/resources/META-INF`. There's a separate file for each release that requires database
changes.
To manually create a change-set add a new file in the above location with the name `jpa-changelog-<version>.xml`. This file
should contain a single `change-set` with `id` equal to the next version to be released and `author` set to your email
address. Then look at Liquibase documentation on how to write this file. Add a reference to this file in `jpa-changelog-master.xml`.
The file should have a single change-set and the id of the change-set should be the next version to be released.
You also need to update `org.keycloak.connections.jpa.updater.JpaUpdaterProvider#LAST_VERSION`. This
is used by Keycloak to quickly determine if the database is up to date or not.
You can also have Liquibase and Hibernate create one for you. To do this follow these steps:
1. Delete existing databases
`rm keycloak*h2.db`
2. Create a database of the old format:
`mvn -f connections/jpa-liquibase/pom.xml liquibase:update -Durl=jdbc:h2:keycloak`
3. Make a copy of the database:
`cp keycloak.h2.db keycloak-old.h2.db`
3. Run KeycloakServer to make Hibernate update the schema:
`mvn -f testsuite/integration exec:java -Pkeycloak-server -Dkeycloak.connectionsJpa.url='jdbc:h2:keycloak' -Dkeycloak.connectionsJpa.databaseSchema='development-update'`
4. Wait until server is completely started, then stop it
5. View the difference:
`mvn -f connections/jpa-liquibase/pom.xml liquibase:diff -Durl=jdbc:h2:keycloak-old -DreferenceUrl=jdbc:h2:keycloak`
6. Create a change-set file:
`mvn -f connections/jpa-liquibase/pom.xml liquibase:diff -Durl=jdbc:h2:keycloak-old -DreferenceUrl=jdbc:h2:keycloak -Dliquibase.diffChangeLogFile=changelog.xml`
This will generate the file `changelog.xml`. Once it's generated edit the file and combine all `change-sets` into
a single `change-set` and change the `id` to the next version to be released and `author` to your email address. Then
follow the steps above to copy it to the correct location and update `jpa-changelog-master.xml`. You have to manually
add entries to the `change-set` to update existing data if required.
When you have update the change-set Hibernate can validate the schema for you. First run:
rm -rf keycloak*h2.db
mvn -f testsuite/integration exec:java -Pkeycloak-server -Dkeycloak.connectionsJpa.url='jdbc:h2:keycloak' -Dkeycloak.connectionsJpa.databaseSchema='update'
Once the server has started fully, stop it and run:
mvn -f testsuite/integration exec:java -Pkeycloak-server -Dkeycloak.connectionsJpa.url='jdbc:h2:keycloak' -Dkeycloak.connectionsJpa.databaseSchema='development-validate'
Creating a Mongo change-set
---------------------------
As Mongo is schema-less it's significantly easier to create a change-set. You only need to create/delete collections as
needed, as well as update any indexes. You will also need to update existing data if required.
Mongo change-sets are written in Java and are located in the `connections/mongo` module, to add a new change-set create
a new class that implements `org.keycloak.connections.mongo.updater.updates.Update` the name of the class should be
`Update<version>` with `.` replaced with `_`.
You also need to add a reference to this file in `org.keycloak.connections.mongo.updater.DefaultMongoUpdaterProvider`.
It should be added last to the `DefaultMongoUpdaterProvider#updates` array.
Testing database migration
--------------------------
Get the database from an old version of Keycloak that includes the demo applications. Start the server with this and test it.

View file

@ -4,7 +4,6 @@ import com.mongodb.DBObject;
import com.mongodb.QueryBuilder;
import org.keycloak.connections.mongo.api.MongoCollection;
import org.keycloak.connections.mongo.api.MongoIdentifiableEntity;
import org.keycloak.connections.mongo.api.MongoIndex;
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.models.entities.ApplicationEntity;
@ -12,7 +11,6 @@ import org.keycloak.models.entities.ApplicationEntity;
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@MongoCollection(collectionName = "applications")
@MongoIndex(fields = { "realmId", "name" }, unique = true)
public class MongoApplicationEntity extends ApplicationEntity implements MongoIdentifiableEntity {
@Override

View file

@ -4,7 +4,6 @@ import com.mongodb.DBObject;
import com.mongodb.QueryBuilder;
import org.keycloak.connections.mongo.api.MongoCollection;
import org.keycloak.connections.mongo.api.MongoIdentifiableEntity;
import org.keycloak.connections.mongo.api.MongoIndex;
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.models.entities.OAuthClientEntity;
@ -12,7 +11,6 @@ import org.keycloak.models.entities.OAuthClientEntity;
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@MongoCollection(collectionName = "oauthClients")
@MongoIndex(fields = { "realmId", "name" }, unique = true)
public class MongoOAuthClientEntity extends OAuthClientEntity implements MongoIdentifiableEntity {
@Override

View file

@ -4,7 +4,6 @@ import com.mongodb.DBObject;
import com.mongodb.QueryBuilder;
import org.keycloak.connections.mongo.api.MongoCollection;
import org.keycloak.connections.mongo.api.MongoIdentifiableEntity;
import org.keycloak.connections.mongo.api.MongoIndex;
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.models.entities.RealmEntity;
@ -12,7 +11,6 @@ import org.keycloak.models.entities.RealmEntity;
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@MongoCollection(collectionName = "realms")
@MongoIndex(fields = { "name" }, unique = true)
public class MongoRealmEntity extends RealmEntity implements MongoIdentifiableEntity {
@Override

View file

@ -6,7 +6,6 @@ import org.jboss.logging.Logger;
import org.keycloak.connections.mongo.api.MongoCollection;
import org.keycloak.connections.mongo.api.MongoField;
import org.keycloak.connections.mongo.api.MongoIdentifiableEntity;
import org.keycloak.connections.mongo.api.MongoIndex;
import org.keycloak.connections.mongo.api.MongoStore;
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.models.entities.RoleEntity;
@ -17,7 +16,6 @@ import java.util.List;
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@MongoCollection(collectionName = "roles")
@MongoIndex(fields = "nameIndex", unique = true)
public class MongoRoleEntity extends RoleEntity implements MongoIdentifiableEntity {
private static final Logger logger = Logger.getLogger(MongoRoleEntity.class);

View file

@ -2,8 +2,6 @@ package org.keycloak.models.mongo.keycloak.entities;
import org.keycloak.connections.mongo.api.MongoCollection;
import org.keycloak.connections.mongo.api.MongoIdentifiableEntity;
import org.keycloak.connections.mongo.api.MongoIndex;
import org.keycloak.connections.mongo.api.MongoIndexes;
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.models.entities.UserEntity;
@ -11,13 +9,8 @@ import org.keycloak.models.entities.UserEntity;
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@MongoCollection(collectionName = "users")
@MongoIndexes({
@MongoIndex(fields = { "realmId", "username" }, unique = true),
@MongoIndex(fields = { "emailIndex" }, unique = true, sparse = true),
})
public class MongoUserEntity extends UserEntity implements MongoIdentifiableEntity {
public String getEmailIndex() {
return getEmail() != null ? getRealmId() + "//" + getEmail() : null;
}

View file

@ -253,7 +253,7 @@ public class MemUserSessionProvider implements UserSessionProvider {
@Override
public void onClientRemoved(RealmModel realm, ClientModel client) {
for (ClientSessionEntity e : clientSessions.values()) {
if (e.getSession().getRealm().equals(realm.getId()) && e.getClientId().equals(client.getId())) {
if (e.getRealmId().equals(realm.getId()) && e.getClientId().equals(client.getId())) {
clientSessions.remove(e.getId());
e.getSession().removeClientSession(e);
}

View file

@ -44,6 +44,7 @@
<selenium.version>2.35.0</selenium.version>
<javax.mail.version>1.4.5</javax.mail.version>
<infinispan.version>6.0.2.Final</infinispan.version>
<liquibase.version>3.2.2</liquibase.version>
<!-- maven-compiler-plugin -->
<maven.compiler.target>1.6</maven.compiler.target>
@ -449,6 +450,11 @@
<artifactId>infinispan-core</artifactId>
<version>${infinispan.version}</version>
</dependency>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
<version>${liquibase.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

View file

@ -67,7 +67,7 @@
"driverDialect": "${keycloak.connectionsJpa.driverDialect:}",
"user": "${keycloak.connectionsJpa.user:sa}",
"password": "${keycloak.connectionsJpa.password:}",
"databaseSchema": "${keycloak.connectionsJpa.databaseSchema:create-drop}",
"databaseSchema": "${keycloak.connectionsJpa.databaseSchema:update}",
"showSql": "${keycloak.connectionsJpa.showSql:false}",
"formatSql": "${keycloak.connectionsJpa.formatSql:true}"
}
@ -78,7 +78,7 @@
"host": "${keycloak.connectionsMongo.host:127.0.0.1}",
"port": "${keycloak.connectionsMongo.port:27017}",
"db": "${keycloak.connectionsMongo.db:keycloak}",
"clearOnStartup": "${keycloak.connectionsMongo.clearOnStartup:false}"
"databaseSchema": "${keycloak.connectionsMongo.databaseSchema:update}"
}
}
}

View file

@ -6,6 +6,15 @@ log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss,SSS} %-5p [%c] %m%n
log4j.logger.org.keycloak=info
# Enable to view loaded SPI and Providers
# log4j.logger.org.keycloak.services.DefaultKeycloakSessionFactory=debug
# Enable to view database updates
# log4j.logger.org.keycloak.connections.jpa.updater.liquibase.LiquibaseJpaUpdaterProvider=debug
# log4j.logger.org.keycloak.connections.mongo.updater.DefaultMongoUpdaterProvider=debug
# log4j.logger.org.keycloak.connections.jpa.DefaultJpaConnectionProviderFactory=debug
log4j.logger.org.xnio=off
log4j.logger.org.hibernate=off
log4j.logger.org.jboss.resteasy=warn

View file

@ -157,15 +157,11 @@ public class AccountTest {
});
}
@Test
@Ignore
public void runit() throws Exception {
Thread.sleep(10000000);
}
// @Test
// @Ignore
// public void runit() throws Exception {
// Thread.sleep(10000000);
// }
@Test
public void returnToAppFromQueryParam() {

View file

@ -54,7 +54,7 @@ public class CompositeImportRoleTest {
@Override
protected void configure(KeycloakSession session, RealmManager manager, RealmModel adminRealm) {
RealmRepresentation representation = KeycloakServer.loadJson(getClass().getResourceAsStream("/testcomposite.json"), RealmRepresentation.class);
representation.setId("Test");
representation.setId("test");
RealmModel realm = manager.importRealm(representation);
realmPublicKey = realm.getPublicKey();
@ -78,7 +78,7 @@ public class CompositeImportRoleTest {
@Test
public void testAppCompositeUser() throws Exception {
oauth.realm("Test");
oauth.realm("test");
oauth.realmPublicKey(realmPublicKey);
oauth.clientId("APP_COMPOSITE_APPLICATION");
oauth.doLogin("APP_COMPOSITE_USER", "password");
@ -92,7 +92,7 @@ public class CompositeImportRoleTest {
AccessToken token = oauth.verifyToken(response.getAccessToken());
Assert.assertEquals(keycloakRule.getUser("Test", "APP_COMPOSITE_USER").getId(), token.getSubject());
Assert.assertEquals(keycloakRule.getUser("test", "APP_COMPOSITE_USER").getId(), token.getSubject());
Assert.assertEquals(1, token.getResourceAccess("APP_ROLE_APPLICATION").getRoles().size());
Assert.assertEquals(1, token.getRealmAccess().getRoles().size());
@ -103,7 +103,7 @@ public class CompositeImportRoleTest {
@Test
public void testRealmAppCompositeUser() throws Exception {
oauth.realm("Test");
oauth.realm("test");
oauth.realmPublicKey(realmPublicKey);
oauth.clientId("APP_ROLE_APPLICATION");
oauth.doLogin("REALM_APP_COMPOSITE_USER", "password");
@ -117,7 +117,7 @@ public class CompositeImportRoleTest {
AccessToken token = oauth.verifyToken(response.getAccessToken());
Assert.assertEquals(keycloakRule.getUser("Test", "REALM_APP_COMPOSITE_USER").getId(), token.getSubject());
Assert.assertEquals(keycloakRule.getUser("test", "REALM_APP_COMPOSITE_USER").getId(), token.getSubject());
Assert.assertEquals(1, token.getResourceAccess("APP_ROLE_APPLICATION").getRoles().size());
Assert.assertTrue(token.getResourceAccess("APP_ROLE_APPLICATION").isUserInRole("APP_ROLE_1"));
@ -127,7 +127,7 @@ public class CompositeImportRoleTest {
@Test
public void testRealmOnlyWithUserCompositeAppComposite() throws Exception {
oauth.realm("Test");
oauth.realm("test");
oauth.realmPublicKey(realmPublicKey);
oauth.clientId("REALM_COMPOSITE_1_APPLICATION");
oauth.doLogin("REALM_COMPOSITE_1_USER", "password");
@ -141,7 +141,7 @@ public class CompositeImportRoleTest {
AccessToken token = oauth.verifyToken(response.getAccessToken());
Assert.assertEquals(keycloakRule.getUser("Test", "REALM_COMPOSITE_1_USER").getId(), token.getSubject());
Assert.assertEquals(keycloakRule.getUser("test", "REALM_COMPOSITE_1_USER").getId(), token.getSubject());
Assert.assertEquals(2, token.getRealmAccess().getRoles().size());
Assert.assertTrue(token.getRealmAccess().isUserInRole("REALM_COMPOSITE_1"));
@ -150,7 +150,7 @@ public class CompositeImportRoleTest {
@Test
public void testRealmOnlyWithUserCompositeAppRole() throws Exception {
oauth.realm("Test");
oauth.realm("test");
oauth.realmPublicKey(realmPublicKey);
oauth.clientId("REALM_ROLE_1_APPLICATION");
oauth.doLogin("REALM_COMPOSITE_1_USER", "password");
@ -164,7 +164,7 @@ public class CompositeImportRoleTest {
AccessToken token = oauth.verifyToken(response.getAccessToken());
Assert.assertEquals(keycloakRule.getUser("Test", "REALM_COMPOSITE_1_USER").getId(), token.getSubject());
Assert.assertEquals(keycloakRule.getUser("test", "REALM_COMPOSITE_1_USER").getId(), token.getSubject());
Assert.assertEquals(1, token.getRealmAccess().getRoles().size());
Assert.assertTrue(token.getRealmAccess().isUserInRole("REALM_ROLE_1"));
@ -172,7 +172,7 @@ public class CompositeImportRoleTest {
@Test
public void testRealmOnlyWithUserRoleAppComposite() throws Exception {
oauth.realm("Test");
oauth.realm("test");
oauth.realmPublicKey(realmPublicKey);
oauth.clientId("REALM_COMPOSITE_1_APPLICATION");
oauth.doLogin("REALM_ROLE_1_USER", "password");
@ -186,13 +186,10 @@ public class CompositeImportRoleTest {
AccessToken token = oauth.verifyToken(response.getAccessToken());
Assert.assertEquals(keycloakRule.getUser("Test", "REALM_ROLE_1_USER").getId(), token.getSubject());
Assert.assertEquals(keycloakRule.getUser("test", "REALM_ROLE_1_USER").getId(), token.getSubject());
Assert.assertEquals(1, token.getRealmAccess().getRoles().size());
Assert.assertTrue(token.getRealmAccess().isUserInRole("REALM_ROLE_1"));
}
}

View file

@ -58,7 +58,7 @@ public class CompositeRoleTest {
public static AbstractKeycloakRule keycloakRule = new AbstractKeycloakRule(){
@Override
protected void configure(KeycloakSession session, RealmManager manager, RealmModel adminRealm) {
RealmModel realm = manager.createRealm("Test");
RealmModel realm = manager.createRealm("test");
KeycloakModelUtils.generateRealmKeys(realm);
realmPublicKey = realm.getPublicKey();
realm.setSsoSessionIdleTimeout(3000);
@ -166,7 +166,7 @@ public class CompositeRoleTest {
@Test
public void testAppCompositeUser() throws Exception {
oauth.realm("Test");
oauth.realm("test");
oauth.realmPublicKey(realmPublicKey);
oauth.clientId("APP_COMPOSITE_APPLICATION");
oauth.doLogin("APP_COMPOSITE_USER", "password");
@ -180,7 +180,7 @@ public class CompositeRoleTest {
AccessToken token = oauth.verifyToken(response.getAccessToken());
Assert.assertEquals(keycloakRule.getUser("Test", "APP_COMPOSITE_USER").getId(), token.getSubject());
Assert.assertEquals(keycloakRule.getUser("test", "APP_COMPOSITE_USER").getId(), token.getSubject());
Assert.assertEquals(1, token.getResourceAccess("APP_ROLE_APPLICATION").getRoles().size());
Assert.assertEquals(1, token.getRealmAccess().getRoles().size());
@ -194,7 +194,7 @@ public class CompositeRoleTest {
@Test
public void testRealmAppCompositeUser() throws Exception {
oauth.realm("Test");
oauth.realm("test");
oauth.realmPublicKey(realmPublicKey);
oauth.clientId("APP_ROLE_APPLICATION");
oauth.doLogin("REALM_APP_COMPOSITE_USER", "password");
@ -208,7 +208,7 @@ public class CompositeRoleTest {
AccessToken token = oauth.verifyToken(response.getAccessToken());
Assert.assertEquals(keycloakRule.getUser("Test", "REALM_APP_COMPOSITE_USER").getId(), token.getSubject());
Assert.assertEquals(keycloakRule.getUser("test", "REALM_APP_COMPOSITE_USER").getId(), token.getSubject());
Assert.assertEquals(1, token.getResourceAccess("APP_ROLE_APPLICATION").getRoles().size());
Assert.assertTrue(token.getResourceAccess("APP_ROLE_APPLICATION").isUserInRole("APP_ROLE_1"));
@ -219,7 +219,7 @@ public class CompositeRoleTest {
@Test
public void testRealmOnlyWithUserCompositeAppComposite() throws Exception {
oauth.realm("Test");
oauth.realm("test");
oauth.realmPublicKey(realmPublicKey);
oauth.clientId("REALM_COMPOSITE_1_APPLICATION");
oauth.doLogin("REALM_COMPOSITE_1_USER", "password");
@ -233,7 +233,7 @@ public class CompositeRoleTest {
AccessToken token = oauth.verifyToken(response.getAccessToken());
Assert.assertEquals(keycloakRule.getUser("Test", "REALM_COMPOSITE_1_USER").getId(), token.getSubject());
Assert.assertEquals(keycloakRule.getUser("test", "REALM_COMPOSITE_1_USER").getId(), token.getSubject());
Assert.assertEquals(2, token.getRealmAccess().getRoles().size());
Assert.assertTrue(token.getRealmAccess().isUserInRole("REALM_COMPOSITE_1"));
@ -245,7 +245,7 @@ public class CompositeRoleTest {
@Test
public void testRealmOnlyWithUserCompositeAppRole() throws Exception {
oauth.realm("Test");
oauth.realm("test");
oauth.realmPublicKey(realmPublicKey);
oauth.clientId("REALM_ROLE_1_APPLICATION");
oauth.doLogin("REALM_COMPOSITE_1_USER", "password");
@ -259,7 +259,7 @@ public class CompositeRoleTest {
AccessToken token = oauth.verifyToken(response.getAccessToken());
Assert.assertEquals(keycloakRule.getUser("Test", "REALM_COMPOSITE_1_USER").getId(), token.getSubject());
Assert.assertEquals(keycloakRule.getUser("test", "REALM_COMPOSITE_1_USER").getId(), token.getSubject());
Assert.assertEquals(1, token.getRealmAccess().getRoles().size());
Assert.assertTrue(token.getRealmAccess().isUserInRole("REALM_ROLE_1"));
@ -270,7 +270,7 @@ public class CompositeRoleTest {
@Test
public void testRealmOnlyWithUserRoleAppComposite() throws Exception {
oauth.realm("Test");
oauth.realm("test");
oauth.realmPublicKey(realmPublicKey);
oauth.clientId("REALM_COMPOSITE_1_APPLICATION");
oauth.doLogin("REALM_ROLE_1_USER", "password");
@ -284,7 +284,7 @@ public class CompositeRoleTest {
AccessToken token = oauth.verifyToken(response.getAccessToken());
Assert.assertEquals(keycloakRule.getUser("Test", "REALM_ROLE_1_USER").getId(), token.getSubject());
Assert.assertEquals(keycloakRule.getUser("test", "REALM_ROLE_1_USER").getId(), token.getSubject());
Assert.assertEquals(1, token.getRealmAccess().getRoles().size());
Assert.assertTrue(token.getRealmAccess().isUserInRole("REALM_ROLE_1"));

View file

@ -64,7 +64,7 @@ public class ExportImportTest {
propsHelper.pushProperty(JPA_CONNECTION_URL, "jdbc:h2:file:" + dbDir + ";DB_CLOSE_DELAY=-1");
connectionURLSet = true;
}
propsHelper.pushProperty(JPA_DB_SCHEMA, "create");
propsHelper.pushProperty(JPA_DB_SCHEMA, "update");
}
@Override
@ -98,7 +98,6 @@ public class ExportImportTest {
addUser(manager.getSession().users(), appRealm, "user1", "password");
addUser(manager.getSession().users(), appRealm, "user2", "password");
addUser(manager.getSession().users(), appRealm, "user3", "password");
addUser(manager.getSession().users(), adminstrationRealm, "admin2", "admin2");
// Import "test-realm" realm
try {
@ -129,6 +128,10 @@ public class ExportImportTest {
systemProps.remove(propToRemove);
}
}
protected String[] getTestRealms() {
return new String[]{"test", "demo", "test-realm"};
}
};
@ClassRule
@ -222,10 +225,6 @@ public class ExportImportTest {
new RealmManager(session).removeRealm(realmProvider.getRealmByName("test"));
Assert.assertEquals(2, realmProvider.getRealms().size());
RealmModel master = realmProvider.getRealmByName(Config.getAdminRealm());
UserModel admin2 = session.users().getUserByUsername("admin2", master);
session.users().removeUser(master, admin2);
assertNotAuthenticated(userProvider, realmProvider, Config.getAdminRealm(), "admin2", "admin2");
assertNotAuthenticated(userProvider, realmProvider, "test", "test-user@localhost", "password");
assertNotAuthenticated(userProvider, realmProvider, "test", "user1", "password");
assertNotAuthenticated(userProvider, realmProvider, "test", "user2", "password");
@ -247,7 +246,6 @@ public class ExportImportTest {
UserProvider userProvider = session.users();
Assert.assertEquals(3, model.getRealms().size());
assertAuthenticated(userProvider, model, Config.getAdminRealm(), "admin2", "admin2");
assertAuthenticated(userProvider, model, "test", "test-user@localhost", "password");
assertAuthenticated(userProvider, model, "test", "user1", "password");
assertAuthenticated(userProvider, model, "test", "user2", "password");
@ -275,11 +273,6 @@ public class ExportImportTest {
new RealmManager(session).removeRealm(realmProvider.getRealmByName("test"));
Assert.assertEquals(2, realmProvider.getRealms().size());
RealmModel master = realmProvider.getRealmByName(Config.getAdminRealm());
UserModel admin2 = session.users().getUserByUsername("admin2", master);
session.users().removeUser(master, admin2);
assertNotAuthenticated(userProvider, realmProvider, Config.getAdminRealm(), "admin2", "admin2");
assertNotAuthenticated(userProvider, realmProvider, "test", "test-user@localhost", "password");
assertNotAuthenticated(userProvider, realmProvider, "test", "user1", "password");
assertNotAuthenticated(userProvider, realmProvider, "test", "user2", "password");
@ -301,13 +294,10 @@ public class ExportImportTest {
UserProvider userProvider = session.users();
Assert.assertEquals(3, realmProvider.getRealms().size());
assertNotAuthenticated(userProvider, realmProvider, Config.getAdminRealm(), "admin2", "admin2");
assertAuthenticated(userProvider, realmProvider, "test", "test-user@localhost", "password");
assertAuthenticated(userProvider, realmProvider, "test", "user1", "password");
assertAuthenticated(userProvider, realmProvider, "test", "user2", "password");
assertAuthenticated(userProvider, realmProvider, "test", "user3", "password");
addUser(userProvider, realmProvider.getRealmByName(Config.getAdminRealm()), "admin2", "admin2");
} finally {
keycloakRule.stopSession(session, true);
}

View file

@ -30,12 +30,15 @@ import java.net.Socket;
* @version $Revision: 1 $
*/
public abstract class AbstractKeycloakRule extends ExternalResource {
protected KeycloakServer server;
protected void before() throws Throwable {
server = new KeycloakServer();
server.start();
removeTestRealms();
setupKeycloak();
}
@ -123,6 +126,7 @@ public abstract class AbstractKeycloakRule extends ExternalResource {
deploymentInfo.addServlet(servlet);
return deploymentInfo;
}
public void deployApplication(String name, String contextPath, Class<? extends Servlet> servletClass, String adapterConfigPath, String role) {
deployApplication(name, contextPath, servletClass, adapterConfigPath, role, true);
@ -147,9 +151,27 @@ public abstract class AbstractKeycloakRule extends ExternalResource {
@Override
protected void after() {
removeTestRealms();
stopServer();
}
protected void removeTestRealms() {
KeycloakSession session = server.getSessionFactory().create();
try {
session.getTransaction().begin();
RealmManager realmManager = new RealmManager(session);
for (String realmName : getTestRealms()) {
RealmModel realm = realmManager.getRealmByName(realmName);
if (realm != null) {
realmManager.removeRealm(realm);
}
}
session.getTransaction().commit();
} finally {
session.close();
}
}
public RealmRepresentation loadJson(String path) throws IOException {
InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(path);
ByteArrayOutputStream os = new ByteArrayOutputStream();
@ -169,7 +191,7 @@ public abstract class AbstractKeycloakRule extends ExternalResource {
public void stopSession(KeycloakSession session, boolean commit) {
KeycloakTransaction transaction = session.getTransaction();
if (commit) {
if (commit && !transaction.getRollbackOnly()) {
transaction.commit();
} else {
transaction.rollback();
@ -208,4 +230,9 @@ public abstract class AbstractKeycloakRule extends ExternalResource {
Thread.currentThread().interrupt();
}
}
protected String[] getTestRealms() {
return new String[]{"test", "demo"};
}
}

View file

@ -1,6 +1,6 @@
{
"id": "Test",
"realm": "Test",
"id": "test",
"realm": "test",
"enabled": true,
"accessTokenLifespan": 600,
"accessCodeLifespan": 600,