KEYCLOAK-18717 KEYCLOAK-18716 KEYCLOAK-18715 KEYCLOAK-18713 KEYCLOAK-18712 KEYCLOAK-18711 JPA clients no-downtime store

This commit is contained in:
vramik 2021-07-17 19:57:52 +02:00 committed by Hynek Mlnařík
parent 848b170a96
commit c6312e3308
36 changed files with 2420 additions and 21 deletions

View file

@ -44,6 +44,10 @@
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-map</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-map-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-infinispan</artifactId>

View file

@ -29,6 +29,7 @@ import org.keycloak.ServerStartupError;
import org.keycloak.common.util.StackUtil;
import org.keycloak.common.util.StringPropertyReplacer;
import org.keycloak.connections.jpa.updater.JpaUpdaterProvider;
import org.keycloak.connections.jpa.updater.liquibase.LiquibaseJpaUpdaterProviderFactory;
import org.keycloak.connections.jpa.util.JpaUtils;
import org.keycloak.migration.MigrationModelManager;
import org.keycloak.models.KeycloakSession;
@ -338,7 +339,7 @@ public class DefaultJpaConnectionProviderFactory implements JpaConnectionProvide
}
void migration(MigrationStrategy strategy, boolean initializeEmpty, String schema, File databaseUpdateFile, Connection connection, KeycloakSession session) {
JpaUpdaterProvider updater = session.getProvider(JpaUpdaterProvider.class);
JpaUpdaterProvider updater = session.getProvider(JpaUpdaterProvider.class, LiquibaseJpaUpdaterProviderFactory.PROVIDER_ID);
JpaUpdaterProvider.Status status = updater.validate(connection, schema);
if (status == JpaUpdaterProvider.Status.VALID) {

View file

@ -28,6 +28,8 @@ import org.keycloak.models.KeycloakSessionFactory;
*/
public class LiquibaseJpaUpdaterProviderFactory implements JpaUpdaterProviderFactory {
public static final String PROVIDER_ID = "liquibase";
@Override
public JpaUpdaterProvider create(KeycloakSession session) {
return new LiquibaseJpaUpdaterProvider(session);
@ -48,7 +50,7 @@ public class LiquibaseJpaUpdaterProviderFactory implements JpaUpdaterProviderFac
@Override
public String getId() {
return "liquibase";
return PROVIDER_ID;
}
}

69
model/map-jpa/pom.xml Normal file
View file

@ -0,0 +1,69 @@
<?xml version="1.0"?>
<!--
~ Copyright 2021 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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-model-pom</artifactId>
<groupId>org.keycloak</groupId>
<version>16.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-model-map-jpa</artifactId>
<name>Keycloak Model Map JPA</name>
<description/>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-map</artifactId>
</dependency>
<dependency>
<groupId>jakarta.persistence</groupId>
<artifactId>jakarta.persistence-api</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-core</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-jpa</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.hibernate.orm.tooling</groupId>
<artifactId>hibernate-enhance-maven-plugin</artifactId>
<version>${hibernate.core.version}</version>
<executions>
<execution>
<configuration>
<failOnError>true</failOnError>
<enableLazyInitialization>true</enableLazyInitialization>
</configuration>
<goals>
<goal>enhance</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,161 @@
/*
* Copyright 2021 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.client;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Stream;
import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaDelete;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Order;
import javax.persistence.criteria.Root;
import org.keycloak.connections.jpa.JpaKeycloakTransaction;
import org.keycloak.models.ClientModel;
import static org.keycloak.models.jpa.PaginationUtils.paginateQuery;
import org.keycloak.models.map.client.MapClientEntity;
import org.keycloak.models.map.client.MapClientEntityDelegate;
import org.keycloak.models.map.common.StringKeyConvertor.UUIDKey;
import org.keycloak.models.map.storage.jpa.client.delegate.JpaClientDelegateProvider;
import org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity;
import static org.keycloak.models.map.storage.jpa.client.JpaClientMapStorage.SUPPORTED_VERSION;
import static org.keycloak.models.map.storage.jpa.client.JpaClientMapStorageProviderFactory.CLONER;
import org.keycloak.models.map.storage.MapKeycloakTransaction;
import org.keycloak.models.map.storage.QueryParameters;
import static org.keycloak.utils.StreamsUtil.closing;
public class JpaClientMapKeycloakTransaction extends JpaKeycloakTransaction implements MapKeycloakTransaction<MapClientEntity, ClientModel> {
public JpaClientMapKeycloakTransaction(EntityManager em) {
super(em);
}
@Override
public MapClientEntity create(MapClientEntity mapEntity) {
JpaClientEntity jpaEntity = (JpaClientEntity) CLONER.from(mapEntity);
if (mapEntity.getId() == null) {
jpaEntity.setId(UUIDKey.INSTANCE.yieldNewUniqueKey().toString());
}
jpaEntity.setEntityVersion(SUPPORTED_VERSION);
em.persist(jpaEntity);
return jpaEntity;
}
@Override
public MapClientEntity read(String key) {
if (key == null) return null;
UUID uuid = UUIDKey.INSTANCE.fromStringSafe(key);
if (uuid == null) return null;
return em.find(JpaClientEntity.class, uuid);
}
@Override
public Stream<MapClientEntity> read(QueryParameters<ClientModel> queryParameters) {
JpaClientModelCriteriaBuilder mcb = queryParameters.getModelCriteriaBuilder()
.flashToModelCriteriaBuilder(new JpaClientModelCriteriaBuilder());
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<JpaClientEntity> query = cb.createQuery(JpaClientEntity.class);
Root<JpaClientEntity> root = query.from(JpaClientEntity.class);
query.select(cb.construct(JpaClientEntity.class,
root.get("id"),
root.get("entityVersion"),
root.get("realmId"),
root.get("clientId"),
root.get("protocol"),
root.get("enabled")
));
//ordering
if (!queryParameters.getOrderBy().isEmpty()) {
List<Order> orderByList = new LinkedList<>();
for (QueryParameters.OrderBy<ClientModel> order : queryParameters.getOrderBy()) {
switch (order.getOrder()) {
case ASCENDING:
orderByList.add(cb.asc(root.get(order.getModelField().getName())));
break;
case DESCENDING:
orderByList.add(cb.desc(root.get(order.getModelField().getName())));
break;
default:
throw new UnsupportedOperationException("Unknown ordering.");
}
}
query.orderBy(orderByList);
}
if (mcb.getPredicateFunc() != null) query.where(mcb.getPredicateFunc().apply(cb, root));
return closing(
paginateQuery(em.createQuery(query), queryParameters.getOffset(), queryParameters.getLimit())
.getResultStream())
.map(c -> new MapClientEntityDelegate(new JpaClientDelegateProvider(c, em)));
}
@Override
public long getCount(QueryParameters<ClientModel> queryParameters) {
JpaClientModelCriteriaBuilder mcb = queryParameters.getModelCriteriaBuilder()
.flashToModelCriteriaBuilder(new JpaClientModelCriteriaBuilder());
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Long> countQuery = cb.createQuery(Long.class);
Root<JpaClientEntity> root = countQuery.from(JpaClientEntity.class);
countQuery.select(cb.count(root));
if (mcb.getPredicateFunc() != null) countQuery.where(mcb.getPredicateFunc().apply(cb, root));
return em.createQuery(countQuery).getSingleResult();
}
@Override
public boolean delete(String key) {
if (key == null) return false;
UUID uuid = UUIDKey.INSTANCE.fromStringSafe(key);
if (uuid == null) return false;
em.remove(em.getReference(JpaClientEntity.class, uuid));
return true;
}
@Override
public long delete(QueryParameters<ClientModel> queryParameters) {
JpaClientModelCriteriaBuilder mcb = queryParameters.getModelCriteriaBuilder()
.flashToModelCriteriaBuilder(new JpaClientModelCriteriaBuilder());
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaDelete<JpaClientEntity> deleteQuery = cb.createCriteriaDelete(JpaClientEntity.class);
Root<JpaClientEntity> root = deleteQuery.from(JpaClientEntity.class);
if (mcb.getPredicateFunc() != null) deleteQuery.where(mcb.getPredicateFunc().apply(cb, root));
// TODO find out if the flush and clear are needed here or not, since delete(QueryParameters)
// is not used yet from the code it's difficult to investigate its potential purpose here
// according to https://thorben-janssen.com/5-common-hibernate-mistakes-that-cause-dozens-of-unexpected-queries/#Remove_Child_Entities_With_a_Bulk_Operation
// it seems it is necessary unless it is sure that any of removed entities wasn't fetched
// Once KEYCLOAK-19697 is done we could test our scenarios and see if we need the flush and clear
// em.flush();
// em.clear();
return em.createQuery(deleteQuery).executeUpdate();
}
}

View file

@ -0,0 +1,40 @@
/*
* Copyright 2021 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.client;
import javax.persistence.EntityManager;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.map.client.MapClientEntity;
import org.keycloak.models.map.storage.MapKeycloakTransaction;
import org.keycloak.models.map.storage.MapStorage;
public class JpaClientMapStorage implements MapStorage<MapClientEntity, ClientModel> {
public static final Integer SUPPORTED_VERSION = 1;
private final EntityManager em;
public JpaClientMapStorage(EntityManager em) {
this.em = em;
}
@Override
public MapKeycloakTransaction<MapClientEntity, ClientModel> createTransaction(KeycloakSession session) {
return new JpaClientMapKeycloakTransaction(em);
}
}

View file

@ -0,0 +1,43 @@
/*
* Copyright 2021 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.client;
import javax.persistence.EntityManager;
import org.keycloak.models.ClientModel;
import org.keycloak.models.map.client.MapClientEntity;
import org.keycloak.models.map.storage.MapStorage;
import org.keycloak.models.map.storage.MapStorageProvider;
import org.keycloak.models.map.storage.MapStorageProviderFactory.Flag;
public class JpaClientMapStorageProvider implements MapStorageProvider {
private final EntityManager em;
public JpaClientMapStorageProvider(EntityManager em) {
this.em = em;
}
@Override
public void close() {
em.close();
}
@Override
public MapStorage<MapClientEntity, ClientModel> getStorage(Class modelType, Flag... flags) {
return new JpaClientMapStorage(em);
}
}

View file

@ -0,0 +1,235 @@
/*
* Copyright 2021 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.client;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import org.hibernate.cfg.AvailableSettings;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.common.Profile;
import org.keycloak.common.util.StackUtil;
import org.keycloak.common.util.StringPropertyReplacer;
import org.keycloak.component.AmphibianProviderFactory;
import org.keycloak.connections.jpa.util.JpaUtils;
import org.keycloak.models.ClientModel;
import org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.dblock.DBLockManager;
import org.keycloak.models.dblock.DBLockProvider;
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.storage.MapStorageProvider;
import org.keycloak.models.map.storage.MapStorageProviderFactory;
import org.keycloak.models.map.storage.jpa.updater.MapJpaUpdaterProvider;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
public class JpaClientMapStorageProviderFactory implements
AmphibianProviderFactory<MapStorageProvider>,
MapStorageProviderFactory,
EnvironmentDependentProviderFactory {
final static DeepCloner CLONER = new DeepCloner.Builder()
.constructor(JpaClientEntity.class, JpaClientEntity::new)
.constructor(MapProtocolMapperEntity.class, MapProtocolMapperEntityImpl::new)
.build();
public static final String PROVIDER_ID = "jpa-client-map-storage";
private volatile EntityManagerFactory emf;
private static final Logger logger = Logger.getLogger(JpaClientMapStorageProviderFactory.class);
private Config.Scope config;
@Override
public MapStorageProvider create(KeycloakSession session) {
lazyInit(session);
return new JpaClientMapStorageProvider(emf.createEntityManager());
}
@Override
public void init(Config.Scope config) {
this.config = config;
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public String getHelpText() {
return "JPA Client Map Storage";
}
@Override
public boolean isSupported() {
return Profile.isFeatureEnabled(Profile.Feature.MAP_STORAGE);
}
@Override
public void close() {
if (emf != null) {
emf.close();
}
}
private void lazyInit(KeycloakSession session) {
if (emf == null) {
synchronized (this) {
if (emf == null) {
logger.debugf("Initializing JPA connections%s", StackUtil.getShortStackTrace());
Map<String, Object> properties = new HashMap<>();
String unitName = "keycloak-client-store";
String dataSource = config.get("dataSource");
if (dataSource != null) {
properties.put(AvailableSettings.JPA_NON_JTA_DATASOURCE, dataSource);
} else {
properties.put(AvailableSettings.JPA_JDBC_URL, config.get("url"));
properties.put(AvailableSettings.JPA_JDBC_DRIVER, config.get("driver"));
String user = config.get("user");
if (user != null) {
properties.put(AvailableSettings.JPA_JDBC_USER, user);
}
String password = config.get("password");
if (password != null) {
properties.put(AvailableSettings.JPA_JDBC_PASSWORD, password);
}
}
String schema = config.get("schema");
if (schema != null) {
properties.put(JpaUtils.HIBERNATE_DEFAULT_SCHEMA, schema);
}
properties.put("hibernate.show_sql", config.getBoolean("showSql", false));
properties.put("hibernate.format_sql", config.getBoolean("formatSql", true));
properties.put("hibernate.dialect", config.get("driverDialect"));
Integer isolation = config.getInt("isolation");
if (isolation != null) {
if (isolation < Connection.TRANSACTION_REPEATABLE_READ) {
logger.warn("Concurrent requests may not be reliable with transaction level lower than TRANSACTION_REPEATABLE_READ.");
}
properties.put(AvailableSettings.ISOLATION, String.valueOf(isolation));
} else {
// default value is TRANSACTION_READ_COMMITTED
logger.warn("Concurrent requests may not be reliable with transaction level lower than TRANSACTION_REPEATABLE_READ.");
}
Connection connection = getConnection();
try {
printOperationalInfo(connection);
customChanges(connection, schema, session, session.getProvider(MapJpaUpdaterProvider.class));
logger.trace("Creating EntityManagerFactory");
Collection<ClassLoader> classLoaders = new ArrayList<>();
if (properties.containsKey(AvailableSettings.CLASSLOADERS)) {
classLoaders.addAll((Collection<ClassLoader>) properties.get(AvailableSettings.CLASSLOADERS));
}
classLoaders.add(getClass().getClassLoader());
properties.put(AvailableSettings.CLASSLOADERS, classLoaders);
this.emf = JpaUtils.createEntityManagerFactory(session, unitName, properties, false);
logger.trace("EntityManagerFactory created");
} finally {
// Close after creating EntityManagerFactory to prevent in-mem databases from closing
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
logger.warn("Can't close connection", e);
}
}
}
}
}
}
}
private void customChanges(Connection connection, String schema, KeycloakSession session, MapJpaUpdaterProvider updater) {
KeycloakModelUtils.runJobInTransaction(session.getKeycloakSessionFactory(), (KeycloakSession lockSession) -> {
DBLockManager dbLockManager = new DBLockManager(lockSession);
DBLockProvider dbLock2 = dbLockManager.getDBLock();
dbLock2.waitForLock(DBLockProvider.Namespace.DATABASE);
try {
updater.update(ClientModel.class, connection, schema);
} finally {
dbLock2.releaseLock();
}
});
}
private void printOperationalInfo(Connection connection) {
try {
HashMap<String, String> operationalInfo = new LinkedHashMap<>();
DatabaseMetaData md = connection.getMetaData();
operationalInfo.put("databaseUrl", md.getURL());
operationalInfo.put("databaseUser", md.getUserName());
operationalInfo.put("databaseProduct", md.getDatabaseProductName() + " " + md.getDatabaseProductVersion());
operationalInfo.put("databaseDriver", md.getDriverName() + " " + md.getDriverVersion());
logger.infof("Database info: %s", operationalInfo.toString());
} catch (SQLException e) {
logger.warn("Unable to prepare operational info due database exception: " + e.getMessage());
}
}
private Connection getConnection() {
try {
String dataSourceLookup = config.get("dataSource");
if (dataSourceLookup != null) {
DataSource dataSource = (DataSource) new InitialContext().lookup(dataSourceLookup);
return dataSource.getConnection();
} else {
Class.forName(config.get("driver"));
return DriverManager.getConnection(
StringPropertyReplacer.replaceProperties(config.get("url"), System.getProperties()),
config.get("user"),
config.get("password"));
}
} catch (ClassNotFoundException | SQLException | NamingException e) {
throw new RuntimeException("Failed to connect to database", e);
}
}
}

View file

@ -0,0 +1,154 @@
/*
* Copyright 2021 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.client;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity;
import org.keycloak.models.map.storage.jpa.client.entity.JpaClientAttributeEntity;
import java.util.Arrays;
import java.util.function.BiFunction;
import java.util.stream.Stream;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Join;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientModel.SearchableFields;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
import org.keycloak.models.map.storage.CriterionNotSupportedException;
import org.keycloak.models.map.storage.ModelCriteriaBuilder;
import org.keycloak.storage.SearchableModelField;
public class JpaClientModelCriteriaBuilder implements ModelCriteriaBuilder<ClientModel, JpaClientModelCriteriaBuilder> {
private BiFunction<CriteriaBuilder, Root<JpaClientEntity>, Predicate> predicateFunc = null;
public JpaClientModelCriteriaBuilder() {
}
private JpaClientModelCriteriaBuilder(BiFunction<CriteriaBuilder, Root<JpaClientEntity>, Predicate> predicateFunc) {
this.predicateFunc = predicateFunc;
}
private void validateValue(Object[] value, SearchableModelField field, Operator op, Class<?>... expectedTypes) {
if (value == null || expectedTypes == null || value.length != expectedTypes.length) {
throw new CriterionNotSupportedException(field, op, "Invalid argument: " + Arrays.toString(value));
}
for (int i = 0; i < expectedTypes.length; i++) {
if (! expectedTypes[i].isInstance(value[i])) {
throw new CriterionNotSupportedException(field, op, "Expected types: " + Arrays.toString(expectedTypes) +
" but got: " + Arrays.toString(value));
}
}
}
private String convertToJson(Object input) {
try {
return JsonbType.MAPPER.writeValueAsString(input);
} catch (JsonProcessingException ex) {
throw new RuntimeException("Unable to write value as String.", ex);
}
}
@Override
public JpaClientModelCriteriaBuilder compare(SearchableModelField<? super ClientModel> modelField, Operator op, Object... value) {
switch (op) {
case EQ:
if (modelField.equals(SearchableFields.REALM_ID) ||
modelField.equals(SearchableFields.CLIENT_ID)) {
validateValue(value, modelField, op, String.class);
return new JpaClientModelCriteriaBuilder((cb, root) ->
cb.equal(root.get(modelField.getName()), value[0])
);
} else if (modelField.equals(SearchableFields.ENABLED)) {
validateValue(value, modelField, op, Boolean.class);
return new JpaClientModelCriteriaBuilder((cb, root) ->
cb.equal(root.get(modelField.getName()), value[0])
);
} else if (modelField.equals(SearchableFields.SCOPE_MAPPING_ROLE)) {
validateValue(value, modelField, op, String.class);
return new JpaClientModelCriteriaBuilder((cb, root) ->
cb.isTrue(cb.function("@>",
Boolean.TYPE,
cb.function("->", JsonbType.class, root.get("metadata"), cb.literal("fScopeMappings")),
cb.literal(convertToJson(value[0]))))
);
} else if (modelField.equals(SearchableFields.ALWAYS_DISPLAY_IN_CONSOLE)) {
validateValue(value, modelField, op, Boolean.class);
return new JpaClientModelCriteriaBuilder((cb, root) ->
cb.equal(
cb.function("->", JsonbType.class, root.get("metadata"), cb.literal("fAlwaysDisplayInConsole")),
cb.literal(convertToJson(value[0])))
);
} else if (modelField.equals(SearchableFields.ATTRIBUTE)) {
validateValue(value, modelField, op, String.class, String.class);
return new JpaClientModelCriteriaBuilder((cb, root) -> {
Join<JpaClientEntity, JpaClientAttributeEntity> join = root.join("attributes");
return cb.and(
cb.equal(join.get("name"), value[0]),
cb.equal(join.get("value"), value[1])
);
});
} else {
throw new CriterionNotSupportedException(modelField, op);
}
case ILIKE:
if (modelField.equals(SearchableFields.CLIENT_ID)) {
validateValue(value, modelField, op, String.class);
return new JpaClientModelCriteriaBuilder((cb, root) ->
cb.like(cb.lower(root.get(modelField.getName())), value[0].toString().toLowerCase())
);
}
default:
throw new CriterionNotSupportedException(modelField, op);
}
}
@Override
public JpaClientModelCriteriaBuilder and(JpaClientModelCriteriaBuilder... builders) {
return new JpaClientModelCriteriaBuilder(
(cb, root) -> cb.and(Stream.of(builders).map(b -> b.getPredicateFunc().apply(cb, root)).toArray(Predicate[]::new))
);
}
@Override
public JpaClientModelCriteriaBuilder or(JpaClientModelCriteriaBuilder... builders) {
return new JpaClientModelCriteriaBuilder(
(cb, root) -> cb.or(Stream.of(builders).map(b -> b.getPredicateFunc().apply(cb, root)).toArray(Predicate[]::new))
);
}
@Override
public JpaClientModelCriteriaBuilder not(JpaClientModelCriteriaBuilder builder) {
return new JpaClientModelCriteriaBuilder(
(cb, root) -> cb.not(builder.getPredicateFunc().apply(cb, root))
);
}
BiFunction<CriteriaBuilder, Root<JpaClientEntity>, Predicate> getPredicateFunc() {
return predicateFunc;
}
}

View file

@ -0,0 +1,73 @@
/*
* Copyright 2021 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.client.delegate;
import java.util.UUID;
import javax.persistence.EntityManager;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.JoinType;
import javax.persistence.criteria.Root;
import org.keycloak.models.map.client.MapClientEntity;
import org.keycloak.models.map.client.MapClientEntityFields;
import org.keycloak.models.map.common.delegate.DelegateProvider;
import org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity;
public class JpaClientDelegateProvider<T extends MapClientEntity> implements DelegateProvider {
private JpaClientEntity delegate;
private final EntityManager em;
public JpaClientDelegateProvider(JpaClientEntity deledate, EntityManager em) {
this.delegate = deledate;
this.em = em;
}
@Override
public JpaClientEntity getDelegate(boolean isRead, Object field, Object... parameters) {
if (delegate.isMetadataInitialized()) return delegate;
if (isRead) {
if (field instanceof MapClientEntityFields) {
switch ((MapClientEntityFields) field) {
case ID:
case REALM_ID:
case CLIENT_ID:
case PROTOCOL:
case ENABLED:
return delegate;
case ATTRIBUTES:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<JpaClientEntity> query = cb.createQuery(JpaClientEntity.class);
Root<JpaClientEntity> root = query.from(JpaClientEntity.class);
root.fetch("attributes", JoinType.INNER);
query.select(root).where(cb.equal(root.get("id"), UUID.fromString(delegate.getId())));
delegate = em.createQuery(query).getSingleResult();
break;
default:
delegate = em.find(JpaClientEntity.class, UUID.fromString(delegate.getId()));
}
} else throw new IllegalStateException("Not a valid client field: " + field);
} else {
delegate = em.find(JpaClientEntity.class, UUID.fromString(delegate.getId()));
}
return delegate;
}
}

View file

@ -0,0 +1,103 @@
/*
* Copyright 2021 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.client.entity;
import java.io.Serializable;
import java.util.Objects;
import java.util.UUID;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import org.hibernate.annotations.Nationalized;
@Entity
@Table(name = "client_attribute")
public class JpaClientAttributeEntity implements Serializable {
@Id
@Column
@GeneratedValue
private UUID id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name="fk_client")
private JpaClientEntity client;
@Column
private String name;
@Nationalized
@Column
private String value;
public JpaClientAttributeEntity() {
}
public JpaClientAttributeEntity(JpaClientEntity client, String name, String value) {
this.client = client;
this.name = name;
this.value = value;
}
public UUID getId() {
return id;
}
public JpaClientEntity getClient() {
return client;
}
public void setClient(JpaClientEntity client) {
this.client = client;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@Override
public int hashCode() {
return getClass().hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof JpaClientAttributeEntity)) return false;
JpaClientAttributeEntity that = (JpaClientAttributeEntity) obj;
return Objects.equals(getClient(), that.getClient()) &&
Objects.equals(getName(), that.getName()) &&
Objects.equals(getValue(), that.getValue());
}
}

View file

@ -0,0 +1,620 @@
/*
* Copyright 2021 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.client.entity;
import java.io.Serializable;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import javax.persistence.Basic;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import org.hibernate.annotations.Type;
import org.hibernate.annotations.TypeDef;
import org.hibernate.annotations.TypeDefs;
import org.keycloak.models.map.client.MapClientEntity.AbstractClientEntity;
import org.keycloak.models.map.client.MapProtocolMapperEntity;
import static org.keycloak.models.map.storage.jpa.client.JpaClientMapStorage.SUPPORTED_VERSION;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
@Entity
@Table(name = "client")
@TypeDefs({@TypeDef(name = "jsonb", typeClass = JsonbType.class)})
public class JpaClientEntity extends AbstractClientEntity implements Serializable {
@Id
@Column
private UUID id;
@Type(type = "jsonb")
@Column(columnDefinition = "jsonb")
private final JpaClientMetadata metadata;
@Column(insertable = false, updatable = false)
@Basic(fetch = FetchType.LAZY)
private Integer entityVersion;
@Column(insertable = false, updatable = false)
@Basic(fetch = FetchType.LAZY)
private String realmId;
@Column(insertable = false, updatable = false)
@Basic(fetch = FetchType.LAZY)
private String clientId;
@Column(insertable = false, updatable = false)
@Basic(fetch = FetchType.LAZY)
private String protocol;
@Column(insertable = false, updatable = false)
@Basic(fetch = FetchType.LAZY)
private Boolean enabled;
@OneToMany(mappedBy = "client", cascade = CascadeType.PERSIST, orphanRemoval = true)
private final Set<JpaClientAttributeEntity> attributes = new HashSet<>();
/**
* No-argument constructor, used by hibernate to instantiate entities.
*/
public JpaClientEntity() {
this.metadata = new JpaClientMetadata();
}
/**
* Used by hibernate when calling cb.construct from read(QueryParameters) method.
* It is used to select client without metadata(json) field.
*/
public JpaClientEntity(UUID id, Integer entityVersion, String realmId, String clientId,
String protocol, Boolean enabled) {
this.id = id;
this.entityVersion = entityVersion;
this.realmId = realmId;
this.clientId = clientId;
this.protocol = protocol;
this.enabled = enabled;
this.metadata = null;
}
public boolean isMetadataInitialized() {
return metadata != null;
}
/**
* In case of any update on entity, we want to update the entityVerion
* to current one.
*/
private void checkEntityVersionForUpdate() {
Integer ev = getEntityVersion();
if (ev != null && ev < SUPPORTED_VERSION) {
setEntityVersion(SUPPORTED_VERSION);
}
}
public Integer getEntityVersion() {
if (isMetadataInitialized()) return metadata.getEntityVersion();
return entityVersion;
}
public void setEntityVersion(Integer entityVersion) {
metadata.setEntityVersion(entityVersion);
}
@Override
public String getId() {
return id == null ? null : id.toString();
}
@Override
public void setId(String id) {
this.id = id == null ? null : UUID.fromString(id);
}
@Override
public String getRealmId() {
if (isMetadataInitialized()) return metadata.getRealmId();
return realmId;
}
@Override
public void setRealmId(String realmId) {
checkEntityVersionForUpdate();
metadata.setRealmId(realmId);
}
@Override
public String getClientId() {
if (isMetadataInitialized()) return metadata.getClientId();
return clientId;
}
@Override
public void setClientId(String clientId) {
checkEntityVersionForUpdate();
metadata.setClientId(clientId);
}
@Override
public void setEnabled(Boolean enabled) {
checkEntityVersionForUpdate();
metadata.setEnabled(enabled);
}
@Override
public Boolean isEnabled() {
if (isMetadataInitialized()) return metadata.isEnabled();
return enabled;
}
@Override
public Map<String, Boolean> getClientScopes() {
return metadata.getClientScopes();
}
@Override
public void setClientScope(String id, Boolean defaultScope) {
checkEntityVersionForUpdate();
metadata.setClientScope(id, defaultScope);
}
@Override
public void removeClientScope(String id) {
checkEntityVersionForUpdate();
metadata.removeClientScope(id);
}
@Override
public MapProtocolMapperEntity getProtocolMapper(String id) {
return metadata.getProtocolMapper(id);
}
@Override
public Map<String, MapProtocolMapperEntity> getProtocolMappers() {
return metadata.getProtocolMappers();
}
@Override
public void removeProtocolMapper(String id) {
checkEntityVersionForUpdate();
metadata.removeProtocolMapper(id);
}
@Override
public void setProtocolMapper(String id, MapProtocolMapperEntity mapping) {
checkEntityVersionForUpdate();
metadata.setProtocolMapper(id, mapping);
}
@Override
public void addRedirectUri(String redirectUri) {
checkEntityVersionForUpdate();
metadata.addRedirectUri(redirectUri);
}
@Override
public Set<String> getRedirectUris() {
return metadata.getRedirectUris();
}
@Override
public void removeRedirectUri(String redirectUri) {
checkEntityVersionForUpdate();
metadata.removeRedirectUri(redirectUri);
}
@Override
public void setRedirectUris(Set<String> redirectUris) {
checkEntityVersionForUpdate();
metadata.setRedirectUris(redirectUris);
}
@Override
public void addScopeMapping(String id) {
checkEntityVersionForUpdate();
metadata.addScopeMapping(id);
}
@Override
public void removeScopeMapping(String id) {
checkEntityVersionForUpdate();
metadata.removeScopeMapping(id);
}
@Override
public Collection<String> getScopeMappings() {
return metadata.getScopeMappings();
}
@Override
public void addWebOrigin(String webOrigin) {
checkEntityVersionForUpdate();
metadata.addWebOrigin(webOrigin);
}
@Override
public Set<String> getWebOrigins() {
return metadata.getWebOrigins();
}
@Override
public void removeWebOrigin(String webOrigin) {
checkEntityVersionForUpdate();
metadata.removeWebOrigin(webOrigin);
}
@Override
public void setWebOrigins(Set<String> webOrigins) {
checkEntityVersionForUpdate();
metadata.setWebOrigins(webOrigins);
}
@Override
public String getAuthenticationFlowBindingOverride(String binding) {
return metadata.getAuthenticationFlowBindingOverride(binding);
}
@Override
public Map<String, String> getAuthenticationFlowBindingOverrides() {
return metadata.getAuthenticationFlowBindingOverrides();
}
@Override
public void removeAuthenticationFlowBindingOverride(String binding) {
checkEntityVersionForUpdate();
metadata.removeAuthenticationFlowBindingOverride(binding);
}
@Override
public void setAuthenticationFlowBindingOverride(String binding, String flowId) {
checkEntityVersionForUpdate();
metadata.setAuthenticationFlowBindingOverride(binding, flowId);
}
@Override
public String getBaseUrl() {
return metadata.getBaseUrl();
}
@Override
public void setBaseUrl(String baseUrl) {
checkEntityVersionForUpdate();
metadata.setBaseUrl(baseUrl);
}
@Override
public String getClientAuthenticatorType() {
return metadata.getClientAuthenticatorType();
}
@Override
public void setClientAuthenticatorType(String clientAuthenticatorType) {
checkEntityVersionForUpdate();
metadata.setClientAuthenticatorType(clientAuthenticatorType);
}
@Override
public String getDescription() {
return metadata.getDescription();
}
@Override
public void setDescription(String description) {
checkEntityVersionForUpdate();
metadata.setDescription(description);
}
@Override
public String getManagementUrl() {
return metadata.getManagementUrl();
}
@Override
public void setManagementUrl(String managementUrl) {
checkEntityVersionForUpdate();
metadata.setManagementUrl(managementUrl);
}
@Override
public String getName() {
return metadata.getName();
}
@Override
public void setName(String name) {
checkEntityVersionForUpdate();
metadata.setName(name);
}
@Override
public Integer getNodeReRegistrationTimeout() {
return metadata.getNodeReRegistrationTimeout();
}
@Override
public void setNodeReRegistrationTimeout(Integer nodeReRegistrationTimeout) {
checkEntityVersionForUpdate();
metadata.setNodeReRegistrationTimeout(nodeReRegistrationTimeout);
}
@Override
public Integer getNotBefore() {
return metadata.getNotBefore();
}
@Override
public void setNotBefore(Integer notBefore) {
checkEntityVersionForUpdate();
metadata.setNotBefore(notBefore);
}
@Override
public String getProtocol() {
if (isMetadataInitialized()) return metadata.getProtocol();
return protocol;
}
@Override
public void setProtocol(String protocol) {
checkEntityVersionForUpdate();
metadata.setProtocol(protocol);
}
@Override
public String getRegistrationToken() {
return metadata.getRegistrationToken();
}
@Override
public void setRegistrationToken(String registrationToken) {
checkEntityVersionForUpdate();
metadata.setRegistrationToken(registrationToken);
}
@Override
public String getRootUrl() {
return metadata.getRootUrl();
}
@Override
public void setRootUrl(String rootUrl) {
checkEntityVersionForUpdate();
metadata.setRootUrl(rootUrl);
}
@Override
public Set<String> getScope() {
return metadata.getScope();
}
@Override
public void setScope(Set<String> scope) {
checkEntityVersionForUpdate();
metadata.setScope(scope);
}
@Override
public String getSecret() {
return metadata.getSecret();
}
@Override
public void setSecret(String secret) {
checkEntityVersionForUpdate();
metadata.setSecret(secret);
}
@Override
public Boolean isAlwaysDisplayInConsole() {
return metadata.isAlwaysDisplayInConsole();
}
@Override
public void setAlwaysDisplayInConsole(Boolean alwaysDisplayInConsole) {
checkEntityVersionForUpdate();
metadata.setAlwaysDisplayInConsole(alwaysDisplayInConsole);
}
@Override
public Boolean isBearerOnly() {
return metadata.isBearerOnly();
}
@Override
public void setBearerOnly(Boolean bearerOnly) {
checkEntityVersionForUpdate();
metadata.setBearerOnly(bearerOnly);
}
@Override
public Boolean isConsentRequired() {
return metadata.isConsentRequired();
}
@Override
public void setConsentRequired(Boolean consentRequired) {
checkEntityVersionForUpdate();
metadata.setConsentRequired(consentRequired);
}
@Override
public Boolean isDirectAccessGrantsEnabled() {
return metadata.isDirectAccessGrantsEnabled();
}
@Override
public void setDirectAccessGrantsEnabled(Boolean directAccessGrantsEnabled) {
checkEntityVersionForUpdate();
metadata.setDirectAccessGrantsEnabled(directAccessGrantsEnabled);
}
@Override
public Boolean isFrontchannelLogout() {
return metadata.isFrontchannelLogout();
}
@Override
public void setFrontchannelLogout(Boolean frontchannelLogout) {
checkEntityVersionForUpdate();
metadata.setFrontchannelLogout(frontchannelLogout);
}
@Override
public Boolean isFullScopeAllowed() {
return metadata.isFullScopeAllowed();
}
@Override
public void setFullScopeAllowed(Boolean fullScopeAllowed) {
checkEntityVersionForUpdate();
metadata.setFullScopeAllowed(fullScopeAllowed);
}
@Override
public Boolean isImplicitFlowEnabled() {
return metadata.isImplicitFlowEnabled();
}
@Override
public void setImplicitFlowEnabled(Boolean implicitFlowEnabled) {
checkEntityVersionForUpdate();
metadata.setImplicitFlowEnabled(implicitFlowEnabled);
}
@Override
public Boolean isPublicClient() {
return metadata.isPublicClient();
}
@Override
public void setPublicClient(Boolean publicClient) {
checkEntityVersionForUpdate();
metadata.setPublicClient(publicClient);
}
@Override
public Boolean isServiceAccountsEnabled() {
return metadata.isServiceAccountsEnabled();
}
@Override
public void setServiceAccountsEnabled(Boolean serviceAccountsEnabled) {
checkEntityVersionForUpdate();
metadata.setServiceAccountsEnabled(serviceAccountsEnabled);
}
@Override
public Boolean isStandardFlowEnabled() {
return metadata.isStandardFlowEnabled();
}
@Override
public void setStandardFlowEnabled(Boolean standardFlowEnabled) {
checkEntityVersionForUpdate();
metadata.setStandardFlowEnabled(standardFlowEnabled);
}
@Override
public Boolean isSurrogateAuthRequired() {
return metadata.isSurrogateAuthRequired();
}
@Override
public void setSurrogateAuthRequired(Boolean surrogateAuthRequired) {
checkEntityVersionForUpdate();
metadata.setSurrogateAuthRequired(surrogateAuthRequired);
}
@Override
public void removeAttribute(String name) {
checkEntityVersionForUpdate();
for (Iterator<JpaClientAttributeEntity> iterator = attributes.iterator(); iterator.hasNext();) {
JpaClientAttributeEntity attr = iterator.next();
if (Objects.equals(attr.getName(), name)) {
iterator.remove();
attr.setClient(null);
}
}
}
@Override
public void setAttribute(String name, List<String> values) {
checkEntityVersionForUpdate();
removeAttribute(name);
for (String value : values) {
JpaClientAttributeEntity attribute = new JpaClientAttributeEntity(this, name, value);
attributes.add(attribute);
}
}
@Override
public List<String> getAttribute(String name) {
return attributes.stream()
.filter(a -> Objects.equals(a.getName(), name))
.map(JpaClientAttributeEntity::getValue)
.collect(Collectors.toList());
}
@Override
public Map<String, List<String>> getAttributes() {
Map<String, List<String>> result = new HashMap<>();
for (JpaClientAttributeEntity attribute : attributes) {
List<String> values = result.getOrDefault(attribute.getName(), new LinkedList<>());
values.add(attribute.getValue());
result.put(attribute.getName(), values);
}
return result;
}
@Override
public void setAttributes(Map<String, List<String>> attributes) {
checkEntityVersionForUpdate();
for (Iterator<JpaClientAttributeEntity> iterator = this.attributes.iterator(); iterator.hasNext();) {
JpaClientAttributeEntity attr = iterator.next();
iterator.remove();
attr.setClient(null);
}
if (attributes != null) {
for (Map.Entry<String, List<String>> attrEntry : attributes.entrySet()) {
setAttribute(attrEntry.getKey(), attrEntry.getValue());
}
}
}
@Override
public int hashCode() {
return getClass().hashCode();
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof JpaClientEntity)) return false;
return Objects.equals(getId(), ((JpaClientEntity) obj).getId());
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright 2021 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.client.entity;
import java.io.Serializable;
import org.keycloak.models.map.client.MapClientEntityImpl;
public class JpaClientMetadata extends MapClientEntityImpl implements Serializable {
private Integer entityVersion;
public Integer getEntityVersion() {
return entityVersion;
}
public void setEntityVersion(Integer entityVersion) {
this.entityVersion = entityVersion;
}
}

View file

@ -0,0 +1,30 @@
/*
* Copyright 2021 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.hibernate.dialect;
import org.hibernate.dialect.PostgreSQL95Dialect;
import org.hibernate.dialect.function.SQLFunctionTemplate;
import org.hibernate.type.StandardBasicTypes;
import org.keycloak.models.map.storage.jpa.hibernate.jsonb.JsonbType;
public class JsonbPostgreSQL95Dialect extends PostgreSQL95Dialect {
public JsonbPostgreSQL95Dialect() {
super();
registerFunction("->", new SQLFunctionTemplate(JsonbType.INSTANCE, "?1->?2"));
registerFunction("@>", new SQLFunctionTemplate(StandardBasicTypes.BOOLEAN, "?1@>?2::jsonb"));
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright 2021 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.hibernate.jsonb;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
public class JpaClientMigration {
private static final List<Function<ObjectNode, ObjectNode>> MIGRATORS = Arrays.asList(
o -> o // no migration yet
);
public static ObjectNode migrateTreeTo(int currentVersion, int targetVersion, ObjectNode node) {
while (currentVersion < targetVersion) {
Function<ObjectNode, ObjectNode> migrator = MIGRATORS.get(currentVersion);
if (migrator != null) {
node = migrator.apply(node);
}
currentVersion++;
}
return node;
}
}

View file

@ -0,0 +1,228 @@
/*
* Copyright 2021 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.hibernate.jsonb;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.node.ObjectNode;
import java.io.IOException;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.hibernate.HibernateException;
import org.hibernate.type.AbstractSingleColumnStandardBasicType;
import org.hibernate.type.descriptor.ValueBinder;
import org.hibernate.type.descriptor.ValueExtractor;
import org.hibernate.type.descriptor.WrapperOptions;
import org.hibernate.type.descriptor.java.AbstractTypeDescriptor;
import org.hibernate.type.descriptor.java.JavaTypeDescriptor;
import org.hibernate.type.descriptor.java.MutableMutabilityPlan;
import org.hibernate.type.descriptor.sql.BasicBinder;
import org.hibernate.type.descriptor.sql.BasicExtractor;
import org.hibernate.type.descriptor.sql.SqlTypeDescriptor;
import org.keycloak.models.map.client.MapClientEntity;
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.common.Serialization.IgnoreUpdatedMixIn;
import org.keycloak.models.map.common.Serialization.IgnoredTypeMixIn;
import org.keycloak.models.map.common.UpdatableEntity;
import org.keycloak.models.map.storage.jpa.client.entity.JpaClientMetadata;
import org.keycloak.models.map.storage.jpa.client.JpaClientMapStorage;
public class JsonbType extends AbstractSingleColumnStandardBasicType<Object> {
public static final JsonbType INSTANCE = new JsonbType();
public static final ObjectMapper MAPPER = new ObjectMapper()
.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false)
.enable(SerializationFeature.INDENT_OUTPUT)
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE)
.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
.activateDefaultTyping(new LaissezFaireSubTypeValidator(), ObjectMapper.DefaultTyping.JAVA_LANG_OBJECT, JsonTypeInfo.As.PROPERTY)
.registerModule(new SimpleModule().addAbstractTypeMapping(MapProtocolMapperEntity.class, MapProtocolMapperEntityImpl.class))
.addMixIn(UpdatableEntity.class, IgnoreUpdatedMixIn.class)
.addMixIn(DeepCloner.class, IgnoredTypeMixIn.class)
.addMixIn(MapClientEntity.class, IgnoredClientFieldsMixIn.class);
abstract class IgnoredClientFieldsMixIn {
@JsonIgnore public abstract String getId();
@JsonIgnore public abstract Map<String, List<String>> getAttributes();
}
public JsonbType() {
super(JsonbSqlTypeDescriptor.INSTANCE, JsonbJavaTypeDescriptor.INSTANCE);
}
@Override
public String getName() {
return "jsonb";
}
private static class JsonbSqlTypeDescriptor implements SqlTypeDescriptor {
private static final JsonbSqlTypeDescriptor INSTANCE = new JsonbSqlTypeDescriptor();
@Override
public int getSqlType() {
return Types.OTHER;
}
@Override
public boolean canBeRemapped() {
return true;
}
@Override
public <X> ValueBinder<X> getBinder(JavaTypeDescriptor<X> javaTypeDescriptor) {
return new BasicBinder<X>(javaTypeDescriptor, this) {
@Override
protected void doBind(PreparedStatement st, X value, int index, WrapperOptions options) throws SQLException {
st.setObject(index, javaTypeDescriptor.unwrap(value, JsonNode.class, options), getSqlType());
}
@Override
protected void doBind(CallableStatement st, X value, String name, WrapperOptions options) throws SQLException {
st.setObject(name, javaTypeDescriptor.unwrap(value, JsonNode.class, options), getSqlType());
}
};
}
@Override
public <X> ValueExtractor<X> getExtractor(JavaTypeDescriptor<X> javaTypeDescriptor) {
return new BasicExtractor<X>(javaTypeDescriptor, this) {
@Override
protected X doExtract(ResultSet rs, String name, WrapperOptions options) throws SQLException {
return javaTypeDescriptor.wrap(extractJson(rs, name), options);
}
@Override
protected X doExtract(CallableStatement statement, int index, WrapperOptions options) throws SQLException {
return javaTypeDescriptor.wrap(extractJson(statement, index), options);
}
@Override
protected X doExtract(CallableStatement statement, String name, WrapperOptions options) throws SQLException {
return javaTypeDescriptor.wrap(extractJson(statement, name), options);
}
};
}
private Object extractJson(ResultSet rs, String name) throws SQLException {
return rs.getObject(name);
}
private Object extractJson(CallableStatement statement, int index) throws SQLException {
return statement.getObject(index);
}
private Object extractJson(CallableStatement statement, String name) throws SQLException {
return statement.getObject(name);
}
}
private static class JsonbJavaTypeDescriptor extends AbstractTypeDescriptor<Object> {
private static final JsonbJavaTypeDescriptor INSTANCE = new JsonbJavaTypeDescriptor();
public JsonbJavaTypeDescriptor() {
super(Object.class, new MutableMutabilityPlan<Object>() {
@Override
protected Object deepCopyNotNull(Object value) {
try {
return MAPPER.readValue(MAPPER.writerFor(value.getClass()).writeValueAsBytes(value), value.getClass());
} catch (IOException e) {
throw new HibernateException("unable to deep copy object", e);
}
}
});
}
@Override
public Object fromString(String json) {
try {
ObjectNode tree = MAPPER.readValue(json, ObjectNode.class);
JsonNode ev = tree.get("entityVersion");
if (ev == null || ! ev.isInt()) throw new IllegalArgumentException("unable to read entity version from " + json);
int entityVersion = ev.asInt();
if (entityVersion > JpaClientMapStorage.SUPPORTED_VERSION + 1) throw new IllegalArgumentException("Incompatible entity version: " + entityVersion + ", supportedVersion: " + JpaClientMapStorage.SUPPORTED_VERSION);
if (entityVersion < JpaClientMapStorage.SUPPORTED_VERSION) {
tree = JpaClientMigration.migrateTreeTo(entityVersion, JpaClientMapStorage.SUPPORTED_VERSION, tree);
}
return MAPPER.treeToValue(tree, JpaClientMetadata.class);
} catch (IOException e) {
throw new HibernateException("unable to read", e);
}
}
@Override
public <X> X unwrap(Object value, Class<X> type, WrapperOptions options) {
if (value == null) return null;
String stringValue = (value instanceof String) ? (String) value : toString(value);
try {
return (X) MAPPER.readTree(stringValue);
} catch (IOException e) {
throw new HibernateException("unable to read", e);
}
}
@Override
public <X> Object wrap(X value, WrapperOptions options) {
if (value == null) return null;
return fromString(value.toString());
}
@Override
public String toString(Object value) {
try {
return MAPPER.writeValueAsString(value);
} catch (IOException e) {
throw new HibernateException("unable to tranform value: " + value + " as String.", e);
}
}
@Override
public boolean areEqual(Object one, Object another) {
if (one == another) return true;
if (one == null || another == null) return Objects.equals(one, another);
try {
return MAPPER.readTree(toString(one)).equals(
MAPPER.readTree(toString(another)));
} catch (IOException e) {
throw new HibernateException("unable to perform areEqual", e);
}
}
}
}

View file

@ -0,0 +1,71 @@
/*
* Copyright 2021 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.updater;
import org.keycloak.provider.Provider;
import java.io.File;
import java.sql.Connection;
public interface MapJpaUpdaterProvider extends Provider {
/**
* Status of database up-to-dateness
*/
enum Status {
/**
* Database is valid and up to date
*/
VALID,
/**
* No database exists.
*/
EMPTY,
/**
* Database needs to be updated
*/
OUTDATED
}
/**
* Updates the Keycloak database for the given model type
* @param modelType Model type
* @param connection DB connection
* @param defaultSchema DB connection
*/
void update(Class modelType, Connection connection, String defaultSchema);
/**
* Checks whether Keycloak database for the given model type is up to date with the most recent changesets
* @param modelType Model type
* @param connection DB connection
* @param defaultSchema DB schema to use
* @return
*/
Status validate(Class modelType, Connection connection, String defaultSchema);
/**
* Exports the SQL update script for the given model type into the given File.
* @param modelType Model type
* @param connection DB connection
* @param defaultSchema DB schema to use
* @param file File to write to
*/
void export(Class modelType, Connection connection, String defaultSchema, File file);
}

View file

@ -0,0 +1,23 @@
/*
* Copyright 2021 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.updater;
import org.keycloak.provider.ProviderFactory;
public interface MapJpaUpdaterProviderFactory extends ProviderFactory<MapJpaUpdaterProvider> {
}

View file

@ -0,0 +1,48 @@
/*
* Copyright 2021 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.updater;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
public class MapJpaUpdaterSpi implements Spi {
public final static String NAME = "mapJpaUpdater";
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return NAME;
}
@Override
public Class<? extends Provider> getProviderClass() {
return MapJpaUpdaterProvider.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return MapJpaUpdaterProviderFactory.class;
}
}

View file

@ -0,0 +1,183 @@
/*
* Copyright 2021 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.updater.liquibase;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
import liquibase.Contexts;
import liquibase.LabelExpression;
import liquibase.Liquibase;
import liquibase.changelog.ChangeSet;
import liquibase.changelog.RanChangeSet;
import liquibase.exception.LiquibaseException;
import org.jboss.logging.Logger;
import org.keycloak.common.util.reflections.Reflections;
import org.keycloak.connections.jpa.updater.liquibase.conn.LiquibaseConnectionProvider;
import org.keycloak.connections.jpa.updater.liquibase.ThreadLocalSessionContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.map.storage.ModelEntityUtil;
import org.keycloak.models.map.storage.jpa.updater.MapJpaUpdaterProvider;
import org.keycloak.models.map.storage.jpa.updater.MapJpaUpdaterProvider.Status;
public class MapJpaLiquibaseUpdaterProvider implements MapJpaUpdaterProvider {
private static final Logger logger = Logger.getLogger(MapJpaLiquibaseUpdaterProvider.class);
private final KeycloakSession session;
public MapJpaLiquibaseUpdaterProvider(KeycloakSession session) {
this.session = session;
}
@Override
public void update(Class modelType, Connection connection, String defaultSchema) {
update(modelType, connection, null, defaultSchema);
}
@Override
public void export(Class modelType, Connection connection, String defaultSchema, File file) {
update(modelType, connection, file, defaultSchema);
}
private void update(Class modelType, Connection connection, File file, String defaultSchema) {
logger.debug("Starting database update");
// Need ThreadLocal as liquibase doesn't seem to have API to inject custom objects into tasks
ThreadLocalSessionContext.setCurrentSession(session);
Writer exportWriter = null;
try {
Liquibase liquibase = getLiquibase(modelType, connection, defaultSchema);
if (file != null) {
exportWriter = new FileWriter(file);
}
updateChangeSet(liquibase, connection);
} catch (LiquibaseException | IOException | SQLException e) {
logger.error("Error has occurred while updating the database", e);
throw new RuntimeException("Failed to update database", e);
} finally {
ThreadLocalSessionContext.removeCurrentSession();
if (exportWriter != null) {
try {
exportWriter.close();
} catch (IOException ioe) {
// ignore
}
}
}
}
protected void updateChangeSet(Liquibase liquibase, Connection connection) throws LiquibaseException, SQLException {
String changelog = liquibase.getChangeLogFile();
List<ChangeSet> changeSets = getLiquibaseUnrunChangeSets(liquibase);
if (!changeSets.isEmpty()) {
List<RanChangeSet> ranChangeSets = liquibase.getDatabase().getRanChangeSetList();
if (ranChangeSets.isEmpty()) {
logger.infov("Initializing database schema. Using changelog {0}", changelog);
} else {
if (logger.isDebugEnabled()) {
logger.debugv("Updating database from {0} to {1}. Using changelog {2}", ranChangeSets.get(ranChangeSets.size() - 1).getId(), changeSets.get(changeSets.size() - 1).getId(), changelog);
} else {
logger.infov("Updating database. Using changelog {0}", changelog);
}
}
liquibase.update((Contexts) null);
logger.debugv("Completed database update for changelog {0}", changelog);
} else {
logger.debugv("Database is up to date for changelog {0}", changelog);
}
}
@Override
public Status validate(Class modelType, Connection connection, String defaultSchema) {
logger.debug("Validating if database is updated");
ThreadLocalSessionContext.setCurrentSession(session);
try {
Liquibase liquibase = getLiquibase(modelType, connection, defaultSchema);
Status status = validateChangeSet(liquibase, liquibase.getChangeLogFile());
if (status != Status.VALID) {
return status;
}
} catch (LiquibaseException e) {
throw new RuntimeException("Failed to validate database", e);
}
return Status.VALID;
}
protected Status validateChangeSet(Liquibase liquibase, String changelog) throws LiquibaseException {
final Status result;
List<ChangeSet> changeSets = getLiquibaseUnrunChangeSets(liquibase);
if (!changeSets.isEmpty()) {
if (changeSets.size() == liquibase.getDatabaseChangeLog().getChangeSets().size()) {
result = Status.EMPTY;
} else {
logger.debugf("Validation failed. Database is not up-to-date for changelog %s", changelog);
result = Status.OUTDATED;
}
} else {
logger.debugf("Validation passed. Database is up-to-date for changelog %s", changelog);
result = Status.VALID;
}
return result;
}
@SuppressWarnings("unchecked")
private List<ChangeSet> getLiquibaseUnrunChangeSets(Liquibase liquibase) {
// TODO tracked as: https://issues.jboss.org/browse/KEYCLOAK-3730
// TODO: When https://liquibase.jira.com/browse/CORE-2919 is resolved, replace the following two lines with:
// List<ChangeSet> changeSets = liquibase.listUnrunChangeSets((Contexts) null, new LabelExpression(), false);
Method listUnrunChangeSets = Reflections.findDeclaredMethod(Liquibase.class, "listUnrunChangeSets", Contexts.class, LabelExpression.class, boolean.class);
return Reflections.invokeMethod(true, listUnrunChangeSets, List.class, liquibase, (Contexts) null, new LabelExpression(), false);
}
private Liquibase getLiquibase(Class modelType, Connection connection, String defaultSchema) throws LiquibaseException {
LiquibaseConnectionProvider liquibaseProvider = session.getProvider(LiquibaseConnectionProvider.class);
String changelog = "META-INF/jpa-" + ModelEntityUtil.getModelName(modelType) + "-changelog.xml";
if (changelog == null) {
throw new IllegalStateException("Cannot find changlelog for modelClass " + modelType.getName());
}
return liquibaseProvider.getLiquibaseForCustomUpdate(connection, defaultSchema, changelog, this.getClass().getClassLoader(), "databasechangelog");
}
@Override
public void close() {
}
public static String getTable(String table, String defaultSchema) {
return defaultSchema != null ? defaultSchema + "." + table : table;
}
}

View file

@ -0,0 +1,53 @@
/*
* Copyright 2021 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.updater.liquibase;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.map.storage.jpa.updater.MapJpaUpdaterProvider;
import org.keycloak.models.map.storage.jpa.updater.MapJpaUpdaterProviderFactory;
public class MapJpaLiquibaseUpdaterProviderFactory implements MapJpaUpdaterProviderFactory {
public static final String PROVIDER_ID = "map-liquibase-updater";
@Override
public MapJpaUpdaterProvider create(KeycloakSession session) {
return new MapJpaLiquibaseUpdaterProvider(session);
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return PROVIDER_ID;
}
}

View file

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2021 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">
<!-- format of id of changeSet: clients-${JpaClientMapStorage.SUPPORTED_VERSION} -->
<changeSet author="keycloak" id="clients-1">
<sql>
create table client (
id uuid primary key not null,
entityVersion integer generated always as ((metadata->>'entityVersion')::int) stored,
realmId varchar(36) generated always as (metadata->>'fRealmId') stored,
clientId varchar(255) generated always as (metadata->>'fClientId') stored,
protocol varchar(36) generated always as (metadata->>'fProtocol') stored,
enabled boolean generated always as ((metadata->>'fEnabled')::boolean) stored,
metadata jsonb
);
create index client_entityVersion on client(entityVersion);
create index client_realmId_clientId on client(realmId, clientId);
create index client_scopeMappings on client using gin ((metadata->'fScopeMappings') jsonb_path_ops);
create table client_attribute (
id uuid primary key not null,
fk_client uuid references client(id) on delete cascade,
name varchar(255),
value text
);
create index client_attr_fk_client on client_attribute(fk_client);
create index client_attr_name_value on client_attribute(name, (value::varchar(250)));
</sql>
</changeSet>
</databaseChangeLog>

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Copyright 2021 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">
<!-- format of id of changelog file names: jpa-clients-changelog-${JpaClientMapStorage.SUPPORTED_VERSION}.xml -->
<include file="META-INF/jpa-clients-changelog-1.xml"/>
</databaseChangeLog>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd">
<persistence-unit name="keycloak-client-store">
<class>org.keycloak.models.map.storage.jpa.client.entity.JpaClientEntity</class>
<class>org.keycloak.models.map.storage.jpa.client.entity.JpaClientAttributeEntity</class>
</persistence-unit>
</persistence>

View file

@ -0,0 +1,18 @@
#
# Copyright 2021 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.storage.jpa.client.JpaClientMapStorageProviderFactory

View file

@ -0,0 +1,18 @@
#
# Copyright 2021 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.storage.jpa.updater.liquibase.MapJpaLiquibaseUpdaterProviderFactory

View file

@ -0,0 +1,18 @@
#
# Copyright 2021 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.storage.jpa.updater.MapJpaUpdaterSpi

View file

@ -16,14 +16,11 @@
*/
package org.keycloak.models.map.common;
import org.keycloak.common.util.reflections.Reflections;
import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreType;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.JsonTypeInfo.As;
import com.fasterxml.jackson.annotation.JsonTypeInfo.Id;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
@ -35,10 +32,7 @@ import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.fasterxml.jackson.datatype.jdk8.StreamSerializer;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;
/**
@ -62,9 +56,9 @@ public class Serialization {
public static final ConcurrentHashMap<Class<?>, ObjectWriter> WRITERS = new ConcurrentHashMap<>();
@JsonIgnoreType
class IgnoredTypeMixIn {}
public class IgnoredTypeMixIn {}
abstract class IgnoreUpdatedMixIn {
public abstract class IgnoreUpdatedMixIn {
@JsonIgnore public abstract boolean isUpdated();
}

View file

@ -32,6 +32,7 @@
<modules>
<module>jpa</module>
<module>map-jpa</module>
<module>infinispan</module>
<module>map</module>
<module>build-processor</module>

View file

@ -1283,6 +1283,11 @@
<artifactId>keycloak-model-map</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-map-jpa</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-infinispan</artifactId>

View file

@ -50,7 +50,7 @@ public interface ClientModel extends ClientScopeModel, RoleContainerModel, Prot
public static final SearchableModelField<ClientModel> ALWAYS_DISPLAY_IN_CONSOLE = new SearchableModelField<>("alwaysDisplayInConsole", Boolean.class);
/**
* Search for attribute value. The parameters is a pair {@code (attribute_name, values...)} where {@code attribute_name}
* Search for attribute value. The parameters is a pair {@code (attribute_name, value)} where {@code attribute_name}
* is always checked for equality, and the value is checked per the operator.
*/
public static final SearchableModelField<ClientModel> ATTRIBUTE = new SearchableModelField<>("attribute", String[].class);

View file

@ -69,7 +69,15 @@
"provider": "${keycloak.client.provider:jpa}",
"map": {
"storage": {
"provider": "${keycloak.client.map.storage.provider:concurrenthashmap}"
"provider": "${keycloak.client.map.storage.provider:concurrenthashmap}",
"jpa-client-map-storage": {
"url": "${keycloak.client.map.storage.connectionsJpa.url:}",
"driver": "org.postgresql.Driver",
"driverDialect": "org.keycloak.models.map.jpa.hibernate.dialect.JsonbPostgreSQL95Dialect",
"user": "${keycloak.client.map.storage.connectionsJpa.user:}",
"password": "${keycloak.client.map.storage.connectionsJpa.password:}",
"showSql": "${keycloak.client.map.storage.connectionsJpa,showSql:false}"
}
}
}
},

