jta transaction abstraction

This commit is contained in:
Bill Burke 2016-08-08 12:32:36 -04:00
parent f14f303dfe
commit 83306963e8
18 changed files with 224 additions and 63 deletions

View file

@ -75,5 +75,13 @@
"default": { "default": {
"cacheContainer" : "java:comp/env/infinispan/Keycloak" "cacheContainer" : "java:comp/env/infinispan/Keycloak"
} }
},
"jta-lookup": {
"provider": "${keycloak.jta.lookup.provider:jboss}",
"jboss" : {
"enabled": true
}
} }
} }

View file

@ -0,0 +1,13 @@
Example User Storage Provider with EJB and JPA
===================================================
This is an example of the User Storage SPI implemented using EJB and JPA. To deploy this provider you must have Keycloak
running in standalone or standalone-ha mode. Then type the follow maven command:
mvn clean install wildfly:deploy
Login and go to the User Federation tab and you should now see your deployed provider in the add-provider list box.
Add the provider, save it, then any new user you create will be stored and in the custom store you implemented. You
can modify the example and hot deploy it using the above maven command again.
This example uses the built in in-memory datasource that comes with keycloak: ExampleDS.

View file

@ -23,7 +23,7 @@
<version>2.1.0-SNAPSHOT</version> <version>2.1.0-SNAPSHOT</version>
</parent> </parent>
<name>Properties Authentication Provider Example</name> <name>User Storage JPA Provider Exapmle</name>
<description/> <description/>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
@ -75,14 +75,7 @@
<target>1.8</target> <target>1.8</target>
</configuration> </configuration>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.jboss.as.plugins</groupId>
<artifactId>jboss-as-maven-plugin</artifactId>
<configuration>
<skip>false</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.wildfly.plugins</groupId> <groupId>org.wildfly.plugins</groupId>
<artifactId>wildfly-maven-plugin</artifactId> <artifactId>wildfly-maven-plugin</artifactId>
<configuration> <configuration>

View file

@ -49,35 +49,11 @@ public class EjbExampleUserStorageProviderFactory implements UserStorageProvider
@Override @Override
public String getId() { public String getId() {
return "example-user-storage"; return "example-user-storage-jpa";
} }
@Override @Override
public void init(Config.Scope config) { public String getHelpText() {
return "JPA Example User Storage Provider";
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
static List<ProviderConfigProperty> OPTIONS = new LinkedList<>();
static {
ProviderConfigProperty prop = new ProviderConfigProperty("propertyFile", "Property File", "file that contains name value pairs", ProviderConfigProperty.STRING_TYPE, null);
OPTIONS.add(prop);
prop = new ProviderConfigProperty("federatedStorage", "User Federated Storage", "use federated storage?", ProviderConfigProperty.BOOLEAN_TYPE, null);
OPTIONS.add(prop);
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return OPTIONS;
}
@Override
public void close() {
} }
} }

View file

@ -36,6 +36,11 @@
</properties> </properties>
<dependencies> <dependencies>
<dependency>
<groupId>org.jboss.spec.javax.transaction</groupId>
<artifactId>jboss-transaction-api_1.2_spec</artifactId>
<scope>provided</scope>
</dependency>
<dependency> <dependency>
<groupId>org.jboss.resteasy</groupId> <groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-jaxrs</artifactId> <artifactId>resteasy-jaxrs</artifactId>

View file

