parent
d20c1f8d1d
commit
513bb809f3
28 changed files with 1053 additions and 23 deletions
|
@ -280,7 +280,7 @@ public class GenerateEntityImplementationsProcessor extends AbstractGenerateEnti
|
|||
GenerateEntityImplementations an = e.getAnnotation(GenerateEntityImplementations.class);
|
||||
TypeElement parentTypeElement = elements.getTypeElement((an.inherits() == null || an.inherits().isEmpty()) ? "void" : an.inherits());
|
||||
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);
|
||||
String className = e.getQualifiedName().toString();
|
||||
|
|
|
@ -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_FEDERATED_IDENTITY = 1;
|
||||
public static final Integer CURRENT_SCHEMA_VERSION_USER_SESSION = 1;
|
||||
public static final Integer CURRENT_SCHEMA_VERSION_LOCK = 1;
|
||||
}
|
||||
|
|
|
@ -81,6 +81,7 @@ import org.keycloak.models.locking.GlobalLockProvider;
|
|||
import org.keycloak.models.map.client.MapProtocolMapperEntity;
|
||||
import org.keycloak.models.map.client.MapProtocolMapperEntityImpl;
|
||||
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.MapAuthenticationExecutionEntityImpl;
|
||||
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.group.JpaGroupMapKeycloakTransaction;
|
||||
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.entity.JpaUserLoginFailureEntity;
|
||||
import org.keycloak.models.map.storage.jpa.realm.JpaRealmMapKeycloakTransaction;
|
||||
|
@ -224,6 +227,8 @@ public class JpaMapStorageProviderFactory implements
|
|||
//user/client session
|
||||
.constructor(JpaClientSessionEntity.class, JpaClientSessionEntity::new)
|
||||
.constructor(JpaUserSessionEntity.class, JpaUserSessionEntity::new)
|
||||
//lock
|
||||
.constructor(JpaLockEntity.class, JpaLockEntity::new)
|
||||
.build();
|
||||
|
||||
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);
|
||||
//sessions
|
||||
MODEL_TO_TX.put(UserSessionModel.class, JpaUserSessionMapKeycloakTransaction::new);
|
||||
//locks
|
||||
MODEL_TO_TX.put(MapLockEntity.class, JpaLockMapKeycloakTransaction::new);
|
||||
}
|
||||
|
||||
private boolean jtaEnabled;
|
||||
|
@ -539,10 +546,15 @@ public class JpaMapStorageProviderFactory implements
|
|||
}
|
||||
|
||||
private void update(Class<?> modelType, Connection connection, KeycloakSession session) {
|
||||
session.getProvider(GlobalLockProvider.class).withLock(modelType.getName(), lockedSession -> {
|
||||
lockedSession.getProvider(MapJpaUpdaterProvider.class).update(modelType, connection, config.get("schema"));
|
||||
return null;
|
||||
});
|
||||
if (modelType == MapLockEntity.class) {
|
||||
// as the MapLockEntity is used by the MapGlobalLockProvider itself, don't create a global lock for creating that schema
|
||||
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
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -25,6 +25,7 @@ limitations under the License.
|
|||
<include file="META-INF/jpa-clients-changelog.xml"/>
|
||||
<include file="META-INF/jpa-events-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-roles-changelog.xml"/>
|
||||
<include file="META-INF/jpa-single-use-objects-changelog.xml"/>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
|
@ -65,5 +65,7 @@
|
|||
<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.JpaUserFederatedIdentityEntity</class>
|
||||
<!--locks-->
|
||||
<class>org.keycloak.models.map.storage.jpa.lock.entity.JpaLockEntity</class>
|
||||
</persistence-unit>
|
||||
</persistence>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -32,6 +32,7 @@ import org.keycloak.models.RoleModel;
|
|||
import org.keycloak.models.UserLoginFailureModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.models.map.lock.MapLockEntity;
|
||||
import org.keycloak.models.map.singleUseObject.MapSingleUseObjectEntity;
|
||||
import org.keycloak.models.map.authSession.MapRootAuthenticationSessionEntity;
|
||||
import org.keycloak.models.map.authorization.entity.MapPermissionTicketEntity;
|
||||
|
@ -96,6 +97,9 @@ public class ModelEntityUtil {
|
|||
// events
|
||||
MODEL_TO_NAME.put(AdminEvent.class, "admin-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));
|
||||
|
||||
|
|
|
@ -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
|
|
@ -12,7 +12,6 @@ import java.util.Set;
|
|||
|
||||
import io.quarkus.agroal.spi.JdbcDataSourceBuildItem;
|
||||
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
|
||||
import liquibase.lockservice.StandardLockService;
|
||||
import org.jboss.jandex.AnnotationInstance;
|
||||
import org.jboss.jandex.ClassInfo;
|
||||
import org.jboss.jandex.DotName;
|
||||
|
@ -29,6 +28,7 @@ import liquibase.parser.ChangeLogParser;
|
|||
import liquibase.parser.core.xml.XMLChangeLogSAXParser;
|
||||
import liquibase.servicelocator.LiquibaseService;
|
||||
import liquibase.sqlgenerator.SqlGenerator;
|
||||
import org.keycloak.models.map.storage.jpa.liquibase.lockservice.KeycloakLockService;
|
||||
import org.keycloak.quarkus.runtime.KeycloakRecorder;
|
||||
|
||||
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))) {
|
||||
services.put(LockService.class.getName(), Collections.singletonList(StandardLockService.class.getName()));
|
||||
services.put(LockService.class.getName(), Collections.singletonList(KeycloakLockService.class.getName()));
|
||||
} else {
|
||||
services.put(LockService.class.getName(), Collections.singletonList(DummyLockService.class.getName()));
|
||||
}
|
||||
|
|
|
@ -203,6 +203,12 @@ final class StoragePropertyMappers {
|
|||
.transformer(StoragePropertyMappers::getGlobalLockProvider)
|
||||
.paramLabel("type")
|
||||
.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)
|
||||
.to("kc.spi-realm-cache-default-enabled")
|
||||
.mapFrom("storage")
|
||||
|
@ -339,10 +345,15 @@ final class StoragePropertyMappers {
|
|||
private static Optional<String> getGlobalLockProvider(Optional<String> storage, ConfigSourceInterceptorContext context) {
|
||||
try {
|
||||
if (storage.isPresent()) {
|
||||
return of(storage.map(StorageType::valueOf)
|
||||
.filter(type -> type.equals(StorageType.hotrod))
|
||||
.map(StorageType::getProvider)
|
||||
.orElse("none"));
|
||||
StorageType storageType = StorageType.valueOf(storage.get());
|
||||
switch (storageType) {
|
||||
case hotrod:
|
||||
return Optional.of(storageType.getProvider());
|
||||
case jpa:
|
||||
return Optional.of("map");
|
||||
default:
|
||||
return Optional.of("none");
|
||||
}
|
||||
}
|
||||
} catch (IllegalArgumentException iae) {
|
||||
throw new IllegalArgumentException("Invalid storage provider: " + storage.orElse(null), iae);
|
||||
|
|
|
@ -34,7 +34,7 @@ public class JPAStoreDistTest {
|
|||
void testSuccessful(LaunchResult result) {
|
||||
CLIResult cliResult = (CLIResult) result;
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,10 @@ import java.time.Instant;
|
|||
*/
|
||||
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.
|
||||
|
@ -31,7 +35,10 @@ public final class LockAcquiringTimeoutException extends RuntimeException {
|
|||
* @param timeWhenAcquired Time instant when the lock held by {@code keycloakInstanceIdentifier} was acquired.
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -910,7 +910,8 @@
|
|||
<keycloak.user.map.storage.provider>jpa</keycloak.user.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>
|
||||
<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>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
@ -1092,7 +1093,8 @@
|
|||
<keycloak.user.map.storage.provider>jpa</keycloak.user.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>
|
||||
<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>
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
|
|
@ -52,7 +52,12 @@
|
|||
},
|
||||
|
||||
"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": {
|
||||
|
|
|
@ -25,6 +25,7 @@ import org.keycloak.models.DeploymentStateSpi;
|
|||
import org.keycloak.models.SingleUseObjectSpi;
|
||||
import org.keycloak.models.UserLoginFailureSpi;
|
||||
import org.keycloak.models.UserSessionSpi;
|
||||
import org.keycloak.models.locking.GlobalLockProviderSpi;
|
||||
import org.keycloak.models.map.authSession.MapRootAuthenticationSessionProviderFactory;
|
||||
import org.keycloak.models.map.authorization.MapAuthorizationStoreFactory;
|
||||
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.storage.MapStorageSpi;
|
||||
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.liquibase.connection.MapLiquibaseConnectionProviderFactory;
|
||||
import org.keycloak.models.map.storage.jpa.liquibase.connection.MapLiquibaseConnectionSpi;
|
||||
|
@ -79,6 +81,7 @@ public class JpaMapStorage extends KeycloakModelParameters {
|
|||
.add(JpaMapStorageProviderFactory.class)
|
||||
.add(MapJpaUpdaterProviderFactory.class)
|
||||
.add(MapLiquibaseConnectionProviderFactory.class)
|
||||
.add(MapGlobalLockProviderFactory.class)
|
||||
.build();
|
||||
|
||||
public JpaMapStorage() {
|
||||
|
@ -114,7 +117,9 @@ public class JpaMapStorage extends KeycloakModelParameters {
|
|||
.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(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
|
||||
|
|
|
@ -17,13 +17,13 @@
|
|||
package org.keycloak.testsuite.model.parameters;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.authorization.store.StoreFactorySpi;
|
||||
import org.keycloak.events.EventStoreSpi;
|
||||
import org.keycloak.models.DeploymentStateSpi;
|
||||
import org.keycloak.models.SingleUseObjectSpi;
|
||||
import org.keycloak.models.UserLoginFailureSpi;
|
||||
import org.keycloak.models.UserSessionSpi;
|
||||
import org.keycloak.models.locking.GlobalLockProviderSpi;
|
||||
import org.keycloak.models.map.authSession.MapRootAuthenticationSessionProviderFactory;
|
||||
import org.keycloak.models.map.authorization.MapAuthorizationStoreFactory;
|
||||
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.group.MapGroupProviderFactory;
|
||||
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.realm.MapRealmProviderFactory;
|
||||
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 {
|
||||
|
||||
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 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"));
|
||||
|
@ -80,6 +79,7 @@ public class JpaMapStorageCockroachdb extends KeycloakModelParameters {
|
|||
.add(JpaMapStorageProviderFactory.class)
|
||||
.add(MapJpaUpdaterProviderFactory.class)
|
||||
.add(MapLiquibaseConnectionProviderFactory.class)
|
||||
.add(MapGlobalLockProviderFactory.class)
|
||||
.build();
|
||||
|
||||
public JpaMapStorageCockroachdb() {
|
||||
|
@ -115,7 +115,9 @@ public class JpaMapStorageCockroachdb extends KeycloakModelParameters {
|
|||
.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(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
|
||||
|
|
|
@ -33,7 +33,12 @@
|
|||
},
|
||||
|
||||
"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": {
|
||||
|
|
Loading…
Reference in a new issue