Add a map storage global locking implementation for JPA

Closes #14734
This commit is contained in:
Alexander Schwartz 2023-01-24 09:59:48 +01:00 committed by Michal Hajas
parent d20c1f8d1d
commit 513bb809f3
28 changed files with 1053 additions and 23 deletions

View file

@ -280,7 +280,7 @@ public class GenerateEntityImplementationsProcessor extends AbstractGenerateEnti
GenerateEntityImplementations an = e.getAnnotation(GenerateEntityImplementations.class); GenerateEntityImplementations an = e.getAnnotation(GenerateEntityImplementations.class);
TypeElement parentTypeElement = elements.getTypeElement((an.inherits() == null || an.inherits().isEmpty()) ? "void" : an.inherits()); TypeElement parentTypeElement = elements.getTypeElement((an.inherits() == null || an.inherits().isEmpty()) ? "void" : an.inherits());
if (parentTypeElement == null) { if (parentTypeElement == null) {
return; processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "Unable to find type " + an.inherits() + " for inherits parameter for annotation " + GenerateEntityImplementations.class.getTypeName(), e);
} }
final List<? extends Element> allParentMembers = elements.getAllMembers(parentTypeElement); final List<? extends Element> allParentMembers = elements.getAllMembers(parentTypeElement);
String className = e.getQualifiedName().toString(); String className = e.getQualifiedName().toString();

View file

@ -39,4 +39,5 @@ public interface Constants {
public static final Integer CURRENT_SCHEMA_VERSION_USER_CONSENT = 1; public static final Integer CURRENT_SCHEMA_VERSION_USER_CONSENT = 1;
public static final Integer CURRENT_SCHEMA_VERSION_USER_FEDERATED_IDENTITY = 1; public static final Integer CURRENT_SCHEMA_VERSION_USER_FEDERATED_IDENTITY = 1;
public static final Integer CURRENT_SCHEMA_VERSION_USER_SESSION = 1; public static final Integer CURRENT_SCHEMA_VERSION_USER_SESSION = 1;
public static final Integer CURRENT_SCHEMA_VERSION_LOCK = 1;
} }

View file

@ -81,6 +81,7 @@ import org.keycloak.models.locking.GlobalLockProvider;
import org.keycloak.models.map.client.MapProtocolMapperEntity; import org.keycloak.models.map.client.MapProtocolMapperEntity;
import org.keycloak.models.map.client.MapProtocolMapperEntityImpl; import org.keycloak.models.map.client.MapProtocolMapperEntityImpl;
import org.keycloak.models.map.common.DeepCloner; import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.lock.MapLockEntity;
import org.keycloak.models.map.realm.entity.MapAuthenticationExecutionEntity; import org.keycloak.models.map.realm.entity.MapAuthenticationExecutionEntity;
import org.keycloak.models.map.realm.entity.MapAuthenticationExecutionEntityImpl; import org.keycloak.models.map.realm.entity.MapAuthenticationExecutionEntityImpl;
import org.keycloak.models.map.realm.entity.MapAuthenticationFlowEntity; import org.keycloak.models.map.realm.entity.MapAuthenticationFlowEntity;
@ -127,6 +128,8 @@ import org.keycloak.models.map.storage.jpa.event.auth.JpaAuthEventMapKeycloakTra
import org.keycloak.models.map.storage.jpa.event.auth.entity.JpaAuthEventEntity; import org.keycloak.models.map.storage.jpa.event.auth.entity.JpaAuthEventEntity;
import org.keycloak.models.map.storage.jpa.group.JpaGroupMapKeycloakTransaction; import org.keycloak.models.map.storage.jpa.group.JpaGroupMapKeycloakTransaction;
import org.keycloak.models.map.storage.jpa.group.entity.JpaGroupEntity; import org.keycloak.models.map.storage.jpa.group.entity.JpaGroupEntity;
import org.keycloak.models.map.storage.jpa.lock.JpaLockMapKeycloakTransaction;
import org.keycloak.models.map.storage.jpa.lock.entity.JpaLockEntity;
import org.keycloak.models.map.storage.jpa.loginFailure.JpaUserLoginFailureMapKeycloakTransaction; import org.keycloak.models.map.storage.jpa.loginFailure.JpaUserLoginFailureMapKeycloakTransaction;
import org.keycloak.models.map.storage.jpa.loginFailure.entity.JpaUserLoginFailureEntity; import org.keycloak.models.map.storage.jpa.loginFailure.entity.JpaUserLoginFailureEntity;
import org.keycloak.models.map.storage.jpa.realm.JpaRealmMapKeycloakTransaction; import org.keycloak.models.map.storage.jpa.realm.JpaRealmMapKeycloakTransaction;
@ -224,6 +227,8 @@ public class JpaMapStorageProviderFactory implements
//user/client session //user/client session
.constructor(JpaClientSessionEntity.class, JpaClientSessionEntity::new) .constructor(JpaClientSessionEntity.class, JpaClientSessionEntity::new)
.constructor(JpaUserSessionEntity.class, JpaUserSessionEntity::new) .constructor(JpaUserSessionEntity.class, JpaUserSessionEntity::new)
//lock
.constructor(JpaLockEntity.class, JpaLockEntity::new)
.build(); .build();
private static final Map<Class<?>, BiFunction<KeycloakSession, EntityManager, MapKeycloakTransaction>> MODEL_TO_TX = new HashMap<>(); private static final Map<Class<?>, BiFunction<KeycloakSession, EntityManager, MapKeycloakTransaction>> MODEL_TO_TX = new HashMap<>();
@ -257,6 +262,8 @@ public class JpaMapStorageProviderFactory implements
MODEL_TO_TX.put(UserModel.class, JpaUserMapKeycloakTransaction::new); MODEL_TO_TX.put(UserModel.class, JpaUserMapKeycloakTransaction::new);
//sessions //sessions
MODEL_TO_TX.put(UserSessionModel.class, JpaUserSessionMapKeycloakTransaction::new); MODEL_TO_TX.put(UserSessionModel.class, JpaUserSessionMapKeycloakTransaction::new);
//locks
MODEL_TO_TX.put(MapLockEntity.class, JpaLockMapKeycloakTransaction::new);
} }
private boolean jtaEnabled; private boolean jtaEnabled;
@ -539,10 +546,15 @@ public class JpaMapStorageProviderFactory implements
} }
private void update(Class<?> modelType, Connection connection, KeycloakSession session) { private void update(Class<?> modelType, Connection connection, KeycloakSession session) {
session.getProvider(GlobalLockProvider.class).withLock(modelType.getName(), lockedSession -> { if (modelType == MapLockEntity.class) {
lockedSession.getProvider(MapJpaUpdaterProvider.class).update(modelType, connection, config.get("schema")); // as the MapLockEntity is used by the MapGlobalLockProvider itself, don't create a global lock for creating that schema
return null; session.getProvider(MapJpaUpdaterProvider.class).update(modelType, connection, config.get("schema"));
}); } else {
session.getProvider(GlobalLockProvider.class).withLock(modelType.getName(), lockedSession -> {
lockedSession.getProvider(MapJpaUpdaterProvider.class).update(modelType, connection, config.get("schema"));
return null;
});
}
} }
@Override @Override

