commit
b652487ef0
5 changed files with 209 additions and 4 deletions
|
@ -21,4 +21,5 @@
|
||||||
.. link:topics/user-storage/augmenting.adoc[Augmenting External Storage]
|
.. link:topics/user-storage/augmenting.adoc[Augmenting External Storage]
|
||||||
.. link:topics/user-storage/import.adoc[Import Implementation Strategy]
|
.. link:topics/user-storage/import.adoc[Import Implementation Strategy]
|
||||||
.. link:topics/user-storage/cache.adoc[User Caches]
|
.. 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]
|
||||||
|
|
|
@ -175,12 +175,29 @@ public class MyEventListenerProviderFactory implements EventListenerProviderFact
|
||||||
|
|
||||||
=== Registering provider implementations
|
=== 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.
|
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
|
==== Register a provider using Modules
|
||||||
|
|
||||||
|
WARNING: We don't recommend this approach.
|
||||||
|
|
||||||
To register a provider using Modules first create a module.
|
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`.
|
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:
|
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:
|
||||||
<spi name="userCache">
|
<spi name="userCache">
|
||||||
<provider name="infinispan" enabled="false"/>
|
<provider name="infinispan" enabled="false"/>
|
||||||
</spi>
|
</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
|
=== Available SPIs
|
||||||
|
|
||||||
|
|
|
@ -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
|
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.
|
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
|
==== ImportedUserValidation Interface
|
||||||
|
|
||||||
If you remember earlier in this chapter, we discussed how querying for a user worked. Local storage is queried first,
|
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`
|
we can't have null values for a property in the property value. We also have to modify the `CredentialInputValidator`
|
||||||
methods to reflect this.
|
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
|
.PropertyFileUserStorageProvider
|
||||||
[source,java]
|
[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