keycloak-uncached
Changes
examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/CompanyRepresentation.java 17(+12 -5)
examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/entities/Company.java 110(+0 -110)
examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/entities/UserAccount.java 73(+0 -73)
examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/entities/UserAccountRegionRole.java 79(+0 -79)
examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/Company.java 46(+25 -21)
examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/ExampleJpaEntityProvider.java 16(+8 -8)
examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/jpa/ExampleJpaEntityProviderFactory.java 4(+2 -2)
examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/CompanyResource.java 41(+26 -15)
examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRealmResourceProvider.java 3(+1 -2)
examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRealmResourceProviderFactory.java 2(+1 -1)
examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRestResource.java 25(+24 -1)
examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/services/repository/AbstractRepository.java 51(+0 -51)
examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleService.java 10(+5 -5)
examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleServiceProviderFactory.java 3(+1 -2)
examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/ExampleSpi.java 3(+1 -2)
examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/impl/ExampleServiceImpl.java 41(+29 -12)
examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/spi/impl/ExampleServiceProviderFactoryImpl.java 8(+4 -4)
examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory 2(+1 -1)
examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.examples.domainextension.spi.ExampleServiceProviderFactory 2(+1 -1)
examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.provider.Spi 2(+1 -1)
examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory 2(+1 -1)
model/jpa/pom.xml 5(+5 -0)
model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/DefaultLiquibaseConnectionProvider.java 79(+15 -64)
model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/LiquibaseConnectionProvider.java 2(+2 -0)
model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java 104(+75 -29)
testsuite/integration/pom.xml 4(+4 -0)
Details
diff --git a/examples/providers/domain-extension/invoke-authenticated.sh b/examples/providers/domain-extension/invoke-authenticated.sh
new file mode 100755
index 0000000..19b56b1
--- /dev/null
+++ b/examples/providers/domain-extension/invoke-authenticated.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+export DIRECT_GRANT_RESPONSE=$(curl -i --request POST http://localhost:8080/auth/realms/master/protocol/openid-connect/token --header "Accept: application/json" --header "Content-Type: application/x-www-form-urlencoded" --data "grant_type=password&username=admin&password=admin&client_id=admin-cli")
+
+echo -e "\n\nSENT RESOURCE-OWNER-PASSWORD-CREDENTIALS-REQUEST. OUTPUT IS:\n\n";
+echo $DIRECT_GRANT_RESPONSE;
+
+export ACCESS_TOKEN=$(echo $DIRECT_GRANT_RESPONSE | grep "access_token" | sed 's/.*\"access_token\":\"\([^\"]*\)\".*/\1/g');
+echo -e "\n\nACCESS TOKEN IS \"$ACCESS_TOKEN\"";
+
+echo -e "\n\nSENDING UN-AUTHENTICATED REQUEST. THIS SHOULD FAIL WITH 401: ";
+curl -i --request POST http://localhost:8080/auth/realms/master/example/companies-auth --data "{ \"name\": \"auth foo company\" }" --header "Content-type: application/json"
+
+echo -e "\n\nSENDING AUTHENTICATED REQUEST. THIS SHOULD SUCCESSFULY CREATE COMPANY AND SUCCESS WITH 201: ";
+curl -i --request POST http://localhost:8080/auth/realms/master/example/companies-auth --data "{ \"name\": \"auth foo company\" }" --header "Content-type: application/json" --header "Authorization: Bearer $ACCESS_TOKEN";
+
+echo -e "\n\nSEARCH COMPANIES: ";
+curl -i --request GET http://localhost:8080/auth/realms/master/example/companies-auth --header "Accept: application/json" --header "Authorization: Bearer $ACCESS_TOKEN";
+
                diff --git a/examples/providers/domain-extension/README.md b/examples/providers/domain-extension/README.md
index c8163cd..e1aa2cd 100644
--- a/examples/providers/domain-extension/README.md
+++ b/examples/providers/domain-extension/README.md
@@ -3,7 +3,8 @@ Example Domain Extension
 
 To run, deploy as a module by running:
 
-    $KEYCLOAK_HOME/bin/jboss-cli.sh --command="module add --name=org.keycloak.examples.domain-extension-example --resources=target/domain-extension-example.jar --dependencies=org.keycloak.keycloak-core,org.keycloak.keycloak-services,org.keycloak.keycloak-model-jpa,org.keycloak.keycloak-server-spi,javax.ws.rs.api"
+    $KEYCLOAK_HOME/bin/jboss-cli.sh --command="module add --name=org.keycloak.examples.domain-extension-example --resources=target/domain-extension-example.jar --dependencies=org.keycloak.keycloak-core,org.keycloak.keycloak-services,org.keycloak.keycloak-model-jpa,org.keycloak.keycloak-server-spi,javax.ws.rs.api,javax.persistence.api,org.hibernate,org.javassist"
+
 
 Then registering the provider by editing keycloak-server.json and adding the module to the providers field:
 
@@ -12,4 +13,32 @@ Then registering the provider by editing keycloak-server.json and adding the mod
         "module:org.keycloak.examples.domain-extension-example"
     ],
 
