Ensure that parent's version ID is incremented when an attribute changes.

This is necessary to allow the optimistic locking functionality to work as expected when changing only attributes on an entity.

Closes #9874
This commit is contained in:
Alexander Schwartz 2022-01-24 14:59:34 +01:00 committed by Hynek Mlnařík
parent 13e02d5f09
commit 9d46b45a9c
7 changed files with 146 additions and 8 deletions

View file

@ -0,0 +1,28 @@
/*
* Copyright 2022. Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.storage.jpa;
/**
* Interface for all child entities for JPA map storage.
*/
public interface JpaChildEntity<R> {
/**
* Parent entity that should get its optimistic locking version updated upon changes in the child
*/
R getParent();
}

View file

@ -0,0 +1,70 @@
/*
* Copyright 2022. Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.models.map.storage.jpa;
import org.hibernate.HibernateException;
import org.hibernate.Session;
import org.hibernate.event.spi.PreDeleteEvent;
import org.hibernate.event.spi.PreDeleteEventListener;
import org.hibernate.event.spi.PreInsertEvent;
import org.hibernate.event.spi.PreInsertEventListener;
import org.hibernate.event.spi.PreUpdateEvent;
import org.hibernate.event.spi.PreUpdateEventListener;
import javax.persistence.LockModeType;
/**
* Listen on changes on child entities and forces an optimistic locking increment on the topmost parent.
*
* This support a multiple level parent-child relationship, where only the upmost parent is locked.
*/
public class JpaChildEntityListener implements PreInsertEventListener, PreDeleteEventListener, PreUpdateEventListener {
public static final JpaChildEntityListener INSTANCE = new JpaChildEntityListener();
/**
* Check if the entity is a child with a parent and force optimistic locking increment on the upmost parent.
*/
public void checkRoot(Session session, Object entity) throws HibernateException {
if(entity instanceof JpaChildEntity) {
Object root = entity;
while (root instanceof JpaChildEntity) {
root = ((JpaChildEntity<?>) entity).getParent();
}
session.lock(root, LockModeType.OPTIMISTIC_FORCE_INCREMENT);
}
}
@Override
public boolean onPreInsert(PreInsertEvent event) {
checkRoot(event.getSession(), event.getEntity());
return false;
}
@Override
public boolean onPreDelete(PreDeleteEvent event) {
checkRoot(event.getSession(), event.getEntity());
return false;
}
@Override
public boolean onPreUpdate(PreUpdateEvent event) {
checkRoot(event.getSession(), event.getEntity());
return false;
}
}

View file

@ -20,6 +20,7 @@ import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
@ -30,7 +31,14 @@ import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import javax.sql.DataSource;
import org.hibernate.boot.Metadata;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.event.service.spi.EventListenerRegistry;
import org.hibernate.event.spi.EventType;
import org.hibernate.jpa.boot.spi.IntegratorProvider;
import org.hibernate.service.spi.SessionFactoryServiceRegistry;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.common.Profile;
@ -166,6 +174,31 @@ public class JpaMapStorageProviderFactory implements
properties.put("hibernate.format_sql", config.getBoolean("formatSql", true));
properties.put("hibernate.dialect", config.get("driverDialect"));
properties.put(
"hibernate.integrator_provider",
(IntegratorProvider) () -> Collections.singletonList(
new org.hibernate.integrator.spi.Integrator() {
@Override
public void integrate(Metadata metadata, SessionFactoryImplementor sessionFactoryImplementor,
SessionFactoryServiceRegistry sessionFactoryServiceRegistry) {
final EventListenerRegistry eventListenerRegistry =
sessionFactoryServiceRegistry.getService( EventListenerRegistry.class );
eventListenerRegistry.appendListeners(EventType.PRE_INSERT, JpaChildEntityListener.INSTANCE);
eventListenerRegistry.appendListeners(EventType.PRE_UPDATE, JpaChildEntityListener.INSTANCE);
eventListenerRegistry.appendListeners(EventType.PRE_DELETE, JpaChildEntityListener.INSTANCE);
}
@Override
public void disintegrate(SessionFactoryImplementor sessionFactoryImplementor,
SessionFactoryServiceRegistry sessionFactoryServiceRegistry) {
}
}
)
);
Integer isolation = config.getInt("isolation");
if (isolation != null) {
if (isolation < Connection.TRANSACTION_REPEATABLE_READ) {

View file

@ -28,10 +28,11 @@ import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import org.hibernate.annotations.Nationalized;
import org.keycloak.models.map.storage.jpa.JpaChildEntity;
@Entity
@Table(name = "client_attribute")
public class JpaClientAttributeEntity implements Serializable {
public class JpaClientAttributeEntity implements JpaChildEntity<JpaClientEntity>, Serializable {
@Id
@Column
@ -100,4 +101,9 @@ public class JpaClientAttributeEntity implements Serializable {
Objects.equals(getName(), that.getName()) &&
Objects.equals(getValue(), that.getValue());
}
@Override
public JpaClientEntity getParent() {
return getClient();
}
}

View file

@ -587,7 +587,6 @@ public class JpaClientEntity extends AbstractClientEntity implements JpaRootEnti
JpaClientAttributeEntity attr = iterator.next();
if (Objects.equals(attr.getName(), name)) {
iterator.remove();
attr.setClient(null);
}
}
}
@ -625,9 +624,7 @@ public class JpaClientEntity extends AbstractClientEntity implements JpaRootEnti
public void setAttributes(Map<String, List<String>> attributes) {
checkEntityVersionForUpdate();
for (Iterator<JpaClientAttributeEntity> iterator = this.attributes.iterator(); iterator.hasNext();) {
JpaClientAttributeEntity attr = iterator.next();
iterator.remove();
attr.setClient(null);
}
if (attributes != null) {
for (Map.Entry<String, List<String>> attrEntry : attributes.entrySet()) {

View file

@ -28,10 +28,12 @@ import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import org.hibernate.annotations.Nationalized;
import org.keycloak.models.map.storage.jpa.JpaChildEntity;
import org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity;
@Entity
@Table(name = "role_attribute")
public class JpaRoleAttributeEntity implements Serializable {
public class JpaRoleAttributeEntity implements JpaChildEntity<JpaRoleEntity>, Serializable {
@Id
@Column
@ -100,4 +102,9 @@ public class JpaRoleAttributeEntity implements Serializable {
Objects.equals(getName(), that.getName()) &&
Objects.equals(getValue(), that.getValue());
}
@Override
public JpaRoleEntity getParent() {
return role;
}
}

View file

@ -257,9 +257,7 @@ public class JpaRoleEntity extends AbstractRoleEntity implements JpaRootEntity {
public void setAttributes(Map<String, List<String>> attributes) {
checkEntityVersionForUpdate();
for (Iterator<JpaRoleAttributeEntity> iterator = this.attributes.iterator(); iterator.hasNext();) {
JpaRoleAttributeEntity attr = iterator.next();
iterator.remove();
attr.setRole(null);
}
if (attributes != null) {
for (Map.Entry<String, List<String>> entry : attributes.entrySet()) {
@ -285,7 +283,6 @@ public class JpaRoleEntity extends AbstractRoleEntity implements JpaRootEntity {
JpaRoleAttributeEntity attr = iterator.next();
if (Objects.equals(attr.getName(), name)) {
iterator.remove();
attr.setRole(null);
}
}
}