2016-12-03 00:55:47 +00:00
=== Import Implementation Strategy
2017-03-29 16:54:50 +00:00
When implementing a user storage provider, there's another strategy you can take. Instead of using user federated storage,
you can create a user locally in the {{book.project.name}} built-in user database and copy attributes from your external
store into this local copy. There are many advantages to this approach.
2016-12-03 00:55:47 +00:00
2017-03-29 16:54:50 +00:00
* {{book.project.name}} basically becomes a persistence user cache for your external store. Once the user is imported
2016-12-03 00:55:47 +00:00
you'll no longer hit the external store thus taking load off of it.
* If you are moving to {{book.project.name}} as your official user store and deprecating the old external store, you
2017-03-29 16:54:50 +00:00
can slowly migrate applications to use {{book.project.name}}. When all applications have been migrated, unlink the
2016-12-03 00:55:47 +00:00
imported user, and retire the old legacy external store.
There are some obvious disadvantages though to using an import strategy:
2017-03-29 16:54:50 +00:00
* Looking up a user for the first time will require multiple updates to {{book.project.name}} database. This can
be a big performance loss under load and put a lot of strain on the {{book.project.name}} database. The user federated
2016-12-03 00:55:47 +00:00
storage approach will only store extra data as needed and may never be used depending on the capabilities of your external store.
2017-03-29 16:54:50 +00:00
* With the import approach, you have to keep local keycloak storage and external storage in sync. The User Storage SPI
2016-12-03 00:55:47 +00:00
has capability interfaces that you can implement to support synchronization, but this can quickly become painful and messy.
2017-03-29 16:54:50 +00:00
To implement the import strategy you simply check to see first if the user has been imported locally. If so return the
local user, if not create the user locally and import data from the external store. You can also proxy the local user
2016-12-03 00:55:47 +00:00
so that most changes are automatically synchronized.
2017-03-29 16:54:50 +00:00
This will be a bit contrived, but we can extend our `PropertyFileUserStorageProvider` to take this approach. We
2016-12-03 00:55:47 +00:00
begin first by modifying the `createAdapter()` method.
.PropertyFileUserStorageProvider
2017-03-28 17:46:10 +00:00
[source,java]
2016-12-03 00:55:47 +00:00
----
protected UserModel createAdapter(RealmModel realm, String username) {
UserModel local = session.userLocalStorage().getUserByUsername(username, realm);
if (local == null) {
local = session.userLocalStorage().addUser(realm, username);
local.setFederationLink(model.getId());
}
return new UserModelDelegate(local) {
@Override
public void setUsername(String username) {
String pw = (String)properties.remove(username);
if (pw != null) {
properties.put(username, pw);
save();
}
super.setUsername(username);
}
};
}
----
In this method we call the `KeycloakSession.userLocalStorage()` method to obtain a reference to local {{book.project.name}}
2017-03-29 16:54:50 +00:00
user storage. We see if the user is stored locally, if not, we add it locally. Also note that we call
`UserModel.setFederationLink()` and pass in the ID of the `ComponentModel` of our provider. This sets a link between
2016-12-03 00:55:47 +00:00
the provider and the imported user.
NOTE: When a user storage provider is removed, any user imported by it will also be removed. This is one of the
purposes of calling `UserModel.setFederationLink()`.
Another thing to note is that if a local user is linked, your storage provider will still be delegated to for methods
2017-03-29 16:54:50 +00:00
that it implements from the `CredentialInputValidator` and `CredentialInputUpdater` interfaces. Returning `false`
2016-12-03 00:55:47 +00:00
from a validation or update will just result in {{book.project.name}} seeing if it can validate or update using
local storage.
Also notice that we are proxying the local user using the `org.keycloak.models.utils.UserModelDelegate' class.
2017-03-29 16:54:50 +00:00
This class is an implementation of `UserModel`. Every method just delegates to the `UserModel` it was instantiated with.
2016-12-03 00:55:47 +00:00
We override the `setUsername()` method of this delegate class to synchronize automatically with the property file.
For your providers, you can use this to _intercept_ other methods on the local `UserModel` to perform synchronization
2017-03-29 16:54:50 +00:00
with your external store. For example, get methods could make sure that the local store is in sync. Set methods
keep the external store in sync with the local one.
2016-12-03 00:55:47 +00:00
2016-12-03 14:41:18 +00:00
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.
2016-12-03 00:55:47 +00:00
==== ImportedUserValidation Interface
If you remember earlier in this chapter, we discussed how querying for a user worked. Local storage is queried first,
if the user is found there, then the query ends. This is a problem for our above implementation as we want
to proxy the local `UserModel` so that we can keep usernames in sync. The User Storage SPI has a callback for whenever
a linked local user is loaded from the local database.
[source,java]
----
package org.keycloak.storage.user;
public interface ImportedUserValidation {
/**
* If this method returns null, then the user in local storage will be removed
*
* @param realm
* @param user
* @return null if user no longer valid
*/
UserModel validate(RealmModel realm, UserModel user);
}
----
Whenever a linked local user is loaded, if the user storage provider class implements this interface, then the
2017-03-29 16:54:50 +00:00
`validate()` method is called. Here you can proxy the local user passed in as a parameter and return it. That
new `UserModel` will be used. You can also optionally do a check to see if the user still exists in the external store.
If `validate()` returns `null`, then the local user will be removed from the database.
2016-12-03 00:55:47 +00:00
==== ImportSynchronization Interface
2017-03-29 16:54:50 +00:00
With the import strategy you can see that it is possible for the local user copy to get out of sync with
external storage. For example, maybe a user has been removed from the external store. The User Storage SPI has
an additional interface you can implement to deal with this, `org.keycloak.storage.user.ImportSynchronization`:
2016-12-03 00:55:47 +00:00
[source,java]
----
package org.keycloak.storage.user;
public interface ImportSynchronization {
SynchronizationResult sync(KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model);
SynchronizationResult syncSince(Date lastSync, KeycloakSessionFactory sessionFactory, String realmId, UserStorageProviderModel model);
}
----
2017-03-29 16:54:50 +00:00
This interface is implemented by the provider factory. Once this interface is implemented by the provider factory, the administration console management page for the provider shows additional options. You can manually force a synchronization by clicking a button. This invokes the `ImportSynchronization.sync()` method. Also, additional configuration options are displayed that allow you to automatically schedule a synchronization. Automatic synchronizations invoke the `syncSince()` method.