-Then start (or restart) the server. Once started do xyz TODO.
\ No newline at end of file
+Then start (or restart) the server.
+
+Testing
+-------
+First you can create some example companies with these CURL requests.
+
+````
+curl -i --request POST http://localhost:8080/auth/realms/master/example/companies --data "{ \"name\": \"foo company\" }" --header "Content-type: application/json"
+curl -i --request POST http://localhost:8080/auth/realms/master/example/companies --data "{ \"name\": \"bar company\" }" --header "Content-type: application/json"
+````
+
+Then you can lookup all companies 
+
+````
+curl -i --request GET http://localhost:8080/auth/realms/master/example/companies --header "Accept: application/json"
+````
+
+If you create realm `foo` in Keycloak admin console and then replace the realm name in the URI (for example like `http://localhost:8080/auth/realms/foo/example/companies` ) you will see
+that companies are scoped per-realm. So you will see different companies for realm `master` and for realm `foo` .
+
+
+Testing with authenticated access
+---------------------------------
+Example contains the endpoint, which is accessible just for authenticated users. REST request must be authenticated with bearer access token
+of authenticated user and the user must be in realm role `admin` in order to access the resource. You can run bash script from the current directory:
+````
+./invoke-authenticated.sh
+````
+The script assumes user `admin` with password `admin` exists in realm `master`. Also it assumes that you have `curl` installed.
\ No newline at end of file
                diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/CompanyResource.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/CompanyResource.java
index b3dab2b..8236264 100644
--- a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/CompanyResource.java
+++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/CompanyResource.java
@@ -1,21 +1,24 @@
 package org.keycloak.examples.domainextension.rest;
 
-import java.util.HashSet;
 import java.util.List;
-import java.util.Set;
 
+import javax.ws.rs.Consumes;
 import javax.ws.rs.GET;
+import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
 
