keycloak-aplcache
Changes
examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesFederationFactory.java 15(+14 -1)
examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesFederationProvider.java 29(+24 -5)
examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/ClasspathPropertiesFederationProvider.java 22(+22 -0)
examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/FilePropertiesFederationFactory.java 8(+6 -2)
examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/FilePropertiesFederationProvider.java 19(+19 -0)
examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/ReadonlyUserModelProxy.java 2(+2 -0)
Details
diff --git a/docbook/reference/en/en-US/master.xml b/docbook/reference/en/en-US/master.xml
index b8785df..10c0684 100755
--- a/docbook/reference/en/en-US/master.xml
+++ b/docbook/reference/en/en-US/master.xml
@@ -27,8 +27,7 @@
<!ENTITY Timeouts SYSTEM "modules/timeouts.xml">
<!ENTITY Audit SYSTEM "modules/audit.xml">
<!ENTITY AdminApi SYSTEM "modules/admin-rest-api.xml">
- <!ENTITY Authentication SYSTEM "modules/authentication-spi.xml">
- <!ENTITY Ldap SYSTEM "modules/ldap.xml">
+ <!ENTITY UserFederation SYSTEM "modules/user-federation.xml">
<!ENTITY ExportImport SYSTEM "modules/export-import.xml">
<!ENTITY ServerCache SYSTEM "modules/cache.xml">
]>
@@ -115,8 +114,7 @@ This one is short
&Timeouts;
&AdminApi;
&Audit;
- &Authentication;
- &Ldap;
+ &UserFederation;
&ExportImport;
&ServerCache;
&Migration;
diff --git a/docbook/reference/en/en-US/modules/MigrationFromOlderVersions.xml b/docbook/reference/en/en-US/modules/MigrationFromOlderVersions.xml
index 1a12fa0..1a8a574 100755
--- a/docbook/reference/en/en-US/modules/MigrationFromOlderVersions.xml
+++ b/docbook/reference/en/en-US/modules/MigrationFromOlderVersions.xml
@@ -4,6 +4,14 @@
<title>Migrating from 1.0 Beta 1 to Beta 4</title>
<itemizedlist>
<listitem>
+ LDAP/AD configuration is changed. It is no longer under the "Settings" page. It is now under
+ Users->Federation. Add Provider will show you an "ldap" option.
+ </listitem>
+ <listitem>
+ Authentication SPI has been removed and rewritten. The new SPI is UserFederationProvider and is
+ more flexible.
+ </listitem>
+ <listitem>
DB Schema has changed again.
</listitem>
<listitem>
diff --git a/docbook/reference/en/en-US/modules/user-federation.xml b/docbook/reference/en/en-US/modules/user-federation.xml
new file mode 100755
index 0000000..6360c7e
--- /dev/null
+++ b/docbook/reference/en/en-US/modules/user-federation.xml
@@ -0,0 +1,140 @@
+<chapter id="user_federation">
+ <title>User Federation SPI and LDAP/AD Integration</title>
+ <para>
+ Keycloak can federate external user databases. Out of the box we have support for LDAP and Active Directory.
+ Before you dive into this, you should understand how Keycloak does federation.
+ </para>
+ <para>
+ Keycloak performs federation a bit differently than other products/projects. The vision of Keycloak is that it
+ is an out of the box solution that should provide a core set of feature irregardless of the backend user storage you
+ want to use. Because of this requirement/vision, Keycloak has a set data model that all of its services use.
+ Most of the time when you want to federate an external user store, much of the metadata that would be needed to
+ provide this complete feature set does not exist in that external store. For example your LDAP server may only
+ provide password validation, but not support TOTP or user role mappings. The Keycloak User Federation SPI was
+ written to support these completely variable locations
+ </para>
+ <para>
+ The way user federation works is that Keycloak will import your federated users on demand to its local storage. How
+ much metadata that is imported depends on the underlying federation plugin and how that plugin is configured. Some
+ federation plugins may only import the username into Keycloak storage, others might import everything from name,
+ address, and phone number, to user role mappings. Some plugins might want to import credentials directly into
+ Keycloak storage and let Keycloak handle credential validation. Others might want to handle credential validation
+ themselves. Thegoal of the Federation SPI is to support all of these scenarios.
+ </para>
+ <section>
+ <title>LDAP and Active Directory Plugin</title>
+ <para>
+ Keycloak comes with a built-in LDAP/AD plugin. Currently it is set up only to import username, email, first and last name.
+ It supports password validation via LDAP/AD protocols and different user metadata synchronization modes. To configure
+ a federated LDAP store go to the admin console. Click on the <literal>Users</literal> menu option to get you
+ to the user management page. Then click on the <literal>Federation</literal> submenu option. When
+ you get to this page there is an "Add Provider" select box. You should see "ldap" within this list. Selecting
+ "ldap" will bring you to the ldap configuration page.
+ </para>
+ <section>
+ <title>Edit Mode</title>
+ <para>
+ Edit mode defines various synchronization options with your LDAP store depending on what privileges
+ you have.
+ <variablelist>
+ <varlistentry>
+ <term>READONLY</term>
+ <listitem>
+ <para>
+ Username, email, first and last name will be unchangable. Keycloak will show an error
+ anytime anybody tries to update these fields. Also, password updates will not be supported.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>WRITABLE</term>
+ <listitem>
+ <para>
+ Username, email, first and last name, and passwords can all be updated and will
+ be synchronized automatically with your LDAP store.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>UNSYNCED</term>
+ <listitem>
+ <para>
+ Any changes to username, email, first and last name, and passwords will be stored
+ in Keycloak local storage. It is up to you to figure out how to synchronize back to
+ LDAP.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </para>
+ </section>
+ <section>
+ <title>Other config options</title>
+ <para>
+ <variablelist>
+ <varlistentry>
+ <term>Display Name</term>
+ <listitem>
+ <para>
+ Name used when this provider is referenced in the admin consle
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>Priority</term>
+ <listitem>
+ <para>
+ The priority of this provider when looking up users or for adding registrations.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>Sync Registrations</term>
+ <listitem>
+ <para>
+ If a new user is added through a registration page or admin console, should the user
+ be eligible to be synchronized to this provider.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term>Other options</term>
+ <listitem>
+ <para>
+ The rest of the configuration options should be self explanatory.
+ </para>
+ </listitem>
+ </varlistentry>
+ </variablelist>
+ </para>
+ </section>
+ </section>
+ <section>
+ <title>Writing your own User Federation Provider</title>
+ <para>
+ The keycloak examples directory contains an example of a simple User Federation Provider backed by
+ a simple properties file. See <literal>examples/providers/federation-provider</literal>. Most of how
+ to create a federation provider is explain directly within the example code, but some information is here too.
+ </para>
+ <para>
+ Writing a User Federation Provider starts by implementing the <literal>UserFederationProvider</literal>
+ and <literal>UserFederationProviderFactory</literal> interfaces. Please see the Javadoc and example
+ for complete details on on how to do this. Some important methods of note:
+ getUserByUsername() and getUserByEmail() require that you query your federated storage and if the user exists
+ create and import the user into Keycloak storage. How much metadata you import is fully up to you. This
+ import is done by invoking methods on the object returned <literal>KeycloakSession.userStorage()</literal>
+ to add and import user information. The proxy() method will be called whenever Keycloak has found an imported
+ UserModel. This allows the federation provider to proxy the UserModel which is useful if you want to support
+ external storage updates on demand.
+ </para>
+ <para>
+ After your code is written you must package up all your classes within a JAR file. This jar file must
+ contain a file called <literal>org.keycloak.models.UserFederationProviderFactory</literal>
+ within the <literal>META-INF/services</literal> directory of the JAR. This file is a list
+ of fully qualified classnames of all implementations of <literal>UserFederationProviderFactory</literal>.
+ This is how Keycloak discovers which providers have been deployment. Place the JAR in the
+ keycloak WAR deployment in the <literal>WEB-INF/lib</literal> directory.
+ </para>
+ </section>
+
+</chapter>
\ No newline at end of file
diff --git a/examples/providers/federation-provider/README.md b/examples/providers/federation-provider/README.md
new file mode 100755
index 0000000..bdb4a1b
--- /dev/null
+++ b/examples/providers/federation-provider/README.md
@@ -0,0 +1,16 @@
+Example User Federation Provider
+===================================================
+
+This is an example of user federation backed by a simple properties file. This properties file only contains username/password
+key pairs. To deploy, build this directory then take the jar and copy it to the WEB-INF/lib of the keycloak server's
+WAR file.
+
+The ClasspathPropertiesFederationProvider is an example of a readonly provider. If you go to the Users/Federation
+ page of the admin console you will see this provider listed under "classpath-properties. To configure this provider you
+specify a classpath to a properties file in the "path" field of the admin page for this plugin. This example includes
+a "test-users.properties" within the JAR that you can use as the variable.
+
+The FilePropertiesFederationProvider is an exxample of a writable provider. It synchronizes changes made to
+username and password with the properties file. If you go to the Users/Federation page of the admin console you will
+see this provider listed under "file-properties". To configure this provider you specify a fully qualified file path to
+a properties file in the "path" field of the admin page for this plugin.
\ No newline at end of file
diff --git a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesFederationFactory.java b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesFederationFactory.java
index 057ce50..04845fb 100755
--- a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesFederationFactory.java
+++ b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesFederationFactory.java
@@ -27,10 +27,12 @@ public abstract class BasePropertiesFederationFactory implements UserFederationP
@Override
public UserFederationProvider getInstance(KeycloakSession session, UserFederationProviderModel model) {
+ // first get the path to our properties file from the stored configuration of this provider instance.
String path = model.getConfig().get("path");
if (path == null) {
throw new IllegalStateException("Path attribute not configured for provider");
}
+ // see if we already loaded the config file
Properties props = files.get(path);
if (props != null) return createProvider(session, model, props);
@@ -43,6 +45,7 @@ public abstract class BasePropertiesFederationFactory implements UserFederationP
} catch (IOException e) {
throw new RuntimeException(e);
}
+ // remember the properties file for next time
files.put(path, props);
return createProvider(session, model, props);
}
@@ -51,7 +54,12 @@ public abstract class BasePropertiesFederationFactory implements UserFederationP
protected abstract BasePropertiesFederationProvider createProvider(KeycloakSession session, UserFederationProviderModel model, Properties props);
-
+ /**
+ * List the configuration options to render and display in the admin console's generic management page for this
+ * plugin
+ *
+ * @return
+ */
@Override
public Set<String> getConfigurationOptions() {
return configOptions;
@@ -62,6 +70,11 @@ public abstract class BasePropertiesFederationFactory implements UserFederationP
return null;
}
+ /**
+ * You can import additional plugin configuration from keycloak-server.json here.
+ *
+ * @param config
+ */
@Override
public void init(Config.Scope config) {
diff --git a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesFederationProvider.java b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesFederationProvider.java
index 6a8460e..0d63474 100755
--- a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesFederationProvider.java
+++ b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/BasePropertiesFederationProvider.java
@@ -32,10 +32,6 @@ public abstract class BasePropertiesFederationProvider implements UserFederation
this.properties = properties;
}
- public static Set<String> getSupportedCredentialTypes() {
- return supportedCredentialTypes;
- }
-
static
{
supportedCredentialTypes.add(UserCredentialModel.PASSWORD);
@@ -71,12 +67,22 @@ public abstract class BasePropertiesFederationProvider implements UserFederation
return null;
}
+ /**
+ * We only search for Usernames as that is all that is stored in the properties file. Not that if the user
+ * does exist in the properties file, we only import it if the user hasn't been imported already.
+ *
+ * @param attributes
+ * @param realm
+ * @param maxResults
+ * @return
+ */
@Override
public List<UserModel> searchByAttributes(Map<String, String> attributes, RealmModel realm, int maxResults) {
String username = attributes.get(USERNAME);
if (username != null) {
// make sure user isn't already in storage
if (session.userStorage().getUserByUsername(username, realm) == null) {
+ // user is not already imported, so let's import it until local storage.
UserModel user = getUserByUsername(realm, username);
if (user != null) {
List<UserModel> list = new ArrayList<UserModel>(1);
@@ -90,19 +96,32 @@ public abstract class BasePropertiesFederationProvider implements UserFederation
@Override
public void preRemove(RealmModel realm) {
-
+ // complete We don't care about the realm being removed
}
@Override
public void preRemove(RealmModel realm, RoleModel role) {
+ // complete we dont'care if a role is removed
}
+ /**
+ * See if the user is still in the properties file
+ *
+ * @param local
+ * @return
+ */
@Override
public boolean isValid(UserModel local) {
return properties.containsKey(local.getUsername());
}
+ /**
+ * hardcoded to only return PASSWORD
+ *
+ * @param user
+ * @return
+ */
@Override
public Set<String> getSupportedCredentialTypes(UserModel user) {
return supportedCredentialTypes;
diff --git a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/ClasspathPropertiesFederationProvider.java b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/ClasspathPropertiesFederationProvider.java
index 107f164..5f52fca 100755
--- a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/ClasspathPropertiesFederationProvider.java
+++ b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/ClasspathPropertiesFederationProvider.java
@@ -26,21 +26,43 @@ public class ClasspathPropertiesFederationProvider extends BasePropertiesFederat
super(session, model, properties);
}
+ /**
+ * Keycloak will call this method if it finds an imported UserModel. Here we proxy the UserModel with
+ * a Readonly proxy which will barf if password is updated.
+ *
+ * @param local
+ * @return
+ */
@Override
public UserModel proxy(UserModel local) {
return new ReadonlyUserModelProxy(local);
}
+ /**
+ * The properties file is readonly so don't suppport registration.
+ *
+ * @return
+ */
@Override
public boolean synchronizeRegistrations() {
return false;
}
+ /**
+ * The properties file is readonly so don't suppport registration.
+ *
+ * @return
+ */
@Override
public UserModel register(RealmModel realm, UserModel user) {
throw new IllegalStateException("Registration not supported");
}
+ /**
+ * The properties file is readonly so don't removing a user
+ *
+ * @return
+ */
@Override
public boolean removeUser(RealmModel realm, UserModel user) {
throw new IllegalStateException("Remove not supported");
diff --git a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/FilePropertiesFederationFactory.java b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/FilePropertiesFederationFactory.java
index 80906a3..5c64b73 100755
--- a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/FilePropertiesFederationFactory.java
+++ b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/FilePropertiesFederationFactory.java
@@ -27,8 +27,12 @@ public class FilePropertiesFederationFactory extends BasePropertiesFederationFac
}
-
-
+ /**
+ * Name of the provider. This will show up under the "Add Provider" select box on the Federation page in the
+ * admin console
+ *
+ * @return
+ */
@Override
public String getId() {
return "file-properties";
diff --git a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/FilePropertiesFederationProvider.java b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/FilePropertiesFederationProvider.java
index dbebfcc..725daee 100755
--- a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/FilePropertiesFederationProvider.java
+++ b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/FilePropertiesFederationProvider.java
@@ -28,11 +28,23 @@ public class FilePropertiesFederationProvider extends BasePropertiesFederationPr
super(session, model, properties);
}
+ /**
+ * Keycloak will call this method if it finds an imported UserModel. Here we proxy the UserModel with
+ * a Writable proxy which will synchronize updates to username and password back to the properties file
+ *
+ * @param local
+ * @return
+ */
@Override
public UserModel proxy(UserModel local) {
return new WritableUserModelProxy(local, this);
}
+ /**
+ * Adding new users is supported
+ *
+ * @return
+ */
@Override
public boolean synchronizeRegistrations() {
return true;
@@ -49,6 +61,13 @@ public class FilePropertiesFederationProvider extends BasePropertiesFederationPr
}
}
+ /**
+ * Update the properties file with the new user.
+ *
+ * @param realm
+ * @param user
+ * @return
+ */
@Override
public UserModel register(RealmModel realm, UserModel user) {
synchronized (properties) {
diff --git a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/ReadonlyUserModelProxy.java b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/ReadonlyUserModelProxy.java
index 0844c99..e63a9e9 100755
--- a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/ReadonlyUserModelProxy.java
+++ b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/ReadonlyUserModelProxy.java
@@ -6,6 +6,8 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.utils.UserModelDelegate;
/**
+ * Readonly proxy for a UserModel that prevents passwords from being updated.
+ *
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
diff --git a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/WritableUserModelProxy.java b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/WritableUserModelProxy.java
index 300f292..a2b4886 100755
--- a/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/WritableUserModelProxy.java
+++ b/examples/providers/federation-provider/src/main/java/org/keycloak/examples/federation/properties/WritableUserModelProxy.java
@@ -10,6 +10,8 @@ import java.io.IOException;
import java.util.Properties;
/**
+ * Proxy that will synchronize password updates to the properties file.
+ *
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
@@ -21,6 +23,13 @@ public class WritableUserModelProxy extends UserModelDelegate {
this.provider = provider;
}
+
+ /**
+ * Updates the properties file if the username changes. If you have a more complex user storage, you can
+ * override other methods on UserModel to synchronize updates back to your external storage.
+ *
+ * @param username
+ */
@Override
public void setUsername(String username) {
if (delegate.getUsername().equals(username)) return;
diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationProvider.java b/model/api/src/main/java/org/keycloak/models/UserFederationProvider.java
index d142464..9403a39 100755
--- a/model/api/src/main/java/org/keycloak/models/UserFederationProvider.java
+++ b/model/api/src/main/java/org/keycloak/models/UserFederationProvider.java
@@ -60,6 +60,14 @@ public interface UserFederationProvider extends Provider {
* @return
*/
boolean synchronizeRegistrations();
+
+ /**
+ * Called if this federation provider has priority and supports synchronized registrations.
+ *
+ * @param realm
+ * @param user
+ * @return
+ */
UserModel register(RealmModel realm, UserModel user);
boolean removeUser(RealmModel realm, UserModel user);
@@ -91,7 +99,19 @@ public interface UserFederationProvider extends Provider {
*/
List<UserModel> searchByAttributes(Map<String, String> attributes, RealmModel realm, int maxResults);
+ /**
+ * called whenever a Realm is removed
+ *
+ * @param realm
+ */
void preRemove(RealmModel realm);
+
+ /**
+ * called before a role is removed.
+ *
+ * @param realm
+ * @param role
+ */
void preRemove(RealmModel realm, RoleModel role);
/**
@@ -111,7 +131,8 @@ public interface UserFederationProvider extends Provider {
Set<String> getSupportedCredentialTypes(UserModel user);
/**
- * Validate credentials for this user.
+ * Validate credentials for this user. This method will only be called with credential parameters supported
+ * by this provider
*
* @param realm
* @param user
diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationProviderFactory.java b/model/api/src/main/java/org/keycloak/models/UserFederationProviderFactory.java
index 7b00987..047c2d2 100755
--- a/model/api/src/main/java/org/keycloak/models/UserFederationProviderFactory.java
+++ b/model/api/src/main/java/org/keycloak/models/UserFederationProviderFactory.java
@@ -10,6 +10,13 @@ import java.util.Set;
* @version $Revision: 1 $
*/
public interface UserFederationProviderFactory extends ProviderFactory<UserFederationProvider> {
+ /**
+ * called per Keycloak transaction.
+ *
+ * @param session
+ * @param model
+ * @return
+ */
UserFederationProvider getInstance(KeycloakSession session, UserFederationProviderModel model);
/**
@@ -18,4 +25,12 @@ public interface UserFederationProviderFactory extends ProviderFactory<UserFeder
* @return
*/
Set<String> getConfigurationOptions();
+
+ /**
+ * This is the name of the provider and will be showed in the admin console as an option.
+ *
+ * @return
+ */
+ @Override
+ String getId();
}
diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationProviderModel.java b/model/api/src/main/java/org/keycloak/models/UserFederationProviderModel.java
index 2542b90..3f6c451 100755
--- a/model/api/src/main/java/org/keycloak/models/UserFederationProviderModel.java
+++ b/model/api/src/main/java/org/keycloak/models/UserFederationProviderModel.java
@@ -4,6 +4,8 @@ import java.util.HashMap;
import java.util.Map;
/**
+ * Stored configuration of a User Federation provider instance.
+ *
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
* @author <a href="mailto:bburke@redhat.com">Bill Burke</a>
*/