View file

@ -0,0 +1,79 @@
/*
* Copyright 2023 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.liquibase.lockservice;
import liquibase.exception.DatabaseException;
import liquibase.lockservice.StandardLockService;
import liquibase.snapshot.DatabaseSnapshot;
import liquibase.snapshot.InvalidExampleException;
import liquibase.snapshot.SnapshotControl;
import liquibase.snapshot.SnapshotGeneratorFactory;
import liquibase.structure.core.PrimaryKey;
import liquibase.structure.core.Schema;
import liquibase.structure.core.Table;
import org.jboss.logging.Logger;
/**
* Extending the Liquibase {@link StandardLockService} for situations where it failed on a H2 database.
*
* @author Alexander Schwartz
*/
public class KeycloakLockService extends StandardLockService {
private static final Logger log = Logger.getLogger(KeycloakLockService.class);
@Override
public int getPriority() {
return super.getPriority() + 1;
}
@Override
protected boolean hasDatabaseChangeLogLockTable() throws DatabaseException {
boolean originalReturnValue = super.hasDatabaseChangeLogLockTable();
if (originalReturnValue) {
/* Liquibase only checks that the table exists. On the H2 database, creation of a table with a primary key is not atomic,
and the primary key might not be visible yet. The primary key would be needed to prevent inserting the data into the table
a second time. Inserting it a second time might lead to a failure when creating the primary key, which would then roll back
the creation of the table. Therefore, at least on the H2 database, checking for the primary key is essential.
An existing DATABASECHANGELOG might indicate that the insertion of data was completed previously.
Still, this isn't working with the DBLockTest which deletes only the DATABASECHANGELOGLOCK table.
See https://github.com/keycloak/keycloak/issues/15487 for more information.
*/
Table lockTable = (Table) new Table().setName(database.getDatabaseChangeLogLockTableName()).setSchema(
new Schema(database.getLiquibaseCatalogName(), database.getLiquibaseSchemaName()));
SnapshotGeneratorFactory instance = SnapshotGeneratorFactory.getInstance();
try {
DatabaseSnapshot snapshot = instance.createSnapshot(lockTable.getSchema().toCatalogAndSchema(), database,
new SnapshotControl(database, false, Table.class, PrimaryKey.class).setWarnIfObjectNotFound(false));
Table lockTableFromSnapshot = snapshot.get(lockTable);
if (lockTableFromSnapshot == null) {
throw new RuntimeException("DATABASECHANGELOGLOCK not found, although Liquibase claims it exists.");
} else if (lockTableFromSnapshot.getPrimaryKey() == null) {
log.warn("Primary key not found - table creation not complete yet.");
return false;
}
} catch (InvalidExampleException e) {
throw new RuntimeException(e);
}
}
return originalReturnValue;
}
}

View file

@ -0,0 +1,64 @@
/*
* Copyright 2023 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 Locks and
* limitations under the License.
*/
package org.keycloak.models.map.storage.jpa.lock;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.map.lock.MapLockEntity;
import org.keycloak.models.map.lock.MapLockEntityDelegate;
import org.keycloak.models.map.storage.jpa.Constants;
import org.keycloak.models.map.storage.jpa.JpaMapKeycloakTransaction;
import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder;
import org.keycloak.models.map.storage.jpa.JpaRootEntity;
import org.keycloak.models.map.storage.jpa.lock.delegate.JpaLockDelegateProvider;
import org.keycloak.models.map.storage.jpa.lock.entity.JpaLockEntity;
import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Root;
import javax.persistence.criteria.Selection;
public class JpaLockMapKeycloakTransaction extends JpaMapKeycloakTransaction<JpaLockEntity, MapLockEntity, MapLockEntity> {
@SuppressWarnings("unchecked")
public JpaLockMapKeycloakTransaction(KeycloakSession session, EntityManager em) {
super(session, JpaLockEntity.class, MapLockEntity.class, em);
}
@Override
protected Selection<JpaLockEntity> selectCbConstruct(CriteriaBuilder cb, Root<JpaLockEntity> root) {
return cb.construct(JpaLockEntity.class,
root.get("id"),
root.get("version"),
root.get("entityVersion"),
root.get("name"));
}
@Override
public void setEntityVersion(JpaRootEntity entity) {
entity.setEntityVersion(Constants.CURRENT_SCHEMA_VERSION_LOCK);
}
@Override
public JpaModelCriteriaBuilder createJpaModelCriteriaBuilder() {
return new JpaLockModelCriteriaBuilder();
}
@Override
protected MapLockEntity mapToEntityDelegate(JpaLockEntity original) {
return new MapLockEntityDelegate(new JpaLockDelegateProvider(original, em));
}
}

View file

