KEYCLOAK-18717 KEYCLOAK-18716 KEYCLOAK-18715 KEYCLOAK-18713 KEYCLOAK-18712 KEYCLOAK-18711 JPA clients no-downtime store
This commit is contained in:
parent
848b170a96
commit
c6312e3308
36 changed files with 2420 additions and 21 deletions
4
dependencies/server-all/pom.xml
vendored
4
dependencies/server-all/pom.xml
vendored
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
69
model/map-jpa/pom.xml
Normal 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>
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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"));
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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> {
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -363,7 +363,7 @@ public class MapClientProvider implements ClientProvider {
|
|||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
|
||||
<modules>
|
||||
<module>jpa</module>
|
||||
<module>map-jpa</module>
|
||||
<module>infinispan</module>
|
||||
<module>map</module>
|
||||
<module>build-processor</module>
|
||||
|
|
5
pom.xml
5
pom.xml
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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": {
|
||||
|
|
Loading…
Reference in a new issue