-import org.keycloak.examples.domainextension.entities.Company;
-import org.keycloak.examples.domainextension.rest.model.CompanyView;
-import org.keycloak.examples.domainextension.services.ExampleService;
+import org.jboss.resteasy.annotations.cache.NoCache;
+import org.keycloak.examples.domainextension.CompanyRepresentation;
+import org.keycloak.examples.domainextension.spi.ExampleService;
 import org.keycloak.models.KeycloakSession;
 
 public class CompanyResource {
 
-	private KeycloakSession session;
+	private final KeycloakSession session;
 	
 	public CompanyResource(KeycloakSession session) {
 		this.session = session;
@@ -23,19 +26,27 @@ public class CompanyResource {
 
     @GET
     @Path("")
-    public Set<CompanyView> getMasterAccounts() {
-        List<Company> companies = session.getProvider(ExampleService.class).listCompanies();
-        Set<CompanyView> companyViews = new HashSet<>();
-        for (Company company : companies) {
-        	companyViews.add(new CompanyView(company));
-        }
-        return companyViews;
+    @NoCache
+    @Produces(MediaType.APPLICATION_JSON)
+    public List<CompanyRepresentation> getCompanies() {
+        return session.getProvider(ExampleService.class).listCompanies();
+    }
+
+    @POST
+    @Path("")
+    @NoCache
+    @Consumes(MediaType.APPLICATION_JSON)
+    public Response createProviderInstance(CompanyRepresentation rep) {
+        session.getProvider(ExampleService.class).addCompany(rep);
+        return Response.created(session.getContext().getUri().getAbsolutePathBuilder().path(rep.getId()).build()).build();
     }
 
     @GET
+    @NoCache
     @Path("{id}")
-    public CompanyView getCompany(@PathParam("id") final String id) {
-        return new CompanyView(session.getProvider(ExampleService.class).findCompany(id));
+    @Produces(MediaType.APPLICATION_JSON)
+    public CompanyRepresentation getCompany(@PathParam("id") final String id) {
+        return session.getProvider(ExampleService.class).findCompany(id);
     }
 
 }
\ No newline at end of file
                diff --git a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRestResource.java b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRestResource.java
index 4309810..db774cf 100644
--- a/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRestResource.java
+++ b/examples/providers/domain-extension/src/main/java/org/keycloak/examples/domainextension/rest/ExampleRestResource.java
@@ -1,15 +1,22 @@
 package org.keycloak.examples.domainextension.rest;
 
+import javax.ws.rs.ForbiddenException;
+import javax.ws.rs.NotAuthorizedException;
 import javax.ws.rs.Path;
 
+
 import org.keycloak.models.KeycloakSession;
+import org.keycloak.services.managers.AppAuthManager;
+import org.keycloak.services.managers.AuthenticationManager;
 
 public class ExampleRestResource {
 
-	private KeycloakSession session;
+	private final KeycloakSession session;
+    private final AuthenticationManager.AuthResult auth;
 	
 	public ExampleRestResource(KeycloakSession session) {
 		this.session = session;
+        this.auth = new AppAuthManager().authenticateBearerToken(session, session.getContext().getRealm());
 	}
 	
     @Path("companies")
@@ -17,4 +24,20 @@ public class ExampleRestResource {
         return new CompanyResource(session);
     }
 
+    // Same like "companies" endpoint, but REST endpoint is authenticated with Bearer token and user must be in realm role "admin"
+    // Just for illustration purposes
+    @Path("companies-auth")
+    public CompanyResource getCompanyResourceAuthenticated() {
+        checkRealmAdmin();
+        return new CompanyResource(session);
+    }
+
+    private void checkRealmAdmin() {
+        if (auth == null) {
+            throw new NotAuthorizedException("Bearer");
+        } else if (auth.getToken().getRealmAccess() == null || !auth.getToken().getRealmAccess().isUserInRole("admin")) {
+            throw new ForbiddenException("Does not have realm admin role");
+        }
+    }
+
 }
                diff --git a/examples/providers/domain-extension/src/main/resources/META-INF/example-changelog.xml b/examples/providers/domain-extension/src/main/resources/META-INF/example-changelog.xml
index 3d96bdb..ec4e5a1 100644
--- a/examples/providers/domain-extension/src/main/resources/META-INF/example-changelog.xml
+++ b/examples/providers/domain-extension/src/main/resources/META-INF/example-changelog.xml
@@ -16,7 +16,7 @@
 
         <addPrimaryKey
             constraintName="PK_COMPANY"
-            tableName="DOCDATA_COMPANY"
+            tableName="EXAMPLE_COMPANY"
             columnNames="ID"
         />
 
@@ -27,117 +27,7 @@
             referencedTableName="REALM"
             referencedColumnNames="ID"
         />
-        
-        <createTable tableName="EXAMPLE_REGION">
-            <column name="ID" type="VARCHAR(36)">
-                <constraints nullable="false"/>
-            </column>
-            <column name="NAME" type="VARCHAR(255)">
-                <constraints nullable="false"/>
-            </column>
-            <column name="COMPANY_ID" type="VARCHAR(36)">
-                <constraints nullable="false"/>
-            </column>
-        </createTable>
-
-        <addPrimaryKey
-            constraintName="PK_REGION"
-            tableName="EXAMPLE_REGION"
-            columnNames="ID"
-        />
-
-        <addForeignKeyConstraint
-            constraintName="FK_REGION_COMPANY"
-            baseTableName="EXAMPLE_REGION"
-            baseColumnNames="COMPANY_ID"
-            referencedTableName="EXAMPLE_COMPANY"
-            referencedColumnNames="ID"
-        />
-
-        <createTable tableName="EXAMPLE_USER_ACCOUNT">
-            <column name="ID" type="VARCHAR(36)">
-                <constraints nullable="false"/>
-            </column>
-            <column name="USER_ID" type="VARCHAR(36)">
-                <constraints nullable="false"/>
-            </column>
-            <column name="COMPANY_ID" type="VARCHAR(36)">
-                <constraints nullable="false"/>
-            </column>
-        </createTable>
-
-        <addPrimaryKey
-            constraintName="PK_USER_ACCOUNT"
-            tableName="EXAMPLE_USER_ACCOUNT"
-            columnNames="ID"
-        />
-
-        <addUniqueConstraint
-            constraintName="UC_USER_ACCOUNT_USER_ID"
-            tableName="EXAMPLE_USER_ACCOUNT"
-            columnNames="USER_ID"
-        />
-
-        <addForeignKeyConstraint
-            constraintName="FK_USER_ACCOUNT_USER_ENTITY"
-            baseTableName="EXAMPLE_USER_ACCOUNT"
-            baseColumnNames="USER_ID"
-            referencedTableName="USER_ENTITY"
-            referencedColumnNames="ID"
-        />
-
-        <addForeignKeyConstraint
-            constraintName="FK_USER_ACCOUNT_COMPANY"
-            baseTableName="EXAMPLE_USER_ACCOUNT"
-            baseColumnNames="COMPANY_ID"
-            referencedTableName="EXAMPLE_COMPANY"
-            referencedColumnNames="ID"
-        />
-
-        <createTable tableName="EXAMPLE_USER_ACCOUNT_REGION_ROLE">
-            <column name="ID" type="VARCHAR(36)">
-                <constraints nullable="false" />
-            </column>
-            <column name="USER_ACCOUNT_ID" type="VARCHAR(36)">
-                <constraints nullable="false" />
-            </column>
-            <column name="REGION_ID" type="VARCHAR(36)">
-                <constraints nullable="false" />
-            </column>
-            <column name="ROLE_ID" type="VARCHAR(36)">
-                <constraints nullable="false" />
-            </column>
-        </createTable>
 
-        <addPrimaryKey
-            constraintName="PK_USER_ACCOUNT_REGION_ROLE"
-            tableName="EXAMPLE_USER_ACCOUNT_REGION_ROLE"
-            columnNames="ID"
-        />
-
-        <addForeignKeyConstraint
-            constraintName="FK_USER_ACCOUNT_REGION_ROLE_USER_ACCOUNT"
-            baseTableName="EXAMPLE_USER_ACCOUNT_REGION_ROLE"
-            baseColumnNames="USER_ACCOUNT_ID"
-            referencedTableName="EXAMPLE_USER_ACCOUNT"
-            referencedColumnNames="ID"
-        />
-
-        <addForeignKeyConstraint
-            constraintName="FK_USER_ACCOUNT_REGION_ROLE_REGION"
-            baseTableName="EXAMPLE_USER_ACCOUNT_REGION_ROLE"
-            baseColumnNames="REGION_ID"
-            referencedTableName="EXAMPLE_REGION"
-            referencedColumnNames="ID"
-        />
-
-        <addForeignKeyConstraint
-            constraintName="FK_USER_ACCOUNT_REGION_ROLE_ROLE"
-            baseTableName="EXAMPLE_USER_ACCOUNT_REGION_ROLE"
-            baseColumnNames="ROLE_ID"
-            referencedTableName="KEYCLOAK_ROLE"
-            referencedColumnNames="ID"
-        />
     </changeSet>
     
 </databaseChangeLog>
                diff --git a/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory b/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory
index 19058b3..c7b88db 100644
--- a/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory
+++ b/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.connections.jpa.entityprovider.JpaEntityProviderFactory
@@ -15,4 +15,4 @@
 # limitations under the License.
 #
 
-org.keycloak.examples.domainextension.DomainExtensionProviderFactory
+org.keycloak.examples.domainextension.jpa.ExampleJpaEntityProviderFactory
                diff --git a/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.provider.Spi
index 47b695e..e013bbd 100644
--- a/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.provider.Spi
+++ b/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.provider.Spi
@@ -15,4 +15,4 @@
 # limitations under the License.
 #
 
-org.keycloak.examples.domainextension.services.spi.ExampleSpi
+org.keycloak.examples.domainextension.spi.ExampleSpi
                diff --git a/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory b/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory
index f46b8a6..ea81617 100644
--- a/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory
+++ b/examples/providers/domain-extension/src/main/resources/META-INF/services/org.keycloak.services.resource.RealmResourceProviderFactory
@@ -15,4 +15,4 @@
 # limitations under the License.
 #
 
-org.keycloak.examples.domainextension.rest.ExampleResourceProviderFactory
\ No newline at end of file
+org.keycloak.examples.domainextension.rest.ExampleRealmResourceProviderFactory
\ No newline at end of file
                model/jpa/pom.xml 5(+5 -0)
diff --git a/model/jpa/pom.xml b/model/jpa/pom.xml
index 47035c5..e1c8742 100755
--- a/model/jpa/pom.xml
+++ b/model/jpa/pom.xml
@@ -102,6 +102,11 @@
             <artifactId>jackson-core</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
     <build>
         <plugins>
                diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntityProvider.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntityProvider.java
index c64c965..567080e 100644
--- a/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntityProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/entityprovider/JpaEntityProvider.java
@@ -44,4 +44,10 @@ public interface JpaEntityProvider extends Provider {
 	 */
 	String getChangelogLocation();
 
+	/**
+	 * Return the ID of provider factory, which created this provider. Might be used to "compute" the table name of liquibase changelog table.
+	 * @return ID of provider factory
+	 */
+	String getFactoryId();
+
 }
                diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/DefaultLiquibaseConnectionProvider.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/DefaultLiquibaseConnectionProvider.java
index ca6d722..5a58702 100644
--- a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/DefaultLiquibaseConnectionProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/DefaultLiquibaseConnectionProvider.java
@@ -18,25 +18,18 @@
 package org.keycloak.connections.jpa.updater.liquibase.conn;
 
 import java.sql.Connection;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Set;
 
 import org.jboss.logging.Logger;
 import org.keycloak.Config;
-import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider;
-import org.keycloak.connections.jpa.entityprovider.ProxyClassLoader;
 import org.keycloak.connections.jpa.updater.liquibase.LiquibaseJpaUpdaterProvider;
 import org.keycloak.connections.jpa.updater.liquibase.PostgresPlusDatabase;
 import org.keycloak.connections.jpa.updater.liquibase.lock.CustomInsertLockRecordGenerator;
 import org.keycloak.connections.jpa.updater.liquibase.lock.CustomLockDatabaseChangeLogGenerator;
 import org.keycloak.connections.jpa.updater.liquibase.lock.DummyLockService;
-import org.keycloak.connections.jpa.util.JpaUtils;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.KeycloakSessionFactory;
 
 import liquibase.Liquibase;
-import liquibase.changelog.ChangeLogParameters;
 import liquibase.changelog.ChangeSet;
 import liquibase.changelog.DatabaseChangeLog;
 import liquibase.database.Database;
@@ -46,8 +39,6 @@ import liquibase.database.jvm.JdbcConnection;
 import liquibase.exception.LiquibaseException;
 import liquibase.logging.LogFactory;
 import liquibase.logging.LogLevel;
-import liquibase.parser.ChangeLogParser;
-import liquibase.parser.ChangeLogParserFactory;
 import liquibase.resource.ClassLoaderResourceAccessor;
 import liquibase.resource.ResourceAccessor;
 import liquibase.servicelocator.ServiceLocator;
@@ -61,12 +52,9 @@ public class DefaultLiquibaseConnectionProvider implements LiquibaseConnectionPr
     private static final Logger logger = Logger.getLogger(DefaultLiquibaseConnectionProvider.class);
 
     private volatile boolean initialized = false;
-
-    private KeycloakSession keycloakSession;
     
     @Override
     public LiquibaseConnectionProvider create(KeycloakSession session) {
-    	this.keycloakSession = session;
         if (!initialized) {
             synchronized (this) {
                 if (!initialized) {
@@ -143,63 +131,26 @@ public class DefaultLiquibaseConnectionProvider implements LiquibaseConnectionPr
         }
 
         String changelog = (database instanceof DB2Database) ? LiquibaseJpaUpdaterProvider.DB2_CHANGELOG :  LiquibaseJpaUpdaterProvider.CHANGELOG;
-        logger.debugf("Using changelog file: %s", changelog);
-
         ResourceAccessor resourceAccessor = new ClassLoaderResourceAccessor(getClass().getClassLoader());
-        DatabaseChangeLog databaseChangeLog = generateDynamicChangeLog(changelog, resourceAccessor, database);
+
+        logger.debugf("Using changelog file %s and changelogTableName %s", changelog, database.getDatabaseChangeLogTableName());
         
-        return new Liquibase(databaseChangeLog, resourceAccessor, database);
+        return new Liquibase(changelog, resourceAccessor, database);
     }
 
-    /**
-     * We want to be able to provide extra changesets as an extension to the Keycloak data model.
-     * But we do not want users to be able to not execute certain parts of the Keycloak internal data model.
-     * Therefore, we generate a dynamic changelog here that always contains the keycloak changelog file
-     * and optionally include the user extension changelog files.
-     * 
-     * @param changelog the changelog file location
-     * @param resourceAccessor the resource accessor
-     * @param database the database
-     * @return
-     */
-    private DatabaseChangeLog generateDynamicChangeLog(String changelog, ResourceAccessor resourceAccessor, Database database) throws LiquibaseException {
-    	ChangeLogParameters changeLogParameters = new ChangeLogParameters(database);
-        ChangeLogParser parser = ChangeLogParserFactory.getInstance().getParser(changelog, resourceAccessor);
-        DatabaseChangeLog keycloakDatabaseChangeLog = parser.parse(changelog, changeLogParameters, resourceAccessor);
-
-        List<String> locations = new ArrayList<>();
-        Set<JpaEntityProvider> entityProviders = keycloakSession.getAllProviders(JpaEntityProvider.class);
-        for (JpaEntityProvider entityProvider : entityProviders) {
-            String location = entityProvider.getChangelogLocation();
-            if (location != null) {
-            	locations.add(location);
-            }
-        }
-        
-        final DatabaseChangeLog dynamicMasterChangeLog;
-        if (locations.isEmpty()) {
-        	// If there are no extra changelog locations, we'll just use the keycloak one.
-        	dynamicMasterChangeLog = keycloakDatabaseChangeLog;
-        } else {
-        	// A change log is essentially not much more than a (big) collection of changesets.
-        	// The original (file) destination is not important. So we can just make one big dynamic change log that include all changesets.
-            dynamicMasterChangeLog = new DatabaseChangeLog();
-            dynamicMasterChangeLog.setChangeLogParameters(changeLogParameters);
-            for (ChangeSet changeSet : keycloakDatabaseChangeLog.getChangeSets()) {
-            	dynamicMasterChangeLog.addChangeSet(changeSet);
-            }
-            ProxyClassLoader proxyClassLoader = new ProxyClassLoader(JpaUtils.getProvidedEntities(keycloakSession));
-            for (String location : locations) {
-            	ResourceAccessor proxyResourceAccessor = new ClassLoaderResourceAccessor(proxyClassLoader);
-                ChangeLogParser locationParser = ChangeLogParserFactory.getInstance().getParser(location, proxyResourceAccessor);
-                DatabaseChangeLog locationDatabaseChangeLog = locationParser.parse(location, changeLogParameters, proxyResourceAccessor);
-                for (ChangeSet changeSet : locationDatabaseChangeLog.getChangeSets()) {
-                	dynamicMasterChangeLog.addChangeSet(changeSet);
-                }
-            }
+    @Override
+    public Liquibase getLiquibaseForCustomUpdate(Connection connection, String defaultSchema, String changelogLocation, ClassLoader classloader, String changelogTableName) throws LiquibaseException {
+        Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(connection));
+        if (defaultSchema != null) {
+            database.setDefaultSchemaName(defaultSchema);
         }
-        
-        return dynamicMasterChangeLog;
+
+        ResourceAccessor resourceAccessor = new ClassLoaderResourceAccessor(classloader);
+        database.setDatabaseChangeLogTableName(changelogTableName);
+
+        logger.debugf("Using changelog file %s and changelogTableName %s", changelogLocation, database.getDatabaseChangeLogTableName());
+
+        return new Liquibase(changelogLocation, resourceAccessor, database);
     }
 
     private static class LogWrapper extends LogFactory {
                diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/LiquibaseConnectionProvider.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/LiquibaseConnectionProvider.java
index 5aa81cc..215bd1d 100644
--- a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/LiquibaseConnectionProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/conn/LiquibaseConnectionProvider.java
@@ -30,4 +30,6 @@ public interface LiquibaseConnectionProvider extends Provider {
 
     Liquibase getLiquibase(Connection connection, String defaultSchema) throws LiquibaseException;
 
+    Liquibase getLiquibaseForCustomUpdate(Connection connection, String defaultSchema, String changelogLocation, ClassLoader classloader, String changelogTableName) throws LiquibaseException;
+
 }
                diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java
index 8e9d253..2610174 100755
--- a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProvider.java
@@ -23,12 +23,17 @@ import liquibase.changelog.ChangeSet;
 import liquibase.changelog.RanChangeSet;
 import liquibase.exception.LiquibaseException;
 import org.jboss.logging.Logger;
+import org.keycloak.common.util.reflections.Reflections;
+import org.keycloak.connections.jpa.entityprovider.JpaEntityProvider;
 import org.keycloak.connections.jpa.updater.JpaUpdaterProvider;
 import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProvider;
+import org.keycloak.connections.jpa.util.JpaUtils;
 import org.keycloak.models.KeycloakSession;
 
+import java.lang.reflect.Method;
 import java.sql.Connection;
 import java.util.List;
+import java.util.Set;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -54,25 +59,20 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
         ThreadLocalSessionContext.setCurrentSession(session);
 
         try {
-            Liquibase liquibase = getLiquibase(connection, defaultSchema);
-
-            List<ChangeSet> changeSets = liquibase.listUnrunChangeSets((Contexts) null);
-            if (!changeSets.isEmpty()) {
-                if (changeSets.get(0).getId().equals(FIRST_VERSION)) {
-                    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");
-                    }
+            // Run update with keycloak master changelog first
+            Liquibase liquibase = getLiquibaseForKeycloakUpdate(connection, defaultSchema);
+            updateChangeSet(liquibase, liquibase.getChangeLogFile());
+
+            // Run update for each custom JpaEntityProvider
+            Set<JpaEntityProvider> jpaProviders = session.getAllProviders(JpaEntityProvider.class);
+            for (JpaEntityProvider jpaProvider : jpaProviders) {
+                String customChangelog = jpaProvider.getChangelogLocation();
+                if (customChangelog != null) {
+                    String factoryId = jpaProvider.getFactoryId();
+                    String changelogTableName = JpaUtils.getCustomChangelogTableName(factoryId);
+                    liquibase = getLiquibaseForCustomProviderUpdate(connection, defaultSchema, customChangelog, jpaProvider.getClass().getClassLoader(), changelogTableName);
+                    updateChangeSet(liquibase, liquibase.getChangeLogFile());
                 }
-
-                liquibase.update((Contexts) null);
-                logger.debug("Completed database update");
-            } else {
-                logger.debug("Database is up to date");
             }
         } catch (Exception e) {
             throw new RuntimeException("Failed to update database", e);
@@ -81,21 +81,50 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
         }
     }
 
+    protected void updateChangeSet(Liquibase liquibase, String changelog) throws LiquibaseException {
+        List<ChangeSet> changeSets = liquibase.listUnrunChangeSets((Contexts) null);
+        if (!changeSets.isEmpty()) {
+            List<RanChangeSet> ranChangeSets = liquibase.getDatabase().getRanChangeSetList();
+            if (ranChangeSets.isEmpty()) {
+                logger.infov("Initializing database schema. Using changelog {0}", changelog);
+            } else {
+                if (logger.isDebugEnabled()) {
+                    logger.debugv("Updating database from {0} to {1}. Using changelog {2}", ranChangeSets.get(ranChangeSets.size() - 1).getId(), changeSets.get(changeSets.size() - 1).getId(), changelog);
+                } else {
+                    logger.infov("Updating database. Using changelog {0}", changelog);
+                }
+            }
+
+            liquibase.update((Contexts) null);
+            logger.debugv("Completed database update for changelog {0}", changelog);
+        } else {
+            logger.debugv("Database is up to date for changelog {0}", changelog);
+
+            // Needs to restart liquibase services to clear changeLogHistory.
+            Method resetServices = Reflections.findDeclaredMethod(Liquibase.class, "resetServices");
+            Reflections.invokeMethod(true, resetServices, liquibase);
+        }
+    }
+
     @Override
     public void validate(Connection connection, String defaultSchema) {
         logger.debug("Validating if database is updated");
 
         try {
-            Liquibase liquibase = getLiquibase(connection, defaultSchema);
-
-            List<ChangeSet> changeSets = liquibase.listUnrunChangeSets((Contexts) null);
-            if (!changeSets.isEmpty()) {
-                List<RanChangeSet> ranChangeSets = liquibase.getDatabase().getRanChangeSetList();
-                String errorMessage = String.format("Failed to validate database schema. Schema needs updating database from %s to %s. Please change databaseSchema to 'update' or use other database",
-                        ranChangeSets.get(ranChangeSets.size() - 1).getId(), changeSets.get(changeSets.size() - 1).getId());
-                throw new RuntimeException(errorMessage);
-            } else {
-                logger.debug("Validation passed. Database is up-to-date");
+            // Validate with keycloak master changelog first
+            Liquibase liquibase = getLiquibaseForKeycloakUpdate(connection, defaultSchema);
+            validateChangeSet(liquibase, liquibase.getChangeLogFile());
+
+            // Validate each custom JpaEntityProvider
+            Set<JpaEntityProvider> jpaProviders = session.getAllProviders(JpaEntityProvider.class);
+            for (JpaEntityProvider jpaProvider : jpaProviders) {
+                String customChangelog = jpaProvider.getChangelogLocation();
+                if (customChangelog != null) {
+                    String factoryId = jpaProvider.getFactoryId();
+                    String changelogTableName = JpaUtils.getCustomChangelogTableName(factoryId);
+                    liquibase = getLiquibaseForCustomProviderUpdate(connection, defaultSchema, customChangelog, jpaProvider.getClass().getClassLoader(), changelogTableName);
+                    validateChangeSet(liquibase, liquibase.getChangeLogFile());
+                }
             }
 
         } catch (LiquibaseException e) {
@@ -103,11 +132,28 @@ public class LiquibaseJpaUpdaterProvider implements JpaUpdaterProvider {
         }
     }
 
-    private Liquibase getLiquibase(Connection connection, String defaultSchema) throws LiquibaseException {
+    protected void validateChangeSet(Liquibase liquibase, String changelog) throws LiquibaseException {
+        List<ChangeSet> changeSets = liquibase.listUnrunChangeSets((Contexts) null);
+        if (!changeSets.isEmpty()) {
+            List<RanChangeSet> ranChangeSets = liquibase.getDatabase().getRanChangeSetList();
+            String errorMessage = String.format("Failed to validate database schema. Schema needs updating database from %s to %s. Please change databaseSchema to 'update' or use other database. Used changelog was %s",
+                    ranChangeSets.get(ranChangeSets.size() - 1).getId(), changeSets.get(changeSets.size() - 1).getId(), changelog);
+            throw new RuntimeException(errorMessage);
+        } else {
+            logger.debugf("Validation passed. Database is up-to-date for changelog %s", changelog);
+        }
+    }
+
+    private Liquibase getLiquibaseForKeycloakUpdate(Connection connection, String defaultSchema) throws LiquibaseException {
         LiquibaseConnectionProvider liquibaseProvider = session.getProvider(LiquibaseConnectionProvider.class);
         return liquibaseProvider.getLiquibase(connection, defaultSchema);
     }
 
+    private Liquibase getLiquibaseForCustomProviderUpdate(Connection connection, String defaultSchema, String changelogLocation, ClassLoader classloader, String changelogTableName) throws LiquibaseException {
+        LiquibaseConnectionProvider liquibaseProvider = session.getProvider(LiquibaseConnectionProvider.class);
+        return liquibaseProvider.getLiquibaseForCustomUpdate(connection, defaultSchema, changelogLocation, classloader, changelogTableName);
+    }
+
     @Override
     public void close() {
     }
                diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/util/JpaUtils.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/util/JpaUtils.java
index d93c02b..5ac7d2f 100644
--- a/model/jpa/src/main/java/org/keycloak/connections/jpa/util/JpaUtils.java
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/util/JpaUtils.java
@@ -82,4 +82,16 @@ public class JpaUtils {
         return providedEntityClasses;
     }
 
+    /**
+     * Get the name of custom table for liquibase updates for give ID of JpaEntityProvider
+     * @param jpaEntityProviderFactoryId
+     * @return table name
+     */
+    public static String getCustomChangelogTableName(String jpaEntityProviderFactoryId) {
+        String upperCased = jpaEntityProviderFactoryId.toUpperCase();
+        upperCased = upperCased.replaceAll("-", "_");
+        upperCased = upperCased.replaceAll("[^A-Z_]", "");
+        return "DATABASECHANGELOG_" + upperCased.substring(0, Math.min(10, upperCased.length()));
+    }
+
 }
                testsuite/integration/pom.xml 4(+4 -0)
diff --git a/testsuite/integration/pom.xml b/testsuite/integration/pom.xml
index d394e2a..c1d0412 100755
--- a/testsuite/integration/pom.xml
+++ b/testsuite/integration/pom.xml
@@ -140,6 +140,10 @@
         </dependency>
         <dependency>
             <groupId>org.keycloak</groupId>
+            <artifactId>keycloak-authz-client</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.keycloak</groupId>
             <artifactId>keycloak-saml-servlet-filter-adapter</artifactId>
         </dependency>
         <dependency>