diff --git a/SUMMARY.adoc b/SUMMARY.adoc index 3d89bd4c0e..44948340d9 100755 --- a/SUMMARY.adoc +++ b/SUMMARY.adoc @@ -21,4 +21,5 @@ .. link:topics/user-storage/augmenting.adoc[Augmenting External Storage] .. link:topics/user-storage/import.adoc[Import Implementation Strategy] .. link:topics/user-storage/cache.adoc[User Caches] - .. link:topics/user-storage/javaee.adoc[Leveraging Java EE ] + .. link:topics/user-storage/javaee.adoc[Leveraging Java EE] + .. link:topics/user-storage/rest.adoc[REST Management API] diff --git a/topics/providers.adoc b/topics/providers.adoc index 619f67bf7c..0b028c3c39 100755 --- a/topics/providers.adoc +++ b/topics/providers.adoc @@ -175,12 +175,29 @@ public class MyEventListenerProviderFactory implements EventListenerProviderFact === Registering provider implementations -Keycloak can load provider implementations from JBoss Modules or directly from the file-system. +There are two ways to register provider implementations. The easiest way is to just throw your provider jar within +the Keycloak `deploy/` directory. Keycloak supports hot deployment as well in this scenario. This is also the best +solution. + +The alternative is not really recommended, but exists for legacy purposes as the Keycloak deployer didn't exist in +previous versions of the project. Keycloak can load provider implementations from JBoss Modules or directly from the file-system. Using Modules is recommended as you can control exactly what classes are available to your provider. -Any providers loaded from the file-system uses a classloader with the Keycloak classloader as its parent. +Any providers loaded from the file-system uses a classloader with the Keycloak classloader as its parent. + +==== Using the Keycloak Deployer + +If you throw your provider jar within the Keycloak `deploy/` directory, your provider will automatically be deployed. +Hot deployment works too. Additionally, your provider jar works similarly to other components deployed in a JBoss/Wildfly +environment in that they can use facilities like the `jboss-deployment-structure.xml` file. This file allows you to +set up dependencies on other components and load third-party jars and modules. + +Provider jars can also be contained within other deployable units like EARs and WARs. Deploying with a EAR actually makes +it really easy to use third party jars as you can just put these libraries in the EAR's `lib/` directory. ==== Register a provider using Modules +WARNING: We don't recommend this approach. + To register a provider using Modules first create a module. To do this you can either use the jboss-cli script or manually create a folder inside `KEYCLOAK_HOME/modules` and add your jar and a `module.xml`. For example to add the event listener sysout example provider using the `jboss-cli` script execute: @@ -275,7 +292,83 @@ For example to disable the Infinispan user cache provider add: ----- +---- + +=== Leveraging Java EE + +The can be packaged within any Java EE component so long as you set up the `META-INF/services` +file correctly to point to your providers. For example, if your provider needs to use third party libraries, you +can package up your provider within an ear and store these third pary libraries in the ear's `lib/` directory. +Also note that provider jars can make use of the `jboss-deployment-structure.xml` file that EJBs, WARS, and EARs +can use in a JBoss/Wildfly environment. See the JBoss/Wildfly documentation for more details on this file. It +allows you to pull in external dependencies among other fine grain actions. + +`ProviderFactory` implementations are required to be plain java objects. But, we also currently support +implementing provider classes as Stateful EJBs. TThis is how you would do it: + +[source,java] +---- +@Stateful +@Local(EjbExampleUserStorageProvider.class) +public class EjbExampleUserStorageProvider implements UserStorageProvider, + UserLookupProvider, + UserRegistrationProvider, + UserQueryProvider, + CredentialInputUpdater, + CredentialInputValidator, + OnUserCache +{ + @PersistenceContext + protected EntityManager em; + + protected ComponentModel model; + protected KeycloakSession session; + + public void setModel(ComponentModel model) { + this.model = model; + } + + public void setSession(KeycloakSession session) { + this.session = session; + } + + + @Remove + @Override + public void close() { + } +... +} +---- + +You have to define the `@Local` annotation and specify your provider class there. If you don't do this, EJB will +not proxy the provider instance correctly and your provider won't work. + +You must put the `@Remove` annotation on the `close()` method of your provider. If you don't, the stateful bean +will never be cleaned up and you may eventually see error messages. + +Implementations of `ProviderFactory` are required to be plain java objects. Your factory class would +perform a JNDI lookup of the Stateful EJB in its create() method. + +[source,java] +---- +public class EjbExampleUserStorageProviderFactory + implements UserStorageProviderFactory { + + @Override + public EjbExampleUserStorageProvider create(KeycloakSession session, ComponentModel model) { + try { + InitialContext ctx = new InitialContext(); + EjbExampleUserStorageProvider provider = (EjbExampleUserStorageProvider)ctx.lookup( + "java:global/user-storage-jpa-example/" + EjbExampleUserStorageProvider.class.getSimpleName()); + provider.setModel(model); + provider.setSession(session); + return provider; + } catch (Exception e) { + throw new RuntimeException(e); + } + } +---- === Available SPIs diff --git a/topics/user-storage/import.adoc b/topics/user-storage/import.adoc index a1b409c5ff..8f2a2ad131 100644 --- a/topics/user-storage/import.adoc +++ b/topics/user-storage/import.adoc @@ -69,6 +69,11 @@ For your providers, you can use this to _intercept_ other methods on the local ` with your extern store. For example, get methods could make sure that the local store is in sync. Set methods keep external store in sync with local one. +NOTE: If your provider is implementing the `UserRegistrationProvider` interface, your `removeUser()` method does not + need to remove the user from local storage. The runtime will automatically perform this operation. Also + note that `removeUser()` will be invoked before it is removed from local storage. + + ==== ImportedUserValidation Interface If you remember earlier in this chapter, we discussed how querying for a user worked. Local storage is queried first, diff --git a/topics/user-storage/registration-query.adoc b/topics/user-storage/registration-query.adoc index 65af4b36ee..b4eb9ebfd7 100644 --- a/topics/user-storage/registration-query.adoc +++ b/topics/user-storage/registration-query.adoc @@ -56,6 +56,10 @@ Notice that when adding a user we set the password value of the property map to we can't have null values for a property in the property value. We also have to modify the `CredentialInputValidator` methods to reflect this. +`addUser()` will be called if the provider implements the `UserRegistrationProvider` interface. If your provider has +a configuration switch to turn of adding a user, returning `null` from this method will skip the provider and call +the next one. + .PropertyFileUserStorageProvider [source,java] ---- diff --git a/topics/user-storage/rest.adoc b/topics/user-storage/rest.adoc new file mode 100644 index 0000000000..49668ecdae --- /dev/null +++ b/topics/user-storage/rest.adoc @@ -0,0 +1,102 @@ + +=== REST Management API + +You can create, remove, and update your user storage provider deployments through the admin REST api. The User Storage SPI +is built on top of a generic component interface so you will be using that generic API to manage your providers. + +The REST Component API lives under your realm admin resource. + +---- +/admin/realms/{realm-name}/components +---- + +We will only show this REST API interaction with the Java client. Hopefully you can extract how do do this from +curl from this api. + +[source,java] +---- +public interface ComponentsResource { + @GET + @Produces(MediaType.APPLICATION_JSON) + public List query(); + + @GET + @Produces(MediaType.APPLICATION_JSON) + public List query(@QueryParam("parent") String parent); + + @GET + @Produces(MediaType.APPLICATION_JSON) + public List query(@QueryParam("parent") String parent, @QueryParam("type") String type); + + @GET + @Produces(MediaType.APPLICATION_JSON) + public List query(@QueryParam("parent") String parent, + @QueryParam("type") String type, + @QueryParam("name") String name); + + @POST + @Consumes(MediaType.APPLICATION_JSON) + Response add(ComponentRepresentation rep); + + @Path("{id}") + ComponentResource component(@PathParam("id") String id); +} + +public interface ComponentResource { + @GET + public ComponentRepresentation toRepresentation(); + + @PUT + @Consumes(MediaType.APPLICATION_JSON) + public void update(ComponentRepresentation rep); + + @DELETE + public void remove(); +} + +---- + +To create a user storage provider, you must specify the provider id, a provider type of the string `org.keycloak.storage.UserStorageProvider`, +as well as the configuration. + +[source,java] +---- +import org.keycloak.admin.client.Keycloak; +import org.keycloak.representations.idm.RealmRepresentation; +... + +Keycloak keycloak = Keycloak.getInstance( + "http://localhost:8080/auth", + "master", + "admin", + "password", + "admin-cli"); +RealmResource realmResource = keycloak.realm("master"); +RealmRepresentation realm = realmResource.toRepresentation(); + +ComponentRepresentation component = new ComponentRepresentation(); +component.setName("home"); +component.setProviderId("readonly-property-file"); +component.setProviderType("org.keycloak.storage.UserStorageProvider"); +component.setParentId(realm.getId()); +component.setConfig(new MultivaluedHashMap()); +component.getConfig().putSingle("path", "~/users.properties"); + +realmResource.components().add(component); + +// retrieve a component + +List components = realmResource.components().query(realm.getId(), + "org.keycloak.storage.UserStorageProvider", + "home"); +component = components.get(0); + +// Update a component + +component.getConfig().putSingle("path", "~/my-users.properties"); +realmResource.components().component(component.getId()).update(component); + +// Remove a component + +realmREsource.components().component(component.getId()).remove(); +----