diff --git a/dependencies/server-all/pom.xml b/dependencies/server-all/pom.xml
index 088d48f1d1..cfc82ee8e1 100755
--- a/dependencies/server-all/pom.xml
+++ b/dependencies/server-all/pom.xml
@@ -44,6 +44,10 @@
org.keycloak
keycloak-model-map
+
+ org.keycloak
+ keycloak-model-map-jpa
+
org.keycloak
keycloak-model-infinispan
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java
index 7c27121a12..1f63166b67 100755
--- a/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/DefaultJpaConnectionProviderFactory.java
@@ -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) {
diff --git a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProviderFactory.java b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProviderFactory.java
index 26eac9a646..6385232e17 100755
--- a/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProviderFactory.java
+++ b/model/jpa/src/main/java/org/keycloak/connections/jpa/updater/liquibase/LiquibaseJpaUpdaterProviderFactory.java
@@ -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;
}
}
diff --git a/model/map-jpa/pom.xml b/model/map-jpa/pom.xml
new file mode 100644
index 0000000000..a03ffbb98d
--- /dev/null
+++ b/model/map-jpa/pom.xml
@@ -0,0 +1,69 @@
+
+
+
+
+
+ keycloak-model-pom
+ org.keycloak
+ 16.0.0-SNAPSHOT
+
+ 4.0.0
+
+ keycloak-model-map-jpa
+ Keycloak Model Map JPA
+
+
+
+ org.keycloak
+ keycloak-model-map
+
+
+ jakarta.persistence
+ jakarta.persistence-api
+
+
+ org.hibernate
+ hibernate-core
+
+
+ org.keycloak
+ keycloak-model-jpa
+
+
+
+
+
+ org.hibernate.orm.tooling
+ hibernate-enhance-maven-plugin
+ ${hibernate.core.version}
+
+
+
+ true
+ true
+
+
+ enhance
+
+
+
+
+
+
+
diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapKeycloakTransaction.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapKeycloakTransaction.java
new file mode 100644
index 0000000000..07a338d9ba
--- /dev/null
+++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapKeycloakTransaction.java
@@ -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 {
+
+ 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 read(QueryParameters queryParameters) {
+ JpaClientModelCriteriaBuilder mcb = queryParameters.getModelCriteriaBuilder()
+ .flashToModelCriteriaBuilder(new JpaClientModelCriteriaBuilder());
+
+ CriteriaBuilder cb = em.getCriteriaBuilder();
+ CriteriaQuery query = cb.createQuery(JpaClientEntity.class);
+ Root 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 orderByList = new LinkedList<>();
+ for (QueryParameters.OrderBy 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 queryParameters) {
+ JpaClientModelCriteriaBuilder mcb = queryParameters.getModelCriteriaBuilder()
+ .flashToModelCriteriaBuilder(new JpaClientModelCriteriaBuilder());
+
+ CriteriaBuilder cb = em.getCriteriaBuilder();
+
+ CriteriaQuery countQuery = cb.createQuery(Long.class);
+ Root 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 queryParameters) {
+ JpaClientModelCriteriaBuilder mcb = queryParameters.getModelCriteriaBuilder()
+ .flashToModelCriteriaBuilder(new JpaClientModelCriteriaBuilder());
+
+ CriteriaBuilder cb = em.getCriteriaBuilder();
+
+ CriteriaDelete deleteQuery = cb.createCriteriaDelete(JpaClientEntity.class);
+
+ Root 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();
+ }
+
+}
diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapStorage.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapStorage.java
new file mode 100644
index 0000000000..ecc1ba7373
--- /dev/null
+++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapStorage.java
@@ -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 {
+
+ public static final Integer SUPPORTED_VERSION = 1;
+ private final EntityManager em;
+
+ public JpaClientMapStorage(EntityManager em) {
+ this.em = em;
+ }
+
+ @Override
+ public MapKeycloakTransaction createTransaction(KeycloakSession session) {
+ return new JpaClientMapKeycloakTransaction(em);
+ }
+
+}
diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapStorageProvider.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapStorageProvider.java
new file mode 100644
index 0000000000..014ed9c940
--- /dev/null
+++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapStorageProvider.java
@@ -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 getStorage(Class modelType, Flag... flags) {
+ return new JpaClientMapStorage(em);
+ }
+}
diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapStorageProviderFactory.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapStorageProviderFactory.java
new file mode 100644
index 0000000000..269a0b988f
--- /dev/null
+++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientMapStorageProviderFactory.java
@@ -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,
+ 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 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 classLoaders = new ArrayList<>();
+ if (properties.containsKey(AvailableSettings.CLASSLOADERS)) {
+ classLoaders.addAll((Collection) 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 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);
+ }
+ }
+}
diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientModelCriteriaBuilder.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientModelCriteriaBuilder.java
new file mode 100644
index 0000000000..0a364a5fb8
--- /dev/null
+++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/JpaClientModelCriteriaBuilder.java
@@ -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 {
+
+ private BiFunction, Predicate> predicateFunc = null;
+
+ public JpaClientModelCriteriaBuilder() {
+ }
+
+ private JpaClientModelCriteriaBuilder(BiFunction, 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 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, Predicate> getPredicateFunc() {
+ return predicateFunc;
+ }
+
+}
diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/delegate/JpaClientDelegateProvider.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/delegate/JpaClientDelegateProvider.java
new file mode 100644
index 0000000000..aa1f656d54
--- /dev/null
+++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/delegate/JpaClientDelegateProvider.java
@@ -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 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 query = cb.createQuery(JpaClientEntity.class);
+ Root 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;
+ }
+
+}
diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/entity/JpaClientAttributeEntity.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/entity/JpaClientAttributeEntity.java
new file mode 100644
index 0000000000..715fcbcfa7
--- /dev/null
+++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/entity/JpaClientAttributeEntity.java
@@ -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());
+ }
+}
diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/entity/JpaClientEntity.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/entity/JpaClientEntity.java
new file mode 100644
index 0000000000..40cb8cccc4
--- /dev/null
+++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/entity/JpaClientEntity.java
@@ -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 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 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 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 getRedirectUris() {
+ return metadata.getRedirectUris();
+ }
+
+ @Override
+ public void removeRedirectUri(String redirectUri) {
+ checkEntityVersionForUpdate();
+ metadata.removeRedirectUri(redirectUri);
+ }
+
+ @Override
+ public void setRedirectUris(Set 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 getScopeMappings() {
+ return metadata.getScopeMappings();
+ }
+
+ @Override
+ public void addWebOrigin(String webOrigin) {
+ checkEntityVersionForUpdate();
+ metadata.addWebOrigin(webOrigin);
+ }
+
+ @Override
+ public Set getWebOrigins() {
+ return metadata.getWebOrigins();
+ }
+
+ @Override
+ public void removeWebOrigin(String webOrigin) {
+ checkEntityVersionForUpdate();
+ metadata.removeWebOrigin(webOrigin);
+ }
+
+ @Override
+ public void setWebOrigins(Set webOrigins) {
+ checkEntityVersionForUpdate();
+ metadata.setWebOrigins(webOrigins);
+ }
+
+ @Override
+ public String getAuthenticationFlowBindingOverride(String binding) {
+ return metadata.getAuthenticationFlowBindingOverride(binding);
+ }
+
+ @Override
+ public Map 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 getScope() {
+ return metadata.getScope();
+ }
+
+ @Override
+ public void setScope(Set 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 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 values) {
+ checkEntityVersionForUpdate();
+ removeAttribute(name);
+ for (String value : values) {
+ JpaClientAttributeEntity attribute = new JpaClientAttributeEntity(this, name, value);
+ attributes.add(attribute);
+ }
+ }
+
+ @Override
+ public List getAttribute(String name) {
+ return attributes.stream()
+ .filter(a -> Objects.equals(a.getName(), name))
+ .map(JpaClientAttributeEntity::getValue)
+ .collect(Collectors.toList());
+ }
+
+ @Override
+ public Map> getAttributes() {
+ Map> result = new HashMap<>();
+ for (JpaClientAttributeEntity attribute : attributes) {
+ List values = result.getOrDefault(attribute.getName(), new LinkedList<>());
+ values.add(attribute.getValue());
+ result.put(attribute.getName(), values);
+ }
+ return result;
+ }
+
+ @Override
+ public void setAttributes(Map> attributes) {
+ checkEntityVersionForUpdate();
+ for (Iterator iterator = this.attributes.iterator(); iterator.hasNext();) {
+ JpaClientAttributeEntity attr = iterator.next();
+ iterator.remove();
+ attr.setClient(null);
+ }
+ if (attributes != null) {
+ for (Map.Entry> 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());
+ }
+}
diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/entity/JpaClientMetadata.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/entity/JpaClientMetadata.java
new file mode 100644
index 0000000000..e4c6492c59
--- /dev/null
+++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/client/entity/JpaClientMetadata.java
@@ -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;
+ }
+
+}
diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/dialect/JsonbPostgreSQL95Dialect.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/dialect/JsonbPostgreSQL95Dialect.java
new file mode 100644
index 0000000000..ea85c215fa
--- /dev/null
+++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/dialect/JsonbPostgreSQL95Dialect.java
@@ -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"));
+ }
+}
diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/JpaClientMigration.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/JpaClientMigration.java
new file mode 100644
index 0000000000..d9bcdc3226
--- /dev/null
+++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/JpaClientMigration.java
@@ -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> MIGRATORS = Arrays.asList(
+ o -> o // no migration yet
+ );
+
+ public static ObjectNode migrateTreeTo(int currentVersion, int targetVersion, ObjectNode node) {
+ while (currentVersion < targetVersion) {
+ Function migrator = MIGRATORS.get(currentVersion);
+ if (migrator != null) {
+ node = migrator.apply(node);
+ }
+ currentVersion++;
+ }
+ return node;
+ }
+
+}
diff --git a/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/JsonbType.java b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/JsonbType.java
new file mode 100644
index 0000000000..872b17eb67
--- /dev/null
+++ b/model/map-jpa/src/main/java/org/keycloak/models/map/storage/jpa/hibernate/jsonb/JsonbType.java
@@ -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
+
+ org.keycloak
+ keycloak-model-map-jpa
+ ${project.version}
+
org.keycloak
keycloak-model-infinispan
diff --git a/server-spi/src/main/java/org/keycloak/models/ClientModel.java b/server-spi/src/main/java/org/keycloak/models/ClientModel.java
index 772e43bcef..ed39e764d4 100755
--- a/server-spi/src/main/java/org/keycloak/models/ClientModel.java
+++ b/server-spi/src/main/java/org/keycloak/models/ClientModel.java
@@ -50,7 +50,7 @@ public interface ClientModel extends ClientScopeModel, RoleContainerModel, Prot
public static final SearchableModelField 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 ATTRIBUTE = new SearchableModelField<>("attribute", String[].class);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
index 9f4ec5d403..a0cbef575e 100755
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/META-INF/keycloak-server.json
@@ -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}"
+ }
}
}
},
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/log4j.properties b/testsuite/integration-arquillian/tests/base/src/test/resources/log4j.properties
index e200365114..5e90666afd 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/log4j.properties
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/log4j.properties
@@ -86,7 +86,7 @@ log4j.logger.org.keycloak.services.clientregistration.policy=debug
## Enable SQL debugging
# Enable logs the SQL statements
-#log4j.logger.org.hibernate.SQL=debug
+#log4j.logger.org.hibernate.SQL=debug
# Enable logs the JDBC parameters passed to a query
#log4j.logger.org.hibernate.type=trace
diff --git a/testsuite/utils/pom.xml b/testsuite/utils/pom.xml
index 8d0094d981..9812c62475 100755
--- a/testsuite/utils/pom.xml
+++ b/testsuite/utils/pom.xml
@@ -293,7 +293,7 @@
keycloak.group.providermap
keycloak.role.providermap
keycloak.user.providermap
- keycloak.serverInfo.providermap
+ keycloak.deploymentState.providermap
keycloak.authSession.providermap
keycloak.userSession.providermap
keycloak.loginFailure.providermap
diff --git a/testsuite/utils/src/main/resources/META-INF/keycloak-server.json b/testsuite/utils/src/main/resources/META-INF/keycloak-server.json
index dcd3201bf4..b3bed71642 100755
--- a/testsuite/utils/src/main/resources/META-INF/keycloak-server.json
+++ b/testsuite/utils/src/main/resources/META-INF/keycloak-server.json
@@ -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": {