diff --git a/model/hybrid/pom.xml b/model/hybrid/pom.xml
new file mode 100755
index 0000000000..344fef31c7
--- /dev/null
+++ b/model/hybrid/pom.xml
@@ -0,0 +1,30 @@
+
+
+
+ keycloak-parent
+ org.keycloak
+ 1.0-beta-4-SNAPSHOT
+ ../../pom.xml
+
+ 4.0.0
+
+ keycloak-model-hybrid
+ Keycloak Model Hybrid
+
+
+
+
+ org.keycloak
+ keycloak-core
+ ${project.version}
+ provided
+
+
+ org.keycloak
+ keycloak-model-api
+ ${project.version}
+ provided
+
+
+
diff --git a/model/hybrid/src/main/java/org/keycloak/models/hybrid/ApplicationAdapter.java b/model/hybrid/src/main/java/org/keycloak/models/hybrid/ApplicationAdapter.java
new file mode 100644
index 0000000000..57ff498158
--- /dev/null
+++ b/model/hybrid/src/main/java/org/keycloak/models/hybrid/ApplicationAdapter.java
@@ -0,0 +1,140 @@
+package org.keycloak.models.hybrid;
+
+import org.keycloak.models.realms.Application;
+import org.keycloak.models.ApplicationModel;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author Stian Thorgersen
+ */
+public class ApplicationAdapter extends ClientAdapter implements ApplicationModel {
+
+ private Application application;
+
+ ApplicationAdapter(HybridModelProvider provider, Application application) {
+ super(provider, application);
+ this.application = application;
+ }
+
+ Application getApplication() {
+ return application;
+ }
+
+ @Override
+ public void updateApplication() {
+ application.updateApplication();
+ }
+
+ @Override
+ public String getName() {
+ return application.getName();
+ }
+
+ @Override
+ public void setName(String name) {
+ application.setName(name);
+ }
+
+ @Override
+ public boolean isSurrogateAuthRequired() {
+ return application.isSurrogateAuthRequired();
+ }
+
+ @Override
+ public void setSurrogateAuthRequired(boolean surrogateAuthRequired) {
+ application.setSurrogateAuthRequired(surrogateAuthRequired);
+ }
+
+ @Override
+ public String getManagementUrl() {
+ return application.getManagementUrl();
+ }
+
+ @Override
+ public void setManagementUrl(String url) {
+ application.setManagementUrl(url);
+ }
+
+ @Override
+ public String getBaseUrl() {
+ return application.getBaseUrl();
+ }
+
+ @Override
+ public void setBaseUrl(String url) {
+ application.setBaseUrl(url);
+ }
+
+ @Override
+ public List getDefaultRoles() {
+ return application.getDefaultRoles();
+ }
+
+ @Override
+ public void addDefaultRole(String name) {
+ if (getRole(name) == null) {
+ addRole(name);
+ }
+
+ application.addDefaultRole(name);
+ }
+
+ @Override
+ public void updateDefaultRoles(String[] defaultRoles) {
+ application.updateDefaultRoles(defaultRoles);
+ }
+
+ @Override
+ public Set getApplicationScopeMappings(ClientModel client) {
+ return provider.mappings().wrap(application.getApplicationScopeMappings(provider.mappings().unwrap(client)));
+ }
+
+ @Override
+ public boolean isBearerOnly() {
+ return application.isBearerOnly();
+ }
+
+ @Override
+ public void setBearerOnly(boolean only) {
+ application.setBearerOnly(only);
+ }
+
+ @Override
+ public RoleModel getRole(String name) {
+ return provider.mappings().wrap(application.getRole(name));
+ }
+
+ @Override
+ public RoleModel addRole(String name) {
+ return addRole(KeycloakModelUtils.generateId(), name);
+ }
+
+ @Override
+ public RoleModel addRole(String id, String name) {
+ return provider.mappings().wrap(application.addRole(id, name));
+ }
+
+ @Override
+ public boolean removeRole(RoleModel role) {
+ if (application.removeRole(provider.mappings().unwrap(role))) {
+ provider.users().onRoleRemoved(role.getId());
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public Set getRoles() {
+ return provider.mappings().wrap(application.getRoles());
+ }
+
+}
diff --git a/model/hybrid/src/main/java/org/keycloak/models/hybrid/ClientAdapter.java b/model/hybrid/src/main/java/org/keycloak/models/hybrid/ClientAdapter.java
new file mode 100644
index 0000000000..a35b662c12
--- /dev/null
+++ b/model/hybrid/src/main/java/org/keycloak/models/hybrid/ClientAdapter.java
@@ -0,0 +1,202 @@
+package org.keycloak.models.hybrid;
+
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.realms.Client;
+
+import java.util.Set;
+
+/**
+ * @author Stian Thorgersen
+ */
+public abstract class ClientAdapter implements ClientModel {
+
+ protected HybridModelProvider provider;
+
+ protected Client client;
+
+ ClientAdapter(HybridModelProvider provider, Client client) {
+ this.provider = provider;
+ this.client = client;
+ }
+
+ @Override
+ public String getId() {
+ return client.getId();
+ }
+
+ @Override
+ public String getClientId() {
+ return client.getClientId();
+ }
+
+ @Override
+ public long getAllowedClaimsMask() {
+ return client.getAllowedClaimsMask();
+ }
+
+ @Override
+ public void setAllowedClaimsMask(long mask) {
+ client.setAllowedClaimsMask(mask);
+ }
+
+ @Override
+ public Set getWebOrigins() {
+ return client.getWebOrigins();
+ }
+
+ @Override
+ public void setWebOrigins(Set webOrigins) {
+ client.setWebOrigins(webOrigins);
+ }
+
+ @Override
+ public void addWebOrigin(String webOrigin) {
+ client.addWebOrigin(webOrigin);
+ }
+
+ @Override
+ public void removeWebOrigin(String webOrigin) {
+ client.removeWebOrigin(webOrigin);
+ }
+
+ @Override
+ public Set getRedirectUris() {
+ return client.getRedirectUris();
+ }
+
+ @Override
+ public void setRedirectUris(Set redirectUris) {
+ client.setRedirectUris(redirectUris);
+ }
+
+ @Override
+ public void addRedirectUri(String redirectUri) {
+ client.addRedirectUri(redirectUri);
+ }
+
+ @Override
+ public void removeRedirectUri(String redirectUri) {
+ client.removeRedirectUri(redirectUri);
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return client.isEnabled();
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ client.setEnabled(enabled);
+ }
+
+ @Override
+ public boolean validateSecret(String secret) {
+ return client.validateSecret(secret);
+ }
+
+ @Override
+ public String getSecret() {
+ return client.getSecret();
+ }
+
+ @Override
+ public void setSecret(String secret) {
+ client.setSecret(secret);
+ }
+
+ @Override
+ public boolean isPublicClient() {
+ return client.isPublicClient();
+ }
+
+ @Override
+ public void setPublicClient(boolean flag) {
+ client.setPublicClient(flag);
+ }
+
+ @Override
+ public boolean isDirectGrantsOnly() {
+ return client.isDirectGrantsOnly();
+ }
+
+ @Override
+ public void setDirectGrantsOnly(boolean flag) {
+ client.setDirectGrantsOnly(flag);
+ }
+
+ @Override
+ public Set getScopeMappings() {
+ return provider.mappings().wrap(client.getScopeMappings());
+ }
+
+ @Override
+ public void addScopeMapping(RoleModel role) {
+ if (!hasScope(role)) {
+ client.addScopeMapping(provider.mappings().unwrap(role));
+ }
+ }
+
+ @Override
+ public void deleteScopeMapping(RoleModel role) {
+ client.deleteScopeMapping(provider.mappings().unwrap(role));
+ }
+
+ @Override
+ public Set getRealmScopeMappings() {
+ return provider.mappings().wrap(client.getRealmScopeMappings());
+ }
+
+ @Override
+ public boolean hasScope(RoleModel role) {
+ Set roles = getScopeMappings();
+ if (roles.contains(role)) {
+ return true;
+ }
+
+ for (RoleModel mapping : roles) {
+ if (mapping.hasRole(role)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ @Override
+ public RealmModel getRealm() {
+ return provider.mappings().wrap(client.getRealm());
+ }
+
+ @Override
+ public int getNotBefore() {
+ return client.getNotBefore();
+ }
+
+ @Override
+ public void setNotBefore(int notBefore) {
+ client.setNotBefore(notBefore);
+ }
+
+ @Override
+ public Set getUserSessions() {
+ return provider.mappings().wrapSessions(getRealm(), provider.sessions().getUserSessionsByClient(client.getRealm().getId(), client.getId()));
+ }
+
+ @Override
+ public int getActiveUserSessions() {
+ return provider.sessions().getActiveUserSessions(client.getRealm().getId(), client.getId());
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!this.getClass().equals(o.getClass())) return false;
+
+ ClientAdapter that = (ClientAdapter) o;
+ return that.getId().equals(getId());
+ }
+
+}
diff --git a/model/hybrid/src/main/java/org/keycloak/models/hybrid/HybridKeycloakTransaction.java b/model/hybrid/src/main/java/org/keycloak/models/hybrid/HybridKeycloakTransaction.java
new file mode 100644
index 0000000000..b6a54eaf09
--- /dev/null
+++ b/model/hybrid/src/main/java/org/keycloak/models/hybrid/HybridKeycloakTransaction.java
@@ -0,0 +1,65 @@
+package org.keycloak.models.hybrid;
+
+import org.keycloak.models.KeycloakTransaction;
+
+/**
+ * @author Stian Thorgersen
+ */
+public class HybridKeycloakTransaction implements KeycloakTransaction {
+
+ private KeycloakTransaction[] txs;
+
+ public HybridKeycloakTransaction(KeycloakTransaction... txs) {
+ this.txs = txs;
+ }
+
+ @Override
+ public void begin() {
+ for (KeycloakTransaction tx : txs) {
+ tx.begin();
+ }
+ }
+
+ @Override
+ public void commit() {
+ // TODO What do we do if one tx fails?
+ for (KeycloakTransaction tx : txs) {
+ tx.commit();
+ }
+ }
+
+ @Override
+ public void rollback() {
+ for (KeycloakTransaction tx : txs) {
+ tx.rollback();
+ }
+ }
+
+ @Override
+ public void setRollbackOnly() {
+ for (KeycloakTransaction tx : txs) {
+ tx.setRollbackOnly();
+ }
+ }
+
+ @Override
+ public boolean getRollbackOnly() {
+ for (KeycloakTransaction tx : txs) {
+ if (tx.getRollbackOnly()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean isActive() {
+ for (KeycloakTransaction tx : txs) {
+ if (tx.isActive()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+}
diff --git a/model/hybrid/src/main/java/org/keycloak/models/hybrid/HybridModelProvider.java b/model/hybrid/src/main/java/org/keycloak/models/hybrid/HybridModelProvider.java
new file mode 100644
index 0000000000..0a3d7785eb
--- /dev/null
+++ b/model/hybrid/src/main/java/org/keycloak/models/hybrid/HybridModelProvider.java
@@ -0,0 +1,256 @@
+package org.keycloak.models.hybrid;
+
+import org.keycloak.models.ApplicationModel;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakTransaction;
+import org.keycloak.models.ModelProvider;
+import org.keycloak.models.OAuthClientModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.SocialLinkModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.UsernameLoginFailureModel;
+import org.keycloak.models.realms.RealmProvider;
+import org.keycloak.models.sessions.SessionProvider;
+import org.keycloak.models.users.UserProvider;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.util.Time;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author Stian Thorgersen
+ */
+public class HybridModelProvider implements ModelProvider {
+
+ private final Mappings mappings;
+
+ private KeycloakSession session;
+
+ private HybridKeycloakTransaction tx;
+
+ public HybridModelProvider(KeycloakSession session) {
+ this.session = session;
+ this.mappings = new Mappings(this);
+ }
+
+ @Override
+ public KeycloakTransaction getTransaction() {
+ if (tx == null) {
+ tx = new HybridKeycloakTransaction(realms().getTransaction(), users().getTransaction(), sessions().getTransaction());
+ }
+
+ return tx;
+ }
+
+ Mappings mappings() {
+ return mappings;
+ }
+
+ SessionProvider sessions() {
+ return session.getProvider(SessionProvider.class);
+ }
+
+ UserProvider users() {
+ return session.getProvider(UserProvider.class);
+ }
+
+ RealmProvider realms() {
+ return session.getProvider(RealmProvider.class);
+ }
+
+ @Override
+ public RealmModel createRealm(String name) {
+ return createRealm(KeycloakModelUtils.generateId(), name);
+ }
+
+ @Override
+ public RealmModel createRealm(String id, String name) {
+ return mappings.wrap(realms().createRealm(id, name));
+ }
+
+ @Override
+ public RealmModel getRealm(String id) {
+ return mappings.wrap(realms().getRealm(id));
+ }
+
+ @Override
+ public RealmModel getRealmByName(String name) {
+ return mappings.wrap(realms().getRealmByName(name));
+ }
+
+ @Override
+ public UserModel getUserById(String id, RealmModel realm) {
+ return mappings.wrap(realm, users().getUserById(id, realm.getId()));
+ }
+
+ @Override
+ public UserModel getUserByUsername(String username, RealmModel realm) {
+ return mappings.wrap(realm, users().getUserByUsername(username, realm.getId()));
+ }
+
+ @Override
+ public UserModel getUserByEmail(String email, RealmModel realm) {
+ return mappings.wrap(realm, users().getUserByEmail(email, realm.getId()));
+ }
+
+ @Override
+ public UserModel getUserBySocialLink(SocialLinkModel socialLink, RealmModel realm) {
+ return mappings.wrap(realm, users().getUserByAttribute("keycloak.socialLink." + socialLink.getSocialProvider() + ".userId", socialLink.getSocialUserId(), realm.getId()));
+ }
+
+ @Override
+ public List getUsers(RealmModel realm) {
+ return mappings.wrapUsers(realm, users().getUsers(realm.getId()));
+ }
+
+ @Override
+ public List searchForUser(String search, RealmModel realm) {
+ return mappings.wrapUsers(realm, users().searchForUser(search, realm.getId()));
+ }
+
+ @Override
+ public List searchForUserByAttributes(Map attributes, RealmModel realm) {
+ return mappings.wrapUsers(realm, users().searchForUserByAttributes(attributes, realm.getId()));
+ }
+
+ @Override
+ public Set getSocialLinks(UserModel user, RealmModel realm) {
+ Set links = new HashSet();
+
+ for (Map.Entry e : user.getAttributes().entrySet()) {
+ if (e.getKey().matches("keycloak\\.socialLink\\..*\\.userId")) {
+ String provider = e.getKey().split("\\.")[2];
+
+ SocialLinkModel link = new SocialLinkModel();
+ link.setSocialProvider(provider);
+ link.setSocialUserId(e.getValue());
+ link.setSocialUsername(user.getAttribute("keycloak.socialLink." + provider + ".username"));
+
+ links.add(link);
+ }
+
+ }
+
+ return links;
+ }
+
+ @Override
+ public SocialLinkModel getSocialLink(UserModel user, String provider, RealmModel realm) {
+ if (user.getAttribute("keycloak.socialLink." + provider + ".userId") != null) {
+ SocialLinkModel link = new SocialLinkModel();
+ link.setSocialProvider(provider);
+ link.setSocialUserId(user.getAttribute("keycloak.socialLink." + provider + ".userId"));
+ link.setSocialUsername(user.getAttribute("keycloak.socialLink." + provider + ".username"));
+ return link;
+ } else {
+ return null;
+ }
+ }
+
+ @Override
+ public RoleModel getRoleById(String id, RealmModel realm) {
+ return mappings.wrap(realms().getRoleById(id, realm.getId()));
+ }
+
+ @Override
+ public ApplicationModel getApplicationById(String id, RealmModel realm) {
+ return mappings.wrap(realms().getApplicationById(id, realm.getId()));
+ }
+
+ @Override
+ public OAuthClientModel getOAuthClientById(String id, RealmModel realm) {
+ return mappings.wrap(realms().getOAuthClientById(id, realm.getId()));
+ }
+
+ @Override
+ public List getRealms() {
+ return mappings.wrap(realms().getRealms());
+ }
+
+ @Override
+ public boolean removeRealm(String id) {
+ if (realms().removeRealm(id)) {
+ users().onRealmRemoved(id);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public UsernameLoginFailureModel getUserLoginFailure(String username, RealmModel realm) {
+ return mappings.wrap(sessions().getUserLoginFailure(username, realm.getId()));
+ }
+
+ @Override
+ public UsernameLoginFailureModel addUserLoginFailure(String username, RealmModel realm) {
+ return mappings.wrap(sessions().addUserLoginFailure(username, realm.getId()));
+ }
+
+ @Override
+ public List getAllUserLoginFailures(RealmModel realm) {
+ return mappings.wrapLoginFailures(sessions().getAllUserLoginFailures(realm.getId()));
+ }
+
+ @Override
+ public UserSessionModel createUserSession(RealmModel realm, UserModel user, String ipAddress) {
+ return mappings.wrap(realm, sessions().createUserSession(realm.getId(), KeycloakModelUtils.generateId(), user.getId(), ipAddress));
+ }
+
+ @Override
+ public UserSessionModel getUserSession(String id, RealmModel realm) {
+ return mappings.wrap(realm, sessions().getUserSession(id, realm.getId()));
+ }
+
+ @Override
+ public List getUserSessions(UserModel user, RealmModel realm) {
+ return mappings.wrapSessions(realm, sessions().getUserSessionsByUser(user.getId(), realm.getId()));
+ }
+
+ @Override
+ public Set getUserSessions(RealmModel realm, ClientModel client) {
+ return mappings.wrapSessions(realm, sessions().getUserSessionsByClient(realm.getId(), client.getClientId()));
+ }
+
+ @Override
+ public int getActiveUserSessions(RealmModel realm, ClientModel client) {
+ return sessions().getActiveUserSessions(realm.getId(), client.getClientId());
+ }
+
+ @Override
+ public void removeUserSession(UserSessionModel session) {
+ sessions().removeUserSession(mappings.unwrap(session));
+ }
+
+ @Override
+ public void removeUserSessions(RealmModel realm, UserModel user) {
+ sessions().removeUserSessions(realm.getId(), user.getId());
+ }
+
+ @Override
+ public void removeExpiredUserSessions(RealmModel realm) {
+ long refreshTimeout = Time.currentTime() - realm.getSsoSessionIdleTimeout();
+ long sessionTimeout = Time.currentTime() - realm.getSsoSessionMaxLifespan();
+ sessions().removeExpiredUserSessions(realm.getId(), refreshTimeout, sessionTimeout);
+ }
+
+ @Override
+ public void removeUserSessions(RealmModel realm) {
+ sessions().removeUserSessions(realm.getId());
+ }
+
+ @Override
+ public void removeAllData() {
+ }
+
+ @Override
+ public void close() {
+ }
+
+}
diff --git a/model/hybrid/src/main/java/org/keycloak/models/hybrid/HybridModelProviderFactory.java b/model/hybrid/src/main/java/org/keycloak/models/hybrid/HybridModelProviderFactory.java
new file mode 100644
index 0000000000..314ea96a28
--- /dev/null
+++ b/model/hybrid/src/main/java/org/keycloak/models/hybrid/HybridModelProviderFactory.java
@@ -0,0 +1,34 @@
+package org.keycloak.models.hybrid;
+
+import org.keycloak.Config;
+import org.keycloak.models.realms.RealmProvider;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.ModelProvider;
+import org.keycloak.models.ModelProviderFactory;
+import org.keycloak.models.sessions.SessionProvider;
+import org.keycloak.models.users.UserProvider;
+
+/**
+ * @author Stian Thorgersen
+ */
+public class HybridModelProviderFactory implements ModelProviderFactory {
+
+ @Override
+ public String getId() {
+ return "hybrid";
+ }
+
+ @Override
+ public ModelProvider create(KeycloakSession session) {
+ return new HybridModelProvider(session);
+ }
+
+ @Override
+ public void init(Config.Scope config) {
+ }
+
+ @Override
+ public void close() {
+ }
+
+}
diff --git a/model/hybrid/src/main/java/org/keycloak/models/hybrid/Mappings.java b/model/hybrid/src/main/java/org/keycloak/models/hybrid/Mappings.java
new file mode 100644
index 0000000000..39972aeac4
--- /dev/null
+++ b/model/hybrid/src/main/java/org/keycloak/models/hybrid/Mappings.java
@@ -0,0 +1,220 @@
+package org.keycloak.models.hybrid;
+
+import org.keycloak.models.ApplicationModel;
+import org.keycloak.models.ClientModel;
+import org.keycloak.models.OAuthClientModel;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.RoleContainerModel;
+import org.keycloak.models.RoleModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.UserSessionModel;
+import org.keycloak.models.UsernameLoginFailureModel;
+import org.keycloak.models.realms.Application;
+import org.keycloak.models.realms.Client;
+import org.keycloak.models.realms.OAuthClient;
+import org.keycloak.models.realms.Realm;
+import org.keycloak.models.realms.Role;
+import org.keycloak.models.sessions.LoginFailure;
+import org.keycloak.models.sessions.Session;
+import org.keycloak.models.users.User;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author Stian Thorgersen
+ */
+public class Mappings {
+
+ private final HybridModelProvider provider;
+ private Map