keycloak-uncached

model migration

4/27/2015 5:12:43 PM

Changes

Details

diff --git a/connections/file/src/main/java/org/keycloak/connections/file/DefaultFileConnectionProviderFactory.java b/connections/file/src/main/java/org/keycloak/connections/file/DefaultFileConnectionProviderFactory.java
index 0c9b5fe..26c14de 100755
--- a/connections/file/src/main/java/org/keycloak/connections/file/DefaultFileConnectionProviderFactory.java
+++ b/connections/file/src/main/java/org/keycloak/connections/file/DefaultFileConnectionProviderFactory.java
@@ -24,6 +24,8 @@ import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+
+import org.codehaus.jackson.JsonToken;
 import org.jboss.logging.Logger;
 import org.keycloak.Config;
 import org.keycloak.exportimport.Strategy;
@@ -84,7 +86,22 @@ public class DefaultFileConnectionProviderFactory implements FileConnectionProvi
             FileInputStream fis = null;
             try {
                 fis = new FileInputStream(kcdata);
+                Model model = JsonSerialization.readValue(fis, Model.class);
                 ImportUtils.importFromStream(session, JsonSerialization.mapper, fis, Strategy.IGNORE_EXISTING);
+                session.realms().getMigrationModel().setStoredVersion(model.getModelVersion());
+
+                List<RealmRepresentation> realmReps = new ArrayList<RealmRepresentation>();
+                for (RealmRepresentation realmRep : model.getRealms()) {
+                    if (Config.getAdminRealm().equals(realmRep.getRealm())) {
+                        realmReps.add(0, realmRep);
+                    } else {
+                        realmReps.add(realmRep);
+                    }
+                }
+                for (RealmRepresentation realmRep : realmReps) {
+                    ImportUtils.importRealm(session, realmRep, Strategy.IGNORE_EXISTING);
+                }
+
             } catch (IOException ioe) {
                 logger.error("Unable to read model file " + kcdata.getAbsolutePath(), ioe);
             } finally {
@@ -128,8 +145,10 @@ public class DefaultFileConnectionProviderFactory implements FileConnectionProvi
         for (RealmModel realm : realms) {
             reps.add(ExportUtils.exportRealm(session, realm, true));
         }
-
-        JsonSerialization.prettyMapper.writeValue(outStream, reps);
+        Model model = new Model();
+        model.setRealms(reps);
+        model.setModelVersion(session.realms().getMigrationModel().getStoredVersion());
+        JsonSerialization.prettyMapper.writeValue(outStream, model);
     }
 
     @Override