@ -0,0 +1,62 @@
/*
* Copyright 2023 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.lock;
import org.keycloak.models.map.common.StringKeyConverter.UUIDKey;
import org.keycloak.models.map.lock.MapLockEntity;
import org.keycloak.models.map.storage.CriterionNotSupportedException;
import org.keycloak.models.map.storage.jpa.JpaModelCriteriaBuilder;
import org.keycloak.models.map.storage.jpa.JpaPredicateFunction;
import org.keycloak.models.map.storage.jpa.authorization.resource.entity.JpaResourceEntity;
import org.keycloak.storage.SearchableModelField;
import java.util.Objects;
import java.util.UUID;
import static org.keycloak.models.map.lock.MapLockEntity.SearchableFields;
public class JpaLockModelCriteriaBuilder extends JpaModelCriteriaBuilder<JpaResourceEntity, MapLockEntity, JpaLockModelCriteriaBuilder> {
public JpaLockModelCriteriaBuilder() {
super(JpaLockModelCriteriaBuilder::new);
}
private JpaLockModelCriteriaBuilder(JpaPredicateFunction<JpaResourceEntity> predicateFunc) {
super(JpaLockModelCriteriaBuilder::new, predicateFunc);
}
@Override
public JpaLockModelCriteriaBuilder compare(SearchableModelField<? super MapLockEntity> modelField, Operator op, Object... value) {
switch (op) {
case EQ:
if (modelField == SearchableFields.NAME) {
validateValue(value, modelField, op, String.class);
return new JpaLockModelCriteriaBuilder((cb, query, root) ->
cb.equal(root.get(modelField.getName()), value[0])
);
} else {
throw new CriterionNotSupportedException(modelField, op);
}
default:
throw new CriterionNotSupportedException(modelField, op);
}
}
}

View file

@ -0,0 +1,65 @@
/*
* Copyright 2023 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.lock.delegate;
import org.keycloak.models.map.common.EntityField;
import org.keycloak.models.map.common.delegate.DelegateProvider;
import org.keycloak.models.map.lock.MapLockEntity;
import org.keycloak.models.map.lock.MapLockEntityFields;
import org.keycloak.models.map.storage.jpa.JpaDelegateProvider;
import org.keycloak.models.map.storage.jpa.lock.entity.JpaLockEntity;
import javax.persistence.EntityManager;
import java.util.UUID;
/**
* A {@link DelegateProvider} implementation for {@link JpaLockEntity}.
*
* @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
public class JpaLockDelegateProvider extends JpaDelegateProvider<JpaLockEntity> implements DelegateProvider<MapLockEntity> {
private final EntityManager em;
public JpaLockDelegateProvider(final JpaLockEntity delegate, final EntityManager em) {
super(delegate);
this.em = em;
}
@Override
public MapLockEntity getDelegate(boolean isRead, Enum<? extends EntityField<MapLockEntity>> field, Object... parameters) {
if (getDelegate().isMetadataInitialized()) return getDelegate();
if (isRead) {
if (field instanceof MapLockEntityFields) {
switch ((MapLockEntityFields) field) {
case ID:
case NAME:
return getDelegate();
default:
setDelegate(em.find(JpaLockEntity.class, UUID.fromString(getDelegate().getId())));
}
} else {
throw new IllegalStateException("Not a valid lock field: " + field);
}
} else {
setDelegate(em.find(JpaLockEntity.class, UUID.fromString(getDelegate().getId())));
}
return getDelegate();
}
}

View file

@ -0,0 +1,169 @@
/*
* Copyright 2023 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.lock.entity;
import java.util.Objects;
import java.util.UUID;
import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import javax.persistence.Version;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
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.JpaRootVersionedEntity;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
import static org.keycloak.models.map.lock.MapLockEntity.AbstractLockEntity;
import static org.keycloak.models.map.storage.jpa.Constants.CURRENT_SCHEMA_VERSION_GROUP;
/**
* There are some fields marked by {@code @Column(insertable = false, updatable = false)}.
* Those fields are automatically generated by database from json field,
* therefore marked as non-insertable and non-updatable to instruct hibernate.
*/
@Entity
@Table(name = "kc_lock", uniqueConstraints = {@UniqueConstraint(columnNames = {"name"})})
@TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)})
public class JpaLockEntity extends AbstractLockEntity implements JpaRootVersionedEntity {
@Id
@Column
private UUID id;
//used for implicit optimistic locking
@Version
@Column
private int version;
@Type(type = "jsonb")
@Column(columnDefinition = "jsonb")
private final JpaLockMetadata metadata;
@Column(insertable = false, updatable = false)
@Basic(fetch = FetchType.LAZY)
private Integer entityVersion;
@Column(insertable = false, updatable = false)
@Basic(fetch = FetchType.LAZY)
private String name;
/**
* No-argument constructor, used by hibernate to instantiate entities.
*/
public JpaLockEntity() {
this.metadata = new JpaLockMetadata();
}
public JpaLockEntity(DeepCloner cloner) {
this.metadata = new JpaLockMetadata(cloner);
}
public JpaLockEntity(final UUID id, final int version, final Integer entityVersion, String name) {
this.id = id;
this.version = version;
this.entityVersion = entityVersion;
this.name = name;
this.metadata = null;
}
public boolean isMetadataInitialized() {
return metadata != null;
}
@Override
public Integer getEntityVersion() {
if (isMetadataInitialized()) return metadata.getEntityVersion();
return entityVersion;
}
@Override
public Integer getCurrentSchemaVersion() {
return CURRENT_SCHEMA_VERSION_GROUP;
}
@Override
public void setEntityVersion(Integer entityVersion) {
metadata.setEntityVersion(entityVersion);
}
@Override
public int getVersion() {
return version;
}
@Override
public String getId() {
return id == null ? null : id.toString();
}
@Override
public void setId(String id) {
String validatedId = UuidValidator.validateAndConvert(id);
this.id = UUID.fromString(validatedId);
}
@Override
public String getName() {
if (isMetadataInitialized()) return metadata.getName();
return name;
}
@Override
public void setName(String name) {
metadata.setName(name);
}
@Override
public String getKeycloakInstanceIdentifier() {
return metadata.getKeycloakInstanceIdentifier();
}
@Override
public void setKeycloakInstanceIdentifier(String keycloakInstanceIdentifier) {
metadata.setKeycloakInstanceIdentifier(keycloakInstanceIdentifier);
}
@Override
public Long getTimeAcquired() {
return metadata.getTimeAcquired();
}
@Override
public void setTimeAcquired(Long timeAcquired) {
metadata.setTimeAcquired(timeAcquired);
}
@Override
public int hashCode() {
return getClass().hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof JpaLockEntity)) return false;
return Objects.equals(getId(), ((JpaLockEntity) obj).getId());
}
}

View file

