commit
b652487ef0
5 changed files with 209 additions and 4 deletions
|
@ -22,3 +22,4 @@
|
|||
.. 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/rest.adoc[REST Management API]
|
||||
|
|
|
@ -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.
|
||||
|
||||
==== 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:
|
||||
|
@ -277,6 +294,82 @@ For example to disable the Infinispan user cache provider add:
|
|||
</spi>
|
||||
----
|
||||
|
||||
=== 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<EjbExampleUserStorageProvider> {
|
||||
|
||||
@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
|
||||
|
||||
Here's a list of the most important available SPIs and a brief description. For more details on each SPI refer to individual sections.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]
|
||||
----
|
||||
|
|
102
topics/user-storage/rest.adoc
Normal file
102
topics/user-storage/rest.adoc
Normal file
|
@ -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<ComponentRepresentation> query();
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public List<ComponentRepresentation> query(@QueryParam("parent") String parent);
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public List<ComponentRepresentation> query(@QueryParam("parent") String parent, @QueryParam("type") String type);
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public List<ComponentRepresentation> 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<ComponentRepresentation> 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();
|
||||
----
|
Loading…
Reference in a new issue