diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaRootEntity.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaRootEntity.java index add757ddf9..643ee74c9a 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaRootEntity.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/JpaRootEntity.java @@ -46,13 +46,17 @@ public interface JpaRootEntity extends AbstractEntity, Serializable { * calls this method whenever the root entity or one of its children changes. * * Future versions of this method might restrict downgrading to downgrade only from the next version. + * + * @return true if the entityVersion was effectively changed, false otherwise. */ - default void updateEntityVersion() { + default boolean updateEntityVersion() { Integer ev = getEntityVersion(); Integer currentEv = getCurrentSchemaVersion(); if (ev != null && !Objects.equals(ev, currentEv)) { setEntityVersion(currentEv); + return true; } + return false; } Integer getCurrentSchemaVersion(); diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authSession/entity/JpaAuthenticationSessionEntity.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authSession/entity/JpaAuthenticationSessionEntity.java index c1dd469249..f14492caa9 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authSession/entity/JpaAuthenticationSessionEntity.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/authSession/entity/JpaAuthenticationSessionEntity.java @@ -37,6 +37,7 @@ import org.keycloak.models.map.authSession.MapAuthenticationSessionEntity; import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.common.UpdatableEntity; import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_AUTH_SESSION; +import org.keycloak.models.map.storage.jpa.JpaChildEntity; import org.keycloak.models.map.storage.jpa.JpaRootVersionedEntity; import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType; import org.keycloak.sessions.CommonClientSessionModel; @@ -47,7 +48,7 @@ import org.keycloak.sessions.CommonClientSessionModel; @Entity @Table(name = "kc_auth_session") @TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)}) -public class JpaAuthenticationSessionEntity extends UpdatableEntity.Impl implements MapAuthenticationSessionEntity, JpaRootVersionedEntity { +public class JpaAuthenticationSessionEntity extends UpdatableEntity.Impl implements MapAuthenticationSessionEntity, JpaRootVersionedEntity, JpaChildEntity{ @Id @Column @@ -86,6 +87,11 @@ public class JpaAuthenticationSessionEntity extends UpdatableEntity.Impl impleme return metadata != null; } + @Override + public JpaRootAuthenticationSessionEntity getParent() { + return root; + } + public void setParent(JpaRootAuthenticationSessionEntity root) { this.root = root; } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/listeners/JpaEntityVersionListener.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/listeners/JpaEntityVersionListener.java index 2749ea01d7..e4aff1bc3a 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/listeners/JpaEntityVersionListener.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/listeners/JpaEntityVersionListener.java @@ -30,22 +30,30 @@ import org.keycloak.models.map.storage.jpa.JpaRootEntity; /** * Listen on changes on child- and root entities and updates the current entity version of the root. * - * This support a multiple level parent-child relationship, where the upmost parent needs the entity version to be updated. + * This support a multiple level parent-child relationship, where all parents needs the entity version to be updated + * in case it was effectively changed. The traversing is stopped at that point when it is detected that parent entity + * version is the same one. + * + * It is based on an assumption that it may happen that one parent entity could be extracted into "parent A -> parent B -> child" + * format. Then the change (insertion, deletion or update) of the child should bump the entity version of both parent B and parent A. */ public class JpaEntityVersionListener implements PreInsertEventListener, PreDeleteEventListener, PreUpdateEventListener { public static final JpaEntityVersionListener INSTANCE = new JpaEntityVersionListener(); /** - * Traverse from current entity up to the upmost parent, then update the entity version if it is a root entity. + * Traverse from current entity through its parent tree and update the entity version of it. + * Stop if non-changed parent is found. */ public void updateEntityVersion(Object entity) throws HibernateException { Object root = entity; while(root instanceof JpaChildEntity) { root = ((JpaChildEntity) entity).getParent(); - } - if (root instanceof JpaRootEntity) { - ((JpaRootEntity) root).updateEntityVersion(); + if (root instanceof JpaRootEntity) { + if (!((JpaRootEntity) root).updateEntityVersion()) { + return; + } + } } } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/listeners/JpaOptimisticLockingListener.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/listeners/JpaOptimisticLockingListener.java index c582634051..fbfbbc0942 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/listeners/JpaOptimisticLockingListener.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/listeners/JpaOptimisticLockingListener.java @@ -30,26 +30,27 @@ import org.keycloak.models.map.storage.jpa.JpaRootVersionedEntity; import javax.persistence.LockModeType; import java.util.Objects; +import org.keycloak.models.map.storage.jpa.JpaRootEntity; /** - * Listen on changes on child entities and forces an optimistic locking increment on the topmost parent aka root. + * Listen on changes on child entities and forces an optimistic locking increment on the closest parent aka root. + * The assumption is that any parent of a child entity is root entity. Optimistic locking is enforced on child entity + * which is not the child entity at the same time. This prevents {@link javax.persistence.OptimisticLockException}s + * when different children in the same parent are being manipulated at the same time by different threads. * - * This support a multiple level parent-child relationship, where only the upmost parent is locked. + * This support a multiple level parent-child relationship, where only the closest parent is locked. */ public class JpaOptimisticLockingListener implements PreInsertEventListener, PreDeleteEventListener, PreUpdateEventListener { public static final JpaOptimisticLockingListener INSTANCE = new JpaOptimisticLockingListener(); /** - * Check if the entity is a child with a parent and force optimistic locking increment on the upmost parent aka root. + * Check if the entity is a child with a parent and force optimistic locking increment on the parent aka root. */ public void lockRootEntity(Session session, Object entity) throws HibernateException { - if(entity instanceof JpaChildEntity) { - Object root = entity; - while (root instanceof JpaChildEntity) { - root = ((JpaChildEntity) entity).getParent(); - Objects.requireNonNull(root, "children must always return their parent, never null"); - } + if (entity instanceof JpaChildEntity && ! (entity instanceof JpaRootEntity)) { + Object root = ((JpaChildEntity) entity).getParent(); + Objects.requireNonNull(root, "children must always return their parent, never null"); // do not lock if root doesn't implement implicit optimistic locking mechanism if (! (root instanceof JpaRootVersionedEntity)) return; diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/realm/entity/JpaComponentEntity.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/realm/entity/JpaComponentEntity.java index ca4d991a00..1222dc9a03 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/realm/entity/JpaComponentEntity.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/realm/entity/JpaComponentEntity.java @@ -39,6 +39,7 @@ import org.keycloak.models.map.common.UpdatableEntity; import org.keycloak.models.map.common.UuidValidator; import org.keycloak.models.map.realm.entity.MapComponentEntity; import org.keycloak.models.map.storage.jpa.Constants; +import org.keycloak.models.map.storage.jpa.JpaChildEntity; import org.keycloak.models.map.storage.jpa.JpaRootVersionedEntity; import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType; @@ -48,10 +49,7 @@ import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType; * to indicate that they are automatically generated from json fields. As such, these fields are non-insertable and non-updatable. *

* Components are independent (i.e. a component doesn't depend on another component) and can be manipulated directly via - * the component endpoints. Because of that, this entity implements {@link JpaRootVersionedEntity} instead of - * {@link org.keycloak.models.map.storage.jpa.JpaChildEntity}. This prevents {@link javax.persistence.OptimisticLockException}s - * when different components in the same realm are being manipulated at the same time - for example, when multiple components - * are being added to the realm by different threads. + * the component endpoints. *

* By implementing {@link JpaRootVersionedEntity}, this entity will enforce optimistic locking, which can lead to * {@link javax.persistence.OptimisticLockException} if more than one thread attempts to modify the same component @@ -62,7 +60,7 @@ import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType; @Entity @Table(name = "kc_component") @TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)}) -public class JpaComponentEntity extends UpdatableEntity.Impl implements MapComponentEntity, JpaRootVersionedEntity { +public class JpaComponentEntity extends UpdatableEntity.Impl implements MapComponentEntity, JpaRootVersionedEntity, JpaChildEntity { @Id @Column @@ -100,6 +98,11 @@ public class JpaComponentEntity extends UpdatableEntity.Impl implements MapCompo this.metadata = new JpaComponentMetadata(cloner); } + @Override + public JpaRealmEntity getParent() { + return root; + } + public void setParent(JpaRealmEntity root) { this.root = root; } diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/entity/JpaUserConsentEntity.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/entity/JpaUserConsentEntity.java index 9c2aa1def6..8e31c707fb 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/entity/JpaUserConsentEntity.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/entity/JpaUserConsentEntity.java @@ -36,7 +36,9 @@ import org.hibernate.annotations.TypeDef; import org.hibernate.annotations.TypeDefs; import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.common.UpdatableEntity; +import org.keycloak.models.map.storage.jpa.Constants; import org.keycloak.models.map.storage.jpa.JpaChildEntity; +import org.keycloak.models.map.storage.jpa.JpaRootEntity; import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType; import org.keycloak.models.map.user.MapUserConsentEntity; @@ -52,7 +54,7 @@ import org.keycloak.models.map.user.MapUserConsentEntity; @UniqueConstraint(columnNames = {"clientId"}) }) @TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)}) -public class JpaUserConsentEntity extends UpdatableEntity.Impl implements MapUserConsentEntity, JpaChildEntity { +public class JpaUserConsentEntity extends UpdatableEntity.Impl implements MapUserConsentEntity, JpaRootEntity, JpaChildEntity { @Id @Column @@ -87,11 +89,13 @@ public class JpaUserConsentEntity extends UpdatableEntity.Impl implements MapUse return metadata != null; } + @Override public Integer getEntityVersion() { if (isMetadataInitialized()) return this.metadata.getEntityVersion(); return entityVersion; } + @Override public void setEntityVersion(Integer version) { this.metadata.setEntityVersion(version); } @@ -105,6 +109,21 @@ public class JpaUserConsentEntity extends UpdatableEntity.Impl implements MapUse this.root = root; } + @Override + public String getId() { + return id == null ? null : id.toString(); + } + + @Override + public void setId(String id) { + this.id = id == null ? null : UUID.fromString(id); + } + + @Override + public Integer getCurrentSchemaVersion() { + return Constants.CURRENT_SCHEMA_VERSION_USER_CONSENT; + } + @Override public String getClientId() { if (isMetadataInitialized()) return this.metadata.getClientId(); diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/entity/JpaUserFederatedIdentityEntity.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/entity/JpaUserFederatedIdentityEntity.java index e43346fc25..d9e3c44ea5 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/entity/JpaUserFederatedIdentityEntity.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/user/entity/JpaUserFederatedIdentityEntity.java @@ -34,7 +34,9 @@ import org.hibernate.annotations.TypeDef; import org.hibernate.annotations.TypeDefs; import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.common.UpdatableEntity; +import org.keycloak.models.map.storage.jpa.Constants; import org.keycloak.models.map.storage.jpa.JpaChildEntity; +import org.keycloak.models.map.storage.jpa.JpaRootEntity; import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType; import org.keycloak.models.map.user.MapUserFederatedIdentityEntity; @@ -47,7 +49,7 @@ import org.keycloak.models.map.user.MapUserFederatedIdentityEntity; @Entity @Table(name = "kc_user_federated_identity") @TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)}) -public class JpaUserFederatedIdentityEntity extends UpdatableEntity.Impl implements MapUserFederatedIdentityEntity, JpaChildEntity { +public class JpaUserFederatedIdentityEntity extends UpdatableEntity.Impl implements MapUserFederatedIdentityEntity, JpaRootEntity, JpaChildEntity { @Id @Column @@ -86,15 +88,22 @@ public class JpaUserFederatedIdentityEntity extends UpdatableEntity.Impl impleme return metadata != null; } + @Override public Integer getEntityVersion() { if (isMetadataInitialized()) return this.metadata.getEntityVersion(); return entityVersion; } + @Override public void setEntityVersion(Integer version) { this.metadata.setEntityVersion(version); } + @Override + public Integer getCurrentSchemaVersion() { + return Constants.CURRENT_SCHEMA_VERSION_USER_FEDERATED_IDENTITY; + } + @Override public JpaUserEntity getParent() { return this.root; @@ -104,6 +113,16 @@ public class JpaUserFederatedIdentityEntity extends UpdatableEntity.Impl impleme this.root = root; } + @Override + public String getId() { + return id == null ? null : id.toString(); + } + + @Override + public void setId(String id) { + this.id = id == null ? null : UUID.fromString(id); + } + @Override public String getToken() { return this.metadata.getToken(); diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/userSession/entity/JpaClientSessionEntity.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/userSession/entity/JpaClientSessionEntity.java index bd7bb54683..d2f6070201 100644 --- a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/userSession/entity/JpaClientSessionEntity.java +++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/userSession/entity/JpaClientSessionEntity.java @@ -39,6 +39,7 @@ import org.hibernate.annotations.TypeDefs; import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.common.UuidValidator; import org.keycloak.models.map.storage.jpa.Constants; +import org.keycloak.models.map.storage.jpa.JpaChildEntity; import org.keycloak.models.map.storage.jpa.JpaRootVersionedEntity; import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType; import org.keycloak.models.map.userSession.MapAuthenticatedClientSessionEntity.AbstractAuthenticatedClientSessionEntity; @@ -49,7 +50,7 @@ import org.keycloak.models.map.userSession.MapAuthenticatedClientSessionEntity.A @Entity @Table(name = "kc_client_session") @TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)}) -public class JpaClientSessionEntity extends AbstractAuthenticatedClientSessionEntity implements JpaRootVersionedEntity { +public class JpaClientSessionEntity extends AbstractAuthenticatedClientSessionEntity implements JpaRootVersionedEntity, JpaChildEntity { @Id @Column @@ -94,6 +95,11 @@ public class JpaClientSessionEntity extends AbstractAuthenticatedClientSessionEn return metadata != null; } + @Override + public JpaUserSessionEntity getParent() { + return root; + } + public void setParent(JpaUserSessionEntity root) { this.root = root; }