@ -0,0 +1,44 @@
/*
* Copyright 2023 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.lock.entity;
import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.lock.MapLockEntityImpl;
import java.io.Serializable;
public class JpaLockMetadata extends MapLockEntityImpl implements Serializable {
public JpaLockMetadata(DeepCloner cloner) {
super(cloner);
}
public JpaLockMetadata() {
super(DeepCloner.DUMB_CLONER);
}
private Integer entityVersion;
public Integer getEntityVersion() {
return entityVersion;
}
public void setEntityVersion(Integer entityVersion) {
this.entityVersion = entityVersion;
}
}

View file

@ -25,6 +25,7 @@ limitations under the License.
<include file="META-INF/jpa-clients-changelog.xml"/> <include file="META-INF/jpa-clients-changelog.xml"/>
<include file="META-INF/jpa-events-changelog.xml"/> <include file="META-INF/jpa-events-changelog.xml"/>
<include file="META-INF/jpa-groups-changelog.xml"/> <include file="META-INF/jpa-groups-changelog.xml"/>
<include file="META-INF/jpa-locks-changelog.xml"/>
<include file="META-INF/jpa-realms-changelog.xml"/> <include file="META-INF/jpa-realms-changelog.xml"/>
<include file="META-INF/jpa-roles-changelog.xml"/> <include file="META-INF/jpa-roles-changelog.xml"/>
<include file="META-INF/jpa-single-use-objects-changelog.xml"/> <include file="META-INF/jpa-single-use-objects-changelog.xml"/>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2023 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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<include file="META-INF/locks/jpa-locks-changelog-1.xml"/>
</databaseChangeLog>

View file

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2023 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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
<changeSet author="keycloak" id="locks-1">
<createTable tableName="kc_lock">
<column name="id" type="UUID">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="version" type="INTEGER" defaultValueNumeric="0">
<constraints nullable="false"/>
</column>
<column name="metadata" type="json"/>
</createTable>
<ext:addGeneratedColumn tableName="kc_lock">
<ext:column name="entityversion" type="INTEGER" jsonColumn="metadata" jsonProperty="entityVersion"/>
<ext:column name="name" type="VARCHAR(255)" jsonColumn="metadata" jsonProperty="fName"/>
</ext:addGeneratedColumn>
<createIndex tableName="kc_lock" indexName="lock_entityVersion">
<column name="entityversion"/>
</createIndex>
<createIndex tableName="kc_lock" indexName="lock_name" unique="true">
<column name="name"/>
</createIndex>
</changeSet>
</databaseChangeLog>

View file

@ -0,0 +1,20 @@
#
# Copyright 2023 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.
#
# This is only used when running via Undertow and using the DefaultLiquibaseConnectionProvider.
# When using Quarkus, the class is explicitly named inside the LiquibaseProcessor.
org.keycloak.models.map.storage.jpa.liquibase.lockservice.KeycloakLockService

View file

@ -65,5 +65,7 @@
<class>org.keycloak.models.map.storage.jpa.user.entity.JpaUserAttributeEntity</class> <class>org.keycloak.models.map.storage.jpa.user.entity.JpaUserAttributeEntity</class>
<class>org.keycloak.models.map.storage.jpa.user.entity.JpaUserConsentEntity</class> <class>org.keycloak.models.map.storage.jpa.user.entity.JpaUserConsentEntity</class>
<class>org.keycloak.models.map.storage.jpa.user.entity.JpaUserFederatedIdentityEntity</class> <class>org.keycloak.models.map.storage.jpa.user.entity.JpaUserFederatedIdentityEntity</class>
<!--locks-->
<class>org.keycloak.models.map.storage.jpa.lock.entity.JpaLockEntity</class>
</persistence-unit> </persistence-unit>
</persistence> </persistence>

View file

@ -0,0 +1,194 @@
/*
* Copyright 2023 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.lock;
import org.keycloak.common.util.Retry;
import org.keycloak.common.util.Time;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionTaskWithResult;
import org.keycloak.models.locking.GlobalLockProvider;
import org.keycloak.models.locking.LockAcquiringTimeoutException;
import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.storage.MapKeycloakTransaction;
import org.keycloak.models.map.storage.MapStorage;
import org.keycloak.models.map.storage.ModelCriteriaBuilder;
import org.keycloak.models.map.storage.QueryParameters;
import org.keycloak.models.map.storage.criteria.DefaultModelCriteria;
import org.keycloak.models.utils.KeycloakModelUtils;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.time.Duration;
import java.time.Instant;
import java.util.Optional;
import java.util.function.Supplier;
import static org.keycloak.models.map.storage.criteria.DefaultModelCriteria.criteria;
/**
* Implementing a {@link GlobalLockProvider} based on a map storage.
* This requires the map store to support the entity type {@link MapLockEntity}. One of the stores which supports
* this is the JPA Map Store. The store needs to support the uniqueness of entries in the lock area, see
* {@link #lock(String)} for details.
*
* @author Alexander Schwartz
*/
public class MapGlobalLockProvider implements GlobalLockProvider {
private final KeycloakSession session;
private final long defaultTimeoutMilliseconds;
private MapKeycloakTransaction<MapLockEntity, MapLockEntity> tx;
/**
* The lockStoreSupplier allows the store to be initialized lazily and only when needed: As this provider is initialized
* for both the outer and the inner transactions, and the store is needed only for the inner transactions.
*/
private final Supplier<MapStorage<MapLockEntity, MapLockEntity>> lockStoreSupplier;
public MapGlobalLockProvider(KeycloakSession session, long defaultTimeoutMilliseconds, Supplier<MapStorage<MapLockEntity, MapLockEntity>> lockStoreSupplier) {
this.defaultTimeoutMilliseconds = defaultTimeoutMilliseconds;
this.session = session;
this.lockStoreSupplier = lockStoreSupplier;
}
@Override
public <V> V withLock(String lockName, Duration timeToWaitForLock, KeycloakSessionTaskWithResult<V> task) throws LockAcquiringTimeoutException {
MapLockEntity[] lockEntity = {null};
try {
if (timeToWaitForLock == null) {
// Set default timeout if null provided
timeToWaitForLock = Duration.ofMillis(defaultTimeoutMilliseconds);
}
String[] keycloakInstanceIdentifier = {null};
Instant[] timeWhenAcquired = {null};
try {
Retry.executeWithBackoff(i -> lockEntity[0] = KeycloakModelUtils.runJobInTransactionWithResult(this.session.getKeycloakSessionFactory(),
innerSession -> {
MapGlobalLockProvider provider = (MapGlobalLockProvider) innerSession.getProvider(GlobalLockProvider.class);
// even if the call to provider.lock() succeeds, due to concurrency one can only be sure after a commit that all DB constraints have been met
return provider.lock(lockName);
}), (iteration, t) -> {
if (t instanceof LockAcquiringTimeoutException) {
LockAcquiringTimeoutException ex = (LockAcquiringTimeoutException) t;
keycloakInstanceIdentifier[0] = ex.getKeycloakInstanceIdentifier();
timeWhenAcquired[0] = ex.getTimeWhenAcquired();
}
}, timeToWaitForLock, 500);
} catch (RuntimeException ex) {
if (!(ex instanceof LockAcquiringTimeoutException)) {
throw new LockAcquiringTimeoutException(lockName, keycloakInstanceIdentifier[0], timeWhenAcquired[0], ex);
}
throw ex;
}
return KeycloakModelUtils.runJobInTransactionWithResult(this.session.getKeycloakSessionFactory(), task);
} finally {
if (lockEntity[0] != null) {
KeycloakModelUtils.runJobInTransaction(this.session.getKeycloakSessionFactory(), innerSession -> {
MapGlobalLockProvider provider = (MapGlobalLockProvider) innerSession.getProvider(GlobalLockProvider.class);
provider.unlock(lockEntity[0]);
});
}
}
}
@Override
public void forceReleaseAllLocks() {
KeycloakModelUtils.runJobInTransaction(this.session.getKeycloakSessionFactory(), innerSession -> {
MapGlobalLockProvider provider = (MapGlobalLockProvider) innerSession.getProvider(GlobalLockProvider.class);
provider.releaseAllLocks();
});
}
@Override
public void close() {
}
private void prepareTx() {
if (tx == null) {
this.tx = lockStoreSupplier.get().createTransaction(session);
session.getTransactionManager().enlist(tx);
}
}
/**
* Create a {@link MapLockEntity} for the provided <code>lockName</code>.
* The underlying store must ensure that a lock with the given name can be created only once in the store.
* This constraint needs to be checked either at the time of creation, or at the latest when the transaction
* is committed. If such a constraint violation is detected at the time of the transaction commit, it should
* throw an exception and the transaction should roll back.
* <p/>
* The JPA Map Store implements this with a unique index, which is checked by the database both at the time of
* insertion and at the time the transaction is committed.
*/
private MapLockEntity lock(String lockName) {
prepareTx();
DefaultModelCriteria<MapLockEntity> mcb = criteria();
mcb = mcb.compare(MapLockEntity.SearchableFields.NAME, ModelCriteriaBuilder.Operator.EQ, lockName);
Optional<MapLockEntity> entry = tx.read(QueryParameters.withCriteria(mcb)).findFirst();
if (entry.isEmpty()) {
MapLockEntity entity = DeepCloner.DUMB_CLONER.newInstance(MapLockEntity.class);
entity.setName(lockName);
entity.setKeycloakInstanceIdentifier(getKeycloakInstanceIdentifier());
entity.setTimeAcquired(Time.currentTimeMillis());
return tx.create(entity);
} else {
throw new LockAcquiringTimeoutException(lockName, entry.get().getKeycloakInstanceIdentifier(), Instant.ofEpochMilli(entry.get().getTimeAcquired()));
}
}
/**
* Unlock the previously created lock.
* Will fail if the lock doesn't exist, or has a different owner.
*/
private void unlock(MapLockEntity lockEntity) {
prepareTx();
MapLockEntity readLockEntity = tx.read(lockEntity.getId());
if (readLockEntity == null) {
throw new RuntimeException("didn't find lock - someone else unlocked it?");
} else if (!lockEntity.isLockUnchanged(readLockEntity)) {
// this case is there for stores which might re-use IDs or derive it from the name of the entity (like the file store map store does in some cases).
throw new RuntimeException(String.format("Lock owned by different instance: Lock [%s] acquired by keycloak instance [%s] at the time [%s]",
readLockEntity.getName(), readLockEntity.getKeycloakInstanceIdentifier(), readLockEntity.getTimeAcquired()));
} else {
tx.delete(readLockEntity.getId());
}
}
private void releaseAllLocks() {
prepareTx();
DefaultModelCriteria<MapLockEntity> mcb = criteria();
tx.delete(QueryParameters.withCriteria(mcb));
}
private static String getKeycloakInstanceIdentifier() {
long pid = ProcessHandle.current().pid();
String hostname;
try {
hostname = InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException e) {
hostname = "unknown-host";
}
String threadName = Thread.currentThread().getName();
return threadName + "#" + pid + "@" + hostname;
}
}