@ -28,7 +28,7 @@ public class UserStorageProviderSpi implements Spi {
@Override @Override
public boolean isInternal() { public boolean isInternal() {
return true; return false;
} }
@Override @Override

View file

@ -28,15 +28,16 @@ import org.keycloak.provider.ProviderManager;
import org.keycloak.provider.ProviderManagerDeployer; import org.keycloak.provider.ProviderManagerDeployer;
import org.keycloak.provider.ProviderManagerRegistry; import org.keycloak.provider.ProviderManagerRegistry;
import org.keycloak.provider.Spi; import org.keycloak.provider.Spi;
import org.keycloak.services.ServicesLogger; import org.keycloak.transaction.JtaRegistration;
import org.keycloak.transaction.JtaTransactionManagerLookup;
import org.keycloak.transaction.JtaTransactionWrapper;
import java.util.Collections; import javax.transaction.TransactionManager;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
@ -48,7 +49,7 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
private Map<Class<? extends Provider>, String> provider = new HashMap<>(); private Map<Class<? extends Provider>, String> provider = new HashMap<>();
private volatile Map<Class<? extends Provider>, Map<String, ProviderFactory>> factoriesMap = new HashMap<>(); private volatile Map<Class<? extends Provider>, Map<String, ProviderFactory>> factoriesMap = new HashMap<>();
protected CopyOnWriteArrayList<ProviderEventListener> listeners = new CopyOnWriteArrayList<>(); protected CopyOnWriteArrayList<ProviderEventListener> listeners = new CopyOnWriteArrayList<>();
private JtaRegistration jta; private TransactionManager tm;
// TODO: Likely should be changed to int and use Time.currentTime() to be compatible with all our "time" reps // TODO: Likely should be changed to int and use Time.currentTime() to be compatible with all our "time" reps
protected long serverStartupTimestamp; protected long serverStartupTimestamp;
@ -72,7 +73,6 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
public void init() { public void init() {
serverStartupTimestamp = System.currentTimeMillis(); serverStartupTimestamp = System.currentTimeMillis();
jta = new JtaRegistration();
ProviderManager pm = new ProviderManager(getClass().getClassLoader(), Config.scope().getArray("providers")); ProviderManager pm = new ProviderManager(getClass().getClassLoader(), Config.scope().getArray("providers"));
spis.addAll(pm.loadSpis()); spis.addAll(pm.loadSpis());
@ -96,6 +96,9 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
} }
// make the session factory ready for hot deployment // make the session factory ready for hot deployment
ProviderManagerRegistry.SINGLETON.setDeployer(this); ProviderManagerRegistry.SINGLETON.setDeployer(this);
JtaTransactionManagerLookup lookup = (JtaTransactionManagerLookup)getProviderFactory(JtaTransactionManagerLookup.class);
if (lookup != null) tm = lookup.getTransactionManager();
} }
protected Map<Class<? extends Provider>, Map<String, ProviderFactory>> getFactoriesCopy() { protected Map<Class<? extends Provider>, Map<String, ProviderFactory>> getFactoriesCopy() {
Map<Class<? extends Provider>, Map<String, ProviderFactory>> copy = new HashMap<>(); Map<Class<? extends Provider>, Map<String, ProviderFactory>> copy = new HashMap<>();
@ -190,15 +193,18 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
} }
Config.Scope scope = Config.scope(spi.getName(), provider); Config.Scope scope = Config.scope(spi.getName(), provider);
factory.init(scope); if (scope.getBoolean("enabled", true)) {
if (spi.isInternal() && !isInternal(factory)) { factory.init(scope);
logger.spiMayChange(factory.getId(), factory.getClass().getName(), spi.getName());
if (spi.isInternal() && !isInternal(factory)) {
logger.spiMayChange(factory.getId(), factory.getClass().getName(), spi.getName());
}
factories.put(factory.getId(), factory);
logger.debugv("Loaded SPI {0} (provider = {1})", spi.getName(), provider);
} }
factories.put(factory.getId(), factory);
logger.debugv("Loaded SPI {0} (provider = {1})", spi.getName(), provider);
} else { } else {
for (ProviderFactory factory : pm.load(spi)) { for (ProviderFactory factory : pm.load(spi)) {
Config.Scope scope = Config.scope(spi.getName(), factory.getId()); Config.Scope scope = Config.scope(spi.getName(), factory.getId());
@ -276,7 +282,9 @@ public class DefaultKeycloakSessionFactory implements KeycloakSessionFactory, Pr
public KeycloakSession create() { public KeycloakSession create() {
KeycloakSession session = new DefaultKeycloakSession(this); KeycloakSession session = new DefaultKeycloakSession(this);
jta.begin(session); if (tm != null) {
session.getTransactionManager().enlist(new JtaTransactionWrapper(tm));
}
return session; return session;
} }

View file

@ -14,25 +14,31 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.services; package org.keycloak.transaction;
import org.keycloak.models.KeycloakSession; import org.keycloak.Config;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.services.ServicesLogger;
import javax.naming.InitialContext; import javax.naming.InitialContext;
import javax.naming.NamingException; import javax.naming.NamingException;
import javax.transaction.Transaction;
import javax.transaction.TransactionManager; import javax.transaction.TransactionManager;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class JtaRegistration { public class JBossJtaTransactionManagerLookup implements JtaTransactionManagerLookup {
private static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER; private static final ServicesLogger logger = ServicesLogger.ROOT_LOGGER;
private TransactionManager tm; private TransactionManager tm;
public JtaRegistration() { @Override
public TransactionManager getTransactionManager() {
return tm;
}
@Override
public void init(Config.Scope config) {
try { try {
InitialContext ctx = new InitialContext(); InitialContext ctx = new InitialContext();
tm = (TransactionManager)ctx.lookup("java:jboss/TransactionManager"); tm = (TransactionManager)ctx.lookup("java:jboss/TransactionManager");
@ -45,9 +51,13 @@ public class JtaRegistration {
} }
public void begin(KeycloakSession session) { @Override
if (tm == null) return; public void postInit(KeycloakSessionFactory factory) {
session.getTransactionManager().enlist(new JtaTransactionWrapper(tm)); }
@Override
public String getId() {
return "jboss";
} }
} }

View file

@ -0,0 +1,40 @@
/*
* Copyright 2016 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.transaction;
import org.keycloak.models.KeycloakSession;
import org.keycloak.services.ServicesLogger;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.transaction.TransactionManager;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class JtaRegistration {
public void begin(KeycloakSession session) {
TransactionManager tm = session.getProvider(JtaTransactionManagerLookup.class).getTransactionManager();
if (tm == null) return;
session.getTransactionManager().enlist(new JtaTransactionWrapper(tm));
}
}

View file

@ -0,0 +1,43 @@
/*
* Copyright 2016 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.transaction;
import org.keycloak.models.KeycloakSession;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import javax.transaction.TransactionManager;
/**
* JTA TransactionManager lookup
*
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public interface JtaTransactionManagerLookup extends Provider, ProviderFactory<JtaTransactionManagerLookup> {
@Override
default void close() {
}
@Override
default JtaTransactionManagerLookup create(KeycloakSession session) {
return this;
}
TransactionManager getTransactionManager();
}

View file

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.services; package org.keycloak.transaction;
import org.keycloak.models.KeycloakTransaction; import org.keycloak.models.KeycloakTransaction;

View file

@ -0,0 +1,49 @@
/*
* Copyright 2016 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.transaction;
import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
*/
public class TransactionManagerLookupSpi implements Spi {
@Override
public boolean isInternal() {
return true;
}
@Override
public String getName() {
return "jta-lookup";
}
@Override
public Class<? extends Provider> getProviderClass() {
return JtaTransactionManagerLookup.class;
}
@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return JtaTransactionManagerLookup.class;
}
}

View file

@ -14,7 +14,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.keycloak.services; package org.keycloak.transaction;
import org.keycloak.models.KeycloakTransaction; import org.keycloak.models.KeycloakTransaction;

View file

@ -18,4 +18,5 @@
org.keycloak.exportimport.ClientDescriptionConverterSpi org.keycloak.exportimport.ClientDescriptionConverterSpi
org.keycloak.wellknown.WellKnownSpi org.keycloak.wellknown.WellKnownSpi
org.keycloak.services.clientregistration.ClientRegistrationSpi org.keycloak.services.clientregistration.ClientRegistrationSpi
org.keycloak.transaction.TransactionManagerLookupSpi

View file

@ -0,0 +1 @@
org.keycloak.transaction.JBossJtaTransactionManagerLookup

View file

@ -130,5 +130,13 @@
"hostname-verification-policy": "${keycloak.truststore.policy:WILDCARD}", "hostname-verification-policy": "${keycloak.truststore.policy:WILDCARD}",
"disabled": "${keycloak.truststore.disabled:false}" "disabled": "${keycloak.truststore.disabled:false}"
} }
},
"jta-lookup": {
"provider": "${keycloak.jta.lookup.provider:jboss}",
"jboss" : {
"enabled": true
}
} }
} }

View file

@ -97,5 +97,13 @@
}, },
"scripting": { "scripting": {
},
"jta-lookup": {
"provider": "${keycloak.jta.lookup.provider:jboss}",
"jboss" : {
"enabled": true
}
} }
} }

View file

@ -61,8 +61,6 @@ public class KeycloakProviderDependencyProcessor implements DeploymentUnitProces
if (!isKeycloakProviderDeployment(deploymentUnit)) return; if (!isKeycloakProviderDeployment(deploymentUnit)) return;
logger.info("FOUND KEYCLOAK PROVIDER DEPLOYMENT!!!!: " + deploymentUnit.getName());
final ModuleSpecification moduleSpecification = deploymentUnit.getAttachment(Attachments.MODULE_SPECIFICATION); final ModuleSpecification moduleSpecification = deploymentUnit.getAttachment(Attachments.MODULE_SPECIFICATION);
final ModuleLoader moduleLoader = Module.getBootModuleLoader(); final ModuleLoader moduleLoader = Module.getBootModuleLoader();
moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, KEYCLOAK_COMMON, false, false, false, false)); moduleSpecification.addSystemDependency(new ModuleDependency(moduleLoader, KEYCLOAK_COMMON, false, false, false, false));