[keycloak-user] UserStorageProvider for an external database

Dmitry Telegin dt at acutus.pro
Mon Dec 10 23:40:33 EST 2018


Hello Bart,

Regarding your initial question on JNDI datasource vs. manual configuration - from the architect's standpoint, each of the approaches has its pros and cons (as always).

Your approach has an obvious benefit of automatic config propagation to the nodes, thanks to Keycloak components model backed by Infinispan and shared database. But if (or when) it comes to advanced configuration options like SSL, connection pooling, tracing etc., you'll end up reimplementing more and more datasource handling logic that has already been implemented in the corresponding Wildfly subsystem.

Having datasources configured in Wildfly (and accessed via JNDI) will get you rid of reinventing the wheel. However, in this case you need to take care of the proper config propagation to the cluster members. This is done automatically in the Keycloak/Wildfly domain mode, but for standalone and its variants you will have to use jboss-cli (maybe in combination with configuration management tools like Ansible etc.)

Many software products allow for both variants, either setting up connection properties manually or using JNDI datasource configured at the application server level. I suggest that if you strive for a production-ready solution, you should consider implementing JNDI too.

Cheers,
Dmitry Telegin
CTO, Acutus s.r.o.
Keycloak Consulting and Training

Pod lipami street 339/52, 130 00 Prague 3, Czech Republic
+42 (022) 888-30-71
E-mail: info at acutus.pro

On Sun, 2018-12-09 at 13:26 +0100, Bart Lievens wrote:
> Hello, 
> 
> I solved the problem not by adding a datasource to WildFly but by adding the configuration parameters to the UserStorageProviderFactory 
> and creating the EntityManager inside the UserStorageProviderFactory and then passing it on when creating a UserStorageProvider.
> 
> My UserStorageProviderFactory looks something like (with 4.6.0.Final & 4.7.0.Final) :
> public class ExternalUserStorageProviderFactory implements UserStorageProviderFactory<ExternalUserStorageProvider> {
>     private static final transient Logger logger = LoggerFactory.getLogger(ExternalUserStorageProviderFactory.class);
>     private static final String CONF_NAME_JDBC_URL = "jdbcUrl";
>     private static final String CONF_NAME_JDBC_USER = "user";
>     private static final String CONF_NAME_JDBC_PASSWORD = "password";
> 
>     protected static final List<ProviderConfigProperty> configMetadata;
>     private Map<String, EntityManager> entityManagers = new HashMap<>();
> 
>     static {
>         ProviderConfigurationBuilder builder = ProviderConfigurationBuilder.create();
>         builder.property().name(CONF_NAME_JDBC_URL).type(ProviderConfigProperty.STRING_TYPE).label("Jdbc Url")
>                .defaultValue("jdbc:postgresql://host:port/database")
>                .helpText("Postgres JDBC Connection URL to external user db")
>                .add();
>         builder.property().name(CONF_NAME_JDBC_USER).type(ProviderConfigProperty.STRING_TYPE).label("Jdbc User")
>                .helpText("JDBC Connection User")
>                .add();
>         builder.property().name(CONF_NAME_JDBC_PASSWORD).type(ProviderConfigProperty.PASSWORD).label("Jdbc Password")
>                .helpText("JDBC Connection Password")
>                .add();
>         configMetadata = builder.build();
>     }
> 
>     @Override
>     public List<ProviderConfigProperty> getConfigProperties() {
>         return configMetadata;
>     }
> 
>     @Override
>     public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel componentModel) throws ComponentValidationException {
>         if (componentModel.getConfig().getFirst(CONF_NAME_JDBC_URL) == null
>             || componentModel.getConfig().getFirst(CONF_NAME_JDBC_USER) == null
>             || componentModel.getConfig().getFirst(CONF_NAME_JDBC_PASSWORD) == null) {
>             throw new ComponentValidationException("The jdbc Url, User and Password are requirec");
>         }
>         try {
>             createEntityManager(componentModel);
>         } catch (Exception e) {
>             logger.warn("Invalid configuration {}", e.getCause() == null ? e.getMessage() : e.getCause().getMessage());
>             throw new ComponentValidationException("Could not setup jdbc connection : " + (e.getCause() == null ? e.getMessage() : e.getCause().getMessage()));
>         }
>     }
> 
>     @Override
>     public ExternalUserStorageProvider create(KeycloakSession session, ComponentModel model) {
>         try {
>             if (entityManagers.get(model.getId()) == null) {
>                 createEntityManager(model);
>             }
>             return new ExternalUserStorageProvider(entityManagers.get(model.getId()), model, session);
>         } catch (Exception e) {
>             throw new RuntimeException(e);
>         }
>     }
> 
>     @Override
>     public String getId() {
>         return "external-user-db";
>     }
> 
>     @Override
>     public String getHelpText() {
>         return "External User Database Storage Provider";
>     }
> 
>     private void createEntityManager(ComponentModel model) {
>         logger.info("creating entityManager for {}", model.getName());
>         Properties properties = getProperties(model);
>         EntityManagerFactory entityManagerFactory = Persistence.createEntityManagerFactory("external-user-storage", properties);
>         entityManagers.put(model.getId(), entityManagerFactory.createEntityManager());
>     }
> 
>     private Properties getProperties(ComponentModel model) {
>         Properties properties = new Properties();
>         // Add class loader needed to find persistence.xml
>         properties.put(AvailableSettings.CLASSLOADERS, Arrays.asList(this.getClass().getClassLoader()));
>         // Set JPA properties
>         properties.put(AvailableSettings.JPA_PERSISTENCE_PROVIDER, HibernatePersistenceProvider.class.getName());
>         properties.put(AvailableSettings.JPA_TRANSACTION_TYPE, PersistenceUnitTransactionType.JTA.name());
>         // postgresql jdbc connection config 
>         properties.put(AvailableSettings.JPA_JDBC_DRIVER, "org.postgresql.Driver");
>         properties.put(AvailableSettings.JPA_JDBC_URL, EnvUtil.replace(model.getConfig().getFirst(CONF_NAME_JDBC_URL)));
>         properties.put(AvailableSettings.JPA_JDBC_USER, EnvUtil.replace(model.getConfig().getFirst(CONF_NAME_JDBC_USER)));
>         properties.put(AvailableSettings.JPA_JDBC_PASSWORD, EnvUtil.replace(model.getConfig().getFirst(CONF_NAME_JDBC_PASSWORD)));
>         // hibernate
>         properties.put(AvailableSettings.DIALECT, org.hibernate.dialect.PostgreSQL95Dialect.class.getName());
>         properties.put(AvailableSettings.SHOW_SQL, Boolean.FALSE);
>         // set JTA properties
>         properties.put(AvailableSettings.JTA_PLATFORM, JBossAppServerJtaPlatform.class.getName());
>         return properties;
>     }
> }
> 
> _______________________________________________
> keycloak-user mailing list
> keycloak-user at lists.jboss.org
> https://lists.jboss.org/mailman/listinfo/keycloak-user


More information about the keycloak-user mailing list