View file

@ -0,0 +1,95 @@
/*
* Copyright 2023 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.lock;
import org.keycloak.Config;
import org.keycloak.common.Profile;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.locking.GlobalLockProvider;
import org.keycloak.models.locking.GlobalLockProviderFactory;
import org.keycloak.models.map.common.AbstractMapProviderFactory;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import java.util.List;
/**
* Factory to create a GlobalLockProvider backed by a Map store.
*
* @author Alexander Schwartz
*/
public class MapGlobalLockProviderFactory extends AbstractMapProviderFactory<GlobalLockProvider, MapLockEntity, MapLockEntity> implements GlobalLockProviderFactory, EnvironmentDependentProviderFactory {
public static final String DEFAULT_TIMEOUT_MILLISECONDS = "defaultTimeoutMilliseconds";
public static final long DEFAULT_VALUE = 5000L;
private long defaultTimeoutMilliseconds;
public MapGlobalLockProviderFactory() {
super(MapLockEntity.class, GlobalLockProvider.class);
}
@Override
public MapGlobalLockProvider createNew(KeycloakSession session) {
return new MapGlobalLockProvider(session, defaultTimeoutMilliseconds, () -> getStorage(session));
}
@Override
public void init(Config.Scope config) {
super.init(config);
defaultTimeoutMilliseconds = config.getLong(DEFAULT_TIMEOUT_MILLISECONDS, DEFAULT_VALUE);
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public boolean isSupported() {
return Profile.isFeatureEnabled(Profile.Feature.MAP_STORAGE);
}
@Override
public String getHelpText() {
return "Lock provider";
}
@Override
public List<ProviderConfigProperty> getConfigMetadata() {
return ProviderConfigurationBuilder.create()
.property()
.name(DEFAULT_TIMEOUT_MILLISECONDS)
.type("int")
.helpText("Default timeout when waiting for a lock")
.defaultValue(DEFAULT_VALUE)
.add()
.build();
}
}

View file

@ -0,0 +1,77 @@
/*
* Copyright 2023 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.lock;
import org.keycloak.models.map.annotations.GenerateEntityImplementations;
import org.keycloak.models.map.common.AbstractEntity;
import org.keycloak.models.map.common.DeepCloner;
import org.keycloak.models.map.common.UpdatableEntity;
import org.keycloak.storage.SearchableModelField;
import java.util.Objects;
/**
* Entity to hold locks needed for the {@link MapGlobalLockProvider}.
*
* @author Alexander Schwartz
*/
@GenerateEntityImplementations(
inherits = "org.keycloak.models.map.lock.MapLockEntity.AbstractLockEntity"
)
@DeepCloner.Root
public interface MapLockEntity extends UpdatableEntity, AbstractEntity {
public static class SearchableFields {
public static final SearchableModelField<MapLockEntity> NAME = new SearchableModelField<>("name", String.class);
}
public abstract class AbstractLockEntity extends Impl implements MapLockEntity {
private String id;
@Override
public String getId() {
return this.id;
}
@Override
public void setId(String id) {
if (this.id != null) throw new IllegalStateException("Id cannot be changed");
this.id = id;
this.updated |= id != null;
}
}
String getName();
void setName(String name);
String getKeycloakInstanceIdentifier();
void setKeycloakInstanceIdentifier(String keycloakInstanceIdentifier);
Long getTimeAcquired();
void setTimeAcquired(Long timeAcquired);
default boolean isLockUnchanged(MapLockEntity otherMapLock) {
return Objects.equals(getKeycloakInstanceIdentifier(), otherMapLock.getKeycloakInstanceIdentifier()) &&
Objects.equals(getTimeAcquired(), otherMapLock.getTimeAcquired()) &&
Objects.equals(getName(), otherMapLock.getName()) &&
Objects.equals(getId(), otherMapLock.getId());
}
}

View file

@ -32,6 +32,7 @@ import org.keycloak.models.RoleModel;
import org.keycloak.models.UserLoginFailureModel; import org.keycloak.models.UserLoginFailureModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel; import org.keycloak.models.UserSessionModel;
import org.keycloak.models.map.lock.MapLockEntity;
import org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntity; import org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntity;
import org.keycloak.models.map.authSession.MapRootAuthenticationSessionEntity; import org.keycloak.models.map.authSession.MapRootAuthenticationSessionEntity;
import org.keycloak.models.map.authorization.entity.MapPermissionTicketEntity; import org.keycloak.models.map.authorization.entity.MapPermissionTicketEntity;
@ -96,6 +97,9 @@ public class ModelEntityUtil {
// events // events
MODEL_TO_NAME.put(AdminEvent.class, "admin-events"); MODEL_TO_NAME.put(AdminEvent.class, "admin-events");
MODEL_TO_NAME.put(Event.class, "auth-events"); MODEL_TO_NAME.put(Event.class, "auth-events");
// locks
MODEL_TO_NAME.put(MapLockEntity.class, "locks");
} }
private static final Map<String, Class<?>> NAME_TO_MODEL = MODEL_TO_NAME.entrySet().stream().collect(Collectors.toUnmodifiableMap(Entry::getValue, Entry::getKey)); private static final Map<String, Class<?>> NAME_TO_MODEL = MODEL_TO_NAME.entrySet().stream().collect(Collectors.toUnmodifiableMap(Entry::getValue, Entry::getKey));