diff --git a/connections/file/src/main/java/org/keycloak/connections/file/InMemoryModel.java b/connections/file/src/main/java/org/keycloak/connections/file/InMemoryModel.java
old mode 100644
new mode 100755
index 8fddce6..2476b44
--- a/connections/file/src/main/java/org/keycloak/connections/file/InMemoryModel.java
+++ b/connections/file/src/main/java/org/keycloak/connections/file/InMemoryModel.java
@@ -37,6 +37,8 @@ public class InMemoryModel {
     //                realmId,    userId, userModel
     private final Map<String, Map<String,UserModel>> allUsers = new HashMap<String, Map<String,UserModel>>();
 
+    private String modelVersion;
+
     public InMemoryModel() {
     }
 
@@ -45,6 +47,14 @@ public class InMemoryModel {
         allUsers.put(id, new HashMap<String, UserModel>());
     }
 
+    public String getModelVersion() {
+        return modelVersion;
+    }
+
+    public void setModelVersion(String modelVersion) {
+        this.modelVersion = modelVersion;
+    }
+
     public RealmModel getRealm(String id) {
         return allRealms.get(id);
     }
diff --git a/connections/file/src/main/java/org/keycloak/connections/file/Model.java b/connections/file/src/main/java/org/keycloak/connections/file/Model.java
new file mode 100755
index 0000000..7350c38
--- /dev/null
+++ b/connections/file/src/main/java/org/keycloak/connections/file/Model.java
@@ -0,0 +1,30 @@
+package org.keycloak.connections.file;
+
+import org.keycloak.representations.idm.RealmRepresentation;
+
+import java.util.List;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class Model {
+    private String modelVersion;
+    private List<RealmRepresentation> realms;
+
+    public String getModelVersion() {
+        return modelVersion;
+    }
+
+    public void setModelVersion(String modelVersion) {
+        this.modelVersion = modelVersion;
+    }
+
+    public List<RealmRepresentation> getRealms() {
+        return realms;
+    }
+
+    public void setRealms(List<RealmRepresentation> realms) {
+        this.realms = realms;
+    }
+}
diff --git a/connections/jpa/src/main/resources/META-INF/persistence.xml b/connections/jpa/src/main/resources/META-INF/persistence.xml
index 83eadb5..4dbad93 100755
--- a/connections/jpa/src/main/resources/META-INF/persistence.xml
+++ b/connections/jpa/src/main/resources/META-INF/persistence.xml
@@ -11,6 +11,7 @@
         <class>org.keycloak.models.jpa.entities.UserFederationProviderEntity</class>
         <class>org.keycloak.models.jpa.entities.RoleEntity</class>
         <class>org.keycloak.models.jpa.entities.FederatedIdentityEntity</class>
+        <class>org.keycloak.models.jpa.entities.MigrationModelEntity</class>
         <class>org.keycloak.models.jpa.entities.UserEntity</class>
         <class>org.keycloak.models.jpa.entities.UserRequiredActionEntity</class>
         <class>org.keycloak.models.jpa.entities.UserAttributeEntity</class>
diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.RC1.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.RC1.xml
index 709d50b..8d09ba5 100755
--- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.RC1.xml
+++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.2.0.RC1.xml
@@ -5,6 +5,14 @@
         <delete tableName="CLIENT_SESSION_NOTE"/>
         <delete tableName="CLIENT_SESSION"/>
         <delete tableName="USER_SESSION"/>
+        <createTable tableName="MIGRATION_MODEL">
+            <column name="ID" type="VARCHAR(36)">
+                <constraints nullable="false"/>
+            </column>
+            <column name="VERSION" type="VARCHAR(36)">
+                <constraints nullable="true"/>
+            </column>
+        </createTable>
 
         <createTable tableName="IDENTITY_PROVIDER_MAPPER">
             <column name="ID" type="VARCHAR(36)">
@@ -70,6 +78,7 @@
                 <constraints nullable="false"/>
             </column>
         </createTable>
+        <addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_MIGMOD" tableName="MIGRATION_MODEL"/>
         <addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_IDPM" tableName="IDENTITY_PROVIDER_MAPPER"/>
         <addPrimaryKey columnNames="IDP_MAPPER_ID, NAME" constraintName="CONSTRAINT_IDPMConfig" tableName="IDP_MAPPER_CONFIG"/>
         <addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_GRNTCSNT_PM" tableName="USER_CONSENT"/>
diff --git a/model/api/src/main/java/org/keycloak/migration/MigrationModel.java b/model/api/src/main/java/org/keycloak/migration/MigrationModel.java
new file mode 100755
index 0000000..936fbcf
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/migration/MigrationModel.java
@@ -0,0 +1,18 @@
+package org.keycloak.migration;
+
+import org.keycloak.provider.Provider;
+import org.keycloak.provider.ProviderFactory;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public interface MigrationModel {
+    /**
+     * Must have the form of major.minor.micro as the version is parsed and numbers are compared
+     */
+    public static final String LATEST_VERSION = "1.2.0.CR1";
+
+    String getStoredVersion();
+    void setStoredVersion(String version);
+}
diff --git a/model/api/src/main/java/org/keycloak/migration/MigrationModelManager.java b/model/api/src/main/java/org/keycloak/migration/MigrationModelManager.java
new file mode 100755
index 0000000..c749676
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/migration/MigrationModelManager.java
@@ -0,0 +1,31 @@
+package org.keycloak.migration;
+
+import org.jboss.logging.Logger;
+import org.keycloak.migration.migrators.MigrationTo1_2_0_RC1;
+import org.keycloak.models.KeycloakSession;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class MigrationModelManager {
+    private static Logger logger = Logger.getLogger(MigrationModelManager.class);
+
+    public static void migrate(KeycloakSession session) {
+        MigrationModel model = session.realms().getMigrationModel();
+        String storedVersion = model.getStoredVersion();
+        if (MigrationModel.LATEST_VERSION.equals(storedVersion)) return;
+        ModelVersion stored = null;
+        if (storedVersion == null) stored = new ModelVersion(0, 0, 0);
+        else stored = new ModelVersion(storedVersion);
+
+        if (stored.lessThan(MigrationTo1_2_0_RC1.VERSION)) {
+            logger.info("Migrating older model to 1.2.0.RC1 updates");
+            new MigrationTo1_2_0_RC1().migrate(session);
+        }
+
+        model.setStoredVersion(MigrationModel.LATEST_VERSION);
+
+
+    }
+}
diff --git a/model/api/src/main/java/org/keycloak/migration/migrators/MigrationTo1_2_0_RC1.java b/model/api/src/main/java/org/keycloak/migration/migrators/MigrationTo1_2_0_RC1.java
new file mode 100755
index 0000000..5c483f9
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/migration/migrators/MigrationTo1_2_0_RC1.java
@@ -0,0 +1,38 @@
+package org.keycloak.migration.migrators;
+
+import org.keycloak.migration.ModelVersion;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.Constants;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+
+import java.util.List;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class MigrationTo1_2_0_RC1 {
+    public static final ModelVersion VERSION = new ModelVersion("1.2.0.RC1");
+
+    public void setupBrokerService(RealmModel realm) {
+        ClientModel client = realm.getClientNameMap().get(Constants.BROKER_SERVICE_CLIENT_ID);
+        if (client == null) {
+            client = KeycloakModelUtils.createClient(realm, Constants.BROKER_SERVICE_CLIENT_ID);
+            client.setEnabled(true);
+            client.setFullScopeAllowed(false);
+
+            for (String role : Constants.BROKER_SERVICE_ROLES) {
+                client.addRole(role).setDescription("${role_"+role+"}");
+            }
+        }
+    }
+    public void migrate(KeycloakSession session) {
+        List<RealmModel> realms = session.realms().getRealms();
+        for (RealmModel realm : realms) {
+            setupBrokerService(realm);
+        }
+
+    }
+}
diff --git a/model/api/src/main/java/org/keycloak/migration/ModelVersion.java b/model/api/src/main/java/org/keycloak/migration/ModelVersion.java
new file mode 100755
index 0000000..1dfcd14
--- /dev/null
+++ b/model/api/src/main/java/org/keycloak/migration/ModelVersion.java
@@ -0,0 +1,69 @@
+package org.keycloak.migration;
+
+import org.jboss.logging.Logger;
+
+/**
+* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+* @version $Revision: 1 $
+*/
+public class ModelVersion {
+    private static Logger logger = Logger.getLogger(ModelVersion.class);
+    int major;
+    int minor;
+    int micro;
+    String qualifier;
+
+    public ModelVersion(int major, int minor, int micro) {
+        this.major = major;
+        this.minor = minor;
+        this.micro = micro;
+    }
+
+    public ModelVersion(String version) {
+        String[] split = version.split("\\.");
+        try {
+            if (split.length > 0) {
+                major = Integer.parseInt(split[0]);
+            }
+            if (split.length > 1) {
+                minor = Integer.parseInt(split[1]);
+            }
+            if (split.length > 2) {
+                micro = Integer.parseInt(split[2]);
+            }
+            if (split.length > 3) {
+                qualifier = split[3];
+            }
+        } catch (NumberFormatException e) {
+            logger.warn("failed to parse version: " + version, e);
+        }
+    }
+
+    public int getMajor() {
+        return major;
+    }
+
+    public int getMinor() {
+        return minor;
+    }
+
+    public int getMicro() {
+        return micro;
+    }
+
+    public String getQualifier() {
+        return qualifier;
+    }
+
+    public boolean lessThan(ModelVersion version) {
+        if (major < version.major) return true;
+        if (minor < version.minor) return true;
+        if (micro < version.micro) return true;
+        if (qualifier == version.qualifier) return false;
+        if (qualifier == null) return false;
+        if (version.qualifier == null) return true;
+        int comp = qualifier.compareTo(version.qualifier);
+        if (comp < 0) return true;
+        return false;
+    }
+}
diff --git a/model/api/src/main/java/org/keycloak/models/Constants.java b/model/api/src/main/java/org/keycloak/models/Constants.java
index 08cf4ee..60d71c7 100755
--- a/model/api/src/main/java/org/keycloak/models/Constants.java
+++ b/model/api/src/main/java/org/keycloak/models/Constants.java
@@ -12,4 +12,6 @@ public interface Constants {
 
     String INSTALLED_APP_URN = "urn:ietf:wg:oauth:2.0:oob";
     String INSTALLED_APP_URL = "http://localhost";
+    String READ_TOKEN_ROLE = "READ_TOKEN";
+    String[] BROKER_SERVICE_ROLES = {READ_TOKEN_ROLE};
 }
diff --git a/model/api/src/main/java/org/keycloak/models/KeycloakSession.java b/model/api/src/main/java/org/keycloak/models/KeycloakSession.java
index b6f35e6..b8ce57b 100755
--- a/model/api/src/main/java/org/keycloak/models/KeycloakSession.java
+++ b/model/api/src/main/java/org/keycloak/models/KeycloakSession.java
@@ -1,5 +1,6 @@
 package org.keycloak.models;
 
+import org.keycloak.migration.MigrationModel;
 import org.keycloak.provider.Provider;
 
 import java.util.Set;
diff --git a/model/api/src/main/java/org/keycloak/models/RealmProvider.java b/model/api/src/main/java/org/keycloak/models/RealmProvider.java
index 17de99d..4556002 100755
--- a/model/api/src/main/java/org/keycloak/models/RealmProvider.java
+++ b/model/api/src/main/java/org/keycloak/models/RealmProvider.java
@@ -1,5 +1,6 @@
 package org.keycloak.models;
 
+import org.keycloak.migration.MigrationModel;
 import org.keycloak.provider.Provider;
 
 import java.util.List;
@@ -11,7 +12,7 @@ import java.util.List;
 public interface RealmProvider extends Provider {
 
     // Note: The reason there are so many query methods here is for layering a cache on top of an persistent KeycloakSession
-
+    MigrationModel getMigrationModel();
     RealmModel createRealm(String name);
     RealmModel createRealm(String id, String name);
     RealmModel getRealm(String id);
diff --git a/model/api/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/model/api/src/main/resources/META-INF/services/org.keycloak.provider.Spi
index 64dd861..9b36900 100755
--- a/model/api/src/main/resources/META-INF/services/org.keycloak.provider.Spi
+++ b/model/api/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -1,5 +1,5 @@
-org.keycloak.models.UserFederationSpi
-org.keycloak.models.RealmSpi
-org.keycloak.models.UserSessionSpi
-org.keycloak.models.UserSpi
+org.keycloak.models.UserFederationSpi
+org.keycloak.models.RealmSpi
+org.keycloak.models.UserSessionSpi
+org.keycloak.models.UserSpi
 org.keycloak.migration.MigrationSpi
\ No newline at end of file
diff --git a/model/api/src/test/java/org/keycloak/models/MigrationVersionTest.java b/model/api/src/test/java/org/keycloak/models/MigrationVersionTest.java
new file mode 100755
index 0000000..c706d8a
--- /dev/null
+++ b/model/api/src/test/java/org/keycloak/models/MigrationVersionTest.java
@@ -0,0 +1,45 @@
+package org.keycloak.models;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.keycloak.migration.ModelVersion;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class MigrationVersionTest {
+
+    @Test
+    public void testVersion() {
+        ModelVersion version_100Beta1 = new ModelVersion("1.0.0.Beta1");
+        Assert.assertEquals(version_100Beta1.getMajor(), 1);
+        Assert.assertEquals(version_100Beta1.getMinor(), 0);
+        Assert.assertEquals(version_100Beta1.getMicro(), 0);
+        ModelVersion version_100RC1 = new ModelVersion("1.0.0.RC1");
+        ModelVersion version_100 = new ModelVersion("1.0.0");
+        ModelVersion version_110Beta1 = new ModelVersion("1.1.0.Beta1");
+        ModelVersion version_110RC1 = new ModelVersion("1.1.0.RC1");
+        ModelVersion version_110 = new ModelVersion("1.1.0");
+        ModelVersion version_111Beta1 = new ModelVersion("1.1.1.Beta1");
+        ModelVersion version_111RC1 = new ModelVersion("1.1.1.RC1");
+        ModelVersion version_111 = new ModelVersion("1.1.1");
+        ModelVersion version_211Beta1 = new ModelVersion("2.1.1.Beta1");
+        ModelVersion version_211RC1 = new ModelVersion("2.1.1.RC1");
+        Assert.assertEquals(version_211RC1.getMajor(), 2);
+        Assert.assertEquals(version_211RC1.getMinor(), 1);
+        Assert.assertEquals(version_211RC1.getMicro(), 1);
+        Assert.assertEquals(version_211RC1.getQualifier(), "RC1");
+        ModelVersion version_211 = new ModelVersion("2.1.1");
+
+        Assert.assertFalse(version_100Beta1.lessThan(version_100Beta1));
+        Assert.assertTrue(version_100Beta1.lessThan(version_100RC1));
+        Assert.assertTrue(version_100Beta1.lessThan(version_100));
+        Assert.assertTrue(version_100Beta1.lessThan(version_110Beta1));
+        Assert.assertTrue(version_100Beta1.lessThan(version_110RC1));
+        Assert.assertTrue(version_100Beta1.lessThan(version_110));
+
+        Assert.assertFalse(version_211.lessThan(version_110RC1));
+
+    }
+}
diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/MigrationModelAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/MigrationModelAdapter.java
new file mode 100755
index 0000000..f92718c
--- /dev/null
+++ b/model/file/src/main/java/org/keycloak/models/file/adapter/MigrationModelAdapter.java
@@ -0,0 +1,26 @@
+package org.keycloak.models.file.adapter;
+
+import org.keycloak.connections.file.InMemoryModel;
+import org.keycloak.migration.MigrationModel;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class MigrationModelAdapter implements MigrationModel {
+    protected InMemoryModel em;
+
+    public MigrationModelAdapter(InMemoryModel em) {
+        this.em = em;
+    }
+
+    @Override
+    public String getStoredVersion() {
+        return em.getModelVersion();
+    }
+
+    @Override
+    public void setStoredVersion(String version) {
+       em.setModelVersion(version);
+    }
+}
diff --git a/model/file/src/main/java/org/keycloak/models/file/FileRealmProvider.java b/model/file/src/main/java/org/keycloak/models/file/FileRealmProvider.java
old mode 100644
new mode 100755
index 3f23b5c..593aa88
--- a/model/file/src/main/java/org/keycloak/models/file/FileRealmProvider.java
+++ b/model/file/src/main/java/org/keycloak/models/file/FileRealmProvider.java
@@ -18,6 +18,7 @@ package org.keycloak.models.file;
 
 import org.keycloak.connections.file.FileConnectionProvider;
 import org.keycloak.connections.file.InMemoryModel;
+import org.keycloak.migration.MigrationModel;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.ModelDuplicateException;
@@ -25,6 +26,7 @@ import org.keycloak.models.RealmModel;
 import org.keycloak.models.RealmProvider;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.entities.RealmEntity;
+import org.keycloak.models.file.adapter.MigrationModelAdapter;
 import org.keycloak.models.file.adapter.RealmAdapter;
 import org.keycloak.models.utils.KeycloakModelUtils;
 
@@ -55,6 +57,11 @@ public class FileRealmProvider implements RealmProvider {
     }
 
     @Override
+    public MigrationModel getMigrationModel() {
+        return new MigrationModelAdapter(inMemoryModel);
+    }
+
+    @Override
     public RealmModel createRealm(String name) {
         return createRealm(KeycloakModelUtils.generateId(), name);
     }
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheRealmProvider.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheRealmProvider.java
index 13f6924..469973f 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheRealmProvider.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheRealmProvider.java
@@ -1,5 +1,6 @@
 package org.keycloak.models.cache;
 
+import org.keycloak.migration.MigrationModel;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakTransaction;
@@ -46,6 +47,12 @@ public class DefaultCacheRealmProvider implements CacheRealmProvider {
     }
 
     @Override
+    public MigrationModel getMigrationModel() {
+        return getDelegate().getMigrationModel();
+    }
+
+
+    @Override
     public boolean isEnabled() {
         return cache.isEnabled();
     }
diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheRealmProvider.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheRealmProvider.java
index 12c28f6..cb8a8a3 100755
--- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheRealmProvider.java
+++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheRealmProvider.java
@@ -1,5 +1,6 @@
 package org.keycloak.models.cache;
 
+import org.keycloak.migration.MigrationModel;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
@@ -50,6 +51,11 @@ public class NoCacheRealmProvider implements CacheRealmProvider {
     }
 
     @Override
+    public MigrationModel getMigrationModel() {
+        return getDelegate().getMigrationModel();
+    }
+
+    @Override
     public RealmModel createRealm(String name) {
         return getDelegate().createRealm(name);
     }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/MigrationModelEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/MigrationModelEntity.java
new file mode 100755
index 0000000..a29873e
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/MigrationModelEntity.java
@@ -0,0 +1,43 @@
+package org.keycloak.models.jpa.entities;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.Table;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+@Table(name="MIGRATION_MODEL")
+@Entity
+public class MigrationModelEntity {
+    public static final String SINGLETON_ID = "SINGLETON";
+    @Id
+    @Column(name="ID", length = 36)
+    private String id;
+
+    @Column(name="VERSION", length = 36)
+    protected String version;
+
+    public String getId() {
+        return id;
+    }
+
+    public void setId(String id) {
+        this.id = id;
+    }
+
+    public String getVersion() {
+        return version;
+    }
+
+    public void setVersion(String version) {
+        this.version = version;
+    }
+}
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java
index 1be423a..758531a 100755
--- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaRealmProvider.java
@@ -1,5 +1,6 @@
 package org.keycloak.models.jpa;
 
+import org.keycloak.migration.MigrationModel;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
@@ -31,6 +32,11 @@ public class JpaRealmProvider implements RealmProvider {
     }
 
     @Override
+    public MigrationModel getMigrationModel() {
+        return new MigrationModelAdapter(em);
+    }
+
+    @Override
     public RealmModel createRealm(String name) {
         return createRealm(KeycloakModelUtils.generateId(), name);
     }
diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/MigrationModelAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/MigrationModelAdapter.java
new file mode 100755
index 0000000..034e141
--- /dev/null
+++ b/model/jpa/src/main/java/org/keycloak/models/jpa/MigrationModelAdapter.java
@@ -0,0 +1,42 @@
+package org.keycloak.models.jpa;
+
+import org.keycloak.migration.MigrationModel;
+import org.keycloak.models.jpa.entities.MigrationModelEntity;
+import org.keycloak.util.Time;
+
+import javax.persistence.EntityManager;
+import javax.persistence.Query;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class MigrationModelAdapter implements MigrationModel {
+    protected EntityManager em;
+
+    public MigrationModelAdapter(EntityManager em) {
+        this.em = em;
+    }
+
+    @Override
+    public String getStoredVersion() {
+        MigrationModelEntity entity = em.find(MigrationModelEntity.class, MigrationModelEntity.SINGLETON_ID);
+        if (entity == null) return null;
+        return entity.getVersion();
+    }
+
+    @Override
+    public void setStoredVersion(String version) {
+        MigrationModelEntity entity = em.find(MigrationModelEntity.class, MigrationModelEntity.SINGLETON_ID);
+        if (entity == null) {
+            entity = new MigrationModelEntity();
+            entity.setId(MigrationModelEntity.SINGLETON_ID);
+            entity.setVersion(version);
+            em.persist(entity);
+        } else {
+            entity.setVersion(version);
+            em.flush();
+        }
+    }
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MigrationModelAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MigrationModelAdapter.java
new file mode 100755
index 0000000..3e06af6
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MigrationModelAdapter.java
@@ -0,0 +1,57 @@
+package org.keycloak.models.mongo.keycloak.adapters;
+
+import com.mongodb.DBObject;
+import com.mongodb.QueryBuilder;
+import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
+import org.keycloak.migration.MigrationModel;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.entities.ProtocolMapperEntity;
+import org.keycloak.models.mongo.keycloak.entities.MongoClientEntity;
+import org.keycloak.models.mongo.keycloak.entities.MongoMigrationModelEntity;
+import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity;
+import org.keycloak.models.mongo.utils.MongoModelUtils;
+import org.keycloak.models.utils.KeycloakModelUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
+ */
+public class MigrationModelAdapter extends AbstractMongoAdapter<MongoMigrationModelEntity> implements MigrationModel {
+
+    protected final MongoMigrationModelEntity entity;
+
+    public MigrationModelAdapter(KeycloakSession session, MongoMigrationModelEntity entity, MongoStoreInvocationContext invContext) {
+        super(invContext);
+        this.entity = entity;
+    }
+
+    @Override
+    public MongoMigrationModelEntity getMongoEntity() {
+        return entity;
+    }
+
+    @Override
+    public String getStoredVersion() {
+        return getMongoEntity().getVersion();
+    }
+
+    @Override
+    public void setStoredVersion(String version) {
+        getMongoEntity().setVersion(version);
+        updateMongoEntity();
+
+    }
+
+
+}
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProvider.java
index c8bdb38..1de862c 100755
--- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProvider.java
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoRealmProvider.java
@@ -5,12 +5,14 @@ import com.mongodb.DBObject;
 import com.mongodb.QueryBuilder;
 import org.keycloak.connections.mongo.api.MongoStore;
 import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
+import org.keycloak.migration.MigrationModel;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RealmProvider;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.mongo.keycloak.entities.MongoClientEntity;
+import org.keycloak.models.mongo.keycloak.entities.MongoMigrationModelEntity;
 import org.keycloak.models.mongo.keycloak.entities.MongoRealmEntity;
 import org.keycloak.models.mongo.keycloak.entities.MongoRoleEntity;
 import org.keycloak.models.utils.KeycloakModelUtils;
@@ -37,6 +39,16 @@ public class MongoRealmProvider implements RealmProvider {
     }
 
     @Override
+    public MigrationModel getMigrationModel() {
+        MongoMigrationModelEntity entity = getMongoStore().loadEntity(MongoMigrationModelEntity.class, MongoMigrationModelEntity.MIGRATION_MODEL_ID, invocationContext);
+        if (entity == null) {
+            entity = new MongoMigrationModelEntity();
+            getMongoStore().insertEntity(entity, invocationContext);
+        }
+        return new MigrationModelAdapter(session, entity, invocationContext);
+    }
+
+    @Override
     public RealmModel createRealm(String name) {
         return createRealm(KeycloakModelUtils.generateId(), name);
     }
diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoMigrationModelEntity.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoMigrationModelEntity.java
new file mode 100755
index 0000000..6acc40c
--- /dev/null
+++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/entities/MongoMigrationModelEntity.java
@@ -0,0 +1,38 @@
+package org.keycloak.models.mongo.keycloak.entities;
+
+import org.keycloak.connections.mongo.api.MongoIdentifiableEntity;
+import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class MongoMigrationModelEntity implements MongoIdentifiableEntity  {
+    public static final String MIGRATION_MODEL_ID = "VERSION";
+    private String id = MIGRATION_MODEL_ID;
+    private String version;
+
+    public String getVersion() {
+        return version;
+    }
+
+    public void setVersion(String version) {
+        this.version = version;
+    }
+
+    @Override
+    public String getId() {
+        return id;
+    }
+
+    @Override
+    public void setId(String id) {
+        this.id = id;
+
+    }
+
+    @Override
+    public void afterRemove(MongoStoreInvocationContext invocationContext) {
+
+    }
+}
diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
index 3d56a2a..4ff3ee1 100755
--- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
@@ -21,7 +21,6 @@ import org.keycloak.models.utils.RepresentationToModel;
 import org.keycloak.representations.idm.ClientRepresentation;
 import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
 import org.keycloak.representations.idm.RealmRepresentation;
-import org.keycloak.services.resources.IdentityBrokerService;
 import org.keycloak.timer.TimerProvider;
 
 import java.util.Collections;
@@ -226,7 +225,7 @@ public class RealmManager {
             client.setEnabled(true);
             client.setFullScopeAllowed(false);
 
-            for (String role : IdentityBrokerService.ROLES) {
+            for (String role : Constants.BROKER_SERVICE_ROLES) {
                 client.addRole(role).setDescription("${role_"+role+"}");
             }
         }
diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
index c10ef1e..a8119ce 100755
--- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
+++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java
@@ -44,7 +44,6 @@ import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.UserModel;
 import org.keycloak.models.UserSessionModel;
-import org.keycloak.protocol.ProtocolMapper;
 import org.keycloak.protocol.oidc.TokenManager;
 import org.keycloak.provider.ProviderFactory;
 import org.keycloak.representations.AccessToken;
@@ -60,17 +59,13 @@ import org.keycloak.services.validation.Validation;
 import org.keycloak.social.SocialIdentityProvider;
 import org.keycloak.util.ObjectUtil;
 
-import javax.ws.rs.Consumes;
 import javax.ws.rs.GET;
 import javax.ws.rs.OPTIONS;
-import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
 import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.HttpHeaders;
-import javax.ws.rs.core.MediaType;
-import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
 import javax.ws.rs.core.UriBuilder;
@@ -95,8 +90,6 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
 
     private static final Logger LOGGER = Logger.getLogger(IdentityBrokerService.class);
     public static final String BROKER_PROVIDER_ID = "BROKER_PROVIDER_ID";
-    public static final String READ_TOKEN_ROLE = "READ_TOKEN";
-    public static final String[] ROLES = {READ_TOKEN_ROLE};
 
     private final RealmModel realmModel;
 
@@ -207,7 +200,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
                 }
                 Map<String, AccessToken.Access> resourceAccess = token.getResourceAccess();
                 AccessToken.Access brokerRoles = resourceAccess == null ? null : resourceAccess.get(Constants.BROKER_SERVICE_CLIENT_ID);
-                if (brokerRoles == null || !brokerRoles.isUserInRole(READ_TOKEN_ROLE)) {
+                if (brokerRoles == null || !brokerRoles.isUserInRole(Constants.READ_TOKEN_ROLE)) {
                     return corsResponse(forbidden("Client [" + audience + "] not authorized to retrieve tokens from identity provider [" + providerId + "]."), clientModel);
 
                 }
@@ -536,7 +529,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal
 
 
         if (context.getIdpConfig().isAddReadTokenRoleOnCreate()) {
-            RoleModel readTokenRole = realmModel.getClientByClientId(Constants.BROKER_SERVICE_CLIENT_ID).getRole(READ_TOKEN_ROLE);
+            RoleModel readTokenRole = realmModel.getClientByClientId(Constants.BROKER_SERVICE_CLIENT_ID).getRole(Constants.READ_TOKEN_ROLE);
             federatedUser.grantRole(readTokenRole);
         }
 
diff --git a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
index 11b59c8..d3586bc 100755
--- a/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
+++ b/services/src/main/java/org/keycloak/services/resources/KeycloakApplication.java
@@ -8,6 +8,7 @@ import org.jboss.resteasy.spi.ResteasyProviderFactory;
 import org.keycloak.Config;
 import org.keycloak.SkeletonKeyContextResolver;
 import org.keycloak.exportimport.ExportImportManager;
+import org.keycloak.migration.MigrationModelManager;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
 import org.keycloak.models.RealmModel;
@@ -84,9 +85,26 @@ public class KeycloakApplication extends Application {
         setupDefaultRealm(context.getContextPath());
 
         importRealms(context);
+        migrateModel();
+
+
         setupScheduledTasks(sessionFactory);
     }
 
+    protected void migrateModel() {
+        KeycloakSession session = sessionFactory.create();
+        try {
+            session.getTransaction().begin();
+            MigrationModelManager.migrate(session);
+            session.getTransaction().commit();
+        } catch (Exception e) {
+            session.getTransaction().rollback();
+            log.error("Failed to migrate datamodel", e);
+        } finally {
+            session.close();
+        }
+    }
+
     public String getContextPath() {
         return contextPath;
     }
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
index 7672df1..b8cf2a8 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java
@@ -32,7 +32,9 @@ import org.keycloak.account.freemarker.model.ApplicationsBean;
 import org.keycloak.events.Details;
 import org.keycloak.events.Event;
 import org.keycloak.events.EventType;
+import org.keycloak.migration.MigrationModel;
 import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.PasswordPolicy;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.UserCredentialModel;
@@ -169,6 +171,15 @@ public class AccountTest {
     }
 
     @Test
+    public void testMigrationModel() {
+        KeycloakSession keycloakSession = keycloakRule.startSession();
+        Assert.assertEquals(keycloakSession.realms().getMigrationModel().getStoredVersion(), MigrationModel.LATEST_VERSION);
+        keycloakSession.close();
+    }
+
+
+
+    @Test
     public void returnToAppFromQueryParam() {
         driver.navigate().to(AccountUpdateProfilePage.PATH + "?referrer=test-app");
         loginPage.login("test-user@localhost", "password");
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java
index 6b294e2..72e743b 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/AbstractIdentityProviderTest.java
@@ -17,11 +17,6 @@
  */
 package org.keycloak.testsuite.broker;
 
-import org.apache.commons.io.IOUtils;
-import org.apache.http.HttpResponse;
-import org.apache.http.client.HttpClient;
-import org.apache.http.client.methods.HttpGet;
-import org.apache.http.impl.client.DefaultHttpClient;
 import org.codehaus.jackson.map.ObjectMapper;
 import org.junit.After;
 import org.junit.Assert;
@@ -29,7 +24,6 @@ import org.junit.Before;
 import org.junit.ClassRule;
 import org.junit.Rule;
 import org.junit.Test;
-import org.keycloak.OAuth2Constants;
 import org.keycloak.models.ClientModel;
 import org.keycloak.models.Constants;
 import org.keycloak.models.FederatedIdentityModel;
@@ -41,9 +35,7 @@ import org.keycloak.models.UserModel;
 import org.keycloak.models.UserModel.RequiredAction;
 import org.keycloak.representations.IDToken;
 import org.keycloak.services.Urls;
-import org.keycloak.services.resources.IdentityBrokerService;
 import org.keycloak.testsuite.OAuthClient;
-import org.keycloak.testsuite.OAuthClient.AccessTokenResponse;
 import org.keycloak.testsuite.broker.util.UserSessionStatusServlet.UserSessionStatus;
 import org.keycloak.testsuite.pages.AccountFederatedIdentityPage;
 import org.keycloak.testsuite.pages.AccountPasswordPage;
@@ -68,7 +60,6 @@ import javax.ws.rs.core.Response.Status;
 import javax.ws.rs.core.UriBuilder;
 import java.io.IOException;
 import java.net.URI;
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Set;
 
@@ -423,7 +414,7 @@ public abstract class AbstractIdentityProviderTest {
 
     protected void configureClientRetrieveToken(String clientId) {
         RealmModel realm = getRealm();
-        RoleModel readTokenRole = realm.getClientByClientId(Constants.BROKER_SERVICE_CLIENT_ID).getRole(IdentityBrokerService.READ_TOKEN_ROLE);
+        RoleModel readTokenRole = realm.getClientByClientId(Constants.BROKER_SERVICE_CLIENT_ID).getRole(Constants.READ_TOKEN_ROLE);
         ClientModel client = realm.getClientByClientId(clientId);
         if (!client.hasScope(readTokenRole)) client.addScopeMapping(readTokenRole);
 
@@ -435,7 +426,7 @@ public abstract class AbstractIdentityProviderTest {
     protected void configureUserRetrieveToken(String username) {
         RealmModel realm = getRealm();
         UserModel user = session.users().getUserByUsername(username, realm);
-        RoleModel readTokenRole = realm.getClientByClientId(Constants.BROKER_SERVICE_CLIENT_ID).getRole(IdentityBrokerService.READ_TOKEN_ROLE);
+        RoleModel readTokenRole = realm.getClientByClientId(Constants.BROKER_SERVICE_CLIENT_ID).getRole(Constants.READ_TOKEN_ROLE);
         if (user != null && !user.hasRole(readTokenRole)) {
             user.grantRole(readTokenRole);
         }
@@ -446,7 +437,7 @@ public abstract class AbstractIdentityProviderTest {
 
     protected void unconfigureClientRetrieveToken(String clientId) {
         RealmModel realm = getRealm();
-        RoleModel readTokenRole = realm.getClientByClientId(Constants.BROKER_SERVICE_CLIENT_ID).getRole(IdentityBrokerService.READ_TOKEN_ROLE);
+        RoleModel readTokenRole = realm.getClientByClientId(Constants.BROKER_SERVICE_CLIENT_ID).getRole(Constants.READ_TOKEN_ROLE);
         ClientModel client = realm.getClientByClientId(clientId);
         if (client.hasScope(readTokenRole)) client.deleteScopeMapping(readTokenRole);
 
@@ -458,7 +449,7 @@ public abstract class AbstractIdentityProviderTest {
     protected void unconfigureUserRetrieveToken(String username) {
         RealmModel realm = getRealm();
         UserModel user = session.users().getUserByUsername(username, realm);
-        RoleModel readTokenRole = realm.getClientByClientId(Constants.BROKER_SERVICE_CLIENT_ID).getRole(IdentityBrokerService.READ_TOKEN_ROLE);
+        RoleModel readTokenRole = realm.getClientByClientId(Constants.BROKER_SERVICE_CLIENT_ID).getRole(Constants.READ_TOKEN_ROLE);
         if (user != null && user.hasRole(readTokenRole)) {
             user.deleteRoleMapping(readTokenRole);
         }