View file

@ -293,7 +293,7 @@
<systemProperty><key>keycloak.group.provider</key><value>map</value></systemProperty>
<systemProperty><key>keycloak.role.provider</key><value>map</value></systemProperty>
<systemProperty><key>keycloak.user.provider</key><value>map</value></systemProperty>
<systemProperty><key>keycloak.serverInfo.provider</key><value>map</value></systemProperty>
<systemProperty><key>keycloak.deploymentState.provider</key><value>map</value></systemProperty>
<systemProperty><key>keycloak.authSession.provider</key><value>map</value></systemProperty>
<systemProperty><key>keycloak.userSession.provider</key><value>map</value></systemProperty>
<systemProperty><key>keycloak.loginFailure.provider</key><value>map</value></systemProperty>

View file

@ -26,23 +26,56 @@
},
"realm": {
"provider": "${keycloak.realm.provider:jpa}"
"provider": "${keycloak.realm.provider:jpa}",
"map": {
"storage": {
"provider": "${keycloak.realm.map.storage.provider:concurrenthashmap}"
}
}
},
"client": {
"provider": "${keycloak.client.provider:jpa}"
"provider": "${keycloak.client.provider:jpa}",
"map": {
"storage": {
"provider": "${keycloak.client.map.storage.provider:concurrenthashmap}",
"jpa-client-map-storage": {
"url": "${keycloak.client.map.storage.connectionsJpa.url:}",
"driver": "org.postgresql.Driver",
"driverDialect": "org.keycloak.models.map.jpa.hibernate.dialect.JsonbPostgreSQL95Dialect",
"user": "${keycloak.client.map.storage.connectionsJpa.user:}",
"password": "${keycloak.client.map.storage.connectionsJpa.password:}",
"showSql": "${keycloak.client.map.storage.connectionsJpa,showSql:false}"
}
}
}
},
"clientScope": {
"provider": "${keycloak.clientScope.provider:jpa}"
"provider": "${keycloak.clientScope.provider:jpa}",
"map": {
"storage": {
"provider": "${keycloak.clientScope.map.storage.provider:concurrenthashmap}"
}
}
},
"group": {
"provider": "${keycloak.group.provider:jpa}"
"provider": "${keycloak.group.provider:jpa}",
"map": {
"storage": {
"provider": "${keycloak.group.map.storage.provider:concurrenthashmap}"
}
}
},
"role": {
"provider": "${keycloak.role.provider:jpa}"
"provider": "${keycloak.role.provider:jpa}",
"map": {
"storage": {
"provider": "${keycloak.role.map.storage.provider:concurrenthashmap}"
}
}
},
"authenticationSessions": {
@ -70,7 +103,12 @@
},
"user": {
"provider": "${keycloak.user.provider:jpa}"
"provider": "${keycloak.user.provider:jpa}",
"map": {
"storage": {
"provider": "${keycloak.user.map.storage.provider:concurrenthashmap}"
}
}
},
"userFederatedStorage": {