View file

@ -0,0 +1,18 @@
#
# Copyright 2023 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.
#
org.keycloak.models.map.lock.MapGlobalLockProviderFactory

View file

@ -12,7 +12,6 @@ import java.util.Set;
import io.quarkus.agroal.spi.JdbcDataSourceBuildItem; import io.quarkus.agroal.spi.JdbcDataSourceBuildItem;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import liquibase.lockservice.StandardLockService;
import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.ClassInfo; import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName; import org.jboss.jandex.DotName;
@ -29,6 +28,7 @@ import liquibase.parser.ChangeLogParser;
import liquibase.parser.core.xml.XMLChangeLogSAXParser; import liquibase.parser.core.xml.XMLChangeLogSAXParser;
import liquibase.servicelocator.LiquibaseService; import liquibase.servicelocator.LiquibaseService;
import liquibase.sqlgenerator.SqlGenerator; import liquibase.sqlgenerator.SqlGenerator;
import org.keycloak.models.map.storage.jpa.liquibase.lockservice.KeycloakLockService;
import org.keycloak.quarkus.runtime.KeycloakRecorder; import org.keycloak.quarkus.runtime.KeycloakRecorder;
import static org.keycloak.config.StorageOptions.STORAGE; import static org.keycloak.config.StorageOptions.STORAGE;
@ -81,7 +81,7 @@ class LiquibaseProcessor {
} }
if (StorageOptions.StorageType.jpa.name().equals(getOptionalValue(NS_KEYCLOAK_PREFIX.concat(STORAGE.getKey())).orElse(null))) { if (StorageOptions.StorageType.jpa.name().equals(getOptionalValue(NS_KEYCLOAK_PREFIX.concat(STORAGE.getKey())).orElse(null))) {
services.put(LockService.class.getName(), Collections.singletonList(StandardLockService.class.getName())); services.put(LockService.class.getName(), Collections.singletonList(KeycloakLockService.class.getName()));
} else { } else {
services.put(LockService.class.getName(), Collections.singletonList(DummyLockService.class.getName())); services.put(LockService.class.getName(), Collections.singletonList(DummyLockService.class.getName()));
} }

View file

@ -203,6 +203,12 @@ final class StoragePropertyMappers {
.transformer(StoragePropertyMappers::getGlobalLockProvider) .transformer(StoragePropertyMappers::getGlobalLockProvider)
.paramLabel("type") .paramLabel("type")
.build(), .build(),
fromOption(StorageOptions.STORAGE_GLOBAL_LOCK_PROVIDER)
.to("kc.spi-global-lock-map-storage-provider")
.mapFrom("storage")
.transformer(StoragePropertyMappers::resolveMapStorageProvider)
.paramLabel("type")
.build(),
fromOption(StorageOptions.STORAGE_CACHE_REALM_ENABLED) fromOption(StorageOptions.STORAGE_CACHE_REALM_ENABLED)
.to("kc.spi-realm-cache-default-enabled") .to("kc.spi-realm-cache-default-enabled")
.mapFrom("storage") .mapFrom("storage")
@ -339,10 +345,15 @@ final class StoragePropertyMappers {
private static Optional<String> getGlobalLockProvider(Optional<String> storage, ConfigSourceInterceptorContext context) { private static Optional<String> getGlobalLockProvider(Optional<String> storage, ConfigSourceInterceptorContext context) {
try { try {
if (storage.isPresent()) { if (storage.isPresent()) {
return of(storage.map(StorageType::valueOf) StorageType storageType = StorageType.valueOf(storage.get());
.filter(type -> type.equals(StorageType.hotrod)) switch (storageType) {
.map(StorageType::getProvider) case hotrod:
.orElse("none")); return Optional.of(storageType.getProvider());
case jpa:
return Optional.of("map");
default:
return Optional.of("none");
}
} }
} catch (IllegalArgumentException iae) { } catch (IllegalArgumentException iae) {
throw new IllegalArgumentException("Invalid storage provider: " + storage.orElse(null), iae); throw new IllegalArgumentException("Invalid storage provider: " + storage.orElse(null), iae);

View file

@ -34,7 +34,7 @@ public class JPAStoreDistTest {
void testSuccessful(LaunchResult result) { void testSuccessful(LaunchResult result) {
CLIResult cliResult = (CLIResult) result; CLIResult cliResult = (CLIResult) result;
cliResult.assertMessage("Experimental features enabled: map-storage"); cliResult.assertMessage("Experimental features enabled: map-storage");
cliResult.assertMessage("[org.keycloak.models.map.storage.jpa.liquibase.updater.MapJpaLiquibaseUpdaterProvider] (main) Initializing database schema. Using changelog META-INF/jpa-realms-changelog.xml"); cliResult.assertMessage("[org.keycloak.models.map.storage.jpa.liquibase.updater.MapJpaLiquibaseUpdaterProvider] (main) Initializing database schema. Using changelog META-INF");
cliResult.assertStarted(); cliResult.assertStarted();
} }
} }

View file

@ -24,6 +24,10 @@ import java.time.Instant;
*/ */
public final class LockAcquiringTimeoutException extends RuntimeException { public final class LockAcquiringTimeoutException extends RuntimeException {
private final String lockName;
private final String keycloakInstanceIdentifier;
private final Instant timeWhenAcquired;
/** /**
* *
* @param lockName Identifier of a lock whose acquiring was unsuccessful. * @param lockName Identifier of a lock whose acquiring was unsuccessful.
@ -31,7 +35,10 @@ public final class LockAcquiringTimeoutException extends RuntimeException {
* @param timeWhenAcquired Time instant when the lock held by {@code keycloakInstanceIdentifier} was acquired. * @param timeWhenAcquired Time instant when the lock held by {@code keycloakInstanceIdentifier} was acquired.
*/ */
public LockAcquiringTimeoutException(String lockName, String keycloakInstanceIdentifier, Instant timeWhenAcquired) { public LockAcquiringTimeoutException(String lockName, String keycloakInstanceIdentifier, Instant timeWhenAcquired) {
super(String.format("Lock [%s] already acquired by keycloak instance [%s] at the time [%s]", lockName, keycloakInstanceIdentifier, timeWhenAcquired.toString())); super(String.format("Lock [%s] already acquired by keycloak instance [%s] at the time [%s]", lockName, keycloakInstanceIdentifier, timeWhenAcquired));
this.lockName = lockName;
this.keycloakInstanceIdentifier = keycloakInstanceIdentifier;
this.timeWhenAcquired = timeWhenAcquired;
} }
/** /**
@ -42,6 +49,21 @@ public final class LockAcquiringTimeoutException extends RuntimeException {
* @param cause The cause. * @param cause The cause.
*/ */
public LockAcquiringTimeoutException(String lockName, String keycloakInstanceIdentifier, Instant timeWhenAcquired, Throwable cause) { public LockAcquiringTimeoutException(String lockName, String keycloakInstanceIdentifier, Instant timeWhenAcquired, Throwable cause) {
super(String.format("Lock [%s] already acquired by keycloak instance [%s] at the time [%s]", lockName, keycloakInstanceIdentifier, timeWhenAcquired.toString()), cause); super(String.format("Lock [%s] already acquired by keycloak instance [%s] at the time [%s]", lockName, keycloakInstanceIdentifier, timeWhenAcquired), cause);
this.lockName = lockName;
this.keycloakInstanceIdentifier = keycloakInstanceIdentifier;
this.timeWhenAcquired = timeWhenAcquired;
}
public String getLockName() {
return lockName;
}
public String getKeycloakInstanceIdentifier() {
return keycloakInstanceIdentifier;
}
public Instant getTimeWhenAcquired() {
return timeWhenAcquired;
} }
} }

View file

@ -910,7 +910,8 @@
<keycloak.user.map.storage.provider>jpa</keycloak.user.map.storage.provider> <keycloak.user.map.storage.provider>jpa</keycloak.user.map.storage.provider>
<keycloak.userSession.map.storage.provider>jpa</keycloak.userSession.map.storage.provider> <keycloak.userSession.map.storage.provider>jpa</keycloak.userSession.map.storage.provider>
<auth.server.quarkus.mapStorage.profile.config>jpa</auth.server.quarkus.mapStorage.profile.config> <auth.server.quarkus.mapStorage.profile.config>jpa</auth.server.quarkus.mapStorage.profile.config>
<keycloak.globalLock.provider>none</keycloak.globalLock.provider> <keycloak.globalLock.provider>map</keycloak.globalLock.provider>
<keycloak.lock.map.storage.provider>jpa</keycloak.lock.map.storage.provider>
</systemPropertyVariables> </systemPropertyVariables>
</configuration> </configuration>
</plugin> </plugin>
@ -1092,7 +1093,8 @@
<keycloak.user.map.storage.provider>jpa</keycloak.user.map.storage.provider> <keycloak.user.map.storage.provider>jpa</keycloak.user.map.storage.provider>
<keycloak.userSession.map.storage.provider>jpa</keycloak.userSession.map.storage.provider> <keycloak.userSession.map.storage.provider>jpa</keycloak.userSession.map.storage.provider>
<auth.server.quarkus.mapStorage.profile.config>jpa</auth.server.quarkus.mapStorage.profile.config> <auth.server.quarkus.mapStorage.profile.config>jpa</auth.server.quarkus.mapStorage.profile.config>
<keycloak.globalLock.provider>none</keycloak.globalLock.provider> <keycloak.globalLock.provider>map</keycloak.globalLock.provider>
<keycloak.lock.map.storage.provider>jpa</keycloak.lock.map.storage.provider>
</systemPropertyVariables> </systemPropertyVariables>
</configuration> </configuration>
</plugin> </plugin>

View file

@ -52,7 +52,12 @@
}, },
"globalLock": { "globalLock": {
"provider": "${keycloak.globalLock.provider:dblock}" "provider": "${keycloak.globalLock.provider:dblock}",
"map": {
"storage": {
"provider": "${keycloak.lock.map.storage.provider,keycloak.mapStorage.provider.default:concurrenthashmap}"
}
}
}, },
"realm": { "realm": {

View file

@ -25,6 +25,7 @@ import org.keycloak.models.DeploymentStateSpi;
import org.keycloak.models.SingleUseObjectSpi; import org.keycloak.models.SingleUseObjectSpi;
import org.keycloak.models.UserLoginFailureSpi; import org.keycloak.models.UserLoginFailureSpi;
import org.keycloak.models.UserSessionSpi; import org.keycloak.models.UserSessionSpi;
import org.keycloak.models.locking.GlobalLockProviderSpi;
import org.keycloak.models.map.authSession.MapRootAuthenticationSessionProviderFactory; import org.keycloak.models.map.authSession.MapRootAuthenticationSessionProviderFactory;
import org.keycloak.models.map.authorization.MapAuthorizationStoreFactory; import org.keycloak.models.map.authorization.MapAuthorizationStoreFactory;
import org.keycloak.models.map.client.MapClientProviderFactory; import org.keycloak.models.map.client.MapClientProviderFactory;
@ -39,6 +40,7 @@ import org.keycloak.models.map.role.MapRoleProviderFactory;
import org.keycloak.models.map.singleUseObject.MapSingleUseObjectProviderFactory; import org.keycloak.models.map.singleUseObject.MapSingleUseObjectProviderFactory;
import org.keycloak.models.map.storage.MapStorageSpi; import org.keycloak.models.map.storage.MapStorageSpi;
import org.keycloak.models.map.storage.chm.ConcurrentHashMapStorageProviderFactory; import org.keycloak.models.map.storage.chm.ConcurrentHashMapStorageProviderFactory;
import org.keycloak.models.map.lock.MapGlobalLockProviderFactory;
import org.keycloak.models.map.storage.jpa.JpaMapStorageProviderFactory; import org.keycloak.models.map.storage.jpa.JpaMapStorageProviderFactory;
import org.keycloak.models.map.storage.jpa.liquibase.connection.MapLiquibaseConnectionProviderFactory; import org.keycloak.models.map.storage.jpa.liquibase.connection.MapLiquibaseConnectionProviderFactory;
import org.keycloak.models.map.storage.jpa.liquibase.connection.MapLiquibaseConnectionSpi; import org.keycloak.models.map.storage.jpa.liquibase.connection.MapLiquibaseConnectionSpi;
@ -79,6 +81,7 @@ public class JpaMapStorage extends KeycloakModelParameters {
.add(JpaMapStorageProviderFactory.class) .add(JpaMapStorageProviderFactory.class)
.add(MapJpaUpdaterProviderFactory.class) .add(MapJpaUpdaterProviderFactory.class)
.add(MapLiquibaseConnectionProviderFactory.class) .add(MapLiquibaseConnectionProviderFactory.class)
.add(MapGlobalLockProviderFactory.class)
.build(); .build();
public JpaMapStorage() { public JpaMapStorage() {
@ -114,7 +117,9 @@ public class JpaMapStorage extends KeycloakModelParameters {
.spi("publicKeyStorage").provider(MapPublicKeyStorageProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) .spi("publicKeyStorage").provider(MapPublicKeyStorageProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi(UserSessionSpi.NAME).provider(MapUserSessionProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID) .spi(UserSessionSpi.NAME).provider(MapUserSessionProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
.spi(EventStoreSpi.NAME).provider(MapEventStoreProviderFactory.PROVIDER_ID) .config("storage-admin-events.provider", JpaMapStorageProviderFactory.PROVIDER_ID) .spi(EventStoreSpi.NAME).provider(MapEventStoreProviderFactory.PROVIDER_ID) .config("storage-admin-events.provider", JpaMapStorageProviderFactory.PROVIDER_ID)
.config("storage-auth-events.provider", JpaMapStorageProviderFactory.PROVIDER_ID); .config("storage-auth-events.provider", JpaMapStorageProviderFactory.PROVIDER_ID)
.spi(GlobalLockProviderSpi.GLOBAL_LOCK) .config("provider", MapGlobalLockProviderFactory.PROVIDER_ID)
.spi(GlobalLockProviderSpi.GLOBAL_LOCK).provider(MapGlobalLockProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID);
} }
@Override @Override

View file

@ -17,13 +17,13 @@
package org.keycloak.testsuite.model.parameters; package org.keycloak.testsuite.model.parameters;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import org.jboss.logging.Logger;
import org.keycloak.authorization.store.StoreFactorySpi; import org.keycloak.authorization.store.StoreFactorySpi;
import org.keycloak.events.EventStoreSpi; import org.keycloak.events.EventStoreSpi;
import org.keycloak.models.DeploymentStateSpi; import org.keycloak.models.DeploymentStateSpi;
import org.keycloak.models.SingleUseObjectSpi; import org.keycloak.models.SingleUseObjectSpi;
import org.keycloak.models.UserLoginFailureSpi; import org.keycloak.models.UserLoginFailureSpi;
import org.keycloak.models.UserSessionSpi; import org.keycloak.models.UserSessionSpi;
import org.keycloak.models.locking.GlobalLockProviderSpi;
import org.keycloak.models.map.authSession.MapRootAuthenticationSessionProviderFactory; import org.keycloak.models.map.authSession.MapRootAuthenticationSessionProviderFactory;
import org.keycloak.models.map.authorization.MapAuthorizationStoreFactory; import org.keycloak.models.map.authorization.MapAuthorizationStoreFactory;
import org.keycloak.models.map.client.MapClientProviderFactory; import org.keycloak.models.map.client.MapClientProviderFactory;
@ -32,6 +32,7 @@ import org.keycloak.models.map.deploymentState.MapDeploymentStateProviderFactory
import org.keycloak.models.map.events.MapEventStoreProviderFactory; import org.keycloak.models.map.events.MapEventStoreProviderFactory;
import org.keycloak.models.map.group.MapGroupProviderFactory; import org.keycloak.models.map.group.MapGroupProviderFactory;
import org.keycloak.models.map.keys.MapPublicKeyStorageProviderFactory; import org.keycloak.models.map.keys.MapPublicKeyStorageProviderFactory;
import org.keycloak.models.map.lock.MapGlobalLockProviderFactory;
import org.keycloak.models.map.loginFailure.MapUserLoginFailureProviderFactory; import org.keycloak.models.map.loginFailure.MapUserLoginFailureProviderFactory;
import org.keycloak.models.map.realm.MapRealmProviderFactory; import org.keycloak.models.map.realm.MapRealmProviderFactory;
import org.keycloak.models.map.role.MapRoleProviderFactory; import org.keycloak.models.map.role.MapRoleProviderFactory;
@ -60,8 +61,6 @@ import static org.keycloak.testsuite.model.transaction.StorageTransactionTest.LO
public class JpaMapStorageCockroachdb extends KeycloakModelParameters { public class JpaMapStorageCockroachdb extends KeycloakModelParameters {
private static final Logger LOG = Logger.getLogger(JpaMapStorageCockroachdb.class.getName());
private static final Boolean START_CONTAINER = Boolean.valueOf(System.getProperty("cockroachdb.start-container", "true")); private static final Boolean START_CONTAINER = Boolean.valueOf(System.getProperty("cockroachdb.start-container", "true"));
private static final String COCKROACHDB_DOCKER_IMAGE_NAME = System.getProperty("keycloak.map.storage.cockroachdb.docker.image", "cockroachdb/cockroach:v22.1.0"); private static final String COCKROACHDB_DOCKER_IMAGE_NAME = System.getProperty("keycloak.map.storage.cockroachdb.docker.image", "cockroachdb/cockroach:v22.1.0");
private static final CockroachContainer COCKROACHDB_CONTAINER = new CockroachContainer(DockerImageName.parse(COCKROACHDB_DOCKER_IMAGE_NAME).asCompatibleSubstituteFor("cockroachdb")); private static final CockroachContainer COCKROACHDB_CONTAINER = new CockroachContainer(DockerImageName.parse(COCKROACHDB_DOCKER_IMAGE_NAME).asCompatibleSubstituteFor("cockroachdb"));
@ -80,6 +79,7 @@ public class JpaMapStorageCockroachdb extends KeycloakModelParameters {
.add(JpaMapStorageProviderFactory.class) .add(JpaMapStorageProviderFactory.class)
.add(MapJpaUpdaterProviderFactory.class) .add(MapJpaUpdaterProviderFactory.class)
.add(MapLiquibaseConnectionProviderFactory.class) .add(MapLiquibaseConnectionProviderFactory.class)
.add(MapGlobalLockProviderFactory.class)
.build(); .build();
public JpaMapStorageCockroachdb() { public JpaMapStorageCockroachdb() {
@ -115,7 +115,9 @@ public class JpaMapStorageCockroachdb extends KeycloakModelParameters {
.spi("publicKeyStorage").provider(MapPublicKeyStorageProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID) .spi("publicKeyStorage").provider(MapPublicKeyStorageProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, ConcurrentHashMapStorageProviderFactory.PROVIDER_ID)
.spi(UserSessionSpi.NAME).provider(MapUserSessionProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID) .spi(UserSessionSpi.NAME).provider(MapUserSessionProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID)
.spi(EventStoreSpi.NAME).provider(MapEventStoreProviderFactory.PROVIDER_ID) .config("storage-admin-events.provider", JpaMapStorageProviderFactory.PROVIDER_ID) .spi(EventStoreSpi.NAME).provider(MapEventStoreProviderFactory.PROVIDER_ID) .config("storage-admin-events.provider", JpaMapStorageProviderFactory.PROVIDER_ID)
.config("storage-auth-events.provider", JpaMapStorageProviderFactory.PROVIDER_ID); .config("storage-auth-events.provider", JpaMapStorageProviderFactory.PROVIDER_ID)
.spi(GlobalLockProviderSpi.GLOBAL_LOCK) .config("provider", MapGlobalLockProviderFactory.PROVIDER_ID)
.spi(GlobalLockProviderSpi.GLOBAL_LOCK).provider(MapGlobalLockProviderFactory.PROVIDER_ID) .config(STORAGE_CONFIG, JpaMapStorageProviderFactory.PROVIDER_ID);
} }
@Override @Override

View file

@ -33,7 +33,12 @@
}, },
"globalLock": { "globalLock": {
"provider": "${keycloak.globalLock.provider:dblock}" "provider": "${keycloak.globalLock.provider:dblock}",
"map": {
"storage": {
"provider": "${keycloak.lock.map.storage.provider,keycloak.mapStorage.provider.default:concurrenthashmap}"
}
}
}, },
"realm": { "realm": {